Electronics, Embedded Systems, and Software are my breakfast, lunch, and dinner.
May 22, 2016
About two years ago I started working with the Teensy 3.1 (which uses a Freescale Kinetis ARM-Cortex microcontroller) and I was super impressed with the ARM processor, both for its power and relative simplicity (it is not simple...its just relatively simple for the amount of power you get for the cost IMO). Almost all my projects before that point had consisted of AVRs and PICs (I'm in the AVR camp now), but now ARM-based microcontrollers had become serious contenders for something that I could go to instead. I soon began working on a small development board project also involving some Freescale Kinetis microcontrollers since those are what I have become the most familiar with. Sadly, I have had little success since I have been trying to make a programmer myself (the official one is a minimum of $200). During the course of this project I came across a LOT of STM32 stuff and it seemed that it was actually quite easy to set up. Lots of the projects used the STM32 Discovery and similar dev boards, which are a great tools and provide an easy introduction to ARM microcontrollers. However, my interest is more towards doing very bare metal development. Like soldering the chip to a board and hooking it up to a programmer. Who needs any of that dev board stuff? For some reason I just find doing embedded development without a development board absolutely fascinating. Some people might interpret doing things this way as a form of masochism. Perhaps I should start seeing a doctor...
Having seen how common the STM32 family was (both in dev boards and in commercial products) and noting that they were similarly priced to the Freescale Kinetis series, I looked in to exactly what I would need to program these, saw that the stuff was cheap, and bought it. After receiving my parts and soldering everything up, I plugged everything into my computer and had a program running on the STM32 in a matter of hours. Contrast that to a year spent trying to program a Kinetis KL26 with only partial success.
This post is a complete step-by-step tutorial on getting an STM32 microcontroller up and running without using a single dev board (breakout boards don't count as dev boards for the purposes of this tutorial). I'm writing this because I could not find a standalone tutorial for doing this with an ARM microcontroller and I ended up having to piece it together bit by bit with a lot of googling. My objective is to walk you through the process of purchasing the appropriate parts, writing a small program, and uploading it to the microcontroller.
I make the following assumptions:
All code, makefiles, and configuration stuff can be found in the project repository on github. Project Repository: `https://github.com/kcuzner/stm32f103c8-blink <https://github.com/kcuzner/stm32f103c8-blink>`__
You will require the following materials:
I was able to acquire all of these parts for less than $20. Now, I did have stuff like the capacitors, led, resistor, and wires lying around in parts boxes, but those are quite cheap anyway.
Side note: Here is an excellent video by the EE guru Dave Jones on surface mount soldering if the prospect is less than palatable to you: https://www.youtube.com/watch?v=b9FC9fAlfQE
Above we decided to use the STM32F103C8 ARM Cortex-M3 microcontroller in a TQFP-48 package. This microcontroller has so many peripherals its no wonder its the one all over eBay. I could see this microcontroller easily satisfying the requirements for all of my projects. Among other things it has:
All this for ~$1.20/part no less! Of course, its like $6 on digikey, but for my purposes having an eBay-sourced part is just fine.
Ok, so when messing with any microcontroller we need to look at its datasheet to know where to plug stuff in. For almost all ARM Microcontrollers there will be no less than 2 datasheet-like documents you will need: The part datasheet and the family reference manual . The datasheet contains information such as the specific pinouts and electrical characteristics and the family reference manual contains the detailed information on how the microcontroller works (core and peripherals). These are both extremely important and will be indispensable for doing anything at all with one of these microcontrollers bare metal.
Find the STM32F103C8 datasheet and family reference manual here (datasheet is at the top of the page, reference manual is at the bottom): http://www.st.com/en/microcontrollers/stm32f103c8.html. They are also found in the "ref" folder of the repository.
After getting the datasheet we need to solder the microcontroller down to the breakout board so that we can start working with it on a standard breadboard. If you prefer to go build your own PCB and all that (I usually do actually) then do that instead of this. However, you will still need to know which pins to hook up.
On the pin diagram posted here you will find the highlighted pins of interest for hooking this thing up. We need the following pins at a minimum:
Below you will find a picture of my breakout board. I soldered a couple extra pins since I want to experiment with USB.
Very important: You may notice that I have some little tiny capacitors (0.1uF) soldered between the power pins (the one on the top is the most visible in the picture). You need to mount your capacitors between each pair of VDD/VSS pins (including AVDD/AVSS) . How you do this is completely up to you, but it must be done and *they should be rather close to the microcontroller itself* . If you don't it is entirely possible that when the microcontroller first turns on and powers up (specifically at the first falling edge of the internal clock cycle), the inductance created by the flying power wires we have will create a voltage spike that will either cause a malfunction or damage. I've broken microcontrollers by forgetting the decoupling caps and I'm not eager to do it again.
Don't do this with the programmer plugged in.
On the right you will see my STLinkV2 clone which I will use for this project. Barely visible is the pinout. We will need the following pins connected from the programmer onto our breadboard. These come off the header on the non-USB end of the programmer. Pinouts may vary. Double check your programmer!
You may notice in the above picture that I have an IDC cable coming off my programmer rather than the dupont wires. I borrowed the cable from my AVR USBASP programmer since it was more available at the time rather than finding the dupont cables that came with the STLinkV2.
Next, we need to connect the following pins on the breadboard:
Here is my breadboard setup:
Project Repository: `https://github.com/kcuzner/stm32f103c8-blink <https://github.com/kcuzner/stm32f103c8-blink>`__
Since we are going to write a program, we need the headers. These are part of the STM32CubeF1 library found here.
Visit the page and download the STM32CubeF1 zip file. It will ask for an email address. If you really don't want to give them your email address, the necessary headers can be found in the project github repository.
Alternately, just clone the repository. You'll miss all the fun of poking around the zip file, but sometimes doing less work is better.
The STM32CubeF1 zip file contains several components which are designed to help people get started quickly when programming STM32s. This is one thing that ST definitely does better than Freescale. It was so difficult to find the headers for the Kinetis microcontrollers that almost gave up at that point. Anyway, inside the zip file we are only interested in the following:
I copied all the files referenced above to various places in my project structure so they could be compiled into the final program. Please visit the repository for the exact locations and such. My objective with this tutorial isn't really to talk too much about project structure, and so I think that's best left as an exercise for the reader.
We need to be able to compile the program and flash the resulting binary file to the microcontroller. In order to do this, we will require the following programs to be installed:
Once you have installed all of the above programs, you should be good to go for ARM development. As for an editor or IDE, I use vim. You can use whatever. It doesn't matter really.
Ok, so we need to write a program for this microcontroller. We are going to simply toggle on and off a GPIO pin (PB0). After reset, the processor uses the internal RC oscillator as its system clock and so it runs at a reasonable 8MHz or so I believe. There are a few steps that we need to go through in order to actually write to the GPIO, however:
Here is my super-simple main program that does all of the above:
1/**
2 * STM32F103C8 Blink Demonstration
3 *
4 * Kevin Cuzner
5 */
6
7#include "stm32f1xx.h"
8
9int main(void)
10{
11 //Step 1: Enable the clock to PORT B
12 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
13
14 //Step 2: Change PB0's mode to 0x3 (output) and cfg to 0x0 (push-pull)
15 GPIOB->CRL = GPIO_CRL_MODE0_0 | GPIO_CRL_MODE0_1;
16
17 while (1)
18 {
19 //Step 3: Set PB0 high
20 GPIOB->BSRR = GPIO_BSRR_BS0;
21 for (uint16_t i = 0; i != 0xffff; i++) { }
22 //Step 4: Reset PB0 low
23 GPIOB->BSRR = GPIO_BSRR_BR0;
24 for (uint16_t i = 0; i != 0xffff; i++) { }
25 }
26
27 return 0;
28}
If we turn to our trusty family reference manual, we will see that the clock gating functionality is located in the Reset and Clock Control (RCC) module (section 7 of the manual). The gates to the various peripherals are sorted by the exact data bus they are connected to and have appropriately named registers. The PORTB module is located on the APB2 bus, and so we use the RCC->APB2ENR to turn on the clock for port B (section 7.3.7 of the manual).
The GPIO block is documented in section 9. We first talk to the low control register (CRL) which controls pins 0-7 of the 16-pin port. There are 4 bits per pin which describe the configuration grouped in to two 2-bit (see how many "2" sounding words I had there?) sections: The Mode and Configuration. The Mode sets the analog/input/output state and the Configuration handles the specifics of the particular mode. We have chosen output (Mode is 0b11) and the 50MHZ-capable output mode (Cfg is 0b00). I'm not fully sure what the 50MHz refers to yet, so I just kept it at 50MHz because that was the default value.
After talking to the CRL, we get to talk to the BSRR register. This register allows us to write a "1" to a bit in the register in order to either set or reset the pin's output value. We start by writing to the BS0 bit to set PB0 high and then writing to the BR0 bit to reset PB0 low. Pretty straightfoward.
It's not a complicated program. Half the battle is knowing where all the pieces fit. The STM32F1Cube zip file contains some examples which could prove quite revealing into the specifics on using the various peripherals on the device. In fact, it includes an entire hardware abstraction layer (HAL) which you could compile into your program if you wanted to. However, I have heard some bad things about it from a software engineering perspective (apparently it's badly written and quite ugly). I'm sure it works, though.
So, the next step is to compile the program. See the makefile in the repository. Basically what we are going to do is first compile the main source file, the assembly file we pulled in from the STM32Cube library, and the C file we pulled in from the STM32Cube library. We will then link them using the linker script from the STM32Cube and then dump the output into a binary file.
1# Makefile for the STM32F103C8 blink program
2#
3# Kevin Cuzner
4#
5
6PROJECT = blink
7
8# Project Structure
9SRCDIR = src
10COMDIR = common
11BINDIR = bin
12OBJDIR = obj
13INCDIR = include
14
15# Project target
16CPU = cortex-m3
17
18# Sources
19SRC = $(wildcard $(SRCDIR)/*.c) $(wildcard $(COMDIR)/*.c)
20ASM = $(wildcard $(SRCDIR)/*.s) $(wildcard $(COMDIR)/*.s)
21
22# Include directories
23INCLUDE = -I$(INCDIR) -Icmsis
24
25# Linker
26LSCRIPT = STM32F103X8_FLASH.ld
27
28# C Flags
29GCFLAGS = -Wall -fno-common -mthumb -mcpu=$(CPU) -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=$(addprefix $(OBJDIR)/,$(notdir $(<:.c=.lst)))
30GCFLAGS += $(INCLUDE)
31LDFLAGS += -T$(LSCRIPT) -mthumb -mcpu=$(CPU) --specs=nosys.specs
32ASFLAGS += -mcpu=$(CPU)
33
34# Flashing
35OCDFLAGS = -f /usr/share/openocd/scripts/interface/stlink-v2.cfg \
36 -f /usr/share/openocd/scripts/target/stm32f1x.cfg \
37 -f openocd.cfg
38
39# Tools
40CC = arm-none-eabi-gcc
41AS = arm-none-eabi-as
42AR = arm-none-eabi-ar
43LD = arm-none-eabi-ld
44OBJCOPY = arm-none-eabi-objcopy
45SIZE = arm-none-eabi-size
46OBJDUMP = arm-none-eabi-objdump
47OCD = openocd
48
49RM = rm -rf
50
51## Build process
52
53OBJ := $(addprefix $(OBJDIR)/,$(notdir $(SRC:.c=.o)))
54OBJ += $(addprefix $(OBJDIR)/,$(notdir $(ASM:.s=.o)))
55
56
57all:: $(BINDIR)/$(PROJECT).bin
58
59Build: $(BINDIR)/$(PROJECT).bin
60
61install: $(BINDIR)/$(PROJECT).bin
62 $(OCD) $(OCDFLAGS)
63
64$(BINDIR)/$(PROJECT).hex: $(BINDIR)/$(PROJECT).elf
65 $(OBJCOPY) -R .stack -O ihex $(BINDIR)/$(PROJECT).elf $(BINDIR)/$(PROJECT).hex
66
67$(BINDIR)/$(PROJECT).bin: $(BINDIR)/$(PROJECT).elf
68 $(OBJCOPY) -R .stack -O binary $(BINDIR)/$(PROJECT).elf $(BINDIR)/$(PROJECT).bin
69
70$(BINDIR)/$(PROJECT).elf: $(OBJ)
71 @mkdir -p $(dir $@)
72 $(CC) $(OBJ) $(LDFLAGS) -o $(BINDIR)/$(PROJECT).elf
73 $(OBJDUMP) -D $(BINDIR)/$(PROJECT).elf > $(BINDIR)/$(PROJECT).lst
74 $(SIZE) $(BINDIR)/$(PROJECT).elf
75
76macros:
77 $(CC) $(GCFLAGS) -dM -E - < /dev/null
78
79cleanBuild: clean
80
81clean:
82 $(RM) $(BINDIR)
83 $(RM) $(OBJDIR)
84
85# Compilation
86$(OBJDIR)/%.o: $(SRCDIR)/%.c
87 @mkdir -p $(dir $@)
88 $(CC) $(GCFLAGS) -c $< -o $@
89
90$(OBJDIR)/%.o: $(SRCDIR)/%.s
91 @mkdir -p $(dir $@)
92 $(AS) $(ASFLAGS) -o $@ $<
93
94
95$(OBJDIR)/%.o: $(COMDIR)/%.c
96 @mkdir -p $(dir $@)
97 $(CC) $(GCFLAGS) -c $< -o $@
98
99$(OBJDIR)/%.o: $(COMDIR)/%.s
100 @mkdir -p $(dir $@)
101 $(AS) $(ASFLAGS) -o $@ $<
The result of this makefile is that it will create a file called "bin/blink.bin" which contains our compiled program. We can then flash this to our microcontroller using openocd.
Source for this step: https://github.com/rogerclarkmelbourne/Arduino_STM32/wiki/Programming-an-STM32F103XXX-with-a-generic-%22ST-Link-V2%22-programmer-from-Linux
This is the very last step. We get to do some openocd configuration. Firstly, we need to write a small configuration script that will tell openocd how to flash our program. Here it is:
1# Configuration for flashing the blink program
2init
3reset halt
4flash write_image erase bin/blink.bin 0x08000000
5reset run
6shutdown
Firstly, we init and halt the processor (reset halt). When the processor is first powered up, it is going to be running whatever program was previously flashed onto the microcontroller. We want to stop this execution before we overwrite the flash. Next we execute "flash write_image erase" which will first erase the flash memory (if needed) and then write our program to it. After writing the program, we then tell the processor to execute the program we just flashed (reset run) and we shutdown openocd.
Now, openocd requires knowledge of a few things. It first needs to know what programmer to use. Next, it needs to know what device is attached to the programmer. Both of these requirements must be satisfied before we can run our script above. We know that we have an stlinkv2 for a programmer and an stm32f1xx attached on the other end. It turns out that openocd actually comes with configuration files for these. On my installation these are located at "/usr/share/openocd/scripts/interface/stlink-v2.cfg" and "/usr/share/openocd/scripts/target/stm32f1x.cfg", respectively. We can execute all three files (stlink, stm32f1, and our flashing routine (which I have named "openocd.cfg")) with openocd as follows:
1openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg \
2 -f /usr/share/openocd/scripts/target/stm32f1x.cfg \
3 -f openocd.cfg
So, small sidenote: If we left off the "shutdown" command, openocd would actually continue running in "daemon" mode, listening for connections to it. If you wanted to use gdb to interact with the program running on the microcontroller, that is what you would use to do it. You would tell gdb that there is a "remote target" at port 3333 (or something like that). Openocd will be listening at that port and so when gdb starts talking to it and trying to issue debug commands, openocd will translate those through the STLinkV2 and send back the translated responses from the microcontroller. Isn't that sick?
In the makefile earlier, I actually made this the "install" target, so running "sudo make install" will actually flash the microcontroller. Here is my output from that command for your reference:
1kcuzner@kcuzner-laptop:~/Projects/ARM/stm32f103-blink$ sudo make install
2arm-none-eabi-gcc -Wall -fno-common -mthumb -mcpu=cortex-m3 -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=obj/system_stm32f1xx.lst -Iinclude -Icmsis -c src/system_stm32f1xx.c -o obj/system_stm32f1xx.o
3arm-none-eabi-gcc -Wall -fno-common -mthumb -mcpu=cortex-m3 -DSTM32F103xB --specs=nosys.specs -g -Wa,-ahlms=obj/main.lst -Iinclude -Icmsis -c src/main.c -o obj/main.o
4arm-none-eabi-as -mcpu=cortex-m3 -o obj/startup_stm32f103x6.o src/startup_stm32f103x6.s
5arm-none-eabi-gcc obj/system_stm32f1xx.o obj/main.o obj/startup_stm32f103x6.o -TSTM32F103X8_FLASH.ld -mthumb -mcpu=cortex-m3 --specs=nosys.specs -o bin/blink.elf
6arm-none-eabi-objdump -D bin/blink.elf > bin/blink.lst
7arm-none-eabi-size bin/blink.elf
8 text data bss dec hex filename
9 1756 1092 1564 4412 113c bin/blink.elf
10arm-none-eabi-objcopy -R .stack -O binary bin/blink.elf bin/blink.bin
11openocd -f /usr/share/openocd/scripts/interface/stlink-v2.cfg -f /usr/share/openocd/scripts/target/stm32f1x.cfg -f openocd.cfg
12Open On-Chip Debugger 0.9.0 (2016-04-27-23:18)
13Licensed under GNU GPL v2
14For bug reports, read
15 http://openocd.org/doc/doxygen/bugs.html
16Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
17Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
18adapter speed: 1000 kHz
19adapter_nsrst_delay: 100
20none separate
21Info : Unable to match requested speed 1000 kHz, using 950 kHz
22Info : Unable to match requested speed 1000 kHz, using 950 kHz
23Info : clock speed 950 kHz
24Info : STLINK v2 JTAG v17 API v2 SWIM v4 VID 0x0483 PID 0x3748
25Info : using stlink api v2
26Info : Target voltage: 3.335870
27Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
28target state: halted
29target halted due to debug-request, current mode: Thread
30xPSR: 0x01000000 pc: 0x08000380 msp: 0x20004ffc
31auto erase enabled
32Info : device id = 0x20036410
33Info : flash size = 64kbytes
34target state: halted
35target halted due to breakpoint, current mode: Thread
36xPSR: 0x61000000 pc: 0x2000003a msp: 0x20004ffc
37wrote 3072 bytes from file bin/blink.bin in 0.249272s (12.035 KiB/s)
38shutdown command invoked
39kcuzner@kcuzner-laptop:~/Projects/ARM/stm32f103-blink$
After doing that I saw the following awesomeness:
Wooo!!! The LED blinks! At this point, you have successfully flashed an ARM Cortex-M3 microcontroller with little more than a cheap programmer from eBay, a breakout board, and a few stray wires. Feel happy about yourself.
For me, this marks the end of one journey and the beginning of another. I can now feel free to experiment with ARM microcontrollers without having to worry about ruining a nice shiny development board. I can buy a obscenely powerful $1 STM32 microcontroller from eBay and put it into any project I want. If I were to try to do that with AVRs, I would be stuck with the ultra-low-end 8-pin ATTiny13A since that's about it for ~$1 AVR eBay offerings (don't worry...I've got plenty of ATMega328PB's...though they weren't $1). I sincerely hope that you found this tutorial useful and that it might serve as a springboard for doing your own dev board-free ARM development.
If you have any questions or comments (or want to let me know about any errors I may have made), let me know in the comments section here. I will try my best to help you out, although I can't always find the time to address every issue.