4. 基本定时器编程实战:TIM6/TIM7初始化
说到基本定时器,TIM6和TIM7在STM32家族里算是"小老弟"级别的存在。功能简单,但胜在稳定可靠。我个人做项目时,但凡需要纯粹的定时功能——比如做个延时、触发个ADC采样——第一反应就是拉TIM6出来用。
为什么?因为它没有外部引脚,没有复杂的捕获比较通道,说白了就是个"定时闹钟"。你设好时间,它到点就响,就这么纯粹。
4.1 基本定时器的硬件特性
先看一张结构图,把TIM6/TIM7的"五脏六腑"摸清楚。
这个图我建议你多看两眼。基本定时器的核心就三个寄存器:PSC、CNT、ARR。时钟进来先过PSC分频,然后CNT开始数数,数到ARR的值就触发更新事件,CNT归零重来。就这么简单。
4.2 寄存器方式实现微秒/毫秒延时
先来硬的——直接操作寄存器。为什么?因为你能看到底层到底发生了什么。我刚开始学的时候,HAL库一封装,反而搞不清时钟怎么走的。
// 寄存器版:TIM6初始化
void TIM6_Init(void)
{
// 1. 使能TIM6时钟 (APB1总线)
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
// 2. 配置预分频器
// 假设APB1时钟 = 72MHz, 目标计数频率 = 1MHz (1us计数一次)
// PSC = 72MHz / 1MHz - 1 = 71
TIM6->PSC = 71; // 72分频, 计数频率 = 72MHz / 72 = 1MHz
// 3. 自动重装载值先设为0
TIM6->ARR = 0;
// 4. 更新事件产生时更新影子寄存器
TIM6->EGR |= TIM_EGR_UG;
// 5. 清除更新标志
TIM6->SR &= ~TIM_SR_UIF;
// 6. 使能计数器
TIM6->CR1 |= TIM_CR1_CEN;
}
// 微秒级延时
void delay_us(uint32_t us)
{
// 设置计数值
TIM6->ARR = us - 1;
// 产生更新事件, 加载ARR到影子寄存器
TIM6->EGR |= TIM_EGR_UG;
// 清除标志
TIM6->SR &= ~TIM_SR_UIF;
// 等待计数完成
while(!(TIM6->SR & TIM_SR_UIF));
}
// 毫秒级延时
void delay_ms(uint32_t ms)
{
for(uint32_t i = 0; i < ms; i++)
{
delay_us(1000);
}
}
我的习惯:每次配置完PSC和ARR后,一定要手动产生一次更新事件(UG位置1)。否则影子寄存器不会立即加载新值,第一次延时会不准。这个坑我踩过,当时调了半天才发现。
4.3 HAL库方式实现延时
再来看看HAL库的写法。说实话,HAL库把很多细节藏起来了,但用起来确实省心。
// HAL库版:TIM6初始化
void HAL_TIM6_Init(void)
{
TIM_HandleTypeDef htim6;
htim6.Instance = TIM6;
htim6.Init.Prescaler = 71; // 72分频, 1MHz计数
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 0xFFFF; // 最大周期, 实际延时靠软件控制
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_Base_Init(&htim6);
}
// HAL库微秒延时
void HAL_Delay_us(TIM_HandleTypeDef *htim, uint32_t us)
{
__HAL_TIM_SET_COUNTER(htim, 0); // 计数器清零
while(__HAL_TIM_GET_COUNTER(htim) < us);
}
// 使用示例
// HAL_Delay_us(&htim6, 500); // 延时500us
你发现没有?HAL库的延时实现其实更"取巧"——它直接读CNT寄存器的值,跟设定的目标值比较。这种做法在短延时场景下很实用,但要注意CNT是16位的,最大只能计到65535,所以单次延时不能超过65535微秒。
4.4 寄存器 vs HAL库:对比分析
我把两种方式的差异整理成了一张表,方便你对照着看。
| 对比维度 | 寄存器方式 | HAL库方式 |
|---|---|---|
| 代码量 | 较少,直接操作寄存器 | 较多,需要句柄和结构体 |
| 可读性 | 需要查阅手册理解位含义 | 函数名自解释,容易理解 |
| 执行效率 | 高,无函数调用开销 | 中等,有抽象层开销 |
| 移植性 | 差,换芯片需重写 | 好,换系列只需改配置 |
| 调试便利性 | 需手动查看寄存器值 | 支持HAL库调试接口 |
| 适用场景 | 资源受限、性能敏感 | 快速开发、项目原型 |
注意:HAL_Delay()函数在中断中调用时要小心。默认的HAL_Delay依赖SysTick中断,如果在中断服务函数里调用,优先级处理不当会导致死锁。我建议在中断里用寄存器版的延时,或者用TIM硬件延时。
4.5 精确延时的关键参数计算
延时的精度取决于PSC和ARR的配置。公式其实就一个:
定时时间 = (PSC + 1) × (ARR + 1) / 定时器时钟频率
举个例子,APB1时钟72MHz,想要1ms中断一次:
- 先定PSC = 71,计数频率 = 72MHz / 72 = 1MHz
- 再算ARR = 1ms × 1MHz - 1 = 999
- 最终定时时间 = 72 × 1000 / 72MHz = 1ms
避坑指南:我曾经在一个项目里发现延时偏大,查了半天发现是APB1时钟配置错了。STM32F1系列里,APB1最大只能到36MHz,如果HCLK是72MHz,APB1必须二分频。所以实际TIM6的时钟是36MHz,不是72MHz。配置PSC时一定要确认时钟树!
4.6 实战:实现高精度微秒延时
最后给一个我项目中常用的"高精度延时"方案。它结合了寄存器的高效和HAL库的易用性。
// 高精度延时:基于TIM6,误差<1us
void TIM6_Init_For_Delay(void)
{
// 使能TIM6时钟
__HAL_RCC_TIM6_CLK_ENABLE();
// 配置PSC,使计数频率为1MHz
TIM6->PSC = 71; // 假设APB1=72MHz
// 使能计数器
TIM6->CR1 |= TIM_CR1_CEN;
}
// 延时指定微秒数
void delay_us_precise(uint32_t us)
{
uint32_t start = TIM6->CNT;
uint32_t target = us;
// 处理CNT溢出情况
if(start + target < 0x10000)
{
while((TIM6->CNT - start) < target);
}
else
{
// 跨溢出边界处理
uint32_t remaining = target;
while(remaining > 0)
{
uint32_t current = TIM6->CNT;
if(current >= start)
{
uint32_t elapsed = current - start;
if(elapsed >= remaining) break;
remaining -= elapsed;
}
else
{
// 发生了溢出
uint32_t elapsed = (0x10000 - start) + current;
if(elapsed >= remaining) break;
remaining -= elapsed;
}
start = current;
}
}
}
为什么这样写?直接读CNT的方式避免了中断响应延迟,精度只受CPU读取寄存器的时间影响。我实测过,在72MHz主频下,这个函数的误差在0.5us以内。但要注意,它不能用在中断里——如果被更高优先级中断打断,延时就会偏长。