中斷的簡介
說起中斷,我們常常就會提到一個經典的例子,就是我們在家里處理手頭上事情的時候,熱水煮開了,這時候我們就需要放下手頭的事情,去關掉煤氣爐。這個就是中斷
書面化的表達就是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
#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;
/ 永久方式等待信號量,獲取到信號量,則執行 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
#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);
}
}
-
上拉電阻
+關注
關注
5文章
366瀏覽量
31044 -
GPIO
+關注
關注
16文章
1269瀏覽量
53529 -
STM32F407
+關注
關注
15文章
188瀏覽量
30225 -
RT-Thread
+關注
關注
32文章
1368瀏覽量
41490 -
按鍵中斷
+關注
關注
0文章
15瀏覽量
6511
發布評論請先 登錄
基于RoboMasterC板的RT-Thread使用分享—PWM擴展實驗

RT-Thread編程指南
RT-Thread Studio驅動SD卡

RT-Thread學習筆記 RT-Thread的架構概述

基于RoboMasterC型開發板的RT-Thread使用分享(一)
基于RoboMasterC型開發板的RT-Thread使用分享(二)
RT-Thread文檔_RT-Thread 潘多拉 STM32L475 上手指南

RT-Thread v5.0.2 發布

評論