第4章:高级外设驱动:定时器与PWM

各位同学,今天我们来啃一块硬骨头——高级定时器。说实话,我刚接触STM32时,觉得定时器不就是个计数器嘛,能有多复杂?直到我在一个伺服电机项目中,被互补输出的死区时间坑了整整三天,才老老实实把参考手册翻烂了。

这一章,我会把高级定时器的核心玩法掰开揉碎讲清楚。包括互补输出、PWM精调、霍尔接口,还有电机控制的基础套路。你想想看,这些东西搞懂了,基本上工业级运动控制项目的底层驱动你就能拿下了。

4.1 高级定时器 vs 通用定时器:到底强在哪?

先看一张对比表,心里有个底:

特性 通用定时器(TIMx) 高级定时器(TIM1/TIM8)
互补输出通道 无 3路(带死区插入)
刹车功能(BRK) 无 有(硬件紧急停止)
霍尔传感器接口 无 有(TI1/2/3输入)
重复计数器 无 有(更新事件可编程)
PWM分辨率 16位 16位(可扩展至32位)

说白了,高级定时器就是为电机控制和电源转换量身定做的。通用定时器能干的事它都能干,但它多出来的那些功能,恰恰是工业级应用离不开的。

4.2 互补输出与死区时间

互补输出,就是一对信号——比如CH1和CH1N——一个高电平的时候另一个必须是低电平。为什么要这样?因为H桥或三相逆变器的上下管不能同时导通,否则就是短路,啪的一下就冒烟了。

但问题来了:功率管从导通到关断需要时间。如果上下管切换得太快,就会出现短暂的直通。这时候就需要死区时间——在切换时插入一段两者都关断的间隔。

核心公式:死区时间 = DTG × tCK_INT

其中DTG是死区发生器寄存器(TIMx_BDTR)的低8位,tCK_INT是定时器时钟周期。

我个人的习惯是,死区时间至少设为功率管关断延迟的1.5倍。比如IRF540的关断延迟典型值是50ns,那我至少设75ns。保守一点设100ns更安全。

// 配置TIM1互补输出,死区时间100ns (假设CK_INT=72MHz)
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_BDTRInitTypeDef TIM_BDTRInitStructure;

// 时基配置:1MHz计数频率,ARR=999 => 1kHz PWM
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;   // 72MHz / 72 = 1MHz
TIM_TimeBaseStructure.TIM_Period = 999;          // 1MHz / 1000 = 1kHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);

// 通道1 PWM模式1,输出比较极性高
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;  // 使能互补输出
TIM_OCInitStructure.TIM_Pulse = 500;  // 50%占空比
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;      // 互补通道高有效
TIM_OC1Init(TIM1, &TIM_OCInitStructure);

// 死区时间配置:DTG = 100 (100 * 1us = 100ns)
TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable;
TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable;
TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1;
TIM_BDTRInitStructure.TIM_DeadTime = 100;  // 死区100个时钟周期
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Enable;
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
TIM_BDTRInit(&TIM1, &TIM_BDTRInitStructure);

// 使能主输出
TIM_Cmd(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM1, ENABLE);

我曾经踩过的坑:死区时间设得太小,MOS管在切换时轻微直通,发热量暴增。用示波器一看,上下管波形之间有微小的重叠。后来我把死区从50ns调到120ns,温度直接降了20°C。所以别为了追求那一点点效率而牺牲可靠性。

4.3 PWM频率与占空比精调

PWM的频率由TIM_Prescaler和TIM_Period决定:

频率 = 定时器时钟 / (Prescaler + 1) / (Period + 1)

占空比则由TIM_Pulse控制:

占空比 = Pulse / (Period + 1) × 100%

举个例子,我需要一个20kHz的PWM驱动一个微型直流电机,要求占空比能从0%到100%精细调节:

// 目标:20kHz PWM,1%步进精度
// 定时器时钟 = 72MHz
// 设Prescaler = 0,则计数频率 = 72MHz
// Period = 72MHz / 20kHz - 1 = 3599
// 这样占空比步进 = 1/3600 ≈ 0.028%,远超1%要求

TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_Period = 3599;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);

// 设置占空比为50%
TIM_SetCompare1(TIM1, 1800);  // 1800/3600 = 50%

精调技巧:如果你需要非常精确的频率,比如电机控制中的特定谐振频率,可以用定时器的重复计数器(RCR)。它可以让更新事件每N+1次溢出才发生一次,相当于把PWM周期扩展了N+1倍。我在一个超声波电机驱动中就用这个功能,把PWM频率精度从0.1%提升到了0.01%。

4.4 霍尔传感器接口

无刷直流电机(BLDC)的转子位置检测,最常用的就是霍尔传感器。三个霍尔元件安装在定子上,输出三路相位差120°的方波信号。

STM32的高级定时器有个很贴心的设计:霍尔传感器模式。你把三路霍尔信号接到TIM1的TI1、TI2、TI3输入上,定时器会自动检测边沿变化,并产生更新事件。

我画了一张霍尔接口的框图,帮你理解数据流:

霍尔传感器接口与换相逻辑 霍尔传感器 霍尔传感器 霍尔传感器 TIM1_TI1 TIM1_TI2 TIM1_TI3 高级定时器 霍尔模式 边沿检测 换相查找表 HA HB HC 001 → 100 010 → 010 011 → 110 100 → 001 ... 说明:霍尔信号边沿触发定时器更新事件,CPU在中断中查表得到下一组导通相, 然后更新OCx和OCxN输出。整个过程在几微秒内完成。

配置霍尔模式其实很简单,关键就两步:

  1. 把三个GPIO引脚配置为定时器输入捕获通道
  2. 设置TIM1的霍尔传感器模式
// 配置TIM1霍尔传感器模式
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;

// 时基:计数频率1MHz
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseStructure.TIM_Period = 65535;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);

// 配置三个通道为霍尔输入
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0F;  // 输入滤波,抗抖动
TIM_ICInit(TIM1, &TIM_ICInitStructure);

// 通道2、3类似配置,略

// 使能霍尔模式
TIM_EncoderInterfaceConfig(TIM1, TIM_EncoderMode_TI12, 
                           TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

// 使能更新中断
TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM1, ENABLE);

实际项目经验:霍尔信号在电机启动和低速时抖动很厉害。我建议把输入滤波器(TIM_ICFilter)设到0x0F以上,相当于对信号做4-8个时钟周期的数字滤波。另外,中断服务程序里别做复杂计算,换相表提前算好放在数组里,中断里直接查表赋值就行。

4.5 电机控制基础:六步换相法

BLDC电机最经典的控制方式就是六步换相。说白了,就是根据霍尔信号的状态,决定哪两相通电,哪一相悬空。

换相表长这样:

霍尔状态(HA:HB:HC) 导通相 电流方向
001 A+ / B- A→B
010 C+ / A- C→A
011 B+ / A- B→A
100 B+ / C- B→C
101 A+ / C- A→C
110 C+ / B- C→B

每次霍尔状态变化,就切换到下一组导通相。这样转子每转60°电角度,换相一次。一个完整的电周期需要6次换相。

// 换相表:索引为霍尔值(1-6),值为CCR1/CCR2/CCR3配置
// 0表示该相悬空(PWM输出关闭)
const uint16_t commutation_table[8][3] = {
    {0, 0, 0},          // 000 - 无效状态
    {PWM_ON, 0, PWM_OFF}, // 001: A+ B-
    {PWM_OFF, PWM_ON, 0}, // 010: C+ A-
    {0, PWM_ON, PWM_OFF}, // 011: B+ A-
    {PWM_OFF, 0, PWM_ON}, // 100: B+ C-
    {PWM_ON, PWM_OFF, 0}, // 101: A+ C-
    {0, PWM_OFF, PWM_ON}, // 110: C+ B-
    {0, 0, 0}           // 111 - 无效状态
};

// 霍尔中断服务程序
void TIM1_UP_IRQHandler(void) {
    if(TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) {
        // 读取霍尔状态(从TIM1的捕获/比较寄存器中获取)
        uint8_t hall_state = (TIM_GetCapture1(TIM1) & 0x01) |
                             ((TIM_GetCapture2(TIM1) & 0x01) << 1) |
                             ((TIM_GetCapture3(TIM1) & 0x01) << 2);
        
        // 查表更新PWM输出
        TIM_SetCompare1(TIM1, commutation_table[hall_state][0]);
        TIM_SetCompare2(TIM1, commutation_table[hall_state][1]);
        TIM_SetCompare3(TIM1, commutation_table[hall_state][2]);
        
        TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
    }
}

我曾经踩过的坑:霍尔信号在电机高速旋转时会产生很大的共模噪声,导致误触发中断。我的解决方案是:在硬件上给每路霍尔信号加一个RC低通滤波器(1kΩ + 10nF),同时在软件里做连续三次采样确认。从那以后,误触发的问题再也没出现过。

4.6 刹车功能与安全保护

高级定时器的刹车功能(BRK)是个救命的设计。当检测到故障信号(比如过流、过温)时,硬件会立即把所有PWM输出强制为安全状态,不需要软件干预。

配置刹车功能:

// 刹车输入引脚:TIM1_BKIN (PA6)
// 配置为高电平触发刹车
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Enable;
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Disable;  // 刹车后不复位

// 刹车后的输出状态
TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable;   // 空闲时输出使能
TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable;   // 空闲时输出使能
TIM_BDTRInit(&TIM1, &TIM_BDTRInitStructure);

刹车触发后,所有PWM输出引脚会立即进入预定义的安全状态。如果你想恢复输出,需要软件清除刹车标志并重新使能主输出。

安全设计原则:刹车信号一定要用硬件直接连接,不要经过任何软件逻辑。我在一个项目中把过流信号通过比较器直接接到TIM1_BKIN引脚,响应时间小于1微秒。如果用ADC采样再软件判断,至少需要几十微秒,MOS管早就烧了。

4.7 实战:一个完整的直流无刷电机驱动框架

最后,我把前面讲的内容整合成一个完整的驱动框架。这个框架我在多个项目中复用,稳定可靠:

// 电机控制初始化
void Motor_Init(void) {
    // 1. 初始化GPIO:PWM输出、霍尔输入、刹车输入
    GPIO_Init();
    
    // 2. 初始化高级定时器:互补PWM + 霍尔模式 + 刹车
    TIM1_Init();
    
    // 3. 初始化ADC:采样母线电压和相电流(可选)
    ADC_Init();
    
    // 4. 使能中断
    NVIC_Init();
    
    // 5. 启动定时器
    TIM_Cmd(TIM1, ENABLE);
    TIM_CtrlPWMOutputs(TIM1, ENABLE);
}

// 主循环:速度环控制
void Motor_SpeedControl(void) {
    while(1) {
        // 读取当前速度(通过霍尔中断计算)
        uint32_t speed = Get_MotorSpeed();
        
        // PID计算目标占空比
        int32_t target_duty = PID_Calculate(speed, target_speed);
        
        // 限制占空比范围
        if(target_duty > MAX_DUTY) target_duty = MAX_DUTY;
        if(target_duty < MIN_DUTY) target_duty = MIN_DUTY;
        
        // 更新PWM占空比(换相由霍尔中断自动完成)
        Set_PWMDuty(target_duty);
        
        // 10ms控制周期
        delay_ms(10);
    }
}

这个框架的核心思想是:换相由硬件中断自动完成,速度环在主循环中慢速调节。这样既保证了实时性,又降低了CPU负载。

我个人习惯把霍尔中断的优先级设到最高(0),PWM更新中断次之(1),主循环里的速度环优先级最低。这样即使主循环被阻塞,电机也能正常换相运行,不会出现堵转或失步。