用STM32F103C8T6做多通道PWM输出,最容易被忽略的一个问题就是同步。去年帮朋友调一台双电机履带底盘,两个轮子分别用TIM2和TIM3输出PWM,启动瞬间底盘总往右偏。一开始怀疑PID参数没整定好,后来用逻辑分析仪一抓波形,两路PWM的上升沿差了接近120us。对1kHz频率来说,这等于相位错开了12%,左右电机在启动阶段拿到的是完全不一样的平均电压,车身不跑偏才怪。后来把定时器主从同步打开,边沿对齐到几十纳秒以内,问题一次解决。这篇文章把我踩过的坑、翻过的数据手册和最终落地的配置方式完整写出来,希望能帮到被类似问题卡住的人。
这篇文章适合几类人:用F103C8T6做多路舵机/电机控制、做轮速模拟或PWM协议输出、以及在H桥和逆变器调试里被互补PWM和死区坑过的朋友。就算你目前只做单路PWM,理解这套同步机制之后,以后遇到多路输出需求也能少走弯路。
1. 先盘资源:F103C8T6到底能输出几路PWM,引脚怎么分配
1.1 定时器家底:四个能出PWM的定时器,别被TIM6/TIM7坑了
STM32F103C8T6属于中容量产品,64KB Flash、20KB RAM,最大主频72MHz。很多新手看资料说F103有TIM1到TIM8,就想当然认为C8T6也能输出很多路PWM,实际不是这样。
C8T6能产生PWM的定时器有四个:TIM1、TIM2、TIM3、TIM4,每个定时器有4个通道,理论最多16路PWM。另外还有TIM6和TIM7两个基本定时器,但它们只有时基功能,没有输出比较和捕获通道,不能用来生成PWM。换句话说,你规划一个需要20路PWM的方案时,C8T6在定时器资源上就不够,得换更大封装的型号比如RCT6,或者用多片MCU协同。
F103C8T6上四个定时器的引脚资源和总线归属我整理成了一张表:
| 定时器 | 通道数 | 所在总线 | 默认引脚(LQFP64封装) | 常见冲突 |
|---|---|---|---|---|
| TIM1 | CH1~CH4,另有CH1N/CH2N/CH3N互补输出 | APB2 | CH1=PA8, CH2=PA9, CH3=PA10, CH4=PA11;CH1N=PB13, CH2N=PB14, CH3N=PB15 | PA9/PA10与USART1冲突,PA11/PA12与USB冲突 |
| TIM2 | CH1~CH4 | APB1 | CH1=PA0, CH2=PA1, CH3=PA2, CH4=PA3 | PA0/PA1常接ADC,PA2/PA3常接USART2 |
| TIM3 | CH1~CH4 | APB1 | CH1=PA6, CH2=PA7, CH3=PB0, CH4=PB1 | PA6/PA7是SPI1的MISO/MOSI |
| TIM4 | CH1~CH4 | APB1 | CH1=PB6, CH2=PB7, CH3=PB8, CH4=PB9 | PB6/PB7是I2C1的SCL/SDA |
另外要注意TIM1的高级定时器特性:它的通道1到通道3都有互补输出(CHxN),可以输出上下桥臂驱动信号,并且自带死区插入和刹车保护功能。通道4没有互补通道。这个特性在做电机驱动时很重要,后面我单独展开。
还有一点容易被忽略:定时器重映射不是所有引脚在C8T6封装里都存在。比如TIM1_CH1可以重映射到PE9,但PE9在64脚的C8T6上根本没有引出。所以我建议核对引脚时直接用芯片封装图对照,不要只看数据手册里的重映射表就去布板。
1.2 引脚规划第一原则:别让串口、调试口和电机驱动抢通道
C8T6最小系统板在网上很常见,大多是LQFP64封装加最小外围电路。板子把IO都引出来了,看着方便,但有个隐藏问题:PA13/PA14/PA15和PB3/PB4默认被调试接口占用,PA9/PA10一旦被USB转串口占用,TIM1_CH2/CH3就别想了。很多人的板载ST-Link和串口都用掉了PA9/PA10,导致做PWM输出时只能绕开这两个引脚。
我的习惯是画板或插面包板之前,先列一张引脚占用表。比如项目里需要16路PWM加一路调试串口,我会这样分配:
| 功能 | 引脚选择 | 对应定时器通道 | 备注 |
|---|---|---|---|
| PWM 1~4 | PA0~PA3 | TIM2_CH1~CH4 | 不接ADC,避免冲突 |
| PWM 5~8 | PA6, PA7, PB0, PB1 | TIM3_CH1~CH4 | SPI1不用时可选 |
| PWM 9~12 | PB6~PB9 | TIM4_CH1~CH4 | 避开I2C1 |
| PWM 13~16 | PA8, PA9, PA10, PA11 | TIM1_CH1~CH4 | PA11不接USB,PA9/PA10不接串口 |
| 调试串口 | PB10, PB11 | — | 用USART3或软件串口 |
如果PA9/PA10必须留给串口,那就把TIM1_CH2/CH3的PWM让出来,把16路方案改成14路,或者考虑用带重映射的引脚,比如TIM2的部分重映射到PA15/PB3/PB10/PB11。但要注意,PB3在调试接口释放之前没法正常用作普通IO,需要先关闭JTAG功能,只保留SWD。
国产替代方面,GD32F103C8T6、APM32F103C8T6这类芯片在引脚和寄存器层面基本兼容原厂,定时器主从同步的连接关系也一样。但有个实际差异是GD32的主频上限更高,默认时钟树和Flash等待周期配置不同,直接从STM32工程迁移过去后,同样的PSC和ARR算出来的PWM频率可能偏了,因为实际时钟不是72MHz。换了替代芯片之后,第一件事就是用示波器测一下PWM频率是否和预期一致。
2. 不同步会怎样:先理解“同步”到底解决什么问题
2.1 电机同时启停时的相位错位,不是PID能救的
回到开头的履带底盘问题。两个电机各用各的定时器,虽然代码里先初始化TIM2再初始化TIM3,PWM_Start的调用顺序也是先TIM2后TIM3,但在MCU内部,两个定时器使能时刻之间存在几十到几百微秒的差异。这不算芯片缺陷,而是正常现象——每条指令执行都需要时间,两条独立的定时器使能指令天然无法保证同时。
对直流电机而言,PWM的占空比决定平均电压,但如果两个轮子的PWM相位错开,启动瞬间左右轮子的瞬时电压不一样。低速时这个现象尤其明显:一个轮子开始转了,另一个轮子还在等待高电平到来。这不是PID参数能解决的,因为从平均占空比看,两个通道的设置值完全一样,示波器上看起来频率和占空比也一样,只有看边沿才会发现问题。
2.2 舵机阵列的抖动和“幽灵偏转”
舵机需要20ms左右的周期,高电平脉宽1ms到2ms对应0到180度。很多人做六路甚至十二路舵机控制时,每个舵机分一个定时器通道,所有通道都在同一个定时器上,这种情况下不会出现同步问题,因为同一个定时器天然共享一个计数器。
问题出在舵机场景超出单个定时器的通道数,比如一组机械臂用了8路舵机,而你的定时器通道全被占满了。这时候如果开第二个定时器来补足剩余通道,两个定时器的启动时刻不同,舵机收到的脉宽起点就不同。看起来所有舵机都在以20ms周期工作,但它们的周期窗口是错开的。高速动作时,舵机阵列会呈现波浪式扫描效果,而不是整齐一致的同步动作。
如果你做过六足机器人或者多关节机械臂,应该能体会到这种“各动各的”带来的调试痛苦。这个问题的根源不是舵机本身,而是PWM基准边沿没有对齐。
2.3 PWM轮速模拟和角度模拟,对边沿对齐是硬指标
有一种场景用到的PWM同步要求比舵机更严格:用PWM输出模拟轮速协议。
在车辆测试台架或ABS开发中,经常需要用PWM信号模拟轮速传感器。协议通常约定:频率代表转速,占空比代表方向或状态,而且同一时刻来自不同车轮的脉冲边沿必须对齐。如果四路轮速PWM各自漂移,被测控制器会误判成轮间转速差,严重时直接触发ABS误动作。
我见过一个案例,客户用多个定时器分别输出四路轮速PWM,频率精度都在0.1%以内,但通道之间存在恒定相位差,导致上位机解析出的轮速序列总是有一个车轮“超前”。后来把所有通道纳入主从同步,相位误差从微秒级降到纳秒级,问题彻底消失。
如果你也需要做类似多路同步脉冲输出,可以先确认你的接收端对边沿对齐的容忍度。老一点的设备可能只要求频率准确,但新一点的测试台架往往对同步有严格要求,这时候定时器主从同步就是唯一靠谱的办法。
2.4 逆变器和H桥场景:同步是安全前提
做单相H桥PWM整流或者逆变器的人,对同步的理解更深刻。同一桥臂上下两个管子必须互补导通,而且切换时中间要有死区,防止上下管直通短路。如果两路PWM不同步,哪怕只是幾微秒的相位差,都可能让死区时间被吃掉一部分,严重时直接炸管。
我在调试H桥驱动电路时,习惯先用逻辑分析仪看两路互补PWM的波形,确认CH和CHN的切换边沿完全对齐、死区时间稳定,才会接功率部分。F103C8T6的TIM1自带死区发生器和刹车保护,这是硬件特性,比用软件来回翻转PSM引脚可靠得多。后面第5章会详细讲。
3. 同步机制拆解:定时器主从触发是怎么工作起来的
3.1 一张表看懂谁跟谁连:内部触发连接
STM32的定时器之间有一条内部通道,叫TRGO(Trigger Output,触发输出)。每个定时器可以通过主模式选择把某些事件广播到TRGO上,其他定时器作为从定时器,通过内部触发输入(ITR0到ITR3)接收这个信号。这条路径完全在芯片内部,不走GPIO,也不需要外部连线。
F103C8T6上四个PWM定时器的内部触发连接是这样:
| 从定时器 | ITR0来源 | ITR1来源 | ITR2来源 | ITR3来源 |
|---|---|---|---|---|
| TIM1 | TIM2 | TIM3 | TIM4 | TIM5(C8T6无效) |
| TIM2 | TIM1 | TIM3 | TIM4 | TIM5(C8T6无效) |
| TIM3 | TIM1 | TIM2 | TIM4 | TIM5(C8T6无效) |
| TIM4 | TIM1 | TIM2 | TIM3 | TIM5(C8T6无效) |
这张表是芯片设计时固定死的,不是软件能随意改的。我经常看到有人问“能不能让TIM2同时触发TIM1和TIM3”,答案是可以,TIM2的TRGO确实连到了TIM1的ITR0、TIM3的ITR1、TIM4的ITR1,理论上可以同时作为三个从定时器的触发源。
但如果你想做一个定时器触发所有其他定时器的级联,就得看具体连接关系。实际操作中,选TIM2做common master的配置方式最自然,因为TIM1/TIM3/TIM4都能直接收TIM2的信号,代码和Cubemx配置都很直观。
3.2 主模式(MMS):TRGO到底能输出什么东西
TRGO不是一根固定的信号线,它输出什么内容由主模式选择位(TIMx_CR2的MMS字段)决定。常见选项有:
| MMS值 | TRGO输出内容 | 适合场景 |
|---|---|---|
| 000 | 复位事件(UG) | 用软件置UG触发从定时器复位 |
| 001 | 使能信号(EN) | 主定时器计数器开始计数时,TRGO立即输出一个事件 |
| 010 | 更新事件(UIF) | 每次计数器溢出时输出事件 |
| 011 | CC1比较脉冲 | 捕获/比较事件作为触发 |
| 100 | OC1REF | 把PWM通道1的原始参考信号直接作为触发时钟 |
在同步输出场景里,最常用的是MMS=001(Enable)和MMS=100(OC1REF)。
MMS=Enable有个很关键的优势:主定时器的CEN位一旦被置1,TRGO立即产生一个事件,从定时器收到后同步启动,整个过程只有芯片内部逻辑延迟,大概几十纳秒。这意味着从定时器和主定时器在同一时刻开始计数,之后如果PSC和ARR配置完全相同,输出的PWM边沿就能始终对齐。
MMS=OC1REF则是把主定时器某个通道的PWM原始波形直接当“时钟总线”传给从定时器,它能做到边沿级对齐,但用法更复杂,在后面4.3会细说。
3.3 从模式:Trigger模式和External Clock Mode1的本质区别
F103的从工作模式(Slave Mode)里,同步输出最常用的是这两种:
触发模式(SMS=110):从定时器收到来自ITR的触发事件后,清零并启动计数器。它的计数时钟仍然是内部的72MHz时钟,PSC和ARR自己独立配置。这种模式适合“大家同时开始,各自按自己的PSC/ARR跑”的场景。因为从定时器收到触发时会自动复位计数器,所以所有计数器的起点也被强制对齐了。
外部时钟模式1(SMS=111):ITR输入信号直接作为从定时器的计数时钟。每个触发上升沿,计数器加一。也就是说,从定时器的计数节拍完全由主定时器的某个事件(比如OC1REF的每个周期)驱动。这种模式下,从定时器自身没有独立运行节奏,它的一切输出都跟着主定时器走,自然做到绝对同步。代价是PWM频率不再直接由72MHz/PSC/ARR决定,而是由触发信号频率和从定时器的PSC/ARR共同决定,配置时要算清楚。
简单类比:Trigger模式就像一排运动员听到发令枪同时起跑,之后各自按自己的节奏跑;External Clock Mode1则是这一排人踩着同一个节拍器迈步,每一步都整齐划一。
如果你只是需要“同时开始”,用Trigger模式就够了。如果你需要“所有输出的每个边沿都对齐在同一个基准”,用External Clock Mode1更彻底。多数电机和舵机项目用Trigger模式就能满足,少数对相位同步要求极高的轮速协议模拟,会用到External Clock Mode1。
4. 实操配置:Cubemx + HAL库四定时器16路同步PWM全流程
4.1 场景规划与Cubemx基础操作
这次我以16路1kHz PWM同步输出为例,所有通道独立调占空比。占空比动态范围10%到90%,频率1kHz,72MHz主频。
F103C8T6的定时器时钟分布要注意:TIM1在APB2总线上,TIM2/TIM3/TIM4在APB1总线上。CubeMX里如果HCLK设为72MHz,APB1预分频器要设为2,这样PCLK1是36MHz,但定时器时钟会自动变成72MHz。检查时钟树时,确认APB1和APB2下的Timer Clocks都显示72MHz,否则后面所有PSC/ARR计算全白搭。
Cubemx里这样配:
| 配置项 | TIM2(主定时器) | TIM3(从定时器) | TIM4(从定时器) | TIM1(从定时器) |
|---|---|---|---|---|
| Clock Source | Internal Clock | Internal Clock | Internal Clock | Internal Clock |
| Channel1~4 | PWM Generation CH1~4 | PWM Generation CH1~4 | PWM Generation CH1~4 | PWM Generation CH1~4 |
| Prescaler | 71 | 71 | 71 | 71 |
| Counter Period | 999 | 999 | 999 | 999 |
| Auto Reload Preload | Enable | Enable | Enable | Enable |
| Pulse(各通道) | 按目标占空比 | 按目标占空比 | 按目标占空比 | 按目标占空比 |
| Slave Mode | 无 | Trigger Mode | Trigger Mode | Trigger Mode |
| Trigger Source | 无 | ITR1 | ITR1 | ITR0 |
关于从定时器的Trigger Source,查前面那张内部连接表:TIM3的ITR1是TIM2,TIM4的ITR1是TIM2,TIM1的ITR0是TIM2。所以TIM3和TIM4选ITR1,TIM1选ITR0。如果你反过来让TIM1做master,那TIM2/TIM3/TIM4都选ITR0(因为它们的ITR0都是TIM1的TRGO)。
注意主定时器TIM2还要在“Trigger Output (TRGO)”里选择触发事件。这里有两个选择:Enable或者Update Event。如果选Update,从定时器会等TIM2跑完第一个周期才启动,整体晚一个PWM周期。所以我建议选Enable,让TIM2的计数器一启动就立刻发TRGO,从定时器同时启动。
4.2 HAL库代码:初始化顺序是同步成败的关键
生成代码后,main函数里默认已经有了各定时器的初始化。需要特别注意PWM启动的先后顺序。
先看主函数里典型的外设初始化顺序:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); MX_TIM2_Init(); MX_TIM3_Init(); MX_TIM4_Init(); ... }初始化完成后,PWM启动顺序建议这样:先启动所有从定时器通道,最后启动主定时器。
// 1. 先把从定时器所有通道配置为输出状态,等待触发 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_TIM_PWM_Start(&htim1, TIM_CHANNEL_4); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4); HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3); HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_4); // 2. 所有从定时器都已就绪,最后启动主定时器TIM2 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);为什么必须先启动从定时器?因为在Trigger模式下,从定时器的计数器虽然不会立刻开始,但它的输出通道必须先使能(CCER里对应位为1)。如果从定时器的通道提前没有打开,TRGO来了之后计数器虽然启动了,但引脚上不会立刻出现PWM。等你在后面某个时刻再执行从定时器的PWM_Start,就会导致从定时器输出比主定时器晚了整整一段启动时间。
主定时器TIM2最好放到最后启动。这样主定时器使能的那一刻,所有从定时器输出通道都已经打开,TRGO一发出,所有通道同步出波形。
对于TIM1作为普通PWM输出(不使用互补输出),上面的代码没问题。如果你要用TIM1的互补输出通道(CH1N/CH2N/CH3N),要改用自己的启动函数:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 主通道 HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); // 互补通道4.3 动态调占空比:预装载必须同时使能
16路PWM输出,最常做的事情是运行时动态修改占空比。很多人直接在中断里写TIM2->CCR1 = xxx,发现PWM波形偶尔出现毛刺或者一个周期跳变。问题多半出在预装载寄存器没有使能。
STM32的定时器通道寄存器CCR有预装载功能。开启后,软件写入CCR的值不会立即生效,而是等下一个更新事件才统一装载到影子寄存器,这样所有通道在同一时刻换占空比,避免一个周期内出现半新半旧的波形。
CubeMX里配置定时器Channel时会有一个“Auto Reload Preload”选项,对应的是ARR预装载。CCR的预装载在HAL库的PWM配置结构体里也有对应字段,生成代码时默认可能没有全开。我习惯在初始化后手动补一段:
__HAL_TIM_ENABLE_OCxPRELOAD(&htim2, TIM_CHANNEL_1); __HAL_TIM_ENABLE_OCxPRELOAD(&htim2, TIM_CHANNEL_2); __HAL_TIM_ENABLE_OCxPRELOAD(&htim2, TIM_CHANNEL_3); __HAL_TIM_ENABLE_OCxPRELOAD(&htim2, TIM_CHANNEL_4);动态调占空比时,建议把所有要修改的通道先写一遍CCR,最后统一触发一次更新事件,比如:
TIM2->CCR1 = 100; TIM2->CCR2 = 200; TIM2->CCR3 = 300; TIM2->CCR4 = 400; TIM2->EGR = TIM_EGR_UG; // 强制产生更新事件,同步装载所有通道这样即使某些通道预装载配置不完整,强制更新也能让同一个定时器的通道保持节奏一致。跨定时器的通道同步更新更麻烦一点:你无法直接让多个定时器同时执行UG。好在如果主从同步配置正确,所有定时器的计数器起点是同一个,而且定时器时钟相同、PSC/ARR相同,它们溢出更新事件天然发生在同一时刻。你只需要保证写入CCR的操作在同一个更新事件之前完成即可。
4.4 用逻辑分析仪验证:看边沿,不要只看频率和占空比
配置完成后,验证同步效果的最好工具是逻辑分析仪或示波器。优先看四个通道的上升沿是否对齐,比如TIM2_CH1、TIM3_CH1、TIM4_CH1、TIM1_CH1。
正确的同步效果是:四个上升沿在采样时间分辨率内落在同一垂直位置,之后四个波形完全同频同相。不同引脚翻转的物理延迟会有纳秒级差异,这是正常现象。如果看到某个通道的上升沿比主通道晚了几微秒甚至几十微秒,说明同步配置还有问题。
有个实用技巧:先把四个通道的占空比都设为相同值,比如50%,这样观察边沿最直观。然后再把占空比改成各不相同但频率相同,确认相位没有漂移。最后动态改变占空比,用逻辑分析仪的连续采样模式抓几秒波形,看是否有周期性的错位。
如果你的逻辑分析仪采样率够高,建议直接测上升沿时间戳,算出相位差记录到调试笔记里。我自己做轮速模拟项目时,会把这种“同步边沿截图”存下来,后面排查问题时作为基准数据。
5. 同步之外的实战组合:占空比批量更新、互补死区与故障保护
5.1 PWM+DMA:高路数占空比刷新不占用CPU
定时器主从同步解决的是“相位对齐”问题,但很多实际场景还有一个痛点:通道太多,CPU来不及逐个更新CCR。比如16路PWM要输出动态呼吸灯效果或者WS2812时序,如果每个通道都在中断里更新,主循环基本就卡死了。
PWM+DMA就是解决这个问题的。原理简单粗暴:把要输出的CCR值按顺序排成数组,用DMA自动搬运到定时器的CCR寄存器,CPU只在传输完成时收到一次中断,甚至完全不用中断。
用PA8的TIM1_CH1驱动WS2812就是一个很典型的例子。WS2812对时序要求严格,一个码元大约1.25us,发送0和1对应的高电平宽度不一样。如果用CPU一条一条翻引脚,根本忙不过来。正确做法是把整个数据帧转换成一串CCR值,再让DMA在PWM更新事件触发下,按顺序把CCR值写入TIM1->CCR1。硬件配置要点:
// 假设72MHz主频,ARR=89,PWM周期为90个时钟周期,约1.25us TIM1->PSC = 0; TIM1->ARR = 89; // DMA请求使能,更新事件触发DMA搬运 __HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_CC1); // DMA控制器配置:内存地址递增,外设地址固定为TIM1->CCR1 hdma_tim1_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_tim1_ch1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_tim1_ch1.Init.MemInc = DMA_MINC_ENABLE; hdma_tim1_ch1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_tim1_ch1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;这里用半字传输是因为TIMx_CCR是16位寄存器。DMA每传输一个值,下一个更新事件时CCR就变成新写入的比较值,PWM的占空比随之改变。整串WS2812数据发完后,DMA正常模式停止,再通过传输完成中断关闭PWM输出,给WS2812一个复位低电平。
如果是16路PWM需要批量更新,思路完全一样:把各通道的CCR值按顺序放在一个数组里,用一个定时器更新事件触发DMA突发传输,把数组依次写入4个CCR寄存器。配合主从同步,所有定时器的更新事件都对齐,DMA搬运完成的时刻也一致,等于所有通道在同一时刻换了占空比。
5.2 TIM1的互补输出与死区:MOS管发热的常见根因
TIM1之所以叫高级定时器,因为它有死区发生器、互补输出和刹车保护。三个互补通道CH1N/CH2N/CH3N可以直接输出反相PWM,不需要在外部搭反相器。CubeMX里的“Break and Dead Time settings”可以设置死区时间,单位是时钟周期,72MHz下1us对应72个计数周期。
很多人在H桥或者BUCK电路上遇到MOS管异常发热,第一反应是换更大电流的MOS管,但问题往往出在驱动时序上。我排查MOS管发热的顺序是这样:
先拿示波器夹在上下管栅极,观察两路PWM的上升沿和下降沿。如果同一桥臂两个管子存在同时导通窗口,说明死区不够或者根本没配。时间上,即使没有死区,信号在示波器上看起来也是“交替”的,但实际MOS管关断有延迟,上升沿还没完全到高,另一个管子已经收到导通指令,瞬间就直通了。
第二步看栅极波形斜率。如果上升沿超过几百纳秒,MOS管在放大区停留太久,开关损耗会非常大。这时候往往是驱动芯片电流不够,或者栅极串联电阻太大。F103的IO直接推大功率MOS管一般推不动,必须加专用的栅极驱动芯片。
第三步确认互补输出的极性。一个人容易踩的坑是CH和CHN反接,导致占空比越高,实际输出电压越低,电机飞车或者堵转。
TIM1的刹车输入(Break Input)和主输出使能(MOE)也要重点用起来。在CubeMX里可以配置刹车极性、刹车输入源、自动恢复方式。发生故障时,刹车信号可以立即把主输出置为安全状态(比如全部输出低电平),这在电机驱动里是保命功能。我见过不止一次因为省掉刹车保护,过流时软件来不及响应,MOS管直接冒烟。
5.3 PWM输入自检:输出同步之后,怎么确认自己没被坑
很多人做多路PWM输出时,只关心“发出去了”,不关心“发得对不对”。在工业设备里,PWM输出故障保护很关键。
F103的定时器除了输出PWM,还能输入捕获。比如TIM3_CH1配置为输入捕获模式,可以测量外部PWM信号的频率和占空比。你可以把一路PWM输出线并联回某个输入捕获引脚,做一个简单的自检回路:程序定期读捕获值,和预期占空比比较,偏差超过阈值就报警。
热词里提到的“pwm input on ch1”,指的就是TIMx_CH1作为输入捕获通道测量PWM。标准做法是用两个通道配合,一个测周期,一个测脉宽:
// TIM3_CH1和TIM3_CH2配置为PWM输入模式 // 通道1捕获上升沿,通道2捕获下降沿 // 随着边沿变化,定时器的IC1和IC2分别记录时间戳四个定时器在同步输出模式下,如果其中一个定时器的某一通道被拉去做输入捕获,不会影响其他通道的输出。但要注意引脚规划:输入捕获需要的是GPIO输入模式,而PWM输出需要复用推挽输出模式。同一个引脚不能同时做输出和输入,所以自检回路需要物理上额外连一根线。
我在轮速模拟项目里的做法是:四路轮速PWM里只取一路做自检,其他三路默认信任同步机制,因为主从同步一旦生效,各路之间的相位关系是固定的。测试结论也验证了这个设计:只要同步边沿对齐,其他通道的占空比和频率可以通过同一套PSC/ARR推导出来,不需要每路都接回读线。
6. 踩坑清单与排查手法:同步失败时怎么定位
6.1 从定时器频率不对:多半是把Trigger Mode选成了外部时钟模式1
我见过一个真实案例,有人用TIM2做master,TIM3做slave,Cubemx里Slave Mode选了External Clock Mode1,结果TIM3输出的PWM频率变成主定时器频率的几分之一。看起来好像是分频配置错了,实际是工作模式理解错了。
Trigger Mode和External Clock Mode1都在接收ITR信号,但本质完全不同。Trigger Mode下,ITR信号只是启动/复位计数器,从定时器仍然用内部时钟计数,PSC和ARR正常生效。External Clock Mode1下,ITR信号直接作为计数时钟,从定时器的PSC会先对ITR分频,ARR再决定一个周期要多少个ITR脉冲。如果主定时器PWM是1kHz,从定时器PSC=71,ARR=999,实际输出频率就会变成1kHz/72/1000,离谱地低。
排查方法:读一下TIMx_SMCR寄存器的SMS字段。SMS=110是Trigger Mode,SMS=111是External Clock Mode1。如果你只需要同时启动,就一定要用Trigger Mode。
6.2 启动瞬间多出半个脉冲:TRGO选择的时机不对
另一个坑在MMS配置。CubeMX里TIM2的“Trigger Output (TRGO)”如果选了Update Event,你会发现从定时器总比主定时器慢一个周期。
原因是Update Event要等计数器溢出才产生。TIM2启动后先跑完999到1000的计数周期,才产生第一个更新事件,从定时器收到这个事件后才启动。于是从定时器从第二周期才开始有完整波形,第一周期里它的状态是未启动的,对应到输出上就是少了一个脉冲,或者第一个PWM波形宽度不对。
正确做法是MMS选Enable。这样TIM2的CEN被置1瞬间,TRGO立刻发出启动事件,从定时器和主定时器在同一个计数起点开始跑。两个配置在波形上的差异非常明显,只要用逻辑分析仪抓第一个周期就能分辨。
6.3 GPIO复用配了但没输出:AFR、AFIO和调试口三座大山
如果寄存器里定时器计数正常,但引脚上就是没有波形,按下面顺序排查:
第一,确认GPIO模式。PWM输出要配置成复用推挽(AF_PP),不是普通推挽输出。CubeMX自动生成的工程一般不会错,但自己手写寄存器初始化时很容易漏。
第二,检查AFR重映射寄存器。F103的GPIO复用功能映射表不直观,比如PA8要复用为TIM1_CH1,AFR寄存器里对应位要写正确。CubeMX生成代码后,HAL_GPIO_Init内部已经处理好了。如果你改引脚,记得重新生成代码。
第三,如果PWM引脚涉及PA15/PB3/PB4这类调试口,而你没有在CubeMX的SYS里把Debug模式改成Serial Wire,JTAG功能还会占用这些引脚。此时GPIO配置写了也没用,引脚电平被调试电路拉住。解决方法是SYS->Debug选择Serial Wire。
第四,查一下引脚是不是被其他外设抢占。最简单的方法是读GPIO配置寄存器,或者看CubeMX的引脚规划图。如果某个引脚在CubeMX里同时被多个外设标成绿色/黄色,那就要人工取舍。
6.4 同步生效了但仍有微秒级错位:从预装载和更新时机找原因
有一种情况比较隐蔽:波形整体上看起来同步了,但每次动态改占空比之后,个别通道的边沿会错开几个微秒,然后等下个周期才恢复。
这种问题通常出在CCR预装载配置不一致。如果一个通道开了预装载,另一个通道没开,那么同一个软件写CCR的操作,在每个通道上的生效时机就不同。没开预装载的通道立即生效,可能导致当前周期被截断;开了预装载的通道要等更新事件才生效。两者对外的表现就是占空比切换瞬间,边沿位置出现短暂偏差。
解决办法很简单:所有PWM输出通道统一开启预装载,并且在修改CCR时尽量把同一PWM组的所有通道一起写完,再等一个更新事件。这样所有通道的边沿变化发生在同一个周期起点。
把TIM1/TIM2/TIM3/TIM4四个定时器的更新事件对齐也很重要。原理上,在Trigger模式下,从定时器收到TRGO后与主定时器同时启动,时钟相同、ARR相同,所以自然会在同一时刻更新。但如果某个定时器的PSC或ARR值被改过,就会失去同步。初始化之后不要随意单独改某个从定时器的ARR,要改就统一改。
6.5 刹车和死区相关的坑:一上电就发热或飞车
最后分享一个和TIM1相关的血泪教训。用TIM1互补输出驱动H桥时,代码里先配置了GPIO复用,但还没调用HAL_TIMEx_PWMN_Start,MOS管驱动芯片就上电了。此时GPIO引脚可能是高电平,导致上管误导通,电机瞬间飞车。
解决方法是:初始化掉电阶段,把PWM引脚先配置为普通GPIO输出低电平,等所有定时器配置完毕、死区设置完毕,再通过函数把引脚切换到复用模式。如果硬件上支持,也可以把驱动芯片的使能脚拉低,软件初始化完成后再把使能脚释放。
还有一种情况是刹车输入极性配反了。如果在CubeMX里把刹车输入极性配成高电平有效,但你的保护信号是低电平有效,那刹车功能会在正常工作时一直触发,输出被锁死,看起来像PWM完全没反应。把示波器探头点在TIM1的BKIN引脚上看电平就清楚了。
最后强调一点:当你看到电机飞车或MOS管发热时,不要急着怀疑PWM代码,先断开功率部分,用逻辑分析仪单独验证PWM波形。波形没问题再接功率,接功率后用限流电源供电,电流异常能及时触发保护,而不是直接炸管。
调试多路PWM同步输出这件事,说难不难,说简单也不简单,核心就是把定时器的主从触发机制吃透。我个人的习惯是,凡是产品上用到多路PWM,一定先在逻辑分析仪里把同步边沿的截图存档,后面调软件问题时随时能翻出来对比,排查速度快很多。这个习惯在我做电机、舵机和轮速模拟项目时帮了很大的忙,建议你也试试。