3、PWM信号生成与MCU配置:定时器PWM模式、死区时间插入、互补PWM输出、寄存器配置实战
好,咱们进入第三章。这一章是电机控制的硬核基础——PWM信号怎么从MCU里生成,怎么配寄存器,死区时间怎么插进去。说白了,你代码写得再花哨,如果PWM波形出来是畸形的,电机照样抖得像筛子。我早期吃过这个亏,所以这一章咱们直接上手实战。
3.1 定时器PWM模式:核心原理
大部分MCU的定时器都支持PWM模式。原理其实不复杂:一个计数器从0往上数,数到自动重装载值(ARR)就归零,这叫向上计数模式。然后你设一个比较值(CCR),当计数值小于CCR时,输出高电平;大于等于CCR时,输出低电平。这样就能调占空比了。
关键公式:
- PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
- 占空比 = CCR / (ARR + 1) × 100%
举个例子:时钟72MHz,PSC=71,ARR=999,那频率就是72M/72/1000 = 1kHz。CCR=500,占空比50%。
我个人习惯先把ARR和PSC算好,再算CCR。你想想看,如果ARR设小了,分辨率就不够,电机低速时抖得厉害。我有个项目做伺服驱动器,ARR设成1999,结果低速时电机一顿一顿的,后来把ARR改成9999,分辨率提上去,问题就解决了。
3.2 互补PWM输出与死区时间
电机驱动常用H桥或三相桥,上下管不能同时导通,否则就是短路。所以需要两路互补的PWM——一路高电平,另一路低电平。但光互补还不够,因为功率管关断比开通慢,如果上管刚关断下管就开通,瞬间就穿通了。这时候就需要死区时间。
死区时间,就是上下管都关断的那一小段空白期。一般几百纳秒到几微秒,取决于功率管的开关速度。
我的经验:死区时间设得太短,容易炸管;设得太长,波形失真,电机发热。我一般先按器件手册的典型值设,然后用示波器看,再微调。IGBT通常设1-3μs,MOSFET设200-500ns。
为什么会这样?因为死区时间本质上是牺牲效率换安全。你想想看,死区期间电机电流是续流二极管走的,压降大,损耗也大。所以死区时间要在安全的前提下尽量短。
3.3 寄存器配置实战:以STM32为例
咱们拿STM32F103的TIM1来演示。TIM1是高级定时器,支持互补输出和死区插入。配置步骤我列一下:
- 使能时钟:RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
- 配置GPIO:PA8(TIM1_CH1)、PA9(TIM1_CH1N)设为复用推挽输出
- 配置时基:设置PSC、ARR、计数模式
- 配置输出比较:设置PWM模式1或2,设置CCR
- 配置死区:设置TIM_BDTR寄存器的DTG位
- 使能输出:使能MOE主输出使能位
- 使能定时器:TIM_Cmd(TIM1, ENABLE);
代码示例:
// TIM1 PWM初始化,互补输出,死区1μs
void TIM1_PWM_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
// 2. 时基配置:72MHz/(71+1)/(999+1)=1kHz
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 999;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// 3. PWM模式1,CCR=500,占空比50%
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;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
// 4. 死区配置:DTG=250,对应1μs(具体算法见数据手册)
TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Disable;
TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Disable;
TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF;
TIM_BDTRInitStructure.TIM_DeadTime = 250;
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable;
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Disable;
TIM_BDTRInit(TIM1, &TIM_BDTRInitStructure);
// 5. 使能主输出和定时器
TIM_CtrlPWMOutputs(TIM1, ENABLE);
TIM_Cmd(TIM1, ENABLE);
}
注意:高级定时器(TIM1、TIM8)必须使能MOE位(通过TIM_CtrlPWMOutputs),否则PWM输出不出来。我见过有人配了半天没波形,就是忘了这步。
3.4 死区时间计算与寄存器映射
死区时间由TIM_BDTR寄存器的DTG[7:0]控制。STM32的死区计算分两段:
| DTG[7:5] | 死区计算公式 | 分辨率 | 示例 |
|---|---|---|---|
| 000 | DT × Ttclk | 1 × Ttclk | DTG=0x00~0x7F,死区0~127个时钟周期 |
| 001 | (64 + DT[4:0]) × 2 × Ttclk | 2 × Ttclk | DTG=0x80~0xBF,死区128~254个时钟周期 |
| 010 | (32 + DT[4:0]) × 8 × Ttclk | 8 × Ttclk | DTG=0xC0~0xDF,死区256~504个时钟周期 |
| 011 | (32 + DT[4:0]) × 16 × Ttclk | 16 × Ttclk | DTG=0xE0~0xFF,死区512~1008个时钟周期 |
假设定时器时钟72MHz,Ttclk=13.89ns。要设1μs死区,就是72个时钟周期。DTG=72(0x48),属于第一段,死区=72×13.89ns≈1μs。
避坑指南:我曾经在项目里直接用DTG=250,没仔细算,结果死区设成了2μs,电机效率掉了3%。后来用示波器一量才发现。所以建议你配完寄存器后,一定用示波器抓一下死区时间,别光靠算。
3.5 知识体系:PWM生成与配置流程
下面这张图把整个流程串起来了,从时钟到波形输出,每一步都清楚。
3.6 实战要点总结
- 频率选择:电机PWM频率一般10-20kHz,避开人耳可听范围。我习惯用16kHz,噪声和损耗平衡得比较好。
- 分辨率:ARR越大,占空比调节越精细。但ARR太大会降低频率,需要权衡。
- 死区对称性:上下管的死区时间要一致,否则波形不对称,电机电流会有直流分量。
- 寄存器备份:配完寄存器后,读回来校验一下。我遇到过寄存器写不进去的情况,读回来全是0,查了半天发现是时钟没使能。
警告:第一次上电调试PWM时,建议先把电机断开,用示波器看波形。确认频率、占空比、死区都对了,再接电机。否则一个短路,MOSFET就冒烟了。我亲眼见过同事的板子直接炸了一个洞。