第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输入上,定时器会自动检测边沿变化,并产生更新事件。
我画了一张霍尔接口的框图,帮你理解数据流:
配置霍尔模式其实很简单,关键就两步:
- 把三个GPIO引脚配置为定时器输入捕获通道
- 设置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),主循环里的速度环优先级最低。这样即使主循环被阻塞,电机也能正常换相运行,不会出现堵转或失步。