開關(guān)檢測二三事:端口不足,濾波時間不同
熟悉灑家寫作風(fēng)格的朋友們都知道,灑家行文一向生動活潑,甚而有時浮夸得沒個章法。但是,在這個全民戰(zhàn)疫的關(guān)鍵時刻,似乎任何的輕佻都是對前線戰(zhàn)士的不恭敬。故而,今天嚴(yán)肅緊張一把,跟大家開門見山,講一講開關(guān)檢測的問題。
本文引用地址:http://cafeforensic.com/article/202003/411046.htm有的朋友可能會覺得,開關(guān)檢測對于每一個嵌入式工程師來講都是入門級別的問題,有什么好講的呢?好吧,對于你這種想法,我只能像春晚上曉明哥哥對祖兒妹妹講的那樣:
我不要你覺得,我要我覺得!
問你三個問題吧。第一,如果因為這樣或那樣的原因,您選用的MCU的IO口不夠,無法一一對應(yīng)地處理那么多路開關(guān)信號,你該咋個辦?
第二,如何區(qū)分開關(guān)的“動作”和“狀態(tài)”?按下和彈起的動作一閃即逝,狀態(tài)卻長期保持,怎么區(qū)分并處理?
第三,是關(guān)于大家耳熟能詳?shù)臑V波問題,您可不要說只需要進行硬件濾波就夠了這種跌份的話哈。假設(shè)您的開關(guān)信號性質(zhì)有所不同,它們需要的濾波時間也不一樣,你怎么以一種統(tǒng)一的方式去處理他們呢?
今天灑家和大家分享的就是關(guān)于這三個問題的想法和解決方案。
一
要想逃避現(xiàn)實,最好的方式就是深深介入現(xiàn)實之中。
嵌入式工程師的日常工作不是在錦繡河山里做文章,而是在螺絲殼里做道場。在日復(fù)一日的工作中,工程師逐漸積累了豐富的實戰(zhàn)經(jīng)驗。比如硬件不夠可以用軟件來湊,內(nèi)存不足可以犧牲實時性,以時間換空間。
那么,IO口不夠呢?
魯迅先生曾說:“希望是本無所謂有,無所謂無的。這正如地上的路,其實地上本沒有路,走的人多了,也便成了路?!?/p>
勇敢的嵌入式工程師是遇山開路、遇水搭橋的開拓者,面對無路之境,自會以大無畏的霹靂手段趟出一條路來。IO口不夠,MCU又不能換(想一想老板那冷颼颼的目光),自然也會另辟蹊徑,柳暗花明,不至于山窮水復(fù),找不到出路的。
聰明的小伙伴們應(yīng)該已經(jīng)躍躍欲試,準(zhǔn)備搶答了。不過,授人以魚不如授人以漁,在直接給出答案之前,大家可以先想一個問題:為什么現(xiàn)在大家用的計算機,基本上沒有并口了呢?
在計算機剛剛出現(xiàn)的那個年代里,打印和繪圖是非常重要的應(yīng)用。由于計算機速度的限制,串行傳輸速度相當(dāng)有限,無法應(yīng)對打印繪圖這種需要高速數(shù)據(jù)傳輸?shù)膽?yīng)用,于是,并口大行其道數(shù)十載。說句不怕暴露年齡的話,筆者剛上班時,單位的臺式機和筆記本都是有并口的。
當(dāng)然,魚與熊掌不可兼得,并行傳輸也有其缺點,那就是需要的端口和線路遠(yuǎn)高于串行傳輸,會消耗較高的電路板資源和軟件解析能力,所以,隨著計算機和串行傳輸速度的提升,并口也開始慢慢地退出了歷史的舞臺。
觸類旁通,見微知著,大家是不是品出來什么了?
對,在嵌入式設(shè)計中,一個MCU端口處理一路開關(guān)信號是“并行處理方式”,類比于計算機并口,需要消耗較多的端口。端口不夠的解決方案自然是“并行轉(zhuǎn)串行”,以串行的方式進行開關(guān)信號的檢測。
在具體的實現(xiàn)上,需要選擇“多路開關(guān)檢測接口芯片”,這種芯片可以檢測多路開關(guān)量輸入信號,并將檢測到的開關(guān)狀態(tài)通過SPI發(fā)送給MCU。這種方式可以極大地節(jié)省MCU的IO口資源,比如說檢測16路開關(guān),并行方式需要16個MCU IO端口,串行方式只需要一個SPI端口就可以了。
二
話不多說,再來看第二個問題:怎么區(qū)分開關(guān)的“動作”和“狀態(tài)”?
至于為什么要區(qū)分“動作”和“狀態(tài)”。是因為在嵌入式產(chǎn)品中,有一種很常見的應(yīng)用邏輯:開關(guān)A、B、C處于閉合狀態(tài)且開關(guān)D、E、F處于斷開狀態(tài)時,按下或松開開關(guān)G,執(zhí)行某個操作。
在這種邏輯里,“按下”和“松開”是兩種動作,“閉合”和“斷開”是兩種狀態(tài)。用電路的知識來類比的話,動作是沿跳變,狀態(tài)是電平。
“動作”是一閃即逝的花火,狀態(tài)是千年不變的承諾。我們做區(qū)分為的是,讓動作“閱后即焚”,不至于成為反復(fù)觸發(fā)操作的脈搏。
為了說明這一點,灑家跟大家分享一下自己設(shè)計的結(jié)構(gòu)體和代碼實現(xiàn),這部分也可以用在對第三個問題的解答上。
typedef struct{
unsigned switch_state:1;
unsigned swon_event:1;
unsigned swoff_event:1;
unsigned cursw:1;
unsigned detect_cnt:4;
e_SwId switch_id;
}s_Switch;
在這個結(jié)構(gòu)體的成員變量里面,switch_id標(biāo)識開關(guān)節(jié)點,大家可以用“解釋性”很強的枚舉來表示它。這里的switch_state表示的是開關(guān)信號的狀態(tài),swon_event和swoff_event分別表示開關(guān)從斷開到閉合和從閉合到斷開的變化,即上述的“動作”。 cursw和detect_cnt用于開關(guān)信號采集的軟件消抖功能。
為了同時檢測開關(guān)狀態(tài)和動作,可以設(shè)置一個10ms的周期定時器,周期性地對每個SWITCH_ID對應(yīng)的開關(guān)信號進行檢測,具體實現(xiàn)為:
void SwDetect(e_SwId sw_id)
{
uint8_t filter_time;
filter_time = SW_DETECT_TIMES;
if(SWITCH_OFF == Sw[sw_id].switch_state){
if(IOVALID == Sw[sw_id].cursw){
if(Sw[sw_id].detect_cnt < filter_time){
Sw[sw_id].detect_cnt++;
}else{
Sw[sw_id].switch_state = SWITCH_ON;
Sw[sw_id].detect_cnt = 0;
Sw[sw_id].swon_event = 1;
}
}else{
Sw[sw_id].detect_cnt = 0;
}
}else{
if(IOINVALID == Sw[sw_id].cursw){
if(Sw[sw_id].detect_cnt < filter_time){
Sw[sw_id].detect_cnt++;
}else{
Sw[sw_id].switch_state = SWITCH_OFF;
Sw[sw_id].swoff_event = 1;
Sw[sw_id].detect_cnt = 0;
}
}else{
Sw[sw_id].detect_cnt = 0;
}
}
}
當(dāng)開關(guān)動作發(fā)生時,swon_event和swoff_event置一,在執(zhí)行完相關(guān)操作之后,將swon_event和swoff_event清零,就完成了讓動作“閱后即焚”。
所以,上面那種根據(jù)某些開關(guān)的狀態(tài)和動作執(zhí)行相關(guān)操作的邏輯的具體實現(xiàn)為:
If(Switch[SWITCH_ID_1].swon_event == 1)
{
If(Switch[SWITCH_ID_2].switch_state == “ON”){
操作1;
}
Switch[SWITCH_ID_1].swon_event = 0;
}
三
下面接著講第三個問題:怎么應(yīng)對不同的濾波時間?
正如上面講過的那樣,對于一般的開關(guān)節(jié)點,設(shè)計一10ms的定時器周期性地讀取開關(guān)當(dāng)前狀態(tài)cursw,然后根據(jù)其維持當(dāng)前狀態(tài)的周期次數(shù)(根據(jù)不同應(yīng)用場景,可以設(shè)置為5次或者10次,分別對應(yīng)50ms或100ms的濾波時間)以判斷switch_state、swon_event、swoff_event。
那么,對于那些特殊的開關(guān)信號,也許需要采用較典型值長或者短的消抖時間,我們只需要針對該開關(guān)信號對應(yīng)的那個SWITCH_ID表征的結(jié)構(gòu)體變量,設(shè)置它的濾波次數(shù)filter_time(見上面那段程序)即可。
四
講到這里,有些不愛看代碼的同學(xué)可能模糊了,這里,幫人幫到底,灑家不惜筆墨,詳細(xì)開展一番。
首先,設(shè)定一個10ms的定時器,在它的中斷服務(wù)程序里,執(zhí)行開關(guān)信號檢測。
對應(yīng)在我們這里,可以認(rèn)為它的中斷服務(wù)程序(ISR)執(zhí)行的就是下面這個IoInputDetect函數(shù)。(需要說明的是,一般情況下我們不會在中斷服務(wù)程序里執(zhí)行這種耗時較長的程序,這里只是為了方便大家理解)
void IoInputDetect(void)
{
e_SwId sw_idx;
ReadIoSwitch();
for(sw_idx = MIN_SWITCH;sw_idx < MAX_SWITCH;sw_idx++){
SwDetect(sw_idx);
}
}
這個函數(shù)里面,在ReadIoSwitch函數(shù)里面讀取每個開關(guān)(以SWITCH_ID標(biāo)識)的當(dāng)前狀態(tài),賦給其cursw,需要注意的是,這里的cursw表示的是當(dāng)下這一刻的開關(guān)狀態(tài),不是經(jīng)過濾波處理后的穩(wěn)定開關(guān)狀態(tài)。
第二步:根據(jù)每個開關(guān)的當(dāng)前狀態(tài)cursw,判斷其穩(wěn)定的開關(guān)狀態(tài)switch_state、開關(guān)動作swon_event和swoff_event。即上面在for循環(huán)中執(zhí)行的SwDetect函數(shù)。
SwDetect函數(shù)語句在第二節(jié)中,它的核心思想就是判斷開關(guān)當(dāng)前狀態(tài)cursw是否持續(xù)穩(wěn)定在SWITCH_ON或者SWITCH_OFF狀態(tài)。當(dāng)前的switch_state為ON的狀態(tài)下,如果持續(xù)filter_time個10ms,cursw一直為OFF狀態(tài),則將switch_state賦為OFF狀態(tài),同時,將swoff_event賦為1。反之亦然。
當(dāng)濾波時間不同時,顯然只需要將該switch_id對應(yīng)的開關(guān)結(jié)構(gòu)體的filter_time置為不同于典型值的特殊值即可。
后記
灑家在這篇文章里面分享的開關(guān)檢測方法,不止適用于數(shù)字IO形式的開關(guān)信號,還適用于其它信號。
比如通過RF方式接收的遙控信號,雖然是一種射頻性質(zhì)的信號,但是這種信號對應(yīng)的是遙控器上的物理按鍵,它在邏輯上自然也等價于本文講的開關(guān)信號,所以,可以用上述那個結(jié)構(gòu)體和那些代碼判斷遙控信號,解析出某個遙控按鍵按下、松開的動作和狀態(tài),同時對它進行濾波處理。
再舉一反三,無論是RF信號、模擬信號、數(shù)字信號、網(wǎng)絡(luò)信號,只要該輸入信號在邏輯上可以等價于物理開關(guān),它就可以使用本文所述的方法處理。
你覺得呢?
評論