本次我们来讲一下外部中断,以按键为例,来实现按键切换灯光的效果。
中断的简介
说起中断,我们常常就会提到一个经典的例子,就是我们在家里处理手头上事情的时候,热水煮开了,这时候我们就需要放下手头的事情,去关掉煤气炉。这个就是中断
书面化的表达就是CPU正在处理某件事时,外部发生了某一事件,请求CPU迅速处理,CPU暂时中断当前的工作,转而处理所发生的事情,处理完后,再回到原来被中断的地方,继续原来的工作。
我们有时候会称中断服务程序为前台程序,循环中的程序为后台程序,它们的特点如下所示。
通过中断机制,在外设不需要CPU介入时,CPU可以执行其他线程,而当外设需要CPU时,通过产生中断信号使CPU立即停止当前线程转而响应中断请求。这样CPU可以不用老是进行轮询或者等待的操作,大大提高了系统实时性以及执行效率。
但是中断也不可以滥用,要用的谨慎一些,特别是上了实时操作系统之后。因为无论线程有着多高的优先级,中断都可以打断线程的运行,因此一般只用于紧急事件,并且只进行简单的处理,比如说标记事件发生,利用信号量等内核对象通知线程,进行更加复杂的操作。
创建工程
本次我们再次以RT-Thread提供的STM32F407-RoboMaster-C bsp文件来创建工程。
创建后我们来看一下开发板原理图,按键KEY对应的是PA0_WKUP
我们进入CubeMX.ioc中看到PA0并没有开启,所以我们要自行开启一下,设置为外部中断模式。
然后进入GPIO标签页中设置为沿上升/下降沿双边触发,上拉电阻。点击GENERAYE CODE即可。
原理介绍
下面解释一下我们在CubeMX中配置的几个选项是什么意思。
第一个就是我们选择PA0-WKUP模式为GPIO_EXTI0,是代表了什么呢?想回答这个问题我们就要来看一下我们使用外部中断的流程。
这里我修改了正点原子的流程图便于讲解。
第一部分涉及的知识比较深,如果是完全没有接触过STM32的同学可以选择忽略。
如图所示我们需要设置输入模式,这里为外部中断模式,之后设置SYSCGF(系统配置寄存器)中的STSCFG_EXTICR1(外部中断配置寄存器),它的作用是设置EXTI和IO映射关系。就如下图所示,我们配置寄存器位15:0,以选择EXTIx外部中断的源输入。那么EXTIx是什么呢,我们为什么要配置它的源输入呢?下面马上解答。
EXTI为扩展中断/事件控制器,其中有4根输入线,每个输入线可以单独进行配置选择类型(中断或事件)和相应的触发事件(上升沿触发、下降沿触发、双边沿触发)。
如下图所示就是EXTI的工作原理图,我们通过配置相关的寄存器,芯片内部进行或运算之后起作用。EXTI和GPIO之间的映射关系就在上面的SYSCGF中设置好了,那么我们这个按键对应的PA0引脚对应的是哪个EXTI扩展中断/事件线呢?
下图就很清晰的告诉了我们,EXTIx就对应着P*x,因此PA0就对应着EXTI0,这里也就讲解了我们一开始设置PA0为GPIO_EXTI0是什么意思了。
流程图中的NVIC为嵌套向量中断控制器,它是管理包括内核异常在内的所有中断,相关的知识这里由于篇幅就不展开讲了,但是不代表不重要,大家最好自行查看资料学习。
下面继续解释一下我们在GPIO标签页中配置的双边触发,上拉电阻是什么意思。
我们这里设置的上升沿触发就是配置我们上文提到的EXTI中的触发选择寄存器,至于为什么要选择沿上升沿触发我们就要来看一下原理图。
根据原理图我们可以看到在按下按键之后引脚将直连GND,处于低电平。
那么上拉电阻这个选择即使原理图没有画我们也要自己可以推断出来了,因为外部中断的引脚肯定不能是浮空的,因为浮空状态下电平是不确定的一会高电平一会低电平会导致中断的误触发。
然后我们希望按下按键之后有电平的变化那么在未按下的状态,引脚应当被上拉电阻钳位在高电平,这一点在原理图中也得到印证。至于双边触发就根据程序而定,后面我们还会用RT-Thread提供的API可以设置这个中断触发条件的。
程序编写
这里我会使用两个方案一个是使用外部中断方式来进行点灯,还有一种方案是通过MultiButton使用类似于传感器的方案,开启一个线程来专门处理按键相关事情。
首先是外部中断的方案,这里我采取的方案是中断服务函数中释放信号量,电平翻转操作在线程中执行。下面是源代码,下面这段代码实现的功能是第一次按下按键蓝灯亮起,第二次按下按键蓝灯熄灭以此循环。这个代码比较简单,大家直接看注释即可。
/*
Copyright (c) 2006-2021, RT-Thread Development Team
SPDX-License-Identifier: Apache-2.0
Change Logs:
Date Author Notes
2023-01-05 Goldengrandpa the first version
/
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
/ 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
#ifndef KEY_PIN_NUM
#define KEY_PIN_NUM GET_PIN(A, 0)
#endif
#ifndef LED_B_PIN
#define LED_B_PIN GET_PIN(H, 10)
#endif
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread1_entry(void parameter)
{
while (1)
{
static rt_err_t result;
static int status;
/ 永久方式等待信号量,获取到信号量,则执行 LED电平翻转的操作 */
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
status = rt_pin_read(LED_B_PIN);
if (status == PIN_LOW)
{
rt_pin_write(LED_B_PIN, PIN_HIGH);
}
else
{
rt_pin_write(LED_B_PIN, PIN_LOW);
}
}
}
}
void key_interrupt_callback(void args)
{
rt_sem_release(dynamic_sem); / 释放信号量 /
}
int key_sample(void)
{
/ LED引脚为输出模式 /
rt_pin_mode(LED_B_PIN, PIN_MODE_OUTPUT);
/ 默认低电平 /
rt_pin_write(LED_B_PIN, PIN_LOW);
/ 按键0引脚为输入模式 /
rt_pin_mode(KEY_PIN_NUM, PIN_MODE_INPUT_PULLUP);
/ 绑定中断,下降沿模式,回调函数名为key_interrupt_callback /
rt_pin_attach_irq(KEY_PIN_NUM, PIN_IRQ_MODE_FALLING, key_interrupt_callback, RT_NULL);
/ 使能中断 /
rt_pin_irq_enable(KEY_PIN_NUM, PIN_IRQ_ENABLE);
/ 创建一个动态信号量,初始值是 0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}
rt_thread_init(&thread1, "thread1", rt_thread1_entry,
RT_NULL, &thread1_stack[0], sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
return 0;
}
但是上面这个程序还有一个问题就是没有进行消抖操作。那么消抖是什么呢?
由于按键的机械结构具有弹性,按下时开关不会立刻接通,断开时也不会立刻断开,这就导致按键的输入信号在按下和断开时都会存在抖动,如果不先将抖动问题进行处理,则读取的按键信号可能会出现错误。
这里我们就需要使用软件滤波的方法即抖动产生在按键按下的边沿时刻,叫下降沿(电平从高到低),所以只需要在边沿时进行延时,等到按键输入已经稳定再进行信号读取即可。
这里我们主要修改的前台程序,即线程里的程序,拿到信号量之后,不要直接进行电平翻转操作,延时20ms后再次读取电平后选择进行操作。
static void rt_thread1_entry(void parameter)
{
while (1)
{
static rt_err_t result;
static int status;
static int falling_flag;
/ 永久方式等待信号量,获取到信号量,则执行 LED电平翻转的操作 /
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
rt_kprintf("t2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
rt_thread_mdelay(20);/ 延时20ms /
falling_flag = rt_pin_read(KEY_PIN_NUM);/ 再次读取按键电平 /
if (falling_flag == PIN_HIGH) / 如果延时后发现是高电平说明是误触发直接返回 /
{
return;
}
else / 验证为下降沿进行电平翻转操作 */
{
status = rt_pin_read(LED_B_PIN);
if (status == PIN_LOW)
{
rt_pin_write(LED_B_PIN, PIN_HIGH);
}
else
{
rt_pin_write(LED_B_PIN, PIN_LOW);
}
}
}
}
}
下面我们再看一下软件包的方案,这里用的就不是外部中断了。
这里我会使用MultiButton软件包并且进行改写,软件包的良好生态也是许多人选择RT-Thread的原因,这个软件包是我刚接触RT-Thread时,参加线上培训的时候苏李果老师推荐的,用起来体验也不错,所以这里也推荐给大家。
在RT-Thread Settings中点击添加软件包,找到MultiButton后进行添加。
我们简单的看一眼源码,我认为这个代码设计思路很值得学习。
面向对象思想
这个按键驱动与RT-Thread源码一样包含着面向对象思想。
它每个按键都抽象为一个按键对象,每个按键对象都是独立的,系统中所有的按键对象使用单链表串起来。
typedef struct button {
uint16_t ticks;
uint8_t repeat : 4;
uint8_t event : 4;
uint8_t state : 3;
uint8_t debounce_cnt : 3;
uint8_t active_level : 1;
uint8_t button_level : 1;
uint8_t (hal_button_Level)(void);
BtnCallback cb[number_of_event];
struct button next;
}button;
其中在变量后面跟冒号的语法称为位域,使用位域的优势是节省内存。
这里常常用于一些通信协议上面,它就相当于把uint8_t 一个字节拆来分别装不同的变量。
就如下图所示本来 6 个uint8_t 类型的变量需要占用 6 个字节,但使用位域语法后,这6个变量只占用两个字节:
但是需要注意的是,位域要求变量内存地址要连续,所以个人认为这个结构体要用_packed进行修饰。
按键对象单链表
MultiButton定义了一个头指针
static struct button* head_handle = NULL;
用户插入一个按键对象的代码如下:
//启动按键
button_start(&button1);
button_start的实现如下
int button_start(struct button* handle)
{
struct button* target = head_handle;
while(target)
{
if(target == handle)
{
return -1; //already exist.
}
target = target->next;
}
handle->next = head_handle;
head_handle = handle;
return 0;
}
在第一次插入时,因为head_handler为NULL,所以直接运行while之后的代码,对象链表如下。
插入下一个按键对象时对象链表则为
状态机处理思想
这个在我们RoboMaster电控代码中有也有大量的体现,云台,底盘多个模式的实现都是使用到了状态机。
MultiButton中使用状态机来处理每个按键对象,在例程中每隔5ms调用button_tick()依次调用状态机对单链表上的所有按键对象进行遍历处理。
void button_ticks(void)
{
struct button* target;
for(target = head_handle; target != NULL; target = target->next)
{
button_handler(target);
}
}
使用button_handler对按键对象进行处理,函数实现如下。
首先调用该按键对象注册的读取状态函数进行读取:
uint8_t read_gpio_level = handle->hal_button_Level();
读取之后,判断当前状态机的状态,如果有功能正在执行(state不为0),则按键对象的tick值加1
if((handle->state) > 0)
{
handle->ticks++;
}
之后进行按键消抖,这次连续读取了3次。每次延时15ms,如果引脚状态一直与之前不同,则改变按键对象中的引脚状态。
if(read_gpio_level != handle->button_level)
{
//not equal to prev one
//continue read 3 times same new level change
if(++(handle->debounce_cnt) >= DEBOUNCE_TICKS)
{
handle->button_level = read_gpio_level;
handle->debounce_cnt = 0;
}
}
else
{
// leved not change ,counter reset.
handle->debounce_cnt = 0;
}
最后就进入状态机处理,例子如下。
switch (handle->state)
{
case 0:
if(handle->button_level == handle->active_level)
{
handle->event = (uint8_t)PRESS_DOWN;
EVENT_CB(PRESS_DOWN);
handle->ticks = 0;
handle->repeat = 1;
handle->state = 1;
}
else
{
handle->event = (uint8_t)NONE_PRESS;
}
break;
整个状态机处理如流程图所示
之后我们来修改一下代码,接下来我想要实现三击的功能,但是软件包中只有双击,因此需要修改代码。
要修改一下状态枚举体,增加三击功能。
typedef enum
{
PRESS_DOWN = 0,
PRESS_UP,
PRESS_REPEAT,
SINGLE_CLICK,
DOUBLE_CLICK,
TRIPLE_CLICK, // TRIPLE_CLICK
LONG_PRESS_HOLD,
number_of_event,
NONE_PRESS
} PressEvent;
修改状态机处理函数,增加三击判断
case 2:
if(handle->button_level == handle->active_level)
{
handle->event = (uint8_t)PRESS_DOWN;
EVENT_CB(PRESS_DOWN);
handle->repeat++;
EVENT_CB(PRESS_REPEAT);
handle->ticks = 0;
handle->state = 3;
}
else if(handle->ticks > SHORT_TICKS)
{
if(handle->repeat == 1)
{
handle->event = (uint8_t)SINGLE_CLICK;
EVENT_CB(SINGLE_CLICK);
}
else if(handle->repeat == 2)
{
handle->event = (uint8_t)DOUBLE_CLICK;
EVENT_CB(DOUBLE_CLICK);
}
else if(handle->repeat ==3)
{
handle->event=(uint8_t)TRIPLE_CLICK;
EVENT_CB(TRIPLE_CLICK);
}
handle->state = 0;
}
break;
之后新建app_button.c文件编写按键回调代码,这里的写法大家可以参照MultiButton提供的样例进行改写。
这里实现的功能为单击、双击、三击开启不同的灯,长按把所有灯熄灭。
/*
Copyright (c) 2006-2021, RT-Thread Development Team
SPDX-License-Identifier: Apache-2.0
Change Logs:
Date Author Notes
2023-01-06 Goldengrandpa the first version
*/
#include <rtthread.h>
#include <rtdevice.h>
#include "board.h"
#include "app_button.h"
#define KEY_PIN_NUM GET_PIN(A,0)
#define LED_B_PIN GET_PIN(H, 10)
#define LED_G_PIN GET_PIN(H, 11)
#define LED_R_PIN GET_PIN(H, 12)
#define key_id 0
struct button key;
static rt_timer_t timer_btn;
uint8_t read_key_GPIO()
{
return rt_pin_read(KEY_PIN_NUM);
}
static void tb_timeout_callback(void *parameter)
{
button_ticks();
}
void key_callback(void *btn)
{
struct Button *dev_btn = (struct Button *)btn;
PressEvent event = get_button_event(dev_btn);
switch (event)
{
case SINGLE_CLICK:
rt_pin_write(LED_B_PIN, PIN_HIGH);
rt_kprintf("key single click.\r\n");
break;
case DOUBLE_CLICK:
rt_pin_write(LED_R_PIN, PIN_HIGH);
rt_kprintf("key double click.\r\n");
break;
case TRIPLE_CLICK:
rt_pin_write(LED_G_PIN, PIN_HIGH);
rt_kprintf("key triple click.\r\n");
break;
case LONG_PRESS_HOLD:
rt_pin_write(LED_B_PIN, PIN_LOW);
rt_pin_write(LED_G_PIN, PIN_LOW);
rt_pin_write(LED_R_PIN, PIN_LOW);
rt_kprintf("key long press hold.\r\n");
break;
default:
break;
}
}
int app_button(void)
{
rt_pin_mode(KEY_PIN_NUM, PIN_MODE_INPUT_PULLUP);
rt_pin_mode(LED_B_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(LED_G_PIN, PIN_MODE_OUTPUT);
rt_pin_mode(LED_R_PIN, PIN_MODE_OUTPUT);
button_init(&key, read_key_GPIO,0);
button_attach(&key, SINGLE_CLICK, key_callback);
button_attach(&key, DOUBLE_CLICK, key_callback);
button_attach(&key, TRIPLE_CLICK, key_callback);
button_attach(&key, LONG_PRESS_HOLD, key_callback);
button_start(&key);
timer_btn = rt_timer_create("timer_btn", tb_timeout_callback,
RT_NULL, 5,
RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER);
if(timer_btn!=RT_NULL)
{
rt_timer_start(timer_btn);
}
}
本次文章就到这里谢谢大家的观看。
原作者:goldengrandpa