Bare Metal STM32 Programming and a Quadcopters Awakening
Last year I got the Crazepony Mini quadcopter, and just recently I figured out how to program it. I will show my progress in this post, and it will also serve as a getting started guide for programming STM32 microcontrollers. We will build a minimal working example to blink an LED with only the GNU ARM compiler (gcc) and without any library dependencies.
If you are just here for the STM32 programming you can skip this part.
The Crazepony Mini #
You can get the quadcopter on eBay for around 100 €. It ships with a remote control that wirelessly connects to the drone. Like the drone the remote control has no casing, which I find for the drone looks really good, but unfortunately makes holding the remote control difficult. Both devices have firmware by Crazepony installed for which they published the source on GitHub. They seem to be using the Keil IDE. Although it’s mainly made for educational purpose you can totally fly the quadcopter, and I had fun with it for a while.
Mine came with two extra motors for replacement, and you can get super cheap replacement propellers from AliExpress. The flight time is supposed to be up to 6 minutes, so I got some extra batteries and a charger so that I can quickly switch batteries when power runs out.
So let’s get into the technical details of the drone. The chip on the quadcopter is a 32-bit ARM Cortex-M processor with 64 KiB flash memory. On board are a wireless module, a 3-axis digital compass which detects orientation using the earth’s magnetic field, an altimeter that measures height by air pressure and an accelerometer and gyro sensor combined into one chip. In this post we will be using the LEDs on one of the 4 arms and most importantly the integrated USB to serial bridge to flash our program. You can get the schematic for the quadcopter here.
The bootloader #
There are a few ways to flash firmware onto STM32 microcontrollers. You can use one of the debugging interfaces JTAG or Serial Wire Debug (SWD) which also have support for on-chip debugging. Note that SWD despite it’s name does not use the standard serial port of the chip. In fact, using the serial connection also known as UART connection is another and the most basic way to flash the controller. That’s what we will be using.
If you want to follow along you should get the reference manual, which is meant for application developers, as well as the datasheet for your processor. For the processor on my drone the STM32F103 reference manual and the STM32F103x8 datasheet apply. You can find them by searching for your processor name together with “reference manual” or “datasheet”.
The STM32 processors have three boot modes as shown in this table from the reference manual. Booting from flash is the default mode and what we will be using to run our program once we uploaded it to flash memory. To flash the microcontroller over UART we will have to boot the processor in system memory mode. System memory refers to a part of ROM on the chip which contains a bootloader since the manufacturing. That means there’s no way to brick the controller. We will be speaking to that bootloader over UART and tell it to write our firmware to flash.
USB to serial converter #
If your computer doesn’t have a serial port which is likely nowadays you need a USB to serial bridge to connect the board to your PC. Fortunately most STM32 boards like the popular Blue Pill board have a USB to UART bridge with the CP2102 chip on board. Below you can see the bridge in the schematic of my drone. If your board has an USB port it most likely connects to a bridge as well. Otherwise you can get a dongle with the CP2102 chip for around 8 € on Amazon. The chip connects to the 5 volts and ground pins to power the microcontroller. And the
TXD serial lines connect to pins
RXD_BT in the schematic) on the processor respectively.
From the pin table from the processors datasheet:
Don’t get confused with the RX reciever and TX transmitter pins. The pins on the processor are named from the processors perspective and the pins on the bridge are named from the computers perspetive.
Flashing the processor #
Before we can start writing to flash we need to boot the processor in system mode. As you can tell from the boot modes table we need to set the
B00T0 pin to high. On my microcontroller the
BOOT0 pin is actually connected to the CP2102 chip as shown in the schematic so this happens automatically when I upload firmware. On some boards like the Blue Pill you can set the
BOOT1 pins using those yellow jumpers.
BOOT0 pin is set correctly you can connect the board to your computer to power it. The microcontroller boots and a serial device should appear at
We will use the stm32loader Python script to upload our program. It requires the pySerial library. A normal user can’t write to
/dev/ttyUSB0 so you either need to run it as root or add your user to the
uucp group depending on your distro which gives a user access to the serial ports. You might need to login again afterwards. If you are having issues with the script on Arch Linux consider trying my patch.
stm32loader.py script can download the current firmware so you might want to do that before flashing your own. To upload your own firmware you would pass it the path to the device and the binary file to write.
-e erases the previous binary
-w means write and
-v will download after uploading and verify that the upload worked.
Startup assembly code #
For now we will start with a C program that does nothing. We call it
main.c and it will just stay in an endless while loop.
Although this alone can’t run on the microcontroller yet. The processor starts executing at a very specific entry point in flash memory. We need finer memory control to set this up correctly and therefore we will use assembly. From the reference manual:
After this startup delay has elapsed, the CPU fetches the top-of-stack value from address
0x0000 0000, then starts code execution from the boot memory starting from
The assembly code will go in a file called
The first two lines configure the assembler for a Cortex-M3 processor with the Thumb instruction set. On ARM a word is 4 bytes. The first
.word line is writing the value
0x20005000 to the beginning of the output binary. When we flash the binary this will become position
0x0000 0000 in flash memory. Note the quote above, that’s the position where the processor reads the stack’s top address. As you might know the program stack lives at the very end of memory and grows backwards, the end of memory on my processor is
0x20005000. You might want to adjust this for your processor, I will get into memory layout later.
The next word we write is the address of the
_reset function which is defined below. So this address ends up at position
0x0000 0004, you probably see the pattern now. That is where the processor starts code execution.
.thumb_func directive is required for functions to run on a processor with the Thumb instruction set like the STM32. For our C code this will be implied for all functions by passing a compiler flag. The
_reset function calls the
main function defined in the C program and afterwards enters an endless loop.
You will often encounter endless loops in microprocessor programming. They are basically the microprocessor equivalent to
exit(0). When you want the program to end or there is some failure you enter an endless loop to stop the control flow. In this state a reset is required to restart the program.
Linker script #
The last file required is a linker script which specifies the processor’s memory layout to the linker. You can see the relevant section of the memory map from the datasheet below.
As you can see the flash memory starts at
0x0800 0000 and the SRAM starts at
0x2000 0000. I could also take from the datasheet that my processor has 64 KiB of flash and 20 KiB of SRAM. This information goes into the linker script called
Now that we have a C program, the startup assembly code and the linker script ready, we can start compiling the final binary.
Compiling for the STM32 #
To compile for ARM processors you can get the GNU ARM toolchain. It provides a C compiler
arm-none-eabi-gcc, a linker
arm-none-eabi-ld, etc. to cross-compile for ARM. On Debian the package is called
gcc-arm-none-eabi and on Arch Linux you need the packages
First we compile the assembly and C code to object files. The
-mthumb flags are required for the C compiler to compile for STM32 processors.
When we link both object files we pass the linker script with the
The ELF format is designed for binaries that run on an operating system. We need to extract the actual raw binary executable, starting with the eight bytes we set in the assembly code. The
objcopy utility can do that.
If everything compiled without errors you can now flash your first firmware and hope for the best.
If everything went right nothing should happen, hurray!
Controlling output pins #
On my drone the two LEDs on the top and bottom of one of the arms are connected to pin
PA11 of the processor.
To turn them on the pin has to be configured as an output pin first. Configuring or toggling any of the
PA pins is done by writing to the
PB pins would be controlled through the
GPIOB registers and so on. In the system architecture figure from the reference manual below, you can see that the
GPIOA registers are part of the
Clock enable register #
To write to the
GPIOA registers the corresponding clock has to be turned on first. As documented in the reference manual the
APB2 peripheral clock enable register is accessible at address
0x4002 1018. It is 32 bits long and bit 2 controls the
Bit 2 IOPAEN: IO port A clock enable
Set and cleared by software.
0: IO port A clock disabled
1: IO port A clock enabled
So to turn on the
GPIOA clock we can set the 32 bit value
0x00000004 (bit 2 set) at address
volatile keyword tells the compiler that the value may change at any time because it’s accessed by other hardware. Without the keyword the compiler could notice that the value is never used by any of our code and optimize the line away. Thanks to dmitrygr for pointing this out in the comments.
Port configuration register #
To configure the
PA pins there are the
GPIOA port configuration register low for pins 0 to 7 and the
GPIOA port configuration register high for pins 8 to 15. Since on my drone I want to configure pin
PA11 I need the high register at address
0x4001 0804. It is 32 bits as well with four bits for each pin. Bits 12 to 15 are for pin
PA11. The first two bits are specifying the
MODE and the latter two bits the
CNF value. From the reference manual:
CNFy[1:0]: Port x configuration bits (y= 8 .. 15)
These bits are written by software to configure the corresponding I/O port.
Refer to Table 20: Port bit configuration table.
In input mode (MODE[1:0] = 00):
00: Analog mode
01: Floating input (reset state)
10: Input with pull-up / pull-down
In output mode (MODE[1:0] > 00):
00: General purpose output push-pull
01: General purpose output Open-drain
10: Alternate function output Push-pull
11: Alternate function output Open-drain
MODEy[1:0]: Port x mode bits (y= 8 .. 15)
These bits are written by software to configure the corresponding I/O port.
Refer to Table 20: Port bit configuration table.
00: Input mode (reset state)
01: Output mode, max speed 10 MHz.
10: Output mode, max speed 2 MHz.
11: Output mode, max speed 50 MHz.
We want to configure the pin as an output and the frequency doesn’t matter for an LED so we go with the lowest 2 MHz. This means the
MODE value will be
10. Because our
MODE value is greater than 0 the second table applies for the
CNF value. We choose a push-pull output for the LED, so the
CNF value is
So the four bits we need for the pin are
0010. In hexadecimal the four bits of a pin are one digit. Because we don’t want to touch the other pins configuration settings we first clear the four bits of pin
PA11 and then set them using a binary or.
Port bit set/reset registers #
Finally, to turn on the pin, there is the
GPIOA port bit set/reset register at address
0x4001 0810. Bits 0 to 15 correspond to the different pins.
BSy: Port x Set bit y (y= 0 .. 15)
These bits are write-only and can be accessed in Word mode only.
0: No action on the corresponding ODRx bit
1: Set the corresponding ODRx bit
For turning it off you can use the
GPIOA port bit reset register at
Putting it all together #
Here is the complete
main.c program to turn the
PA11 pin on and off. The
stdint.h header is required for the
uint32_t type. A for-loop is used as a simple way to get some delay.
Once you’ve adjusted everything for your board compile it and flash it. If everything went right the output pin should be powered and if an LED is connected it should start blinking.
You can find the complete source code on GitHub, including a Makefile. All the manuals quoted in this post can also be found in the repository.
In the next post, we will learn how to use the CMSIS library. It defines pointers with meaningful names for the registeres we saw in this article, so we don’t have to hardcode all the addresses. And it provides functions for properly timed delay so we can get rid of the for-loops which lack a fixed delay.
You can leave comments on Hacker News. I’d be happy about any feedback and corrections.