4. 基本定时器编程实战:TIM6/TIM7初始化

说到基本定时器,TIM6和TIM7在STM32家族里算是"小老弟"级别的存在。功能简单,但胜在稳定可靠。我个人做项目时,但凡需要纯粹的定时功能——比如做个延时、触发个ADC采样——第一反应就是拉TIM6出来用。

为什么?因为它没有外部引脚,没有复杂的捕获比较通道,说白了就是个"定时闹钟"。你设好时间,它到点就响,就这么纯粹。

4.1 基本定时器的硬件特性

先看一张结构图,把TIM6/TIM7的"五脏六腑"摸清楚。

TIM6/TIM7 基本定时器内部结构 APB1 时钟 PSC 预分频器 CNT 计数器 ARR 自动重装载 更新事件 (UEV) UIF 中断标志 核心工作流程: APB1时钟 → PSC分频 → CNT递增计数 CNT == ARR → 产生更新事件(UEV) → 置位UIF标志 CNT自动清零 → 重新开始计数 关键寄存器: TIMx_PSC — 预分频系数 (16位, 0~65535) TIMx_ARR — 自动重装载值 (16位, 0~65535)

这个图我建议你多看两眼。基本定时器的核心就三个寄存器: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中断一次:

  1. 先定PSC = 71,计数频率 = 72MHz / 72 = 1MHz
  2. 再算ARR = 1ms × 1MHz - 1 = 999
  3. 最终定时时间 = 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以内。但要注意,它不能用在中断里——如果被更高优先级中断打断,延时就会偏长。