Part 23: C Language Button Polling — Making a Button Control an LED for the First Time
In the previous four articles, we discussed everything from circuit principles to the GPIO input APIs in the HAL library. Now it is time to tie all this knowledge together and write a program that actually runs.
The goal of this article is straightforward: Use pure C to write a complete button-controlled LED program, flash it to the board, and see with your own eyes how severe mechanical bounce really is. No debounce, no tricks—just the most primitive "read pin → write pin". Only by seeing the problem first can we understand why we need to solve it later.
1. Complete C Code
Let's put aside debounce and state machines for now—our goal today is to wire the circuit, get the code right, and make the LED follow the button. Let's get things moving before we talk about optimization.
Hardware Wiring Review
| Pin | Function | Connection |
|---|---|---|
| PA0 | Button Input | One end to GND, the other end to PA0 |
| PC13 | LED Output | On-board LED (Active Low, lights up at Low) |
PA0 is configured as pull-up input mode. When the button is not pressed, the pull-up resistor pulls PA0 to a high level; when the button is pressed, PA0 is shorted directly to GND, reading a low level.
Complete Code
Below is a complete, compilable, and flashable .c file. Every line is commented to ensure you know what is happening at each step.
Expand (63 lines)Collapse
#include "main.h"
// Define the button and LED objects
// Note: The port must match the hardware schematic (PA0 for button, PC13 for LED)
// The Pin number matches the pin definition in the HAL module (GPIO_PIN_0, GPIO_PIN_13)
GPIO_InitTypeDef GPIO_InitStruct = {0};
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
int main(void) {
// 1. Reset all peripherals, initialize the Flash interface, and the Systick.
HAL_Init();
// 2. Configure the system clock
SystemClock_Config();
// 3. Initialize all configured peripherals
MX_GPIO_Init();
// Main loop
while (1) {
// Read the button state
// HAL_GPIO_ReadPin returns 0 or 1 (GPIO_PIN_RESET or GPIO_PIN_SET)
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// Button is pressed (PA0 is Low)
// Turn on LED (PC13 is Low)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
} else {
// Button is released (PA0 is High)
// Turn off LED (PC13 is High)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
}
}
}
void SystemClock_Config(void) {
// Clock configuration code omitted for brevity
// Usually generated by STM32CubeMX
}
static void MX_GPIO_Init(void) {
// Enable GPIO Clocks
// We need to enable the clock for Port A (Button) and Port C (LED)
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// Configure PA0 as Input (Button)
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // Enable internal pull-up resistor
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// Configure PC13 as Output (LED)
GPIO_InitStruct.Pin = GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // Push-pull output
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// Initialize LED state to Off (High level)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
The code structure is very clear: initialize the clock, configure pins, and repeatedly read the button status in the main loop. There is no debounce logic, just the most straightforward polling.
If you aren't familiar with parameters like
GPIO_MODE_INPUTorGPIO_PULLUP, look back at Part 04 for a detailed API explanation.
2. Flash and Run: Looks Normal... Really?
Compile and flash the code to the board. Hold the button—the LED lights up. Release the button—the LED goes out. Everything looks normal?
Don't celebrate too soon. Try this operation: Press the button as fast as you can and immediately release it.
You will likely find that sometimes the LED state is wrong—you clearly intended to press it once, but the LED behaves as if you pressed it several times. It might light up and then go out, go out and then light up, or simply not react.
Quantifying the Problem with a Counter
Claims are cheap, so let's use a counter to quantify how severe the bounce is. Add a line inside the if branch:
// Add a global variable at the top of the file
volatile uint32_t button_press_count = 0;
// Inside the main loop if branch:
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
button_press_count++; // Increment counter
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
}2
3
4
5
6
7
8
Then, set a breakpoint in debug mode and read the value of button_press_count.
You clearly only pressed the button once, but button_press_count might show 3, 5, or even over 8.
This is the direct manifestation of mechanical bounce at the software level. To the naked eye, you pressed once, but the MCU sampled multiple press-release oscillations.
3. Why Does It Trigger Multiple Times?
Remember the bounce waveform diagram from Part 03? The moment the button is pressed or released, the contacts don't cleanly transition "from 0 to 1" or "from 1 to 0". Instead, they bounce between high and low levels for approximately 5 to 20 milliseconds.
The problem lies in this time difference.
Let's do the math:
- The
SystemClock_Configabove configures a 72MHz system clock (Note: the project template uses HSI multiplied to 64MHz; here we use the more common HSE 72MHz scheme for demonstration, but the calculation principle is the same). - The main loop does simple things: read a pin, judge a condition, write a pin. The entire loop body consumes about a few dozen clock cycles. Let's estimate 100.
- So the main loop executes approximately every 1.4 microseconds (about 1.6 microseconds at 64MHz, same order of magnitude).
- During a 10 ms bounce, the CPU can run approximately 7,000 loops.
In these 7,000 samples, every "false transition" generated by the bounce—even if it only lasts a few microseconds—will be faithfully captured by HAL_GPIO_ReadPin. If your code in the if branch toggles the LED (Toggle) instead of simply setting it high or low, the multiple toggles caused by the bounce will be directly reflected on the LED: you press once, the LED flashes three or four times.
// If we used toggle logic:
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // Toggle LED state
}2
3
4
The MCU's sampling speed is simply too fast—so fast that it can read a pin thousands of times in a few milliseconds of bounce. There is nothing wrong with our code; the problem lies in the physical characteristics of the button itself. Therefore, debounce is not a "nice to have"; it is a necessity for button input.
4. Simplest Debounce Attempt: HAL_Delay
Since the problem is "sampling too fast, capturing false transitions during bounce," the most direct idea is: After detecting a press, wait a while and read again to confirm the level is stable before deciding if it's a real press.
The simplest implementation of "wait a while" is HAL_Delay:
while (1) {
// 1. First read: Check if button is pressed (Low)
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// 2. Wait for 20ms to let the bounce settle
HAL_Delay(20);
// 3. Second read: Confirm it is still pressed
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// Confirmed press: Toggle LED
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
The logic is clear:
- First read low → Might be bounce, might be a real press, don't rush.
- Wait 20 ms → The bounce is definitely over.
- Read again → If it's still low, it's a real press; if it went back to high, the previous one was just bounce.
Flash and try it—sure enough, pressing the button quickly makes the LED light only once. The counter is normal too. Problem solved?
Only half solved.
The Problem with This Solution
The essence of HAL_Delay is making the CPU spin in a while loop, constantly checking if the SysTick timer has reached the time. During these 20 milliseconds, the CPU can't do any productive work—it is "blocked".
If your project only has one button and one LED, blocking for 20ms might not be a big deal. But imagine these scenarios:
- You need to read a temperature sensor in the main loop with a sampling interval precision of 1ms.
- You are receiving data via serial port, and the buffer might overflow during these 20ms.
- You have an OLED screen refreshing at 60fps, and a 20ms stutter will cause screen tearing.
In a slightly more complex project, blocking debounce is a ticking time bomb. It makes the entire system's response unpredictable.
⚠️ Warning: In a real project, never use blocking debounce in the main loop. It looks simple and effective, but as features increase, it becomes the system's biggest source of instability.
So What Do We Do?
The idea is simple: Don't block the CPU; record the time instead. Every time a level change is detected, don't wait; instead, record the current moment. The next time the loop reads a change, check "how long has it been since the last change". Only if it has been more than 20ms do we consider the level truly stable.
This is the idea of non-blocking debounce—it requires using the SysTick timer or a hardware timer. We will leave the detailed implementation for the next article.
Summary
In this article, we did three things:
- Wrote the first complete button-controlled LED program, from clock configuration and GPIO initialization to main loop polling, all in one go.
- Witnessed the harm of mechanical bounce firsthand—a single press was sampled as multiple triggers, and we quantified this problem with a counter.
- Tried the simplest debounce solution (
HAL_Delay), understanding that it solves the problem but blocks the CPU, leading to the need for non-blocking debounce.
Now you know the "why" and the "simplest how-to" for button debounce. In the next article, we will implement a true engineering-grade non-blocking debounce solution—no CPU blocking, no real-time sacrificed, and the code isn't as much as you might think.