嵌入式从零开始(第六篇):时间管理大师 —— 定时器与系统滴答
不仅是延时,更是 PWM、捕获、RTOS 心跳的基石
前言:为什么需要“硬件时钟”?
在上一篇文章中,我们用中断实现了异步响应,但还有一个常见的需求:“过一段时间后做某事” 或 “每隔固定时间做某事”。
你可能会想到 HAL_Delay(1000) —— 但这有两个致命问题:
- 阻塞:延时期间 CPU 啥也干不了,浪费算力。
- 不精确:依赖系统时钟和循环计数,受中断影响,长时间累积误差大。
有没有一种方式,让 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 寄存器中,并触发中断。通过连续两次捕获值的差值,结合计数频率,即可计算出时间。
测量高电平时间:
- 配置为上升沿捕获,记录时间 T1。
- 立刻改为下降沿捕获,记录时间 T2。
- 高电平时间 = (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 更灵活)。
八、常见问题与注意事项
- 时钟未使能:每个定时器都有自己的总线时钟(APB1 或 APB2),忘记
__HAL_RCC_TIMx_CLK_ENABLE()会导致寄存器无法操作。 - 预分频器与 ARR 的数值范围:它们都是 16 位(基本/通用)或 32 位,不要超出范围(0~65535 或 0~4294967295)。
- 中断标志未清除:如果忘记清除中断标志,ISR 会反复进入,导致系统假死。
- PWM 频率与占空比分辨率权衡:ARR 越大,分辨率越高(CCR 步进更细),但 PWM 频率越低。需要根据应用平衡。
- 输入捕获的溢出处理:如果脉冲周期大于计数器最大溢出时间,必须配合溢出中断累加计数,否则测量错误。
- SysTick 优先级:默认最低,但如果你需要高精度延时,可提高其优先级(
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0))。 - 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 | 总结 | 读手册、写可维护代码、回顾 |
更多推荐
所有评论(0)