1. 为什么你需要掌握互补SPWM与死区控制如果你正在玩电机驱动、逆变器或者任何需要把直流电变成交流电的项目那你肯定绕不开PWM脉宽调制技术。但普通的单路PWM在很多功率应用里是不够的特别是涉及到半桥或全桥电路的时候。这时候互补PWM和死区控制就成了你必须搞明白的两大核心技能。简单来说互补PWM就是一对“你开我关你关我开”的对称波形分别驱动桥式电路的上管和下管。听起来简单但直接让两路信号完全反相是有风险的——万一开关器件动作没那么快在切换的瞬间上下管可能会同时导通那么一丁点时间这就叫“直通”。对于MOS管或IGBT直通意味着巨大的短路电流轻则芯片发烫重则直接“放烟花”让你的心血瞬间归零。所以死区时间就是为了防止这个悲剧而插入的一个短暂“安全间隔”。在这段时间里确保上下管都处于关闭状态给功率器件足够的切换余量。这个时间不能太长太长会影响输出波形的有效电压和线性度也不能太短太短了起不到保护作用。如何精确地生成互补PWM并灵活、可靠地配置这个生死攸关的死区时间就是咱们今天要啃下的硬骨头。好消息是很多STM32芯片内置的高级定时器比如TIM1, TIM8天生就是干这个的硬件专家。它内部集成了互补输出通道和可编程的死区生成电路你只需要配置几个寄存器它就能帮你自动生成带死区的、严丝合缝的互补PWM波完全不用CPU频繁干预既精准又省心。咱们今天的主角是经典的“蓝桥杯小钢炮”——STM32F103C8T6。别看它价格亲民资源一点不弱它的TIM1定时器正是这样一个高级定时器。我将带你从零开始手把手用它的TIM1生成频率为20kHz的互补SPWM波并精细调整死区时间。最后我们用一个简单的RC滤波电路就能把这组高频PWM波还原成纯净的50Hz正弦波。这个过程就像把一块块乐高积木PWM脉冲搭建成一座平滑的拱桥正弦波非常直观有趣。无论你是正在做毕业设计的学生还是从事新能源、电机控制的工程师亦或是单纯的电子爱好者掌握这套方法都能让你在面对桥式驱动电路时心里更有底调试更高效。咱们不搞虚的直接进入实战。2. 硬件与原理你的“武器库”与“作战地图”在写代码之前咱们得先搞清楚手里的家伙什儿和要达成的目标原理。这就像打仗前得熟悉自己的武器和战场地形。2.1 认识你的核心装备STM32F103C8T6的高级定时器TIM1STM32F103C8T6的TIM1是一个16位的高级控制定时器功能非常强大。对于咱们这个项目你需要重点关注它的这几个特性互补输出通道TIM1的通道1CH1、通道2CH2、通道3CH3都配有对应的互补输出通道CH1N, CH2N, CH3N。CH1和CH1N就是一对天生的“搭档”可以输出极性可配置的互补PWM波。集成死区时间发生器这是高级定时器的灵魂功能之一。它位于输出通道和最终输出引脚之间可以自动在你设定的互补信号切换点插入一段可控的延迟死区时间完全由硬件实现精度极高且不占用CPU。刹车功能高级定时器通常有一个“刹车”输入引脚比如TIM1的BKIN。当这个引脚检测到特定电平比如故障信号时定时器可以立即强制所有输出通道进入一种安全状态比如全部拉低防止故障扩大。这是一个重要的安全特性在电机驱动中常用。丰富的时钟源与分频可以产生非常宽频率范围的PWM从几Hz到几十MHz都能覆盖。在我们的实践中我们将使用CH1 (PA8)作为主输出通道。CH1N (PB13)作为CH1的互补输出通道。BKIN (PB12)作为刹车输入引脚本例中配置为启用状态但可通过上拉电阻保持无效作为安全备份。2.2 SPWM是什么如何用PWM“拼”出正弦波PWM是宽度变化的脉冲那SPWM正弦波脉宽调制就是让这些脉冲的宽度按照正弦函数的规律来变化。想象一下我们要得到一个50Hz的正弦波电压。如果我们直接用单片机DAC输出频率和精度可能受限。换个思路如果我们先产生一个频率很高比如20kHz的PWM波但这个PWM波的占空比在每个周期里不是固定的而是按照50Hz正弦波的瞬时幅度值来实时调整会怎样在一个20kHz的PWM周期50us里如果占空比很大输出高电平的时间就很长平均电压就高如果占空比很小平均电压就低。当我们用这个高频PWM去驱动一个LC低通滤波器时高频成分20kHz会被滤掉剩下的就是那个随着占空比缓慢变化的平均电压——这个平均电压的包络线正是我们想要的50Hz正弦波这就是SPWM的核心思想用高频载波20kHz PWM的占空比变化来“模拟”低频调制波50Hz正弦波的幅度变化。载波频率越高经过滤波后得到的正弦波就越平滑谐波成分越少。那么如何让PWM的占空比按正弦规律变化呢我们需要一个“正弦表”。在程序初始化时我们预先计算好一个正弦周期内多个等分点的正弦值并转换为对应的PWM比较寄存器值CCR。然后通过一个定时中断比如50us中断一次对应20kHz的更新率依次将这个表里的值更新到PWM发生器的CCR寄存器中。这样PWM的输出占空比就会循环地、平滑地跟随正弦表变化从而在滤波后呈现出正弦波形。2.3 死区时间那个关键的“安全气囊”前面提到了死区的重要性这里再深入一下它的硬件实现原理。在STM32的高级定时器中死区时间是通过一个专门的寄存器TIMx_BDTR中的DTG位域来配置的。这个配置值TIM_DeadTime不是一个直接的时间值而是一个编码值。芯片手册里会提供一个公式或表格告诉你不同的DTG值对应多少纳秒ns的死区时间。这个时间指的是在输出状态需要切换时比如CH1从高变低CH1N从低变高硬件会自动在两个信号的有效边沿之间插入一段两者都为无效电平通常是我们配置的“关闭状态”的延时。计算死区时间需要知道定时器的内部时钟频率。以我们的配置为例系统时钟72MHz经过TIM1的预分频器PSC9-18得到TIM1的计数时钟CK_CNT 72MHz / (81) 8MHz周期为125ns。死区时间发生器以这个CK_CNT时钟的某个分频比如fDTS为基准进行延时。配置值0x40根据手册公式计算大约对应0.9us的死区。这个时间对于很多中小功率的MOSFET驱动来说是一个比较常用且安全的起点。3. 从零开始代码实战与逐行解析理论说再多不如一行代码来得实在。咱们现在就打开你的开发环境Keil、IAR、STM32CubeIDE都行跟着我一步步搭建工程。我会把关键代码拆开揉碎了讲确保你知其然更知其所以然。3.1 第一步高级定时器TIM1的初始化配置这是整个工程的核心代码量稍大但每一行都有其意义。我们创建一个pwm.c和pwm.h文件。// pwm.h #ifndef __PWM_H #define __PWM_H #include stm32f10x.h void PWM_Init(void); void PWM_SetCompare1(uint16_t Compare); #endif// pwm.c #include stm32f10x.h #include pwm.h void PWM_Init(void) { // 1. 开启外设时钟任何操作前必须先给模块上电 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_TIM1, ENABLE); // 2. 配置GPIO引脚为复用推挽输出用于PWM和浮空输入用于刹车 GPIO_InitTypeDef GPIO_InitStructure; // 配置PA8 (TIM1_CH1) 为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 高速输出 GPIO_Init(GPIOA, GPIO_InitStructure); // 配置PB13 (TIM1_CH1N) 为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_Init(GPIOB, GPIO_InitStructure); // 配置PB12 (TIM1_BKIN) 为浮空输入。通常外部接上拉电阻保持高电平刹车无效。 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); // 3. 配置TIM1时基单元决定PWM的频率和计数模式 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 399; // 自动重装载值ARR: 400-1 TIM_TimeBaseStructure.TIM_Prescaler 8; // 预分频器PSC: 9-1 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频影响死区时钟和数字滤波 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseStructure.TIM_RepetitionCounter 0; // 重复计数器高级定时器特有用于控制PWM周期数0表示每个ARR更新都输出 TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // 计算一下PWM频率CK_CNT 72MHz / (PSC1) 8MHz。PWM频率 CK_CNT / (ARR1) 8MHz / 400 20kHz。完美 // 4. 配置TIM1通道1的输出比较模式PWM模式 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // PWM模式1CNTCCR时有效 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // 主输出使能 TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; // 互补输出使能 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_OCInitStructure.TIM_Pulse 0; // 初始占空比后面会动态改这里先设为0 TIM_OC1Init(TIM1, TIM_OCInitStructure); // 初始化通道1 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // 使能CCR1的预装载寄存器避免更新时产生毛刺 // 5. 配置刹车和死区时间寄存器BDTR这是关键 TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; // 运行模式下“关闭状态”选择 TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Enable; // 空闲模式下“关闭状态”选择 TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_1; // 锁定级别防止软件误写BDTR寄存器 TIM_BDTRInitStructure.TIM_DeadTime 0x40; // 死区时间设置对应约0.9us需根据时钟计算确认 TIM_BDTRInitStructure.TIM_Break TIM_Break_Enable; // 使能刹车功能 TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_Low; // 刹车输入低电平有效 TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; // 刹车后自动恢复输出 TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure); // 6. 启动定时器 TIM_Cmd(TIM1, ENABLE); // 使能TIM1计数器开始计数 TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须单独使能PWM主输出这个很重要 } // 一个简单的函数用于动态改变通道1的占空比 void PWM_SetCompare1(uint16_t Compare) { TIM_SetCompare1(TIM1, Compare); // CCR1的值设置为Compare范围0~ARR }注意TIM_CtrlPWMOutputs这个函数对于高级定时器TIM1, TIM8是必须调用的它控制着最终输出是否真正送到引脚。普通定时器没有这个函数。很多初学者调了半天没波形问题就出在这里。3.2 第二步生成正弦波查找表我们需要一个数组存储一个完整正弦周期内各个点对应的PWM比较值。创建spwm.c和spwm.h。// spwm.h #ifndef __SPWM_H #define __SPWM_H #include stm32f10x.h #define PI 3.1415926f #define SINE_POINTS 400 // 一个正弦周期采样的点数点数越多波形越细腻 #define MAX_CCR 400 // 对应PWM的ARR值决定了PWM的分辨率 extern uint16_t sineTable[SINE_POINTS]; extern float modulationIndex; // 调制比控制正弦波幅度 void SPWM_GenerateTable(void); #endif// spwm.c #include spwm.h #include math.h // 使用sin函数 uint16_t sineTable[SINE_POINTS] {0}; float modulationIndex 0.85f; // 调制比0.85即输出正弦波幅值为直流母线电压的85% void SPWM_GenerateTable(void) { uint16_t i; float radianStep; // 弧度步进值 float sineValue; // 计算出的正弦值-1 ~ 1 uint16_t ccrValue; // 最终要存入表格的CCR值 // 计算出一个正弦周期2π被分成SINE_POINTS份后每一步的弧度 radianStep (2 * PI) / SINE_POINTS; for(i 0; i SINE_POINTS; i) { // 1. 计算当前点的正弦值 sineValue sinf(i * radianStep); // 结果在 -1 到 1 之间 // 2. 将正弦值缩放到我们PWM的“工作区间”。 // 思路PWM的占空比不能为负。我们让正弦波叠加在一个直流偏置上。 // 直流偏置 MAX_CCR / 2 这样正弦波就在偏置上下波动。 // 最终公式CCR 直流偏置 正弦波幅值 * modulationIndex * 直流偏置 // 简化后CCR (MAX_CCR/2) * (1 modulationIndex * sin(theta)) ccrValue (uint16_t)( (MAX_CCR / 2.0f) * (1.0f modulationIndex * sineValue) ); // 3. 防止计算出的值超出CCR的有效范围0 ~ MAX_CCR-1做个保护 if(ccrValue MAX_CCR) { ccrValue MAX_CCR - 1; } // 最小值保护通常不需要因为公式算出来不会小于0但加上更严谨 // if(ccrValue 0) ccrValue 0; sineTable[i] ccrValue; } }这个函数执行一次就会把一个周期400点的正弦波数据准备好存到sineTable数组里。modulationIndex调制比是个很重要的参数它决定了最终正弦波的幅度。当它为1时正弦波会达到最大峰峰值接近直流母线电压当它小于1时幅度同比缩小。在实际逆变器中调节这个值就可以实现变压VV功能。3.3 第三步用定时器中断驱动SPWM更新PWM的硬件已经以20kHz的频率运行了但它的占空比需要以50Hz的节奏来变化。我们需要另一个定时器比如TIM3产生一个50us的中断在这个中断里更新PWM的占空比。50us中断一次对应20kHz正好和PWM载波频率同步这样每个PWM周期更新一次占空比是最简单的方式。创建timer.c和timer.h。// timer.h #ifndef __TIMER_H #define __TIMER_H void Timer3_Init(void); // 初始化TIM3用于SPWM更新 #endif// timer.c #include stm32f10x.h #include timer.h void Timer3_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 使能TIM3时钟在APB1总线上 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. 配置时基单元产生50us中断 // 系统时钟72MHzAPB1分频系数为2但TIM3/4的时钟是APB1时钟的2倍所以仍是72MHz。 // 目标50us中断一次。计时时间 (PSC1)*(ARR1) / 72MHz 50e-6 s // 取 PSC 36-1 35, 则计数器时钟 72MHz / 36 2MHz周期0.5us。 // 需要计数次数 50us / 0.5us 100。所以 ARR 100-1 99。 TIM_TimeBaseStructure.TIM_Period 100 - 1; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler 36 - 1; // 预分频器 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 3. 清除更新中断标志使能更新中断 TIM_ClearFlag(TIM3, TIM_FLAG_Update); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 4. 配置NVIC嵌套向量中断控制器 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 先设置优先级分组 NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 5. 启动定时器3 TIM_Cmd(TIM3, ENABLE); }3.4 第四步主函数与中断服务程序最后我们把所有模块组合起来。在main.c中#include stm32f10x.h #include pwm.h #include spwm.h #include timer.h int main(void) { // 初始化各个模块 PWM_Init(); // 初始化TIM1 PWM输出 SPWM_GenerateTable(); // 生成正弦波查找表 Timer3_Init(); // 初始化TIM3中断用于更新PWM占空比 // 主循环里什么都不用做所有工作都在中断里自动完成 while(1) { // 这里可以添加其他任务比如按键调整调制比、串口通信等 // 但注意不要进行长时间阻塞的操作以免影响中断响应 } } // TIM3中断服务函数在这里更新PWM占空比 void TIM3_IRQHandler(void) { static uint16_t tableIndex 0; // 静态变量用于记录当前查表位置 if(TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) // 检查是否是更新中断 { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 清除中断标志位必须 // 从查找表中取出当前点对应的CCR值并设置给PWM通道1 PWM_SetCompare1(sineTable[tableIndex]); // 索引递增指向下一个点 tableIndex; // 如果索引到达表末尾则归零实现循环查表 if(tableIndex SINE_POINTS) { tableIndex 0; } } }至此所有代码就完成了。编译、下载到你的STM32F103C8T6核心板用示波器探头分别测量PA8和PB13你应该能看到一对20kHz的、占空比缓慢变化的互补PWM波并且在它们的上升沿和下降沿之间能看到一个微小的空白间隔——那就是我们设置的死区。4. 调试、测量与效果验证眼见为实代码跑起来只是第一步用仪器验证波形是否符合预期才是工程实践的闭环。这里分享几个关键的调试点和测量技巧。4.1 如何准确测量死区时间死区时间通常很短在几百纳秒到几微秒之间。要准确测量你需要一台带宽足够的数字示波器并掌握正确的触发方法。使用双通道测量将示波器的通道1接PA8CH1通道2接PB13CH1N。设置触发将触发源设为通道1触发类型为“边沿触发”触发条件设为“上升沿”。这样波形会稳定显示。放大观察切换点调整水平时基将波形展开重点关注其中一个脉冲的结束边沿。你会看到当CH1的脉冲结束下降沿后CH1N并不会立即上升而是等待了一段空白时间后才开始上升。这段空白时间就是死区。使用光标测量打开示波器的光标测量功能将两个垂直光标分别对准CH1下降沿的50%幅度点和CH1N上升沿的50%幅度点。示波器会直接显示出两者之间的时间差这就是你配置的死区时间。在我们的配置中这个值应该接近0.9us。提示如果测量出的死区时间与你计算的值有偏差请首先检查TIM1的时钟配置PSC和TIM_ClockDivision的设置因为死区时间发生器的时钟fDTS来源于CK_INT经过CKD分频后的时钟。仔细查阅STM32参考手册中关于“死区时间生成”的章节和计算公式。4.2 观察SPWM波与滤波后的正弦波测量完死区我们来看看SPWM的整体效果和最终目标——正弦波。观察SPWM波将示波器时基调回较慢档位比如1ms/div观察PA8的波形。你应该能看到一簇密集的PWM脉冲但其脉冲的宽度占空比在缓慢地、周期性地变化。这个变化的周期就是50Hz20ms。这就是SPWM的时域形态。滤波得到正弦波准备一个简单的RC低通滤波器。例如使用一个10Ω的电阻和一个10μF的电解电容注意极性。电阻一端接PA8另一端接电容正极和示波器探头电容负极接地。RC滤波器的截止频率计算公式为f_c 1 / (2πRC)。对于R10Ω C10μFf_c ≈ 1.6kHz。这个频率远低于我们的PWM载波频率20kHz但高于我们想要的50Hz正弦波频率。因此它能很好地滤除20kHz的高频成分同时保留50Hz的低频变化。将示波器探头接在滤波器的输出端即电容两端。调整示波器为AC耦合垂直档位适当调大。你应该能看到一个平滑的、频率为50Hz的正弦波。它的幅度由PWM的调制比modulationIndex和直流母线电压决定。观察互补通道的滤波结果用同样的滤波器接在PB13上你会得到另一个50Hz正弦波。关键来了用双通道同时观察PA8和PB13滤波后的波形调整触发你会发现这两个正弦波是相位相差180度的这正是互补SPWM经过滤波后的必然结果也是驱动H桥电路形成交流电压的基础。4.3 常见问题与排查指南在实践过程中你可能会遇到一些问题这里我列举几个常见的问题1完全没有波形输出。检查TIM_CtrlPWMOutputs(TIM1, ENABLE);这行代码是否遗漏这是高级定时器输出使能的关键。检查GPIO模式是否配置为GPIO_Mode_AF_PP复用推挽输出检查TIM_Cmd(TIM1, ENABLE);是否调用检查硬件连接是否正确PA8/PB13是否被其他外设占用问题2有波形但不是互补的或者两个通道一样。检查TIM_OCInitStructure.TIM_OutputNState是否设置为Enable检查TIM_OCInitStructure.TIM_OCNPolarity是否配置正确如果你配置了相同的极性波形就可能同相。问题3死区时间看不到或者时间不对。检查TIM_BDTRConfig函数是否被正确调用死区时间配置TIM_DeadTime是否设置检查测量方法是否正确需要用高时基放大观察边沿。检查计算一下你的定时器时钟CK_CNT和fDTS根据手册公式重新核算DTG寄存器的设置值。问题4滤波后的正弦波畸变严重毛刺多。检查RC滤波器的截止频率是否合适截止频率太低R或C太大会衰减50Hz信号本身太高则滤除20kHz载波不干净。可以尝试调整R或C的值。检查PWM载波频率20kHz是否稳定可以在主循环中增加其他繁重任务试试如果载波频率因中断处理过长而抖动会影响滤波效果。确保SPWM更新中断TIM3的执行时间尽可能短。尝试增加SPWM一个周期的点数比如从400增加到800这样正弦波采样更密波形更平滑。调试的过程就是不断发现问题、分析原因、验证猜想的过程。当你第一次在示波器上看到那个干净完美的正弦波时那种成就感就是驱动我们不断探索的最大乐趣。这套基于STM32F103C8T6高级定时器的互补SPWM生成方案虽然用的是基础型号但其原理和代码结构对于STM32整个F1、F4甚至H7系列都是相通的具有很强的迁移性。希望这篇详细的实践指南能成为你进入电机控制和电力电子世界的一块坚实跳板。