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.
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.
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/).
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/.
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.
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
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).
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.
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
$ openocd -f openocdstm32.cfg
$ telnet localhost 4444
> help run above to see all OpenOCD commands > halt > poll > stm32x unlock I'm not sure above is needed > flash erase_sector 0 0 0 erase sector 0 (where we will load our new program) > flash info 0 > flash erase_check 0 this shows we erased sector 0 successfully > flash write_bank 0 led-stm32.bin 0 > mdw 0 7 above displays the 7 vector slots as 32-bit words, note all slots except the first have the least significant bit set > mdb 0 28 above displays the 7 vector slots as 28 bytes (note, little endian) > cortex_m3 disassemble 0x1c 0x20 above disassembles the program starting at the label "_start" > reset init > resume
You need several other files, whose contents are listed below. See the next section for the download bundle.
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) } }
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
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.
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)
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
The file http://pygmy.utoh.org/riscy/cortex/led-stm32.zip contains
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".
Please email me with any comments or corrections or questions.
Date: 2011-01-16 Sun
HTML generated by org-mode 6.21b in emacs 23