STM32 Primer - A minimal example

In this section I'll show how to get the STM32F103RB running with a minimal "hello world" example - a flashing LED. This example involves the following five files:

Technically, only the first two files are needed for a true "minimal" example; but the other three files greatly simplify the process of building and programming. First we look at the C code:

/*******************************
 * stm32 minimal example main.c
 *******************************/
 
/* memory and peripheral start addresses */
#define FLASH_BASE      0x08000000
#define SRAM_BASE       0x20000000
#define PERIPH_BASE     0x40000000

/* work out end of RAM address as initial stack pointer */
#define SRAM_SIZE       20*1024     // STM32F103RB has 20 Kbye of RAM
#define SRAM_END		(SRAM_BASE + SRAM_SIZE)

/* LED connected to PIN 8 of GPIOA */
#define LED_PIN      	8
#define OUTPUT_MODE		(0x10|0x03) // output mode: push-pull + 50MHz

/* RCC peripheral addresses applicable to GPIOA */
#define RCC_BASE        (PERIPH_BASE + 0x21000)
#define RCC_APB2ENR     (*(volatile unsigned long*)(RCC_BASE + 0x18))

/* GPIOA peripheral addresses */
#define GPIOA_BASE      (PERIPH_BASE + 0x10800)
#define GPIOA_CRL       (*(volatile unsigned long*)(GPIOA_BASE + 0x00))
#define GPIOA_CRH       (*(volatile unsigned long*)(GPIOA_BASE + 0x04))
#define GPIOA_BSRR      (*(volatile unsigned long*)(GPIOA_BASE + 0x10))
#define GPIOA_BRR       (*(volatile unsigned long*)(GPIOA_BASE + 0x14))

/* user functions */
int main(void);
void delay(unsigned long count);

/* vector table */
unsigned long *vector_table[] __attribute__((section(".vector_table"))) =
{
    (unsigned long *)SRAM_END,   // initial stack pointer 
    (unsigned long *)main        // main as Reset_Handler
};

int main()
{
    /* enable clock on GPIOA peripheral */
    RCC_APB2ENR = 0x04;
    
    /* set LED pin output mode */
    //GPIOA_CRL |= OUTPUT_MODE << ((LED_PIN) << 2); // if pins 0 to 7
    GPIOA_CRH |= OUTPUT_MODE << ((LED_PIN-8) << 2); // if pins 8 to 15

    while(1)
    {
        GPIOA_BSRR = 1<<LED_PIN;  // set LED pin high
        delay(200000);
        GPIOA_BRR  = 1<<LED_PIN;  // set LED pin low
        delay(200000);
    }
}

void delay(unsigned long count)
{
    while(count--);
}

The code is pretty much self-explanatory. Most of it are actually macros difining memory and peripheral addresses. ST supplies a library with all these done for us in header files, we'll use that in the next section in a moment. One important thing to note, though, is the vector table. The first two entries must be the top-of-stack address and the address of the reset handler. Next we look at the linker script:

/*****************************************
 * stm32 minimal example stm32_minimal.ld
 *****************************************/
 
/* memory layout for an STM32F103RB */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 128K
    SRAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 20K
}

/* output sections */
SECTIONS
{
    /* program code into FLASH */
    .text :
    {
        *(.vector_table)    /* Vector table */
        *(.text)            /* Program code */
    } >FLASH

    /* uninitialized global and static variables (which 
       we don't have any in this example) into SRAM */
    .data :
    {
        *(.data)            
    } >SRAM
}	

Again, here we see that the vector table needs to be place before any application code in Flash. Also note that, because there is no global or static variables in the application code, the '.data' section is actually not needed. Now we take a look at the Makefile:

##################################
# stm32 minimal example Makefile
##################################

CC = arm-none-eabi-gcc
LD = arm-none-eabi-ld
CP = arm-none-eabi-objcopy

LKR_SCRIPT = stm32_minimal.ld

CFLAGS  = -c -fno-common -O0 -g -mcpu=cortex-m3 -mthumb
LFLAGS  = -nostartfiles -T$(LKR_SCRIPT)
CPFLAGS = -Obinary

all: main.bin

main.o: main.c
	$(CC) $(CFLAGS) -o main.o main.c

main.elf: main.o
	$(LD) $(LFLAGS) -o main.elf main.o

main.bin: main.elf
	$(CP) $(CPFLAGS) main.elf main.bin

clean:
	rm -rf *.o *.elf *.bin

write: 
	./write_bin.sh openocd.cfg main.elf	

Noticed the command used to invoke the compiler - 'arm-none-eabi-gcc', the linker - 'arm-none-eabi-ld' and the object copy binary utility - 'arm-none-eabi-objcopy'? We installed these in the previous section. The last line './write_bin.sh openocd.cfg main.elf' runs a bash script that takes two arguments - an OpenOcd configuration file and a binary image to be programmed into the micro. We have seen the contents of 'openocd.cfg' in the previous section:

##############################################
# stm32 minimal example openocd.cfg
# config file for J-link used with stm32f1x
##############################################

source [find interface/jlink.cfg]
source [find target/stm32f1x.cfg]

And the bash script has the following contents:

#!/bin/bash

################################################################### 
# stm32 minimal example write_bin.sh
# openocd commands to program the micro, invoked in the Makefile
###################################################################
OPENOCD_CFG=$1
BIN_IMAGE=$2

killall -s 9 openocd
openocd -f ${OPENOCD_CFG} -c init -c "reset halt" -c "flash write_image erase ${BIN_IMAGE}" -c "verify_image ${BIN_IMAGE}" -c reset -c shutdown 

All it does is first kill any OpenOcd process already running; then start a new one to read the config file sent as the first argument, find and write the binary image as specified in the second argument into the micro with multiple commands. Like what's in the Makefile, we could've typed all those commands one by one in the shell but that would be much more time-consuming.

Now let's make the project:

me@pandafruits:~$ make
arm-none-eabi-gcc -c -fno-common -O0 -g -mcpu=cortex-m3 -mthumb -o main.o main.c
arm-none-eabi-ld -nostartfiles -Tstm32_minimal.ld -o main.elf main.o
arm-none-eabi-objcopy -Obinary main.elf main.bin
me@pandafruits:~$ 

There should be three more files in the directory: 'main.o', 'main.elf' and 'main.bin'. Let's JTAG the micro:

me@pandafruits:~$ make write
./write_bin.sh openocd.cfg main.elf
openocd: no process found
Open On-Chip Debugger 0.5.0 (2011-12-03-08:57)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.berlios.de/doc/doxygen/bugs.html
Warn : Adapter driver 'jlink' did not declare which transports it allows; assuming legacy JTAG-only
Info : only one transport option; autoselect 'jtag'
1000 kHz
adapter_nsrst_delay: 100
jtag_ntrst_delay: 100
cortex_m3 reset_config sysresetreq
Info : J-Link initialization started / target CPU reset initiated
Info : J-Link ARM V8 compiled Jan 12 2012 20:43:19
Info : J-Link caps 0xb9ff7bbf
Info : J-Link hw version 80000
Info : J-Link hw type J-Link
Info : J-Link max mem block 9440
Info : J-Link configuration
Info : USB-Address: 0x0
Info : Kickstart power on JTAG-pin 19: 0x0
Info : Vref = 3.313 TCK = 1 TDI = 0 TDO = 1 TMS = 0 SRST = 0 TRST = 0
Info : J-Link JTAG Interface ready
Info : clock speed 1000 kHz
Info : JTAG tap: stm32.cpu tap/device found: 0x3ba00477 (mfg: 0x23b, part: 0xba00, ver: 0x3)
Info : JTAG tap: stm32.bs tap/device found: 0x16410041 (mfg: 0x020, part: 0x6410, ver: 0x1)
Info : stm32.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : JTAG tap: stm32.cpu tap/device found: 0x3ba00477 (mfg: 0x23b, part: 0xba00, ver: 0x3)
Info : JTAG tap: stm32.bs tap/device found: 0x16410041 (mfg: 0x020, part: 0x6410, ver: 0x1)
target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000008 msp: 0x20005000
auto erase enabled
Info : device id = 0x20036410
Info : flash size = 128kbytes
wrote 1024 bytes from file main.elf in 0.316950s (3.155 KiB/s)
verified 148 bytes in 0.311988s (0.463 KiB/s)
Info : JTAG tap: stm32.cpu tap/device found: 0x3ba00477 (mfg: 0x23b, part: 0xba00, ver: 0x3)
Info : JTAG tap: stm32.bs tap/device found: 0x16410041 (mfg: 0x020, part: 0x6410, ver: 0x1)
shutdown command invoked

The LED should be flashing now. Finally let's see if we could debug with GDB. First, do:

me@pandafruits:~$ openocd
Open On-Chip Debugger 0.5.0 (2011-12-03-08:57)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.berlios.de/doc/doxygen/bugs.html
Warn : Adapter driver 'jlink' did not declare which transports it allows; assuming legacy JTAG-only
Info : only one transport option; autoselect 'jtag'
1000 kHz
adapter_nsrst_delay: 100
jtag_ntrst_delay: 100
cortex_m3 reset_config sysresetreq
Info : J-Link initialization started / target CPU reset initiated
Info : J-Link ARM V8 compiled Jan 12 2012 20:43:19
Info : J-Link caps 0xb9ff7bbf
Info : J-Link hw version 80000
Info : J-Link hw type J-Link
Info : J-Link max mem block 9440
Info : J-Link configuration
Info : USB-Address: 0x0
Info : Kickstart power on JTAG-pin 19: 0x0
Info : Vref = 3.313 TCK = 1 TDI = 0 TDO = 1 TMS = 0 SRST = 0 TRST = 0
Info : J-Link JTAG Interface ready
Info : clock speed 1000 kHz
Info : JTAG tap: stm32.cpu tap/device found: 0x3ba00477 (mfg: 0x23b, part: 0xba00, ver: 0x3)
Info : JTAG tap: stm32.bs tap/device found: 0x16410041 (mfg: 0x020, part: 0x6410, ver: 0x1)
Info : stm32.cpu: hardware has 6 breakpoints, 4 watchpoints

With the OpenOcd server running, open a new terminal and connect to the server using telnet by doing:

me@pandafruits:~$ telnet local host 4444
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
>

Enter 'reset halt' to halt the target:

> reset halt
JTAG tap: stm32.cpu tap/device found: 0x3ba00477 (mfg: 0x23b, part: 0xba00, ver: 0x3)
JTAG tap: stm32.bs tap/device found: 0x16410041 (mfg: 0x020, part: 0x6410, ver: 0x1)
target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000008 msp: 0x20005000
>

Now, open a third terminal and do:

me@pandafruits:~$ arm-none-eabi-gdbtui --eval-command="target remote localhost:3333" main.elf

You should see something similar to this:

Let's try setting a couple of breakpoints so that we can turn the LED on and off from GDB. In my case the two lines in the code that turns the LED on and off are line 51 and line 53. So at the GDB command prompt I do:

(gdb) break main.c:51
Breakpoint 1 at 0x8000032: file main.c, line 51
(gbd) break main.c:53
Breakpoint 2 at 0x800004c: file main.c, line 53
(gbd)

Note the addresses of these breakpoints are pretty close to the beginning of the Flash, which is expected for such a small program. I can now step between the two breakpoints to turn the LED on and off by typing 'c' (for continue) in GDB:

(gdb) c
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, main () at main.c:51
(gdb) c
Continuing.

Breakpoint 2, main () at main.c:53
(gdb) 

When done, type 'q' to quit GDB.

Great, in this section I've tried building, programming and debugging a STM32 micro with a minimal project. But there are some serious limitations. Among them are:

In the next section I'll show a more complete linker script, use a startup file and bring the STM32 peripheral library into the picture.