
Dealing with LEDs using an STM32
Important notice
This article has been superseded by Hands-on exercises with LEDs and ChibiOS PAL. The information contained here may no longer be accurate or useful due to changes in the ChibiOS codebase. If you are looking for an exhaustive article about LED theory A complete overview of LEDs: from theory to applications is what you are looking for.
This article is scheduled for deletion on June 1st, 2023. We recommend that you read the new article to stay up-to-date with the latest information.
Thank you for your interest!
About
This article includes some simple examples to understand how to deal with LEDs when you are approaching STM32 and ChibiOS. The LED can be considered the simplest peripheral output you can connect to a microcontroller. Because of that, usually, every embedded development board is equipped with a LED marked as “User LED” and this means that it is actually connected to a GPIO pin you can drive via software.
Requirements
To understand this article with proficiency you should match few requirements:
- You should have an STM32 development kit and be able to do basic stuff with ChibiStudio (e.g. launch the tool-chain; import, duplicate and create a new project; flash and run and similar). If you are not able to do that, you ought to take a look to From 0 to STM32, Developing on STM32: introducing ChibiStudio and A close look to ChibiOS demos for STM32.
- You should know how STM32 GPIO peripheral works and how to exploit this flexible Input\Output controller with ChibiOS’ PAL driver. In case you don’t, we have some good references here too (Using STM32 GPIO with ChibiOS PAL Driver).
Theory inside
The Light Emitting Diode

I am quite sure you perfectly know what a LED is and how it works. Anyway, let us do a quick recap. A LED is a diode which emits light when is fed with a current in a proper direction. Like diodes, LEDs have an anode and a cathode:
- the anode is the electrode through which the current flows-in; on a “through hole” LED, the anode is the longer wire;
- the cathode is the electrode through which the current flows out; on a “through hole” LED, the cathode is the shorter wire.
To make the current flow in the right direction, the anode shall be connected to the positive voltage and the cathode to the negative. It is important to notice that the LED light is proportional to its current and not to the voltage. This has important consequences. We will explore those in a later article about light dimming.
Basically, microcontrollers and other circuits work on voltage thus we will drive LED by voltage instead of current. Note that the higher is the voltage we put across the LED, the higher the current, the higher light intensity will be. Mind you, too much current would irreversibly break the device.
The thermal drift

Driving a LED by voltage instead of current introduces some problems as LEDs suffer from thermal drift. Applying a voltage across the LED, a current will flow in it; now, due to its working principle, the flow heats the LED and this lead to an increase of free carriers which means an increase of current itself, and thus again an increase of temperature which leads to carriers increase and so on until the current in the LED is so high that it is permanently damaged.
A LED cannot be directly connected to a voltage generator because it suffers thermal drift. By the thermal drift a LED can be permanently damaged.
The simplest solution to this problem is a series resistor. Considering the circuit in figure 2 the current flows from VCC across the resistor the LED reaching the ground. According to Kirchhoff Voltage Law, the total voltage provided by our generator is split between the LED and the resistor, i.e.
As before, the current flow heats the LED causing an increase of free carriers and an increase of current itself. Anyway, according to Ohm Law this lead to an increase of voltage drop of the resistor and subsequently a decrease of voltage drop on the LED which reduces the current flow. We can conclude that the resistor introduces such compensation which can limit current in case of thermal drift.
To avoid thermal drift, a LED shall be connected to a voltage generator with a properly sized series resistor.
How to size series resistor

The series resistor directly influences the LED current hence its emitted light intensity. A common problem dealing with LED is to decide which is the proper resistor size. This basically depends on the LED color and main voltage value.
To better understand how this works, we have to consider the V-I curve of a LED: it is basically an exponential curve where the shape depends on materials used to create the LED, and it exhibits a high deviation from LED to LED even if they belong to the same production batch.
To have proper knowledge of a LED, its curve shall be obtained doing measurement using laboratory test equipment. From this curve, one can obtain the LED turn-on voltage and the resistance that LED exhibits.
Considering figure 3, we can see how to get an equivalent model for a LED retrieving turn-on voltage and equivalent on resistance from its characteristic.
With reference to this example, we can spot that turn-on voltage is about 1.6V and that equivalent resistance (when the LED is ON) is

In figure 4, we have replaced LED with its equivalent circuit and now the circuit can be analyzed with ease. Considering known the desired current which should flow inside the LED we can use this model to size the external resistor.
Solving the Kirchoff Voltage Law for this circuit we have

I use to choose 10mA as current for LEDs because it is not quite far from being the maximum current a LED can manage but nonetheless, with this current, you can get a proper light intensity.
Looking at figure 5 we can spot that turn-on voltage is greater on a blue LED than on red LED: color has a great influence on how resistance shall be sized.
Another thing that influences a lot the selection is also the main power supply voltage level which in case of an embedded system often is 3.3V or 5V.
Doing some computation you would notice that the series resistance can be neglected because is ten/twenty time smaller than the resistance of the external resistor and can be easily confused with component tolerance.
In the next table, I am reporting some conceivable turn-on voltage, equivalent turn-on resistance and series resistance value when desired LED forward current is 10 mA and Vcc is 3.3 and 5 V.
Colour | Von [V] | Req [Ω] | Rs @10mA (Vcc = 3.3V) @ [Ω] | Rs @10mA (Vcc = 5.0V) [Ω] |
Infrared | 1.0 | 7.5 | 217.5 | 387.5 |
---|---|---|---|---|
Red | 1.6 | 10 | 160 | 330 |
Orange | 1.7 | 11.7 | 145 | 315 |
Green | 2.0 | 13.3 | 110 | 280 |
Yellow | 2.1 | 16.7 | 100 | 270 |
Blue | 2.3 | 16.7 | 80 | 250 |
White | 2.5 | 21.7 | 55 | 225 |
UV | 3.3 | 21.7 | impossible | 145 |
These values have been calculated taking figure 5 as a reference and using a simple spreadsheet to compute data
Connecting LED to an STM32
To drive a LED with an STM32 we have to replace the connection to Vcc or GND with a GPIO configured as a digital output.

This solution leads to two different configurations schematized in figure 6:
- The first configuration is usually called Active High because LED is on when GPIO is set in High Logical State (i.e. Vcc, usually 3.3V on an official STM32 development board).
- The second one is called Active Low because LED is on when GPIO is set in Low Logical State (i.e. GND)
Examples
Blinking a LED
Let us create a simple project with a single thread and a blinking LED.
The solution here depends on the development board we are going to use. Firstly we should understand whereas our development board has a LED connected to a GPIO and which kind of connection is implemented between the two proposed in figure 6.
Information related to our development board is on the board user manual. In here, we can usually find also schematics that show board connections. Actually, every development board is equipped with at least one user LED and the default demo already makes it blinking. For example, all the STM32 Nucleo-64 boards (Nucleo-F401RE, NUCLEO-F303RE, NUCLEO-L476RG) have the same schematic and are equipped with a green LED which is connected to Arduino D13 (i.e. PA5) in active high mode. Browsing the board files we could also notice that PA5 is already configured as OUTPUT PUSH PULL and this explain why how default demo can blink the LED apparently without configuring PA5.
Things are slightly different on other boards like as example STM32F3 Discovery where we have up to 8 user LEDs connected to PE8, PE9, PE10, PE11, PE12, PE13, PE14 and PE15. Anyway, even if things are different, again default demo blinks these LEDs and to reach our goal we could start duplicating the default demo and edit it.
As the goal is to create the simplest LED blinker, we should go for a single threaded solution e.g. using only the main() thread.
Thus let us start duplicating the default demo as shown into the Chapter 5 of the article “A close look to ChibiOS demos for STM32”. As a quick reference here a video which shows how to do that:
So let’s create a new project giving it a proper name, for instance, RT-STM32F401RE-NUCLEO64-Simple_blinker. After editing properly the makefile and debug configuration we can start to edit the main.c:
- removing the inclusion of ch_test.h, because actually, we will not use the test suite;
- removing the sdStart, because we are not going to use the serial driver which previously was used by the test suite;
- removing the chThdCreateStatic, because we are going for a single threaded solution and we do not want to create additional threads;
- editing the loop of the main with the loop of Thread1 which already blinks a LED;
- deleting the declaration of waThread1 and Thread1 which now are not necessary anymore;
- editing the comment within the code (not necessary but is a good habit).
Implementation
For an STM32 Nucleo-64, the main.c would look like
#include "ch.h" #include "hal.h" /* * Application entry point. */ int main(void) { /* * System initializations. * - HAL initialization, this also initializes the configured device drivers * and performs the board-specific initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ halInit(); chSysInit(); /* * Normal main() thread activity, in this demo it just blinks a LED. */ while (true) { palToggleLine(LINE_LED_GREEN); chThdSleepMilliseconds(500); } }

This code is valid for each STM32 Nucleo-64 because all these boards have a Green LED which line is named (LINE_LED_GREEN). This code could be also used on other development board by editing this line properly.
If you are not aware of what a “Line” is you should read again Using STM32’s GPIO with ChibiOS’ PAL Driver. As a quick reference, remember that all the lines related to your board are declared on the board.h file which is easily reachable through the linked folder inside your project as shown in Fig.1
The code we have written here blinks the LED with a period of 1 second and duty cycle of 50%. This because the palToggleLine() inverts the logic state of the line where the LED is connected and the chThdSleepMilliseconds() adds a pause between each toggle. The next figure is the timing diagram of the main thread as is.

Final remarks
We have learned an important lesson here: how to blink a LED. Usually, in every project I made, there is always a blinking LED because it is a cheap probe inside my firmware: if LED stops to blink the firmware has crashed!
We can still optimise our code: with our modification, we have deleted all the code related to Serial Driver and Test suite. This means we could completely disable all the code related to these parts.
It is possible to disable the Serial Driver acting on halconf.h:
/** * @brief Enables the SERIAL subsystem. */ #if !defined(HAL_USE_SERIAL) || defined(__DOXYGEN__) #define HAL_USE_SERIAL FALSE #endif
We should also exclude the Test Suite from the makefile reducing the footprint of our code. To do that go into the makefile and comment the entry related to test-suite (a comment in the makefile begins with hash key):
# Imported source files and paths CHIBIOS = ../../chibios176 # Startup files. include $(CHIBIOS)/os/common/ports/ARMCMx/compilers/GCC/mk/startup_stm32f4xx.mk # HAL-OSAL files (optional). include $(CHIBIOS)/os/hal/hal.mk include $(CHIBIOS)/os/hal/ports/STM32/STM32F4xx/platform.mk include $(CHIBIOS)/os/hal/boards/ST_NUCLEO_F401RE/board.mk include $(CHIBIOS)/os/hal/osal/rt/osal.mk # RTOS files (optional). include $(CHIBIOS)/os/rt/rt.mk include $(CHIBIOS)/os/rt/ports/ARMCMx/compilers/GCC/mk/port_v7m.mk # Other files (optional). # include $(CHIBIOS)/test/rt/test.mk
Changing LED speed
Let us edit the newly created project to change the blinking speed.
The suggestion here is to duplicate the newly created project before to edit it. This because we can use an old project as a kind of checkpoint: in case everything starts to go mad we can roll back and re-start from a point where everything was working. This is a good strategy, especially when we are taking our early steps and we are not so knowledgeable with the IDE.
Looking back at the previous timing diagram, we can notice that the timing is kept with “sleep”. Basically to change the speed we just need to act on the chThdSleepMillisecond value.
Implementation
For example, we can double the blinking speed halving the sleep time. For a generic STM32 Nucleo-64 the implementation would be
#include "ch.h" #include "hal.h" /* * Application entry point. */ int main(void) { /* * System initializations. * - HAL initialization, this also initializes the configured device drivers * and performs the board-specific initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ halInit(); chSysInit(); /* * Normal main() thread activity, in this demo it just blinks a LED. */ while (true) { palToggleLine(LINE_LED_GREEN); chThdSleepMilliseconds(250); } }
This code blinks the LED with a period of half second and duty cycle of 50% and the next figure is the timing diagram of the main thread as is now.

Final remarks
To conclude this exercise lets point out something about timing. In all the timing diagrams we have stated that the turn-on/turn-off of the Green LED is instantaneous. In reality, the change of the status of a digital output PIN involves some logic circuitry and this means that palTogglePad() requires a non-zero time to propagate the signal. This latency is much bigger the more is high the load. In the case of small loads like LEDs, the execution time is in the order of hundreds of nanoseconds. Thus, in our case, we can confidently say that the turn-on/turn-off of the LED is almost instantaneous.
In the blinker thread what beats the time is the thread sleep. Except in case of extreme CPU load or tight scheduling, the timing provided by a ChibiOS’ sleep is extremely reliable: if the blinker thread is the thread with the highest priority and sleeps are in the order of few milliseconds, even in case of extreme CPU load its schedule could be reliable.
If you are interested in this argument you should absolutely walk through this further reading about scheduling and multi-threading.
Changing the duty cycle
Let us edit the newly created project to change the blinking duty cycle.
Looking back to previous examples we can notice that in both cases we had a 50% duty cycle. This because turn-on and turn-off are timed in the same way.
The duty cycle is a concept which is specific of square waves we are going to deepen in an article related to PWM. For now, let us focus on common sense: the duty cycle is a percentage representing the fraction of a period in which the signal is active. In our case since the LED is active on the high side the duty cycle is
Let us assume we would keep the period to 1 second but change the duty cycle from 50% to 25%. This means the LED should be turned on for 250 ms and turned off for 750 ms. In this way the duty would be
Implementation
To implement this solution we have to slightly change the code in the main loop replacing the palToggleLine. This would allow us to assign different pauses after turn-on and turn-off. For a generic STM32 Nucleo-64 it would sound like this
#include "ch.h" #include "hal.h" /* * Application entry point. */ int main(void) { /* * System initializations. * - HAL initialization, this also initializes the configured device drivers * and performs the board-specific initializations. * - Kernel initialization, the main() function becomes a thread and the * RTOS is active. */ halInit(); chSysInit(); /* * Normal main() thread activity, in this demo it just blinks a LED. */ while (true) { palClearLine(LINE_LED_GREEN); chThdSleepMilliseconds(750); palSetLine(LINE_LED_GREEN); chThdSleepMilliseconds(250); } }

This code works as expected only if LED is connected as active high or, in other words, the LED stands lighted during the high phase of the signal.
Looking at figure 4 we can see that the LD2 which is the green LED has its cathode connected to the ground and its anode connected in series with a 510Ω resistor to the PA5 (alias Arduino D13). Comparing this scheme with those shown in figure 6 we can notice that this is an active high configuration: palSet means turn-on, palClear means turn-off.
What follows is the timing diagram of the main loop for this example.

Exercises
To complete this article let me propose you some assignment you should do on your own.
Connecting an external LED
Connect an external LED to your board as active high and make it blinks
Hints
- Read the user manual check the schematic, be sure that the GPIO you are going use is connected to anything else than your LED;
- Be sure to add a properly sized series resistor to the LED, in case you are not sure of your calculation to use a slightly bigger resistor: the LED would be less bright but would not break down.
- Configure the GPIO as OUTPUT PUSH PULL
Blink two LEDs in counter-phase
Blink the on board LED and the external LED alternatively (e.g. when one is on the other is off and vice versa)
Hints
- Start from exercise 4.1
- You can do all with a single-threaded solution.
LED dimming
Reduce the LED bright using only the PAL Driver
Hints
- Read about the persistence of vision
- It is related to the duty cycle
- ChibiOS allows sleeps in the order of hundreds of microseconds using the API chThdSleepMicroseconds().
Sir, Thank you for the article.
but sir how to test our code when we don’t have development board. Any simulation software?
I suggest you to buy a development board as it costs something like 10$. Anyway, there is the windows simulator which is used for code coverage purpose. If you need more info about simulator I suggest you to ask on ChibiOS forum as my knowledge about that is extremely poor.
Is it possible to have a blinking led, lets say 50ms on and 600ms off without using cpu demanding delay functions so that I can run other tasks while led is blinking?
Hello pauledd,
the chTHdSleepMillisecond is a non blocking function: this means that while the thread invoking the function is going to sleep, the CPU is free to serve other threads. So, any of the examples here, should do what your aiming to.
If you want to know more about the topic take a look to this article about Multi-threading
so table say green led should use 110ohm resistor but schematic uses 510?
Is that to really reduce the current and light intensity? Seems a bit odd if you want to dim with PWM anyway then I would expect the max current to be closer to using the 110ohm.
Or is it a different LED?
The LED in the schematic is an SMD LED, maybe the max current for such an LED is lower.
It is possible the designers calculated the resistor for a lower current in order to reduce the overall current consumption of the entire board.
Quick math suggest the current for such a configuration would be 2.5 mA