ARM Cortex M3 Assembly Language Example

Table of Contents

1 Abstract

This note describes an LED blinking program for the Olimex STM32-P103 ARM Cortex M3 board written in assembly language.

It shows the commands for assembling and linking with the GNU assembler and linker and also the commands for burning the program into the board's flash memory via a JTAG connector and the OpenOCD software.

The binary file is also available in case you would like to jump right to burning it into flash.

2 The hardware

The Olimex STM32-P103 ARM Cortex M3 board is available from Olimex (http://olimex.com/dev/stm32-p103.html) or SparkFun (http://www.sparkfun.com/products/8560).

The MCU is an ST Micros STM32F103RBT6 with 128KB flash and 20KB RAM (and lots of IO, timers, serial ports, etc.).

The board connects bit 12 of Port C to the LED (labeled "STAT" on the board). When bit 12 of Port C is low, the LED lights up.

3 The software tools

The software described here, needed to preprocess, assemble, link, and burn the demo program into the ARM Cortex board, runs on Linux. If you do not have Linux available, it can be as simple as temporarily booting an Ubuntu live CD (see details at http://pygmy.utoh.org/riscy/).

3.1 The assembler and linker

We use (parts of) the GNU toolchain, running on Linux (such as Ubuntu 10.04). In particular we need the GNU "binutils" package, which includes the ARM assembler and linker and some utilities to manipulate the output files. We do not need the C or C++ compiler.

This is a cross assembler. It runs on a PC (such as a typical desktop or laptop with an Intel CPU) but produces code for an ARM CPU.

If you don't have GNU binutils for the ARM installed, you can download my tools bundle at http://pygmy.utoh.org/riscy/.

3.2 The preprocessor

The GNU ARM assembler prefers to use the at-sign for comments. I prefer to use the semicolon for comments. So, I write the assembly language program using semicolons for comments then run the program through a preprocessor that converts semicolons to at-signs before feeding the program to the ARM assembler.

The preprocessor program is named preasm.tcl and is included in the tools bundle mentioned in the previous section. If you prefer, you can manually convert the semicolons to at-signs.

4 The assembly language program

4.1 Source code listing

Here is the program source code (from the file named led-stm32.asm).

;;; led-stm32.asm
;;; written by Frank Sergeant
;;;    frank@pygmy.utoh.org
;;;    http://pygmy.utoh.org/riscy
;;; This program is in the public domain.  See http://pygmy.utoh.org/riscy/cortex/ 
;;; for notes about the program and how to assemble, link, and burn to flash.

;;; Blink the LED on the Olimex STM32-P103 ARM Cortex M3 board.

;;; Directives
        .thumb                  ; (same as saying '.code 16')
        .syntax unified

;;; Equates
        .equ GPIOC_CRH,   0x40011004
        .equ GPIOC_ODR,   0x4001100C
        .equ RCC_APB2ENR, 0x40021018
        .equ STACKINIT,   0x20005000
        
        .equ LEDDELAY,    800000
        
.section .text
        .org 0

;;; Vectors
vectors:        
        .word STACKINIT         ; stack pointer value when stack is empty
        .word _start + 1        ; reset vector (manually adjust to odd for thumb)
        .word _nmi_handler + 1  ; 
        .word _hard_fault  + 1  ; 
        .word _memory_fault + 1 ; 
        .word _bus_fault + 1    ; 
        .word _usage_fault + 1  ; 

_start:

        ;; Enable the Port C peripheral clock by setting bit 4
        ldr r6, = RCC_APB2ENR
        mov r0, 0x10
        str r0, [r6]
        
        ;; Set the config and mode bits for Port C bit 12 so it will
        ;; be a push-pull output (up to 50 MHz) by setting bits 19-16
        ;; to '0011'.

        ldr r6, = GPIOC_CRH
        ldr r0, = 0x44434444
        str r0, [r6]

        ;; Load R2 and R3 with the "on" and "off" constants
        mov r2, 0              ; value to turn on LED
        mov r3, 0x1000         ; value to turn off LED

        ldr r6, = GPIOC_ODR    ;  point to Port C output data register
        
loop:
        str r2, [r6]           ; clear Port C, pin 12, turning on LED
        ldr r1, = LEDDELAY
delay1:
        subs r1, 1
        bne delay1
        
        str r3, [r6]           ; set Port C, pin 12, turning off LED
        ldr r1, = LEDDELAY
delay2:
        subs r1, 1
        bne delay2

        b loop                 ; continue forever

_dummy:                        ; if any int gets triggered, just hang in a loop
_nmi_handler:
_hard_fault:
_memory_fault:
_bus_fault:
_usage_fault:
        add r0, 1
        add r1, 1
        b _dummy                                                                                          

4.2 Notes about the source code

The goal for this program (other than blinking the LED) is to start with a very simple program. This lets us check out our toolchain, our flash utility, our JTAG connector and software, and our mental model of how it all works without getting bogged down in unnecessary complexities. For example, we do not use subroutines. We only set up the hardware stack because the ARM Cortex loads the stack pointer automatically. We hard-code the output port configuration register (rather than setting just the bits we need for the LED pin). We do not alter the clock source or speed (we use the default internal 8 MHz clock). We just want something to run now !

The STM32 MCU uses the "Thumb-2" instruction set, not the "ARM" instruction set. Even though the .thumb directive is equivalent to the .code 16 directive, the processor is still a 32-bit processor.

We define some symbolic constants, including the initial stack value (STACKINIT) and the LED delay value (LEDDELAY). If, after getting the program to run, you would prefer to make the LED blink faster or slower, then change the value of LEDDELAY.

The program starts at address zero (0x0000 0000) with the vector table. Each vector table slot is 4 bytes long. The first entry is the initial value for the stack pointer. We use 0x2000 5000 so the hardware stack will grow downward from the very top of on-board RAM.

The second slot in the vector table holds the address where program execution will begin. In this case, it is at the label _start. Actually, we adjust the address slightly in the vector table by adding 1 to it to make it odd. This is required because, in Thumb mode, a value loaded into the program counter must have its least significant bit set. I suppose there is a way to avoid doing this by putting the vectors is a special section and giving the linker certain command options.

We fill in several more slots in the vector table for various exceptions. They all point to a loop at the end of the program. We do not expect any of these exceptions to be triggered, but if they are, the program will just hang in a loop, incrementing two registers (so we will have something to look at if we are tracing the program in a debugger).

On this MCU, the IO ports do not work until you enable their clocks. We need to use Port C so we enable its peripheral clock.

The rest of the logic should be straightforward. We alternately write a "1" bit and then a "0" bit to the output data register for bit 12 of Port C (to turn the LED off and on), killing time so the LED flashes at exactly the speed we like best (adjust LEDDELAY to your personal taste).

5 Assembling and linking

Assuming you have the assembler and linker and other tools installed where I install them, here are the commands to preprocess, assemble, link, and build a binary:

$ /usr/local/bin/preasm.tcl led-stm32.asm led-stm32.s 
$ /usr/local/arm/bin/arm-elf-as -mcpu=cortex-m3 -mthumb -mapcs-32 -gstabs -ahls=led-stm32.lst -o led-stm32.o led-stm32.s
$ /usr/local/arm/bin/arm-elf-ld -v -T stm32.ld -nostartfiles -o led-stm32.elf led-stm32.o
$ /usr/local/arm/bin/arm-elf-objcopy -O binary led-stm32.elf  led-stm32.bin

The goal is to produce the file led-stm32.bin to burn into the MCU's flash memory.

6 Burning the flash with a JTAG connector

You can use any method you prefer to burn led-stm32.bin into the flash. Here is how I do it.

You can burn led-stm32.bin into the target board's flash with OpenOCD and the Olimex ARM USB Tiny JTAG adaptor (http://olimex.com/dev/arm-usb-tiny.html) this way

7 Other files

You need several other files, whose contents are listed below. See the next section for the download bundle.

7.1 The link file

stm32.ld

/* Simple linker script for the STM32 ARM Cortex M3.  Link the text 
   of the program into on-board flash and use on-board RAM for data and stack. 
*/
   
SECTIONS
{
        /* interrupt vectors start at zero */
        . = 0x0;  /* start of flash */

        .text :  {  *(.text)   }

        /* constant data follows code but still in flash */
        .data :
        { 
          *(.data) 
          *(.rom)
        }

        /* internal RAM starts at 0x20000000 */
        . = 0x20000000; 
        .ram : { *(.ram) }

        .bss :
        {
          *(.bss)
          *(.ram)
        }
}                                                                                          

7.2 The OpenOCD configuration file

openocdstm32.cfg

# This file is for use with the Olimex STM32-P103 board.  
# It is named openocdstm32.cfg.  Run it this way:
#    $ openocd -f openocdstm32.cfg

# This is the JTAG connector I use
source [find interface/olimex-jtag-tiny.cfg]

# This is close enough to the board I use
source [find board/olimex_stm32_h103.cfg]

# tell gdb our flash memory map and enable flash programming
gdb_memory_map enable
gdb_flash_program enable

7.3 Bonus section

Although this note does not go into the details of using the GNU debugger (GDB) with this board and JTAG, here are the listings for my GDB configuration files.

7.3.1 Common GDB settings go in the home directory

This file is named .gdbinit. It goes in my home directory. It is always loaded first by GDB. It contains only settings that are common to all my projects.

# Note, start this within Emacs as
#  M-x arm-elf-gdb --annotate=3

# This .gdbinit file is in the home directory.  It is always read
# first.  Here we put only common settings.  We also use a .gdbinit
# file in each working directory which is specific to the particular
# project.

set complaints 1

set output-radix 0x10
set input-radix 0x10

set endian little

dir .
set prompt (arm-gdb) 

7.3.2 Project-specific GDB settings go in the project subdirectory

This file is also named .gdbinit. GDB loads this after loading the one in the home directory. A separate .gdbinit goes in each project subdirectory. For this LED example, it goes in ~/riscy/cortex/, e.g., /home/frank/riscy/cortex/.gdbinit.

# Note, start this within Emacs as
#  M-x arm-elf-gdb --annotate=3

# This .gdbinit file is in the working directory for the STM32 ARM
# Cortex work.  It handles settings specific to this project and is
# read by GDB after reading the .gdbinit in the home directory (which
# sets common options such as the radix).

cd ~/riscy/cortex
file ~/riscy/cortex/led-stm32.elf

dir .
set prompt (cortex-gdb) 

# connect to openOCD running on gdb port 3333
target remote localhost:3333

# Set a breakpoint
b _start

8 Download

The file http://pygmy.utoh.org/riscy/cortex/led-stm32.zip contains

led-stm32.html
this file
openocdstm32.cfg
the JTAG/OpenOCD configuration file
stm32.ld
the linker file
led-stm32.asm
the LED blinking program source code
led-stm32.s
the preprocessed LED blinking program source code
led-stm32.lst
the listing file produced by the assembler
led-stm32.bin
the binary ready to burn into the MCU's flash

Also, the GNU binutils compiled on 32-bit Ubuntu 10.04 to cross assemble for the ARM (and ARM Cortex) along with the preprocessor are available at http://pygmy.utoh.org/riscy/ – look for "The Bundle of Tools".

9 Conclusion

Please email me with any comments or corrections or questions.

Author: Frank Sergeant <frank@pygmy.utoh.org>

Date: 2011-01-16 Sun

HTML generated by org-mode 6.21b in emacs 23