Programming AT92SAM7 ARMs - An IntroductionA detailed description on how to start programming ARM microcontrollers featuring a minimalistic and easy-to-understand "hello world"-program targeting the highly-integrated, cheap and easily obtainable AT91SAM7S microcontrollers. IntroductionIn my opinion, devices like Atmel's AT91SAM7S64 are the perfect microcontrollers to get started with ARM development: Fairly small and cheap (an AT91SAM7S64 is below EUR 10 and has 64 pins), equiped with on-board RAM and flash memories, JTAG port with on-chip debugger and plenty of I/O options. Furthermore, all the software you need is available for free. Required SoftwareFirst, you need to obtain all the required software. Of course, you may use integrated development environments (IDEs) like Eclipse but a description on how to set this up is beyond the scope of this page. I will assume that you have a text editor and basic experience on how to write programs using the GNU tools (make, gcc). Furthermore, this page will demonstrate on how to do things under Linux although there is not much difference when using the GNU tools on other operating systems including Windows. So, basically all you need is the following:
Required HardwareAfter installing the software, you need the hardware toys to play with:
The Minimalistic Hello-World Program
There are several demo and example programs available for the AT91SAM7S
controllers but in contrast to all all the source code I have seen on
the net, the program presented here is as short as reasonable possible
(but not shorter). I deliberately try to avoid unnecessary overhead, and
keep things together in a few files to give better overview for the
beginner.
The source tarball above contains the following files:
The actual hello-world program consists of only two files: main.c and crt.s. crt.s is the low-level startup code written in assembler. It's task is to initialize static variables, set up the stacks and IRQ handlers and finally branch to the main() routine written in C language. (The assembler code is not explained here; please read the comments in the source tarball.) Below is the source code of main.c. Comments have been stripped down; refer to the source tarball for more details. Note the function Initialize() which does the main hardware initialization including switching on the main (crystal) oscillator. The PanicBlinker() function will never return and is used to diagnose errors like invaid memory accesses. It's called from crt.s. /* * main.c - AT91SAM7 hello world example program. * * Copyright (c) 2007 by Wolfgang Wieser ] wwieser (a) gmx <*> de [ */ #include "AT91SAM7.h" typedef signed char int8; typedef unsigned char uint8; #define nop() __asm__ __volatile__("nop") #define LED_A (1U<<0) /* PA0, pin 48 */ #define LED_B (1U<<1) /* PA1, pin 47 */ static void DefaultInterruptHandler(void) { /* Do nothing. */ } static void SpuriousInterruptHandler(void) { /* Never do anything; just return as quickly as possible. */ } // Endless loop of LED_A (PA0) blinks for error diagnosis. extern void PanicBlinker(uint8 code); void PanicBlinker(uint8 code) { volatile AT91PS_PIO pPIO = AT91C_BASE_PIOA; pPIO->PIO_PER |= LED_A; // Allow PIO to control LEDs. pPIO->PIO_OER |= LED_A; // Enable output. pPIO->PIO_SODR = LED_A; // Start with LED off. for(;;) { uint8 i; unsigned int j; for(i=0; i<code; i++) { pPIO->PIO_CODR = LED_A; // LOW = turn LED on. for(j=300000; j; j--) nop(); pPIO->PIO_SODR = LED_A; // HIGH = turn LED off. for(j=300000; j; j--) nop(); } for(j=300000*3; j; j--) nop(); // Wait some time... } } // Hardware initialization function. static void Initialize(void) { // Set Flash Wait sate: 0 wait states. AT91C_BASE_MC->MC_FMR = ((AT91C_MC_FMCN)&(22 <<16)) | AT91C_MC_FWS_0FWS; // Disable watchdog. AT91C_BASE_WDTC->WDTC_WDMR= AT91C_WDTC_WDDIS; // Start up the main oscillator. AT91PS_PMC pPMC = AT91C_BASE_PMC; pPMC->PMC_MOR = (( AT91C_CKGR_OSCOUNT & (6U <<8)) | AT91C_CKGR_MOSCEN ); while(!(pPMC->PMC_SR & AT91C_PMC_MOSCS)); // Select master clock (MCK): Main oscillator. pPMC->PMC_MCKR = AT91C_PMC_CSS_MAIN_CLK; while(!(pPMC->PMC_SR & AT91C_PMC_MCKRDY)); pPMC->PMC_MCKR = AT91C_PMC_CSS_MAIN_CLK | AT91C_PMC_PRES_CLK; while(!(pPMC->PMC_SR & AT91C_PMC_MCKRDY)); // Enable user reset. This aids in debugging. AT91C_BASE_RSTC->RSTC_RMR = 0xa5000400U | AT91C_RSTC_URSTEN; // Set up the default interrupt handlers. 0 = FIQ, 1 = SYS. int i; for(i=0; i<31; i++) { AT91C_BASE_AIC->AIC_SVR[i] = (unsigned)&DefaultInterruptHandler; } AT91C_BASE_AIC->AIC_SPU = (unsigned)&SpuriousInterruptHandler; // Set up the IOs. volatile AT91PS_PIO pPIO = AT91C_BASE_PIOA; pPIO->PIO_PER = LED_A | LED_B; // Allow PIO to control LEDs. pPIO->PIO_OER = LED_A | LED_B; // Enable outputs for LED pins. pPIO->PIO_SODR = LED_A | LED_B; // Set outputs HIGH to turn LEDs off. } int main(void) { Initialize(); // *(int*)0x800000=177; // <-- Causes data abort. // (*((void(*)(void))0x800000))(); // <-- Causes prefetch abort. // Toggle LEDs as fast as the processor can. // You need an oscilloscope to see this. volatile AT91PS_PIO pPIO = AT91C_BASE_PIOA; for(;;) { // Toggle... pPIO->PIO_CODR = LED_A; pPIO->PIO_SODR = LED_A; // Toggle again... pPIO->PIO_CODR = LED_A; pPIO->PIO_SODR = LED_A; // Wait a bit... int i; for(i=0; i<10; i++) nop(); } return(0); } As you can see, the main() routine first initializes the hardware and then enters an endless loop toggling the LED_A pin (PA0, pin 48). Since the program actvates the main oscillator, it requires a crystal oscillator attached to the microcontroller. Mine has 20MHz while most eval boards use a strange value somewhere above 18MHz. The PLL is not used and hence no second order filter connected to PLLRC is needed. Note that you will need an oscilloscope to see the LED_A pin toggling unless you introduce wait loops like in PanicBlinker(). Compiling the ProgramOf course, you can compile the program into a flat binary by simply calling make, but it's good to know the required steps:
The linker script ld_flash.cmd is vital in this step and has the following content: /* Identify the entry point (_vec_reset is defined in file crt.s). */ ENTRY(_vec_reset) /* Specify the memory areas for AT91SAM7S64: */ MEMORY { flash : ORIGIN = 0, LENGTH = 64K /* FLASH EPROM */ ram : ORIGIN = 0x00200000, LENGTH = 16K /* static RAM area */ } /* Define a global symbol _stack_end: */ _stack_end = 0x203FFC; /* AT91SAM7S64 */ /* Now define the output sections: */ SECTIONS { . = 0; /* set location counter to address zero */ .text : /* collect all sections that should go into FLASH after startup */ { /* after startup */ *(.text) /* all .text sections (code) */ *(.rodata) /* all .rodata sections (constants, strings, etc.) */ *(.rodata*) /* all .rodata* sections (constants, strings, etc.) */ *(.glue_7) /* all .glue_7 sections (no idea what these are) */ *(.glue_7t) /* all .glue_7t sections (no idea what these are) */ _etext = .; /* define a global symbol _etext just after the last code byte */ } >flash /* put all the above into FLASH */ .data : /* collect all initialized .data sections that go into RAM */ { _data = .; /* create a global symbol marking the start of the .data section */ *(.data) /* all .data sections */ _edata = .; /* define a global symbol marking the end of the .data section */ } >ram AT >flash /* put all the above into RAM (but load the LMA initializer */ /* copy into FLASH) */ .bss : /* collect all uninitialized .bss sections that go into RAM */ { _bss_start = .; /* define a global symbol marking the start of the .bss section */ *(.bss) /* all .bss sections */ } >ram /* put all the above in RAM; will be cleared in the startup code */ . = ALIGN(4); /* advance location counter to the next 32-bit boundary */ _bss_end = . ; /* define a global symbol marking the end of the .bss section */ } _end = .; /* define a global symbol marking the end of application RAM */ NOTE: The minimalistic hello-world program as presented on this pages is meant to run natively on an AT91SAM7S64. For an AT91SAM7S256, all you have to do is the following:
Flashing and Running the ProgramWe're now ready to actually run things on the target hardware. In order to get the binary program onto the flash content, you can simply run make flash. This will call OpenOCD to download main.bin. But before you do so, make sure to configure OpenOCD correctly. Therefore, have a look at openocd.cfg: telnet_port 4444 gdb_port 3333 # Commands specific to USB-AtmelPrg. # You need to change this if you use a different JTAG adapter. interface usbatmelprg jtag_speed 0 jtag_nsrst_delay 200 jtag_ntrst_delay 200 reset_config srst_only srst_pulls_trst # Target is an AT91SAM7: jtag_device 4 0x1 0xf 0xe target arm7tdmi little run_and_halt 0 arm7tdmi run_and_halt_time 0 30 flash bank at91sam7 0 0 0 0 0 daemon_startup reset Basically, the only thing you will change is the JTAG adapter definition. The above one makes use of by self-developed USB-AtmelPrg cable but if you e.g. use the Amontec JTAGKey, you may want to replace the JTAG section with the following content: # Commands specific to Amontec JTAGKey. interface ft2232 ft2232_device_desc "Amontec JTAGkey A" ft2232_layout jtagkey ft2232_vid_pid 0x0403 0xcff8 jtag_speed 2 jtag_nsrst_delay 200 jtag_ntrst_delay 200 Next, it's a good idea to quickly test the JTAG connection by running OpenOCD: Open two terminals. In the first one, you execute openocd -f openocd.cfg (left column below) while in the second one you open a telnet connection telnet localhost 4444 (right column; Windows users launch telnet from the start menu and connect to port 4444 on localhost which has address 127.0.0.1). Transcripts are shown below:
As you can see, the telnet session allows you to enter OpenOCD commands. For a full list of commands, use the help command or refer to the OpenOCD documentation. It's just important that OpenOCD reports a valid target state and cpsr value; if you see this, then JTAG communication is probably working correctly. So, we can now finally download and run the program. Just copy your changes of the JTAG definition in openocd.cfg to the similar file openocd_flash.cfg. (Note: There is one little hidden difference between these two files: The target command in openocd.cfg uses run_and_halt while it is run_and_init in openocd_flash.cfg.) make flash will do very much the same as the telnet session above but instead of using telnet, OpenOCD takes the commands from an extra file called openocd_doflash: # AT91SAM7Sxx flash programming script. # Written by Wolfgang Wieser 07/2007. wait_halt armv4_5 core_state arm mww 0xffffff60 0x003c0100 # MC_FMR: flash mode (FWS=1,FMCN=60, should be safe) mww 0xfffffd44 0x00008000 # disable watchdog mww 0xfffffc20 0x00000601 # CKGR_MOR : enable the main oscillator wait 100 # wait a little for osc to stabilize (100ms) mww 0xfffffc30 0x00000001 # PMC_MCKR : MCK = MAIN_CLK (master clock = main) wait 100 arm7_9 fast_memory_access enable # for clocks >32kHz flash write 0 main.bin 0x0 # Write the flash. wait 10 dump_image flash.bin 0x00100000 2048 # Read back first 2kb for verification. mww 0xfffffd08 0xa5000401 # enable user reset reset shutdown The first mww (memory write word) commands enable the main oscillator and make the ARM core run from the main clock instead of the slow clock. This speeds up programming as it allows to use the fast_memory_access option. After writing main.bin into the flash, the first 2kb are read back and put into a file called flash.bin. make will compare this file to the original main.bin to verify if programming succeeded (at least for the first 2kb). The last lines from the make flash output should hence look like this: cmp main.bin flash.bin cmp: EOF on main.bin make: [flash] Error 1 (ignored) Note: You need to manually check what the cmp command reports back. The "Error 1" is okay as long as it is due to "EOF on main.bin" or "EOF on flash.bin". But if cmp reports "the two files differ", then programming did not succeed. Done! You should now be able to see the a signal on PA0 (pin 48) with an oscilloscope. If a LED is connected, it should emit light but at a very dim rate (low duty cycle) unless you introduced delay loops in main.c. Some remarks: Erasing the flash ala flash erase 0 0 15 is not necessary since the flash write command includes an auto-erase cycle. Enabling the user reset (mww 0xfffffd08 0xa5000401) is necessary for the reset command to work properly because by default the AT91SAM7 won't pay attention to the NRST pin. Debugging the ProgramUsing OpenOCD, it is possible to debug (halt, single-step, etc.) our program on the target ARM chip! This is done by opening two terminals again. In the first one, you launch openocd -f openocd.cfg as you did above while testing the JTAG connection. (Alternatively, you can call make debug.) In the second terminal, you run the GNU debugger arm-elf-gdb main.elf and attach it to OpenOCD by issuing target remote :3333 inside GDB. You can now debug the program much like you normally do with GDB. But there is one pitfall: Breakpoints! Breakpoints normally require to replace the instruction at the breakpoint with a special break instruction so that the debugger can detect when the program flow arrives at the breakpoint. However, this is not possible in flash, so you are limited to 2 special hardware breakpoints built into the ARM's on-chip debugger. In order to enable these issue the following GDB command before using any breakpoints: monitor arm7_9 force_hw_bkpts enable Note that you can directly call OpenOCD commands from within GDB by prefixing them with monitor. For example, monitor reset will reset the microcontroller by shortly pulling NRST LOW. Here's a sample GDB session: bash# arm-elf-gdb main.elf GNU gdb 6.4 Copyright 2005 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "--host=i686-pc-linux-gnu --target=arm-elf"... (gdb) target remote :3333 Remote debugging using :3333 main () at main.c:166 166 for(i=0; i<10; i++) nop(); (gdb) monitor arm7_9 force_hw_bkpts enable force hardware breakpoints enabled (gdb) print i $1 = 7 (gdb) bt #0 main () at main.c:166 (gdb) info regi r0 0xfffff400 -3072 [...] r12 0x1 1 sp 0x203fe4 2113508 lr 0x22c 556 pc 0x2e0 736 fps 0x0 0 cpsr 0x800000d7 -2147483433 (gdb) cont Continuing. [^C pressed] Program received signal SIGINT, Interrupt. 0x000002e8 in main () at main.c:166 166 for(i=0; i<10; i++) nop(); (gdb) kill Kill the program being debugged? (y or n) y Resources, LinksMost of the content of this page was based on the excellent (Windows-) tutorial "Using Open Source Tools for AT91SAM7S Cross Development" by James P. Lynch, which can be found on the Atmel site under application notes/development tools. A short and good article (althouh in German) is AT91SAM7S mit OpenOCD programmieren.
|