STM32 GPIO LED Blink with HAL on Nucleo-F401RE and Blue Pill

Blinking an LED is the hardware version of Hello World, but on STM32 it teaches more than it seems. You learn how GPIO ports are named, why each port needs its clock enabled, what the output modes mean, and how the HAL functions map onto real registers. In this tutorial you will blink the onboard LED of a Nucleo-F401RE and a Blue Pill, then add an external LED and run a simple pattern on it.

How STM32 GPIO Works

STM32 pins are grouped into ports named GPIOA, GPIOB, GPIOC and so on, each with up to 16 pins numbered 0 to 15. A pin is referred to by its port and number, for example PA5 or PC13. Every port is a peripheral with its own clock; after reset these clocks are off to save power, so the first step is always to enable the port clock. CubeMX does this for you with macros such as __HAL_RCC_GPIOA_CLK_ENABLE().

Each pin can be configured as input, output, alternate function (connected to a peripheral such as a UART or timer) or analog. Outputs have further options, summarised below.

OptionHAL valueMeaning
Push-pull outputGPIO_MODE_OUTPUT_PPPin actively drives both high (3.3 V) and low (0 V). Use for LEDs and logic signals.
Open-drain outputGPIO_MODE_OUTPUT_ODPin only pulls low; high is left floating and needs a pull-up. Used for shared lines such as I2C.
Pull-up / pull-downGPIO_PULLUP / GPIO_PULLDOWN / GPIO_NOPULLInternal resistor that sets the level when nothing drives the pin.
SpeedGPIO_SPEED_FREQ_LOW up to VERY_HIGHEdge rate of the output driver. Low speed is enough for LEDs and reduces noise.

Hardware and Pins

BoardOnboard LEDLED is on whenExternal LED pin used here
Nucleo-F401RELD2 (green) on PA5PA5 is highPA10 (Arduino D2)
Blue PillLED on PC13PC13 is lowPA0

For the external LED, connect the pin to a 330 ohm resistor, the resistor to the LED anode (longer leg), and the LED cathode to GND. At 3.3 V with a red LED dropping about 2 V, the current is roughly (3.3 - 2.0) / 330, which is about 4 mA. That is well inside the safe limits of an STM32 pin and still clearly visible.

Configuring the Pins in CubeMX

  1. Open the .ioc file of a Nucleo-F401RE project. PA5 is already set as GPIO_Output with the label LD2.
  2. Click PA10 in the pinout view and choose GPIO_Output. Right-click it, choose Enter User Label, and type EXT_LED.
  3. Under System Core, GPIO, select PA10 and check: Output level Low, Mode Output Push Pull, No pull-up and no pull-down, Maximum output speed Low.
  4. For a Blue Pill project, set PC13 and PA0 as GPIO_Output, enable SYS Debug as Serial Wire, and under RCC set High Speed Clock to Crystal/Ceramic Resonator to use the 8 MHz crystal.
  5. Save to generate code. CubeMX creates the macros LD2_Pin, LD2_GPIO_Port, EXT_LED_Pin and EXT_LED_GPIO_Port in main.h.

The Blink Code

The program toggles LD2 every 500 ms and runs a short double-flash pattern on the external LED. Only the USER CODE sections of main.c are shown; CubeMX generates the rest.

main.c (Nucleo-F401RE): blink LD2 and flash an external LED on PA10
/* USER CODE BEGIN PFP */
static void ext_led_double_flash(void);
/* USER CODE END PFP */

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();          /* enables GPIOA clock and configures PA5, PA10 */

  /* USER CODE BEGIN 2 */
  uint32_t last_toggle = HAL_GetTick();
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE BEGIN 3 */
    /* Toggle LD2 every 500 ms without blocking the loop */
    if (HAL_GetTick() - last_toggle >= 500)
    {
      last_toggle = HAL_GetTick();
      HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);

      /* Run the external LED pattern once every time LD2 turns on */
      if (HAL_GPIO_ReadPin(LD2_GPIO_Port, LD2_Pin) == GPIO_PIN_SET)
      {
        ext_led_double_flash();
      }
    }
  }
  /* USER CODE END 3 */
}

/* USER CODE BEGIN 4 */
/* Two short flashes: on 80 ms, off 80 ms, on 80 ms, off.
   This blocks for 240 ms, which is acceptable for a demo. */
static void ext_led_double_flash(void)
{
  for (int i = 0; i < 2; i++)
  {
    HAL_GPIO_WritePin(EXT_LED_GPIO_Port, EXT_LED_Pin, GPIO_PIN_SET);
    HAL_Delay(80);
    HAL_GPIO_WritePin(EXT_LED_GPIO_Port, EXT_LED_Pin, GPIO_PIN_RESET);
    if (i == 0) HAL_Delay(80);
  }
}
/* USER CODE END 4 */

For the Blue Pill, the simplest blink looks like this. Note that writing GPIO_PIN_RESET turns the LED on because it is wired between 3.3 V and PC13.

Blue Pill main loop: active-low LED on PC13
  while (1)
  {
    /* USER CODE BEGIN 3 */
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); /* LED on  */
    HAL_Delay(200);
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);   /* LED off */
    HAL_Delay(800);
    /* USER CODE END 3 */
  }

What the HAL Calls Do

  • HAL_GPIO_WritePin(port, pin, state) writes to the port's BSRR register. Setting a bit in the lower half drives the pin high and setting a bit in the upper half drives it low, in one atomic write, so an interrupt can never corrupt another pin on the same port.
  • HAL_GPIO_TogglePin(port, pin) reads the current output state from ODR and flips the requested pin.
  • HAL_GPIO_ReadPin(port, pin) reads the IDR register and returns GPIO_PIN_SET or GPIO_PIN_RESET. It works on output pins too, returning the actual pin level.
  • HAL_Delay(ms) waits by watching the 1 ms tick counter. It is blocking: nothing else in the main loop runs while it waits.
  • HAL_GetTick() returns the tick count. Comparing HAL_GetTick() - last against an interval is the non-blocking pattern, and the subtraction stays correct even when the 32-bit counter wraps after about 49.7 days.

Look at MX_GPIO_Init() in the generated code. It enables the GPIOA clock, sets the initial output level with HAL_GPIO_WritePin, fills a GPIO_InitTypeDef structure with Pin, Mode, Pull and Speed, and passes it to HAL_GPIO_Init(). Reading this function is the quickest way to learn how to configure pins by hand later.

Common Mistakes

  • LED never lights: the LED is reversed, or the resistor is in the wrong breadboard row. Test the LED by connecting it to 3.3 V through the resistor.
  • Blue Pill LED behaves inverted: remember PC13 is active low.
  • Code compiles but nothing happens after flashing: press the black reset button, or enable Reset and Run in the run configuration.
  • Pin works in one project but not another: the port clock is not enabled because the pin was configured by hand without __HAL_RCC_GPIOx_CLK_ENABLE().
  • Using HAL_Delay inside an interrupt handler: the tick will not advance if the SysTick priority is lower than the current interrupt, and the program hangs.

Summary

You configured GPIO outputs in CubeMX, drove them with HAL_GPIO_WritePin and HAL_GPIO_TogglePin, and used HAL_GetTick for non-blocking timing. You also saw why PC13 on the Blue Pill is active low and why pin current limits matter. Next, you will read a push button using an EXTI interrupt instead of polling.