Series / Working with STM32 Peripherals / Blinking an LED on STM32 Without Magic

Blinking an LED on STM32 Without Magic

Blink the Nucleo L433RC-P user LED by identifying the board pin, enabling the GPIO peripheral clock, configuring output mode, and toggling the pin deliberately.

On this page

The previous article toured the STM32 Nucleo L433RC-P from a peripheral-project point of view. It identified the board features that matter before firmware touches real pins.

This article creates the first visible behavior in the series: blinking the user LED.

The goal is not just to paste a blink example. The goal is to understand the minimum pieces that make a GPIO output work on STM32: the board pin, the peripheral clock, the pin mode, and the output state.

What Without Magic Means

Without magic does not mean avoiding STM32 HAL entirely.

In this article, it means the code should make the important steps visible:

  • Identify which MCU pin is connected to the LED.
  • Enable the clock for that GPIO port.
  • Configure the pin as an output.
  • Drive or toggle the pin.
  • Build, flash, and debug the exact firmware image being tested.

HAL can still be useful, but it should not hide the mental model. If the LED does not blink, you should know which assumptions to check instead of guessing.

Verify the LED Pin First

Before writing code, verify the user LED connection for the exact Nucleo L433RC-P board revision you are using.

Use the board user manual or schematic to answer two questions:

  • Which STM32 GPIO port and pin drives the user LED?
  • Is the LED active high or active low?

Many Nucleo examples use names such as LD2, but firmware needs the MCU signal behind that board label, such as a GPIOx port and GPIO_PIN_y pin.

Do not assume another Nucleo board uses the same pin. Nucleo board names are similar, but user LED mappings can differ.

For the code below, replace the LED port and pin macros with the values verified from your board documentation.

GPIO Output Mental Model

To drive an STM32 GPIO output, several things must be true.

First, the GPIO peripheral clock must be enabled. STM32 peripherals are usually clock-gated to save power. If the GPIO port clock is disabled, writes to its configuration registers will not have the intended effect.

Second, the pin must be configured for output mode. A reset pin state may be analog, input, or another default state depending on the part and startup path. Firmware should configure the mode it needs.

Third, the output type should match the circuit. A normal LED driven by the MCU usually uses push-pull output. Open-drain output is useful for buses and wired-logic behavior, but it is not the default choice for a simple LED blink.

Fourth, pull-up and pull-down settings should be deliberate. For a driven output, no internal pull is usually appropriate unless the board circuit requires something specific.

Fifth, the output speed should be reasonable. Blinking a user LED does not need a high-speed GPIO edge setting.

Finally, the firmware must write the output state. That can mean setting, resetting, or toggling the pin.

Assuming the previous series' CubeMX/HAL integration is available, a minimal main.c can look like this:

#include "stm32l4xx_hal.h"

#define USER_LED_GPIO_PORT GPIOA
#define USER_LED_PIN GPIO_PIN_5

static void user_led_init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = USER_LED_PIN;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;

    HAL_GPIO_Init(USER_LED_GPIO_PORT, &gpio);
}

int main(void)
{
    HAL_Init();
    user_led_init();

    while (1)
    {
        HAL_GPIO_TogglePin(USER_LED_GPIO_PORT, USER_LED_PIN);
        HAL_Delay(500);
    }
}

The GPIOA and GPIO_PIN_5 values are examples. Use the port and pin verified for your board. If the LED is active low, toggling still blinks it, but explicit on/off code will need inverted logic.

What Each Piece Does

The HAL header provides the STM32 HAL declarations and device-specific GPIO names:

#include "stm32l4xx_hal.h"

The LED macros put the board mapping in one place:

#define USER_LED_GPIO_PORT GPIOA
#define USER_LED_PIN GPIO_PIN_5

Later, this mapping should probably move into board support code. For the first blink, keeping it near the code makes the dependency obvious.

The clock-enable macro turns on the GPIO port clock:

__HAL_RCC_GPIOA_CLK_ENABLE();

This macro must match the selected port. If the LED is on GPIOB, enable the GPIOB clock instead. Enabling the wrong GPIO port clock is a common reason a pin configuration appears to do nothing.

The GPIO_InitTypeDef structure describes the pin configuration:

GPIO_InitTypeDef gpio = {0};
gpio.Pin = USER_LED_PIN;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;

GPIO_MODE_OUTPUT_PP selects push-pull output. GPIO_NOPULL avoids internal pull resistors. GPIO_SPEED_FREQ_LOW is enough for an LED.

HAL_GPIO_Init writes the GPIO configuration registers for the selected port:

HAL_GPIO_Init(USER_LED_GPIO_PORT, &gpio);

HAL_GPIO_TogglePin changes the output state:

HAL_GPIO_TogglePin(USER_LED_GPIO_PORT, USER_LED_PIN);

HAL_Delay(500) waits about 500 milliseconds when the HAL time base is working:

HAL_Delay(500);

For early board bring-up, that is good enough. Later timer articles will replace vague delay loops with a more deliberate timing discussion.

Build and Flash

Build the firmware from the project root:

cmake --build build

Then flash it with the same OpenOCD workflow from the tooling series:

openocd -f interface/stlink.cfg -f target/stm32l4x.cfg -c "program build/firmware.elf verify reset exit"

If the LED starts blinking, the board mapping, GPIO clock, pin configuration, output write, build, and flash path are all working well enough for this first peripheral example.

If the LED does not blink, use the debugger before changing random settings.

Start the VSCode debug configuration from the previous series and stop in main.

Useful checks:

  • Confirm execution reaches user_led_init().
  • Step over the GPIO clock enable.
  • Step over HAL_GPIO_Init.
  • Put a breakpoint on HAL_GPIO_TogglePin.
  • Confirm the loop keeps running.
  • Confirm the firmware.elf path is the one you just built.

If the code reaches the toggle call repeatedly, the build and control flow are probably fine. The remaining likely causes are board pin mapping, GPIO clock selection, LED polarity, or hardware assumptions.

Common Mistakes

If the LED pin is wrong, the firmware may successfully toggle a different pin while the visible LED never changes.

If the GPIO port clock is not enabled, the pin configuration may not take effect. Make sure the clock-enable macro matches the LED port.

If the HAL GPIO driver source file is missing from CMake, the project may fail to link with undefined HAL_GPIO_Init or HAL_GPIO_TogglePin symbols.

If USE_HAL_DRIVER is missing, HAL-related declarations may not be enabled correctly through the STM32 headers.

If HAL_Delay never returns, the HAL time base is not running as expected. Check SysTick setup, interrupt handlers, and whether HAL_Init() completed.

If the LED appears inverted, check whether the board circuit is active high or active low. Active-low LEDs turn on when the pin is driven low.

If nothing changes after editing code, make sure the firmware was rebuilt and the debugger or flash command is using the current build/firmware.elf.

What This Proves

A blinking LED proves more than "the LED works."

It proves that:

  • The firmware builds and flashes.
  • The program reaches main.
  • HAL initialization is good enough for this example.
  • The GPIO port clock can be enabled.
  • The selected pin can be configured as an output.
  • The MCU can drive a board-level signal.

That is the first visible peripheral milestone.

What This Does Not Prove

This example does not prove the full clock tree is understood.

It does not prove timer behavior, interrupt behavior, UART output, or external module wiring. It also does not turn GPIO into a reusable driver yet.

That is intentional. The first LED blink should stay small enough that failures are easy to isolate.

Next Steps

The next article reads the user button on STM32.

That will use the same board-oriented approach, but it will introduce GPIO input behavior, pull configuration, active logic level, polling, and simple debounce.