2026年5月4日星期一

NK-N9H31A2 開發板:ETIMER

在 Timer 這篇中已介紹相關基本觀念,本篇將配合實際範例進行操作,使用 N9H31 的 ETIMER 控制 LED 定時閃爍,並透過 UART 輸出訊息。
本範例以官方提供的 ETIMER_Periodic 為基礎進行修改。原始範例僅透過 UART 輸出計數秒數,未對應到開發板上的 LED,因此本篇額外加入 GPIO 控制,使 LED 能夠呈現固定週期的亮滅。
同時,透過 serial port tool(例如 Arduino Serial Monitor)觀察 UART 訊息時間戳記,可進一步驗證 Timer 的實際運作頻率。

實驗目的

透過 Timer 定時觸發中斷:
  • 控制 LED 週期性亮滅
  • 透過 UART 輸出訊號,觀察時間間隔

使用範例

官方範例 ETIMER_Periodic

程式碼

核心概念:Timer 週期性產生中斷 → CPU 進入 ISR → 在 ISR 中控制 LED 並輸出 UART 訊息。

範例是基於 Nuvoton 官方 Sample Code(ETIMER_Periodic)進行修改,
僅作為學習與教學用途,以下為修改後的範例程式:

static int state = 0;

/**
 * ETIMER0 interrupt handler. Toggle LED state and print message every time timer timeout.
 */
void ETMR0_IRQHandler(void)
{
	ETIMER_ClearIntFlag(0);

	if (state)
	{
		/* LED ON */
		outpw(REG_GPIOF_DATAOUT, inpw(REG_GPIOF_DATAOUT) & ~BIT10);
		outpw(REG_GPIOI_DATAOUT, inpw(REG_GPIOI_DATAOUT) & ~BIT10);
		state = 0;
		sysprintf("H\n");
	}
	else
	{
		/* LED OFF */
		outpw(REG_GPIOF_DATAOUT, inpw(REG_GPIOF_DATAOUT) | BIT10);
		outpw(REG_GPIOI_DATAOUT, inpw(REG_GPIOI_DATAOUT) | BIT10);
		state = 1;
		sysprintf("L\n");
	}
}

/*-----------------------------------------------------------------------------*/
int main(void)
{
	sysDisableCache();
	sysFlushCache(I_D_CACHE);
	sysEnableCache(CACHE_WRITE_BACK);
	sysInitializeUART();

	sysprintf("\nETIMER0 periodic + LED toggle test\n");

	/* Enable ETIMER0 clock */
	outpw(REG_CLK_PCLKEN0, inpw(REG_CLK_PCLKEN0) | (1 << 4));

	/* Enable GPIO clock */
	outpw(REG_CLK_PCLKEN0, inpw(REG_CLK_PCLKEN0) | (1 << 3));

	/* PF10 PI10 set as GPIO function */
	outpw(REG_SYS_GPF_MFPH, inpw(REG_SYS_GPF_MFPH) & ~(0xF << 8));
	outpw(REG_SYS_GPI_MFPH, inpw(REG_SYS_GPI_MFPH) & ~(0xF << 8));

	/* PF10 PI10 set as output */
	outpw(REG_GPIOF_DIR, inpw(REG_GPIOF_DIR) | BIT10);
	outpw(REG_GPIOI_DIR, inpw(REG_GPIOI_DIR) | BIT10);

	/* PF10 PI10 initial low */
	outpw(REG_GPIOF_DATAOUT, inpw(REG_GPIOF_DATAOUT) & ~BIT10);
	outpw(REG_GPIOI_DATAOUT, inpw(REG_GPIOI_DATAOUT) & ~BIT10);

	/* Set timer frequency to 1Hz */
	ETIMER_Open(0, ETIMER_TOGGLE_MODE, 1);

	/* Enable timer interrupt */
	ETIMER_EnableInt(0);
	sysInstallISR(HIGH_LEVEL_SENSITIVE | IRQ_LEVEL_1, ETMR0_IRQn, (PVOID)ETMR0_IRQHandler);
	sysSetLocalInterrupt(ENABLE_IRQ);
	sysEnableInterrupt(ETMR0_IRQn);

	/* Start Timer 0 */
	ETIMER_Start(0);

	while (1)
		;
}
補充說明:本範例同時控制 PF10 與 PI10,因為開發板上分別對應兩顆 LED。透過 Timer 中斷同時切換兩個 GPIO,可快速驗證 Timer 與中斷機制是否正常運作。

函式說明

ETIMER_Open(timerNo, mode, freq)
  • 設定 Timer 的運作模式與目標頻率。
  • 設定 Timer mode(One-shot / Periodic / Continuous / Toggle)
  • 自動計算 prescaler 與 compare value
  • 回傳實際可達到的頻率(可能略有誤差)
注意:此函式不會啟動 Timer,需搭配 ETIMER_Start()


ETIMER_EnableInt(timerNo)
    啟用 Timer 中斷功能。

sysInstallISR(level, irq, handler)
    註冊中斷服務函式(ISR)
 將 interrupt source 對應到 handler

sysSetLocalInterrupt(ENABLE_IRQ)
啟用 CPU 層級中斷(全域開關)

sysEnableInterrupt(irq)
啟用中斷控制器(AIC)中的指定 IRQ

ETIMER_Start(timerNo)
啟動 Timer 計數


ETIMER_ClearIntFlag(timerNo)
清除 Timer 中斷旗標

ETMR0_IRQHandler()
  • Timer 中斷服務函式(ISR)
  • 要處理的事情
目前流程:清除中斷旗標 -> 執行應用邏輯(toggle LED + UART 輸出)
原則:保持簡短 避免阻塞

Timer 中斷的觸發需要同時啟用:
1. Timer 中斷功能(ETIMER_EnableInt)
2. 中斷控制器(sysEnableInterrupt)
3. CPU 中斷(sysSetLocalInterrupt)

三者缺一不可。

實驗結果


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

延伸閱讀

         

打賞按讚