
简介基于STM32的三相SPWM波输出资料包面向嵌入式开发与电机控制方向的工程师和高校学生重点解决通过STM32高级定时器生成三相正弦脉宽调制波形的问题适用于逆变器、变频调速及交流电机驱动等场景。压缩包共174个文件大小2.81MB以C源文件、H头文件为主辅以uvproj/uvopt工程配置、hex可烧录固件以及axf/map等编译产物整体结构清晰可快速定位代码与构建关系。目前已有564人学习下载。内容围绕三相SPWM生成展开涵盖定时器PWM模式配置、死区时间设定、三相互差120度的相位控制、正弦波表与调制算法实现等关键环节并提供了MDK工程备份方便对照代码逐步验证输出波形适合作为电机控制项目入门参考或二次开发的基础框架。1. 三相SPWM这份工程最先要看的是timer.c不是main.c拿到基于STM32的三相SPWM波输出这类 Keil 工程包很多人会先打开 main.c 找调用入口但真正决定输出波形质量的其实是 timer.c 里的 TIM1 配置和中断更新逻辑。工程里的 PWM.uvproj.bak 是工程备份PWM.axf 是上次编译的产物stm32f10x_it.c 才放着中断服务函数。三相 SPWM 的核心不是把正弦表算得多漂亮而是三个相位互差 120° 的调制波与同一路三角载波完成比较载波频率决定开关管损耗调制波频率决定电机转速。下面按生成原理 → TIM1 配置 → 三相编排 → 中断更新 → 示波器验证的顺序把这份工程从头拆一遍顺带把普通定时器做不了这件事的底层原因讲透。2. 三相SPWM生成原理为什么先立起数学模型再碰寄存器2.1 正弦波与三角载波占空比公式才是配置源头SPWM 的实质是用占空比近似正弦。把幅值为 Vt 的三角载波与幅值为 Vm 的正弦调制波送入比较器正弦瞬时值大于载波时输出高电平否则输出低电平输出脉冲宽度就随正弦波瞬时值连续变化。当调制波为 Vm·sin(ωt) 时每路 PWM 的理论占空比为D(t) 0.5 × (1 M × sin(ωt))其中 M Vm / Vt 叫调制比M ≤ 1 时输出正弦电压幅值约为 Vdc/2 × M。映射到 STM32 定时器上占空比直接对应比较寄存器 CCR 与自动重载值 ARR 的关系CCR(t) (ARR 1) × D(t)用这个公式反推要输出 50Hz、调制比 0.85且 ARR 5999 的正弦波正弦峰值处 CCR ≈ 0.5 × 6000 × (1 0.85) ≈ 5550谷值处 CCR ≈ 0.5 × 6000 × (1 - 0.85) ≈ 450。可见 CCR 不能超过 ARR也不能太小否则脉宽饱和输出已经不是正弦调制而是直流直接接到电机上就是失控。三相系统与单相的区别在于三路正弦调制波共用同一路三角载波。A/B/C 三相互差 120° 电角度如果用 N 个 PWM 载波周期合成一个完整工频周期B 相相对 A 相就偏移 N/3 个点C 相偏移 2N/3 个点。这里 N 必须能被 3 整除否则相位会系统性偏出 120°这一点在后面的表格和代码里会反复出现。2.2 为什么非TIM1不可高级定时器的死区与MOE位STM32F103 有 TIM1、TIM8 两个高级定时器和若干通用定时器。通用定时器如 TIM2、TIM3 的 PWM 输出没有死区插入逻辑也没有刹车输入。要驱动三相全桥逆变电路上下桥臂切换瞬间必须插入死区外部硬件加 RC 延迟不可靠软件死区又会吃掉 CPU 时间两者都不适合电机驱动。TIM1 和 TIM8 自带可编程死区发生器、刹车输入和主输出使能位 MOE一片芯片就能完成三相桥臂的安全切换。选型上还有个实用细节TIM1 的 CH1、CH2、CH3 分别映射到 PA8、PA9、PA10三路通道共用同一个 ARR 和预分频寄存器天然适合三相复用。CH4 还可以配置为 ADC 触发做电流采样同步。TIM8 也可以做同样的事但通道在 PC6、PC7、PC8很多控制板没把这些引脚引出来。这份工程激活的是 TIM1也是绝大多数三相 SPWM 教学工程的首选。2.3 工程包内部文件逐个定位哪些是核心哪些可以直接删拿到工程包后先别急着编译把文件按用途分一下类。文件作用处理建议timer.c / timer.hTIM1 初始化、正弦表、中断更新核心文件重点看stm32f10x_it.c中断服务函数入口核心文件PWM.uvproj.bakKeil 工程备份可直接打开PWM.uvgui_Administrator.bakKeil 窗口布局配置删除不影响编译PWM.axf编译生成的映像文件删除不影响工程PWM_uvopt.bak、PWM.opt.bak工程选项备份保留一份即可PWM.axf 是上次编译成功的映像可以直接烧录看现象但它只是编译成功过的证据。真正改代码时打开 PWM.uvproj.bak确认器件型号与你的板子一致改动集中在 timer.c 和 stm32f10x_it.c 里。3. 高级定时器TIM1的PWM与死区配置3.1 从72MHz系统时钟反推预分频与ARRSTM32F103 最大主频 72MHzTIM1 挂在 APB2 总线上。只要 APB1 分频系数不为 1APB2 定时器时钟就是 72MHz 满速。载波频率 f_PWM 由三个参数决定f_PWM f_clk / ((PSC1) × (ARR1))反过来说确定了载波频率和每个工频周期采样点数 N就能算出 PSC 和 ARR。设 N 为一个正弦周期内 PWM 脉冲的个数输出基波频率为 f_sine f_PWM / N。常见组合如下。目标载波PSCARR采样点数N基波频率备注10 kHz0719920050 HzN 不是 3 的倍数三相有相位误差12 kHz0599924050 HzN 3×80推荐16 kHz0449932050 HzN 不是 3 的倍数仅作对比18 kHz0399936050 HzN 3×120开关损耗偏高推荐 12kHz 这行既绕开了人耳可闻噪声区又满足 N 能被 3 整除ARR 5999 在 16 位定时器范围内留足余量。如果你拿到手的工程原先是 10kHz/200 点的配置要么接受 118.8° 的相位误差要么把载波改成 12kHz不要直接拿去带三相电机。时基初始化代码如下。TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 5999; // ARR配合 PSC0 得 12kHz TIM_TimeBaseStructure.TIM_Prescaler 0; // PSC72MHz 不分频 TIM_TimeBaseStructure.TIM_ClockDivision 0; // 内部时钟无需分频 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure);注意 TIM_ClockDivision 与计数器计数方向无关这里用 TIM_CounterMode_Up 是锯齿波向上计数想要对称三角载波应改用 TIM_CounterMode_CenterAligned1谐波特性更好但中断频率翻倍。入门阶段先跑通向上计数模式。3.2 PWM模式1与CCR限幅TIM1 每个输出比较通道都支持 PWM 模式 1 和模式 2。PWM 模式 1 的规则是CNT CCR 时输出有效电平CNT ≥ CCR 时输出无效电平。三路通道配置如下。TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // CNTCCR 输出高 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 5999 / 2; // 初始占空比 50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); TIM_OC3Init(TIM1, TIM_OCInitStructure);三路复用同一个 OCInitStructure只是 Init 函数分别对应 OC1、OC2、OC3。初始化完成时三路占空比相同真正拉开三相相位差的任务交给第 4 章的更新中断。这里有个常见坑TIM_Pulse 如果写大了比如 8000 ARR 5999CNT 永远小于 CCR引脚恒高PWM 直接变成直流接到逆变桥就是直通。安全范围一般取 60 到 ARR-60 之间。下限给死区留余量上限防止占空比饱和。改别人的工程先查这个值。3.3 死区时间计算0x24是怎么来的死区是上下桥臂切换时防止直通的关键参数。TIM1 的 BDTR 寄存器里的 DTG[7:0] 决定死区时长当 DTG[7:5] 为 000 时死区时间等于 DTG[7:0] × Tdts。72MHz 下 Tdts ≈ 13.89ns要配 500ns 死区DTG 500 / 13.89 ≈ 36 0x24对应代码TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime 0x24; // 500ns 72MHz TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRInit(TIM1, TIM_BDTRInitStructure);死区不是越大越好。死区越大基波中低次谐波越明显电机力矩脉动越大。IGBT 模块通常取 1.53us小功率 MOSFET 电路取 300ns1us。低压 12V/24V 教学验证用 500ns 足够换成功率大一点的电路再把 DTG 调上去。提示配置完 BDTR 结构体后必须在 TIM_Cmd 之后调用 TIM_CtrlPWMOutputs(TIM1, ENABLE)否则高级定时器所有 PWM 输出通道处于关断状态。PA8/PA9/PA10 完全没有输出时先查这一句。4. 正弦表与三相120°相位编排4.1 用Python离线生成正弦表正弦表可以直接写在 C 数组里也可以离线生成。生成逻辑就是第 2 章的占空比公式只是把计算挪到脚本里。import math N 240 # 一个正弦周期的采样点数3的倍数 ARR 5999 # 自动重载值与 TIM1 配置一致 M 0.85 # 调制比 DEAD 60 # CCR 下限避开死区 tab [] for i in range(N): theta 2 * math.pi * i / N ccr int((ARR 1) * 0.5 * (1 M * math.sin(theta))) ccr max(DEAD, min(ARR - DEAD, ccr)) tab.append(ccr) print(, .join(str(x) for x in tab))脚本输出顺序查表数组中断每周期取一个值。两个生成端就要注意的点N 必须能被 3 整除否则 B/C 相无法偏移整数个点CCR 限幅上下都留余量不能到 0 也不能顶到 ARR。这样生成出来的数组峰值大约 5550谷值大约 450和理论计算一致。4.2 中断更新代码索引偏移代替实时三角函数实时在中断里算 sin 不现实72MHz 主频下三角函数要好几微秒占掉一个 PWM 周期的可观比例。工程里把正弦表放进 Flash中断里做加法和取模就够了。#define SPWM_POINTS 240 static u16 step 0; void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); step; if (step SPWM_POINTS) step 0; TIM_SetCompare1(TIM1, sin_tab[step]); TIM_SetCompare2(TIM1, sin_tab[(step SPWM_POINTS / 3) % SPWM_POINTS]); TIM_SetCompare3(TIM1, sin_tab[(step 2 * SPWM_POINTS / 3) % SPWM_POINTS]); } }这段代码里没有一条浮点运算所有索引都是整数取模单次中断执行在几百纳秒级。三相值从同一张表取不同偏移A 相在当前点B 相在 80 个点之后240/380对应 120°C 相在 160 个点之后。如果你拿到的工程是 200 点配置200/366 余 2B 相实际偏 118.8°每周期都产生同向误差三相电流不平衡这是 SPWM 工程里最隐蔽的 bug 之一。注意使用索引偏移法的前提是 N 是 3 的倍数。若必须用 10kHz 载波可取 N201基波频率变成 49.75Hz仍在多数电机允许的 ±0.5Hz 内。4.3 预装载影子寄存器为什么不能直接写CCRSTM32 定时器的 CCR、ARR 都有两级结构用户可见寄存器和影子寄存器。对用户可见寄存器写入后要等更新事件发生时影子寄存器才把新值加载进比较逻辑。如果不使能预装载写入发生在计数值任意时刻计数器还没走完当前 PWM 周期而 CCR 已经变了输出就会在一个周期中间出现不规则的宽窄变化电流上表现为毛刺。正确做法是使能预装载TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE);配置完预装载后中断里调 TIM_SetCompare1 只是写入影子寄存器下一个计数周期统一生效。这对 SPWM 特别有利三路 CCR 在同一事件同步切换三相相位关系不会因中断执行时间的抖动而错乱。4.4 载波比与中断频率的CPU折中正弦表点数 N 越大输出波形越接近理想正弦但每个 PWM 周期都触发一次更新中断。以 72MHz 的 STM32F103 为例12kHz 载波、N240 时中断频率 12kHz每次中断几微秒CPU 占用约 2% 到 5%。等把电流环、速度环、通讯都加进去这个占用率还能接受。如果贪波形把载波提到 40kHz中断频率 40kHzCPU 占用可能破 20%主循环实时性会被拖垮。设计时先测中断执行时间再给主循环留 30% 以上余量。5. 示波器验证、FFT检查与换HAL库的坑5.1 双通道快速验证三相相位关系三相 PWM 输出在 PA8/PA9/PA10。示波器两个探头分别夹 A 相和 B 相输出引脚共地夹 GND触发方式选边沿时基调到能看清载波50us/div 左右。两路 PWM 来自同一个 TIM1上升沿之间的时间差 Δt 应当稳定不变换算相位差的公式是相位差 Δt / T_sine × 360°以旧工程里常见的 10kHz/200 点为例N/366Δt6.6msT_sine20ms算出来 118.8°这正是 N200 不能被 3 整除的代价。把 N 改成 201或者直接用 12kHz/240 配置Δt 与 T_sine 的比值会精确落在 1/3 上。5.2 RC低通看包络FFT量化谐波示波器直接看 PWM 高频翻转看不出正弦质量最省事的验证法是接一阶 RC 低通滤波器1kΩ 电阻串联、10nF 电容到地截止频率约 16kHz。探头夹在电容两端应该看到稳定对称的三相正弦包络。包络幅度随调制比变化说明 SPWM 调制深度在正常工组范围。要量化谐波用示波器 FFT 功能中心频率 50Hz扫宽 1kHz。理想 SPWM 的对称性好低频端不该有大于 3% 的 3、5、7 次谐波。如果低频谐波明显先查死区是否过大、CCR 是否饱和再看滤波电容是不是用了电解电容影响高频响应。5.3 从SPL迁移到HAL库最容易踩的3个点新项目用 HAL 库时这份工程逻辑可以平移但有几处不能直接替换。第一是 MOE 位SPL 里叫 TIM_CtrlPWMOutputs(TIM1, ENABLE)HAL 里对应__HAL_TIM_MOE_ENABLE(htim1);漏掉这行PA8/PA9/PA10 静默无输出。第二是更新中断HAL 不让你直接在 TIM1_UP_IRQHandler 里写业务正确做法是在中断函数里调用 HAL_TIM_PeriodElapsedCallback再在回调里改 CCR。第三是时钟树HAL 的 MX_TIM1_Init 只设置预分频和 ARR时钟分频由 MX 工具生成手动迁工程时最容易把 APB2 分频设错导致实际载波频率与预期差两倍。迁移完按 5.1 和 5.2 的步骤复测逐项对上再往下接驱动电路。本文还有配套的精品资源点击获取