4、定时器基础与应用:SysTick定时器、基本定时器、PWM输出、输入捕获测量频率
定时器这玩意儿,可以说是嵌入式开发里最常用的外设之一了。我刚开始做项目那会儿,总觉得定时器不就是计个数嘛,有啥好研究的?后来踩了几个坑才明白——定时器用得好不好,直接决定了你产品的实时性和稳定性。
这一章,咱们把STM32的定时器体系捋一遍。从最简单的SysTick,到基本定时器,再到PWM和输入捕获。每个知识点我都会结合项目经验来讲,尤其是那些容易翻车的地方。
4.1 SysTick定时器:FreeRTOS的心跳
SysTick是Cortex-M内核自带的24位递减计数器。说白了,它就是给操作系统提供“心跳”的。FreeRTOS的任务调度、时间片轮转、vTaskDelay这些功能,全都依赖SysTick。
核心要点:SysTick的时钟源通常选择处理器时钟(HCLK)的8分频。比如HCLK=72MHz,那么SysTick的计数频率就是9MHz。每计数一次,耗时1/9MHz ≈ 0.111μs。
配置SysTick其实很简单。HAL库已经帮我们封装好了:
// SysTick中断优先级配置
HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0);
// 使能SysTick中断
HAL_NVIC_EnableIRQ(SysTick_IRQn);
// 配置SysTick重装载值,产生1ms中断
// 假设HCLK=72MHz,8分频后=9MHz
// 重装载值 = 9000000 / 1000 - 1 = 8999
SysTick_Config(8999);
嗯,这里要注意——SysTick的中断优先级一定要设成最低(15)。为什么?因为FreeRTOS的临界区保护依赖这个优先级。如果你把SysTick优先级设高了,在临界区里它照样能打断,那临界区就形同虚设了。
我曾经踩过的坑:有个项目,任务调度偶尔会莫名其妙卡死。查了两天才发现,是某次代码升级时不小心把SysTick优先级改成了5。结果在操作队列时,SysTick中断进来触发了任务切换,队列数据还没写完就被读走了……从那以后,我每次移植FreeRTOS第一件事就是检查SysTick优先级。
4.2 基本定时器:最纯粹的计时单元
基本定时器(TIM6、TIM7)是STM32定时器家族里最“单纯”的成员。它只有向上计数功能,没有外部引脚,不能输出PWM,也不能做输入捕获。那它有什么用?
- 产生精准的时间基准——比如每隔100ms采集一次传感器数据
- 触发DMA传输——定时器更新事件可以触发DMA,实现自动数据搬运
- 作为其他定时器的从模式——级联使用,扩展定时范围
基本定时器的配置流程,我习惯这么写:
// 假设TIM6,时钟源为APB1,通常=36MHz
// 目标:产生1ms中断
// 预分频器:36000-1,得到1MHz计数频率
// 自动重装载值:1000-1,得到1ms周期
TIM_HandleTypeDef htim6;
htim6.Instance = TIM6;
htim6.Init.Prescaler = 36000 - 1; // 预分频
htim6.Init.Period = 1000 - 1; // 自动重装载
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT(&htim6); // 启动并使能中断
个人习惯:我一般把基本定时器留给“周期性任务”用。比如一个温湿度采集任务,每2秒读一次传感器,用TIM6的更新中断触发一个标志位,主循环里检测到标志位就去采集。这样比在任务里硬等要优雅得多。
4.3 PWM输出:让硬件帮你干活
PWM(脉冲宽度调制)说白了就是输出一个频率固定、占空比可调的方波。你想想看,控制电机转速、调节LED亮度、驱动舵机角度——这些场景全都能用PWM搞定。
STM32的高级定时器和通用定时器都支持PWM输出。以通用定时器TIM2为例,配置PWM的步骤:
- 配置定时器时基——决定PWM频率
- 配置输出比较通道——决定PWM占空比
- 配置GPIO为复用推挽输出——让引脚输出PWM波形
- 启动PWM输出
// 配置TIM2通道1输出PWM,频率=1kHz
// 预分频器=72-1,得到1MHz计数频率
// 自动重装载值=1000-1,得到1kHz
// 占空比=50%,即比较值=500
TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 72 - 1;
htim2.Init.Period = 1000 - 1;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 占空比50%
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);
关键点:PWM频率 = 定时器时钟 / (预分频器+1) / (自动重装载值+1)。占空比 = 比较值 / (自动重装载值+1) × 100%。
我在项目中遇到过一个问题:用PWM驱动一个直流电机,电机在低速时抖动得很厉害。后来发现是PWM频率太低了(只有50Hz)。电机线圈在PWM关断期间电流掉得太快,导致转矩不连续。把频率提高到1kHz后,问题就解决了。
避坑指南:PWM频率不是越高越好。频率太高,MOS管的开关损耗会急剧增加,驱动芯片可能会过热。我一般遵循这个原则:电机驱动用1kHz~20kHz,LED调光用100Hz~1kHz,舵机控制用50Hz(标准周期20ms)。
4.4 输入捕获:测量外部信号的频率
输入捕获是定时器的“反向操作”——它不输出波形,而是测量外部信号的频率或脉宽。原理很简单:当检测到指定边沿(上升沿或下降沿)时,定时器会把当前的计数值“捕获”到寄存器里。两次捕获的差值,就是信号的周期。
测量频率的典型步骤:
- 配置定时器输入捕获通道,选择上升沿触发
- 使能捕获中断
- 在中断服务函数里读取捕获值
- 计算两次捕获的差值,得到周期
- 频率 = 1 / 周期
// 配置TIM3通道1输入捕获
// 预分频器=72-1,计数频率=1MHz,精度=1μs
TIM_IC_InitTypeDef sConfigIC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72 - 1;
htim3.Init.Period = 0xFFFF; // 最大计数值
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_IC_Init(&htim3);
sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
中断服务函数里处理捕获值:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
static uint16_t lastCapture = 0;
static uint8_t firstCapture = 1;
uint16_t currentCapture;
uint16_t period;
uint32_t frequency;
if(htim->Instance == TIM3)
{
currentCapture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
if(!firstCapture)
{
// 处理计数器溢出
if(currentCapture >= lastCapture)
period = currentCapture - lastCapture;
else
period = (0xFFFF - lastCapture) + currentCapture + 1;
// 频率 = 1MHz / period
frequency = 1000000 / period;
// 这里可以保存frequency供主循环使用
}
else
{
firstCapture = 0;
}
lastCapture = currentCapture;
}
}
个人经验:输入捕获测量低频信号(比如10Hz以下)时,要注意定时器溢出问题。如果信号周期超过了定时器的最大计数值,捕获值会“绕圈”。我一般会配合定时器的更新中断,记录溢出次数,这样就能测量很宽的频率范围了。
4.5 知识体系总览
为了让你更直观地理解这四种定时器应用的关系,我画了一张结构图:
4.6 四种定时器应用对比
我把这四种应用的关键参数整理成了表格,方便你对照选择:
| 类型 | 硬件资源 | 典型用途 | 精度 | 中断频率 |
|---|---|---|---|---|
| SysTick | 内核内置 | RTOS心跳、软件延时 | 1ms(可调) | 固定(通常1kHz) |
| 基本定时器 | TIM6/TIM7 | 周期性任务、DMA触发 | 1μs~1s | 可配置 |
| PWM输出 | 通用/高级定时器 | 电机、LED、舵机 | 占空比分辨率1~65535级 | 无中断(硬件自动输出) |
| 输入捕获 | 通用/高级定时器 | 频率测量、脉宽测量 | 取决于计数频率 | 每次捕获边沿触发 |
选型建议:如果你只是需要一个简单的周期性中断,用基本定时器就够了,别浪费通用定时器。PWM和输入捕获必须用通用或高级定时器,因为它们需要硬件通道支持。SysTick是操作系统的“专属资源”,裸机开发时也可以用它做延时,但一旦上了RTOS,就别再动SysTick的配置了。
好了,这一章的内容就到这里。定时器这块东西,光看代码是不够的,我建议你拿一块开发板,把每个例程都跑一遍。尤其是PWM和输入捕获,用示波器看看波形,比看一百遍代码都管用。