STM32实现SPWM生成正弦波:从原理到硬件滤波实战
1. 项目缘起为什么用STM32做SPWM最近在做一个需要精密交流信号源的小项目核心需求是生成一个1kHz、波形质量不错的正弦波。市面上专用的信号发生器模块要么太贵要么频率、幅度调整不够灵活。手头正好有块STM32F103的开发板就琢磨着能不能用它的定时器和DAC或者PWM加滤波来搞定。一番调研下来发现用SPWM正弦脉宽调制配合RC滤波是一个在嵌入式领域非常经典且成本极低的方案。这个方案的思路很直接单片机本身很难直接输出模拟量但它擅长输出高精度的数字PWM波。SPWM的本质就是让PWM波的占空比按照正弦函数的规律变化。当这个高频的PWM波通过一个低通滤波器后高频的开关分量被滤除剩下的就是其占空比包络所对应的平均电压也就是我们想要的正弦波了。STM32的定时器功能强大用HAL库和CubeMX配置起来非常直观能快速搭建出SPWM的生成框架。而最终波形质量的好坏关键就落在了“滤波”这一步上。网上很多教程只讲到生成SPWM对滤波电路的设计和参数计算往往一笔带过导致实际做出来波形毛刺多、失真大。这次我就把从CubeMX配置到硬件滤波调试的完整过程特别是那些容易踩坑的细节彻底梳理一遍。2. 核心原理拆解从PWM到正弦波的魔法在开始配置之前我们必须先搞清楚SPWM是怎么变出正弦波的。这不是玄学其数学基础是面积等效原理冲量相等而形状不同的窄脉冲加在具有惯性的环节上其效果基本相同。说得直白点对于一个低通滤波器比如RC电路它反应慢跟不上PWM快速的变化只能感受到一段时间内电压的平均值。如果我们能控制PWM在一个周期内高电平的“面积”即脉冲宽度随时间呈正弦变化那么滤波器输出端得到的平均电压也会是正弦变化的。2.1 SPWM的生成方法查表法对于单片机来说最常用的方法是“查表法”。我们预先计算好一个正弦函数周期内各个相位点所对应的PWM占空比值把这些值存成一个数组正弦表。程序运行时定时器以固定的频率载波频率触发中断在每次中断中按顺序从表中取出下一个占空比值更新到定时器的比较寄存器中。这样PWM输出的占空比就会循环地、阶梯状地逼近正弦波。这里有几个关键参数决定了生成波形的质量正弦表点数N把一个正弦波周期分成多少份。点数越多生成的阶梯波越平滑经过滤波后的正弦波失真越小。但点数越多对定时器中断频率的要求也越高消耗的CPU资源和内存也越多。对于1kHz正弦波通常取64点或128点已经能获得很好的效果。载波频率Fc即PWM本身的频率。它必须远高于我们想要的正弦波频率1kHz这样低通滤波器才能有效滤除PWM开关噪声。通常要求载波频率至少是基波频率的20倍以上对于1kHz载波频率选择20kHz-50kHz是比较合适的起点。载波频率也决定了定时器中断的频率因为每个PWM周期我们至少要更新一次占空比实际上在中心对齐模式下一个PWM周期会触发两次中断事件这点后面会细说。调制比M正弦表幅值与计数器最大值之比。它决定了输出正弦波的幅度。调制比不能超过1否则会导致过调制波形顶部会被削平。2.2 滤波器的角色与挑战SPWM波是幅值不变、宽度变化的方波它包含我们需要的低频正弦分量1kHz。高频的载波分量及其边带谐波比如20kHz, 40kHz等。RC低通滤波器的任务就是尽可能无衰减地通过1kHz的信号同时极大地衰减20kHz及以上的噪声。滤波器的截止频率-3dB点需要精心设计。如果截止频率设得太高比如10kHz那么高频噪声滤不干净输出正弦波上会有明显的锯齿或毛刺。如果截止频率设得太低比如500Hz虽然滤得干净但我们想要的1kHz正弦波本身也会被严重衰减导致输出幅度大幅下降。因此滤波器的设计是在“波形纯度”和“信号幅度”之间寻找最佳平衡点。这也是本项目调试的核心和难点所在。3. CubeMX工程配置详解我们以STM32F103C8T6蓝色pill开发板为例使用TIM1的通道1产生PWM并通过一个简单的RC滤波器输出。3.1 时钟树配置这是确保定时器计数频率准确的基础。在RCC设置中选择外部高速晶振HSE。然后进入Clock Configuration标签页。将HCLK系统时钟设置为72MHz这是F103的常见最高值。找到APB2定时器时钟APB2 Timer Clocks。因为TIM1挂载在APB2总线上当APB2预分频器不为1时它的时钟会倍频。在我们默认配置下APB2 prescaler 1TIM1的时钟就是72MHz。记下这个频率后面计算PWM频率要用。3.2 定时器PWM模式配置在Pinout视图找到TIM1展开其通道选择CH1为PWM Generation CH1。这会在PA8引脚上产生PWM输出。进入Configuration-TIM1进行参数设置。Prescaler预分频器设置为71。定时器时钟是72MHz经过71172分频得到1MHz的计数时钟。这样计数器每微秒计数一次方便计算。Counter Mode计数模式选择Center Aligned 1中心对齐模式1。这是关键中心对齐模式下计数器先向上计数到自动重载值ARR然后向下计数到0。PWM输出更加对称谐波特性更好非常适合用于生成SPWM。而且它会在计数器向上和向下计数时都触发比较事件这意味着我们可以在一个PWM周期内更新两次占空比数据理论上可以使等效的载波频率翻倍或者用同样的中断频率获得更平滑的正弦波。Counter Period自动重载值ARR这是我们调节PWM频率的主要参数。PWM频率的计算公式为Fpwm Ftim_clk / ((ARR 1) * 分频比)。对于中心对齐模式公式稍有不同但CubeMX会直接显示计算结果。我们先设为999即ARR999。此时Fpwm 1MHz / 1000 1kHz。注意这是PWM的基频但我们最终要通过查表法让这个基频“抖动”起来形成SPWM的载波。实际上ARR值决定了PWM的计数分辨率更大的ARR值意味着我们可以用更精细的阶梯正弦表值去逼近正弦波。Pulse初始脉冲值先设为0。CH Polarity通道极性保持默认High即可。在NVIC Settings中使能TIM1的更新中断TIM1 update interrupt。这是我们执行查表更新操作的地方。3.3 生成代码与基础框架点击GENERATE CODE生成工程。在生成的tim.c文件中我们找到HAL_TIM_PWM_MspInit函数可以看到中断已经配置好。在main.c中我们需要启动PWM和定时器更新中断。// 在main函数初始化部分后启动 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 启动PWM输出 HAL_TIM_Base_Start_IT(htim1); // 启动定时器基中断更新中断4. 软件实现正弦表与中断服务程序4.1 生成正弦表我们使用一个简单的Python脚本或任何计算工具来生成正弦表。这里以64点为例。import math POINTS 64 # 正弦表点数 MAX_VAL 1000 # 对应定时器的ARR值这里为999我们取1000便于计算 AMPLITUDE 0.8 # 调制比0.8避免过调制 sine_table [] for i in range(POINTS): # 计算0到2π之间的正弦值范围[-1, 1] radian 2 * math.pi * i / POINTS sine_value math.sin(radian) # 将正弦值映射到PWM比较寄存器值并考虑调制比 # 先归一化到[0, 1]: (sine_value 1) / 2 # 再乘以幅度和最大值 pwm_value int(((sine_value 1) / 2) * AMPLITUDE * MAX_VAL) # 确保值在[0, ARR]范围内 pwm_value max(0, min(pwm_value, MAX_VAL)) sine_table.append(pwm_value) print(const uint16_t sine_table[%d] { % POINTS) for i, val in enumerate(sine_table): if i % 8 0: print( , end) print(%4d, % val, end) if i % 8 7 or i len(sine_table)-1: print() print(};)将生成的数组复制到main.c或一个头文件中。4.2 实现更新中断回调函数定时器更新中断发生时我们需要更新比较寄存器的值。这里有一个非常重要的细节在中心对齐模式下为了避免在计数器正在与旧比较值匹配时更新寄存器导致输出异常通常会在计数器为0时即更新事件发生时进行更新。HAL库的中断回调函数HAL_TIM_PeriodElapsedCallback正是在此时被调用。// 在main.c的用户代码区定义全局变量 uint16_t sine_index 0; #define SINE_TABLE_SIZE 64 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { // 更新TIM1通道1的比较值占空比 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, sine_table[sine_index]); // 更新索引指向下一个点 sine_index; if (sine_index SINE_TABLE_SIZE) { sine_index 0; } } }4.3 计算实际输出频率现在我们来算一下实际输出的正弦波频率是多少。定时器更新中断的频率 定时器时钟 / ((PSC1) * (ARR1)) 这里要注意对于中心对齐模式计数器要经历从0-ARR-0一个完整周期才产生一次更新事件。所以更新事件的频率是F_update Ftim_clk / ((PSC1) * (ARR1) * 2)。代入我们的值F_update 72MHz / (72 * 1000 * 2) 72,000,000 / 144,000 500 Hz。这意味着我们每秒能更新500次占空比。而我们一个完整的正弦波有64个点。所以最终输出的正弦波频率为F_sine F_update / SINE_TABLE_SIZE 500 Hz / 64 ≈ 7.81 Hz。这离我们目标的1kHz差得太远了问题出在哪在于我们混淆了PWM载波频率和正弦波生成频率。ARR999决定了PWM的周期是1000个计数时钟1ms即1kHz这个频率对于后续滤波来说太低了仅比目标正弦波频率高一点根本无法有效滤波。我们需要大幅提高PWM的载波频率。核心调整我们的目标是生成1kHz正弦波载波频率至少需要20kHz。因此我们需要重新设定ARR值让PWM基频载频在20kHz以上。同时我们需要提高定时器更新中断的频率使其能够驱动足够多的点比如64点来合成1kHz的正弦波。4.4 参数重新设计与优化让我们重新设定目标期望正弦波频率 Fsine 1kHz。期望载波频率 Fcarrier ≥ 20kHz。正弦表点数 N 64。定时器更新中断频率 Fupdate Fsine * N 1kHz * 64 64kHz。这是为了每秒能输出64k个点组成1000个完整的正弦波。计算步骤步骤一确定ARR值以满足载波频率。Fcarrier Ftim_clk / ((PSC1) * (ARR1))边沿对齐模式公式中心对齐频率是它的一半我们先按边沿对齐算取更保守的值。为了留有余地我们设Fcarrier_target 40kHz。ARR Ftim_clk / (Fcarrier_target * (PSC1)) - 1 72MHz / (40kHz * 72) - 1 ≈ 25 - 1 24。取整设ARR 24。此时实际载波频率Fcarrier 72MHz / (72 * 25) 40kHz边沿对齐。在中心对齐模式下PWM频率为20kHz仍然满足 20kHz 的要求。步骤二验证在此ARR下能否实现64kHz的更新中断频率。中心对齐模式的更新中断频率公式为Fupdate Ftim_clk / ((PSC1) * (ARR1) * 2)。Fupdate 72MHz / (72 * 25 * 2) 72,000,000 / 3600 20,000 Hz。这只有20kHz远达不到我们需要的64kHz。这意味着我们无法在每次PWM周期都更新一个正弦表点。如果我们坚持用64点表那么能生成的正弦波频率最高为Fupdate / N 20kHz / 64 312.5 Hz。矛盾出现了高载波频率需要小的ARR和高正弦波分辨率/频率需要高的更新中断频率对ARR的要求是冲突的。解决方案有两种降低正弦表点数例如改用32点或16点。这样在20kHz更新频率下可以生成20kHz / 32 625Hz或20kHz / 16 1250Hz的正弦波。对于1kHz目标16点表勉强可用但波形失真会比较大。使用定时器的DMA或触发输出模式这是更专业的方法。可以预先将整个正弦表通过DMA循环搬运到定时器的比较寄存器完全不需要CPU中断干预。这样更新速率可以达到定时器时钟的极高频率不受中断处理能力限制。但配置更为复杂。为了简化本例我们采用折中方案使用32点正弦表目标生成约625Hz正弦波验证SPWM生成和滤波的基本流程。理解了原理后你可以通过提高主频如果芯片支持、使用更高性能的定时器如STM32G4系列的高级定时器、或者采用DMA方式来逼近1kHz的目标。调整CubeMX配置将TIM1的Counter Period (ARR)修改为24。重新生成代码并更新正弦表生成脚本中的MAX_VAL为24ARR值。将SINE_TABLE_SIZE改为32重新生成正弦表。此时理论计算PWM载波频率中心对齐Fcarrier_center 72MHz / (72 * 25 * 2) 20kHz。定时器更新中断频率Fupdate 20kHz中心对齐模式下一次完整计数周期产生一次更新中断。输出正弦波频率Fsine Fupdate / 32 20kHz / 32 625 Hz。5. 硬件滤波电路设计与调试这是将数字SPWM变为干净模拟正弦波的关键一步。我们使用最经典的无源RC低通滤波器。5.1 滤波器参数计算RC低通滤波器的截止频率公式为Fc 1 / (2πRC)。我们的目标是通过625Hz的正弦波滤除20kHz的载波及其谐波。需要在625Hz和20kHz之间选择一个合适的截止频率。如果选得太靠近625Hz正弦波会有衰减选得太靠近20kHz滤波效果不佳。通常取载波频率的1/10到1/20作为截止频率能取得较好的效果。我们取Fc_target 2kHz。选择常见的电阻值例如R 1kΩ。计算电容值C 1 / (2π * Fc * R) 1 / (2 * 3.1416 * 2000 * 1000) ≈ 79.6 nF。选择最接近的标准电容值例如82nF0.082uF。此时实际截止频率Fc 1 / (2π * 1000 * 82e-9) ≈ 1940 Hz。5.2 电路搭建与实测问题按照计算在PA8引脚和地之间串联一个1kΩ电阻然后在电阻后输出端对地接一个82nF的电容就构成了一个简单的RC低通滤波器。用示波器探头连接到滤波器的输出端电容两端。你可能会看到以下情况现象A波形幅度非常小。这是因为RC滤波器在截止频率附近对信号已有衰减。我们的正弦波频率625Hz滤波器截止频率约1.94kHz衰减不算大但PWM的占空比平均电压范围是0到3.3V假设单片机供电为3.3V经过调制正弦波的峰值幅度会小于1.65V。实测可能在1Vpp左右。如果需要更大电压可以在滤波后加一级运算放大器进行放大。现象B波形上有明显的高频毛刺。这说明20kHz的载波没有被完全滤除。可能的原因截止频率还是太高尝试将截止频率降低。例如将电阻改为2.2kΩ电容改为100nF此时Fc ≈ 723 Hz。重新测量你会发现毛刺少了很多但正弦波幅度也进一步下降了。这就是之前提到的权衡。单级滤波不够可以考虑使用两级RC滤波两个电阻和两个电容组成。两级滤波的滚降斜率是-40dB/十倍频比一级的-20dB/十倍频更陡峭滤波效果更好。设计时每级的截止频率可以设为相同比如都是2kHz或者第二级稍低一些。负载效应示波器探头本身有输入阻抗通常是1MΩ或10MΩ和输入电容通常十几pF。这个负载并联在你的滤波电容上会改变实际的滤波特性。对于高阻抗节点探头电容的影响尤其明显。5.3 进阶优化使用有源滤波器当对波形质量要求较高时无源RC滤波器可能力不从心。可以考虑使用运算放大器搭建有源低通滤波器例如Sallen-Key或多重反馈MFB拓扑。有源滤波器可以提供更陡峭的衰减斜率、更好的带内平坦度并且由于运放的高输入阻抗和低输出阻抗可以隔离前后级避免负载影响。例如一个二阶Sallen-Key低通滤波器可以轻松实现-40dB/十倍频的衰减在20kHz处提供远超无源RC电路的抑制能力。实操心得在面包板上搭建RC滤波器时元件布局和引线长度非常关键。尽量让电阻和电容的引脚剪短紧靠在一起并且靠近单片机的输出引脚。过长的引线会引入额外的寄生电感和电容影响高频滤波效果甚至可能形成天线辐射噪声。如果条件允许直接在设计PCB时集成滤波电路是最好选择。6. 关键问题排查与性能提升思路在实际操作中你可能会遇到一些意想不到的问题。6.1 输出波形频率不准或抖动检查时钟源确保系统时钟HSE准确。如果使用内部RC振荡器HSI精度较差可能导致频率漂移。检查中断响应时间如果主程序中还有其他高优先级中断或耗时任务可能会延迟定时器中断的响应导致更新占空比的时刻不准确引起波形抖动。确保SPWM更新中断具有足够高的优先级。使用DMA这是消除中断延迟影响、获得最稳定波形的终极方案。将正弦表配置到DMA循环模式让DMA自动在定时器更新事件时将下一个数据搬运到比较寄存器完全解放CPU精度和稳定性极高。6.2 滤波器输出有直流偏置理想的SPWM波形其占空比在0%到100%之间对称变化平均电压会在电源电压的一半上下波动。但如果你的正弦表计算映射时最小值不是0或者PWM极性配置问题可能导致输出有一个固定的直流偏移。用示波器的直流耦合档测量观察正弦波是否关于1.65V3.3V/2对称。如果不是检查正弦表生成算法和__HAL_TIM_SET_COMPARE设置的值是否在0到ARR之间。6.3 追求更高的正弦波频率如1kHz如前所述受限于定时器中断频率用查询法很难在保持高载波频率和高点数的情况下生成高频正弦波。解决方案包括换用更高主频的MCU如STM32F4、H7系列主频可达几百MHz能大幅提升定时器时钟和中断处理能力。使用定时器的PWM DMA模式这是最推荐的方法。以STM32F1为例虽然TIM1的DMA功能可能不如高级系列强大但依然可以尝试。配置DMA循环模式将正弦表首地址指向TIM1-CCR1通道1比较寄存器。在CubeMX中配置DMA请求来自TIM1_CH1方向为存储器到外设。这样每次定时器更新事件或比较匹配事件会自动触发DMA搬运无需中断。使用芯片内置的DAC如果只是要产生正弦波且对功率没有要求使用DAC直接输出是更简单纯净的方案。可以结合DMA和定时器触发同样能产生高质量波形且无需外部滤波。但DAC的输出驱动能力通常较弱。6.4 测量与验证技巧观察SPWM原始波形在连接滤波器之前先用示波器看PA8引脚的原始输出。你应该能看到一个PWM波其脉冲宽度在缓慢地、周期性地变化。打开示波器的测量功能测量其频率应该等于你计算的载波频率20kHz左右。使用示波器的FFT功能这是分析滤波器效果的利器。在滤波器的输出端打开FFT。你会看到一个主要的尖峰在625Hz基波在20kHz、40kHz等处应该还有较小的尖峰谐波。对比滤波前后这些高频谐波的幅度衰减了多少dB可以直观地评价滤波器的性能。一个好的滤波器应该让20kHz处的谐波幅度比基波低40dB以上。负载测试如果你的正弦波需要驱动后续电路如运放、功率放大器一定要在带负载的情况下测试波形。负载可能会拉低输出幅度甚至影响滤波特性。
