開關除彈跳(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:

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

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
    }
}

留言

這個網誌中的熱門文章

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

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

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