☰
STM32 TIM1互补PWM与死区配置:全桥逆变实战详解
2026/10/4 1:23:24 网站建设 项目流程

去年帮客户调一块全桥逆变控制板,核心工作就是用STM32F103C8T6在HAL库环境下生成两路带死区的互补PWM,送给驱动芯片去推全桥MOS管。折腾了一个多星期,炸过管子、烧过采样电阻,最后也把波形调到了能稳定跑的状态。这篇就围绕这个项目把PWM生成、TIM1高级定时器配置、死区计算、HAL库代码框架、以及调试时真正踩过的坑从头到尾梳理一遍,给正在做逆变、UPS、电机驱动这类需要互补PWM的同学一个可以照着抄的参考。内容会尽量落在实操层面,涉及参数计算和代码的地方都会交代清楚原因,不会只贴一段配置就完事。

1. 全桥逆变为什么离不开一路好PWM

1.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可能是一块被用烂了的芯片,但配置并不过时。它主频72MHz,Flash 64KB,SRAM 20KB,有三个定时器:一个高级定时器TIM1、两个通用定时器TIM2/TIM3。单做PWM生成和逆变控制逻辑,这些资源完全够用。

很多人看到64KB Flash会觉得小,实际逆变项目里,控制代码加状态机加保护逻辑也就十几个KB,HAL库本身就占掉一部分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互补PWM

3.1 时钟树与PWM频率的账先算清楚

配置之前需要先把TIM1的时钟源搞明白,否则频率一定算错。F103的TIM1挂在APB2总线上,而APB2定时器时钟需要看AHB预分频。常见开发板时钟配置是HSE 8MHz经过PLL倍频到72MHz,APB1分频为36MHz,APB2不分频保持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填35,Counter 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选Enable,Break Polarity选High,Automatic 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这里等于72MHz,1us大约需要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引脚。当外部保护电路检测到过流、过压等异常时,拉高BKIN,TIM1的硬件会在几个纳秒内封锁所有输出通道,同时产生刹车中断。这个中断里只需要记录故障状态,不需要也不应该在这里做复杂的恢复逻辑:

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波形一出来先用示波器量足再进下一步,每一步都按部就班来,全桥逆变并没有想象中那么神秘。希望这篇能把你的调试弯路缩短一些。

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

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

立即咨询