前言
硬體定時器(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)。
圖 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 的場合(如馬達控制、逆變器)。
圖 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
圖 3:PWM 模式 1 — ARR=9 定義週期,CCRx=5 定義工作週期(Duty=50%)

圖 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 暫存器。
圖 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 定時器比較

圖 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 就等於掌握了嵌入式系統的時間維度。
文章評論