第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式
- 解决按键扫描,松手检测时阻塞的问题
- 实现LED闪烁的非阻塞
- 总结
- 补充(为什么不会阻塞)
参考江协科技
KEY1和KEY2两者独立控制互不影响
阻塞:如果按下按键不松手,程序就会卡死在while循环里,主程序的其他程序无法执行,直到松手,函数才能结束。CPU花很长时间等大地。
非阻塞:程序执行很快且很快结束。
任务:按下K1慢闪,再按下K1熄灭
常规方法:
为什么开灯灵敏,关灯就不灵敏呢?因为开灯之后,程序会执行delay等待以及while等待,阻塞按键扫描程序,只有长按按键才能熄灭LED。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "LED.h"uint8_t KeyNum = 0;
uint8_t FlashFlag = 0;int main(void)
{OLED_Init();Key_Init();LED_Init();while (1){KeyNum = Key_GetNum();if(KeyNum == 1){FlashFlag = !FlashFlag;}if(FlashFlag){LED1_ON();Delay_ms(500);LED1_OFF();Delay_ms(500);}else{LED1_OFF();}}
}
阻塞测试
按下按键之前,屏幕快速刷新,按下按键后,数字停止刷新,表明程序阻塞在等待按键松手的地方。放手后,LED会闪烁同事数字继续自增。
主循环始终保持快速刷新状态,定时器定时中断可达到类似多线程的效果。
解决按键扫描松手检测时阻塞的问题。办法是用定时器扫描按键。
定时器扫描按键-单按键思路
上次采样电平 本次采样电平 结论
1 1 按键没按下
1 0 按键按下
0 0 按键按下没松开
0 1 按键按下并松开
根据以上情况置相应的标志位来执行操作。
定时器扫描按键-多按键
解决按键扫描,松手检测时阻塞的问题
如果把按键代码直接写在定时中断里面,不利于按键模块的独立封装,如果把该定时中断直接放到Key里面,那么Key就会独占这个定时器。综合考虑定义Key_Tick(void)函数。再把该函数放到定时器中断函数中,每隔1ms调用Key_Tick(void), 相当于Key模块多了个中断函数。它每隔1ms就会自动执行一次。实现多模块共用一个定时器来实现定时。
按键关灯变得灵敏的原因是按键扫描位于定时器中断里,即使主程序卡在Delay里面,定时器中断仍然能够执行,按键检测仍然能够执行。按键只要检测到了,就会置相应的标志位记录按键按下。
实现LED闪烁的非阻塞
LED以1s为周期,亮500ms,灭500ms
加入SetMode函数,用按键控制LED闪烁,如果不用的话,LED通过定时器无脑闪烁。
while (1){KeyNum = Key_GetNum();if(KeyNum == 1){FlashFlag = !FlashFlag;}if(FlashFlag){LED1_SetMode(1);}else{LED1_SetMode(0);}OLED_ShowNum(1,1,i++,5);}
void LED1_SetMode(uint8_t Mode)
{LED1_Mode = Mode;
}void LED_Tick(void)
{if(LED1_Mode == 0){LED1_OFF();}else{LED1_Count++;//if(LED1_Count > 999) LED1_Count = 0;LED1_Count %= 1000; //Count < 1000, 取余等于本身,等于1000,取余等于0,大于1000时,取余后会得到1000以内的余数,防止自增越界if(LED1_Count < 500){LED1_ON();}else{LED1_OFF();}}}
实验现象:
刚开始LED熄灭,主循环快速刷新
按下按键,LED闪烁
再按下按键,LED熄灭
根据OLED显示可知道主循环始终没有阻塞
继续完善代码,执行熄灭-常亮-慢闪-快闪-点闪,设置相应的状态机。
if(LED1_Mode == 0){LED1_OFF();}else if(LED1_Mode == 1){LED1_ON();}else if(LED1_Mode == 2){LED1_Count++;//if(LED1_Count > 999) LED1_Count = 0;LED1_Count %= 1000; //Count < 1000, 取余等于本身,等于1000,取余等于0,大于1000时,取余后会得到1000以内的余数,防止自增越界if(LED1_Count < 500){LED1_ON();}else{LED1_OFF();}}else if(LED1_Mode == 3){LED1_Count++;//if(LED1_Count > 999) LED1_Count = 0;LED1_Count %= 100; //Count < 1000, 取余等于本身,等于1000,取余等于0,大于1000时,取余后会得到1000以内的余数,防止自增越界if(LED1_Count < 50){LED1_ON();}else{LED1_OFF();}}else{LED1_Count++;//if(LED1_Count > 999) LED1_Count = 0;LED1_Count %= 1000; //Count < 1000, 取余等于本身,等于1000,取余等于0,大于1000时,取余后会得到1000以内的余数,防止自增越界if(LED1_Count < 100){LED1_ON();}else{LED1_OFF();}}
实现状态机轮转
if(KeyNum == 1){LED1_MODE++;LED1_MODE %= 5;LED1_SetMode(LED1_MODE);}
如果想要每次模式切换后,闪烁都要从一个周期的最开始进行。需要额外添加代码。
非阻塞的代码可以保证主循环的快速执行,让每部分功能都能够得到及时响应。
注意:定时中断被多个模块复用,要确保这些模块的中断代码执行时间不要过久。
可能会出现中断重叠,如果要判断中断是否重叠,可以再进入中断的最开始就清除中断标志位。等结束之后再查看这个标志位,如果这时还没有被置1,说明中断没有重叠。
实验现象:
两个按键分别独立控制LED的亮灭以及闪烁,led始终刷新数字,主程序没有被阻塞。
全局变量,在主程序和中断中加入全局变量在多线程中加入互斥锁。
总结
- 定时器配置与中断机制
定时器初始化:
Timer_Init 函数配置 TIM2 定时器:
时钟源:内部时钟 72MHz。
预分频:72-1,使定时器时钟为 1MHz(72MHz / 72)。
周期:1000-1,定时器每 1ms 触发一次中断(1MHz 计数 1000 次)。
中断配置:使能更新中断,设置 NVIC 优先级。
中断服务函数:
TIM2_IRQHandler 每 1ms 执行一次:
调用 Key_Tick 和 LED_Tick 处理按键和 LED 状态。
清除中断标志,避免重复触发。
- 按键的非阻塞检测
Key_Tick 函数:
20ms 消抖:通过静态变量 Count 累计中断次数,每 20ms 检测一次按键状态。
状态机逻辑:
CurrState 记录当前按键状态,PrevState 记录上一次状态。
检测按键释放瞬间(CurrState == 0 且 PrevState != 0),记录键值到 Key_Num。
非阻塞读取:主循环通过 Key_GetNum 获取键值后立即清零,避免重复触发。
- LED 的非阻塞控制
LED_Tick 函数:
模式驱动:根据 LED1_Mode 和 LED2_Mode 控制 LED 行为:
模式 0:关闭。
模式 1:常亮。
模式 2:500ms 亮,500ms 灭(周期 1s)。
模式 3:50ms 亮,50ms 灭(周期 100ms)。
计数器机制:静态变量 LEDx_Count 在每次中断自增,通过取余运算实现周期性切换状态。
- 主循环的非阻塞特性
主循环逻辑:
不断读取按键值 KeyNum,更新 LED 模式。
显示信息到 OLED,无需等待定时任务。
中断与主循环分工:
中断处理耗时短的任务(按键消抖、LED 状态切换)。
主循环处理非实时任务(如显示更新),避免被阻塞。
- 关键设计点
时间片划分:定时器中断以 1ms 为基准,任务按需分频(如按键 20ms 检测一次)。
状态保持:使用静态变量(如 Count, LEDx_Count)保存任务状态,在中断间维持数据。
资源隔离:中断仅更新标志位或状态,主循环处理业务逻辑,降低耦合。
总结
通过定时器中断周期性触发任务,结合状态机和计数器机制,程序将耗时短且需周期性执行的操作(按键检测、LED 控制)放在中断中处理,主循环仅负责非实时任务(如显示更新)。这种设计确保了系统的高响应性和非阻塞特性。
补充(为什么不会阻塞)
LED_Tick() 函数在定时器中断(TIM2_IPQHandler)中被调用,而 LED1_Count++ 是中断服务程序(ISR)中的一个操作。LED1_Count++ 不会阻塞程序运行的原因与中断的机制和代码设计密切相关,以下是详细解释:
- 中断的抢占特性
中断优先级:
定时器中断(如 TIM2_IRQHandler)具有高于主循环的优先级。当定时器中断触发时,CPU 会立即暂停主循环的执行,跳转到中断服务函数中运行 LED_Tick()。
中断执行时间短:
LED_Tick() 中的操作(如 LED1_Count++、条件判断、LED 状态切换)均为简单操作,执行时间极短(通常在微秒级)。中断服务函数会快速完成并退出,释放 CPU 控制权,主循环随即恢复执行。
- 非阻塞设计的关键
计数器自增的原子性:
LED1_Count++ 是一个原子操作(在大多数嵌入式架构中,uint16_t 自增是单指令操作),不会被主循环打断。即使主循环正在修改 LED1_Mode,中断服务函数也能安全地更新 LED1_Count。
状态机与模式分离:
主循环:仅负责更新 LED1_Mode(通过按键触发),不直接操作硬件或耗时逻辑。
中断:根据 LED1_Mode 的值,通过 LED_Tick() 周期性更新 LED1_Count 和 LED 状态。
两者的职责分离,确保主循环不被阻塞。
- 代码中的具体实现
LED_Tick() 的分支逻辑:
c
复制
else if(LED1_Mode == 2) {
LED1_Count++;
LED1_Count %= 1000;
if(LED1_Count < 500) LED1_ON();
else LED1_OFF();
}
快速执行:每个分支仅包含简单的算术运算(%)、比较和 GPIO 操作,无耗时操作(如延时、循环等待)。
计数器自增可控:LED1_Count 的范围通过 %= 1000 或 %= 100 限制,避免溢出问题。
- 主循环与中断的协作
主循环无等待:
主循环中的代码(如 OLED_ShowNum())无需等待 LED_Tick() 完成。即使中断频繁触发,主循环也能在中断间隙继续执行。
中断频率合理:
定时器中断周期为 1ms(由 TIM_Period 和 TIM_Prescaler 决定),中断处理时间远小于中断间隔,不会导致中断堆积或主循环饥饿。
- 数据一致性问题(额外注意事项)
虽然 LED1_Count++ 本身不会阻塞,但需要注意 主循环和中断共享变量 的潜在风险:
LED1_Mode 的并发修改:
如果主循环正在修改 LED1_Mode(如 LED1_MODE++),而中断同时读取 LED1_Mode,可能导致数据不一致(如读到中间状态)。
解决方案:
使用原子操作或禁用中断保护共享变量:
c
复制
// 主循环中修改 LED1_Mode 时,临时禁用中断
__disable_irq();
LED1_MODE++;
__enable_irq();
将 LED1_Mode 声明为 volatile,防止编译器优化导致意外行为:
c
复制
volatile uint8_t LED1_MODE = 0;
总结
LED1_Count++ 不会阻塞程序,是因为:
中断服务函数执行时间极短(微秒级)。
主循环和中断职责分离,无耗时操作。
定时器中断频率合理,避免抢占主循环。
共享变量(如 LED1_Mode)需注意并发访问问题,但代码中未显式处理,可能存在潜在风险。
通过这种设计,LED 状态更新和主循环任务(如 OLED 显示、按键检测)可以并行执行,实现非阻塞的系统行为。