做电机驱动或者逆变器项目的朋友迟早都会碰上一道绕不过去的坎生成SPWM波。不管你是做变频器、UPS、还是玩航模无刷电调SPWM都是最基础的调制手段。我用STM32调SPWM也算踩了不少坑从最早用纯定时器中断硬凑波形到后来用高级定时器的互补输出加死区再到配合DMA做查表发波整套流程走下来其实核心思路并不复杂。这篇就把我从原理到代码再到硬件电路和调试踩坑的完整经历捋一遍给正准备做相关项目的朋友一个可以直接上手参考的路线。很多刚接触SPWM的人容易一上来就纠结“用哪种生成方式最好”我给你的建议是先别急着选型先把SPWM的本质吃透。SPWM说白了就是“脉冲宽度按正弦规律变化”的PWM。逆变器想输出正弦交流电但功率开关管只会导通和关断做不到直接输出连续变化的正弦电压那就用一系列宽度按正弦分布的脉冲去等效。只要脉冲足够密、滤波足够顺滑负载看到的电压波形就是正弦的。这个思路叫“面积等效原理”——每个脉冲的面积与该时间段内正弦波覆盖的面积成正比。1. 项目整体设计与方案选型思路1.1 为什么用STM32做SPWM控制器市面上生成SPWM的方式不少有纯硬件的模拟电路方案比如用运放搭正弦波发生器再接比较器和三角波也有专用的PWM芯片方案比如SG3524、SG3525这类。硬件方案响应快、没有软件延时但缺点是参数不灵活想改载波频率、调调制比得换电阻电容调试起来非常痛苦。STM32做SPWM控制器的好处在于灵活性。它内部的定时器模块天生就是为PWM场景设计的尤其是带互补输出的高级定时器TIM1和TIM8可以同时输出带死区的一对PWM信号正好用来驱动半桥或全桥电路。而且STM32主频够高、DMA模块又好用正弦表一动输出频率、电压幅值都能随时调整配合ADC还能搞闭环控制。这就是软件实现的真正价值——同样一块板子改几行参数就能适配不同的电机或负载不用动硬件。我这个项目最终选型是STM32F103系列具体型号是STM32F103C8T6“国产神芯”名不虚传做主频72MHz的SPWM生成完全够用。真正干活的是TIM1的高级定时器配合DMA循环搬运正弦表数据到CCR寄存器CPU几乎不参与发波过程剩余算力可以去做电压电流采样、过流保护逻辑甚至通信。1.2 硬件调制和软件实现的本质差异“硬件调制”和“软件实现”这两条路线指的是SPWM波形产生的两种不同路径。硬件调制路线是把正弦波参考信号和三角波载波信号分别接在模拟比较器的两个输入端比较器输出就是SPWM。这个方案在几十年前的逆变器里很流行元件多、温漂大、调参靠拧电位器现在基本只在模拟电源类课程里出现了实际工程中很少再用分立元件搭这套东西。软件实现路线是在单片机里用数字方式复现上述过程。三角波载波由定时器计数器的计数值模拟正弦波参考信号则是一张预先算好的正弦表每个载波周期的比较值由“正弦值乘上定时器周期”得到。当计数器计数值到达比较值时引脚电平翻转输出的就是SPWM。从工程角度看软件方案的优势极其明显改载波频率就是改定时器时钟分频和ARR改输出频率就是改查表的步进改电压幅值就是改正弦表乘的系数。整个发波过程不需要动任何硬件这对产品迭代和调试来说太关键了。我强烈建议新手直接走软件方案先跑通逻辑再回头对比硬件调制的原理理解会深很多。1.3 核心功能拆解载波、信号波与死区SPWM系统里有两个最基本的频率概念必须先弄清楚后面所有参数设计都围绕它们展开。载波频率就是三角波的频率决定PWM脉冲的疏密程度。这个频率同时也约等于功率管的开关频率。载波频率越高输出波形里的高频谐波分量就越容易被滤波器滤除但开关损耗也会同步增加。一般情况下用STM32F103做100kHz以上的载波就不太现实了开关损耗太大驱动器也会过热电机驱动领域常用8kHz到20kHz感性负载加热、逆变电源这类场景常用20kHz左右可以避开音频噪声。我的项目里选的是20kHz高频段人耳听不见又能保证响应速度。信号波频率就是你想输出的正弦交流电的频率。国内市电是50Hz电机调速则是从零点几赫兹到几百赫兹不等。信号波频率和载波频率的比值叫载波比在数字实现里这个比值决定了每个正弦周期里有多少个PWM脉冲。载波比越高波形越精细THD越小。还有一个不能忽略的点是死区。凡是上下桥臂对管的结构同一时刻绝对不能两个管子同时导通否则就是直流母线短路瞬间炸管。IGBT和MOSFET都存在关断延迟所以必须在上下管的驱动信号之间插入一段“双低”时间这就是死区。STM32高级定时器的死区插入是硬件完成的设好寄存器就自动在互补输出上加延时非常方便这也是我选TIM1的核心理由之一。2. 关键参数解析占空比、调制度与线电压利用率2.1 SPWM的占空比计算过程要生成SPWM核心就是解决一个问题每个载波周期里PWM的占空比应该是多少假设正弦信号是 (v_{ref}(t) A \sin(2\pi f_{sig} t))载波是幅度为 (V_{tri}) 的三角波。调制比调制度(M) 定义为正弦波幅值与三角波幅值的比值(M A / V_{tri})。在单极性规则采样法下第 (k) 个载波周期的占空比可以写成[ D(k) \frac{1 M \sin(2\pi k / N)}{2} ]其中 (N) 是一个正弦周期内的载波个数也就是载波比。这个公式理解起来不复杂当正弦值最大时占空比接近100%正弦值为0时占空比50%正弦值最小时占空比接近0%。这正是SPWM的基本特征——脉冲宽度跟随正弦波有规律地变化。在STM32代码里这个公式最终体现为对定时器比较寄存器CCR的赋值。假设定时器采用中心对齐模式计数周期为ARR即三角波峰值那么每个载波周期的比较值就是[ CCR(k) \frac{ARR}{2} \left(1 M \sin(2\pi k / N)\right) ]这里有个小细节中心对齐模式下计数器先递增再递减ARR其实是三角波幅值的一半所以公式里的 (ARR/2) 是基线中心点。实际写代码时通常把正弦表预先算好表中存的就是每个点的比值或原始CCR值运行时通过查表索引直接赋值避免实时算sin函数省下大量CPU时间。2.2 调制度M的选择与实际限制调制度M直接决定输出正弦电压的基波幅值。在理想线性调制区M的取值范围是0到1。当M等于1时SPWM的相电压基波幅值最高等于直流母线电压的一半即 (V_{ph,max} U_d / 2)。如果M超过1会怎么样进入过调制区。此时正弦波的峰值已经被载波“削顶”了输出波形出现明显的平顶失真。但有意思的是过调制反而能进一步提高基波电压幅值代价是低次谐波含量显著增加。很多逆变器产品在电压利用率不够时会刻意采用过调制策略但初学者先别碰这个老老实实让M不超过1先把波形质量做干净。实际工程中M的上限通常留5%到10%的裕量比如设定最大调制比0.9到0.95。这么做的原因有两个一是M过于接近1时占空比接近0%或100%这一段的PWM脉冲非常窄开关管的开关时间有限脉冲可能来不及完整响应导致波形畸变二是死区时间也会压缩有效占空比M太满时系统几乎没有修正空间。2.3 三相输出的线电压利用率为什么是86.6%这是SPWM理论里被提到最多的问题之一很多人知其然不知其所以然。这里把推导过程完整写一遍。单相SPWM在M1时相电压基波幅值为 (U_d / 2)。如果做三相系统三个相位相差120度的SPWM信号分别驱动三相桥臂任意两相之间的线电压基波幅值为[ V_{line,peak} \sqrt{3} \times V_{ph,peak} \sqrt{3} \times \frac{U_d}{2} 0.866 U_d ]也就是说线电压的基波峰值最高只能达到直流母线电压的86.6%。这就是网上常说的“SPWM线电压利用率最高86.6%”的由来。这个数值反映的是正弦波SPWM在不过调制前提下的物理上限不是代码或电路实现不好导致的而是调制方式本身的约束。如果想突破这个上限工程上常采用SVPWM空间矢量调制技术。SVPWM在线电压利用率上可以达到100%其本质是让零矢量和相邻非零矢量的时间分配最优在空间矢量图上让相电压矢量轨迹更接近内切圆。这也是为什么电机矢量控制中普遍用SVPWM而不是SPWM的原因。但如果你的项目只是单纯做逆变电源、UPS或者对THD要求不那么苛刻的驱动场景SPWM加适当滤波完全够用不必一上来就上SVPWM。3. 基于STM32的软件SPWM完整实现3.1 硬件准备与开发环境搭建做这个项目需要的硬件清单不多核心是STM32最小系统板、MOSFET/IGBT驱动板、直流电源和示波器。如果你用的是带功率板的开发套件最好没有的话单独搭一个H桥或三相桥也行。我调试时用的是STM32F103C8T6最小系统板加一块自制的三相全桥驱动板母线电压24V后级带一个阻感负载看波形。开发环境方面我用的Keil MDK5。这里提醒一句如果你是第一次装Keil注意MDK5和C51并不通用装完MDK5之后还需要通过Pack Installer安装STM32F1系列的芯片包否则新建工程时找不到芯片型号。STM32F103的固件库现在主流是标准外设库和HAL库两种我习惯用标准外设库逻辑直白、寄存器操作透明适合理解底层原理。HAL库代码抽象度高一些但移植性更好看个人取舍。这个项目的关键代码我会尽量用寄存器或者标准库操作描述方便对应到数据手册。3.2 正弦表生成与存储策略SPWM软件实现里正弦表是“发动机”。生成正弦表有两种方式一种是上位机算好之后以数组形式写死在代码里另一种是单片机启动时通过数学函数实时生成。前者省RAM但改起来麻烦后者灵活但要占用一部分启动时间。我推荐工程上使用写死数组的方式因为正弦值是一个确定的数学函数没必要每次启动都重复算一遍。假设我们取4096点的正弦表覆盖0到360度每点对应的CCR值可以这样计算#define SINE_POINTS 4096 uint16_t sin_table[SINE_POINTS]; void generate_sine_table(uint16_t arr) { for (uint16_t i 0; i SINE_POINTS; i) { float angle 2.0f * 3.1415926f * i / SINE_POINTS; float sin_val sinf(angle); sin_table[i] (uint16_t)((arr / 2.0f) * (1.0f sin_val)); } }如果不想在代码里实时生成也可以把这段逻辑拿出来在Python或者Excel里算好直接生成一个数组放在源文件里。重点在于表项数据要跟你的ARR值匹配比如ARR设为3000那么表项范围应该在0到3000之间中点大约1500。表项数量选择上4096点算比较奢侈了。对于单相50Hz、载波20kHz的情况一个正弦周期只有400个载波周期取512点就已经非常充裕。点数太多并不会提升输出精度到可感知的程度反而浪费存储空间。2048点是性能和资源的均衡点我在最终版本里用的就是2048点。3.3 定时器配置与中心对齐PWM模式STM32F103的TIM1是高级定时器支持边沿对齐和中心对齐两种PWM模式。SPWM用中心对齐模式更合适因为它产生的三角载波对称度好输出脉冲的中心在每个载波周期的中间谐波特性比边沿对齐更均匀。中心对齐模式需要关注计数方向带来的更新事件。计数器先向上计数到ARR匹配一次CCR输出高电平然后向下计数再次匹配CCR输出低电平。这样载波频率和定时器的计数频率关系是载波频率 定时器时钟频率 / (2 * ARR)因为计数器在一个三角波周期里走了2 * ARR个计数步。假设定时器时钟是72MHz想要20kHz载波那么ARR 72MHz / (2 * 20kHz) 1800。这个计算很关键ARR直接决定三角波幅值也决定后面CCR的取值范围。配置TIM1的代码大致长这样TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; // 使能TIM1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 定时器基本配置72MHz / (2 * 1800) 20kHz TIM_InitStructure.TIM_Prescaler 1; // 预分频注意APB2总线时钟倍频到72MHz TIM_InitStructure.TIM_CounterMode TIM_CounterMode_CenterAligned2; TIM_InitStructure.TIM_Period 1800 - 1; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, TIM_InitStructure); // PWM模式1配置 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 900; // 初始占空比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); // 设置死区时间这里配置为1us左右具体值由BDTR寄存器DBG位决定 TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime 18; // 死区时间配置需要根据时钟算出实际值 TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_BreakFilter 0; TIM_BDTRInitStructure.TIM_DeadTime 72; // 1us72MHz TIM_BDTRInit(TIM1, TIM_BDTRInitStructure); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE);需要注意高级定时器TIM1默认情况下PWM输出是关闭的必须调用TIM_CtrlPWMOutputs使能主输出否则引脚上没有波形。这个函数对应的是TIM1的BDTR寄存器中的MOE主输出使能位非常容易漏掉尤其是从普通定时器转过来的人几乎都在这卡过。3.4 用DMA查表发波实现免CPU干预当你有一张正弦表、定时器也配好之后最笨的软件实现就是在定时器更新中断里查表并修改CCR。但这有两个问题一是每次中断要进进出出20kHz载波频率下中断频率其实不高但如果后续做高频载波就会很吃力二是中断里做乘法、查表这些操作会影响发波的实时性。更好的办法是使用DMA。STM32的DMA可以把内存数组中的数据自动搬运到定时器的CCR寄存器而搬运的时机正好可以设定为更新事件。这样就形成了一条“硬件流水线”定时器溢出一次DMA自动把正弦表下一个值送进CCRCCR改变占空比。CPU全程不参与只负责启动传输。实现思路是把正弦表数组的地址作为DMA的源地址把TIM1-CCR1寄存器的地址作为DMA的目标地址传输方向是内存到外设外设地址不自增、内存地址自增。配置成循环模式DMA就会持续循环发送整个正弦表。void dma_pwm_start(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)TIM1-CCR1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)sin_table; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize SINE_POINTS; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); // 选择TIM1更新事件作为DMA请求 TIM_DMAConfig(TIM1, TIM_DMABase_CCR1, TIM_DMABurstLength_1Transfer); TIM_DMACmd(TIM1, TIM_DMAUpdate, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }这段配置里有个容易踩坑的地方DMA搬运的数据宽度必须和CCR寄存器匹配CCR是16位所以选HalfWord。另外内存地址必须指向数组的首地址且要保证数组不被编译器优化掉必要的时候用volatile修饰。如果你用的是标准库记得在启动DMA之前确认定时器更新事件已触发过一次否则第一次DMA搬运时机可能会偏。3.5 可变频率输出与死区参数实测查表发波已经能输出固定频率的SPWM了但很多场景要求输出频率可调比如电机调速。实现频率可调的核心是改变查表的步进量。假设正弦表有2048个点表里每点对应的角度是360/2048度如果你每次更新事件跳过2个点输出频率就翻倍。更一般地步进 (step 2048 \times f_{sig} / f_{carrier})。实际操作中step通常是浮点数这就要用到累加器来避免取整误差uint32_t phase_acc 0; uint32_t step_fp (uint32_t)(step * 65536); // 定点化16位小数 // 在DMA更新中断里或者自己处理中断时 phase_acc step_fp; uint16_t index (phase_acc 16) (SINE_POINTS - 1); TIM1-CCR1 sin_table_processed[index];但如果用了DMA自动循环修改频率就有点麻烦了。DMA的搬运地址是固定的不能直接修改步进。一个替代方案是用每隔一个更新事件才更新一次索引或者用普通中断方式更新索引用DMA方式只负责搬运。工程上常见的做法是频率切换不频繁的场合每次改频率就重新初始化正弦表和DMA参数频率连续可调的场景直接切换到中断法20kHz载波下中断占用其实可以接受。我做的变频输出最终是回到中断方式实测20kHz载波、STM32F103主频72MHz中断里做累加、查表、赋值三个操作CPU占用不到3%。死区时间也是需要实测调整的参数。STM32的BDTR寄存器死区时间有专门的编码格式单位取决于TDTS时钟。72MHz时钟下TDTS约为13.9ns所以寄存器值72大约对应1us。但实际死区时间要结合你选用的功率管栅极驱动电阻的关断延迟来定。IGBT一般需要2us到3us低压MOSFET 1us左右就够。死区时间太短会炸管太长会增大输出畸变。我的经验是第一版先按功率管数据手册里的关断延迟拐点时间取1.5倍往上加然后用示波器同时抓上下管栅极波形确认没有交叠之后再逐步缩短。4. 硬件调制电路对比与后级滤波设计4.1 纯硬件SPWM调制电路的局限既然标题里提到“硬件调制”这里在实操层面做个对比方便你理解为什么现在主流是软件实现。经典的硬件SPWM调制电路由正弦波发生器和三角波发生器构成。正弦波用文氏桥振荡器或者ICL8038这类函数发生器芯片产生三角波用运放积分电路对方波积分得到两者一起接到LM311比较器的两个输入端比较器输出就是SPWM。再经过IR2110之类驱动芯片去推MOS管。这套电路我学生时代搭过调起来真的很想摔东西。文氏桥振荡器要调节振幅稳定三角波发生器的对称性直接影响波形质量比较器的电平匹配又跟前后级有关。更麻烦的是你想改个频率就得换电阻电容想调节调制比就得调电位器稍微一动整个电路的工作点就全变了。硬件方案还有一个硬伤是无法方便地处理死区你得额外加RC延时电路来生成死区精度完全取决于元件容差。软件方案把这些全部归一为代码里的数值设置从工程迭代效率来看差异是代际级别的。除非你有超低延迟的极端需求否则完全没有理由在新设计里用纯硬件调制。4.2 输出级LC低通滤波参数计算SPWM本身输出的是一串高频脉冲要得到光滑的正弦波必须在输出端加低通滤波器。滤波器的截止频率选择有个经验法则设置在载波频率的1/10到1/20之间。载波20kHz的话截止频率放1kHz到2kHz比较合适。这样既能保留50Hz工频分量又能有效衰减20kHz的载波谐波。LC低通滤波器的截止频率公式为[ f_c \frac{1}{2\pi\sqrt{LC}} ]假设取截止频率1kHz可以选择L 5mHC 10uF[ f_c \frac{1}{2\pi\sqrt{5 \times 10^{-3} \times 10 \times 10^{-6}}} \approx 712\text{Hz} ]713Hz的截止频率在1kHz附近基本符合设计要求。实际调试时电感值未必刚好是5mH绕制或购买的时候测一下实际值即可。滤波器还有一个重要参数是特征阻抗 (Z_0 \sqrt{L/C})它应该和负载阻抗大致匹配否则阻尼特性太差容易发生振铃。5mH对10uF的特征阻抗约22欧姆适合低阻负载。如果你推动的是电机这类感性负载电机绕组本身就有电感可以省掉或者大幅减小滤波电感。电机本身的绕组电感配合PWM的高频分量抑制效果就很明显这也是变频器直接输出PWM而不加额外滤波器的原因。而普通的阻性负载逆变电源就需要完整的两级滤波器来保证输出电压质量。4.3 驱动电路保护要点生成SPWM信号只是第一步信号要经过驱动电路才能真正控制功率管。常用的驱动方案有光耦隔离驱动TLP250、HCPL3120、半桥驱动芯片IR2104、IR2110以及集成隔离驱动芯片Si8261等。低压小功率场景用IR2104加自举电容最方便成本低、外围简单。但自举电容有个问题是低频输出比如低于10Hz时自举电压会下降导致上管驱动不足这时候就要考虑隔离供电方案。死区保护之外硬件层面还有一个必须有的保护——过流保护。最简单的方式是在直流母线串联采样电阻把电流信号送给比较器超过阈值立即封锁PWM输出。STM32高级定时器的刹车输入引脚BKIN就是干这个的配置成刹车功能后一旦触发刹车定时器输出立即被硬件强制为安全电平不需要软件参与。这个响应速度是微秒级的比任何中断都快。做功率电路的朋友一定要把这个功能用起来别指望软件保护短路的时候CPU来不及反应的。我在实测中遇到过最危险的一次情况就是死区配置寄存器被意外改写导致上下管直接导通24V电源瞬间短路MOS管当场冒烟。从那以后我的保护流程一定是先空载测驱动波形确认死区正常再接功率电测试最后才敢接负载。5. 常见问题排查与实战经验5.1 波形异常排查速查表SPWM调试过程中最容易碰到的几个问题我整理成了一张速查表这些问题基本覆盖了90%的入门调板场景现象可能原因排查方法输出全高或全低高级定时器MOE主输出未使能检查是否调用TIM_CtrlPWMOutputs上下管直通冒烟死区时间配置过短或走丢失用示波器双通道看上下管栅极确认无交叠输出频率比预期高/低预分频或ARR计算错误重算载波频率定时器时钟/(2*ARR)确认分频正弦波形出现明显台阶正弦表点数太少或滤波截止频率偏高增加表点数或降低LC截止频率带载后波形塌陷母线电压被拉低、滤波电感饱和检查供电能力换更大饱和电流的电感修改参数后无变化DMA搬运了旧的缓冲区确认DMA内存地址指向新表格并重新初始化5.2 波形失真死区效应与补偿思路死区时间虽然能保护功率管但也会带来一个副作用电压损失和波形畸变。死区期间输出既不是高也不是低等效于占空比被缩短了表现为输出电压基波幅值下降同时出现低次谐波。载波频率越高、死区占比越大这个问题越明显。解决思路有几类。最简单粗暴的是开环补偿把死区时间对应的占空比损失折算成固定补偿量加到CCR上但这个对负载电流方向敏感补偿方向搞反了反而会更差。常见一点的做法是电流极性检测后做双向补偿检测到电流流出桥臂时补偿这一侧流入时补偿另一侧这就是所谓的“死区补偿”。更智能的方式是直接在算法上做修正用电压反馈闭环把死区造成的畸变直接闭掉。对初学阶段来说我建议先用示波器观察清楚死区对波形的影响不用急着做补偿。先把死区设置在两倍经验值确认波形整体形态正确再逐步缩短死区到正常范围。波形畸变如果只是轻微增大对大多数负载驱动来说影响不大以后做精密逆变电源时再认真处理。5.3 后续扩展方向SVPWM与闭环控制SPWM跑通之后这套软硬件平台可以平滑过渡到更高级的控制算法。如果你往电机控制方向走下一步就是SVPWM。它的基本思想是把三个桥臂的开关组合映射到六个非零矢量和两个零矢量用相邻矢量合成参考电压矢量。和SPWM相比SVPWM的直流电压利用率高15.5%动态响应也更好。实现SVPWM时前面搭好的定时器PWM框架可以完全复用只需要把合成矢量算出的占空比直接写入三个通道的CCR即可。如果往逆变器方向走可以在现有平台上加电压采样和电流采样做双闭环控制。内环电流环用PI调节器外环电压环也用PI输出SPWM时把PI输出乘到正弦表上当作调制比。这个时候SPWM的平台优势就体现出来了——控制策略可以随便改底层发波框架纹丝不动。5.4 我在这个项目里踩过的坑最后说几个让我印象深刻的教训每一个都是真金白银换来的。第一个坑是STM32高级定时器的PWM输出使能顺序。只调用TIM_Cmd使能定时器是不够的还需要显式调用TIM_CtrlPWMOutputs来打开主输出。我第一次调的时候输出一直是低电平查了一个晚上数据手册才发现MOE位没有置1。这个低级错误当年劝退了好多人希望现在的初学者看了这篇可以少走这个弯路。第二个坑是示波器采样和解释。SPWM波形在示波器上看起来是密度均匀的窄脉冲很多人误以为没调对其实把时间轴拉长看整体脉冲宽度的包络才会发现它确实呈现正弦轮廓。判断SPWM是否正常的最快方法是用示波器的“余晖”模式或者用滤波后输出端的正弦波形来反推。第三个坑是电源和地线的处理。控制板和功率板共用地线的时候功率开关瞬间的di/dt会在地线上产生很大的噪声严重时会导致SPWM信号抖动甚至让单片机跑飞。后来我把控制板的地和功率地单点连接PWM信号经过光耦隔离问题就消失了。做任何功率电子项目布局和接地都不是小事越早建立这个意识越好。