Arm Cortex-M Introduction

This section will describe some concepts of the Arm Cortex-M architecture using example code running on the QEMU virtual machine emulator.

QEMU can emulate many types of machine, for the following example we shall use the Stellaris LM3S6965 evaluation board from Texas Instruments.

The datasheet (containing the programmer’s model)  can be found here, other examples running on real hardware can be found in the STM32 section which demonstrate examples running on a specific board and covers topics such as JTAG programming/debugging and GPIO control.

To install the Arm version of QEMU, on Linux Mint, use the following command:

sudo apt-get install qemu-system-arm

Now we need the arm-none-eabi binutils and gcc toolchain:

sudo binutils-arm-none-eabi
sudo apt-get install gcc-arm-none-eabi
sudo apt-get install gdb-arm-none-eabi

And also newlib for header files (stdint.h):

sudo apt-get install libnewlib-arm-none-eabi

This example demonstrates some simple startup code, in an appropriate directory create the following Makefile:

OBJ := vector_table.o main.o
CFLAGS := -nostdlib -g -mcpu=cortex-m3 -mthumb
LDFLAGS := -g -o
ASMFLAGS := -g -o

TTY := $(shell tty)
MACH := lm3s6965evb
CDEV := tty,id=any,path=$(TTY)

QEMU_FLAGS := -S -gdb tcp::1234 -nographic -kernel

all: $(TARGET)

%.o: %.s
    $(CROSS)as $(ASMFLAGS) $@ $<

%.o: %.c
    $(CROSS)gcc $(CFLAGS) -c $<

    $(CROSS)ld $(LDFLAGS) $@ -T $(LDSCRIPT) $(OBJ)

    $(RM) *.o *.elf

run: $(TARGET)
    qemu-system-arm -machine $(MACH) $(QEMU_FLAGS) $< -chardev $(CDEV)    

debug: $(TARGET)
    $(CROSS)gdb -x gdbinit

Then create an initial vector table file in assembler called vector_table.s with the following contents:

.syntax unified

.cpu cortex-m3

/* Vector Table */

.word 0x20001000        /* Stack Pointer */
.word reset_handler     /* 1 Reset Handler */
.word default_handler   /* 2 NMI */
.word default_handler   /* 3 Hard Fault */
.word default_handler   /* 4 Mem Management Fault */
.word default_handler   /* 5 Bus Fault */
.word usage_handler     /* 6 Usage Fault */
.word default_handler   /* 7 RESERVED */
.word default_handler   /* 8 RESERVED */
.word default_handler   /* 9 RESERVED*/
.word default_handler   /* 10 RESERVED */
.word default_handler   /* 11 SVCall */
.word default_handler   /* 12 Debug Monitor */
.word default_handler   /* 13 RESERVED */
.word default_handler   /* 14 PendSV */
.word default_handler   /* 15 SysTick */

.global reset_handler
.type reset_handler, STT_FUNC
        bl main
        bl halt

.global default_handler
.type default_handler, STT_FUNC
        bl default_handler

.global usage_handler
.type usage_handler, STT_FUNC
        bl usage_handler

Then create a linker script called startup.ld with the following contents:

    mem : ORIGIN = 0x00000000, LENGTH = 0x4000

    .text : { *(.text*) } > mem

and finally a C file called main.c with the following contents:

int main() 
    int i = 0x1234abcd;
    return 0;    

Now build and run the startup code in QEMU:

make run
arm-none-eabi-as -g -o vector_table.o vector_table.s
arm-none-eabi-gcc -nostdlib -g -mcpu=cortex-m3 -mthumb -c main.c
arm-none-eabi-ld -g -o startup.elf -T startup.ld vector_table.o main.o
qemu-system-arm -machine lm3s6965evb -gdb tcp::1234 -nographic -kernel startup.elf -chardev tty,id=any,path=/dev/pts/1

In another terminal window create (in the same directory as the other files) a file called gdbinit containing the following:

target remote localhost:1234
load startup.elf
file startup.elf
break reset_handler
info reg

and then run the make debug target as follows:

make debug
arm-none-eabi-gdb -x gdbinit
GNU gdb (
For help, type "help".
Type "apropos word" to search for commands related to "word".
0x00000044 in ?? ()
Loading section .text, size 0x70 lma 0x0
Start address 0x0, load size 112
Transfer rate: 896 bits in <1 sec, 112 bytes/write.
Breakpoint 1 at 0x40: file vector_table.s, line 29.

Breakpoint 1, reset_handler () at vector_table.s:29
29        bl main
r0             0x0    0
r1             0x0    0
r2             0x0    0
r3             0x0    0
r4             0x0    0
r5             0x0    0
r6             0x0    0
r7             0x0    0
r8             0x0    0
r9             0x0    0
r10            0x0    0
r11            0x0    0
r12            0x0    0
sp             0x20001000    0x20001000
lr             0x49    73
pc             0x40    0x40 <reset_handler>
cpsr           0x40000173    1073742195

Set a breakpoint on main(), and continue execution:

(gdb) b main
Breakpoint 2 at 0x56: file main.c, line 3.
(gdb) cont
Breakpoint 2, main () at main.c:3
3        int i = 0x1234abcd;

Now step a line of code and then dump the registers:

(gdb) step
4        return 0;    
(gdb) info reg
r0             0x0    0
r1             0x0    0
r2             0x0    0
r3             0x1234abcd    305441741
r4             0x0    0
r5             0x0    0
r6             0x0    0
r7             0x20000ff0    536874992
r8             0x0    0
r9             0x0    0
r10            0x0    0
r11            0x0    0
r12            0x0    0
sp             0x20000ff0    0x20000ff0
lr             0x45    69
pc             0x60    0x60 <main+16>
cpsr           0x40000173    1073742195

From the registers we can see that the line from main.c:

int i = 0x1234abcd;

Has set register r3 to:

r3             0x1234abcd

This demonstrates we have a working QEMU setup and basic startup code.

You may notice that this code is very similar from the startup code from STM32-P405 Startup Code – In Detail and it is worth reading that example for some more detail about how the code works. As this example is a different board and CPU (M3 versus M4F) you will notice some changes to the CPU directives in the Makefile and vector_table.s files. And also the memory locations given in the linker script (startup.ld)