Toggling LEDs with Buttons Using STM32 and C++

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

  1. Install PlatformIO in VS Code.
  2. Use PlatformIO to create a new project, choosing your board (e.g., nucleo_l433rc_p).
  3. 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

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.