開關除彈跳(de-bounce)、Timer delay、功能選單,以新唐N76S003為例
新年快樂,這是003Clock系列的第三篇文章,前兩篇沒跟上的記得回追一下喔!
(一)製作新唐新唐N76E003/N76S003白光LED電子時鐘套件
(二)以VS Code, SDCC, Git建立新唐8051編譯環境
(三)新唐N76E003/N76S003白光LED電子時鐘套件韌體技巧說明
Part 1:開關除彈跳(de-bounce)、Timer delay、功能選單
Part 2:Timer auto-reload產生中斷來精確計時,以函式指標(unction pointer)切換中斷服務程式的功能
Part 3:以N76S003的SPI控制PT6961白光LED七段顯示器
(四)免用燒錄器更新韌體:NuMicro ISP Programming Tool
接下來要介紹003Clock的韌體技巧,文章寫到一半發現內容實在太多了,還是將它分為三篇來說明。完整程式碼請見GitHub source code。https://github.com/danchouzhou/003Clock/blob/4cb1b697c0290f18e6e634b82753c02781767a5e/firmware/003Clock/main.c
開關除彈跳(de-bounce)
彈跳現象
理想的觸摸開關(Tact switch)應該是按下去形成導通,放開恢復為斷路。但實際上因為由機械構造組成的開關,在切換過程中會有一段時間像是彈簧一樣,接點反覆地開開合合,產生短暫的不穩定的情形,稱之為「彈跳現象」。這段不穩定的時間一般都會在50 ms以內結束。
如下圖所示,雖然我們只按了一下開關,但是卻測量到數個脈波。以前實習課拿開關當成計數器時脈的讀者一定很有經驗,這種信號當成送進去計數器,按一下肯定不只加1 XD同樣道理,在韌體中單純在一個迴圈不斷用if確認開關的邏輯電位,也會讀到數個脈波,誤認按了好幾次,一定會降低使用者體驗的,因此開關除彈跳在設計上是必要的。
硬體除彈跳
開關除彈跳有人會從硬體著手,在開關兩端直接並聯上一顆電容,直接與上拉電阻形成一個RC低通濾波器,這個方法很適合用在一些純硬體的電路上。
軟體除彈跳
微控制器其中一個有趣的地方就是,很多電路上遇到的問題,原先要用硬體解決的事情,運用微控制器則可以用軟體去解決。像我自己就喜歡用軟體除彈跳,除了可以節省硬體成本,要調整參數時也直接改韌體就好,相當方便。
最簡單的處理方式也許就是在程式中加上一個夠長的delay,讓程式慢下來。但是這樣難以和使用者操作同步,比方這邊delay一秒,當使用者一秒內連續按個三次,也只會執行一次if中的內容;或者delay 100 ms,當使用者按一次會因為執行速度太快,重複執行好幾次if中的內容。
if (!SW1) { /* SW1 pressed */ } delay(1000);
要解決同步問題,就是像下面一樣,開關按下去以後等開關穩定以後,再用while原地等待使用者放開開關。
if (!SW1) { /* SW1 pressed */ delay(20); while (!SW1); }
上述兩種方式都有個共同缺點,就是會讓程式停下來等開關,在等待的期間沒辦法做其他事情。以003Clock的應用來說可能沒事,因為七段顯示器是由內建的IC PT6961掃描驅動的,顯示的內容也是在Timer 3中斷服務程式中進行更新。
像是之前的應用範例,直接以MCU掃描驅動標準的七段顯示器,停下來等開關同時也會讓掃描停下來,顯示就會異常。總之,佔著CPU資源停下來等絕對不是件好事。
Non-blocking軟體除彈跳,還能分辨長按與短按
003Clock只有一個觸摸開關,功能選單是以開關長按與短按來區分[設定/確認]和[調整]來設計,因此軟體除了要能夠除彈跳之外,還要能分辨長按與短按。回顧第一篇提到的操作方式:
- 單一按鈕操作介面,操作直覺,不會久了忘記每顆按鈕是什麼功能
- 短按開關依序切換顯示模式: 時鐘(時針:分針) > 時鐘秒針 > 計時器(最多99分59秒)
- 持續按著開關大於兩秒為長按,時間模式長按可設定時間,設定完畢時間的秒針會自動歸零;計時器模式長按將計時器歸零
- 切換顯示模式後,程式都會在背景繼續計時,例如: 切換為顯示時間,計時器仍會繼續計時,下次切換成計時器,能夠正確顯示計時器時間
在這裡我就想了一個方法,在一個1 ms的迴圈不斷去判斷開關的邏輯電位,是0的話(按下去為0)就將u16swCount這個計數值加1,超過2000視為長按;放開以後邏輯電位變成1,判斷看看計數值是否介於20 ~ 2000,將這個區間定義為短按。由於迴圈以1 ms為單位,因此計數值與開關按下的時間有直接的關係。這邊計數值設為20就表示低電位信號要持續保持20 ms才視為短按的有效信號,而持續超過2000 ms(2秒鐘)則視為長按。
while(1) { if (!SW1) { u16swCount++; if (u16swCount == 2000) /* long press */ } if (SW1) { if (u16swCount > 20 && u16swCount < 2000) /* short press */ u16swCount = 0; } /* 1 ms delay */ delay(1); }
Timer delay
那麼1 ms的迴圈怎麼產生?那就是加delay!在8051自己從底層寫就不像Arduino那麼方便,能直接呼叫delay()函式。但是不用擔心,了解原理以後就能如魚得水。
寫組合語言時,由於程式開發者還能充分掌握CPU到底執行了哪些指令,且每條指令都查得到它要花幾個clock cycle,因此能夠寫一個迴圈執行一定次數的NOP指令,達到delay的功能。
在C語言我們可以用空的for迴圈做delay,大致能夠評估for迴圈執行幾次、要多久時間。不過,我們很難掌握編譯器把我們寫的高階語言編出什麼東西來;即便真的很熟悉編譯器了,也可能在換了一個版本的編譯器,編出來的內容有所差異(編譯器能保證程式邏輯正確,但不能保證執行時間、效率),這會讓程式很難維護。所以希望delay時間準確的話,我們就要依靠MCU中的硬體計時器(Timer)來計時。
在標準型號的8051就有提供Timer 0和1,2組計時器。如下表,2組計時器都有Mode 0~3,4種運作模式,它們都是上數計數器。詳細內容還請讀者看一下N76S003手冊中的說明:
https://www.nuvoton.com/export/resource-files/en-us--TRM_N76S003_Series_EN_Rev1.00.pdf#page=226
TMOD |
Mode |
Description |
|
M1 |
M0 |
||
0 |
0 |
0 |
13-bit timer (to compatible with the predecessor 8048) THx and TLx are combined into a 13-bit counter. The upper three
bits of TLx are ignored in this mode. |
0 |
1 |
1 |
16-bit timer THx and TLx are combined into a 16-bit counter. |
1 |
0 |
2 |
8-bit auto reload timer THx holds the reload value, TLx as counter. Reload THx value automatically while TLx overflow. |
1 |
1 |
3 |
Two separate 8-bit timers THx and TLx operate independently as two 8-bit timers. |
基本上Timer的運作就是溢位時,硬體會自己將timer overflow flag ‘TFx’設為1,那麼怎麼用Timer做到delay的功能?比方這邊我們要產生一個1 ms的delay,就可以來算算看多少個時脈數目是1 ms,將溢位數值扣除算出的時脈數目預先填入Timer當中,然後用一個while迴圈原地等待,直到Timer溢位,TFx變為1為止,即可做到delay的功能。
我使用Timer 0來做delay的功能,首先從CKCON這個暫存器將Timer 0的時脈來源從預設的Fsys/12,改為直接從Fsys輸入,Fsys就是003Clock電路中的震盪器16 MHz。16 MHz的週期是1 / 16 MHz,如果要產生1 ms的delay就需要16000個時脈。
1 / 16
MHz x 16000 = 1 ms
16000個時脈需要Mode 1的16-bit才算得到,所以從TMOD將Timer 0模式選擇成Mode 1,由TH0和TL0組合成一個16-bit的Timer。16-bit溢位數值65536扣除16000就是49536,將TH0和TL0分別填入49536的高8位元與低8位元。TCON & 0x20是從TCON這的暫存器中提取出TF0 -- Timer 0的溢位旗標,溢位時硬體會自己把這個旗標設為1,用while迴圈原地等待直到TF0變為1。最後要記得把TF0清為0,清除時是要由軟體介入將溢位旗標清除。
CKCON |= 0x08; // Timer 0 source from Fsys directly TMOD |= 0x01; // Timer 0 mode 1 TCON |= 0x10; // Timer 0 run /* 1 ms delay */ TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000 TL0 = (uint8_t)(49536 & 0xFF); while (!(TCON & 0x20)); // Wait until timer 0 overflow TCON &= 0xDF; // Clear TF0
有仔細看完整程式碼的讀者,會發現填入TH0、TL0與while等待的順序實際上是反過來的,這麼做是為了增加delay的準確性。填入TH0、TL0後才回到整個while迴圈的開頭,這會讓程式執行的同時Timer 0也正在計時我們設定的1 ms,也就是程式執行花費的時間也會納入1 ms的計算,使整個while迴圈幾乎就是1 ms執行一次。
while(1) { if (!SW1) { u16swCount++; if (u16swCount == 2000) /* long press */ } if (SW1) { if (u16swCount > 20 && u16swCount < 2000) /* short press */ u16swCount = 0; } /* 1 ms delay */ while (!(TCON & 0x20)); // Wait until timer 0 overflow TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000 TL0 = (uint8_t)(49536 & 0xFF); TCON &= 0xDF; // Clear TF0 }
功能選單
功能選單是由switch case組成,並由變數u8mode來選擇顯示時間、設定時間、秒針顯示、計時器等功能。u8mode會在長按觸摸開關時加1,在部分case之下也會直接改變u8mode的值,例如case 4設定完分鐘長按進入case 5,在case 5中就會將u8mode設為0,下次執行迴圈時就會執行case 0中顯示時間的功能。
switch (u8mode) |
功能說明 |
case 0: |
顯示時間 |
case 1: |
關閉Timer 3中斷停止計時、設定時間 小時 十位數 |
case 2: |
設定時間 小時
個位數 |
case 3: |
設定時間 分鐘
十位數 |
case 4: |
設定時間 分鐘
個位數 |
case 5: |
將秒針歸零、啟動Timer 3中斷開始計時、將u8mode設為0(顯示時間) |
case 6: |
顯示秒針 |
case 7: |
留空,會去執行default: 中的程式 |
case 8: |
顯示計時器(00分00秒~99分59秒) |
case 9: |
將計時器歸零 |
default: |
將u8mode設為0(顯示時間) |
以下是選單程式的架構:
void main() { uint16_t u16swCount = 0; uint8_t u8mode = 0; while(1) { if (!SW1) { u16swCount++; if (u16swCount == 2000) /* long press */ u8mode++; } switch (u8mode) { case 0: if (SW1) { if (u16swCount > 20 && u16swCount < 2000) /* short press */ u8mode = 6; u16swCount = 0; } break; case 1: EIE1 &= 0xFD; // Disable timer 3 interrupt showSetup(u8mode); if (SW1) { if (u16swCount > 20 && u16swCount < 2000) { if (time.hour < 14) time.hour += 10; else if (time.hour < 20) time.hour = 20; else time.hour -= 20; } u16swCount = 0; } break; case 2: ... break; ... } /* 1 ms delay */ while (!(TCON & 0x20)); // Wait until timer 0 overflow TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000 TL0 = (uint8_t)(49536 & 0xFF); TCON &= 0xDF; // Clear TF0 } }
留言
張貼留言