ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32 TIM1互补PWM与死区配置:全桥逆变实战详解

STM32 TIM1互补PWM与死区配置:全桥逆变实战详解 去年帮客户调一块全桥逆变控制板核心工作就是用STM32F103C8T6在HAL库环境下生成两路带死区的互补PWM送给驱动芯片去推全桥MOS管。折腾了一个多星期炸过管子、烧过采样电阻最后也把波形调到了能稳定跑的状态。这篇就围绕这个项目把PWM生成、TIM1高级定时器配置、死区计算、HAL库代码框架、以及调试时真正踩过的坑从头到尾梳理一遍给正在做逆变、UPS、电机驱动这类需要互补PWM的同学一个可以照着抄的参考。内容会尽量落在实操层面涉及参数计算和代码的地方都会交代清楚原因不会只贴一段配置就完事。1. 全桥逆变为什么离不开一路好PWM1.1 先看懂全桥电路在干什么全桥逆变电路的基本结构是四个功率开关管组成H形桥臂负载接在两个桥臂中点之间。对逆变来说目标是把直流母线电压转换成交流输出。最常见的方式就是让对角的两只管子轮流导通Q1、Q4同时导通时电流正向流过负载Q2、Q3同时导通时电流反向流过负载两组管子交替工作负载上就得到交流波形。这里有个很关键的认知要纠正很多人一上来就想着“用单片机输出50Hz方波直接接MOS管”这在全桥拓扑里基本行不通。因为负载通常是变压器或LC滤波网络方波里的高次谐波不仅会让铁芯发热、噪音变大开关瞬间的电流尖峰还会直接影响管子寿命。所以实际工程里都是让PWM工作在比较高的开关频率比如20kHz左右用高频斩波配合后级滤波来还原出低频正弦波功率管只工作在开关状态损耗小输出波形也更干净。全桥电路里PWM的核心任务有三个频率和占空比要精确可调、上下桥臂信号要严格互补、切换过程中必须插入死区。这三个任务如果全用软件去拼不仅占用CPU而且时序精度很难保证。这也是我选STM32F103C8T6的主要原因它带的高级定时器TIM1天生就是干这个的。1.2 互补PWM与死区保住桥臂的两条命先解释一下什么叫互补PWM。全桥的四个管子分为两组Q1/Q4为一组Q2/Q3为另一组。同一时刻只能有一组导通另一组必须完全关断。那么单片机输出的控制信号自然就是两路相位相反的波形一路高电平的时候另一路低电平这就是互补。问题出在功率管不是理想开关。MOS管有开通延迟、关断延迟关断延迟通常比开通延迟长。如果同一桥臂的上管还没完全关断下管就开始导通了母线电压就直接通过两个管子短路瞬间大电流会把管子烧穿。这种现象叫桥臂直通是逆变器炸管最常见的原因。解决办法就是插入死区在一组管子关断后、另一组管子开通前人为插入一小段“两边都关断”的时间。这段间隙给前一个管子充分的关断时间确保不会出现重疊导通。如果完全靠外部模拟电路或者CPU中断来做死区时序一致性很成问题。TIM1的方案是硬件级解决你只需要配置好死区时间芯片会在输出引脚上自动完成“CH1高电平”和“CH1N高电平”之间的错位CPU完全不用干预。这正是高级定时器相对普通定时器的核心价值所以做全桥逆变首选TIM1而不是TIM2、TIM3这一点务必记住。2. 用STM32F103C8T6做逆变控制资源够不够2.1 F103C8T6的资源盘点STM32F103C8T6可能是一块被用烂了的芯片但配置并不过时。它主频72MHzFlash 64KBSRAM 20KB有三个定时器一个高级定时器TIM1、两个通用定时器TIM2/TIM3。单做PWM生成和逆变控制逻辑这些资源完全够用。很多人看到64KB Flash会觉得小实际逆变项目里控制代码加状态机加保护逻辑也就十几个KBHAL库本身就占掉一部分Flash但剩余空间依然充足。真正的关键是TIM1它提供三个互补输出通道CH1/CH1N、CH2/CH2N、CH3/CH3N带死区生成器、带刹车输入BKIN还能在硬件层面响应故障信号立即封锁输出。我现在做这类板子还会特意注意国产替代型号。市面上很多丝印为“STM32F103C8T6”的芯片实际是国产兼容方案或者国内厂商的引脚兼容型号寄存器、HAL库兼容性做得都不错Flash容量甚至更大。但批量上产线前一定要实测尤其是TIM1这种高级定时器的死区和刹车功能不同批次或不同厂家芯片会有细微差异踩过坑才知道。2.2 HAL库还是LL库别做无谓纠结网上关于HAL和LL库吵得很凶。标准库过时了HAL库代码啰嗦、效率低LL库轻量、性能好。这些说法都对但放在这个项目里HAL库完全不是瓶颈。PWM生成的工作频率是20kHz虽然听起来不低但对72MHz主频来说每个PWM周期有3600个时钟周期可用。HAL库函数调用的那点开销换算成时间也就几个微秒级别对死区插入、互补输出这些完全不影响因为关键时序是芯片硬件完成的HAL库只是负责写寄存器。所以我的建议很简单用CubeMX图形化配置加HAL库开发重点关注中断回调里不要做耗时的阻塞操作。如果后面要做多路高频电流环控制中断里需要极短执行时间时再考虑把关键路径改成LL库或者直接操作寄存器。项目初期就被代码效率绑架会严重拖慢调试进度得不偿失。3. CubeMX里5分钟配置出TIM1互补PWM3.1 时钟树与PWM频率的账先算清楚配置之前需要先把TIM1的时钟源搞明白否则频率一定算错。F103的TIM1挂在APB2总线上而APB2定时器时钟需要看AHB预分频。常见开发板时钟配置是HSE 8MHz经过PLL倍频到72MHzAPB1分频为36MHzAPB2不分频保持72MHz此时TIM1的时钟就是72MHz。我这次设计的目标开关频率是20kHz并且选用中心对齐模式原因后面讲。中心对齐模式下计数器先从0向上计数到ARR再向下计数到0一个完整的PWM周期需要经历一次向上和一次向下所以频率公式是f TIM时钟 / ((PSC 1) × (ARR 1) × 2)取PSC 35就是36分频计数频率为72MHz / 36 2MHz再取ARR 49也就是每50个计数脉冲翻转一次方向最终频率是2MHz / (50 × 2) 20kHz。如果用边沿对齐模式公式就不需要乘以2这是新手最容易算错的地方。占空比50%时CCR应该设为ARR的一半也就是25。中心对齐模式下CCR 25表示计数器不论向上还是向下都在计数值达到25时翻转输出电平高电平时间正好占整个周期的一半。选择中心对齐而不是边沿对齐主要考虑到逆变器后级滤波和EMI。中心对齐模式下PWM脉冲两侧对称输出谐波能量集中在开关频率的奇数倍频频谱更干净LC滤波器设计起来更顺手。代价只是中断触发频率翻倍但这个项目的CPU负载完全可以承受。3.2 关键配置项逐个过一遍在CubeMX里打开TIM1的配置页面几个关键位置按下面的参数填Clock Source选择Internal Clock。Channel1选择PWM Generation CH1 CH1N这样会自动把PA8和PB13对应到两个互补输出引脚。如果只需要两路互补信号CH2和CH3可以全部Disable。Prescaler填35Counter Mode选Center Aligned mode 1或mode 2都可以我习惯用Center Aligned mode 1即计数器向下计数时输出比较标志才置位。Counter Period填49。Pulse填25这就是初始占空比对应的比较值。AutoReload Preload选择Enable。这个选项会把ARR和CCR的更新变成“下一个周期生效”避免运行中调整参数时波形出现毛刺。如果配置成Disable直接在寄存器写入更新一次改到一半时输出会发生撕裂后面代码部分会详细说。死区配置在TIM1的Break and Dead Time setting页面。Dead Time一栏直接填1000单位是纳秒也就是1us。这个数值不是随便拍的要结合驱动芯片和MOS管的关断延迟来算。以常见的IRF3205这一类MOS管为例典型关断延迟在60到100ns左右驱动芯片比如IR2104的传播延迟加上去整个关断路径的总延迟大概在200到250ns。死区时间应该留足余量一般取关断延迟的3到5倍1us是一个很稳妥的起点。死区太大输出波形会失真谐波增加变压器容易发热死区太小直通风险挡不住。留1us既安全又不至于对THD造成明显影响。Break功能建议直接打开Break State选EnableBreak Polarity选HighAutomatic Output选Disable。这样当BKIN引脚收到高电平故障信号时TIM1的PWM输出会被硬件立即锁死所有输出引脚进入安全电平完全不需要CPU介入。Automatic Output如果选Enable故障信号消失后PWM会自动恢复输出这在某些应用里很方便但对逆变器来说很危险故障没有确认消除就重新输出搞不好二次炸管所以务必保持Disable。NVIC设置里把TIM1 Update中断和Break中断使能打开后面软件状态机要用。生成代码前检查一下PA8、PB13有没有被其他外设占用F103C8T6引脚不多很容易在配置其他功能时冲突。4. HAL库代码实现初始化、启动与状态机4.1 定时器初始化和死区参数CubeMX生成的初始化代码会放在MX_TIM1_Init函数里结构大致如下static void MX_TIM1_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; TIM_OC_InitTypeDef sConfigOC {0}; TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; htim1.Instance TIM1; htim1.Init.Prescaler 35; htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period 49; htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim1); // 注意不同HAL库版本HAL_TIM_PWM_Init的签名有差异 // 老版本可能需要传入sClockSourceConfig参数 sClockSourceConfig.ClockSource TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(htim1, sClockSourceConfig); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 25; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); sBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime 71; sBreakDeadTimeConfig.BreakState TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.BreakFilter 0; sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_DISABLE; HAL_TIMEx_ConfigBreakDeadTime(htim1, sBreakDeadTimeConfig); HAL_TIM_MasterConfigStep(htim1, sMasterConfig); }代码里有几个点要说清楚。DeadTime填71是因为死区寄存器DTG的步进是DTS时钟周期DTS这里等于72MHz1us大约需要72个tick所以寄存器值在71到72之间。CubeMX图形界面直接填1000纳秒就行生成代码时会自动换算成寄存器值但如果你手写寄存器或者从别人的工程里找代码看到DeadTime 71就要能反应过来这是72MHz时钟下配置1us死区的值。OCIdleState和OCNIdleState这两项要注意它代表定时器进入空闲状态时输出脚的电平默认RESET就是低电平。对逆变器来说低电平意味着管子关断这是安全的。如果手滑改成SET主输出关闭后管子反而导通在接功率板时直接炸管。4.2 启动互补输出别漏了CH1N配置完初始化很多新手会在main函数里只调用HAL_TIM_PWM_Start就开门跑了然后发现PWM只有一路输出另一路始终没有信号。这是最典型的坑。高级定时器的主输出通道和互补输出通道是分开控制的。启动PWM必须同时调两个函数HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1);第一个函数使能CH1主输出同时打开主输出使能位MOE第二个函数专门使能互补输出CH1N。两者缺一不可只调第一个PA8有波形PB13一直低电平只调第二个反过来。而且MOE位没有被置位时所有输出引脚都是安全电平看起来就像定时器完全没工作。调试PWM时我习惯先把两根示波器探头分别夹在PA8和PB13上确认两路信号都出来后再接到驱动板。这样可以快速区分是MCU配置问题还是后面驱动电路的问题。如果你发现CH1正常、CH1N没有信号首先检查是不是没调HAL_TIMEx_PWMN_Start其次检查CubeMX里是不是只勾了PWM Generation CH1而没勾CH1N。运行中改占空比的API是__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, ccr);因为初始化时开了AutoReload Preload这个比较值会在下一个PWM周期开始时统一生效不会有运行中被撕裂的波形。如果追求极致的快速响应可以关掉预装载但前提是自己清楚后果在逆变这种应用里没有必要。4.3 软启动与故障保护的框架硬件层面的PWM输出就绪后软件还应该有一套完整的状态机。别小看这一步很多项目做到“PWM能输出了”就觉得完工了结果上电直接炸管就是因为没有控制好启动过程。我在这类逆变项目里习惯用一个简单的状态机typedef enum { INVERTER_IDLE 0, INVERTER_SOFTSTART, INVERTER_RUNNING, INVERTER_FAULT } Inverter_State; typedef struct { Inverter_State state; uint16_t target_ccr; uint16_t current_ccr; uint8_t fault_flag; } Inverter_HandleTypeDef;上电后状态为IDLE此时所有PWM输出关闭母线电容通过限流电阻预充电。确认母线电压稳定后进入SOFTSTART状态占空比从0开始按固定步进逐渐增加而不是直接跳到50%。如果变压器或滤波电感直接承受50%占空比的阶跃冲击电流非常大轻则烧保险重则磁芯饱和炸管子。软启动的渐变可以在定时器更新中断里做每次中断增加一点CCR直到达到目标值void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { if (Inverter.state INVERTER_SOFTSTART) { Inverter.current_ccr 1; if (Inverter.current_ccr Inverter.target_ccr) { Inverter.current_ccr Inverter.target_ccr; Inverter.state INVERTER_RUNNING; } __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, Inverter.current_ccr); } } }故障保护依赖分配给TIM1的BKIN引脚。当外部保护电路检测到过流、过压等异常时拉高BKINTIM1的硬件会在几个纳秒内封锁所有输出通道同时产生刹车中断。这个中断里只需要记录故障状态不需要也不应该在这里做复杂的恢复逻辑void HAL_TIMEx_BreakRisingCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { Inverter.state INVERTER_FAULT; Inverter.fault_flag 1; // MOE位已被硬件自动清零PWM输出已经安全封锁 } }想要恢复输出必须确认故障源已经排除再重新使能主输出__HAL_TIM_MOE_ENABLE(htim1);注意我的措辞确认故障源已经排除。如果只是把BKIN拉低就自动恢复保护就失去意义了。AutomaticOutput保持Disable加上这套手动恢复逻辑才是逆变器保护的正确姿态。5. 调试实录从示波器上验证波形5.1 先测驱动信号再接功率级我把调试分为三步顺序千万不能乱先测MCU引脚波形再测驱动芯片输出的栅极波形最后才允许接上母线功率级。直接跳过前两步上强电等于赌运气赌输了就是一声响和一块废板。第一步PA8和PB13对地分别测量。确认两路20kHz方波、占空比约50%、波形互补、死区约1us。如果你用的是隔离示波器探头直接夹引脚没问题如果探头不隔离地线夹子只能夹在开发板的GND上别去碰驱动板的母线地否则会形成地环路。第二步把信号接到驱动芯片输入端用示波器两个通道同时看驱动芯片的HO和LO输出。这里能看到的是经过驱动芯片后的波形注意观察上升沿和下降沿是否有明显振铃、平顶是否正常。MOS管栅极驱动电流不足时边沿会变得很缓导致开关损耗变大。第三步才接功率板而且母线电压先从低压开始。手头有调压器就把输入从12V慢慢往上加没有调压器就把负载和限流措施准备好。我这个项目最后稳定工作在直流母线400V条件下输出220V交流但前面大部分调试都是24V母线完成的。低压下把逻辑和保护调通再升高压只做参数微调安全压力小很多。MOS管散热片和母线端子在通电状态下绝对不能手摸这是基本底线。5.2 死区波形到底该怎么读把示波器两个通道分别夹在同一桥臂上下管的栅极调到合适的时间档位你能清楚看到两路方波在交叠处有一段同时为低电平的状态这段“双低”时间就是死区。使用示波器的光标功能可以直接量出死区宽度。正常情况应该稳定在设计值附近比如1us。如果实测死区明显小于设定值首先怀疑配置里DTS时钟设置错了如果死区大到3us以上但配置看起来正常检查是不是ClockDivision配置成了DIV2或DIV4它会额外增大死区步进。还有一种情况看到两路波形在切换瞬间有毛刺或多次翻转。这通常不是死区问题而是示波器探头地线过长引入的振铃换短地线弹簧或改用差分探头即可。如果毛刺出现在所有边沿且幅度一致才考虑是不是驱动电路走线过长引起的振铃那需要在栅极电阻和走线布局上做文章。调试时还要留意中心对齐模式的特殊外形PWM高电平脉冲的宽度随着调制波变化但脉冲在周期内是对称分布的而不是像边沿对齐那样在周期头或尾堆积。如果你看到的波形完全不对称多半是CounterMode配置成了Edge Aligned没有切成Center Aligned。5.3 遇到过最坑的四个问题第一个最典型启动后只有主输出有波形互补输出始终低电平。原因前面提过HAL_TIMEx_PWMN_Start漏调用或者CubeMX里根本没勾CH1N。排查优先级最高不改代码前先确认这两个点。第二个是上电瞬间管子发烫伴随“吱吱”的异响。这是死区不足的典型症状特别是软启动还没做、直接满占空比跑起来的时候更明显。用示波器量实际死区如果只有一两百纳秒果断加死区从1us起步再观察温升。某些情况下驱动芯片固有延迟比较大死区1us仍然不够就继续往上加直到双低时间稳定且管子不异常发热为止。第三个是频率偏差。打算配置20kHz实测只有1kHz或者差了一半。频率差一半基本都是中心对齐模式公式没算对把除以2丢了频率差很多基本都是PSC或ARR填错位或APB2预分频配置导致TIM1不是72MHz。用公式反推一遍马上能找到问题。第四个是我个人最心痛的一次教训断点调试时炸管。程序跑到软启动的中断里我打了断点然后停在断点处几十秒。期间PWM输出被调试器暂停输出电平停在半高状态MOS管处于线性区大电流持续通过管子直接过热击穿。从那以后我调试功率电路时不用断点暂停需要看变量就在中断里通过串口打印或者干脆用SWO输出彻底避免程序长期停留在线性状态。再补一个接线问题STM32F103C8T6最小系统板的PA8在一些老款板上被复用为LED或其他功能PB13附近也常有其他排针。如果发现CubeMX配置正确、代码正确引脚却没有波形先看板子原理图确认这两个引脚是不是被板载外设拉低或短接了。仿真器连不上芯片时把BOOT0拉高进ISP模式擦除后再拉回基本都能救回来。6. 一点扩展思路这套TIM1互补PWM框架稍微改改就是其他功率电路的雏形。全桥逆变只是其中一个方向你完全可以基于同样的初始化思路做双向全桥DC-DC、单相UPS、无刷电机六步换相、甚至Half-bridge驱动。进一步升级的方向是SPWM正弦调制。目前这套代码输出的是固定占空比方波只能出方波或者简单的“准正弦”效果。想要输出高质量正弦波需要让CCR在每个载波周期按正弦规律变化。常用做法是建立一个正弦查找表在定时器更新中断里查表更新CCR或者用DMA把正弦表的数值按PWM周期自动搬运到CCR寄存器。中心对齐模式配合更新中断正好适合做对称规则采样得到的SPWM谐波含量更低。还有更进阶的玩法用TIM1的重复计数器RCR控制输出指定数量的PWM脉冲后自动停止这在变压器退磁、软启动限流等场景很实用。配合ADC注入通道在PWM中心点触发采样可以精确采集电流值把电流环闭环控制做起来。这些都在本项目的代码框架上扩展不需要推翻重来。调试这个项目给我最大的体会是模板代码只能保证输出波形真正决定板子能不能安全可靠工作的是死区配置、软启动流程、故障恢复逻辑这些细节。PWM波形一出来先用示波器量足再进下一步每一步都按部就班来全桥逆变并没有想象中那么神秘。希望这篇能把你的调试弯路缩短一些。
返回列表