当前位置: 首页 > news >正文

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波是幅值不变、宽度变化的方波,它包含:

  1. 我们需要的低频正弦分量(1kHz)。
  2. 高频的载波分量及其边带谐波(比如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 = 1),TIM1的时钟就是72MHz。记下这个频率,后面计算PWM频率要用。

3.2 定时器PWM模式配置

  1. 在Pinout视图,找到TIM1,展开其通道,选择CH1PWM Generation CH1。这会在PA8引脚上产生PWM输出。
  2. 进入Configuration->TIM1进行参数设置。
    • Prescaler(预分频器):设置为71。定时器时钟是72MHz,经过(71+1)=72分频,得到1MHz的计数时钟。这样计数器每微秒计数一次,方便计算。
    • Counter Mode(计数模式):选择Center Aligned 1(中心对齐模式1)。这是关键!中心对齐模式下,计数器先向上计数到自动重载值(ARR),然后向下计数到0。PWM输出更加对称,谐波特性更好,非常适合用于生成SPWM。而且,它会在计数器向上和向下计数时都触发比较事件,这意味着我们可以在一个PWM周期内更新两次占空比数据,理论上可以使等效的载波频率翻倍,或者用同样的中断频率获得更平滑的正弦波。
    • Counter Period(自动重载值ARR):这是我们调节PWM频率的主要参数。PWM频率的计算公式为:Fpwm = Ftim_clk / ((ARR + 1) * 分频比)。对于中心对齐模式,公式稍有不同,但CubeMX会直接显示计算结果。我们先设为999,即ARR=999。此时,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 计算实际输出频率

现在我们来算一下实际输出的正弦波频率是多少。

  • 定时器更新中断的频率 = 定时器时钟 / ((PSC+1) * (ARR+1))? 这里要注意,对于中心对齐模式,计数器要经历从0->ARR->0一个完整周期才产生一次更新事件。所以更新事件的频率是:F_update = Ftim_clk / ((PSC+1) * (ARR+1) * 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载波频率和正弦波生成频率。ARR=999决定了PWM的周期是1000个计数时钟(1ms,即1kHz),这个频率对于后续滤波来说太低了(仅比目标正弦波频率高一点,根本无法有效滤波)。我们需要大幅提高PWM的载波频率。

核心调整:我们的目标是生成1kHz正弦波,载波频率至少需要20kHz。因此,我们需要重新设定ARR值,让PWM基频(载频)在20kHz以上。同时,我们需要提高定时器更新中断的频率,使其能够驱动足够多的点(比如64点)来合成1kHz的正弦波。

4.4 参数重新设计与优化

让我们重新设定目标:

  1. 期望正弦波频率 Fsine = 1kHz
  2. 期望载波频率 Fcarrier ≥ 20kHz
  3. 正弦表点数 N = 64
  4. 定时器更新中断频率 Fupdate = Fsine * N = 1kHz * 64 = 64kHz。这是为了每秒能输出64k个点,组成1000个完整的正弦波。

计算步骤:

  • 步骤一:确定ARR值以满足载波频率。Fcarrier = Ftim_clk / ((PSC+1) * (ARR+1))(边沿对齐模式公式,中心对齐频率是它的一半,我们先按边沿对齐算,取更保守的值)。为了留有余地,我们设Fcarrier_target = 40kHz
    • ARR = Ftim_clk / (Fcarrier_target * (PSC+1)) - 1 = 72MHz / (40kHz * 72) - 1 ≈ 25 - 1 = 24
    • 取整,设ARR = 24。此时实际载波频率Fcarrier = 72MHz / (72 * 25) = 40kHz(边沿对齐)。在中心对齐模式下,PWM频率为20kHz,仍然满足 >20kHz 的要求。
  • 步骤二:验证在此ARR下,能否实现64kHz的更新中断频率。中心对齐模式的更新中断频率公式为:Fupdate = Ftim_clk / ((PSC+1) * (ARR+1) * 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的要求是冲突的。解决方案有两种:

  1. 降低正弦表点数:例如改用32点或16点。这样在20kHz更新频率下,可以生成20kHz / 32 = 625Hz20kHz / 16 = 1250Hz的正弦波。对于1kHz目标,16点表勉强可用,但波形失真会比较大。
  2. 使用定时器的DMA或触发输出模式:这是更专业的方法。可以预先将整个正弦表通过DMA循环搬运到定时器的比较寄存器,完全不需要CPU中断干预。这样,更新速率可以达到定时器时钟的极高频率,不受中断处理能力限制。但配置更为复杂。

为了简化,本例我们采用折中方案:使用32点正弦表,目标生成约625Hz正弦波,验证SPWM生成和滤波的基本流程。理解了原理后,你可以通过提高主频(如果芯片支持)、使用更高性能的定时器(如STM32G4系列的高级定时器)、或者采用DMA方式来逼近1kHz的目标。

调整CubeMX配置

  • 将TIM1的Counter Period (ARR)修改为24
  • 重新生成代码,并更新正弦表生成脚本中的MAX_VAL24(ARR值)。
  • 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
  • 选择最接近的标准电容值,例如82nF(0.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的载波没有被完全滤除。可能的原因:
    1. 截止频率还是太高:尝试将截止频率降低。例如,将电阻改为2.2kΩ,电容改为100nF,此时Fc ≈ 723 Hz。重新测量,你会发现毛刺少了很多,但正弦波幅度也进一步下降了。这就是之前提到的权衡。
    2. 单级滤波不够:可以考虑使用两级RC滤波(两个电阻和两个电容组成)。两级滤波的滚降斜率是-40dB/十倍频,比一级的-20dB/十倍频更陡峭,滤波效果更好。设计时,每级的截止频率可以设为相同,比如都是2kHz,或者第二级稍低一些。
    3. 负载效应:示波器探头本身有输入阻抗(通常是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.65V(3.3V/2)对称。如果不是,检查正弦表生成算法和__HAL_TIM_SET_COMPARE设置的值是否在0到ARR之间。

6.3 追求更高的正弦波频率(如1kHz)

如前所述,受限于定时器中断频率,用查询法很难在保持高载波频率和高点数的情况下生成高频正弦波。解决方案包括:

  1. 换用更高主频的MCU:如STM32F4、H7系列,主频可达几百MHz,能大幅提升定时器时钟和中断处理能力。
  2. 使用定时器的PWM DMA模式:这是最推荐的方法。以STM32F1为例,虽然TIM1的DMA功能可能不如高级系列强大,但依然可以尝试。配置DMA循环模式,将正弦表首地址指向TIM1->CCR1(通道1比较寄存器)。在CubeMX中配置DMA请求来自TIM1_CH1,方向为存储器到外设。这样,每次定时器更新事件(或比较匹配事件)会自动触发DMA搬运,无需中断。
  3. 使用芯片内置的DAC:如果只是要产生正弦波,且对功率没有要求,使用DAC直接输出是更简单纯净的方案。可以结合DMA和定时器触发,同样能产生高质量波形,且无需外部滤波。但DAC的输出驱动能力通常较弱。

6.4 测量与验证技巧

  • 观察SPWM原始波形:在连接滤波器之前,先用示波器看PA8引脚的原始输出。你应该能看到一个PWM波,其脉冲宽度在缓慢地、周期性地变化。打开示波器的测量功能,测量其频率,应该等于你计算的载波频率(20kHz左右)。
  • 使用示波器的FFT功能:这是分析滤波器效果的利器。在滤波器的输出端,打开FFT。你会看到一个主要的尖峰在625Hz(基波),在20kHz、40kHz等处应该还有较小的尖峰(谐波)。对比滤波前后,这些高频谐波的幅度衰减了多少dB,可以直观地评价滤波器的性能。一个好的滤波器应该让20kHz处的谐波幅度比基波低40dB以上。
  • 负载测试:如果你的正弦波需要驱动后续电路(如运放、功率放大器),一定要在带负载的情况下测试波形。负载可能会拉低输出幅度,甚至影响滤波特性。
http://www.jsqmd.com/news/1284624/

相关文章:

  • 协同过滤算法全解析:从核心原理到工程实践与优化策略
  • 嵌入式OLED显示实战:从驱动到波形与菜单的完整实现
  • 冷库维护常见问题与专业解决方案
  • 074、LLC谐振变换器的励磁电感设计
  • 影子寄存器:改ARR下个周期才生效,防波形“断片”
  • 2026年7月鹤壁全屋定制/汤阴橱柜定制厂家推荐评估_汤阴务美全屋定制 - 行业平台推荐
  • Play Integrity API Checker:Android设备完整性检测的终极实战指南
  • Allegro DRC检查哪些必须修改
  • Apache Doris实战:部署调优与生产运维指南
  • 大模型应用开发实战:从提示词工程到企业级部署全流程
  • 2026 年现阶段徐汇比较好的设备机房吸音铝扣板直销厂家深度解析与优选指南,你敢信机房里装这玩意儿,居然能同时解决噪音超标和设备散热大难题? - 企业信息推荐【官方】
  • 使用Anaconda3搭建TensorFlow 1.15.0环境:从虚拟环境创建到GPU配置全攻略
  • 如何用Python免费备份你的QQ空间历史说说?完整指南来了!
  • 本地优先的智能体运行时优势解析
  • 如何快速配置SPT-AKI存档编辑器:新手完整教程与游戏修改指南
  • 2026黄鹤杯杯网络安全人才创新大赛学生组(失序货栈)
  • 一个 Agent 看完 30 个文件后,为什么该让它先退出?
  • 2026年7月嘉兴汽车维修厂/宝马汽车维修厂怎么选_嘉兴市洪合镇洪运汽车修理厂 - 行业平台推荐
  • [C++17/std::any] + [彻底封印 void* 黑色死角与内存崩溃/解密 SOO 虚表类型擦除] + [高并发事件总线与泛型框架安全演进]
  • IIR滤波器设计:从原理到Matlab实现与工程部署
  • Warcraft Helper终极优化指南:5个简单步骤让魔兽争霸3在Win10/Win11上流畅运行
  • GPT-4应用安全实战:Python构建提示词注入攻击防御体系
  • 2026年7月双月湾抖音团购运营/惠东海边本地生活代运营公司如何选_惠州市乘数科技网络有限责任公司 - 行业平台推荐
  • 2026年7月热门的餐饮运营培训公司哪个靠谱,中小餐饮门店/餐饮创业/餐饮/餐饮小吃/餐饮爆店,餐饮培训机构怎么选择 - 品牌推荐师
  • GPT5.6 Sol满血版付费使用教程:从配置到项目集成全流程
  • 2026本地AI与代码工具开源生态及实战指南
  • 淘天二面追问:Agentic RAG和普通RAG差在哪?不是加个智能体那么简单
  • Streamlit入门指南:快速构建数据科学Web应用的核心原理与实践
  • LS-DYNA显式动力学分析中负体积错误诊断与系统性解决方案
  • Linux线程实现原理:从task_struct、LWP到pthread完整解析