尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F103C8T6多通道PWM同步输出:定时器主从配置与排坑实战

STM32F103C8T6多通道PWM同步输出:定时器主从配置与排坑实战 用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封装常见冲突TIM1CH1~CH4另有CH1N/CH2N/CH3N互补输出APB2CH1PA8, CH2PA9, CH3PA10, CH4PA11CH1NPB13, CH2NPB14, CH3NPB15PA9/PA10与USART1冲突PA11/PA12与USB冲突TIM2CH1~CH4APB1CH1PA0, CH2PA1, CH3PA2, CH4PA3PA0/PA1常接ADCPA2/PA3常接USART2TIM3CH1~CH4APB1CH1PA6, CH2PA7, CH3PB0, CH4PB1PA6/PA7是SPI1的MISO/MOSITIM4CH1~CH4APB1CH1PB6, CH2PB7, CH3PB8, CH4PB9PB6/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~4PA0~PA3TIM2_CH1~CH4不接ADC避免冲突PWM 5~8PA6, PA7, PB0, PB1TIM3_CH1~CH4SPI1不用时可选PWM 9~12PB6~PB9TIM4_CH1~CH4避开I2C1PWM 13~16PA8, PA9, PA10, PA11TIM1_CH1~CH4PA11不接USBPA9/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再初始化TIM3PWM_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的定时器之间有一条内部通道叫TRGOTrigger Output触发输出。每个定时器可以通过主模式选择把某些事件广播到TRGO上其他定时器作为从定时器通过内部触发输入ITR0到ITR3接收这个信号。这条路径完全在芯片内部不走GPIO也不需要外部连线。F103C8T6上四个PWM定时器的内部触发连接是这样从定时器ITR0来源ITR1来源ITR2来源ITR3来源TIM1TIM2TIM3TIM4TIM5C8T6无效TIM2TIM1TIM3TIM4TIM5C8T6无效TIM3TIM1TIM2TIM4TIM5C8T6无效TIM4TIM1TIM2TIM3TIM5C8T6无效这张表是芯片设计时固定死的不是软件能随意改的。我经常看到有人问“能不能让TIM2同时触发TIM1和TIM3”答案是可以TIM2的TRGO确实连到了TIM1的ITR0、TIM3的ITR1、TIM4的ITR1理论上可以同时作为三个从定时器的触发源。但如果你想做一个定时器触发所有其他定时器的级联就得看具体连接关系。实际操作中选TIM2做common master的配置方式最自然因为TIM1/TIM3/TIM4都能直接收TIM2的信号代码和Cubemx配置都很直观。3.2 主模式MMSTRGO到底能输出什么东西TRGO不是一根固定的信号线它输出什么内容由主模式选择位TIMx_CR2的MMS字段决定。常见选项有MMS值TRGO输出内容适合场景000复位事件UG用软件置UG触发从定时器复位001使能信号EN主定时器计数器开始计数时TRGO立即输出一个事件010更新事件UIF每次计数器溢出时输出事件011CC1比较脉冲捕获/比较事件作为触发100OC1REF把PWM通道1的原始参考信号直接作为触发时钟在同步输出场景里最常用的是MMS001Enable和MMS100OC1REF。MMSEnable有个很关键的优势主定时器的CEN位一旦被置1TRGO立即产生一个事件从定时器收到后同步启动整个过程只有芯片内部逻辑延迟大概几十纳秒。这意味着从定时器和主定时器在同一时刻开始计数之后如果PSC和ARR配置完全相同输出的PWM边沿就能始终对齐。MMSOC1REF则是把主定时器某个通道的PWM原始波形直接当“时钟总线”传给从定时器它能做到边沿级对齐但用法更复杂在后面4.3会细说。3.3 从模式Trigger模式和External Clock Mode1的本质区别F103的从工作模式Slave Mode里同步输出最常用的是这两种触发模式SMS110从定时器收到来自ITR的触发事件后清零并启动计数器。它的计数时钟仍然是内部的72MHz时钟PSC和ARR自己独立配置。这种模式适合“大家同时开始各自按自己的PSC/ARR跑”的场景。因为从定时器收到触发时会自动复位计数器所以所有计数器的起点也被强制对齐了。外部时钟模式1SMS111ITR输入信号直接作为从定时器的计数时钟。每个触发上升沿计数器加一。也就是说从定时器的计数节拍完全由主定时器的某个事件比如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%频率1kHz72MHz主频。F103C8T6的定时器时钟分布要注意TIM1在APB2总线上TIM2/TIM3/TIM4在APB1总线上。CubeMX里如果HCLK设为72MHzAPB1预分频器要设为2这样PCLK1是36MHz但定时器时钟会自动变成72MHz。检查时钟树时确认APB1和APB2下的Timer Clocks都显示72MHz否则后面所有PSC/ARR计算全白搭。Cubemx里这样配配置项TIM2主定时器TIM3从定时器TIM4从定时器TIM1从定时器Clock SourceInternal ClockInternal ClockInternal ClockInternal ClockChannel1~4PWM Generation CH1~4PWM Generation CH1~4PWM Generation CH1~4PWM Generation CH1~4Prescaler71717171Counter Period999999999999Auto Reload PreloadEnableEnableEnableEnablePulse各通道按目标占空比按目标占空比按目标占空比按目标占空比Slave Mode无Trigger ModeTrigger ModeTrigger ModeTrigger Source无ITR1ITR1ITR0关于从定时器的Trigger Source查前面那张内部连接表TIM3的ITR1是TIM2TIM4的ITR1是TIM2TIM1的ITR0是TIM2。所以TIM3和TIM4选ITR1TIM1选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 PWMDMA高路数占空比刷新不占用CPU定时器主从同步解决的是“相位对齐”问题但很多实际场景还有一个痛点通道太多CPU来不及逐个更新CCR。比如16路PWM要输出动态呼吸灯效果或者WS2812时序如果每个通道都在中断里更新主循环基本就卡死了。PWMDMA就是解决这个问题的。原理简单粗暴把要输出的CCR值按顺序排成数组用DMA自动搬运到定时器的CCR寄存器CPU只在传输完成时收到一次中断甚至完全不用中断。用PA8的TIM1_CH1驱动WS2812就是一个很典型的例子。WS2812对时序要求严格一个码元大约1.25us发送0和1对应的高电平宽度不一样。如果用CPU一条一条翻引脚根本忙不过来。正确做法是把整个数据帧转换成一串CCR值再让DMA在PWM更新事件触发下按顺序把CCR值写入TIM1-CCR1。硬件配置要点// 假设72MHz主频ARR89PWM周期为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做masterTIM3做slaveCubemx里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从定时器PSC71ARR999实际输出频率就会变成1kHz/72/1000离谱地低。排查方法读一下TIMx_SMCR寄存器的SMS字段。SMS110是Trigger ModeSMS111是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_CH1AFR寄存器里对应位要写正确。CubeMX生成代码后HAL_GPIO_Init内部已经处理好了。如果你改引脚记得重新生成代码。第三如果PWM引脚涉及PA15/PB3/PB4这类调试口而你没有在CubeMX的SYS里把Debug模式改成Serial WireJTAG功能还会占用这些引脚。此时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_StartMOS管驱动芯片就上电了。此时GPIO引脚可能是高电平导致上管误导通电机瞬间飞车。解决方法是初始化掉电阶段把PWM引脚先配置为普通GPIO输出低电平等所有定时器配置完毕、死区设置完毕再通过函数把引脚切换到复用模式。如果硬件上支持也可以把驱动芯片的使能脚拉低软件初始化完成后再把使能脚释放。还有一种情况是刹车输入极性配反了。如果在CubeMX里把刹车输入极性配成高电平有效但你的保护信号是低电平有效那刹车功能会在正常工作时一直触发输出被锁死看起来像PWM完全没反应。把示波器探头点在TIM1的BKIN引脚上看电平就清楚了。最后强调一点当你看到电机飞车或MOS管发热时不要急着怀疑PWM代码先断开功率部分用逻辑分析仪单独验证PWM波形。波形没问题再接功率接功率后用限流电源供电电流异常能及时触发保护而不是直接炸管。调试多路PWM同步输出这件事说难不难说简单也不简单核心就是把定时器的主从触发机制吃透。我个人的习惯是凡是产品上用到多路PWM一定先在逻辑分析仪里把同步边沿的截图存档后面调软件问题时随时能翻出来对比排查速度快很多。这个习惯在我做电机、舵机和轮速模拟项目时帮了很大的忙建议你也试试。
返回列表