STM32 TIM1实现频率与幅度独立可调SPWM的设计与实现

STM32 TIM1实现频率与幅度独立可调SPWM的设计与实现 简介STM32F103 高级定时器 TIM1 配合 SPWM 技术实现频率与幅度可调而载波频率不变的 PWM 输出方案面向嵌入式开发者及逆变电源、电机驱动方向的学习者。压缩包共 219 个文件以 C 源码、H 头文件和 Keil 工程文件为主体也包含编译生成的 HEX/AXF、MAP 及中间 .crf/.o 文件大小约 5.72MB可直接用 Keil 打开查看或重新编译其中 41 个 .h 头文件与 39 个 .c 源文件便于梳理工程结构39 个 .crf 与 40 个 .o 中间文件有助于理解编译流程。已有 615 人学习下载具备一定参考价值。内容覆盖 TIM1 时钟、预分频、自动重装载配置互补输出引脚 CCxE/CCxNE 设置以及占空比与频率的动态调整思路并结合中断服务函数实现实时参数修改。通过学习读者可掌握基于标准库的 SPWM 生成方法理解 3 路互补 PWM 信号的构造要点为设计逆变电源或电机驱动器提供可运行、可改写的工程样例。 做逆变器或者电机驱动的时候很多人会卡在一个看似简单的问题上我需要一个正弦波频率和幅度都能调但PWM的载波频率必须固定。比如我要做一个变频电源输出从50Hz调到200Hz但开关频率始终锁定在10kHz不变。这种需求听起来简单真上手会发现有不少细节容易翻车。这篇博文就完整分享一下我用STM32F103的TIM1做频率、幅度独立可调SPWM的实现思路包括参数计算、查表原理、中断代码和一些实测踩坑记录。做逆变电源、电机调速、可调电源或者纯学习SPWM原理的都可以参考这套方案。1. 为什么用TIM1做SPWM高级定时器的核心优势1.1 SPWM的底层原理面积等效SPWM正弦脉宽调制的核心就是“冲量等效”思想一个正弦波可以用一系列宽度按正弦规律变化的脉冲去等效。脉冲越窄、数量越多等效出来的波形谐波越少越接近理想正弦波。实际工程中我们用高频三角波载波和低频正弦波调制波比较交点处决定PWM开通和关断的时刻这就是自然采样法如果不做实时比较而是提前算好每个载波周期内正弦波对应的占空比存成表格查表输出那就是规则采样法。这篇文章用的是规则采样法思路非常朴素每个载波周期也就是一次PWM更新中断里我们查一次正弦表算出一个和当前正弦相位对应的占空比写到比较寄存器里。载波频率固定是因为定时器的计数周期固定正弦波频率可变是因为查表时的相位步进可以任意调节。1.2 为什么是TIM1而不是TIM2、TIM3很多初学者习惯用TIM2、TIM3做PWM因为它们也能输出配置还更简单。但做正经的SPWM电源类应用TIM1这类高级定时器几乎是不可替代的原因有三个互补输出和死区插入TIM1有CH1和CH1N这样的互补输出对可以直接驱动半桥或全桥的上下管。关键是死区是硬件自动插入的不需要软件去延时翻转这对可靠性影响非常大——上下管直通炸管的事故多半是死区没做好。刹车输入BKIN引脚可以在过流、过压等故障时瞬间封波把输出引脚强行拉成安全电平。这对于逆变器这种功率设备来说是一条保命功能。重复计数器TIM1的RCR寄存器可以设置每N次更新事件才产生一次中断这个在做逐波限流、低频控制时非常有用。即便你只是做个单路SPWM实验不接功率部分用TIM1提前把驱动架构的底子打好后续扩展全桥、加保护都方便得多不用推倒重来。1.3 载波频率不变的含义题目里的核心约束是“载波频率不变”。载波就是定时器的三角波向上计数模式载波频率等于定时器计数频率除以(ARR1)公式如下f_carrier f_timer / ((PSC 1) * (ARR 1))只要PSC和ARR写死载波频率就是恒定的。在这个前提下要改变正弦波频率直觉上有个矛盾载波周期固定一个正弦周期内的采样点数就会随输出频率变化而变化。比如载波10kHz输出50Hz时有200个点输出200Hz时只剩50个点。所以“每个正弦周期的点数不固定”正是这个方案的正常状态不要试图去把点数凑成整数否则你会发现频率一变载波也跟着变那就违背初衷了。2. 核心参数计算与方案设计2.1 定时器时钟与载波频率的设定以STM32F103C8T6为例主频72MHzTIM1挂在APB2上。如果APB2预分频为1那么TIM1的时钟就是72MHz。为了计算方便我把预分频PSC设为71这样计数时钟变成1MHz也就是每个计数周期1微秒。再设ARR为99那么一次完整的计数周期就是100微秒对应载波频率f_carrier 1MHz / (99 1) 10kHz这个10kHz不是随便选的。频率太低音频噪声明显变压器和滤波电感体积也会变大频率太高开关损耗和死区占比会上升。10kHz是很多逆变电源和电机驱动的常用折中值。如果你后续把载波提高到20kHz规避音频噪声PSC和ARR的对应关系也很好改。2.2 正弦表与相位累加器的设计这是整个方案的灵魂。我没有用实时调用sin()函数的方式因为在10kHz中断频率下每中断一次就算一次三角函数浮点运算会吃大量CPU时间还要处理编译器的数学库开销非常不划算。更稳的做法是预生成256点正弦表放在内存里中断里只做查表。具体到相位控制我用的是一个32位相位累加器思路和DDS直接数字频率合成完全一样每次进PWM更新中断相位累加器加上一个步进值phase_step。取累加器的高8位作为查表索引phase_acc 24对应256个点覆盖一个完整正弦周期。查表得到的值经过幅度缩放后直接写入TIM1的CCR寄存器设置当前脉冲宽度。相位累加器最大的优点是频率分辨率极高而且任意频率都能连续调节不存在除不尽、周期跳变的问题。步进值的计算如下phase_step (f_out / f_carrier) * 2^32以输出50Hz、载波10kHz为例phase_step (50 / 10000) * 4294967296 ≈ 21474836 ≈ 0x0147AE14每次进中断累加器都加这个值。累加器溢出一次正好对应一个完整的正弦周期。溢出是自然发生的不需要额外处理。2.3 幅度调节与调制比约束幅度调节相对简单思路是查表得到的是0到255的正弦值乘以一个幅度系数后右移8位得到对应的比较值。幅度系数amp_scale的范围是0到255对应调制比0%到100%。调制比M CCR / ARR这个比值不能超过1否则就是过调制输出波形会发生畸变。实际工程中更要留出余量因为死区会占用一部分脉宽。假设死区设置2微秒而PWM周期是100微秒那么死区占2%也就是说调制比最大只能走到大约98%。如果调制比拉满死区会把脉冲“吃掉”一部分波形畸变就很明显。有一个很容易被忽略的细节关闭输出时不要直接把CCR写到0这样输出会瞬间跌零对感性负载非常不友好。更平滑的做法是让幅度系数在中断里按照一定斜率逐步减小到0实现软关断。同理启动时也建议从0逐步升到目标幅度避免冲击电流。3. 基于HAL库的完整代码实现3.1 CubeMX侧的配置用STM32CubeMX生成工程时主要配置如下TIM1选择“Internal Clock”通道1选择“PWM Generation CH1”引脚自动分配到PA8。预分频器PSC设为71计数器模式选Up自动重载值ARR设为99重复计数器RCR保持0。PWM模式选PWM1脉冲Pulse初始值设为0输出极性选High。使能TIM1全局中断TIM1_UPDATE_IRQn也就是更新事件产生的中断。时钟源RCC用HSE或HSI都行注意最终确保APB2定时器时钟是72MHz否则PSC的计算会偏。如果你用的不是F103而是F407、F429这类芯片TIM1的时钟树路径可能不同记得先确认APB2预分频和定时器倍频后的实际数值再去算PSC和ARR不要直接照抄参数。3.2 全局变量与正弦表生成正弦表有几种方式生成可以用Python脚本算好粘贴到工程里也可以在main函数初始化阶段用浮点计算填充。对于256点表长不大我一般直接用初始化代码生成方便后续修改点数/* 全局变量 */ #define SPWM_SIN_TABLE_SIZE 256 #define SPWM_CARRIER_FREQ 10000.0f uint16_t sin_table[SPWM_SIN_TABLE_SIZE]; /* 0~255的正弦值 */ volatile uint32_t phase_acc 0; /* 相位累加器 */ volatile uint32_t phase_step 0; /* 频率步进 */ volatile uint16_t amp_scale 255; /* 幅度系数0~255 */ volatile uint16_t ccr_value 0; /* 每次更新后写入的CCR值 */初始化时生成正弦表for (int i 0; i SPWM_SIN_TABLE_SIZE; i) { sin_table[i] (uint16_t)((sinf(2 * 3.14159265f * i / SPWM_SIN_TABLE_SIZE) 1.0f) * 255.0f / 2.0f 0.5f); }注意这里生成的是单极性正弦表范围是0到255。因为本实验用PWM1模式占空比永远在0%到100%之间变化对应半桥或单管拓扑的单极性工作。如果是做全桥双极性调制查表结果需要再做一个偏置处理这个后面再说。3.3 设置频率和幅度的接口函数频率的设置接口需要把目标频率换算成phase_stepvoid spwm_set_freq(uint16_t freq_hz) { /* 相位步进 (f_out / f_carrier) * 2^32 */ phase_step (uint32_t)(((double)freq_hz / SPWM_CARRIER_FREQ) * 4294967296.0); }这里用了浮点计算但只在用户调节频率时调用一次中断里完全不涉及浮点所以性能不受影响。万一你的产品对代码体积敏感也可以用定点逼近的方式把一个周期的扇形系数写死成整数乘法效果同样够用。幅度调节接口更简单void spwm_set_amplitude(uint8_t percent) { if (percent 100) percent 100; amp_scale (uint16_t)(percent * 255 / 100); }这就是我说“频率和幅度解耦”的代码层面的体现频率只改phase_step幅度只改amp_scale两者互不干扰载波频率由ARR锁死。三层参数各管各的结构非常清晰。3.4 中断服务函数的核心逻辑TIM1更新中断里做的事情极少就四步清中断标志、相位累加、查表、写比较寄存器。void TIM1_UP_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim1, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim1, TIM_FLAG_UPDATE); /* 相位累加高8位作为正弦表索引 */ phase_acc phase_step; uint8_t index (uint8_t)(phase_acc 24); /* 查表并做幅度缩放 */ uint32_t ccr ((uint32_t)sin_table[index] * amp_scale) 8; /* 写入比较寄存器注意PWM1模式下CCR即脉冲宽度 */ __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, ccr); } }这里用“phase_acc 24”而不是“ 16”是因为正弦表只有256个点取32位累加器的高8位正好均匀映射到整个360度相位。如果你把正弦表扩到1024个点那就改成“ 22”让相位索引落到0到1023的范围逻辑是一样的。主循环里只需处理按键或串口指令调用上面的spwm_set_freq和spwm_set_amplitude即可不需要额外干预PWM波形。3.5 死区配置示例如果后面还要接全桥驱动TIM1的死区必须配置。在HAL库的初始化结构体里不用手动去算寄存器位直接设置sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; 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; htim1.DeadTime 20; /* 典型值单位取决于DTSCubeMX界面里可以直接看到实际时间 */DearTime设置成多少合适取决于功率管的开关特性和驱动电路。250ns到几个us都常见。设置太短起不到保护作用太长则输出波形畸变变大。我一般先按1us左右设再看温升和波形调整。注意一旦配置了死区调制比上限就需要重新评估前面说的“留出死区余量”绝对不能省。4. 常见问题与排查技巧实录4.1 高低温、频率切换时波形毛刺相位步进与定时器同步调试中我遇到过最典型的问题就是在切换频率的瞬间输出的正弦波会出现半个周期的畸形脉冲甚至偶尔多一两个窄脉冲。这是因为相位累加器的更新和PWM更新事件并不同步你改phase_step的那个瞬间如果刚好处于某个周期的中段下一个中断就用了新步进相当于相位发生了跳变。解决的办法是在定时器更新中断里切换步进值而不是在主循环里直接改全局变量。或者更简单一点维护一个target_step中断里每步逼近当前目标不仅在频率切换时平滑过渡还能避免相位跳变。电机驱动里这种“频率爬坡”是标配变频器说明书里那个加速时间底层实现就是干这个的。4.2 下载失败no stm32 target found很多人做完PWM实验之后重新下载程序时会遇到“error: no stm32 target found”这类提示特别是用了PA13、PA14、PA15、PB3、PB4这些引脚之后。他们没意识到这些引脚默认是SWD调试口和JTAG口如果你把其中的某个引脚复用成PWM或GPIO并且代码里执行了引脚完全的复用重映射调试器就再也连不上芯片了。解决思路是按照优先级来如果还能连上在CubeMX里把调试口选成Serial Wire重新生成代码覆盖配置。如果连不上但芯片在运行用BOOT0拉高上电后从系统存储器启动然后用ST-Link Utility或CubeProgrammer连接执行整片擦除再BOOT0拉低重新下载。如果是因为程序一上电就执行“禁用JTAG”导致的重连困难用“connect under reset”模式或者按住复位键点击下载的瞬间松开让调试器在复位窗口期内拿住内核。这个坑几乎每个玩STM32的人都会踩一次提前知道后面就不用靠拆Flash慢慢恢复。4.3 死区设置不当导致上下管直通如果你从单路PWM实验升级到半桥、全桥实战第一节课就是死区。用示波器看两个互补PWM波形放大边沿如果发现上升沿和下降沿存在重叠哪怕只有几十纳秒都不可以上功率。因为MOS管或IGBT在开关瞬间的导通阻抗下降是需要时间的重叠部分相当于短路。TIM1的互补输出本身就带硬件死区但前提是你要用CH1和CH1N这一对引脚并且配置了正确的极性和死区时间。测试时挂上假负载用电流探头盯着母线电流发现电流有毛刺尖峰首先怀疑的就是死区不够而不是滤波电感的问题。4.4 输出频率高时正弦波变形前面说过载波固定、频率升高会让一个正弦周期的采样点数减少。实测200Hz输出时50个点的阶梯感已经肉眼可见。这个不是bug而是PWM本身的量化效应。如果应用场景要求高输出频率下波形依然平滑有几个方向可以考虑提高载波频率比如从10kHz升到20kHz代价是开关损耗上升。扩正弦表点数比如从256点扩到1024点同时调整相位索引的提取位数。外部加一级低通滤波器把载波频率的纹波滤掉保留基波。实际工程没有完美的无条件方案都是在开关损耗、波形质量、器件成本三者之间做权衡。做之前先把需求里的最大输出频率定清楚再去反推载波频率需要多高比做出来再改要快得多。5. 实测效果与进阶扩展方向5.1 实验现象与关键点检查我把这套代码烧进STM32F103C8T6用一个RC低通滤波器比如10kohm电阻加1uF电容接在PA8上示波器观察滤波后的波形载波频率10kHz固定用频率计测PA8的开关周期稳定在100us不变。调节输出频率从50Hz到200Hz滤波后的正弦波周期相应变化载波纹波频率不变。调节幅度系数从0到100%正弦波峰峰值线性变化未出现顶部削平。比较关键的是查看占空比极限幅度设为100%时脉冲最宽处接近整个周期但仍保留死区占用的时间没有出现全开的饱和状态。这就是留出调制比余量的效果。5.2 DMA方案把中断开销降到零中断方式虽然简单但如果你的系统里还有别的实时任务10kHz的中断频率多少会占用CPU。进阶做法是预计算整周期的CCR序列存放在数组里用TIM1更新事件触发DMA循环搬运到TIM1的CCR寄存器CPU全程不参与单个PWM周期的更新。这种方案的优点很明显中断里什么都不用做CPU使用率几乎为零。但要注意RAM开销比如载波10kHz、输出50Hz时一个周期200个点每个点2字节需要400字节完全可以接受。但假如输出频率降到1Hz点数变成10000个就需要20KB字节很多小容量芯片就吃不消了。所以DMA方案适合输出频率不太低的固定频率段。5.3 从开环到闭环下一步能做什么做完这套频率、幅度可调的SPWM实际上你已经拥有了一个开环的可编程正弦电源的核心调制部分。后续可以接ADC采样输出电压用PID闭环控制幅度稳定性也可以加入过流保护接入TIM1的刹车引脚实现硬件封波甚至可以把查表的数据源改成任意波形表去输出三角波、梯形波、自定义谐波波形。我现在手上这套方案就是把相位累加器那套逻辑保留然后在闭环部分加了一个平均电压环实测下来负载突变时电压跌落能控制在几个百分点内。基于这套基础去做各种电源类项目确实省了不少底层功夫。本文还有配套的精品资源点击获取