消除開關彈跳現象(de-bounce)、Timer delay、功能選單,以新唐N76S003為例

新年快樂,這是003Clock系列的第三篇文章,前兩篇沒跟上的記得回追一下喔!

(一)製作新唐N76E003/N76S003白光LED電子時鐘套件
(二)以VS Code, SDCC, Git建立新唐8051編譯環境
(三)新唐N76E003/N76S003白光LED電子時鐘套件韌體技巧說明
Part 1:消除開關彈跳現象(de-bounce)、Timer delay、功能選單,以新唐N76S003為例
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而已。在韌體中單純在一個迴圈不斷用if確認開關的邏輯電位,也會讀到數個脈波,誤認按了好幾次,一定會降低使用者體驗的,因此開關除彈跳在設計上是必要的。

 


硬體除彈跳

開關除彈跳有人會從硬體著手,在開關兩端直接並聯上一顆電容,直接與上拉電阻形成一個RC低通濾波器,這個方法很適合用在一些純硬體的電路上。

軟體除彈跳

微控制器其中一個有趣的地方就是,很多電路上遇到的問題,原先要用硬體解決的事情,運用微控制器則可以用軟體去解決。像我自己就喜歡用軟體除彈跳,除了可以節省硬體成本,要調整參數時也直接改韌體就好,相當方便。

最簡單的處理方式也許就是在程式中加上一個夠長的delay,讓程式慢下來。但是這樣難以和使用者操作同步,比方這邊delay一秒,當使用者一秒內連續按個三次,也只會執行一次if中的內容;或者delay 100 ms,當使用者按一次會因為執行速度太快,重複執行好幾次if中的內容。

  1. if (!SW1) {
  2. /* SW1 pressed */
  3. }
  4. delay(1000);

要解決同步問題,就是像下面一樣,開關按下去以後等開關穩定以後,再用while原地等待使用者放開開關。

  1. if (!SW1) {
  2. /* SW1 pressed */
  3. delay(20);
  4. while (!SW1);
  5. }

上述兩種方式都有個共同缺點,就是會讓程式停下來等開關,在等待的期間沒辦法做其他事情。以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秒鐘)則視為長按。

  1. while(1)
  2. {
  3. if (!SW1) {
  4. u16swCount++;
  5. if (u16swCount == 2000)
  6. /* long press */
  7. }
  8.  
  9. if (SW1) {
  10. if (u16swCount > 20 && u16swCount < 2000)
  11. /* short press */
  12. u16swCount = 0;
  13. }
  14.  
  15. /* 1 ms delay */
  16. delay(1);
  17. }

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 counters.

基本上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,清除時是要由軟體介入將溢位旗標清除。

  1. CKCON |= 0x08; // Timer 0 source from Fsys directly
  2. TMOD |= 0x01; // Timer 0 mode 1
  3. TCON |= 0x10; // Timer 0 run
  4.  
  5. /* 1 ms delay */
  6. TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000
  7. TL0 = (uint8_t)(49536 & 0xFF);
  8. while (!(TCON & 0x20)); // Wait until timer 0 overflow
  9. TCON &= 0xDF; // Clear TF0

有仔細看完整程式碼的讀者,會發現填入TH0、TL0與while等待的順序實際上是反過來的,這麼做是為了增加delay的準確性。填入TH0、TL0後才回到整個while迴圈的開頭,這會讓程式執行的同時Timer 0也正在計時我們設定的1 ms,也就是程式執行花費的時間也會納入1 ms的計算,使整個while迴圈幾乎就是1 ms執行一次。

  1. while(1)
  2. {
  3. if (!SW1) {
  4. u16swCount++;
  5. if (u16swCount == 2000)
  6. /* long press */
  7. }
  8.  
  9. if (SW1) {
  10. if (u16swCount > 20 && u16swCount < 2000)
  11. /* short press */
  12. u16swCount = 0;
  13. }
  14.  
  15. /* 1 ms delay */
  16. while (!(TCON & 0x20)); // Wait until timer 0 overflow
  17. TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000
  18. TL0 = (uint8_t)(49536 & 0xFF);
  19. TCON &= 0xDF; // Clear TF0
  20. }

功能選單

003Clock包含了顯示時間、設定時間、讀秒、計時器等功能,因此我就在主程式迴圈中用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:

顯示計時器(0000秒~9959秒)

case 9:

將計時器歸零

default:

u8mode設為0(顯示時間)

以下是選單程式的架構:

  1. void main()
  2. {
  3. uint16_t u16swCount = 0;
  4. uint8_t u8mode = 0;
  5.  
  6. while(1)
  7. {
  8. if (!SW1) {
  9. u16swCount++;
  10. if (u16swCount == 2000) /* long press */
  11. u8mode++;
  12. }
  13.  
  14. switch (u8mode) {
  15. case 0:
  16. if (SW1) {
  17. if (u16swCount > 20 && u16swCount < 2000) /* short press */
  18. u8mode = 6;
  19. u16swCount = 0;
  20. }
  21. break;
  22.  
  23. case 1:
  24. EIE1 &= 0xFD; // Disable timer 3 interrupt
  25. showSetup(u8mode);
  26. if (SW1) {
  27. if (u16swCount > 20 && u16swCount < 2000) {
  28. if (time.hour < 14)
  29. time.hour += 10;
  30. else if (time.hour < 20)
  31. time.hour = 20;
  32. else
  33. time.hour -= 20;
  34. }
  35. u16swCount = 0;
  36. }
  37. break;
  38. case 2:
  39. ...
  40. break;
  41.  
  42. ...
  43. }
  44.  
  45. /* 1 ms delay */
  46. while (!(TCON & 0x20)); // Wait until timer 0 overflow
  47. TH0 = (uint8_t)(49536 >> 8); // 65536 - 16000
  48. TL0 = (uint8_t)(49536 & 0xFF);
  49. TCON &= 0xDF; // Clear TF0
  50. }
  51. }

留言

這個網誌中的熱門文章

無法被取代的指針型三用電表(一):前言

關於新唐科技NuMicro ISP的介紹和使用方式

新唐火神板開箱實作(一):NuMaker-Volcano與NuEclipse IDE入門篇