4. STM32 GPIO与PWM输出:从定时器到电机绕组的信号链路
这一章,我们聊聊STM32怎么把脑子里的控制算法,变成电机能听懂的PWM波。说白了,就是GPIO和定时器那点事。但你别小看它,我见过太多项目死在这第一步——配置错了,电机要么不动,要么乱抖,严重的时候直接炸管子。
咱们一步步来。先从定时器怎么产生PWM讲起,再说互补PWM和死区,最后聊聊GPIO的驱动能力。嗯,这些都是电机驱动的根基。
4.1 STM32定时器:PWM的心脏
STM32的定时器分三类:基本定时器、通用定时器、高级定时器。做电机驱动,你主要跟高级定时器(TIM1/TIM8)打交道。为什么?因为它自带互补输出、死区插入、刹车输入——这些是电机控制硬刚需。
核心概念:PWM的本质就是定时器计数器(CNT)跟一个比较值(CCR)不断比大小。CNT从0数到ARR(自动重装载值),每比一次,输出电平就翻转一次。占空比 = CCR / (ARR + 1)。
举个例子。你想输出20kHz的PWM,系统时钟72MHz。那ARR就设成3599,CCR设成1800,占空比就是50%。
// 以TIM1为例,输出20kHz PWM,占空比50%
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频,72MHz
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 3599; // ARR = 3599
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 1800; // CCR = 1800
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
TIM_Cmd(TIM1, ENABLE);
// 高级定时器别忘了使能主输出
TIM_CtrlPWMOutputs(TIM1, ENABLE);
我的习惯:ARR和CCR我一般用宏定义,方便调试时改。另外,高级定时器有个坑——主输出使能位(MOE)默认是关闭的。你配完所有寄存器,PWM死活不出来,八成是忘了调TIM_CtrlPWMOutputs()。
4.2 互补PWM与死区时间:别让上下管直通
电机驱动用H桥或三相逆变器,上下两个开关管交替导通。理想情况是上管关断瞬间下管立刻导通。但现实是——管子关断需要时间。如果上管还没完全关断,下管就导通了,那就直通短路,电流瞬间飙升,管子秒炸。
所以我们需要死区时间。就是在上管关断后,等一小段时间,再让下管导通。这段时间就叫死区。
| 参数 | 说明 | 典型值 |
|---|---|---|
| 死区时间 | 上下管切换时的等待时间 | 0.5µs ~ 3µs |
| 死区设置寄存器 | TIMx_BDTR中的DTG[7:0] | 取决于定时器时钟 |
| 死区影响 | 太短→直通风险;太长→波形畸变、效率降低 | 需折中 |
STM32高级定时器支持硬件死区插入。你只需要配好BDTR寄存器,硬件自动在互补信号之间插入死区。省心,而且精度高。
// 配置死区时间,假设定时器时钟72MHz,死区1µs
// DTG = 72 * 1 = 72,对应BDTR寄存器的DTG位
TIM_BDTRInitTypeDef TIM_BDTRInitStructure;
TIM_BDTRInitStructure.TIM_OSSR = TIM_OSSR_Enable;
TIM_BDTRInitStructure.TIM_OSSI = TIM_OSSI_Enable;
TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1;
TIM_BDTRInitStructure.TIM_DeadTime = 72; // 死区1µs
TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable;
TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High;
TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable;
TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure);
我曾经踩过的坑:有次做一款低压伺服驱动,死区设了0.3µs,觉得够用了。结果MOS管结电容偏大,关断时间比手册标的长。上电一跑,管子直接冒烟。后来老老实实拿示波器量了实际关断时间,死区改到1µs才稳定。所以,死区时间别光看手册,一定要实测。
互补PWM的配置也很直接。使能TIM_OCInitStructure里的互补输出通道就行。
// 使能互补输出
TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;
TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High;
TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
4.3 GPIO驱动能力与上拉/下拉电阻
PWM信号从定时器输出到GPIO引脚,最后送到隔离芯片或驱动芯片。这中间,GPIO的配置直接影响信号质量。
STM32的GPIO输出模式有几种:推挽输出、开漏输出、复用推挽、复用开漏。做PWM输出,我们一般用复用推挽输出(AF_PP)。为什么?因为定时器的输出信号是通过复用功能连接到GPIO的,不是普通IO口控制的。
| GPIO模式 | 适用场景 | 驱动能力 |
|---|---|---|
| 复用推挽(AF_PP) | PWM输出、USART、SPI等 | 强,可输出高电平 |
| 复用开漏(AF_OD) | 需要外部上拉的场合 | 弱,需外接上拉 |
| 推挽输出(GP_PP) | 普通IO控制 | 强 |
驱动能力方面,STM32每个GPIO引脚可以输出最大20mA电流(最大额定值)。但整片芯片的总电流有限制,具体看数据手册。做电机驱动时,PWM信号频率高(10kHz~100kHz),GPIO的压摆率(Slew Rate)也要注意。STM32提供了速度等级配置:2MHz、10MHz、50MHz。我一般设成50MHz,保证信号边沿陡峭,减少开关损耗。
// 配置PA8为TIM1_CH1的PWM输出
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 高速
GPIO_Init(GPIOA, &GPIO_InitStructure);
上拉/下拉电阻怎么选?PWM输出一般不需要内部上拉或下拉,因为推挽输出本身就能驱动高低电平。但有一种情况——芯片刚上电、GPIO还没配置时,引脚默认是浮空输入。这时候如果外部电路已经上电,可能会误触发。我的做法是在外部加一个10kΩ下拉电阻到地,确保上电瞬间PWM引脚是低电平,驱动芯片不会误动作。
4.4 知识结构总览
下面这张图把整个信号链路串起来了。从定时器配置到GPIO输出,每一步都有讲究。
4.5 避坑指南与经验总结
- 死区时间别照搬手册:我习惯先设一个保守值(比如2µs),然后用示波器看实际波形,再慢慢往下调。调到波形刚好没有直通毛刺为止。
- GPIO速度别设太低:有次为了省电,把GPIO速度设成2MHz,结果PWM波形变成梯形波,电机嗡嗡响。后来改回50MHz,问题解决。
- 上电时序要考虑:STM32上电后GPIO默认是浮空输入。如果驱动芯片先上电,PWM引脚悬空,驱动芯片可能误判为高电平。加个外部下拉电阻,或者让驱动芯片后上电。
- 高级定时器的MOE位:这个我提过好几次了,但还是要强调——TIM_CtrlPWMOutputs(TIM1, ENABLE) 这句不能省。
一句话总结:PWM信号链路从定时器到GPIO,每一步都要精准配置。死区时间、GPIO速度、上拉下拉,这些细节决定了你的电机驱动是稳定运行还是冒烟炸管。