中斷的簡介
說起中斷,我們常常就會提到一個經(jīng)典的例子,就是我們在家里處理手頭上事情的時候,熱水煮開了,這時候我們就需要放下手頭的事情,去關(guān)掉煤氣爐。這個就是中斷
書面化的表達就是CPU正在處理某件事時,外部發(fā)生了某一事件,請求CPU迅速處理,CPU暫時中斷當(dāng)前的工作,轉(zhuǎn)而處理所發(fā)生的事情,處理完后,再回到原來被中斷的地方,繼續(xù)原來的工作。
我們有時候會稱中斷服務(wù)程序為前臺程序,循環(huán)中的程序為后臺程序,它們的特點如下所示。
通過中斷機制,在外設(shè)不需要CPU介入時,CPU可以執(zhí)行其他線程,而當(dāng)外設(shè)需要CPU時,通過產(chǎn)生中斷信號使CPU立即停止當(dāng)前線程轉(zhuǎn)而響應(yīng)中斷請求。這樣CPU可以不用老是進行輪詢或者等待的操作,大大提高了系統(tǒng)實時性以及執(zhí)行效率。
但是中斷也不可以濫用,要用的謹慎一些,特別是上了實時操作系統(tǒng)之后。因為無論線程有著多高的優(yōu)先級,中斷都可以打斷線程的運行,因此一般只用于緊急事件,并且只進行簡單的處理,比如說標記事件發(fā)生,利用信號量等內(nèi)核對象通知線程,進行更加復(fù)雜的操作。
創(chuàng)建工程
本次我們再次以RT-Thread提供的STM32F407-RoboMaster-C bsp文件來創(chuàng)建工程。
創(chuàng)建后我們來看一下開發(fā)板原理圖,按鍵KEY對應(yīng)的是PA0_WKUP
我們進入CubeMX.ioc中看到PA0并沒有開啟,所以我們要自行開啟一下,設(shè)置為外部中斷模式。
然后進入GPIO標簽頁中設(shè)置為沿上升/下降沿雙邊觸發(fā),上拉電阻。點擊GENERAYE CODE即可。
原理介紹
下面解釋一下我們在CubeMX中配置的幾個選項是什么意思。
第一個就是我們選擇PA0-WKUP模式為GPIO_EXTI0,是代表了什么呢?想回答這個問題我們就要來看一下我們使用外部中斷的流程。
這里我修改了正點原子的流程圖便于講解。
第一部分涉及的知識比較深,如果是完全沒有接觸過STM32的同學(xué)可以選擇忽略。
如圖所示我們需要設(shè)置輸入模式,這里為外部中斷模式,之后設(shè)置SYSCGF(系統(tǒng)配置寄存器)中的STSCFG_EXTICR1(外部中斷配置寄存器),它的作用是設(shè)置EXTI和IO映射關(guān)系。就如下圖所示,我們配置寄存器位15:0,以選擇EXTIx外部中斷的源輸入。那么EXTIx是什么呢,我們?yōu)槭裁匆渲盟脑摧斎肽?下面馬上解答。
EXTI為擴展中斷/事件控制器,其中有4根輸入線,每個輸入線可以單獨進行配置選擇類型(中斷或事件)和相應(yīng)的觸發(fā)事件(上升沿觸發(fā)、下降沿觸發(fā)、雙邊沿觸發(fā))。
如下圖所示就是EXTI的工作原理圖,我們通過配置相關(guān)的寄存器,芯片內(nèi)部進行或運算之后起作用。EXTI和GPIO之間的映射關(guān)系就在上面的SYSCGF中設(shè)置好了,那么我們這個按鍵對應(yīng)的PA0引腳對應(yīng)的是哪個EXTI擴展中斷/事件線呢?
下圖就很清晰的告訴了我們,EXTIx就對應(yīng)著P*x,因此PA0就對應(yīng)著EXTI0,這里也就講解了我們一開始設(shè)置PA0為GPIO_EXTI0是什么意思了。
流程圖中的NVIC為嵌套向量中斷控制器,它是管理包括內(nèi)核異常在內(nèi)的所有中斷,相關(guān)的知識這里由于篇幅就不展開講了,但是不代表不重要,大家最好自行查看資料學(xué)習(xí)。
下面繼續(xù)解釋一下我們在GPIO標簽頁中配置的雙邊觸發(fā),上拉電阻是什么意思。
我們這里設(shè)置的上升沿觸發(fā)就是配置我們上文提到的EXTI中的觸發(fā)選擇寄存器,至于為什么要選擇沿上升沿觸發(fā)我們就要來看一下原理圖。
根據(jù)原理圖我們可以看到在按下按鍵之后引腳將直連GND,處于低電平。
那么上拉電阻這個選擇即使原理圖沒有畫我們也要自己可以推斷出來了,因為外部中斷的引腳肯定不能是浮空的,因為浮空狀態(tài)下電平是不確定的一會高電平一會低電平會導(dǎo)致中斷的誤觸發(fā)。
然后我們希望按下按鍵之后有電平的變化那么在未按下的狀態(tài),引腳應(yīng)當(dāng)被上拉電阻鉗位在高電平,這一點在原理圖中也得到印證。至于雙邊觸發(fā)就根據(jù)程序而定,后面我們還會用RT-Thread提供的API可以設(shè)置這個中斷觸發(fā)條件的。
程序編寫
這里我會使用兩個方案一個是使用外部中斷方式來進行點燈,還有一種方案是通過MultiButton使用類似于傳感器的方案,開啟一個線程來專門處理按鍵相關(guān)事情。
首先是外部中斷的方案,這里我采取的方案是中斷服務(wù)函數(shù)中釋放信號量,電平翻轉(zhuǎn)操作在線程中執(zhí)行。下面是源代碼,下面這段代碼實現(xiàn)的功能是第一次按下按鍵藍燈亮起,第二次按下按鍵藍燈熄滅以此循環(huán)。這個代碼比較簡單,大家直接看注釋即可。
/*
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
#include
#include
#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;
/ 永久方式等待信號量,獲取到信號量,則執(zhí)行 LED電平翻轉(zhuǎn)的操作 */
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);
/ 綁定中斷,下降沿模式,回調(diào)函數(shù)名為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);
/ 創(chuàng)建一個動態(tài)信號量,初始值是 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;
}
但是上面這個程序還有一個問題就是沒有進行消抖操作。那么消抖是什么呢?
由于按鍵的機械結(jié)構(gòu)具有彈性,按下時開關(guān)不會立刻接通,斷開時也不會立刻斷開,這就導(dǎo)致按鍵的輸入信號在按下和斷開時都會存在抖動,如果不先將抖動問題進行處理,則讀取的按鍵信號可能會出現(xiàn)錯誤。這里我們就需要使用軟件濾波的方法即抖動產(chǎn)生在按鍵按下的邊沿時刻,叫下降沿(電平從高到低),所以只需要在邊沿時進行延時,等到按鍵輸入已經(jīng)穩(wěn)定再進行信號讀取即可。
這里我們主要修改的前臺程序,即線程里的程序,拿到信號量之后,不要直接進行電平翻轉(zhuǎn)操作,延時20ms后再次讀取電平后選擇進行操作。
static void rt_thread1_entry(void parameter)
{
while (1)
{
static rt_err_t result;
static int status;
static int falling_flag;
/ 永久方式等待信號量,獲取到信號量,則執(zhí)行 LED電平翻轉(zhuǎn)的操作 /
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) / 如果延時后發(fā)現(xiàn)是高電平說明是誤觸發(fā)直接返回 /
{
return;
}
else / 驗證為下降沿進行電平翻轉(zhuǎn)操作 */
{
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軟件包并且進行改寫,軟件包的良好生態(tài)也是許多人選擇RT-Thread的原因,這個軟件包是我剛接觸RT-Thread時,參加線上培訓(xùn)的時候蘇李果老師推薦的,用起來體驗也不錯,所以這里也推薦給大家。
在RT-Thread Settings中點擊添加軟件包,找到MultiButton后進行添加。
面向?qū)ο笏枷?/p>
這個按鍵驅(qū)動與RT-Thread源碼一樣包含著面向?qū)ο笏枷搿?/p>
它每個按鍵都抽象為一個按鍵對象,每個按鍵對象都是獨立的,系統(tǒng)中所有的按鍵對象使用單鏈表串起來。
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;
其中在變量后面跟冒號的語法稱為位域,使用位域的優(yōu)勢是節(jié)省內(nèi)存。
這里常常用于一些通信協(xié)議上面,它就相當(dāng)于把uint8_t 一個字節(jié)拆來分別裝不同的變量。
就如下圖所示本來 6 個uint8_t 類型的變量需要占用 6 個字節(jié),但使用位域語法后,這6個變量只占用兩個字節(jié):
但是需要注意的是,位域要求變量內(nèi)存地址要連續(xù),所以個人認為這個結(jié)構(gòu)體要用_packed進行修飾。
按鍵對象單鏈表
MultiButton定義了一個頭指針
static struct button* head_handle = NULL;
用戶插入一個按鍵對象的代碼如下:
//啟動按鍵
button_start(&button1);
button_start的實現(xiàn)如下
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之后的代碼,對象鏈表如下。
狀態(tài)機處理思想
這個在我們RoboMaster電控代碼中有也有大量的體現(xiàn),云臺,底盤多個模式的實現(xiàn)都是使用到了狀態(tài)機。
MultiButton中使用狀態(tài)機來處理每個按鍵對象,在例程中每隔5ms調(diào)用button_tick()依次調(diào)用狀態(tài)機對單鏈表上的所有按鍵對象進行遍歷處理。
void button_ticks(void)
{
struct button* target;
for(target = head_handle; target != NULL; target = target->next)
{
button_handler(target);
}
}
使用button_handler對按鍵對象進行處理,函數(shù)實現(xiàn)如下。
首先調(diào)用該按鍵對象注冊的讀取狀態(tài)函數(shù)進行讀?。?/p>
uint8_t read_gpio_level = handle->hal_button_Level();
讀取之后,判斷當(dāng)前狀態(tài)機的狀態(tài),如果有功能正在執(zhí)行(state不為0),則按鍵對象的tick值加1
if((handle->state) > 0)
{
handle->ticks++;
}
之后進行按鍵消抖,這次連續(xù)讀取了3次。每次延時15ms,如果引腳狀態(tài)一直與之前不同,則改變按鍵對象中的引腳狀態(tài)。
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;
}
最后就進入狀態(tài)機處理,例子如下。
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;
整個狀態(tài)機處理如流程圖所示
之后我們來修改一下代碼,接下來我想要實現(xiàn)三擊的功能,但是軟件包中只有雙擊,因此需要修改代碼。
要修改一下狀態(tài)枚舉體,增加三擊功能。
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;
修改狀態(tài)機處理函數(shù),增加三擊判斷
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文件編寫按鍵回調(diào)代碼,這里的寫法大家可以參照MultiButton提供的樣例進行改寫。
這里實現(xiàn)的功能為單擊、雙擊、三擊開啟不同的燈,長按把所有燈熄滅。
/*
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
#include
#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.rn");
break;
case DOUBLE_CLICK:
rt_pin_write(LED_R_PIN, PIN_HIGH);
rt_kprintf("key double click.rn");
break;
case TRIPLE_CLICK:
rt_pin_write(LED_G_PIN, PIN_HIGH);
rt_kprintf("key triple click.rn");
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.rn");
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);
}
}
-
上拉電阻
+關(guān)注
關(guān)注
5文章
357瀏覽量
30540 -
GPIO
+關(guān)注
關(guān)注
16文章
1189瀏覽量
51837 -
STM32F407
+關(guān)注
關(guān)注
15文章
187瀏覽量
29332 -
RT-Thread
+關(guān)注
關(guān)注
31文章
1261瀏覽量
39840 -
按鍵中斷
+關(guān)注
關(guān)注
0文章
15瀏覽量
6436
發(fā)布評論請先 登錄
相關(guān)推薦
評論