Introduction
If you’re starting out with embedded development using STM32 microcontrollers, a classic beginner project is to toggle an LED using a push-button. It may sound simple, but it’s a perfect opportunity to introduce some key C++ concepts — like classes, encapsulation, and abstraction — all while interacting directly with hardware.
In this post, we’ll explore how to:
- Set up a basic STM32 project using PlatformIO with Visual Studio Code
- Control an LED and read a button input
- Wrap the functionality in simple C++ classes
- Understand how object-oriented programming helps organize embedded code
This tutorial assumes you have an STM32 Nucleo board (such as Nucleo-L433C-P or similar), but you can adapt the principles to most STM32 boards.
Why Use C++ for Embedded?
C++ brings features like classes and abstraction, making code more modular and easier to maintain. While C is common in embedded, modern microcontrollers can comfortably run lightweight C++ code if used carefully.
Using C++ doesn’t mean sacrificing performance; it’s about writing clearer code while keeping full control over low-level hardware.
Project Setup
For this project, we'll use:
- PlatformIO: a cross-platform build system for embedded development
- Visual Studio Code: an editor with great integration
- STM32Cube HAL: the STM32 Hardware Abstraction Layer to handle low-level details
Create the Project
- Install PlatformIO in VS Code.
- Use PlatformIO to create a new project, choosing your board (e.g.,
nucleo_l433rc_p
). - Make sure to select the framework as
stm32cube
.
Hardware: LED and Button
Most Nucleo boards have a built-in LED (for Nucleo L433RC P board, it's PB13) and a user button (e.g., PC13
).
We'll use these to:
- Turn the LED on/off when the button is pressed.
- Learn how to write this logic cleanly in C++.
Writing the Code
Step 1: Blink an LED (Procedural Style)
Let’s first make sure we can blink an LED:
#include "stm32l4xx.h"
#define LED_PIN GPIO_PIN_13
#define LED_GPIO_PORT GPIOB
#define LED_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
int main(void)
{
HAL_Init();
LED_GPIO_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.Pin = LED_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(LED_GPIO_PORT, &GPIO_InitStruct);
while (1)
{
HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_PIN);
HAL_Delay(1000);
}
}
extern "C" {
void SysTick_Handler(void) {
HAL_IncTick();
}
void NMI_Handler(void) {
}
void HardFault_Handler(void) {
while (1) {}
}
void MemManage_Handler(void) {
while (1) {}
}
void BusFault_Handler(void) {
while (1) {}
}
void UsageFault_Handler(void) {
while (1) {}
}
void SVC_Handler(void) {
}
void DebugMon_Handler(void) {
}
void PendSV_Handler(void) {
}
}
This is a typical C-style program: initialize, loop, toggle. It works, but as projects grow, this can get messy.
Step 2: Introduce a C++ LED Class
Let’s wrap the LED logic into a C++ class. This shows:
- Encapsulation: hiding details inside a class
- Constructor: to initialize the LED
- Method: to toggle the LED
// LED.h
#ifndef LED_H
#define LED_H
#include "stm32l4xx.h"
class LED {
public:
LED(GPIO_TypeDef* port, uint16_t pin);
void toggle();
void on();
void off();
private:
GPIO_TypeDef* _port;
uint16_t _pin;
};
#endif
// LED.cpp
#include "LED.h"
LED::LED(GPIO_TypeDef* port, uint16_t pin) : _port(port), _pin(pin) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
if (port == GPIOA) __HAL_RCC_GPIOA_CLK_ENABLE();
if (port == GPIOB) __HAL_RCC_GPIOB_CLK_ENABLE();
if (port == GPIOC) __HAL_RCC_GPIOC_CLK_ENABLE();
HAL_GPIO_Init(port, &GPIO_InitStruct);
}
void LED::toggle() {
HAL_GPIO_TogglePin(_port, _pin);
}
void LED::on() {
HAL_GPIO_WritePin(_port, _pin, GPIO_PIN_SET);
}
void LED::off() {
HAL_GPIO_WritePin(_port, _pin, GPIO_PIN_RESET);
}
Step 3: Add a Button Class
Let’s do the same for reading a button input.
// Button.h
#ifndef BUTTON_H
#define BUTTON_H
#include "stm32l4xx.h"
class Button {
public:
Button(GPIO_TypeDef* port, uint16_t pin);
bool isPressed();
private:
GPIO_TypeDef* _port;
uint16_t _pin;
};
#endif
// Button.cpp
#include "Button.h"
Button::Button(GPIO_TypeDef* port, uint16_t pin) : _port(port), _pin(pin) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
if (port == GPIOA) __HAL_RCC_GPIOA_CLK_ENABLE();
if (port == GPIOB) __HAL_RCC_GPIOB_CLK_ENABLE();
if (port == GPIOC) __HAL_RCC_GPIOC_CLK_ENABLE();
HAL_GPIO_Init(port, &GPIO_InitStruct);
}
bool Button::isPressed() {
return HAL_GPIO_ReadPin(_port, _pin) != GPIO_PIN_RESET; // active high
}
Step 4: Main Logic
Now we can write the main logic clearly:
#include "stm32l4xx.h"
#include "LED.h"
#include "Button.h"
int main(void)
{
HAL_Init();
LED led(GPIOB, GPIO_PIN_13); // Built-in LED
Button button(GPIOC, GPIO_PIN_13); // User button
while (1)
{
if (button.isPressed()) {
led.toggle();
HAL_Delay(200); // simple debounce
}
}
}
extern "C" {
void SysTick_Handler(void) {
HAL_IncTick();
}
void NMI_Handler(void) {
}
void HardFault_Handler(void) {
while (1) {}
}
void MemManage_Handler(void) {
while (1) {}
}
void BusFault_Handler(void) {
while (1) {}
}
void UsageFault_Handler(void) {
while (1) {}
}
void SVC_Handler(void) {
}
void DebugMon_Handler(void) {
}
void PendSV_Handler(void) {
}
}
Notice how the logic is easier to follow:
- The hardware initialization is inside the classes.
- The main loop just describes what we want: if pressed, toggle LED.
Explaining the C++ Concepts
By wrapping hardware in classes, we:
- Use encapsulation: keep low-level details inside the class
- Use constructors to initialize the hardware when creating an object
- Use methods to act on the hardware (like
toggle()
) - Make our code more readable and maintainable
This also makes it easier to expand: add more LEDs or buttons by creating new objects.
Tips for Using C++ in Embedded
- Avoid dynamic memory (
new
,delete
) unless you really need it. - Prefer lightweight classes and inline functions.
- Use
const
and references to avoid unnecessary copies. - Still rely on the HAL or low-level libraries for actual register control.
Conclusion
Toggling an LED with a button may seem simple, but it’s a great project to:
- Learn how STM32 HAL initializes GPIO
- Discover why C++ helps structure embedded code
- Practice object-oriented design with hardware
With these basics, you’re ready to tackle more advanced projects like PWM, ADC, UART, or even RTOS tasks — all while keeping your code organized and modular.