2026年4月26日星期日

韌體工程師需要懂一點硬體:Timer 計時器概念與實務應用

在嵌入式系統中,有一個幾乎無所不在、卻常常被低估的重要模組,就是 Timer(計時器)。無論是最基本的 LED 閃爍、系統運行時間統計,還是進階的 PWM 控制與週期性任務排程,都離不開 Timer 的運作。很多時候,我們在撰寫韌體時,只是「把 Timer 設起來讓它動」,但當遇到計時不準、頻率錯誤,或是系統行為異常時,才發現問題往往不是出在程式邏輯,而是對底層硬體運作理解不夠深入。

其實,Timer 的本質並不複雜,可以把它想像成一個搭配時脈(Clock)運作的計數器,就像日常生活中的碼表或鬧鐘一樣:時脈提供節奏,計數器負責累積,當達到某個條件時觸發對應的行為。

本篇文章將從硬體角度出發,說明 Timer 的基本運作原理、時脈來源與計時概念,並整理實務上常見的計時誤差原因,幫助韌體工程師建立更扎實的底層觀念,為後續實際應用(如 MCU Timer 設定與範例)打下基礎。



一、計時器使用概念

計時器的運作方式主要可以分為幾種基本模式:
  • 上數(Up Counter):從 0 開始累加,直到達到設定值
  • 下數(Down Counter):從設定值開始往下遞減至 0
當計數器達到指定條件時,通常會觸發事件(例如中斷或輸出變化),並依設定進行:
  • 自動重載(Auto-reload):計數器自動回到初始值,持續週期性運作
  • 單次模式(One-shot):計數完成後停止

二、時間誤差觀念

Timer 的精準度取決於其時脈來源(Clock Source),而這些時脈通常來自實體振盪器(如晶振或內部 RC 振盪器),本質上仍會存在微小誤差,這些誤差在短時間內幾乎難以察覺,但隨著時間累積,可能會逐漸放大。例如:每秒僅有極小誤差,長時間運行後,可能累積成數秒甚至數分鐘的偏差。


    Timer 的誤差主要來自於「時脈穩定性」,而時脈又會受到晶振品質、溫度與電壓變動影響。
    常見誤差來源
  1. 晶振誤差(Frequency tolerance)
  2. 溫度影響(Temperature drift)
  3. 電壓變動(Voltage variation)

小故事

小時候,不論是指針式還是數位手錶,為了能夠準確掌握下課鐘聲,總會特別把時間對準。當鐘聲響起的那一刻,按下調整鍵,讓手錶與鐘聲同步,開始下一堂課休息的倒數。

而現在的手錶,多半支援 Wi-Fi 或藍牙,可以自動與網路時間同步,不需要再手動校正,也大幅降低了時間誤差累積的問題。



三、如何減少誤差

在這個部分首先要釐清問題的種類,因為 Timer 的誤差並不是單一原因造成,也沒有一種方法可以套用在所有情境。在實務上,應先判斷應用需求,再選擇合適的改善方式。以下常見分為三類

    案例一、低精度週期應用

    這類應用通常不需要高精度時間,只要大致週期正確即可 ex. LED / Timeout / 簡單週期 。

    常見問題:
  • Timer 設定錯誤(clock / prescaler)
  • ISR 執行時間過長
    建議作法:
  • 確認 clock source 與分頻設定正確
  • ISR 僅執行必要操作
  • 避免使用 printf 或大量運算
    誤差在可接受範圍內即可,不需過度設計

    案例二、長時間計時(RTC / System Time)

    這類的應用主要是時間的長時間累積而導致的誤差

    建議作法:
  • 使用精度較高的晶振(Crystal / TCXO)
  • 避免極端溫度與電壓變化
  • 長時間運作時,定期校正時間
  • 使用時間校正機制(如 RTC、NTP)
    問題不在短時間,而是在「累積」

    案例三、高穩定波形輸出(PWM)

    PWM 的問題通常不是「慢慢偏掉」,而是「瞬間不穩定」,特別是在使用 Software PWM(透過 Timer interrupt 控制)時:
    常見問題:
  • ISR 執行時間不固定
  • 中斷延遲(latency)
  • Duty cycle 抖動(jitter)
    建議作法:
  • 優先使用硬體 PWM 模組
  • 減少 ISR 執行時間
  • 避免在 ISR 中做複雜運算
  • 使用示波器確認實際波形
  • 確認操作 PWM 硬體的流程
    PWM 不準通常是「抖動問題」,不是 ppm 誤差


結語

Timer 看似只是簡單的計數器,但其準確度與穩定性,實際上取決於時脈來源、硬體設計與韌體使用方式,在不同應用情境下,應選擇合適的設計策略,才能在精度與系統負擔之間取得平衡。

2026年4月14日星期二

NK-N9H31A2 開發板:GPIO input

NK-N9H31A2 開發板:GPIO output 中,已經可以控制 LED 亮滅,接下來偵測按鈕並控制 LED 亮滅,關於 GPIO input 的知識可以參考韌體工程師需要懂一點硬體:GPIO 輸入輸出模式與實務應用整理

實驗目的

使用提供的函式偵測板子上的按鈕,按下後 LED 亮,反之則滅掉

使用範例

在官方的 SampleCode 中,找到 GPIO,匯入方式就不再贅述,可以參考上一篇

電路說明
        由於下載下來的範例程式中,對應的腳位不一樣,所以要依照板子上的電路圖稍作調整將程式碼中的按鍵腳位設定改為 PI9 及 PH1
        由於按鍵電路使用外部 pull-up 電阻,未按下時 GPIO 讀到為 1,按下時接地,GPIO 讀到為 0,因此按鍵為 Active Low 設計。
        按鍵狀態最終會反映在 GPIO 的輸入數值中,透過函式讀取即可判斷目前按鍵狀態。
       

程式碼

        以下的程式碼可以直接複製貼上,此篇文章中主要學會使用函式偵測按鍵是否按下的數值,並控制對應的 LED 亮滅,所以將原本官方的程式進行修改,僅保留偵測按鍵及控制 LED 的部分
int main(void)
{
    int32_t i32Err;

    sysDisableCache();
    sysFlushCache(I_D_CACHE);
    sysEnableCache(CACHE_WRITE_BACK);
    sysInitializeUART();

    sysprintf("+-------------------------------------------------+\n");
    sysprintf("|                 GPIO Sample Code                |\n");
    sysprintf("+-------------------------------------------------+\n\n");

    /* Configure Port F10 to output mode and pull-up, LED_1 */
	GPIO_OpenBit(GPIOF, BIT10, DIR_OUTPUT, PULL_UP);
    /* Configure Port I10 to output mode and pull-up, LED_2 */
	GPIO_OpenBit(GPIOI, BIT10, DIR_OUTPUT, PULL_UP);
    
    /* Configure Port I10 to input mode and pull-up, KEY_2 */
	GPIO_OpenBit(GPIOH, BIT1, DIR_INPUT, PULL_UP);
    /* Configure Port I9 to input mode and pull-up, KEY_1 */
	GPIO_OpenBit(GPIOI, BIT9, DIR_INPUT, PULL_UP);

    while (1)
    {
        /* use KEY_1 control LED_1 */
        if (GPIO_ReadBit(GPIOI, BIT9) == 0) /* KEY_1 is push down */
        {
        	GPIO_SetBit(GPIOF, BIT10); /* LED_1 OFF */
        }
        else
        {
            GPIO_ClrBit(GPIOF, BIT10); /* LED_1 ON */
        }
        
        /* use KEY_2 control LED_2 */
        if (GPIO_ReadBit(GPIOH, BIT1) == 0) /* KEY_2 is push down */
        {
        	GPIO_SetBit(GPIOI, BIT10); /* LED_2 OFF */
        }
        else
        {
        	GPIO_ClrBit(GPIOI, BIT10); /* LED_2 ON */
        }
    }
    
//    GPIO_CloseBit(GPIOF, BIT10);
//    GPIO_CloseBit(GPIOI, BIT10);
//    GPIO_CloseBit(GPIOH, BIT1);
//    GPIO_CloseBit(GPIOI, BIT9);

    while(1);
}

函式說明

這裡只針對上面程式使用到的 GPIO 函式說明,更多說明可以參考 gpio.h gpio.c
  • GPIO_OpenBit 
    • 設定單一 GPIO 腳位的模式(port, bit, direction, pull)
  • GPIO_SetBit 
    • 將指定 port 的 bit 驅動為高電位(1)
  • GPIO_ClrBit 
    • 將指定 port 的 bit 驅動為低電位(0)
  • GPIO_CloseBit 
    • 將單一 bit 恢復為預設/關閉狀態
  • GPIO_ReadBit  
    • 讀取偵測到的 GOIO 狀態,高電位(1)低電位(0)

實際效果



程式參考

        Nuvoton 官方 github https://github.com/OpenNuvoton/N9H31_NonOS

延伸閱讀

         

2026年4月13日星期一

NK-N9H31A2 開發板:GPIO output

開發環境建立好之後,當然就是要開始寫程式,那怎麼知道寫的程式碼動作正確運行,就是使用GPIO 控制 LED 燈閃爍啦,這篇文章僅說明腳位輸出控制,不包含中斷,未來會在別的章節進行說明,GPIO 的一些知識可以參考"GPIO 輸入輸出模式與實務應用整理"

實驗目的

透過程式控制板子上的 LED 燈亮面,使用提供的函式

使用範例

在官方的 SampleCode 中,找到 GPIO,匯入方式就不再贅述,可以參考此篇

電路說明

        由於下載下來的範例程式中,對應的腳位不一樣,所以要依照板子上的電路圖稍作調整將程式碼中的腳位設定改為 PI10 及 PF10
        在 N9H31 中,GPIO 控制 LED 的本質是透過設定對應的暫存器位元,而 LED 是否亮起,取決於外部電路的接法(開發板 Active Low)。
N9H31 開發板上的 LED

程式碼

        以下的程式碼可以直接複製貼上,此篇文章中主要學會使用函式控制 LED 亮滅,所以將原本官方的程式進行修改,僅保留控制 LED 的部分
void delay(uint32_t count)
{
	while(count--);
}

int main(void)
{
   sysDisableCache();
   sysFlushCache(I_D_CACHE);
   sysEnableCache(CACHE_WRITE_BACK);
   sysInitializeUART();
   sysprintf("+-------------------------------------------------+\n");
   sysprintf("|                 GPIO Sample Code                |\n");
   sysprintf("+-------------------------------------------------+\n\n");
   /* Configure Port F10 to output mode and pull-up, LED_2 */
	GPIO_OpenBit(GPIOF, BIT10, DIR_OUTPUT, PULL_UP);
   /* Configure Port I10 to output mode and pull-up, LED_1 */
	GPIO_OpenBit(GPIOI, BIT10, DIR_OUTPUT, PULL_UP);
   /* Blink LEDs */
   for (int i = 0; i &lt 10; i++)
   {
       GPIO_SetBit(GPIOF, BIT10);
       GPIO_SetBit(GPIOI, BIT10);
       delay(0xFFFFFF);
       GPIO_ClrBit(GPIOF, BIT10);
       GPIO_ClrBit(GPIOI, BIT10);
       delay(0xFFFFFF);
   }
   GPIO_CloseBit(GPIOF, BIT10);
   GPIO_CloseBit(GPIOI, BIT10);
   sysprintf("  [OK].\n");
   while(1);
}

函式說明

這裡只針對上面程式使用到的 GPIO 函式說明,更多說明可以參考 gpio.h gpio.c
  • GPIO_OpenBit 
    • 設定單一 GPIO 腳位的模式(port, bit, direction, pull)
  • GPIO_SetBit 
    • 將指定 port 的 bit 驅動為高電位(1)
  • GPIO_ClrBit 
    • 將指定 port 的 bit 驅動為低電位(0)
  • GPIO_CloseBit 
    • 將單一 bit 恢復為預設/關閉狀態

實際效果


程式參考

    Nuvoton 官方 github https://github.com/OpenNuvoton/N9H31_NonOS

延伸閱讀






打賞按讚