On this page
The previous article configured a GPIO output and blinked the user LED. That proved the firmware could drive a visible board signal.
This article goes the other direction: read the user button as a GPIO input.
Inputs are slightly less obvious than outputs. A button is not just a pin that magically becomes true or false. The firmware needs to know which MCU pin is connected, whether the pressed state is high or low, whether the circuit needs a pull-up or pull-down, and how to avoid treating one physical press as many rapid presses.
What This Article Covers
This article covers:
- Verifying the user button pin from board documentation.
- Configuring a GPIO pin as an input.
- Reading the raw pin state.
- Converting the electrical level into a logical pressed/released value.
- Adding a simple debounce check.
- Using the LED from the previous article as visible feedback.
It does not use interrupts yet. Polling is easier to reason about for the first button example. Interrupt-driven input is useful, but it adds NVIC configuration, interrupt handlers, shared state, and debounce tradeoffs that deserve separate treatment.
Verify the Button Pin
Before writing code, check the Nucleo L433RC-P user manual or schematic for the user button connection.
You need to know:
- The MCU GPIO port.
- The MCU GPIO pin.
- Whether pressing the button drives the pin high or low.
- Whether the board already includes an external pull-up or pull-down.
- Whether firmware should enable an internal pull-up, pull-down, or no pull.
Board labels such as B1 or USER are useful on the PCB, but firmware needs the MCU-level signal mapping.
The code below uses example macros. Replace them with the port, pin, and polarity verified for your board.
GPIO Input Mental Model
A GPIO input has fewer configuration pieces than an output, but the important assumptions are different.
First, the GPIO port clock must be enabled. The input pin belongs to a GPIO peripheral block, and that block needs a clock before configuration and reads are reliable.
Second, the pin must be configured as an input. The pin should not still be in an output, analog, or alternate-function mode from earlier code.
Third, pull configuration matters. A button input must not float. A floating input can randomly read high or low because nothing is forcing the voltage to a known idle state.
Fourth, the raw electrical level must be converted into logical meaning. GPIO_PIN_SET does not always mean pressed. If the button circuit is active low, pressed means the pin reads reset.
Fifth, mechanical contacts bounce. A single physical press can produce several fast transitions before settling.
Minimal Button Read
This example keeps the LED from the previous article and adds a button input.
Use verified values for the LED and button macros before testing on hardware:
#include "stm32l4xx_hal.h"
#include <stdbool.h>
#define USER_LED_GPIO_PORT GPIOA
#define USER_LED_PIN GPIO_PIN_5
#define USER_BUTTON_GPIO_PORT GPIOC
#define USER_BUTTON_PIN GPIO_PIN_13
#define USER_BUTTON_PRESSED_STATE GPIO_PIN_SET
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);
}
static void user_button_init(void)
{
__HAL_RCC_GPIOC_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = USER_BUTTON_PIN;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(USER_BUTTON_GPIO_PORT, &gpio);
}
static bool user_button_is_pressed(void)
{
GPIO_PinState state = HAL_GPIO_ReadPin(USER_BUTTON_GPIO_PORT, USER_BUTTON_PIN);
return state == USER_BUTTON_PRESSED_STATE;
}
int main(void)
{
HAL_Init();
user_led_init();
user_button_init();
while (1)
{
if (user_button_is_pressed())
{
HAL_GPIO_WritePin(USER_LED_GPIO_PORT, USER_LED_PIN, GPIO_PIN_SET);
}
else
{
HAL_GPIO_WritePin(USER_LED_GPIO_PORT, USER_LED_PIN, GPIO_PIN_RESET);
}
}
}
The button values above are examples. The USER_BUTTON_PRESSED_STATE macro is deliberately visible because active polarity is one of the easiest button assumptions to get wrong.
Explain the Button Pieces
The button clock-enable macro must match the button port:
__HAL_RCC_GPIOC_CLK_ENABLE();
If the button is on another port, this macro must change. Enabling the LED port clock does not automatically enable every GPIO port.
The input configuration uses GPIO_MODE_INPUT:
gpio.Mode = GPIO_MODE_INPUT;
The pull setting depends on the board circuit:
gpio.Pull = GPIO_NOPULL;
GPIO_NOPULL is correct only if the board already gives the input a defined idle level. If the schematic shows that the button needs an internal pull-up or pull-down, use GPIO_PULLUP or GPIO_PULLDOWN instead.
The read function keeps raw electrical state separate from logical button meaning:
GPIO_PinState state = HAL_GPIO_ReadPin(USER_BUTTON_GPIO_PORT, USER_BUTTON_PIN);
return state == USER_BUTTON_PRESSED_STATE;
That separation matters. The rest of the application should ask whether the button is pressed, not whether a raw pin happens to be high.
Add Simple Debounce
Mechanical buttons do not switch cleanly from released to pressed. The contacts can bounce for a few milliseconds, producing several transitions.
For a first implementation, use a simple stable-read check:
static bool user_button_is_pressed_debounced(void)
{
if (!user_button_is_pressed())
{
return false;
}
HAL_Delay(30);
return user_button_is_pressed();
}
Then use it in the loop:
while (1)
{
if (user_button_is_pressed_debounced())
{
HAL_GPIO_TogglePin(USER_LED_GPIO_PORT, USER_LED_PIN);
while (user_button_is_pressed())
{
HAL_Delay(10);
}
}
}
This code toggles the LED once per press and then waits for release.
It is intentionally simple. It blocks while the button is held, so it is not a final architecture for firmware that must do several things at once. For the first button article, it makes the bounce problem visible without introducing timers, interrupts, or state machines too early.
Build and Flash
Build the firmware:
cmake --build build
Flash it through OpenOCD:
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg -c "program build/firmware.elf verify reset exit"
Press the user button. The LED should react according to the final loop behavior you chose: either follow the button state or toggle once per debounced press.
Debug the Button Read
If the LED does not respond, use the debugger before changing multiple things at once.
Useful checks:
- Stop in
mainand confirmuser_button_init()runs. - Set a breakpoint inside
user_button_is_pressed(). - Inspect the raw
GPIO_PinStatevalue. - Press and release the button while stopped at repeated reads.
- Confirm the raw state changes.
- Confirm
USER_BUTTON_PRESSED_STATEmatches the actual pressed level. - Confirm the loop reaches the LED write or toggle code.
If the raw pin state never changes, suspect the board pin mapping, GPIO port clock, input mode, pull configuration, or board variant.
If the raw state changes but the logic is backward, suspect active polarity.
If the LED toggles several times per press, suspect bounce or repeated loop execution while the button is held.
Common Mistakes
If the button pin is wrong, the code may read a valid GPIO pin that never changes when the physical button is pressed.
If the active polarity is wrong, released may be interpreted as pressed and pressed may be interpreted as released.
If the GPIO port clock is wrong, input configuration and reads may not behave as expected.
If the input is left floating, the value may appear random. Check the schematic and choose the correct pull configuration.
If there is no debounce, one press may be counted as several presses.
If debounce blocks for too long, the firmware may feel unresponsive once more tasks are added.
If the LED feedback does not match the code, confirm the LED polarity too. A button bug can be confused with an LED output assumption.
If behavior does not change after editing, rebuild and verify the flashed or debugged firmware.elf is current.
What This Proves
Reading the user button proves that the firmware can observe a board-level input, not only drive an output.
It also proves that the project can handle the basic input assumptions needed for later peripherals:
- Correct pin mapping.
- Correct GPIO port clock.
- Correct input mode.
- Correct pull behavior.
- Correct active-level interpretation.
- Basic filtering of noisy physical input.
That is a useful step toward real firmware, where external state is rarely as clean as a variable in memory.
What This Does Not Prove
This does not prove interrupt handling.
It does not prove low-power wakeup behavior, event queues, long-term debounce architecture, or timer-based scheduling. It also does not prove that every GPIO input on the board should use the same pull or polarity pattern.
The point is narrower: read one real button correctly and understand each assumption involved.
Next Steps
The next article explains STM32 clocks enough to build projects.
GPIO examples can appear simple because the default clock configuration is often good enough for LED and button work. Timers, UART, I2C, and SPI make clock assumptions much more visible, so the next step is to understand the clock tree at a practical level.