Nav:  [home][elec][pdev][arm] > [at91sam7]
 
← [What are these? Why?]

Programming AT92SAM7 ARMs - An Introduction

A 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.

Introduction

In 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 Software

First, 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:

  • ARM-GCC: The "GNU toolchain" containing compiler, assembler and linker for cross-compling ARM programs. After installation the programs are called arm-elf-gcc, arm-elf-as and arm-elf-ld, respectively.
    Linux and Mac OS X users can find installation instructions and pre-built binaries from a microcontroller.net article. Windows users will find an excellent package bundle on YAGARTO.
  • OpenOCD: The open on-chip debugger. I recommend checking out the SVN repository and compiling the sources; see the open facts page for a detailed description on how to do this. For Windows users, OpenOCD is available at YAGARTO (see above).
  • Usual utilities like GNU make, a shell, a text editor, etc. Linux and Mac OS X users will find them pre-installed on their systems while Windows users get everything they need bundeled with YAGARTO (see above).

Required Hardware

After installing the software, you need the hardware toys to play with:

Photo of hardware setup [17kb]
[click to enlarge: 218kb JPEG]

Circuit diagram of minimalistic AT91SAM7S board [6kb]
[click to enlarge: 21kb PNG]

  • JTAG cable: A JTAG cable to connect your ARM microcontroller to the PC. There are plenty of devices but be sure to choose one which is supported by OpenOCD. I won't give a recommendation here; I'm personally using my self-developed USB-AtmelPrg interface cable which includes OpenOCD support.
    The cheapest interface cable is probably the so-called "wiggler" attaching to the parallel (printer) port and also supported by OpenOCD. You can easily build the wiggler yourself by searching the schematic on the web, however be warned that people have reported instabilities and failures with this device. A lot of people seem to be using the Amontec JTAGKey-Tiny: Quite cheap (about USD 40), with USB interface and OpenOCD support.
  • AT91SAM7S64 board: This is the actual ARM microcontroller we'll be running the hello-world program on. You can buy an evaluation board like Atmel's AT91SAM7S-EK from digi-key for about USD 150 or one of the similar third-party clones at half the price if you are lucky. Personally, I'm using a minimalistic self-developed test/breakout board which just contains the basic "life-support" components required to run an AT91SAM7S64 or an AT91SAM7S256. (Germans can buy the ARM microcontroller alone e.g. from Reichelt for about EUR 10.) There is a 20MHz crystal and some capacitors as well as pull-up resistors, that's all.

Images on the left: The upper image shows the minimalistic hardware setup: The AT91SAM7S board is on the right bottom and its circuit diagram is shown below. There is also a PDF version of the circuit diagram. (You cannot see the crystal and most of the caps on the photo because they are mounted on the bottom side of the board.)

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.
Looking for a comprehensible "hello-world" program for ARM/AT91SAM7S? Here it is!

Source: minimalistic-hello-world-1.0.tar.gz   [61kb gzipped source tarball]
Version:1.0   (2007-07-19)
Author:Wolfgang Wieser   (report bugs here)
License:GNU GPL (Version 2)
Requires:ARM-GCC

The source tarball above contains the following files:

AT91SAM7S64.h File from Atmel; contains lots of hardware definitions for the AT91SAM7S64.
AT91SAM7S256.h Dito for the AT91SAM7S256.
AT91SAM7.h Symlink to either of the files above. (Windows users make a copy instead of symlinking.)
main.c Main hello-world program written in C.
crt.s Low-level startup code written in assembler language.
Makefile Usual file for GNU make.
ld_flash.cmd Linker command script (details below).
openocd-flash.cfg   OpenOCD config file which will download the program binary onto the AT91SAM7S.
openocd.cfg The same config file without instructions for flashing.
openocd_doflash OpenOCD target script which contains the actual flashing commands.

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 Program

Of course, you can compile the program into a flat binary by simply calling make, but it's good to know the required steps:

  • First, crt.s is assembled into an object crt.o and main.c is compiled into main.o:

    arm-elf-as -mapcs-32 crt.s -o crt.o
    arm-elf-gcc -I. -c -fno-common -O2 -g main.c
    

    (-fno-common seems not really necessary but protects you from certain possible errors; check the GCC docu when interested.)

  • The object files need to be linked. Note that we pass -nostartfiles since we have our own startup code in crt.o and be sure to specify crt.o first among all objects and libraries! The memory locations and sizes for stack and heap are defined in the linker scipt ld_flash.cmd:

    arm-elf-ld -T ld_flash.cmd -nostartfiles -o main.elf crt.o main.o

    We now have the final binary main.elf in ELF format. It includes debugging information since the program was compiled with option -g.

  • The content to be put into the microcontroller's flash storage is the raw binary progam. This has to be extracted from the ELF file:

    arm-elf-objcopy --output-target=binary main.elf main.bin

    main.bin can now be written into the flash storage of the AT91SAM7S.

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:

  • Make the symlink AT91SAM7.h point to AT91SAM7S256.h instead of AT91SAM7S64.h. (Windows users copy/rename AT91SAM7S256.h to AT91SAM7.h.)

  • Change the memory and stack definitions in the linker script ld_flash.cmd since the AT91SAM7S256 has four times more flash and RAM:

    MEMORY
    {
        flash    : ORIGIN = 0,          LENGTH =256K      /* FLASH EPROM       */
        ram      : ORIGIN = 0x00200000, LENGTH = 64K      /* static RAM area   */
    }
    
    _stack_end = 0x20FFFC;      /* AT91SAM7S256 */
    

Flashing and Running the Program

We'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:

OpenOCD session Telnet session
bash# openocd -f openocd.cfg
Info:    ../../trunk/src/openocd.c:86 main(): Open
 On-Chip Debugger (2007-06-28 12:30 CEST)
Opened USB connection to 0403:6003:
  Hardware: USB-AtmelPrg, Rev. 2/2
Info:    ../../../trunk/src/jtag/usbatmelprg.cc:21
4 usbatmelprg_init():
Connected to 0403:6003 (USB-AtmelPrg, Rev. 2/2):
  Device: USB AtmelPrg Rev 2.3
  Vendor: Wolfgang WIESER, 07/2007
  Firmware version: 0x203
  Protocol version: 5

Warning: ../../../trunk/src/target/arm7_9_common.c
:685 arm7_9_assert_reset(): srst resets test logic
, too
Info:    ../../../trunk/src/server/server.c:67 add
_connection(): accepted 'telnet' connection from 0
Info:    ../../../trunk/src/server/server.c:367 se
rver_loop(): dropped 'telnet' connection
  
bash# telnet localhost 4444
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> poll
target state: halted
target halted in ARM state due to debug
 request, current mode: System
cpsr: 0x800000df pc: 0x000005e4
> resume
Target 0 resumed
> halt
requesting target halt...
> Target 0 halted
target halted in ARM state due to debug
 request, current mode: System
cpsr: 0x800000df pc: 0x000005e0
> shutdown
Connection closed by foreign host.

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 Program

Using 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, Links

Most 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.


[home] [site map]
Valid HTML 4.01!
Copyright © 2003-2007 by Wolfgang Wieser
Last modified: 2007-07-20 16:57:41