STM32 TIM1实现频率与幅度独立可调SPWM的设计与实现
2026/9/9 22:36:38 网站建设 项目流程

简介: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 载波频率不变的含义

题目里的核心约束是“载波频率不变”。载波就是定时器的三角波(向上计数模式),载波频率等于定时器计数频率除以(ARR+1),公式如下:

f_carrier = f_timer / ((PSC + 1) * (ARR + 1))

只要PSC和ARR写死,载波频率就是恒定的。在这个前提下要改变正弦波频率,直觉上有个矛盾:载波周期固定,一个正弦周期内的采样点数就会随输出频率变化而变化。比如载波10kHz,输出50Hz时有200个点;输出200Hz时只剩50个点。所以“每个正弦周期的点数不固定”正是这个方案的正常状态,不要试图去把点数凑成整数,否则你会发现频率一变,载波也跟着变,那就违背初衷了。

2. 核心参数计算与方案设计

2.1 定时器时钟与载波频率的设定

以STM32F103C8T6为例,主频72MHz,TIM1挂在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_step:

void 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; /* 典型值,单位取决于DTS,CubeMX界面里可以直接看到实际时间 */

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的刹车引脚实现硬件封波;甚至可以把查表的数据源改成任意波形表,去输出三角波、梯形波、自定义谐波波形。

我现在手上这套方案,就是把相位累加器那套逻辑保留,然后在闭环部分加了一个平均电压环,实测下来负载突变时电压跌落能控制在几个百分点内。基于这套基础去做各种电源类项目,确实省了不少底层功夫。

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

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

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

立即咨询