0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

硬體定時器(Timer)完整教學:從計數原理到 STM32/ESP32 PWM 與輸入捕捉實作

2026 年 5 月 31 日 2點熱度 0人點贊 0條評論

前言

硬體定時器(Timer)是 MCU 中最靈活也最常被低估的週邊。從精確延時、PWM 調光、感測器脈衝測量到編碼器解碼,Timer 幾乎參與了嵌入式系統的每個層面。STM32 擁有業界最完整的定時器樹(基本/通用/進階三級架構),ESP32 則提供 Timer Group 與 LEDC 兩種定位不同的計時方案。本文將從 Timer 的核心原理出發,透過時序圖逐步拆解六大工作模式,並提供 STM32 HAL 與 ESP32 Arduino/IDF 的完整程式碼。

一、硬體定時器基本原理

1.1 什麼是硬體定時器?

硬體定時器本質上是一個由硬體時鐘驅動的計數器,與軟體 delay 不同:

  • 精準:由專屬硬體電路驅動,不受 CPU 負載影響
  • 無阻塞:計數過程中 CPU 可執行其他任務
  • 多功能:同一個計數器可同時產生中斷、PWM、捕捉外部訊號

1.2 三大核心暫存器

暫存器 全名 功能
CNT Counter 當前計數值,每個時鐘週期遞增或遞減
ARR Auto-Reload Register 計數上限(或下限),溢位時自動重載
PSC Prescaler 對時鐘源進行除頻,控制計數速度

計數頻率公式:CK_CNT = CK_INT / (PSC + 1)

溢位時間公式:T_overflow = (ARR + 1) / CK_CNT

例如 STM32 系統時鐘 72 MHz,PSC=71(除 72),ARR=9999:
CK_CNT = 72 MHz / 72 = 1 MHz(1 μs/tick)
T_overflow = 10000 × 1 μs = 10 ms(100 Hz 中斷頻率)

二、TIM 工作模式

2.1 向上計數模式(Up-counting)

這是最基礎的模式:CNT 從 0 遞增到 ARR,溢位時歸零並產生 Update Event(UEV)。

Timer Up-counting

圖 1:向上計數模式時序 — CNT 從 0→ARR→0,ARR=11 時每 12 個脈衝觸發一次 UEV

// STM32 HAL — 基本定時器中斷(TIM6, 10ms)
TIM_HandleTypeDef htim6;

void MX_TIM6_Init(void)
{
    htim6.Instance = TIM6;
    htim6.Init.Prescaler = 71;          // 72 MHz / 72 = 1 MHz
    htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim6.Init.Period = 9999;           // 10000 ticks = 10ms
    htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htim6);
    HAL_TIM_Base_Start_IT(&htim6);      // 啟動並啟用中斷
}

// 中斷回呼 — 每 10ms 執行一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        toggle_led();  // 或讀取感測器、更新狀態機
    }
}

2.2 中央對齊模式(Center-aligned Mode)

CNT 從 0 遞增到 ARR,然後遞減回 0,形成對稱三角波。在頂點和谷底各產生一次 UEV,頻率為向上計數的一半。適用於需要對稱 PWM 的場合(如馬達控制、逆變器)。

Timer Center-aligned

圖 2:中央對齊模式 — ARR=4,CNT 在 0↔4 之間往復,頂點與谷底皆觸發 UEV

// STM32 — 中央對齊模式設定
htim2.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED1;
// Center-aligned 1: 計數向下時設定比較旗標
// Center-aligned 2: 計數向上時設定比較旗標
// Center-aligned 3: 向上和向下都設定比較旗標

三、PWM 輸出模式

PWM(Pulse Width Modulation)是 Timer 最廣泛的應用。透過比較計數器 CNT 與通道比較值 CCRx:

  • CNT ≤ CCRx → OCxREF = High
  • CNT > CCRx → OCxREF = Low

Timer PWM Output

圖 3:PWM 模式 1 — ARR=9 定義週期,CCRx=5 定義工作週期(Duty=50%)

PWM Duty Cycle

圖 6:不同工作週期 PWM 波形示意 — 從 10% 到 90%

3.1 STM32 PWM 程式碼

// STM32 — TIM2 CH1 (PA0) PWM 輸出, 1kHz, 50% duty
TIM_OC_InitTypeDef sConfigOC = {0};

void MX_TIM2_PWM_Init(void)
{
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;          // 72 MHz → 1 MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 999;            // 1 MHz / 1000 = 1 kHz
    HAL_TIM_PWM_Init(&htim2);

    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 500;              // CCRx = 500 → 50% duty
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}

// 動態調整 Duty Cycle
void set_pwm_duty(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t duty)
{
    __HAL_TIM_SET_COMPARE(htim, channel, duty);
}

3.2 ESP32 Arduino PWM (ledc)

// ESP32 Arduino — LEDC PWM 輸出
// ESP32 內建 LEDC 控制器,專為 PWM 設計
const int ledPin = 2;       // GPIO2 (內建 LED)
const int freq = 5000;      // 5 kHz
const int resolution = 8;   // 8-bit (0~255)

void setup() {
    ledcSetup(0, freq, resolution);  // channel 0
    ledcAttachPin(ledPin, 0);        // 綁定 GPIO
    ledcWrite(0, 128);               // 50% duty (128/255)
}

void loop() {
    // 呼吸燈效果
    for (int duty = 0; duty <= 255; duty++) { ledcWrite(0, duty); delay(5); } for (int duty = 255; duty >= 0; duty--) {
        ledcWrite(0, duty);
        delay(5);
    }
}

3.3 ESP32 IDF Timer Group 計時中斷

// ESP32 IDF — Timer Group 0, 1ms 週期中斷
#include "esp_timer.h"
#include "driver/gptimer.h"

static bool IRAM_ATTR timer_isr_callback(gptimer_handle_t timer,
    const gptimer_alarm_event_data_t *edata, void *user_ctx)
{
    // 此處在 ISR 中執行,保持簡短
    static int counter = 0;
    counter++;
    if (counter >= 1000) {  // 1 秒到
        gpio_set_level(GPIO_NUM_2, !gpio_get_level(GPIO_NUM_2));
        counter = 0;
    }
    return true;  // 喚醒
}

void app_main(void)
{
    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT,
        .direction = GPTIMER_COUNT_UP,
        .resolution_hz = 1000000,  // 1 MHz (1 μs/tick)
    };
    gptimer_new_timer(&timer_config, &gptimer);

    gptimer_alarm_config_t alarm_config = {
        .alarm_count = 1000,  // 1000 ticks = 1 ms
        .reload_count = 0,     // 重載到 0(向上計數)
        .flags.auto_reload_on_alarm = true,
    };
    gptimer_set_alarm_action(gptimer, &alarm_config);

    gptimer_event_callbacks_t cbs = {
        .on_alarm = timer_isr_callback,
    };
    gptimer_register_event_callbacks(gptimer, &cbs, NULL);
    gptimer_enable(gptimer);
    gptimer_start(gptimer);
}

四、輸入捕捉模式(Input Capture)

輸入捕捉用於測量外部訊號的頻率、週期或脈衝寬度。當外部訊號在指定邊緣(上升/下降)變化時,Timer 自動將當前 CNT 值鎖存到 CCRx 暫存器。

Timer Input Capture

圖 4:輸入捕捉 — 外部訊號上升沿捕捉 CNT,兩次捕捉值相減得訊號週期

4.1 STM32 輸入捕捉測量頻率

// STM32 — TIM2 CH1 輸入捕捉,測量外部訊號頻率
volatile uint16_t cap1 = 0, cap2 = 0;
volatile uint8_t cap_ready = 0;

void MX_TIM2_Capture_Init(void)
{
    TIM_IC_InitTypeDef sConfigIC = {0};

    htim2.Init.Prescaler = 71;  // 1 MHz
    htim2.Init.Period = 0xFFFF;
    HAL_TIM_IC_Init(&htim2);

    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
    sConfigIC.ICFilter = 0;     // 無濾波
    HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
}

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
        static uint16_t last = 0;
        uint16_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        if (now > last) {
            uint16_t period = now - last;
            uint32_t freq = 1000000 / period;  // 1 MHz 時基
            printf("Freq: %lu Hz\n", freq);
        }
        last = now;
    }
}

4.2 ESP32 Pulse Counter (PCNT) 脈衝計數

// ESP32 — PCNT 脈衝計數,適用於編碼器與感測器脈衝
#include "driver/pulse_cnt.h"

void app_main(void)
{
    pcnt_unit_handle_t pcnt_unit = NULL;
    pcnt_unit_config_t unit_config = {
        .high_limit = 1000,
        .low_limit = -1000,
    };
    pcnt_new_unit(&unit_config, &pcnt_unit);

    pcnt_glitch_filter_config_t filter = {
        .max_glitch_ns = 1000,  // 1 μs 雜訊濾波
    };
    pcnt_unit_set_glitch_filter(pcnt_unit, &filter);

    pcnt_chan_config_t chan_config = {
        .edge_gpio_num = 4,      // GPIO4 脈衝輸入
        .level_gpio_num = -1,    // 不使用方向腳
    };
    pcnt_channel_handle_t channel = NULL;
    pcnt_new_channel(pcnt_unit, &chan_config, &channel);
    pcnt_channel_set_edge_action(channel, PCNT_CHANNEL_EDGE_ACTION_INCREASE,
                                          PCNT_CHANNEL_EDGE_ACTION_HOLD);
    pcnt_unit_enable(pcnt_unit);
    pcnt_unit_start(pcnt_unit);

    int pulse_count = 0;
    while (1) {
        pcnt_unit_get_count(pcnt_unit, &pulse_count);
        printf("Pulse count: %d\n", pulse_count);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

五、STM32 vs ESP32 定時器比較

Timer Comparison

圖 5:STM32 定時器樹與 ESP32 計時方案比較 — STM32 三級架構完整,ESP32 分工明確

項目 STM32 TIM (基本/通用/進階) ESP32 Timer Group ESP32 LEDC
計數位元 16-bit / 32-bit 64-bit 20-bit
計數方向 向上/向下/中央對齊 向上/向下 僅向上
通道數 4 通道 / 定時器 1 組警報 8 通道
PWM 解析度 16-bit (0~65535) — 20-bit (0~1,048,575)
互補 PWM + 死區 ✔(進制定時器) — —
編碼器支援 ✔(通用定時器) — —
硬體自動漸變 — — ✔(Fade)
Break 輸入(急停) ✔(進制定時器) — —
DMA 觸發 ✔ ✔ ✔

六、進階應用:編碼器模式

通用定時器支援編碼器介面模式,可直接連接增量式旋轉編碼器的 A/B 相輸出,硬體自動解析方向和位置。

// STM32 — TIM3 編碼器模式 (PA6=TI1, PA7=TI2)
TIM_Encoder_InitTypeDef sEncoder = {0};

void MX_TIM3_Encoder_Init(void)
{
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 0;
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 0xFFFF;

    sEncoder.EncoderMode = TIM_ENCODERMODE_TI12;  // 同時採樣 A+B
    sEncoder.IC1Polarity = TIM_ICPOLARITY_RISING;
    sEncoder.IC1Selection = TIM_ICSELECTION_DIRECTTI;
    sEncoder.IC1Prescaler = TIM_ICPSC_DIV1;
    sEncoder.IC1Filter = 10;
    sEncoder.IC2Polarity = TIM_ICPOLARITY_RISING;
    sEncoder.IC2Selection = TIM_ICSELECTION_DIRECTTI;
    sEncoder.IC2Prescaler = TIM_ICPSC_DIV1;
    sEncoder.IC2Filter = 10;
    HAL_TIM_Encoder_Init(&htim3, &sEncoder);
    HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
}

// 讀取編碼器位置
int16_t get_encoder_pos(void)
{
    return (int16_t)__HAL_TIM_GET_COUNTER(&htim3);
}

七、實戰:超音波測距(TIM 輸入捕捉 + GPIO 觸發)

結合 PWM 輸出(觸發 HC-SR04)與輸入捕捉(測量 Echo 脈衝寬度),實現精確測距:

// STM32 — HC-SR04 超音波測距
// 使用 TIM2 CH1 PWM 輸出 40kHz (Trigger 10μs pulse)
// 使用 TIM3 CH1 輸入捕捉測量 Echo 脈衝寬度

float measure_distance(void)
{
    // 發送 10μs Trigger 脈衝
    HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET);
    delay_us(10);
    HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET);

    // 等待輸入捕捉中斷獲取 Echo 寬度
    // 距離 (cm) = pulse_width_us / 58
    uint32_t pulse_us = get_captured_pulse_us();
    return pulse_us / 58.0;
}

八、常見陷阱

  • PSC 寫入時機:STM32 的 PSC 寫入後需要等待硬體更新(UEV)才生效。使用 HAL 時自動處理,但直接操作暫存器需注意
  • ARR 預載:啟用 Auto-Reload Preload 時,ARR 在軟體寫入後不會立即生效,需等待下次 UEV
  • 中斷優先級:定時器中斷應設定合適的搶佔優先級,避免影響即時性(一般設為 1~3)
  • 16-bit 溢位:STM32 基本/通用定時器多為 16-bit(65535 上限),長時間計時需軟體擴展或使用 32-bit 定時器
  • ESP32 LEDC 解析度 vs 頻率權衡:解析度越高,最大可達頻率越低(20-bit @ 5 kHz, 8-bit @ 40 MHz)

九、總結

硬體定時器是嵌入式開發中投資報酬率最高的週邊 — 理解向上計數、PWM 和輸入捕捉這三大核心模式,就能應對 90% 的應用場景。STM32 的三級定時器架構提供彈性但需要較多學習成本,ESP32 的 Timer Group + LEDC 分工明確更易上手。無論你選哪個平台,掌握 Timer 就等於掌握了嵌入式系統的時間維度。

標籤: ESP32 工業通訊
最後更新:2026 年 5 月 31 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang