关于单片机实现非阻塞式程序的思路(不使用rtos)
系列文章目录
文章目录
前言
提示:以下是本篇文章正文内容
一、什么是非阻塞式程序
二、设计思想
1.中断
2.遍历
3.其他形式
三.设计思路
这里我会从最经典的按键扫描开始,不断向后更新后续的经典非阻塞式程序的设计思路。
1.GPIO输入输出端口扫描
a.阻塞式设计
- 我们使用实体来与单片机进行交互的方式有很多,如编码器,实体按键等,最终表现形式就是直接占用多个IO来进行输入输出,对于主流8位或是32位单片机而言,每个IO都或多或少带了中断触发机制或者脉冲读取机制。
- 这个初始化就不用我多赘述,按键最终的表现结果会有很多种,我们实际生活中使用的比较多的就是单击+双击+长按的三种形式,那么我们这里就可以直接根据常规思路来设计。
- 比如我这里,对于第一次单击进行读取后,进入while并对使用delay系统延时,并记录延时时长,松手在单次延时后就会进入二级判断,判断按键时长是否超过500ms,若小于则会进入双击判断机制并衰减记录的时间值,若大于则进入长按判断机制。
- 直接上阻塞式代码给观众审查。我这里注释还是写得比较清楚的,我就直接说阻塞式的代码的问题。
- 首先,就是实时性问题,无法及时响应导致用户体验很差。
- 其次,对于系统架构很危险,若是将此段程序用中断的形式进行处理,日常使用没有问题,当上升到复杂情况就可能会导致程序崩溃。我这里是使用遍历的形式进行处理。
- 这里也可以试一下将延时写到中断中,去研究一下后果。
/**
* @brief 获取编码器按键状态
*
* @param encoder 编码器结构体指针
*
* 该函数用于检测编码器按键的状态,并根据按键按下的时间长度设置不同的按键值。
* 具体步骤如下:
* 1. 初始化 `encoder_key` 为 0。
* 2. 检测 GPIOB 的第 0 引脚是否为高电平(按键按下)。
* 3. 如果按键按下,延时 5ms 后再次检测,确认按键是否仍然按下。
* 4. 如果按键仍然按下,设置 `encoder_key` 为 1,并进入一个循环,直到按键释放。
* 5. 在按键释放后,根据 `encoder_key_time` 的值进行不同的处理:
* - 如果 `encoder_key_time` 小于 100,增加 `encoder_key_time` 并进入一个延时循环。
* 在延时循环中,如果按键再次按下,设置 `encoder_key` 为 2,并显示数字 333。
* - 如果 `encoder_key_time` 大于等于 400,重置 `encoder_key_time` 并设置 `encoder_key` 为 3。
*/
void Encoder_KeyGet(encoder_num *encoder) {
encoder->encoder_key = 0;
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET) {
Delay_ms(5);
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET) {
encoder->encoder_key = 1;
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET) {
Delay_ms(5);
encoder->encoder_key_time++;
}
}
}
switch (encoder->encoder_key_time < 100) {
case 1:
encoder->encoder_key_time += 20;
while (encoder->encoder_key_time--) {
Delay_ms(5);
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET) {
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET) {
encoder->encoder_key = 2;
break;
}
}
}
break;
default:
if (encoder->encoder_key_time >= 400) {
encoder->encoder_key_time = 0;
encoder->encoder_key = 3;
}
break;
}
}
b.非阻塞式设计
-
首先,我们能看出来我们上述的代码中主要的问题是delay系统延时函数的使用,在这个过程中浪费了大量CPU的算力,这是不可取的,那么该如何优化这个delay函数呢?是直接删掉还是怎么的呢?
-
那么我先来明确目标,我们想要的是能在某个时间段内判断IO口的电平高低,也就是按键是否按下。
-
那么根据常规思想我们就能想到设计一个定时任务,在设定时间段后定时获取IO电平信息,这样的话就不会长时间占用CPU了,很好,能想这里实际上就是一种多进程的思想出现了。
-
但是单片机直接使用这一种方式就要占用一个中断资源,那么还有什么方法呢?不使用中断的方式来实现,那我们只剩遍历了,遍历该怎么做呢。
-
我们无法从时间上设置事件任务,但想一下我们可不可以在任务中获取时间呢?是可行的,只要在不占用系统资源下单片机有好几种获取系统时间的方式,如systick,rtc,dwt等,但是systick一般用作freertos和lvgl等中间件的“心跳”,故而一般不适用,rtc一般用于基本时钟设计,那么还剩dwt可以使用,也正是DWT,他就是专门用于获取系统运行时间的。
-
对于DWT的初始化我这里不过多赘述,但是我们要用到他的CYCCNT寄存器,因为这个寄存器中存储了从系统启动到结束的晶振跳变次数,而这就是我们要的时间。
-
通过对晶振跳变次数的处理,我们可以得到每次调用这个寄存器时记录的时间,这个就是关键,根据这个时间我们就可以获取程序遍历过程中每次经历到此的时间差,而这就是我们可以用来判断单击双击长按的关键。
-
直接上代码,当然这个代码的处理逻辑还可以优化,并且将微秒改为毫秒机制,这样对于双击间隔不容易溢出。同时这个代码是遍历的形式放在主循环中实现的。
#include "bsp_dwt.h"
#define SINGLE_CLICK_THRESHOLD 200000 // 单击阈值,单位为微秒
#define DOUBLE_CLICK_INTERVAL 300000 // 双击间隔阈值,单位为微秒
#define LONG_PRESS_THRESHOLD 1000000 // 长按阈值,单位为微秒
typedef struct {
uint32_t last_press_time;
uint32_t last_release_time;
uint8_t click_count;
} KeyState;
KeyState key_state;
void Key_Init(void) {
key_state.last_press_time = 0;
key_state.last_release_time = 0;
key_state.click_count = 0;
}
void Key_Update(void) {
static uint8_t button_pressed = 0;
uint32_t current_time = DWT_GetTimeline_us();
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == SET) {
if (!button_pressed) {
// 按键按下
button_pressed = 1;
key_state.last_press_time = current_time;
}
} else {
if (button_pressed) {
// 按键释放
button_pressed = 0;
uint32_t press_duration = current_time - key_state.last_press_time;
if (press_duration < SINGLE_CLICK_THRESHOLD) {
// 单击
key_state.click_count++;
key_state.last_release_time = current_time;
if (key_state.click_count == 2 && (current_time - key_state.last_release_time) < DOUBLE_CLICK_INTERVAL) {
// 双击
key_state.click_count = 0;
// 处理双击事件
} else if (key_state.click_count == 1 && (current_time - key_state.last_release_time) > DOUBLE_CLICK_INTERVAL) {
// 单击
key_state.click_count = 0;
// 处理单击事件
}
} else if (press_duration >= LONG_PRESS_THRESHOLD) {
// 长按
key_state.click_count = 0;
// 处理长按事件
}
}
}
}
- 这里会有好几种写法,我们可以将中断作为时间间隔,实现定时扫描,以提高实时响应能力,但是对于部分情况,定时的对单片机程序进行打断确实在任务复杂情况下有着更佳的实时响应能力
- 对于代码的话,和上面遍历其实一样,只不过整个key_update函数会在中断函数中执行,逻辑判断也会在中断中进行。
- 我们优先初始化一个定时器,基本定时器就够用了,将其设定为多少ms的定时中断任务,并将逻辑处理代码加入就能达到效果。
- 记得清除中断标志位就行。当然定时器你也可以使用其他的,只要是附带了基本定时中断功能就行。
总结
后续再更新吧,这个月太忙了还没写别的文章。
作者:BeyondPNF