
1. 项目概述与核心价值最近在做一个需要精密交流信号源的小项目手头正好有块STM32F103的开发板就琢磨着能不能直接用MCU生成一个干净的正弦波。直接上DAC输出阶梯波当然可以但想要波形质量好、频率稳定还得上SPWM正弦脉宽调制配合滤波的方案。这听起来有点复杂但用上ST官方力推的HAL库和图形化配置工具CubeMX整个过程其实可以变得相当清晰和高效。最终我成功实现了一个基于定时器输出SPWM波经过简单LC滤波后在负载上得到1kHz、失真度较低的正弦波的方案。这套方法不仅适用于信号发生器在逆变器、电机驱动、音频D/A转换等需要高质量正弦波或可变频交流输出的场景里都是非常核心的技术。如果你也在为如何用STM32产生“像样”的正弦波而头疼或者对SPWM的原理和HAL库下的实现感到好奇那么这篇从原理到配置、再到代码调试的完整记录应该能给你提供一个可以直接“抄作业”的参考。我会重点拆解为什么选SPWM、CubeMX里那些关键参数到底该怎么设、以及如何用最少的代码让定时器“吐”出我们想要的波形。2. 核心思路与方案选型为什么是SPWM在决定用STM32产生正弦波时我们通常有几个备选方案直接DAC输出、PWM滤波、以及SPWM滤波。每种方案都有其适用的场景和优缺点。直接DAC输出是最直观的。STM32内部的DAC可以设置成直接输出一个电压值我们只需要在定时器中断里按照正弦函数计算下一个点的电压并刷新DAC寄存器即可。这种方法简单但输出频率受限于中断速度和DAC的建立时间。对于1kHz的正弦波一个周期至少需要几十个点才能保证波形不太畸形这意味着中断频率要达到几十kHz。在F103这类主频72MHz的芯片上虽然能做到但会消耗可观的CPU资源。更关键的是DAC输出的信号是阶梯状的包含了丰富的高次谐波即使后面接滤波电路也很难完全滤除靠近基频的谐波导致波形在过零点附近有畸变。PWM滤波是另一种思路。我们产生一个固定占空比的PWM方波经过低通滤波器后可以得到一个直流电压其值等于PWM的占空比乘以电源电压。如果想得到交流信号就需要动态地改变这个占空比。这就是SPWMSinusoidal Pulse Width Modulation的核心思想用一个正弦波作为调制波一个高频三角波作为载波通过比较两者的瞬时值来产生一系列脉冲宽度按正弦规律变化的PWM波。这个PWM波经过一个截止频率介于载波频率和正弦波频率之间的低通滤波器后高频的PWM载波成分被滤除剩下的就是幅值和频率与调制波一致的正弦信号。选择SPWM方案我主要基于以下几点考量波形质量高SPWM的本质是“能量等效”理论上只要载波频率足够高滤波后得到的正弦波谐波失真THD可以做得非常低。这对于要求信号纯净度的应用至关重要。效率与驱动能力强SPWM信号可以直接驱动半桥或全桥的功率开关管如MOSFET、IGBT经过LC滤波后给负载供电效率远高于线性放大方案。这对于逆变器、电机驱动等功率应用是唯一可行的选择。充分利用硬件STM32的定时器高级功能如PWM输出、互补输出、带死区插入等就是为这类应用设计的。使用硬件定时器产生PWM不占用CPU计算资源CPU只需要在必要时如需要改变频率或幅值时更新一下比较寄存器CCR的值即可。灵活性好通过软件改变正弦表数据或调制比可以轻松实现输出电压幅值、频率的调节甚至实现变频调速VFD等功能。因此对于目标“输出1kHz正弦波”并且考虑到可能的扩展性如调压、变频SPWM滤波方案是一个在性能、资源占用和灵活性上取得很好平衡的选择。3. 硬件设计与关键参数计算在动手配置软件之前必须先明确硬件电路因为滤波器的参数直接决定了载波频率即PWM频率的选择。3.1 系统框图与电路原理整个系统的信号链非常清晰STM32定时器 - SPWM波 (PIN) - 低通滤波器 - 纯正弦波 (负载)STM32使用一个高级定时器如TIM1或TIM8的一个通道产生中心对齐的PWM波。滤波电路采用二阶LC低通滤波器。相比一阶RC滤波器二阶LC滤波器具有更陡峭的滚降特性能更有效地滤除载波频率及其谐波。一个典型的电路是在PWM输出引脚后串联一个电感L再并联一个电容C到地负载接在电容两端。负载可能是示波器、后续电路或者一个电阻。负载阻抗会影响滤波器的实际截止频率。3.2 关键参数计算载波频率与滤波器设计这是整个项目的核心计算部分参数选择是否合理直接决定最终波形的质量。1. 载波频率PWM频率的选择载波频率Fc需要远高于我们想要得到的正弦波频率Fsin这里是1kHz。通常Fc至少是Fsin的20倍以上才能保证滤波后的波形平滑。对于1kHz正弦波我选择Fc 20kHz。这个频率对于STM32的定时器来说很容易实现同时也能被常见的LC元件有效滤波。注意载波频率也不是越高越好。过高的Fc会导致开关损耗增加在功率应用中并且对定时器的时钟源和分频器设置提出更高要求。20kHz是一个在音频范围以上、又不会对硬件造成太大压力的折中选择。2. 每个正弦波周期的采样点数N这决定了我们正弦表的长度。N Fc / Fsin 20000 / 1000 20。这意味着一个1kHz的正弦波周期内我们的PWM占空比需要变化20次。用20个点来描绘一个正弦波略显粗糙可能会导致一些谐波失真。为了提高波形质量我们可以增加点数。一个常用的技巧是让定时器的更新频率即载波频率Fc是正弦波频率Fsin的整数倍K同时K也是我们正弦表长度N的整数倍。这样每个正弦波周期内占空比会均匀地变化N次。 我选择N 100K 20那么Fc Fsin * K * (N / N) 1000 * 20 20kHz。这里K20可以理解为“每个正弦波点重复输出的PWM周期数”。实际上我们是在每个PWM周期载波周期都更新一次占空比所以更新频率就是Fc。我们准备一个包含100个点的正弦表每输出一个PWM脉冲就切换到表中的下一个点。这样输出100个不同占空比的PWM脉冲后正好对应正弦波的一个完整周期。因此Fsin Fc / N 20000 / 100 200Hz等等这里逻辑错了。让我们理清关系目标Fsin 1kHz设定正弦表点数N 100那么为了在1秒内输出1000个正弦波我们需要输出1000 * 100 100,000个不同的占空比值。这意味着占空比的更新频率F_update必须为 100kHz。而占空比的更新我们计划在每一个PWM周期载波周期开始时进行。因此载波频率Fc必须等于更新频率F_update。所以Fc Fsin * N 1000 * 100 100kHz。结论为了用100点的正弦表产生1kHz正弦波我们需要将PWM的载波频率设置为100kHz。20kHz的假设需要修正。100kHz对于STM32F10372MHz主频来说仍然可行但需要精心计算定时器分频。3. 低通滤波器截止频率计算滤波器需要滤除100kHz的载波及其谐波同时保留1kHz的基波。截止频率Fcut应设置在两者之间。通常选择Fcut为Fsin的5-10倍同时远小于Fc。例如选择Fcut 10kHz。 对于二阶LC低通滤波器其截止频率公式为Fcut 1 / (2π * sqrt(L * C))我们需要选择合理的L和C值。如果负载电阻为R例如1kΩ为了达到较好的匹配和阻尼通常让滤波器的特征阻抗Z0 sqrt(L/C)约等于负载电阻R以避免谐振峰。 假设R 1kΩFcut 10kHz。 由Fcut 1 / (2π * sqrt(LC))得sqrt(LC) 1 / (2π * Fcut) ≈ 1.59e-5由Z0 sqrt(L/C) R 1000得L/C 1e6联立两式sqrt(LC) * sqrt(L/C) L 1.59e-5 * 1000 0.0159 H 15.9 mHC L / 1e6 0.0159 / 1e6 1.59e-8 F 0.0159 µF ≈ 0.016 µF实际中15.9mH的电感比较大不易获得且可能有较高内阻。我们可以调整Fcut或接受更低的特征阻抗。例如选用更常见的电感值如L 1mH。 则C 1 / ( (2π * Fcut)^2 * L ) 1 / ( (2*3.14*10000)^2 * 0.001 ) ≈ 2.53e-7 F 0.253 µF。可以选择标称值0.22µF或0.33µF的电容。 此时特征阻抗Z0 sqrt(0.001 / 2.5e-7) ≈ 63.2 Ω与1kΩ负载不匹配滤波效果会打折扣但对于演示和轻负载情况可以接受。更好的方法是使用运放构成的有源滤波器但这里为了简洁我们暂用LC无源滤波。实操心得滤波器设计是SPWM成败的关键。如果发现输出正弦波上有明显的高频毛刺载波泄漏多半是滤波器截止频率过高或阻尼不够。可以尝试增大电感或电容值降低截止频率或者在电感上串联一个小电阻增加阻尼。用示波器的FFT功能观察输出频谱能非常直观地看到载波频率处的残余分量。4. CubeMX工程配置详解有了理论计算我们就可以在CubeMX中进行具体配置了。这里以STM32F103C8T6和定时器TIM1的通道1PA8引脚为例。4.1 时钟树配置系统时钟是定时器计时的基准必须首先配置正确。在“Clock Configuration”标签页。选择外部晶振HSE作为时钟源。假设板载8MHz晶振。配置PLL将HSE输入倍频。对于F103系统时钟最高72MHz。我们可以设置PLL倍频因子为9这样PLLCLK 8MHz * 9 72MHz。将系统时钟源SYSCLK选择为PLLCLK此时HCLK (AHB总线时钟) 72MHz。高级定时器TIM1挂载在APB2总线上。确保APB2的预分频器APB2 Prescaler为1即PCLK2 72MHz。TIM1的时钟来源于PCLK2。4.2 定时器TIM1配置这是产生SPWM的核心。在“Pinout Configuration”标签页左侧找到“Timers” - “TIM1”。Clock Source选择“Internal Clock”。Parameter Settings标签Counter Settings:Prescaler (PSC): 这是定时器时钟的分频系数。定时器的实际计数时钟CK_CNT PCLK2 / (PSC 1)。我们的目标是产生Fc 100kHz的PWM波。对于中心对齐模式PWM频率Fpwm CK_CNT / (ARR 1)。我们先设定ARR。Counter Mode: 选择“Center Aligned Mode 1/2/3”中的任意一个。中心对齐模式产生的PWM信号关于计数器中心对称其谐波特性比边沿对齐模式更好更适合用于功率转换和滤波。Counter Period (ARR): 自动重装载值。这个值决定了PWM的分辨率。ARR越大我们可以设定的占空比精度越高。但同时为了达到固定的PWM频率就需要更高的CK_CNT。我们需要权衡。假设我们选择ARR 999那么PWM的占空比分辨率是1/1000 0.1%。现在计算PSCFpwm CK_CNT / (ARR 1) (PCLK2 / (PSC1)) / (ARR1)。 代入Fpwm100kHz,PCLK272MHz,ARR999。 得PSC 1 PCLK2 / (Fpwm * (ARR1)) 72e6 / (100e3 * 1000) 0.72。 这显然不合理PSC必须为整数。我们需要调整ARR。重新计算令ARR 719则ARR1720。PSC 1 72e6 / (100e3 * 720) 1.0。所以PSC 0。 此时CK_CNT 72MHz / (01) 72MHz。 实际Fpwm 72MHz / 720 100kHz。完美匹配。Auto-reload preload: 使能Enable。这允许在下一个更新事件时才更新ARR的值防止在计数中途改变周期导致不可预测的行为。PWM Generation Channel 1:Mode: 选择“PWM mode 1”或“PWM mode 2”。两者的区别是输出极性。我们选“PWM mode 1”表示当计数器小于CCR时输出有效高电平。Pulse (CCR1): 初始占空比可以先设为ARR / 2 360。CH Polarity: 根据后续驱动电路决定。如果直接驱动滤波电路高电平有效即可。CH Idle State: 保持默认。Fast Mode: 禁用。CH Output: 使能Enable。NVIC Settings标签使能“TIM1 update interrupt”和“TIM1 CC interrupt”。更新中断用于在每次PWM周期结束时中心对齐模式是周期中心更新占空比CCR值。CC中断在这里不是必须的但开启也无妨。4.3 GPIO配置TIM1通道1对应的引脚PA8会自动被CubeMX配置为复用推挽输出Alternate Function Push Pull。通常无需额外修改但可以检查一下输出速度Speed对于100kHz信号选择“High”即可。4.4 生成工程代码点击“Project Manager”标签设置好工程名称、路径、IDE如MDK-ARM V5然后点击“GENERATE CODE”。CubeMX会生成包含HAL库初始化的完整工程。5. 软件代码实现与SPWM算法打开生成的工程我们需要在main.c和stm32f1xx_it.c中添加核心逻辑。5.1 生成正弦表首先我们需要一个正弦函数值表。这个表存储了在一个正弦周期内不同相位点所对应的比较寄存器CCR值。CCR值决定了PWM的占空比。/* Private variables ---------------------------------------------------------*/ #define SIN_TABLE_LEN 100 // 正弦表长度与之前计算一致 #define PWM_PERIOD 720 // ARR1的值注意我们的ARR是719所以周期是720 uint16_t sin_table[SIN_TABLE_LEN]; uint8_t sin_index 0; /* Private function prototypes -----------------------------------------------*/ void generate_sin_table(void);在main()函数初始化部分调用函数生成正弦表generate_sin_table(); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 启动PWM输出 HAL_TIM_Base_Start_IT(htim1); // 启动定时器更新中断generate_sin_table函数的实现void generate_sin_table(void) { for (int i 0; i SIN_TABLE_LEN; i) { // 计算0到2π之间的角度 float angle 2 * 3.1415926f * i / SIN_TABLE_LEN; // 计算正弦值范围[-1, 1] float sin_val sinf(angle); // 将正弦值映射到PWM的CCR值范围。 // 我们希望正弦波幅值最大时占空比为50% ± 调制深度。 // 设置调制深度为90%即CCR在 (0.5 ± 0.45) * PWM_PERIOD 之间变化。 // 防止过调制占空比0%或100%导致波形削顶。 float modulation_depth 0.45f; // 计算占空比0.5 0.45 * sin(angle) float duty_cycle 0.5f modulation_depth * sin_val; // 将占空比转换为CCR值并限制在安全范围内 uint32_t ccr_val (uint32_t)(duty_cycle * PWM_PERIOD); if (ccr_val PWM_PERIOD - 1) ccr_val PWM_PERIOD - 1; if (ccr_val 1) ccr_val 1; // 避免完全关闭或完全开启有助于滤波 sin_table[i] (uint16_t)ccr_val; } }注意这里使用了浮点运算sinf对于F103这类没有FPU的芯片在中断中频繁计算会消耗大量时间。因此最佳实践是在初始化时预先计算好正弦表并存入数组中断中只需查表。这正是上面代码所做的。如果追求极致的速度和确定性可以预先用电脑计算好表直接以常量数组形式写在代码里。5.2 定时器更新中断处理每次PWM周期结束中心对齐模式的中心点或端点取决于配置都会产生更新中断。我们在中断服务程序ISR中更新CCR值以输出下一个正弦点。 在stm32f1xx_it.c中找到定时器更新中断向量TIM1_UP_IRQHandler并在其中调用HAL库的中断处理回调函数。但更关键的是我们要实现自己的更新逻辑。 我们可以在main.c中重写HAL库的更新中断回调函数/* Private user code ---------------------------------------------------------*/ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { // 更新下一个点的CCR值 __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, sin_table[sin_index]); // 更新索引准备下一个点 sin_index; if (sin_index SIN_TABLE_LEN) { sin_index 0; } } }关键点解释HAL_TIM_PeriodElapsedCallback是HAL库定义的弱函数当定时器更新中断发生时如果用户没有重写它什么都不做。我们重写它来加入我们的逻辑。__HAL_TIM_SET_COMPARE是一个宏用于安全地设置指定定时器通道的比较寄存器CCR。它会在下一个PWM周期生效。我们在每个PWM周期更新一次CCR值。因为Fpwm 100kHzSIN_TABLE_LEN100所以正弦波输出频率Fsin Fpwm / SIN_TABLE_LEN 1kHz与设计目标一致。5.3 调试与验证代码在main函数的while(1)循环中我们可以添加一些调试代码例如通过串口打印当前的索引和CCR值或者用另一个GPIO引脚翻转来观察中断频率。while (1) { // 可以添加其他逻辑如通过按键改变频率或幅值 // 改变调制深度即可改变输出正弦波幅值 // 改变查表步长如每次索引2即可改变输出频率但会引入失真 HAL_Delay(1000); }实操心得调试时首先不要接滤波器直接用示波器观察PA8引脚的PWM输出。你应该能看到一个占空比在不断变化的方波。用示波器的测量功能检查其频率是否为100kHz。然后打开示波器的持久显示Persistence或单次触发观察占空比的变化是否呈现出一个完整的正弦包络。这是验证SPWM算法是否正确工作的第一步。6. 滤波器搭建、测试与波形优化软件调通后就可以搭建硬件滤波器了。6.1 无源LC滤波器搭建根据之前的计算我们暂定L 1mHC 0.22µF或0.33µF。将电感串联在STM32的PA8引脚和负载之间。将电容连接在负载两端即电感之后负载对地。负载可以使用一个1kΩ的电阻。务必注意电感应选择功率和电流合适的类型普通工字电感或磁环电感均可。电容应选择高频特性好的瓷片电容或C0G/NP0材质的贴片电容避免使用电解电容。用示波器探头连接在负载电阻两端观察波形。6.2 典型问题与排查技巧问题输出正弦波幅值太小。排查检查PWM的调制深度。在generate_sin_table函数中modulation_depth决定了输出幅值。理论上最大为0.5占空比从0%到100%变化但为了避免过调制失真通常设为0.45-0.48。尝试增大这个值。排查检查滤波器负载。如果负载阻抗太小会从电容吸收过多电流导致电压被拉低。尝试增大负载电阻如10kΩ观察。排查测量PWM引脚输出高电平的电压。确保是3.3V或5V取决于电路。如果电压不足可能是驱动能力问题可以考虑在PWM输出后加一个缓冲器如74HC04非门接成同相缓冲或MOSFET驱动芯片。问题正弦波顶部或底部有平坦现象削顶。原因过调制。即正弦表计算出的CCR值超过了PWM周期ARR或低于0。我们的代码中已经做了限制ccr_val PWM_PERIOD - 1和ccr_val 1但如果modulation_depth设置得过大限制后的波形就会在峰值处被削平。解决降低modulation_depth值。问题正弦波上有明显的高频纹波毛刺。排查这是载波100kHz没有被完全滤除的表现。首先用示波器FFT功能确认毛刺频率是否为100kHz或其倍频。解决调整滤波器参数尝试增大电感或电容值以降低滤波器的截止频率。例如将电容增加到0.47µF或1µF注意电容类型。增加滤波器阶数使用两级LC滤波器π型滤波器滤波效果会更好。检查布线滤波电路应尽量靠近MCU引脚引线要短避免引入额外的寄生电感和电容。使用有源滤波器如果对波形质量要求极高可以考虑使用运放如LM324、TL084搭建二阶或多阶有源低通滤波器如Sallen-Key或MFB结构其性能优于无源滤波器且不受负载影响。问题正弦波频率不准。排查用示波器精确测量输出正弦波的周期。理论上应为1ms。原因1系统时钟不准。检查外部晶振是否起振CubeMX中时钟树配置是否正确。原因2定时器分频计算错误。回顾第4.2节的计算确认PSC、ARR、SIN_TABLE_LEN与Fsin、Fpwm的关系。原因3中断处理时间过长。如果中断服务程序执行时间超过了PWM周期10µs会导致某些更新事件被错过从而降低有效频率。确保中断函数尽可能短小只做最基本的查表和写寄存器操作。避免在中断中使用HAL_Delay或进行复杂计算。问题波形失真不是完美的正弦波。排查除了削顶和纹波还可能因为正弦表点数太少SIN_TABLE_LEN太小导致阶梯感明显。增加SIN_TABLE_LEN可以改善但需要同步提高Fpwm以维持Fsin不变否则频率会降低。这需要重新计算定时器参数可能受限于定时器最大计数频率。高级技巧——对称规则采样法我们目前使用的是自然采样法的查表实现即每个PWM周期均匀地取正弦表中的一个点。在低载波比Fc/Fsin下这种方法会产生低次谐波。一种改进方法是规则采样法。它不是在每个三角载波周期自然比较而是以固定的时间间隔通常是载波峰值或谷值时刻对正弦调制波采样并保持这个值在整个载波周期内不变。这种方法产生的谐波主要集中在载波频率整数倍附近更容易被滤除且计算量小。在中断中我们可以不简单地顺序查表而是根据一个累加的相位角phase_accumulator来查表这样可以更灵活地实现频率变化。7. 性能提升与扩展思路基础功能实现后可以考虑以下优化和扩展动态调频与调幅调频不改变PWM载波频率Fc而是改变查表的速度。例如在中断中索引不是固定1而是根据一个频率控制字phase_inc来增加sin_index phase_inc。当phase_inc1时输出基频Fsin当phase_inc2时输出频率翻倍。但这样会跳过一些表项可能引入失真。更好的方法是使用直接数字频率合成DDS的思想使用一个32位的相位累加器用其高几位作为正弦表的索引。通过改变相位增量值可以实现高分辨率、无失真的频率调节。调幅动态修改modulation_depth变量并重新计算或缩放整个正弦表。更高效的方法是在中断中将查表得到的CCR值乘以一个幅值系数0.0~1.0再写入CCR寄存器。注意结果不能超过ARR。使用DMA自动更新CCR为了进一步解放CPU可以使用DMA直接存储器访问来搬运正弦表数据到定时器的CCR寄存器。可以配置DMA循环模式在每次定时器更新事件时触发一次DMA传输自动将数组中的下一个值送到CCR。这样CPU完全不用干预可以处理其他任务。这在需要产生多路同步SPWM如三相逆变器时尤其有用。死区时间插入如果驱动的是半桥或全桥电路上下管需要互补的PWM信号并且中间必须插入死区时间Dead Time防止上下管直通短路。STM32的高级定时器TIM1/TIM8硬件支持带死区插入的互补PWM输出。在CubeMX中配置通道为“PWM Generation CHx and CHxN”并设置“Dead Time”参数即可。闭环控制如果需要稳定的正弦波输出电压如在线式UPS可以在输出端加入电压采样电路如互感器运放调理ADCMCU通过ADC读取实际输出电压与期望的正弦表参考值进行比较通过PID算法动态调整调制深度或直接调整CCR值实现闭环稳压。这构成了一个数字式的正弦波逆变器核心。这个项目从原理分析、参数计算、到CubeMX配置、代码编写、硬件调试完整地走通了一个基于STM32 HAL库的SPWM正弦波发生器。它不仅仅是一个简单的信号源其背后涉及的定时器高级应用、调制原理、滤波器设计和数字控制思想是电力电子、电机驱动、音频处理等众多领域的通用基础。希望这份超详细的拆解能帮你不仅做出这个波形更能理解每一个环节背后的“为什么”。