One of the simplest projects to get started with the STM32 microcontroller series: turn on the lights!

Windows Users

This series of write-ups assumes the reader is on a Linux operating system. Windows users can utilize the Windows Subsystems for Linux though your mileage may vary!

Straight to the Chase

For those that want to cut to the chase and save time, here is the full source code with friendly names to get you started:

Source Code

#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/rcc.h>

#define LED_PORT    GPIOC
#define LED_BLU     GPIO8
#define LED_GRN     GPIO9

int main(void) {
    gpio_set_output_options(LED_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_LOW, LED_BLU | LED_GRN);
    gpio_set(LED_PORT, LED_BLU | LED_GRN);

    while (1);

Getting Started with libopencm3

libopencm3 is a very powerful, useful, open-source firmware library for use in writing programs for various different ARM Cortex-M microcontrollers. It’s read me contains plenty of information on the basics of getting started (typically done via git submodule).

Additionally, there is a libopencm3-template repository to help in getting started.


Prior to doing any ARM Cortex-M development, the necessary dependencies need to be installed in order to successfully build/compile source code into a binary capable of being flashed (written) onto the microcontroller:

  • GNU Arm Embedded Toolchain1: Typically available from the package manager (i.e. arm-none-eabi-gcc, arm-none-eabi-binutils, arm-none-eabi-newlib, and optionally arm-none-eabi-gdb)
  • make: Usually pre-installed with most Linux distributions, a build automation tool exceptionally useful for C/C++ compiling.
  • Text Editor or IDE: Anything, really.

Flashing the STM32F0 Discovery Board

The discovery series boards provided by ST come with an on-board ST-LINK/V2 programmer. There are several ways to flash your build programs using this, though my preference is stlink by Texane.

The GCC ARM GDB (GNU Debugger) does let you write programs, but requires some additional know-how and minor legwork that may complicate understandings. However, it is an immensely powerful debugging tool that should not be overlooked for too long! For the sake of brevity, this guide will omit diving into that until later.


The aforementioned libopencm3-examples repository provides a useful, yet overly complex, Makefile. For the reader, this has been boiled down (assuming they are also using stlink mentioned above) the following, simple Makefile2 on my GitLab3.

To flash, it’s as simple as make flash (will also build the binary for your convenience).

Linker Script

The loader (.ld) file is specific to the flavor of ARM Cortex-M microcontroller being used. The authors of libopencm3 provide example loader files that can be used for most projects (e.g. located in libopencm3/lib/stm32/f0/ of the repo). However, these may not always be available and may need to be modified or created from scratch, by the developer, for proper use. There are several articles online that go into detail about linker scripts

Project Structure

The Makefile, as of writing this, assumes your project directory structure has libopencm3 either cloned, copied, or initialized as a git submodule within the same directory of your main.c. It is advised that you look through the Makefile’s variables of things you may want to change:

├── libopencm3
├── main.c
├── Makefile
└── stm32f0.ld


Naming Convention

As a note to the reader: below I will not refer to the GPIO port or pins using the #define friendly names from above. This is purely for the sake of clarity in hopes of avoiding confusion.

Although the source code is fairly simple, lets dive into it at least somewhat.

For starters, why were pins GPIO8 and GPIO9 on the GPIOC port being used? The answer can be found after a quick review of the STM32F0 Discovery User Manual4:

The Discovery board comes with two LEDs for use by the user, tied to Port C pins 8 (blue LED), and 9 (green LED).

Reset and Clock Control (RCC)

The RCC, and it’s registers, are an important part in using the STM32 microcontroller’s peripherals. Luckily, utilizing libopencm3 we can forego bit-banging our way through each register’s bits found in the reference manual5 and simply utilize the GPIO port that we need – in this case GPIOC:


GPIO Setup

Next, we need to define what mode we want the GPIO pins on their respective port to be along with the internal pull-up or pull-down resistor mode:

GPIO Mode Description
GPIO_MODE_INPUT (default) Digital input
GPIO_MODE_OUTPUT Digital output
GPIO_MODE_AF Alternate Function (requires defining which alternate function desired)
GPIO_MODE_ANALOG Analog (for use with ADC or DAC capable GPIO)
PUPD Mode Description
GPIO_PUPD_NONE (default) No internal pull-up or pull-down resistor
GPIO_PUPD_PULLUP Internal pull-up resistor
GPIO_PUPD_PULLDOWN Internal pull-down resistor
Note: The documentation for these functions, provided by libopencm3 authors, along with the function definition can be found here

Having clarified that, as we want to drive the LEDs, we will need to configure the pins as outputs with no internal pull-up or pull-down resistor:


Simplified using bitwise6 OR:


GPIO Output Options Setup

Now that the GPIO mode has been set up, the GPIO output options need to be defined as well. This will encompass the output type, and output speed:

Output Type Description
GPIO_OTYPER_PP (default) Push-pull “totem pole” output
GPIO_OTYPER_OD Open-drain output
Output Speed Description
GPIO_OSPEED_HIGH High output speed
GPIO_OSPEED_MED Medium output speed
GPIO_OSPEED_LOW (default) Low output speed
GPIO_OSPEED_100MHZ Up to 100MHz output speed (equivalent to high)
GPIO_OSPEED_50MHZ Up to 50MHz output speed
GPIO_OSPEED_25MHZ Up to 25MHz output speed (equivalent to medium)
GPIO_OSPEED_2MHZ Up to 2MHz output speed (equivalent to low)
Refer to the device datasheet for the frequency specifications and the power supply and load conditions for each speed

We’ll be driving an output LED, as opposed to sinking it (typical open-drain/open-collector sink configuration), push-pull output mode will be required. Since there isn’t any switching to be done aside from the initial “on”, we don’t require any “speed” – “no speed” not being an option GPIO_OSPEED_LOW will suffice:

gpio_set_output_options(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_LOW, GPIO8);
gpio_set_output_options(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_LOW, GPIO9);


gpio_set_output_options(GPIOC, GPIO_OTYPE_PP, GPIO_OSPEED_LOW, GPIO8 | GPIO9);

Turn it On

There are no additional options required for the user to be able to now set, or clear, the desired GPIO pins. Thus, we set it and forget it:

gpio_set(GPIOC, GPIO8);
gpio_set(GPIOC, GPIO9);


gpio_set(GPIOC, GPIO8 | GPIO9);

Lastly, we need to make sure our program never exits and does something undesirable by keeping it inside a loop:


This is just a condensed version of the following:

while(1) {
    ; // Do nothing
The details of why this is important can be found in the While(1) in Embedded C - Explained article


  1. GNU Arm Embedded Toolchain ↩︎

  2. Makefile as of writing this post ↩︎

  3. ↩︎

  4. STM32F0 Discovery User Manual ↩︎

  5. STM32F0 Reference Manual ↩︎

  6. Bitwise Operators in C ↩︎