不仅是延时,更是 PWM、捕获、RTOS 心跳的基石


前言:为什么需要“硬件时钟”?

在上一篇文章中,我们用中断实现了异步响应,但还有一个常见的需求:“过一段时间后做某事”“每隔固定时间做某事”

你可能会想到 HAL_Delay(1000) —— 但这有两个致命问题:

  1. 阻塞:延时期间 CPU 啥也干不了,浪费算力。
  2. 不精确:依赖系统时钟和循环计数,受中断影响,长时间累积误差大。

有没有一种方式,让 CPU “闹钟”到点时自动中断,而不需要一直傻等?这就是硬件定时器的职责。

STM32 内部集成了多种定时器,它们就像一个个独立的小闹钟,可以:

  • 产生精确的延时(非阻塞)
  • 生成 PWM 波(控制电机、调光)
  • 测量输入信号的频率/脉宽(捕获)
  • 触发 ADC 采样、DMA 传输
  • 作为 RTOS 的系统心跳(SysTick)

一、定时器的本质:计数器 + 时钟源

无论多高级的定时器,核心都是一个计数器(Counter),它在时钟驱动下递增(或递减)。当计数值达到预设的 自动重装载值(ARR, Auto-Reload Register) 时,可以产生更新事件(溢出中断)。

基本公式(向上计数模式):

溢出时间 = (ARR + 1) × (PSC + 1) / 定时器时钟频率
  • PSC(Prescaler):预分频器,对输入时钟分频,得到计数器的时钟。
  • ARR:自动重装载值,计数器从 0 计数到 ARR 时溢出。

例如:定时器时钟 72 MHz,PSC = 7199,ARR = 9999,则计数频率 = 72M / 7200 = 10 kHz,溢出周期 = 10000 / 10kHz = 1 秒。


二、STM32 定时器家族

STM32 系列根据功能将定时器分为三类:

类型 位数 典型型号(F103) 功能
基本定时器(TIM6, TIM7) 16 TIM6, TIM7 只有最基本的溢出中断,用于简单定时或触发 DAC
通用定时器(TIM2~TIM5) 16/32 TIM2, TIM3, TIM4, TIM5 除了溢出中断,还支持输入捕获、输出比较、PWM、编码器接口
高级定时器(TIM1, TIM8) 16 TIM1, TIM8 在通用基础上增加互补输出死区插入刹车功能,用于电机控制

注意:不同系列的 STM32 定时器数量、位数不同。比如 F4 系列有 32 位通用定时器,H7 系列更多。


三、定时器基本用法:非阻塞延时(中断方式)

我们想实现一个 LED 每隔 500ms 翻转一次,但不想用 HAL_Delay 阻塞主循环。

1. 配置定时器(以 TIM2 为例)

TIM_HandleTypeDef htim2;

void TIM2_Init(void) {
    __HAL_RCC_TIM2_CLK_ENABLE();
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 7200 - 1;      // 72MHz / 7200 = 10kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 5000 - 1;         // 10kHz 下计数 5000 次 = 0.5秒
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htim2);
    HAL_TIM_Base_Start_IT(&htim2);        // 启动并开启更新中断
}

2. 中断服务函数(在 stm32f1xx_it.c 或自己定义)

void TIM2_IRQHandler(void) {
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) {
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

3. 主循环彻底解放

int main(void) {
    HAL_Init();
    SystemClock_Config();
    LED_Init();
    TIM2_Init();
    while (1) {
        // 主循环可以自由处理其他任务,LED 自动 0.5s 翻转
    }
}

这种非阻塞延时是裸机多任务的基础。但注意:如果中断频率太高,会频繁打断主循环,需要合理设计。


四、PWM 输出:呼吸灯与电机控制

PWM(脉冲宽度调制) 通过改变方波的占空比来模拟不同的电压输出。占空比 = 高电平时间 / 周期。

1. PWM 原理

通用定时器可以配置一个或多个通道输出 PWM。计数器从 0 向上计数到 ARR,当计数值小于捕获比较寄存器(CCR) 时,输出高电平;大于时输出低电平(取决于极性配置)。

  • 频率 = 定时器时钟 / (PSC+1) / (ARR+1)
  • 占空比 = CCR / (ARR+1)

2. 配置 PWM 输出(以 TIM3 通道 1 为例,PA6)

TIM_HandleTypeDef htim3;
TIM_OC_InitTypeDef sConfigOC;

void PWM_Init(void) {
    __HAL_RCC_TIM3_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    // GPIO 复用为 TIM3_CH1
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_6;
    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &gpio);
    
    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 7200 - 1;      // 72M/7200 = 10kHz
    htim3.Init.Period = 1000 - 1;         // 10kHz/1000 = 10Hz PWM 频率
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    HAL_TIM_PWM_Init(&htim3);
    
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 500;                // 初始占空比 50% (500/1000)
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
    
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
}

3. 动态改变占空比

// 设置占空比为 30% (300/1000)
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 300);

应用场景

  • 呼吸灯:循环改变占空比。
  • 直流电机调速:改变占空比控制转速。
  • 舵机控制:周期 20ms,高电平 0.5ms~2.5ms 对应角度。
  • DAC 模拟输出:PWM 加 RC 滤波产生模拟电压。

五、输入捕获:测量脉冲宽度与频率

输入捕获可以测量外部信号的周期、频率、占空比、高电平时间等。

原理

定时器的一个通道(比如 CH1)捕获外部输入的边沿(上升沿/下降沿)。当边沿发生时,计数器当前值被保存到 CCR 寄存器中,并触发中断。通过连续两次捕获值的差值,结合计数频率,即可计算出时间。

测量高电平时间

  1. 配置为上升沿捕获,记录时间 T1。
  2. 立刻改为下降沿捕获,记录时间 T2。
  3. 高电平时间 = (T2 - T1) × 计数器周期(注意溢出处理)。

简单示例:捕获上升沿(不考虑溢出)

volatile uint32_t captureValue = 0;

void TIM2_IRQHandler(void) {
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_CC1) != RESET) {
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_CC1);
        captureValue = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
        // 可在主循环中处理 captureValue
    }
}

实际工程中需要处理计数器溢出的情况,否则长时间测量会出错。可以用溢出中断累加次数。


六、SysTick:系统滴答定时器

SysTick 是 ARM Cortex-M 内核内置的一个24 位向下计数器,它不属于 STM32 的外设定时器,但极其重要。

特性

  • 24 位递减计数器,到达 0 时可触发中断。
  • 时钟源可以是系统时钟(HCLK)或其 8 分频。
  • 通常用于 RTOS 的心跳(如 FreeRTOS 的 configTICK_RATE_HZ),以及 HAL_Delay 的实现。

配置 SysTick 产生 1ms 中断

HAL 库在 HAL_Init() 中会自动调用 HAL_SYSTICK_Config() 配置 SysTick 为 1ms 中断(如果未重定义)。你也可以手动配置:

// 系统时钟 72MHz,HCLK 8 分频后 9MHz,计数值 9000 得到 1ms 中断
if (SysTick_Config(SystemCoreClock / 1000)) {
    // 配置失败
}

SysTick 中断服务函数

void SysTick_Handler(void) {
    HAL_IncTick();           // HAL 库的计时变量增加
    // 用户代码:比如执行 1ms 一次的扫描任务
}

利用 SysTick 实现非阻塞延时(裸机状态机)

uint32_t tick = 0;

void SysTick_Handler(void) {
    tick++;
}

void delay_ms_nonblock(uint32_t ms) {
    uint32_t start = tick;
    while ((tick - start) < ms) {
        // 等待期间可以执行其他任务(需要状态机配合)
    }
}

注意:纯 while 等待仍然阻塞,真正的非阻塞延时需要结合状态机或 RTOS。


七、定时器的进阶玩法

1. 编码器模式

通用定时器支持直接连接正交编码器(比如电机编码器),自动计数脉冲和方向。这对做平衡小车、伺服驱动非常方便。

2. 触发 ADC 采样

定时器的更新事件或某个输出比较事件可以触发 ADC 启动采样,实现精确时间间隔采样(如音频、电力谐波分析)。

3. 从模式与主从同步

多个定时器可以级联:一个定时器作为主,输出触发信号;另一个作为从,响应触发开始计数。用于产生复杂的时序。

4. 输出比较翻转模式

在 CCR 匹配时翻转输出电平,可以产生任意频率的方波(甚至比 PWM 更灵活)。


八、常见问题与注意事项

  1. 时钟未使能:每个定时器都有自己的总线时钟(APB1 或 APB2),忘记 __HAL_RCC_TIMx_CLK_ENABLE() 会导致寄存器无法操作。
  2. 预分频器与 ARR 的数值范围:它们都是 16 位(基本/通用)或 32 位,不要超出范围(0~65535 或 0~4294967295)。
  3. 中断标志未清除:如果忘记清除中断标志,ISR 会反复进入,导致系统假死。
  4. PWM 频率与占空比分辨率权衡:ARR 越大,分辨率越高(CCR 步进更细),但 PWM 频率越低。需要根据应用平衡。
  5. 输入捕获的溢出处理:如果脉冲周期大于计数器最大溢出时间,必须配合溢出中断累加计数,否则测量错误。
  6. SysTick 优先级:默认最低,但如果你需要高精度延时,可提高其优先级(HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0))。
  7. HAL_Delay 的阻塞性:它使用 SysTick 中断来累加计数,但内部是 while 循环等待,会阻塞。在中断中调用 HAL_Delay 是危险的(可能导致优先级反转)。

九、总结

  • 定时器 = 计数器 + 时钟源 + 自动重装载 + 多种输出/输入模式。
  • 基本定时器:简单延时、触发事件。
  • 通用定时器:PWM、输入捕获、输出比较、编码器。
  • 高级定时器:电机控制专用(互补、死区、刹车)。
  • SysTick:内核自带,RTOS 心跳、精确延时基准。
  • PWM:占空比控制电机/灯光,频率控制音调。
  • 输入捕获:测量外部信号的时间参数。

系列全篇快速索引:

篇目 标题 核心关键词
1 嵌入式到底是什么? LED、时钟、GPIO、ARM vs C51 vs STM32
2 串口江湖 UART、RS-232、RS-485、TTL
番外 波特率解析 波特率、比特率、晶振分频
3 两线走天下 I2C、开漏、上拉、地址、应答
4 极速先锋 SPI、四线、CPOL/CPHA、全双工
5 嵌入式大脑 中断、NVIC、ISR、事件驱动
6 时间管理大师 定时器、PWM、输入捕获、SysTick
7 存储与地址 内存映射、大小端、4GB 寻址
8 从裸机到 RTOS 任务、调度、FreeRTOS、优先级
9 任务间悄悄话 队列、信号量、互斥量、IPC
10 位运算的艺术 &、
11 芯片选型 STM32 vs ESP32、选型框架
12 调试与工具链 printf、调试器、逻辑分析仪
13 总结 读手册、写可维护代码、回顾
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐