STM32 FOC六路互补PWM与死区配置实战:从CubeMX到示波器验证
2026/9/19 15:18:38 网站建设 项目流程

我正在调试一个无刷电机的FOC项目,第一次上电就烧了一路MOS管。后来反复复盘,问题就出在六路PWM的互补时序没处理干净、死区设置又太小,上下管在切换瞬间直接短路。很多人找STM32无刷电机FOC驱动代码,代码本身并不难找,难的是底层PWM和死区机制没吃透,跑起来照样炸。这篇文章我就把STM32 + CubeMX配置六路互补PWM的完整过程捋一遍,重点讲清楚死区时间怎么算、怎么配、怎么用示波器验证,顺便把引脚冲突、HAL库启动顺序这些坑也填上,给准备上手FOC的朋友一份可以直接照做的参考。

1. FOC控制为什么绕不开六路互补PWM:从桥臂结构说起

1.1 三相全桥的物理结构决定了必须有六路驱动信号

FOC最终是通过三相全桥逆变器来驱动电机绕组的。所谓三相全桥,就是三对半桥、六个功率开关管,一般是MOSFET或IGBT。每一对半桥对应电机的一相绕组,上管接直流母线正极,下管接直流母线负极,两个管子中间的连接点就是电机的相线输出。

这样一看就明白了:STM32要控制这六个功率管,至少要输出六路PWM信号。其中每一对上下管在逻辑上必须“互补”——上管导通时下管必须关断,下管导通时上管必须关断。绝不能出现同一个桥臂上下管同时导通的情况,否则直流母线正负极直接短路,这就是所谓的“直通”或者“桥臂穿通”,瞬间大电流足以把MOSFET的管芯击穿。

FOC算法里,Clarke变换和Park变换算出的是电压矢量,最终要转换成三个桥臂上每个功率管的开关时间,也就是六路PWM各自的占空比。这一整条链路里,PWM配置是物理层的地基,地基没打稳,后面电机跑得再顺也是假象。

1.2 死区时间不是可有可无,它是功率级的保命设置

现实中的功率管不是理想开关。MOSFET有开通延迟、上升时间、关断延迟、下降时间,栅极驱动芯片也有传播延迟。如果上管从导通变成关断的同时,下管立刻从关断变成导通,而上管此时还没完全关断、其漏源电压还没建立起来,下管就已经开始导通,两个管子之间就会形成短暂的低阻通路,也就是直通。直通哪怕只持续几百纳秒,在母线电压几十伏甚至上百伏时都会产生很尖锐的电流浪涌,烧管子往往就是这一下的事。

死区就是在这个上下管切换的瞬间,故意插入一段“两个管都不导通”的空白时间。典型做法是:先让上管关断,等一小段时间,再允许下管开通;反向切换时同样处理。STM32的高级定时器内部带有死区插入器,可以在互补PWM切换边沿自动插入这个延迟,不需要外置逻辑电路,也不需要软件去精细控制关断时序。

但死区不能无限大。死区期间电机相电流没有正常通路,只能靠MOSFET体二极管或外部续流二极管续流,这会造成电压利用率下降、电流波形畸变、电机噪声变大。所以死区是可靠性(不炸管)和波形质量(控制精度)之间的平衡点,这正是“死区优化”这句话的本质。

1.3 为什么非要用TIM1和TIM8这类高级定时器

STM32家族里的定时器分三类:基本定时器、通用定时器、高级控制定时器。TIM1和TIM8属于高级控制定时器,和通用定时器相比,最大的区别就是多了几样电机控制专用的功能:

  • 互补PWM输出,CHx和CHxN成对输出,硬件上天然适合半桥驱动。
  • 可编程死区插入器,通过DTG寄存器直接配置死区时间。
  • 刹车输入(Break Input),外部故障信号可以硬性关断所有PWM输出。
  • 重复计数器,可以让更新事件周期性地延迟发生,配合PWM控制更方便。
  • 与ADC的同步触发机制,用定时器事件直接触发电流采样,减少软件延迟。

假如用通用定时器,我只能输出六路独立的PWM,无法从硬件上保证上下管的互补关系。要么在软件里做关断时序,但中断延迟不可控,电机运行中分分钟出危险;要么在外面加一堆逻辑电路,成本和复杂度全上去了。所以做FOC,首选就是TIM1或TIM8。

2. CubeMX配置前必算:时钟、PWM频率、死区时间的选择逻辑

2.1 定时器时钟到底是多少,别再被APB2绕晕

很多人把CubeMX时钟树配完了,却不清楚定时器实际跑在多少频率。STM32F103的TIM1挂在APB2总线上,TIM2到TIM4挂在APB1总线上。这里有一个非常容易踩的规律:如果APBx预分频器系数为1,定时器时钟就等于PCLKx;如果APBx预分频器系数大于1,定时器时钟等于PCLKx的两倍。

以STM32F103外部8MHz晶振、系统主频72MHz为例。标准配置里APB1预分频一般为2,所以PCLK1是36MHz,TIM2到TIM4的时钟是36MHz乘以2,也就是72MHz;APB2预分频为1,所以PCLK2就是72MHz,TIM1的时钟同样是72MHz。

我的建议是,在CubeMX时钟树页面把APB2这条支路看清楚,确认定时器时钟没有被压到很低的数值。只要定时器时钟是72MHz,后面所有死区计算都按72MHz来算,就基本不会出错。

2.2 PWM频率、ARR和PSC怎么搭配更合理

FOC常用的PWM频率在10kHz到20kHz之间,再高能进一步降低电机噪音,但开关损耗也会相应增加。我以20kHz为例,定时器时钟72MHz,向上计数模式,公式是这样的:

fPWM = TIM_CLK / ((PSC+1) × (ARR+1))

代入20kHz:

20kHz = 72MHz / ((PSC+1) × (ARR+1))

为了得到最大的占空比分辨率,让PSC保持为0,于是ARR等于3599,也就是Counter Period写3599。

为什么优先让PSC保持0?因为PSC越大,ARR越小,同一个PWM周期内可以细分的占空比级数就越少。比如PSC设71,ARR只有50,那么占空比最小步进是1/50,也就是2%,对FOC来说这个分辨率完全不够看。FOC对占空比的精细度要求很高,所以一开始就压小PSC,给后面的调节留足余量。

2.3 死区时间到底选多大:从器件手册反推

死区时间不是一个拍脑袋就能定的数,主要看这几个因素:

  • 功率MOSFET的开通延迟、关断延迟、上升时间、下降时间。
  • 栅极驱动芯片的传播延迟。
  • 母线电压和电流等级。
  • 驱动电阻大小,栅极电阻越大,开关过程越慢,死区需求越大。

工程上常见的经验范围我列一个参考表,实际数值一定要以你的器件手册为准:

应用场景典型死区时间
小型无人机电机(几瓦到几十瓦)300ns ~ 800ns
中小功率伺服/机器人关节(几十瓦到几百瓦)500ns ~ 1.5μs
电动车/工业驱动器(几百瓦到数十千瓦)1μs ~ 3μs

提示:写死区参数之前,先查MOSFET数据手册里的td(on)、tr、td(off)、tf这些开关时间参数,再叠加驱动芯片延迟,并留出至少20%到50%的安全裕量。

网上很多人说“用1μs就行”,千万别照抄。不同MOSFET开关速度差很多,同一个值在这块板子上没问题是运气,在另一块板子上炸管是常态。

2.4 死区分辨率与CKD时钟分割的隐藏关系

CubeMX的TIM1参数里有一项“Internal Clock Division”,也就是CKD,它直接影响死区时钟步进。很多人忽略这个选项,导致死区配置出来后实际值和预期对不上。

当CKD设为No Division时,死区时钟步进Tdtg等于定时器时钟周期tCK_INT。在72MHz定时器时钟下,Tdtg大约是13.89ns。如果CKD设为Division 2,Tdtg就是27.78ns;如果设为Division 4,Tdtg是55.56ns。也就是说,CKD选得越大,死区调节粒度越粗。

F103的TIMx_BDTR寄存器中,死区时间可以由DTG[7:0]位配置,具体分四档计算:

  • DTG[7:5] = 0xx:DT = DTG[7:0] × Tdtg
  • DTG[7:5] = 10x:DT = (64 + DTG[4:0]) × 2 × Tdtg
  • DTG[7:5] = 110:DT = (32 + DTG[4:0]) × 8 × Tdtg
  • DTG[7:5] = 111:DT = (32 + DTG[4:0]) × 16 × Tdtg

举个实际例子:我想在72MHz下配置约1μs死区。Tdtg为13.89ns,用第一档计算,DTG约等于72,写成十六进制就是0x48。CubeMX图形界面里其实可以之间填时间值,不需要手算DTG,但你自己懂了这套映射关系,用示波器验证时心里才有底。

3. CubeMX六路互补PWM实操:一步步配好TIM1并避免引脚冲突

3.1 新建工程时的基础设置

打开CubeMX,新建工程,芯片选择STM32F103C8T6或者你实际用的型号。先到System Core里,把RCC的HSE设为Crystal/Ceramic Resonator,也就是外部高速晶振模式。再进SYS,把Debug设为Serial Wire,否则后面下载调试时SWD引脚可能会被重新映射,导致连不上调试器。

时钟树页面里,把HCLK直接拉到72MHz。CubeMX会自动分配APB1和APB2的预分频值。这时候重点看一眼:APB1外设时钟是36MHz,但定时器时钟是72MHz;APB2外设时钟和定时器时钟都是72MHz。确认无误后进入TIM1配置。

这些基础步骤决定了整个工程的根基,虽然不起眼,但很多人最后发现PWM频率不对、死区时间不对,回头排查才发现时钟树里某个分频系数被调过。

3.2 启用TIM1的PWM生成通道与互补通道

在Pinout & Configuration里选中TIM1,激活内部时钟,然后在“PWM Generation”这一栏里,把六个输出全部勾上:

  • PWM Generation CH1
  • PWM Generation CH1N
  • PWM Generation CH2
  • PWM Generation CH2N
  • PWM Generation CH3
  • PWM Generation CH3N

勾选之后,CubeMX会自动分配引脚。默认情况下,PA8对应TIM1_CH1,PB13对应TIM1_CH1N,PA9对应TIM1_CH2,PB14对应TIM1_CH2N,PA10对应TIM1_CH3,PB15对应TIM1_CH3N。这个映射关系在后续焊接驱动板、设计PCB时一定要提前确认,避免信号交叉。

参数面板里,我常用的典型配置是:

  • Prescaler = 0
  • Counter Mode = Up
  • Counter Period = 3599
  • Auto-reload preload = Enable
  • CH1、CH2、CH3的Pulse都先设为1800,也就是50%占空比的初始值
  • Break and Dead Time settings里面,Dead Time填写1000ns

这样六路互补PWM的硬件底层就算配好了,后续生成代码直接就能跑出波形。

3.3 引脚分配与重映射的坑

默认映射看起来挺方便,但PA9和PA10同时是USART1的TX和RX。如果开发板板载串口占了这两个引脚,或者你的项目中要用串口打印调试信息,就要提前规划好。CubeMX如果在Pinout页面检测到冲突,会在工程里报错,别硬生成。

F103的TIM1支持部分重映射和完全重映射。部分重映射可以把互补输出通道换到PA7、PB0、PB1等引脚,完全重映射则搬到PE8到PE15。小封装如C8T6没有那么多PE引脚,基本只能走默认映射或部分重映射。如果六个引脚都挤在一起和你的驱动板布局冲突,要么换大封装芯片,要么把驱动板设计提前和引脚分配一起做,别等画完板子再回来改。

还有一个细节:配置TIM1的PWM时,如果同时启用了TIM1的刹车功能,BKIN引脚会被占用。BKIN默认在PB12,也可能重映射到PA6。没用到外部刹车输入时,我建议在CubeMX里不要随便开启Break功能,不然初始化后MOE位被刹车信号锁死,六路PWM根本没输出,排查起来很耽误时间。

3.4 输出空闲状态与BKIN刹车输入别乱配

在TIM1的Break and Dead Time settings里,有两项叫做“Output Idle State”和“Output N Idle State”。它们的作用是,当MOE主输出使能被关闭时,比如刹车触发或初始化阶段,CHx和CHxN引脚输出什么电平。

默认值一般是Reset,也就是低电平。但如果有人把它配置成了高电平,在PWM还没启动的时候,引脚就已经输出高,接到驱动电路上就可能导致功率管误导通。这个问题非常隐蔽,很多人在上电瞬间炸管,查来查去最后发现就是空闲状态配置错了。

我建议保持低电平,并且在驱动板硬件设计上,给每个功率管栅极加一个10k到100k的下拉电阻,保证MCU初始化完成之前栅极被可靠拉低。软件电平状态和硬件上电默认状态一致,才能把误触发风险降到最低。

4. 工程生成后的代码级细节:启动顺序、占空比更新与死区动态调整

4.1 HAL库启动顺序不对,就会出现“有主输出没互补输出”

CubeMX生成工程后,最常规的启动代码是这样:

HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_2); HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_3);

一定要把主输出和互补输出分别启动。HAL_TIMEx_PWMN_Start内部会打开对应通道的互补输出使能位,同时确保MOE主输出使能。如果只启动了主输出,互补输出引脚会一直没波形;如果只启动了互补输出,主输出同样没有波形。两套都要调。

启动时还有一种常见现象:引脚输出恒高或者恒低。这时候先检查MOE位是否被关闭,直接读一下TIM1的BDTR寄存器看MOE位就行。同时要检查有没有发生刹车事件,也就是BIF标志。很多驱动板上接了故障反馈到BKIN脚,如果反馈信号一直处于触发状态,MOE会被强制关闭,PWM自然全无输出。

4.2 运行中如何安全地更新占空比

FOC算法每个PWM周期都要更新三相占空比,最常用的宏是:

__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, value); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, value); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, value);

value的取值范围是0到ARR。如果你用的是SVPWM,最终给到每个通道的比较值,是SVPWM算法算出来的三相占空比映射结果。PWM外设本身不关心这个值是怎么算出来的,它只负责每一个计数周期把比较器值和计数器当前值做比较,从而翻转输出电平。

占空比的更新最好放在PWM周期中断里做,避免在任意时刻写寄存器导致输出波形出现毛刺:

void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { // 在这里计算下一周期的三相占空比并更新 } }

还需要用__HAL_TIM_ENABLE_IT(&htim1, TIM_IT_UPDATE)使能更新中断,并且调用HAL_TIM_IRQHandler来驱动HAL库的回调机制。这套链路不复杂,但初始化遗漏一个环节,中断就永远进不去。

4.3 运行时调整死区可行吗,什么时候不可行

理论上可以在运行中直接修改BDTR寄存器的DTG位来改变死区时间:

TIM1->BDTR = (TIM1->BDTR & ~TIM_BDTR_DTG_Msk) | (new_dtg << TIM_BDTR_DTG_Pos);

但我不建议在PWM正常输出时频繁修改死区。原因是,对DTG的改动会立刻作用于后续边沿,如果写寄存器的时机恰好落在切换边沿附近,可能产生一个不完整的异常脉冲。这个脉冲出现在功率级上,就是不可控的开关动作,风险很大。

更稳妥的做法是:先停机,在PWM停止输出的状态下修改死区,再重新启动。死区本身是设计阶段就该定好的参数,不是FOC运行中需要实时调整的量。硬件上如果实在想灵活调,可以选带可编程死区时间的外部栅极驱动芯片,通过SPI或引脚去设置,但这样又增加了系统的复杂度和成本。

5. 死区优化实测:示波器测量方法与常见问题速查

5.1 示波器上如何准确测量死区

死区优化必须有依据,不能靠猜。最直接的方法是拿双通道示波器,一路接CHx,一路接CHxN,同时看两路波形之间的间隔。测量时有一个非常重要的细节:探头的地线夹要用短地线,不要用那根又细又长的地线夹,否则测量噪声和振铃会大得离谱,甚至测出错误的死区宽度。

把时间轴缩放,观察两路波形从一路变低到另一路变高的间隔,这段两个输出都为低的时间就是死区。实际死区时间和配置值之间如果有偏差,优先检查CKD设置,因为死区步进取决于它。

如果没有双通道示波器,只有单通道,也可以通过半桥输出点波形做一个粗测,但信息量少很多,很难判断死区到底是偏大还是偏小。预算允许的话,300MHz以上带宽的双通道示波器是做电机驱动调试的底线配置。

有一个实测技巧:把PWM频率临时调低到1kHz,占空比调到50%,死区在示波器上会显示得更宽、更容易测量。测完再恢复实际运行频率,这样测量误差更小。

5.2 死区过大和过小分别是什么表现

死区参数设置不合适,故障现象往往很典型:

症状大概率原因验证手段
上电瞬间或切换瞬间MOSFET发烫、冒烟死区太小,存在直通示波器看互补输出交叉点,调大死区
相电流波形畸变、过零平台明显死区太大,续流二极管长时间导通调小死区,对比相电流波形
电机低速时噪声大、振动明显死区太大导致电压误差被放大结合相电流波形和FFT分析,逐步减小死区
空载正常、带载一高就炸管死区裕量不足,负载切换瞬间出现直通提高死区时间,检查母线电压尖峰

死区优化不是让死区越小越好。减小死区可以改善电流波形质量,但如果功率管本身的开关参数不够好、驱动电路布局又比较差,那一丁点时间裕量根本不够用。我的习惯是:先按器件手册算出一个偏保守的值,再用示波器逐步把死区往小调,直到观察到相电流波形开始出现明显畸变,再回调一点,留出余量。

5.3 常见问题排查速查表与踩坑实录

我整理了一张在实际调试中反复用到的排查表:

现象可能原因排查方法
六路引脚全部无输出MOE未使能 / 发生刹车事件读BDTR的MOE、BIF;检查BKIN引脚电平
只有CHx输出,CHxN没有没调用HAL_TIMEx_PWMN_Start补上互补输出启动
CHx和CHxN同时为高死区未生效或配置错误检查DTG寄存器值和死区时间
引脚输出恒高输出空闲状态被设为高在CubeMX中把Idle State改为低电平
上电瞬间误触发引脚悬空期间电平不定驱动板栅极加下拉电阻
烧MOSFET但波形看起来正常死区不够或栅极尖峰过大用短地线探头看开关瞬态,检查死区

这些坑我基本都踩过。最惨的一次是驱动一个500W电机,自认为配置完全没问题,上电后一启动就炸了两路MOS管。后来拆下板子仔细测,发现死区只设了300ns,而那只MOSFET的关断时间在1μs以上,直通电流瞬间把管子击穿。所以无论算法多漂亮,第一步永远是验证功率级。

还有一个在样机阶段特别实用的经验:在直流母线回路串一个限流电阻或者白炽灯,能极大保护功率级。如果死区配置有问题,现象就是灯发亮、电流偏大;逐步排查排除问题后,再把限流电阻调到正常水平。这个方法看起来土,但在FOC联调初期能帮你省下一堆器件成本。

6. 从六路PWM到完整FOC系统的衔接要点

把六路互补PWM和死区配置讲透之后,很多朋友下一步就是在线搜“FOC代码”或者说想立刻把算法跑起来。别急,这里有三个衔接点,我建议一次过完,否则下一个环节照样会卡壳。

6.1 PWM只是执行层,电流采样才是FOC的下一个门槛

FOC闭环控制必须要知道电机相电流。电流采样一般用采样电阻或者霍尔电流传感器,读ADC的时机非常关键。简单可靠的方案是让ADC由TIM1的TRGO触发,在PWM周期的固定时刻自动采样,这样不用在软件里纠结到底哪一行读取ADC最合适。在CubeMX中,把TIM1的Trigger Output设置为Update Event,再把ADC触发源选成TIM1即可。采样点要尽量避开关管的开关时刻,否则尖峰干扰会被采进去,电流环反馈乱跳。

6.2 开环跑起来再进闭环,转子初始定位别跳过

六路PWM配好之后,先写一个最简单的程序,让三相依次通电,验证电机能不能步进转动、方向对不对。接着做转子初始位置检测,这是无感FOC启动前必须做的一步。很多无感方案用高频注入或者矢量脉冲法来判定转子初始角度,而不是盲目给定一个角度直接启动。先把PWM和电流采样这两个硬件基础验证好,再去跑复杂算法,效率反而更高。

6.3 从“能转”到“控制好”的调试路径

我的建议是这样:先用开环VF或者六步换向让电机转起来,确认驱动方向、相序都没问题;再上FOC开环,给定Iq、Id目标,观察电流是否跟随;最后才能切入闭环速度环和位置环。每一步都是一个独立的小验证节点,这样即使出问题,也不会把故障点扩散到整个系统里。

调试工具方面,CubeMX只负责生成初始化代码,真正调FOC算法还需要调试器实时看变量,J-Link或ST-Link都行。我习惯在串口上打印Iq、Id反馈这些关键变量,实时性要求高的时候就上DAC或者虚拟示波器。这些属于后话,但提前把调试链路搭好,后面会顺手很多。

最后再分享一个我的习惯:每次改完高级定时器的配置,都先不要急着接电机。用示波器记录一段CH1和CH1N的波形,确认实际死区时间和配置值之间的误差在可接受范围内,再接电机通电。项目稳定之后,把死区参数、PWM参数单独抽到一个配置头文件里,方便以后换器件时快速调整。死区优化本质上是和功率管的开关速度、驱动电路、母线电压做匹配,这个参数一旦调好,后续FOC控制算法才能在一个可靠的功率地基上跑起来。别嫌这一步麻烦,它值得被认真对待。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询