STM32 非阻塞式编程:以 LED 和按键为例
一、前言
在嵌入式开发中,我们经常需要处理多个任务,比如测距、闪灯、响应用户按键、处理串口接收等等。如果你在主循环中使用 delay()
之类的阻塞函数,那这些任务就只能一个一个做,效率低下,体验不佳。
非阻塞式编程,就是为了解决这个问题的一种思路。它避免在主循环中使用任何会"卡住 CPU"的阻塞操作,从而允许 CPU 每时每刻都能轮询检查所有任务是否需要处理,实现"多任务调度"的效果。
本文将从原理讲起,并用 STM32F407 开发板配合 LED 和按键 做一个完整的寄存器级非阻塞例程,帮你彻底理解这个核心知识点。
二、阻塞 vs 非阻塞:到底有什么区别
类型 | 示例 | 优点 | 缺点 |
---|---|---|---|
阻塞式 | delay(1000); |
写起来简单直观 | 会让 CPU 等待,浪费资源 |
非阻塞式 | if (millis - last >= 1000) |
不卡主循环,适合多任务 | 写起来需要设计思路、变量管理 |
举个例子:
c
// 阻塞式延时方式闪烁 LED
LED_ON();
delay_ms(1000);
LED_OFF();
delay_ms(1000);
这个时候,CPU 就被 delay_ms()
卡死了,什么事也干不了。
而非阻塞的写法如下:
c
if (SysTickCounter - lastToggleTime >= 1000) {
lastToggleTime = SysTickCounter;
toggle_LED();
}
CPU 每次循环只判断一下时间是否到了,LED 闪烁的同时,主循环还能继续干别的事。
三、非阻塞的核心思路
非阻塞的核心是:"记录上一次事件发生的时间,并每次循环中判断是否满足条件。"
用人话解释:
- 如果你要等一分钟,就不要站着等(阻塞);
- 而是每隔几秒瞄一眼表,看时间到了没有(非阻塞)。
四、实现非阻塞:我们用 SysTick 来做时间基准
我们需要一个系统计时器来定时递增变量,用于判断时间间隔。在 STM32F4 中,最简单的方法就是用 SysTick 定时器。
1. 初始化 SysTick 计时器(1ms中断一次)
c
void SysTick_Init(void)
{
// 设置重装载值,使得每 1ms 触发一次中断
// 假设系统主频是 168MHz
// 168000000 / 1000 = 168000
SysTick->LOAD = 168000 - 1;
// 设置时钟源为处理器时钟
SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;
// 使能 SysTick 中断
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
// 使能 SysTick 定时器
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
}
2. SysTick 中断服务函数
c
volatile uint32_t systick_ms = 0; // 毫秒计数变量
void SysTick_Handler(void)
{
systick_ms++; // 每毫秒加一
}
五、LED 非阻塞闪烁代码
1. GPIO 初始化(LED 接 PA5)
c
void LED_Init(void)
{
RCC->AHB1ENR |= (1 << 0); // 使能 GPIOA 时钟
GPIOA->MODER &= ~(3 << (5 * 2)); // 清除模式位
GPIOA->MODER |= (1 << (5 * 2)); // 设置为输出模式
GPIOA->OTYPER &= ~(1 << 5); // 推挽输出
GPIOA->OSPEEDR |= (3 << (5 * 2)); // 高速
GPIOA->PUPDR &= ~(3 << (5 * 2)); // 无上下拉
}
void LED_Toggle(void)
{
GPIOA->ODR ^= (1 << 5); // 取反输出
}
2. 非阻塞闪烁逻辑
c
void LED_Blink_NonBlocking(void)
{
static uint32_t last_time = 0;
// 判断当前时间与上次时间的间隔是否 >= 500ms
if (systick_ms - last_time >= 500)
{
last_time = systick_ms; // 记录新的时间戳
LED_Toggle(); // 切换 LED 状态
}
}
六、按键非阻塞检测(支持消抖)
1. GPIO 初始化(按键接 PC13)
c
void Key_Init(void)
{
RCC->AHB1ENR |= (1 << 2); // 使能 GPIOC 时钟
GPIOC->MODER &= ~(3 << (13 * 2)); // 设置为输入模式
GPIOC->PUPDR |= (1 << (13 * 2)); // 上拉
}
2. 非阻塞方式读取按键(支持简单消抖)
c
uint8_t Key_GetPress(void)
{
static uint8_t key_state = 1; // 初始为释放状态(PC13 默认为高)
static uint32_t last_debounce_time = 0;
if ((GPIOC->IDR & (1 << 13)) == 0) // 按键被按下(低电平)
{
if (systick_ms - last_debounce_time >= 20) // 20ms去抖
{
last_debounce_time = systick_ms;
if (key_state == 1)
{
key_state = 0; // 记录状态为"已按下"
return 1; // 返回按键事件
}
}
}
else
{
key_state = 1; // 松开后恢复初始状态
}
return 0; // 没有新事件
}
七、主函数整合
c
int main(void)
{
SysTick_Init();
LED_Init();
Key_Init();
while (1)
{
LED_Blink_NonBlocking(); // 每500ms闪烁一次
if (Key_GetPress()) // 按键按下就立刻响应
{
// 可以加入更多操作,比如切换模式、串口输出等
GPIOA->ODR ^= (1 << 5); // 再次切换 LED
}
// 可拓展:串口接收处理、传感器轮询等其他非阻塞任务
}
}
八、总结:非阻塞的精髓
- 避免用 delay(),而是用时间戳差值判断
- 每个任务都维护自己的状态变量和计时变量
- 适合多个任务并发运行的情况(状态机 + 事件驱动)
九、扩展阅读建议
- 状态机编程模型
- 事件驱动模型
- 基于定时器的软定时器管理模块设计
- 使用 RTOS 代替非阻塞轮询的多任务调度
(完)