news 2026/9/10 4:25:52

STM32F103RC全桥驱动死区PWM配置与补偿解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103RC全桥驱动死区PWM配置与补偿解析

简介:面向STM32全桥驱动与电机控制场景的PWM死区实验资源,使用Keil开发环境基于STM32F103RC寄存器方式实现四路带死区PWM输出,适合嵌入式初学者及需要理解死区配置、TIM定时器寄存器操作的开发者。压缩包共60个文件,以h头文件、c源文件、o目标文件、crf编译列表等Keil工程文件为主,同时包含uvproj工程、hex烧录文件、map映射文件、README说明及TXT文档,可清晰看出工程结构并直接编译、烧录验证。已有574人学习下载。内容覆盖TIM模块初始化、PWM通道极性/占空比配置、死区时间寄存器设置以及运行中动态调整占空比等关键环节,并在描述中给出前死区与后死区的安全间隔思路,便于对照全桥上下管直通问题做实物调试。对于正在学习STM32定时器输出比较功能、计划用PWM驱动全桥电路的读者,这套寄存器版代码和说明文档能提供直观的配置参考与实操起点。

1. 死区不是软件延时:STM32F103RC全桥驱动为什么绕不开它

调试全桥驱动的第一个月,最容易犯的错误是把死区理解为“两个PWM之间错开一点时间”。实际上,死区是互补输出信号在电平切换时特意插入的一段“上下管全部关闭”的空白区,它必须由硬件在每周期、每次翻转时自动插入,紧贴PWM发生器本身,而不是靠主循环里的delay或定时器中断。STM32F103RC的高级定时器TIM1和TIM8内部专门设计了死区发生器,配置BDTR寄存器的DTG位段即可输出带死区的互补四路PWM,直接驱动全桥上下臂。没配过的人通常先量波形再找问题,配过的人才知道真正要算的是死区时间与占空比之间的约束关系。这篇文章顺着“为什么有死区——死区怎么算——Keil里怎么写——波形怎么验——死区失真怎么补偿”这条线,把全桥驱动带死区PWM的坑一个个填平。

2. 高级定时器死区发生器:DTG编码公式与四路PWM通道映射

全桥驱动要的“带死区四路PWM”,在STM32F103RC上不是软件模拟的组合,而是TIM1/TIM8硬件单元直接生成的。这一章把死区发生器的结构、DTG编码怎么换算死区时间、四路PWM怎么分配到全桥讲透,后面写代码就是照着这个模型填寄存器。

2.1 TIM1/TIM8区别于普通定时器的三件事

STM32F103RC的定时器资源里,TIM2/TIM3/TIM4只能输出单端PWM,TIM1和TIM8是高级控制定时器,多了三样东西:互补输出(CHx与CHxN)、死区插入、刹车输入(BKIN)。全桥四个功率管需要两对互补信号,每路PWM还要能独立调占空比,恰好就是TIM1两个通道干的事。

死区插入在硬件上位于输出比较单元之后、输出引脚之前,工作链路是OCxREF信号。每次OCxREF发生电平翻转,死区发生器先保持CHx和CHxN均为无效电平,经过死区时间后再把目标通道置为有效电平。因为是纯硬件等待,死区时间不受中断延迟和指令周期影响,这在PWM频率提到20kHz级别时尤其关键——中断里抖动几个周期,制动区就可能被压缩掉,上下管在过零点直接重叠。

刹车输入是另一个容易被忽略的功能。BKIN引脚拉低可以让所有PWM输出立即进入安全状态(高阻或强制无效电平),不经过CPU。全桥做过流保护时,硬件比较器输出直接接BKIN,比在中断里关PWM快得多,这也是PWM故障保护的标准做法。

2.2 死区时间的4种计算公式与DTG参数表

死区时间由BDTR寄存器的DTG[7:0]控制,参考手册RM0008把DTG编码分成四段,每一段的步进不同。设tDTS为死区时间基准,当TIM1的时钟来自APB2且CR1的CKD[1:0]=00时,tDTS等于定时器输入时钟周期,72MHz下就是13.89ns。

DTG[7:0]范围死区时间公式DTG=0x50(80)示例
0x00~0x7FDT = DTG × tDTS80 × 13.89ns ≈ 1.11us
0x80~0xBFDT = (64 + (DTG & 0x3F)) × tDTS × 2(64+16) × 13.89 × 2 ≈ 2.22us
0xC0~0xDFDT = (32 + (DTG & 0x1F)) × tDTS × 8(32+16) × 13.89 × 8 ≈ 5.37us
0xE0~0xFFDT = (32 + (DTG & 0x1F)) × tDTS × 16(32+16) × 13.89 × 16 ≈ 10.7us

注意每段公式取的低位位数不同,0x80以后不是直接拿DTG全值乘系数,而是先取低6位或低5位再加偏移。Keil的标准外设库里TIM_BDTRInitTypeDef.TIM_DeadTime直接对应DTG[7:0],所以给值之前必须对照这个表,不然很容易出现“死区设了却比预期大好几倍”的怪现象。

tDTS也可以被CR1的CKD位放大。CKD=01时tDTS=2×tCK_INT,等于把死区最小步进翻倍但牺牲了精度。电机控制里死区一般取500ns~2us,直接保持CKD_DIV1、让tDTS=13.89ns,用第一段公式就能覆盖0~1.76us,大多数情况够用。

2.3 四路PWM怎么映射到全桥四个功率管

TIM1默认映射下,CH1在PA8、CH1N在PB13、CH2在PA9、CH2N在PB14。全桥左半桥用CH1/CH1N,右半桥用CH2/CH2N,刚好四路信号。CH1接左桥上管驱动输入,CH1N接左桥下管驱动输入,CH2/CH2N接右桥,驱动芯片的使能脚和故障输出另行处理。

如果做三相无刷电机(BLDC),还会用到CH3/CH3N(PA10/PB15),三对互补输出正好对应三相全桥。标题里的“四路PWM”对应两对互补,所以CH3保持普通GPIO即可。通道之间的相位关系由CCR寄存器的值独立决定,死区同样作用于每一对互补信号,互不干扰。

3. Keil MDK下用标准外设库配置TIM1输出带死区四路PWM

工程以Keil MDK为主,芯片选STM32F103RCTx。库的选择上,我建议老项目用标准外设库V3.5,这套代码在Keil里跑得很稳,寄存器结构的可读性比HAL好,对调试高级定时器尤其方便。以下代码是把TIM1配置成10kHz、占空比50%、死区约694ns的两对互补PWM,可以直接放进工程跑。

3.1 工程建立时的三个选择

第一个选择是时钟。F103RC外部8MHz晶振经PLL倍频到72MHz,TIM1挂载在APB2上,记得先检查RCC各总线分频系数。用标准库时系统时钟配置通常由SystemInit()完成,默认就是72MHz,不需要额外改时钟树,但要在初始化PWM前通过RCC_APB2PeriphClockCmd打开TIM1和GPIOA/GPIOB的时钟。

第二个选择是GPIO模式。PA8、PA9、PB13、PB14作为TIM1的复用功能输出,要设为GPIO_Mode_AF_PP推挽复用,速度50MHz。漏开或者普通推挽都会导致波形出不来的问题。

第三个选择是工程里是否启用重映射。F103RC这四个引脚正好是TIM1的默认映射位置,不需要调用GPIO_PinRemapConfig。如果板子上PA8被占用了才考虑部分重映射,但互补输出引脚会跟着变,不建议在入门阶段碰这个。

3.2 完整初始化代码:从GPIO到BDTR全部配置

#include "stm32f10x.h" void FullBridge_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; /* 打开TIM1、GPIOA、GPIOB时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); /* PA8=TIM1_CH1, PA9=TIM1_CH2,上管PWM */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* PB13=TIM1_CH1N, PB14=TIM1_CH2N,下管PWM */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); /* 时基:预分频0,72MHz计数,向上计数模式 */ TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period = 7199; /* 72MHz/7200 = 10kHz */ TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); /* CH1、CH2带互补输出的比较配置 */ TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse = 3600; /* 占空比=3600/7200=50% */ TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); /* 死区:DTG=0x32,50 x 13.89ns ≈ 694ns */ TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime = 50; TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, &TIM_BDTRInitStructure); /* 启动定时器,并必须使能主输出MOE */ TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); }

代码里最关键的是TIM_BDTRConfig之后必须调用TIM_CtrlPWMOutputs(TIM1, ENABLE)。F103的高级定时器有一层主输出开关(MOE位),只调用TIM_Cmd只能让计数器跑起来,PWM引脚依然是高阻,很多人第一次调就是挂在这一点上。TIM_LOCKLevel_1把BDTR的死区、极性等参数锁定,之后写CCR不会再被意外改写,但要在初始化阶段就设好。

死区DTG=50对应第一段公式,50 × 13.89ns约等于694ns,对大多中小功率MOSFET全桥足够。TIM_OCPolarityTIM_OCNPolarity都设为High,含义是CH和CHN的有效电平都是高电平,CHN在逻辑上与CH反相,中间插入死区。若把某一路极性设成低有效,波形会看起来很怪,占空比概念也颠倒。

3.3 占空比计算公式、频率调整与参数组合建议

向上计数模式下,PWM频率公式为:

PWM频率 = fCK / ((PSC+1) × (ARR+1)) 占空比 = CCR / (ARR+1)
  • fCK=72MHz,PSC=0,ARR=7199,则频率=72MHz/7200=10kHz,占空比=CCR/7200
  • 电枢电流纹波随频率升高而减小,但开关损耗增加,直流电机常用10~20kHz,BLDC和步进驱动器常用16~20kHz
  • PSC增大能降低PWM频率但会损失占空比分辨率,PSC=71、ARR=99时频率=72MHz/72/100=10kHz,但CCR每步跳1%而不是0.014%

跑电机做电流环时用PSC=0获得最高分辨率,跑LED调光这类低频场合才用大PSC。死区设定先粗后细,先给1.5~2us保证不炸管,确认驱动波形正常后再逐步减小到满足开关特性的最小值。死区过大对电流波形影响明显,低速时电机噪音和转速抖动都会加重,这是后面第5章要处理的。

4. 全桥驱动接线与波形验证:死区参数这么调才不出问题

代码能编译通过不代表驱动能安全跑起来。这一章把STM32到全桥的接线、示波器测量死区的方法、以及最容易让PWM故障保护失效的几个坑逐个说清楚,全是实操里踩过之后才能总结出来的细节。

4.1 从STM32到栅极驱动IC再到MOSFET的接线

STM32的GPIO输出3.3V电平,驱动能力不足以直接开关大功率MOSFET的栅极电容,中间必须加栅极驱动IC。常见方案是IR2104这类半桥驱动芯片,一路芯片负责一个半桥。接法是PA8输出接到左半桥IR2104的HIN,PB13接到同一芯片的LIN,PA9和PB14接到右半桥对应输入。HIN控制半桥上管,LIN控制下管,芯片内部自动处理自举电容和死区配合的逻辑。

驱动芯片的VS引脚要接到半桥中点,即两个MOSFET的源极和漏极连接处;HO和LO分别接上管和下管的栅极;自举电容跨接在VB和VS之间,典型值100nF~1uF。自举电容太小或PWM占空比长时间接近100%会导致上管驱动电压不足,波形上表现为上管PWM丢脉冲,这时候别怀疑定时器配置,先看自举电容。

功率部分要单独注意地线回路。驱动芯片的COM端、STM32的GND、母线电容地应单点汇聚,避免大电流地平面噪声串进控制信号。示波器探头地线夹子要尽量短,夹长地线会引入明显振铃,把本来就存在的开关尖峰误判成死区问题。

4.2 示波器量死区波形的正确姿势

死区测量用示波器双通道同时接PA8和PB13,触发方式选下降沿或上升沿,时间轴放到500ns/格~2us/格。观察两个波形的交叉点:上管关断后到下管开通前,有一段两个通道都是低电平的低电平区,这段宽度就是死区。

实测时要注意探头的传输延迟差异。同一型号的两根无源探头通常延迟差在1~2ns内,可以忽略;混用不同型号探头时死区测量结果可能偏差明显。更精确的做法是测量CH1N和CH2N之间的死区时,把两根探头对调再测一次取平均,抵消探头延迟。示波器带宽不够时上升沿变缓,容易误读死区边界,建议至少100MHz带宽。

如果示波器带死区时间测量功能,直接设置测量CH1下降沿到CH1N上升沿的时间,读数最直接。没有这个功能就用光标卡在50%电平点之间读差值。死区读数稳定且大于设定值的80%就算正常,因为MOSFET栅极阈值电压不是0V,实际死区会略大于配置值。

4.3 三个让PWM故障保护失效的坑

第一个坑是忘了使能MOE主输出。前面代码里TIM_CtrlPWMOutputs必须在TIM_Cmd之后调用,且两者顺序不能反。有些工程师把PWM初始化放在电机使能之前,只开了TIM_Cmd,结果引脚一直高阻,误以为是GPIO配置错了,查半天发现是输出开关没开。

第二个坑是STM32定时器输出PWM时100%占空比异常。向上计数、PWM1模式下设置CCR大于等于ARR+1确实能得到100%占空比,但此时互补输出CHN长时间处于无效电平,如果负载是电机,持续100%占空比意味着没时间刷新自举电容,上管驱动会掉电。所以工业驱动器一般软件限制最大占空比在95%左右,留出自举刷新和死区余量。

第三个坑是BKIN刹车功能没配好反而引入误触发。TIM_Break = TIM_Break_Disable时BKIN引脚不起作用,但如果硬件上BKIN引脚悬空且外部干扰耦合,电平抖动可能触发刹车。稳妥做法是把BKIN引脚配置为输入浮空并接上拉电阻到高电平,或者干脆禁用Break功能并在PCB上把BKIN隔离。反过来做故障保护时,要把TIM_Break设为Enable,将TIM_BreakPolarity设为低有效,外部故障信号拉低BKIN就能硬件封锁全部PWM输出。

4.4 死区过小和过大分别怎么判断

死区过小的直接表现是上电或动态切换时MOSFET发热异常,甚至冒烟。听不到驱动啸叫时,用示波器高分辨率模式抓取上下臂驱动波形交叉点,如果交叉处出现明显负压尖峰或毛刺,说明已经直通了。行业经验:死区至少大于最慢功率管的关断延时与最快功率管开通延时的差值,再加20%~50%安全裕量。国产MOS管数据手册给的td(off)和td(on)往往比较保守,计算时取最大、最小值的组合来算,不能用典型值。

死区过大则表现为电流波形过零处出现台阶,电机低速运转时听觉上有一顿一顿的感觉。PWM频率越高,死区占比越大,失真越严重。把死区从2us降到500ns后,同样负载下电机噪音下降明显,这是典型的死区过大症状。调死区的方式是把DTG值从中间档位往下减,每次减10%左右,配合示波器观察,直到交叉点没有直通尖峰为止。

5. 死区补偿:把死区吃掉的那部分电压补回来

死区是安全代价,代价就是输出电压畸变。理想情况下,互补PWM在CCR翻转点瞬间切换,平均输出电压只由占空比决定。插入死区后,上下管都关闭的那段时间里,感性负载电流会通过续流二极管续流,把输出电压钳位到母线电压或地,实际平均电压偏离理想值。这个偏差在电流换向附近尤其明显,表现为电机低速转矩脉动和电流波形过零点畸变,这就是电机死区补偿算法要解决的问题。

5.1 电流方向是补偿的唯一依据

死区造成的电压误差方向与负载电流方向强相关。假设负载电流从半桥中点流出,死区内下管续流二极管导通,中点电压被钳到接近0V,本应输出高电平的时间被“吃掉”一段,实际平均电压低于指令值;电流反向流入半桥时,死区内上管续流二极管导通,中点电压被钳到母线电压,实际平均电压高于指令值。所以补偿量不是固定加的,必须判断电流方向后加减占空比。

5.2 基于电流方向的占空比补偿代码

/* 死区补偿:current_dir>0表示电流流出桥臂,<0表示流入 */ static int32_t target_pulse; /* 目标占空比计数 */ static int16_t current_dir; /* 来自电流采样或换向逻辑 */ static uint16_t dead_comp_pulse; /* 补偿量 = 死区时间/计数周期 */ void Update_PWM_With_DeadCompensation(TIM_TypeDef *TIMx, uint16_t channel, int32_t pulse) { int32_t comp_pulse = pulse; if (current_dir > 0) { /* 电流流出:实际电压偏低,补大占空比 */ comp_pulse += dead_comp_pulse; } else if (current_dir < 0) { /* 电流流入:实际电压偏高,减小占空比 */ comp_pulse -= dead_comp_pulse; } /* 过零附近不补偿,避免来回抖动 */ if (comp_pulse < 0) comp_pulse = 0; if (comp_pulse > 3600) comp_pulse = 3600; /* 留出死区余量 */ if (channel == 1) TIM1->CCR1 = (uint16_t)comp_pulse; else if (channel == 2) TIM1->CCR2 = (uint16_t)comp_pulse; }

补偿量dead_comp_pulse由死区时间除以计数器时钟周期得到。死区694ns、时钟13.89ns时,该值约50个计数。实际工程里还要叠加功率管开通延时和关断延时的差值,通常先用理论值上机,再通过观察电流波形过零畸变微调。补偿在过零附近要做滞回处理,电流方向频繁跳变时如果持续切换补偿方向,会让PWM占空比抖个不停。

5.3 补偿效果的验证方法

验证补偿效果用电流探头看电感电流或电机相电流波形。补偿前波形过零点附近有明显平坦段或台阶,补偿后台阶消失,波形更接近正弦。也可以对比相同转速和负载下的电流THD,补偿有效时THD下降几个百分点。没有电流探头时,听电机噪音是次优方案——死区补偿正常后,低速时的高频“嘶嘶”声和顿挫感明显减弱。

补偿量超调会出现新的畸变,方向是反向台阶,这时候把dead_comp_pulse减半再观察。这组调试手法对永磁同步电机、直流无刷电机和步进电机都适用,区别只在电流方向判断的来源:有的用采样电阻加比较器,有的直接用霍尔电流传感器,有的在FOC算法里拿DQ轴电流换算。判断准了方向,补偿算法本身是一样的。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 4:25:10

考虑需求响应的电热综合能源系统两阶段优化调度及Matlab实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 4:24:57

AI Agent记忆系统实战:从架构到遗忘机制

你有没有过这种体验&#xff1a;昨天刚和某个AI助手聊完旅行计划&#xff0c;今天再打开它&#xff0c;对方一脸无辜地反问“你想去哪儿玩来着”。如果你只是个普通用户&#xff0c;顶多吐槽一句“人工智障”&#xff1b;但如果你正在做AI Agent开发&#xff0c;这种“金鱼记忆…

作者头像 李华
网站建设 2026/9/10 4:22:36

深入解析x86处理器06H机器检查异常(MCE)故障定位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 4:22:20

30分钟跑通Dify:Docker Compose部署实战指南

去年年底我帮一个做电商运营的朋友搭知识库问答&#xff0c;他自己折腾了一个礼拜&#xff0c;光 Python 环境就坏了好几次&#xff0c;最后找我远程一看&#xff0c;问题全出在依赖冲突和系统环境上。后来我直接给他换成了 Dify 社区版加 Docker Compose 的部署方式&#xff0…

作者头像 李华
网站建设 2026/9/10 4:22:18

CANN/ge格式建模与API解析

GE 中的 Format 建模与接口语义解析 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 …

作者头像 李华