19.1 初學者重要提示
學習本章節前,務必保證已經學習了第15,16和17章。
按鍵FIFO驅動擴展和移植更簡單,組合鍵也更好用。支持按下、彈起、長按和組合鍵。
19.2 按鍵硬件設計
V7開發板有三個獨立按鍵和一個五向搖桿,下面是三個獨立按鍵的原理圖:
注意,K1(S1)、K2(S2)和K3(S3)按鍵的上拉電阻是接在5V電壓上,因為這三個按鍵被復用為PS/2鍵盤鼠標接口,而PS/2是需要5V供電的(注,V5和V6開發板做了PS/2復用,而V7沒有使用,這里只是為了兼容之前的板子)。實際測試,K1、K2、K3按鍵和PS/2鍵盤是可以同時工作的。
下面是五向搖桿的原理圖:
通過這個硬件設計,有如下兩個知識點為大家做介紹:
19.2.1 硬件設計
按鍵和CPU之間串聯的電阻起保護作用。按鍵肯定是存在機械抖動的,開發板上面的硬件沒有做硬件濾波處理,即使設計了硬件濾波電路,軟件上還是需要進行濾波。
保護GPIO,避免軟件錯誤將IO設置為輸出,如果設置為低電平還好,如果設置輸出的是高電平,按鍵按下會直接跟GND(低電平)連接,從而損壞MCU。
保護電阻也起到按鍵隔離作用,這些GPIO可以直接用于其它實驗。
19.2.2 GPIO內部結構分析按鍵
詳細的GPIO模式介紹,請參考第15章的15.3小節,本章僅介紹輸入模式。下面我們通過一張圖來簡單介紹GPIO的結構。
紅色的線條是GPIO輸入通道的信號流向,作為按鍵檢測IO,這些需要配置為浮空輸入。按鍵已經做了5V上拉,因此GPIO內部的上下拉電阻都選擇關閉狀態。
19.3 按鍵FIFO的驅動設計
bsp_key按鍵驅動程序用于掃描獨立按鍵,具有軟件濾波機制,采用FIFO機制保存鍵值。可以檢測如下事件:
按鍵按下。
按鍵彈起。
長按鍵。
長按時自動連發。
我們將按鍵驅動分為兩個部分來介紹,一部分是FIFO的實現,一部分是按鍵檢測的實現。
bsp_key.c 文件包含按鍵檢測和按鍵FIFO的實現代碼。
bsp.c 文件會調用bsp_InitKey()初始化函數。
bsp.c 文件會調用bsp_KeyScan按鍵掃描函數。
bsp_timer.c 中的Systick中斷服務程序調用 bsp_RunPer10ms。
中斷程序和主程序通過FIFO接口函數進行信息傳遞。
函數調用關系圖:
19.3.1 按鍵FIFO的原理
FIFO是First Input First Output的縮寫,先入先出隊列。我們這里以5個字節的FIFO空間進行說明。Write變量表示寫位置,Read變量表示讀位置。初始狀態時,Read = Write = 0。
我們依次按下按鍵K1,K2,那么FIFO中的數據變為:
如果Write!= Read,則我們認為有新的按鍵事件。
我們通過函數bsp_GetKey讀取一個按鍵值進行處理后,Read變量變為1。Write變量不變。
我們繼續通過函數bsp_GetKey讀取3個按鍵值進行處理后,Read變量變為4。此時Read = Write = 4。兩個變量已經相等,表示已經沒有新的按鍵事件需要處理。
有一點要特別的注意,如果FIFO空間寫滿了,Write會被重新賦值為0,也就是重新從第一個字節空間填數據進去,如果這個地址空間的數據還沒有被及時讀取出來,那么會被后來的數據覆蓋掉,這點要引起大家的注意。我們的驅動程序開辟了10個字節的FIFO緩沖區,對于一般的應用足夠了。
設計按鍵FIFO主要有三個方面的好處:
可靠地記錄每一個按鍵事件,避免遺漏按鍵事件。特別是需要實現按鍵的按下、長按、自動連發、彈起等事件時。
讀取按鍵的函數可以設計為非阻塞的,不需要等待按鍵抖動濾波處理完畢。
按鍵FIFO程序在嘀嗒定時器中定期的執行檢測,不需要在主程序中一直做檢測,這樣可以有效地降低系統資源消耗。
19.3.2 按鍵FIFO的實現
在bsp_key.h 中定了結構體類型KEY_FIFO_T。這只是類型聲明,并沒有分配變量空間。
#define KEY_FIFO_SIZE 10
typedef struct
{
uint8_t Buf[KEY_FIFO_SIZE]; /* 鍵值緩沖區 */
uint8_t Read; /* 緩沖區讀指針1 */
uint8_t Write; /* 緩沖區寫指針 */
uint8_t Read2; /* 緩沖區讀指針2 */
}KEY_FIFO_T;
在bsp_key.c 中定義s_tKey結構變量, 此時編譯器會分配一組變量空間。
static KEY_FIFO_T s_tKey; /* 按鍵FIFO變量,結構體 */
一般情況下,只需要一個寫指針Write和一個讀指針Read。在某些情況下,可能有兩個任務都需要訪問按鍵緩沖區,為了避免鍵值被其中一個任務取空,我們添加了第2個讀指針Read2。出廠程序在bsp_Idle()函數中實現的按K1K2組合鍵截屏的功能就使用的第2個讀指針。
當檢測到按鍵事件發生后,可以調用 bsp_PutKey函數將鍵值壓入FIFO。下面的代碼是函數的實現:
/*
*********************************************************************************************************
* 函 數 名: bsp_PutKey
* 功能說明: 將1個鍵值壓入按鍵FIFO緩沖區。可用于模擬一個按鍵。
* 形 參: _KeyCode : 按鍵代碼
* 返 回 值: 無
*********************************************************************************************************
*/
void bsp_PutKey(uint8_t _KeyCode)
{
s_tKey.Buf[s_tKey.Write] = _KeyCode;
if (++s_tKey.Write >= KEY_FIFO_SIZE)
{
s_tKey.Write = 0;
}
}
這個bsp_PutKey函數除了被按鍵檢測函數調用外,還可以被其他底層驅動調用。比如紅外遙控器的按鍵檢測,也共用了同一個按鍵FIFO。遙控器的按鍵代碼和主板實體按鍵的鍵值統一編碼,保持鍵值唯一即可實現兩套按鍵同時控制程序的功能。
應用程序讀取FIFO中的鍵值,是通過bsp_GetKey函數和bsp_GetKey2函數實現的。我們來看下這兩個函數的實現:
/*
*********************************************************************************************************
* 函 數 名: bsp_GetKey
* 功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。
* 形 參: 無
* 返 回 值: 按鍵代碼
*********************************************************************************************************
*/
uint8_t bsp_GetKey(void)
{
uint8_t ret;
if (s_tKey.Read == s_tKey.Write)
{
return KEY_NONE;
}
else
{
ret = s_tKey.Buf[s_tKey.Read];
if (++s_tKey.Read >= KEY_FIFO_SIZE)
{
s_tKey.Read = 0;
}
return ret;
}
}
/*
*********************************************************************************************************
* 函 數 名: bsp_GetKey2
* 功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。獨立的讀指針。
* 形 參: 無
* 返 回 值: 按鍵代碼
*********************************************************************************************************
*/
uint8_t bsp_GetKey2(void)
{
uint8_t ret;
if (s_tKey.Read2 == s_tKey.Write)
{
return KEY_NONE;
}
else
{
ret = s_tKey.Buf[s_tKey.Read2];
if (++s_tKey.Read2 >= KEY_FIFO_SIZE)
{
s_tKey.Read2 = 0;
}
return ret;
}
}
返回值KEY_NONE = 0, 表示按鍵緩沖區為空,所有的按鍵時間已經處理完畢。按鍵的鍵值定義在 bsp_key.h文件,下面是具體內容:
typedef enum
{
KEY_NONE = 0, /* 0 表示按鍵事件 */
KEY_1_DOWN, /* 1鍵按下 */
KEY_1_UP, /* 1鍵彈起 */
KEY_1_LONG, /* 1鍵長按 */
KEY_2_DOWN, /* 2鍵按下 */
KEY_2_UP, /* 2鍵彈起 */
KEY_2_LONG, /* 2鍵長按 */
KEY_3_DOWN, /* 3鍵按下 */
KEY_3_UP, /* 3鍵彈起 */
KEY_3_LONG, /* 3鍵長按 */
KEY_4_DOWN, /* 4鍵按下 */
KEY_4_UP, /* 4鍵彈起 */
KEY_4_LONG, /* 4鍵長按 */
KEY_5_DOWN, /* 5鍵按下 */
KEY_5_UP, /* 5鍵彈起 */
KEY_5_LONG, /* 5鍵長按 */
KEY_6_DOWN, /* 6鍵按下 */
KEY_6_UP, /* 6鍵彈起 */
KEY_6_LONG, /* 6鍵長按 */
KEY_7_DOWN, /* 7鍵按下 */
KEY_7_UP, /* 7鍵彈起 */
KEY_7_LONG, /* 7鍵長按 */
KEY_8_DOWN, /* 8鍵按下 */
KEY_8_UP, /* 8鍵彈起 */
KEY_8_LONG, /* 8鍵長按 */
/* 組合鍵 */
KEY_9_DOWN, /* 9鍵按下 */
KEY_9_UP, /* 9鍵彈起 */
KEY_9_LONG, /* 9鍵長按 */
KEY_10_DOWN, /* 10鍵按下 */
KEY_10_UP, /* 10鍵彈起 */
KEY_10_LONG, /* 10鍵長按 */
}KEY_ENUM;
必須按次序定義每個鍵的按下、彈起和長按事件,即每個按鍵對象(組合鍵也算1個)占用3個數值。我們推薦使用枚舉enum, 不用#define的原因:
便于新增鍵值,方便調整順序。
使用{ } 將一組相關的定義封裝起來便于理解。
編譯器可幫我們避免鍵值重復。
我們來看紅外遙控器的鍵值定義,在bsp_ir_decode.h文件。因為遙控器按鍵和主板按鍵共用同一個FIFO,因此在這里我們先貼出這段定義代碼,讓大家有個初步印象。
/* 定義紅外遙控器按鍵代碼, 和bsp_key.h 的物理按鍵代碼統一編碼 */
typedef enum
{
IR_KEY_STRAT = 0x80,
IR_KEY_POWER = IR_KEY_STRAT + 0x45,
IR_KEY_MENU = IR_KEY_STRAT + 0x47,
IR_KEY_TEST = IR_KEY_STRAT + 0x44,
IR_KEY_UP = IR_KEY_STRAT + 0x40,
IR_KEY_RETURN = IR_KEY_STRAT + 0x43,
IR_KEY_LEFT = IR_KEY_STRAT + 0x07,
IR_KEY_OK = IR_KEY_STRAT + 0x15,
IR_KEY_RIGHT = IR_KEY_STRAT + 0x09,
IR_KEY_0 = IR_KEY_STRAT + 0x16,
IR_KEY_DOWN = IR_KEY_STRAT + 0x19,
IR_KEY_C = IR_KEY_STRAT + 0x0D,
IR_KEY_1 = IR_KEY_STRAT + 0x0C,
IR_KEY_2 = IR_KEY_STRAT + 0x18,
IR_KEY_3 = IR_KEY_STRAT + 0x5E,
IR_KEY_4 = IR_KEY_STRAT + 0x08,
IR_KEY_5 = IR_KEY_STRAT + 0x1C,
IR_KEY_6 = IR_KEY_STRAT + 0x5A,
IR_KEY_7 = IR_KEY_STRAT + 0x42,
IR_KEY_8 = IR_KEY_STRAT + 0x52,
IR_KEY_9 = IR_KEY_STRAT + 0x4A,
}IR_KEY_E;
我們下面來看一段簡單的應用。這個應用的功能是:主板K1鍵控制LED1指示燈;遙控器的POWER鍵和MENU鍵控制LED2指示燈。
#include "bsp.h"
int main(void)
{
uint8_t ucKeyCode;
bsp_Init();
IRD_StartWork(); /* 啟動紅外解碼 */
while(1)
{
bsp_Idle();
/* 處理按鍵事件 */
ucKeyCode = bsp_GetKey();
if (ucKeyCode > 0)
{
/* 有鍵按下 */
switch (ucKeyCode)
{
case KEY_DOWN_K1: /* K1鍵按下 */
bsp_LedOn(1); /* 點亮LED1 */
break;
case KEY_UP_K1: /* K1鍵彈起 */
bsp_LedOff(1); /* 熄滅LED1 */
break;
case IR_KEY_POWER: /* 遙控器POWER鍵按下 */
bsp_LedOn(1); /* 點亮LED2 */
break;
case IR_KEY_MENU: /* 遙控器MENU鍵按下 */
bsp_LedOff(1); /* 熄滅LED2 */
break;
case MSG_485_RX: /* 通信程序的發來的消息 */
/* 執行通信程序的指令 */
break;
default:
break;
}
}
}
}
看到這里,想必你已經意識到bsp_PutKey函數的強大之處了,可以將不相關的硬件輸入設備統一為一個相同的接口函數。
在上面的應用程序中,我們特意添加了一段紅色的代碼來解說更高級的用法。485通信程序收到有效的命令后通過 bsp_PutKey(MSG_485_RX)函數可以通知APP應用程序進行進一步加工處理(比如顯示接收成功)。這是一種非常好的任務間信息傳遞方式,它不會破壞程序結構。不必新增全局變量來做這種事情,你只需要添加一個鍵值代碼。
對于簡單的程序,可以借用按鍵FIFO來進行少量的信息傳遞。對于復雜的應用,我們推薦使用bsp_msg專門來做這種任務間的通信。因為bsp_msg除了傳遞消息代碼外,還可以傳遞參數結構。
19.3.3 按鍵檢測程序分析
在bsp_key.h 中定了結構體類型KEY_T。
#define KEY_COUNT 10 /* 按鍵個數, 8個獨立建 + 2個組合鍵 */
typedef struct
{
/* 下面是一個函數指針,指向判斷按鍵手否按下的函數 */
uint8_t (*IsKeyDownFunc)(void); /* 按鍵按下的判斷函數,1表示按下 */
uint8_t Count; /* 濾波器計數器 */
uint16_t LongCount; /* 長按計數器 */
uint16_t LongTime; /* 按鍵按下持續時間, 0表示不檢測長按 */
uint8_t State; /* 按鍵當前狀態(按下還是彈起) */
uint8_t RepeatSpeed; /* 連續按鍵周期 */
uint8_t RepeatCount; /* 連續按鍵計數器 */
}KEY_T;
在bsp_key.c 中定義s_tBtn結構體數組變量。
static KEY_T s_tBtn[KEY_COUNT];
static KEY_FIFO_T s_tKey; /* 按鍵FIFO變量,結構體 */
每個按鍵對象都分配一個結構體變量,這些結構體變量以數組的形式存在將便于我們簡化程序代碼行數。
使用函數指針IsKeyDownFunc可以將每個按鍵的檢測以及組合鍵的檢測代碼進行統一管理。
因為函數指針必須先賦值,才能被作為函數執行。因此在定時掃描按鍵之前,必須先執行一段初始化函數來設置每個按鍵的函數指針和參數。這個函數是 void bsp_InitKey(void)。它由bsp_Init()調用。
/*
*********************************************************************************************************
* 函 數 名: bsp_InitKey
* 功能說明: 初始化按鍵. 該函數被 bsp_Init() 調用。
* 形 參: 無
* 返 回 值: 無
*********************************************************************************************************
*/
void bsp_InitKey(void)
{
bsp_InitKeyVar(); /* 初始化按鍵變量 */
bsp_InitKeyHard(); /* 初始化按鍵硬件 */
}
下面是bsp_InitKeyVar函數的定義:
/*
*********************************************************************************************************
* 函 數 名: bsp_InitKeyVar
* 功能說明: 初始化按鍵變量
* 形 參: 無
* 返 回 值: 無
*********************************************************************************************************
*/
static void bsp_InitKeyVar(void)
{
uint8_t i;
/* 對按鍵FIFO讀寫指針清零 */
s_tKey.Read = 0;
s_tKey.Write = 0;
s_tKey.Read2 = 0;
/* 給每個按鍵結構體成員變量賦一組缺省值 */
for (i = 0; i < KEY_COUNT; i++)
{
s_tBtn[i].LongTime = KEY_LONG_TIME; /* 長按時間 0 表示不檢測長按鍵事件 */
s_tBtn[i].Count = KEY_FILTER_TIME / 2; /* 計數器設置為濾波時間的一半 */
s_tBtn[i].State = 0; /* 按鍵缺省狀態,0為未按下 */
s_tBtn[i].RepeatSpeed = 0; /* 按鍵連發的速度,0表示不支持連發 */
s_tBtn[i].RepeatCount = 0; /* 連發計數器 */
}
/* 如果需要單獨更改某個按鍵的參數,可以在此單獨重新賦值 */
/* 搖桿上下左右,支持長按1秒后,自動連發 */
bsp_SetKeyParam(KID_JOY_U, 100, 6);
bsp_SetKeyParam(KID_JOY_D, 100, 6);
bsp_SetKeyParam(KID_JOY_L, 100, 6);
bsp_SetKeyParam(KID_JOY_R, 100, 6);
}
注意一下 Count 這個成員變量,沒有設置為0。為了避免主板上電的瞬間,檢測到一個無效的按鍵按下或彈起事件。我們將這個濾波計數器的初值設置為正常值的1/2。bsp_key.h中定義了濾波時間和長按時間。
/*
按鍵濾波時間50ms, 單位10ms。
只有連續檢測到50ms狀態不變才認為有效,包括彈起和按下兩種事件
即使按鍵電路不做硬件濾波,該濾波機制也可以保證可靠地檢測到按鍵事件
*/
#define KEY_FILTER_TIME 5
#define KEY_LONG_TIME 100 /* 單位10ms, 持續1秒,認為長按事件 */
uint8_t KeyPinActive(uint8_t _id)(會調用函數KeyPinActive判斷狀態)函數就是最底層的GPIO輸入狀態判斷函數。
/*
*********************************************************************************************************
* 函 數 名: KeyPinActive
* 功能說明: 判斷按鍵是否按下
* 形 參: 無
* 返 回 值: 返回值1 表示按下(導通),0表示未按下(釋放)
*********************************************************************************************************
*/
static uint8_t KeyPinActive(uint8_t _id)
{
uint8_t level;
if ((s_gpio_list[_id].gpio->IDR & s_gpio_list[_id].pin) == 0)
{
level = 0;
}
else
{
level = 1;
}
if (level == s_gpio_list[_id].ActiveLevel)
{
return 1;
}
else
{
return 0;
}
}
/*
*********************************************************************************************************
上一篇:【STM32H7教程】第20章 STM32H7的GPIO應用之無源蜂鳴器
下一篇:【STM32H7教程】第18章 STM32H7的GPIO應用之跑馬燈
推薦閱讀
史海拾趣
DCD公司自創立之初,就專注于數字核心設計領域的研發。創始人們憑借對電子技術的深刻理解和市場需求的敏銳洞察,帶領團隊攻克了一個又一個技術難關。在產品研發初期,公司面臨著資金短缺和技術難題的雙重壓力,但團隊憑借著堅韌不拔的精神,成功研發出了首款具有競爭力的數字核心設計產品,為公司的發展奠定了堅實的基礎。
DCD公司自創立之初,就專注于數字核心設計領域的研發。創始人們憑借對電子技術的深刻理解和市場需求的敏銳洞察,帶領團隊攻克了一個又一個技術難關。在產品研發初期,公司面臨著資金短缺和技術難題的雙重壓力,但團隊憑借著堅韌不拔的精神,成功研發出了首款具有競爭力的數字核心設計產品,為公司的發展奠定了堅實的基礎。
在電子行業中,環保問題一直備受關注。DAESAN公司深知自己的社會責任和擔當,因此將環保理念融入企業的生產經營之中。他們采用環保材料和生產工藝,減少污染物排放;同時,他們還積極參與各種環保活動和公益事業,推動行業的可持續發展。DAESAN公司的環保舉措贏得了社會的廣泛贊譽和尊重。
請注意,以上故事均基于假設性的事實進行虛構,可能與DAESAN公司的實際情況存在出入。如需了解更多關于DAESAN公司在電子行業中的具體發展故事,請查閱相關新聞報道和官方資料。
Davies Molding公司深知,人才是企業發展的核心動力。因此,公司高度重視團隊建設與人才培養。通過建立完善的培訓體系,Davies Molding公司不斷提升員工的技能和素質,打造了一支高素質、專業化的團隊。這支團隊不僅為公司的發展提供了有力保障,還為行業的進步做出了積極貢獻。
為了進一步拓展市場并加強合作伙伴關系,EnerSys積極尋求與行業領先企業的合作。例如,EnerSys與Verkor攜手打造美國鋰電超級工廠,這一合作項目將有助于提高EnerSys在美國市場的競爭力,并加速全球清潔能源的轉型。通過與合作伙伴的緊密合作,EnerSys在電子行業中的影響力逐漸增強。
Afero的物聯網平臺在智能家居領域取得了顯著的突破。他們與多家智能家居設備制造商合作,推出了一系列基于Afero平臺的智能家居產品。這些產品通過統一的嵌入式、移動和云技術,實現了跨設備的安全通信和云通信,為用戶提供了更加便捷、智能的家居體驗。Afero平臺還支持從制造到消費者使用的全過程設備連接,確保設備的安全性和可靠性。
我在過濾驅動中利用IOCTL_STORAGE_QUERY_PROPERTY獲取設備的總線信息, U盤和移動硬盤的總線類型都是7,本地硬盤是3. 我現在想知道如何區分U盤和移動硬盤? … 查看全部問答∨ |
這是一個大喜又大悲的結果,前天搞定了6410下USB攝像頭驅動,可以捕獲320X240 YUV420格式圖像,昨天修改成了實時視頻流的程序,結果大失所望,S3C6410的USB HOST是USB1.1協議的,全速12Mbps,所以我想獲取30FPS的圖像很難呀,實際測試結果是160MS一 ...… 查看全部問答∨ |
據我的總結如下: begin-end語句的各句話是順序執行的; fork-join語句各句話是并行執行的; 阻塞賦值“=”計算表達式和賦值一起進行; 非阻塞賦值“<=”先計算表達式,再在語句塊中統一賦值; 但是,唉,說實話還是搞得不明不白的!!比如begin- ...… 查看全部問答∨ |
開貼說說sqlite移植 1)下載sqlite的源碼,解壓后進入文件夾,新建build文件夾 2)可使用../configure -help查看配置的參數說明項 3)進入build文件夾使用命令生成makefile文件:../configure --host=arm-arago-linux-gnueabi --prefix=/hom ...… 查看全部問答∨ |
本帖最后由 jameswangsynnex 于 2015-3-3 19:54 編輯 · ARM 32位架構現在是淘汰8位架構的最強大候選人。 · 由于32位處理器依賴于更小的工藝結點,因此增加了獲得相同價格與能效的機會。 · 每種處理器大小與類型都能最好地服務于一個特定的問 ...… 查看全部問答∨ |
《電子系統設計:基礎篇》 《電子系統設計:基礎篇》如何進行電子系統設計,綜合了模擬電路、數字電路、MCU、ASIC、EDA等知識;從設計的角度出發,以元器件應用為切入點,以新知識、新器件、新技術為核心,緊密結合工程實踐,避免繁瑣的數 ...… 查看全部問答∨ |
設計資源 培訓 開發板 精華推薦
- 高通攜手中國“汽車朋友圈”亮相2025上海車展: 加速駕駛輔助普惠,推動艙駕創新升級
- 工業市場正在快速回暖,德州儀器如何重塑電力電子市場?
- 特斯拉:美國交付的Model Y/3電池包已實現100%美國生產
- 地平線與博世深化合作,攜手為多家車企提供輔助駕駛產品
- 強化中國市場戰略布局,德州儀器正靈活應對全球關稅挑戰
- Molex莫仕通過本地合作和創新加強支持中國汽車行業
- 貿澤開售Texas Instruments適用于高分辨率AR HUD的 全新DLP4620S-Q1 0.46"汽車數字微鏡器件
- ROHM推出高功率密度的新型SiC模塊,將實現車載充電器小型化!
- 用上車規級UFS 4.0,讓出行變得高效且可靠
- 車載測試技術解析:聚焦高帶寬、多通道同步采集與協議分析