Fork me on GitHub

Project Notes

#549 Bare Metal C on the Blue Pill

Bare metal programming the STM32F103C8T6 (as used in the Blue Pill) by hand with gcc and the stlink open source STM32 MCU programming toolset.



The STM32F103C8T6 is an ARM Cortex-M3 processor thats very popular in cheap development boards like the Blue Pill.

Usually one would program the device with an IDE such as STM32CubeIDE, Keil, or even the Arduino IDE with STM32 extensions. Hoever, that hides much of the “fun” of programming a specific device, so here I thought I would investigate pure bare metal coding to see what it takes to get the most minimal program up and running.

I was inspired to try this after watching the bare metal embedded lecture series from Fastbit Embedded Brain Academy on YouTube. It focused on a different STM32 model, but the details of the approach were very useful for working with the STM32F103C8T6.

My goal is modest - blink the LED that’s usually attached to PC13 on most development boards.

I’m running this on MacOSX, and for tooling I’m using:

Note that all the details that follow are specific to the STM32F103C8T6. The specifics may vary (a lot) for other STM32 models.

Bare Metal Programming Overview

  • write the main C program. I am not using any C standard library functions so far, which simplifies the linking process. The program comprises:
    • blinky.c/h - the main program
    • led.c/h - all the functions to enable and blink the LED on GPIO PC13
    • delay.c/h - a simple delay function that approximates a millisecond-resolution
  • implement the start up code - see stm32_startup.c
    • defines the ISR vector map and implements the main reset handler
    • the reset handler sets up memory for execution then hands over control to the main program
  • write the linker script stm32_ls.ld which defines where all the code pieces should be assembled in memory
  • compile and link everything with gcc
  • use objcopy to produce the binary image to be flashed on the device
  • use the stlink tools to flash the device
  • optionally, fire up st-util and gdb to debug the program on the device

All the steps are automated with a Makefile.

About the STM32F103C8T6

To program bare metal, it’s necessary to know the precise hardware details, in particular the memory map and register definitions. The two critical resources I’ve used for this can be obtained from the STM32F103 product info page:

  • DS5319: STM32F103x8/B datasheet
  • RM0008: STM32F103xx (etc) reference manual

Some of the main details follow..

Device overview from datasheet section 2.1

Feature STM32F103C8T6
Flash 64 Kbytes
SRAM 20 Kbytes
Timers - general purpose 3
Timers - advanced control 1
Communication - SPI 2
Communication - I2C 2
Communication - USART 3
Communication - USB 1
Communication - CAN 1
GPIOs 37
12 bit ADC 2
ADC channels 10
CPU Frequency 72 MHz
Operating Voltage 2.0 to 3.6 V
Operating Temperature –40 to 85 °C
Package LQFP48

STM32F103C8T6 (numbering scheme from datasheet section 7):

  • STM32 = ARM-based 32-bit microcontroller
  • F = Product type: general-purpose
  • 103 = Device subfamily: performance line
  • C = Pin count: 48 pins
  • 8 = Flash memory size: 64 Kbytes of Flash memory
  • T = Package: LQFP
  • 6 = Industrial temperature range, –40 to 85 °C.

Memory Map

Memory map from datasheet section 4:


Interrupt Vectors

STM32F103C8T6 is classified by ST as a “Medium-density performance line” device.

Interrupt and exception vectors are detailed in RM0008 10.1.2, Table 63: Vector table for other STM32F10xxx devices.

The details were used to create the isr vector table in stm32_startup.c

Linker Script

The linker script stm32_ls.ld is used to define three critical details for the linker:

  • ENTRY - the function used as the program entry point
  • MEMORY - the device-specific memory regions. We care about two: FLASH and SRAM.
  • SECTIONS - how program code is collected into memory regions


For this example, I’m just going to control GPIO on PC13. This is pin 2 on the LQFP48 package, and usually connected to an on-board LED on most development boards.

During and just after reset, the alternate functions are not active and the I/O ports are configured in Input Floating mode (CNFx[1:0]=01b, MODEx[1:0]=00b). When configured as output, the value written to the Output Data register (GPIOx_ODR) is output on the I/O pin.

It is possible to use the output driver in Push-Pull mode or Open-Drain mode. I’m going to use Push-Pull mode, so that it doesn’t matter whether the LED is wired to source or sink from the pin.


  • whether the LED is wired to source or sink from PC13 will determine if the pin logic is active high or low
  • the current handling of PC13 is very limited (+/-3mA) but I trust that the development board makers have sized the LED current limiting resistor accordingly.

There are four steps required to drive a GPIO pin output:

  • The GPIO port clock must be enabled for anything to work (RCC_APB2ENR register)
  • the GPIO pin configuration must be set correctly (GPIOCx_CRH or GPIOCx_CRH registers depending on the port and pin in question)
  • the GPIO pin mode must be set correctly (GPIOCx_CRH or GPIOCx_CRH registers depending on the port and pin in question)
  • finally, the output value can be set in the GPIOx_ODR register

For PC13 - GPIO port C, bit 13 - these are the specifics, found in the RM0008 STM32F103xx (etc) reference manual:

Reset and clock control RCC:

  • Memory region: 0x4002 1000 - 0x4002 13FF
  • RCC_APB2ENR is at offset 0x18: 0x4002 1018
  • IOPCEN is bit 4 of RCC_APB2ENR: I/O port C clock enable. Set to 1 for I/O port C clock enabled


GPIO Port C:

  • Memory region: 0x4001 1000 - 0x4001 13FF
  • GPIOC_CRH is at offset 0x4: 0x4001 1004
  • PC13 configuration is set with CNF13[1:0] in GPIOC_CRH
    • Possible CNFx values when the port is set for output:
    • 00: General purpose output push-pull
    • 01: General purpose output Open-drain (reset state)
    • 10: Alternate function output Push-pull
    • 11: Alternate function output Open-drain
  • PC13 mode is set with MODE13[1:0] in GPIOC_CRH. Possible values:
    • 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
  • GPIOC_ODR is at offset 0xC: 0x4001 100C
    • sets the output data value, 1 bit per pin.


Connecting the Programmer

The ST-Link/V2 programmer connects to the SWD header on the module. Only 4 pins need to be connected from the 10-pin shroud on the programmer: 3.3V, GND, SWDIO, SWDCLK



Building the project

Default make or make all will just compile. make flash compiles as needed and flashes the device with st-flash:

$ cd blinky
$ make flash
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -std=gnu11 -Wall -g -O0   -c -o blinky.o blinky.c
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -std=gnu11 -Wall -g -O0   -c -o delay.o delay.c
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -std=gnu11 -Wall -g -O0   -c -o led.o led.c
arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb -std=gnu11 -Wall -g -O0   -c -o stm32_startup.o stm32_startup.c
arm-none-eabi-gcc -T stm32_ls.ld -nostdlib -Wl, -o blinky.elf blinky.o delay.o led.o stm32_startup.o
arm-none-eabi-objcopy -O binary blinky.elf blinky.bin
st-flash write blinky.bin 0x08000000
st-flash 1.6.0
2020-05-07T20:00:56 INFO common.c: Loading device parameters....
2020-05-07T20:00:56 INFO common.c: Device connected is: F1 Medium-density device, id 0x20036410
2020-05-07T20:00:56 INFO common.c: SRAM size: 0x5000 bytes (20 KiB), Flash: 0x10000 bytes (64 KiB) in pages of 1024 bytes
2020-05-07T20:00:56 INFO common.c: Attempting to write 708 (0x2c4) bytes to stm32 address: 134217728 (0x8000000)
Flash page at addr: 0x08000000 erased
2020-05-07T20:00:56 INFO common.c: Finished erasing 1 pages of 1024 (0x400) bytes
2020-05-07T20:00:56 INFO common.c: Starting Flash write for VL/F0/F3/F1_XL core id
2020-05-07T20:00:56 INFO flash_loader.c: Successfully loaded flash loader in sram
  1/1 pages written
2020-05-07T20:00:56 INFO common.c: Starting verification of write complete
2020-05-07T20:00:56 INFO common.c: Flash written and verified! jolly good!

Calibrating the Delay

The delay function used in the program is not timer based - it just does a NOP loop for an appropriate number of cycles.

I changed the program to use a 10ms delay between on and off transitions, used a scope to measure the resulting waveform, and then calcualted what the correct number of loops per millisecond should be. An alternative approach would be to disassemble the code and count the operations and operation timing, but this was quicker!

At 10ms per transition (20ms full cycle), we should see a 50Hz square wave. Hooking up to an oscilloscope, that’s exactly what I’m recording:


Debugging with GDB

The makefile is set to build the project with debug symbols, so inspecting program operation with gdb is made a little easier.

Running st-util in one terminal window:

$ make debughost
st-util 1.6.0
2020-05-07T23:23:51 INFO common.c: Loading device parameters....
2020-05-07T23:23:51 INFO common.c: Device connected is: F1 Medium-density device, id 0x20036410
2020-05-07T23:23:51 INFO common.c: SRAM size: 0x5000 bytes (20 KiB), Flash: 0x10000 bytes (64 KiB) in pages of 1024 bytes
2020-05-07T23:23:51 INFO gdb-server.c: Chip ID is 00000410, Core ID is  1ba01477.
2020-05-07T23:23:51 INFO gdb-server.c: Listening at *:4242...
2020-05-07T23:24:39 INFO gdb-server.c: Found 6 hw breakpoint registers
2020-05-07T23:24:39 INFO gdb-server.c: GDB connected.

Then connecting with gdb. Here’s a session where I break on the led_on function and examine the effect on the GPIOC_ODR register:

$ make gdb
arm-none-eabi-gdb blinky.elf
GNU gdb (GNU Tools for Arm Embedded Processors 9-2019-q4-major)
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-apple-darwin10 --target=arm-none-eabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
Find the GDB manual and other documentation resources online at:

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from blinky.elf...
(gdb) target extended localhost:4242
Remote debugging using localhost:4242
Reset_Handler () at stm32_startup.c:179
179 void Reset_Handler(void) {
(gdb) break led_on
Breakpoint 1 at 0x800021e: file led.c, line 27.
(gdb) cont
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, led_on () at led.c:27
27    uint32_t *pGPIOC_ODR = (uint32_t *)GPIOC_ODR;
(gdb) list
23    led_off();
24  }
26  void led_on(void) {
27    uint32_t *pGPIOC_ODR = (uint32_t *)GPIOC_ODR;
28    *pGPIOC_ODR |= ( 1 << PC13);
29  }
31  void led_off(void) {
(gdb) next
28    *pGPIOC_ODR |= ( 1 << PC13);
(gdb) x/x pGPIOC_ODR
0x4001100c: 0x00000000
(gdb) next
29  }
(gdb) x/x pGPIOC_ODR
0x4001100c: 0x00002000
(gdb) info breakpoints
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x0800021e in led_on at led.c:27
  breakpoint already hit 1 time
(gdb) delete 1
(gdb) cont

Credits and References

About LEAP#549 STM32C
Project Source on GitHub Project Gallery Return to the LEAP Catalog

This page is a web-friendly rendering of my project notes shared in the LEAP GitHub repository.

LEAP is my personal collection of electronics projects, usually involving an Arduino or other microprocessor in one way or another. Some are full-blown projects, while many are trivial breadboard experiments, intended to learn and explore something interesting (IMHO!).

The projects are usually inspired by things found wild on the net, or ideas from the sources such as:

Feel free to borrow liberally, and if you spot any issues do let me know. See the individual projects for credits where due. There are even now a few projects contributed by others - send your own over in a pull request if you would also like to add to this collection.