My notes on the STM32F103C8T, aka. Blue Pill

Table of Contents


So I got me some of these off of ebay or aliexpress a while ago. Mostly I wanted them because they're about as cheap and small as an arduino nano, but on paper these small blue pills have way better specs. The only problem, and the reason it's taken me over a year to try again to get these boys running, is that they're not as convenient to use as arduinos. Bootloaders need to be flashed for USB programming to work, for example.

1 Specs and comparison vs Arduino Nano

Device Blue Pill Arduino Nano
MCU STM32 F103 ATmega328
Core ARM Cortex-M3 32bit AVR RISC 8bit
Freq. 72 MHz 20 MHz
Flash 64 kB 32 kB
SRAM 20 kB 2 kB
PWM Res. 16 bit 10 bit
ADC Res. 12 bit 10 bit
GPIOs 32 24
PWM Channel 15 6
Analog Channel 10 8
I2C Buses 2 1
SPI Buses 2 1
CAN Bus Yes No


If using the ST-Link V2 programmer for flashing/debugging and USB for serial input/output at the same time, make sure to disconnect the VCC line from the programmer, as power will be supplied by the USB.

3 STM32Duino

Ok so, unlike an Arduino, these chips aren't exactly plug-and-play out of the box. Following are the steps I had to take to get them programmable and running on par with an Arduino using STM32Duino. By flashing the STM32duino bootloader, we'll be able to upload Arduino sketches via USB in the Arduino IDE.

In all steps of the process, the STM32duino wiki has been a great help!

3.1 Step 1: Install stlink

On arch, the package is just called stlink and provides the stlink-gui application we'll use to flash shit.

3.2 Step 2: Get hold of a ST-Link V2 programmer

They look like this.


3.3 Step 3: Configure STM32 boot pins

To allow us to flash the board, set pin Boot0 to state 1. Leave Boot1 at false. Boot0 is the one of the two boot-pins furthest from the reset button.

3.4 Step 4: Connect the device to the programmer to the PC

Connect SWCLK, SWDIO, GND, 3.3V pins from programmer to corresponding pins (Note: probably not in the same order) on the side-angled 4-pin header on the board. Like in this image. stlink-wiring.jpg. Then connect the programmer to PC via USB.

3.5 Step 5: Flash bootloader

Open stlink-gui and press the connect button. Some information about the device should show in a box. It should not say "device: unknown" and "memory size: 0" etc.

Still in the GUI, open the bootloader file. Can be found at I used genericboot20pc13.bin. Then flash the image to memory address 0x08000000 (should be default suggested).

3.6 Step 6: Disconnect

isconnect the device first in the GUI, then physically. Set Boot0 pin to 0. We should be all good to go now!

3.7 Step 7: Verification

To check that the flashing worked as expected, connect the stm32 via it's on-board USB, and read its serial output, e.g. with "screen /dev/ttyXXX". If the flashing was successful, we should read a repeating message:

Congratulations, you have installed the STM32duino bootloader

For more information about Arduino on STM32

3.8 Step 8: Setup arduino etc

Cba writing this part, but it's all on the wiki.

On my linux PC, I had some problems with errors along the lines of "dfu cannot open device". I didn't manage to fix this, but installing all the necessary drivers on Windows did the trick.

3.9 Done!

But I don't think I'll use them this way. Arduino is not as fun as Rust ;)

4 Rust

4.1 Step 1: Install rust target

rustup target add thumbv7m-none-eabi

4.2 Step 2: Setup cargo project

Set project file Cargo.toml to something like this:

name = "stm32_blink"
version = "0.1.0"
edition = "2018"

# optimize for size ('z' would optimize even more)
opt-level = 's'
# link with link time optimization (lto).
lto = true
# enable debugging in release mode.
debug = true

# Gives us access to the STM32F1 registers
stm32f1 = {version = "0.6.0", features = ["stm32f103", "rt"]}
# provides startup code for the ARM CPU
cortex-m-rt = "0.6.7"
# provides access to low level ARM CPU registers (used for delay)
cortex-m = "0.5.8"
# provies a panic-handler (halting cpu)
# (required when not using stdlib)
panic-halt = "0.2.0"

The cortex-m-rt crate also requires a memory.x file, which specifies the memory layout of the board. The documentation already provides the file for the BluePill as an example, so we only have to create the file memory.x with the following content:

/* Linker script for the STM32F103C8T6 */
  FLASH : ORIGIN = 0x08000000, LENGTH = 64K
  RAM : ORIGIN = 0x20000000, LENGTH = 20K

Create file .cargo/config with the following content:

# Instruction set of Cortex-M3 (used in BluePill)
target = "thumbv7m-none-eabi"

rustflags = [
  # use the Tlink.x scrip from the cortex-m-rt crate
  "-C", "link-arg=-Tlink.x",

4.3 Step 3: Example program

Here is an example Blink program. Set src/ to the following:

// std and main are not available for bare metal software

extern crate stm32f1;
extern crate panic_halt;
extern crate cortex_m_rt;

use cortex_m_rt::entry;
use stm32f1::stm32f103;

// use `main` as the entry point of this application
fn main() -> ! {
    // get handles to the hardware
    let peripherals = stm32f103::Peripherals::take().unwrap();
    let gpioc = &peripherals.GPIOC;
    let rcc = &peripherals.RCC;

    // enable the GPIO clock for IO port C
    rcc.apb2enr.write(|w| w.iopcen().set_bit());
    gpioc.crh.write(|w| unsafe{

        gpioc.bsrr.write(|w| w.bs13().set_bit());
        gpioc.brr.write(|w| w.br13().set_bit());

4.4 Step 4: Creating the binary

Compile with cargo build --release.

4.5 Step 5: Connect the stm32

With the ST-Link V2 programmer. See 3.

4.6 Step 5: Flash / debug

Install stlink. Run st-util to automatically connect to the STM32 and start a GDB server at some port. Tip: st-util seems to die every time GDB disconnects, so maybe run it in a while loop, like while true; do st-util; sleep 1; done.

Open your compiled rust-program in GDB with arm-none-eabi-gdb -q target/thumbv7m-none-eabi/release/ELF_BINARY_NAME, then connect to the server with (gdb) target remote :PORT. Finally, flash the binary with (gdb) load, and you're ready to debug!

The program starts stopped at its entry point. To just run the program, enter (gdb) continue. Otherwise it's like normal GDB debugging!

Tip: Start GDB in emacs with command gdb and command gdb -i=mi -q ../target/thumbv7m-none-eabi/debug/ELF_BINARY_NAME -ex "target remote :4242".

Note: Debugging seems to be really buggy when I've tried it. load, monitor reset halt, and continue all seem to work, but breakpoints, backtrace, and stepping seems quite broken. A better method of debugging is probably good-ol print debugging via serial port.