当前位置: 首页 > news >正文

第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式

第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始终刷新数字,主程序没有被阻塞。
在这里插入图片描述
全局变量,在主程序和中断中加入全局变量在多线程中加入互斥锁。

总结

  1. 定时器配置与中断机制
    定时器初始化:
    Timer_Init 函数配置 TIM2 定时器:

时钟源:内部时钟 72MHz。

预分频:72-1,使定时器时钟为 1MHz(72MHz / 72)。

周期:1000-1,定时器每 1ms 触发一次中断(1MHz 计数 1000 次)。

中断配置:使能更新中断,设置 NVIC 优先级。

中断服务函数:
TIM2_IRQHandler 每 1ms 执行一次:

调用 Key_Tick 和 LED_Tick 处理按键和 LED 状态。

清除中断标志,避免重复触发。

  1. 按键的非阻塞检测
    Key_Tick 函数:

20ms 消抖:通过静态变量 Count 累计中断次数,每 20ms 检测一次按键状态。

状态机逻辑:

CurrState 记录当前按键状态,PrevState 记录上一次状态。

检测按键释放瞬间(CurrState == 0 且 PrevState != 0),记录键值到 Key_Num。

非阻塞读取:主循环通过 Key_GetNum 获取键值后立即清零,避免重复触发。

  1. LED 的非阻塞控制
    LED_Tick 函数:

模式驱动:根据 LED1_Mode 和 LED2_Mode 控制 LED 行为:

模式 0:关闭。

模式 1:常亮。

模式 2:500ms 亮,500ms 灭(周期 1s)。

模式 3:50ms 亮,50ms 灭(周期 100ms)。

计数器机制:静态变量 LEDx_Count 在每次中断自增,通过取余运算实现周期性切换状态。

  1. 主循环的非阻塞特性
    主循环逻辑:

不断读取按键值 KeyNum,更新 LED 模式。

显示信息到 OLED,无需等待定时任务。

中断与主循环分工:

中断处理耗时短的任务(按键消抖、LED 状态切换)。

主循环处理非实时任务(如显示更新),避免被阻塞。

  1. 关键设计点
    时间片划分:定时器中断以 1ms 为基准,任务按需分频(如按键 20ms 检测一次)。

状态保持:使用静态变量(如 Count, LEDx_Count)保存任务状态,在中断间维持数据。

资源隔离:中断仅更新标志位或状态,主循环处理业务逻辑,降低耦合。

总结
通过定时器中断周期性触发任务,结合状态机和计数器机制,程序将耗时短且需周期性执行的操作(按键检测、LED 控制)放在中断中处理,主循环仅负责非实时任务(如显示更新)。这种设计确保了系统的高响应性和非阻塞特性。

补充(为什么不会阻塞)

LED_Tick() 函数在定时器中断(TIM2_IPQHandler)中被调用,而 LED1_Count++ 是中断服务程序(ISR)中的一个操作。LED1_Count++ 不会阻塞程序运行的原因与中断的机制和代码设计密切相关,以下是详细解释:

  1. 中断的抢占特性
    中断优先级:
    定时器中断(如 TIM2_IRQHandler)具有高于主循环的优先级。当定时器中断触发时,CPU 会立即暂停主循环的执行,跳转到中断服务函数中运行 LED_Tick()。

中断执行时间短:
LED_Tick() 中的操作(如 LED1_Count++、条件判断、LED 状态切换)均为简单操作,执行时间极短(通常在微秒级)。中断服务函数会快速完成并退出,释放 CPU 控制权,主循环随即恢复执行。

  1. 非阻塞设计的关键
    计数器自增的原子性:
    LED1_Count++ 是一个原子操作(在大多数嵌入式架构中,uint16_t 自增是单指令操作),不会被主循环打断。即使主循环正在修改 LED1_Mode,中断服务函数也能安全地更新 LED1_Count。

状态机与模式分离:

主循环:仅负责更新 LED1_Mode(通过按键触发),不直接操作硬件或耗时逻辑。

中断:根据 LED1_Mode 的值,通过 LED_Tick() 周期性更新 LED1_Count 和 LED 状态。
两者的职责分离,确保主循环不被阻塞。

  1. 代码中的具体实现
    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 限制,避免溢出问题。

  1. 主循环与中断的协作
    主循环无等待:
    主循环中的代码(如 OLED_ShowNum())无需等待 LED_Tick() 完成。即使中断频繁触发,主循环也能在中断间隙继续执行。

中断频率合理:
定时器中断周期为 1ms(由 TIM_Period 和 TIM_Prescaler 决定),中断处理时间远小于中断间隔,不会导致中断堆积或主循环饥饿。

  1. 数据一致性问题(额外注意事项)
    虽然 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 显示、按键检测)可以并行执行,实现非阻塞的系统行为。

相关文章:

  • skywalking实现原理
  • unity学习39:连续动作之间的切换,用按键控制角色的移动
  • 编程技巧(基于STM32)第一章 定时器实现非阻塞式程序——按键控制LED灯闪烁模式
  • Spring Boot 定时任务:轻松实现任务自动化
  • PyQt6/PySide6 的信号与槽原理
  • YOLOv5-Seg 深度解析:与 YOLOv5 检测模型的区别
  • 四元数如何用于 3D 旋转(代替欧拉角和旋转矩阵)【ESP32指向鼠标】
  • 基于Python的Optimal Interpolation (OI) 方法实现
  • ZZNUOJ(C/C++)基础练习1091——1100(详解版)⭐
  • 差分解方程
  • [矩形绘制]
  • 图的遍历: 广度优先遍历和深度优先遍历
  • FPGA的星辰大海
  • Windows环境下使用Ollama搭建本地AI大模型教程
  • MAC 系统关闭屏幕/睡眠 后被唤醒 Wake Requests
  • spring针对抽象类注入属性
  • 6.2.1 数据模型的基本概念、数据模型三要素
  • Linux alias使用
  • Ant-Design-Vue:Button按钮SVG图标垂直未居中问题
  • 深度学习R4周:LSTM-火灾温度预测
  • 中国科学院院士、我国航天液体火箭技术专家朱森元逝世
  • 陈吉宁龚正黄莉新胡文容等在警示教育基地参观学习,出席深入贯彻中央八项规定精神学习教育交流会
  • 盛和资源海外找稀土矿提速:拟超7亿元收购匹克,加快推动坦桑尼亚项目
  • 重庆市委原常委、政法委原书记陆克华被决定逮捕
  • 兰州大学教授安成邦加盟复旦大学中国历史地理研究所
  • 教育部基础教育教指委:稳步推进中小学人工智能通识教育