尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32生成SPWM程序:中心对齐定时器与DMA联动实现

STM32生成SPWM程序:中心对齐定时器与DMA联动实现 简介这是一份基于STM32的SPWM程序源码包面向嵌入式开发者以及电机控制、电源变换方向的学习者用于理解并实现脉宽调制输出。压缩包共40个文件、570KB以C源码、头文件、Keil工程文件uv2/opt/plg为主体同时包含编译生成的o/axf/map/lst以及说明文档txt既可直接查看逻辑也可对照工程配置进行调试。已有703人学习下载属于入门到进阶均可参考的紧凑型示例。资源围绕“一个定时器生成SPWM”展开涵盖定时器初始化、PWM通道占空比设置、中心对齐模式、死区时间等关键知识点通过阅读和运行工程读者可以掌握HAL库或标准库下的SPWM波形生成方法并进一步理解如何通过修改比较寄存器实时调整输出为电机调速或逆变电路设计提供基础模板对于需要自行配置定时器中断或DMA的应用场景这份代码也能提供清晰的参考。1. STM32 生成 SPWM 程序搜索这串关键词的人大多卡在同一个地方“STM32 生成 SPWM 程序”在搜索引擎里会带出一堆标题带“201003”的压缩包资源下载量高但真正能一次跑出干净正弦波的没有几个。SPWM 的原理一句话能说清让 PWM 脉冲的高电平时间按正弦函数连续变化再用低通滤波器或感性负载把它平滑成近似正弦的电压。可落到 STM32 上大多数人卡在同一个地方正弦表算出来了定时器也出波形了示波器上却是一段跳变的毛刺而不是 50 Hz 正弦。问题通常不在算法而在比较寄存器 CCR 的更新时机和定时器计数模式不匹配。这套方案最稳妥的落法是用高级定时器 TIM1 或 TIM8 生成中心对齐 PWM配合 DMA 把一个完整正弦周期的占空比表循环搬进比较寄存器。载波频率取 10 kHz 到 20 kHz正弦输出 50 Hz一个正弦周期内就有 400 到 2000 个电压台阶滤波后足够干净。它覆盖的场景很典型单相逆变器、三相变频器、异步电机交流调速、UPS以及大量基于 STM32 的毕业设计。刚入门的照着章节抄完能点亮波形有五年以上经验的重点看第二章的中心对齐公式、第四章的死区插入和第五章的频率合成边界——这几个地方是“能出波”和“能稳定驱动负载”之间的分水岭。2. SPWM 生成原理与 STM32 外设选型为什么是高级定时器2.1 把“生成 SPWM”拆成三步正弦表、定时器、比较寄存器任何 SPWM 程序都逃不开三个动作先算出一张正弦占空比表再由定时器按固定频率产生 PWM最后在每个载波周期把表中对应的比较值写进 CCR。后两步在 STM32 上有一个专门的叫法——更新事件触发 DMA把内存里的表项搬到定时器的捕获/比较寄存器。先明确一个单位问题。占空比表不存“0 到 100%”的浮点比例而存“CCR 的整数值”。CCR 的范围由定时器重载值 ARR 决定而 ARR 又由载波频率反推。这里用中心对齐模式时的关系式f_carrier f_timer / (2 × (ARR 1))之所以写成“2 倍”是因为中心对齐模式下计数器从 0 向上数到 ARR再向下递减回 0完成一个完整 PWM 周期需要两个 ARR 时长。这也是新手最容易写错的地方按边沿对齐模式的公式去算 ARR出来的载波频率恰好差一倍输出毛刺立刻出现。STM32 的 PWM 有边沿对齐和中心对齐两种工作方式前者适合数字电源的一般的开关控制后者因为波形对称、谐波含量更集中在 SPWM 这种强调“滤波后正弦纯度”的场景里更合适。正弦表一项对应一个 PWM 周期所以表长 N f_carrier / f_out。20 kHz 载波、50 Hz 输出时N 400。换个说法一个正弦周期被切成 400 个小台阶。台阶越多经 LC 或电机绕组滤波后的正弦度越好但表长和中断更新频率也会一起升高不是越大越好。2.2 中心对齐模式CCR 计算公式与最常见的半周期错误用中心对齐模式生成 PWM 时输出高电平的时间在一个完整 PWM 周期里跨越两次计数上升沿和下降沿占空比与 CCR 的关系是Duty CCR / ARR这里的 ARR 是中心对齐模式下的峰值计数值。如果希望正弦峰值对应最大占空比 90%调制比 M 取 0.80那么每个采样点的 CCR 按下面这条公式算CCR[i] (1 M × sin(θ[i])) × ARR / 2θ[i] 2 × π × i / N注意“除以 2”。假设 ARR 1800M 0.88正弦峰值处 CCR (1 0.88) × 1800 / 2 ≈ 1692对应占空比约 94%谷值处 CCR ≈ 108占空比约 6%。这样整个输出围绕 50% 中心对称摆动平均直流分量为零这正是变压器和电机绕组需要的。和边沿对齐模式对比两者参数关系很不一样。边沿对齐向上计数时一个 PWM 周期是 ARR 1 个时钟占空比 CCR / (ARR 1)。如果拿边沿对齐的公式去匹配中心对齐的 ARR实际占空比会比预期大一倍正弦峰值处可能直接削顶输出变成近似方波。下面这张表列出两个模式的差异方便对着检查对比项边沿对齐模式中心对齐模式计数方向仅向上向上再向下PWM 周期公式(ARR 1) / f_timer2 × ARR / f_timer占空比计算CCR / (ARR 1)CCR / ARR更新事件频率每个 PWM 周期一次向上和向下各一次谐波分布偶次与奇次谐波都有波形对称偶次谐波被抑制SPWM 适用度低频简易驱动可用推荐谐波较干净这里有个附加坑中心对齐模式在每个 PWM 周期里会产生两次计数溢出事件一次在 ARR 顶端一次在 0 点。如果 DMA 在两次事件上都触发搬运正弦表会被以 2 倍速度刷新输出频率也会跟着翻倍。正确做法是只在单一方向上触发更新也就是把定时器的更新事件来源限制在计数器归零时刻。STM32 中心对齐模式下通过 TIMx_CR1 的 CMS 位配合更新中断过滤可以让 DMA 只在一个端点触发。2.3 TIM1 与 TIM8 的三个硬件优势互补输出、死区、刹车几乎所有 STM32 系列都有通用定时器但 SPWM 驱动逆变桥时业内标准做法是用 TIM1 或 TIM8 这类高级控制定时器而不是 TIM2、TIM3。原因有三条。第一是互补输出。CH1 和 CH1N 成对出现一个 PWM 信号同时产生反相的一路硬件上自动翻转不需要软件里用逻辑去反向。半桥电路的上桥臂和下桥臂分别接这两根脚驱动信号天然匹配。第二是死区单元。互补输出的两路之间要求有一小段“都不导通”的时间防止上下桥臂直通烧毁这个时间由 BDTR 寄存器里的 DTG 位段直接配置硬件会在边沿处自动插入死区不占用内核时间。第三是刹车输入。故障信号拉低后定时器可以把输出引脚强制到安全电平这在电机过流、母线过压时是保命功能。通用定时器通过软件反转引脚也能做到“互补”但补出来的是理想波形插入死区要额外增加定时器中断和电平翻转操作一旦死区时间设置受中断延时影响波形就会抖动。下面这张能力对比表是我选型时必查的功能点TIM1 / TIM8TIM2 / TIM3 / TIM4 / TIM5互补输出 CHx / CHxN支持不支持硬件死区插入支持DTG 可配置不支持刹车输入与强制输出支持不支持重复计数器支持部分系列支持DMA 映射每个捕获/比较通道独立多数共用一个 DMA 请求所以“STM32 生成 SPWM”的程序合理解释不是拿普通定时器硬写逻辑而是把高级定时器当作专用波形发生器正弦表只负责给它喂数据。后面的实现章节全部围绕 TIM1 展开。3. 在 STM32 上跑通 SPWM 程序的最小框架正弦表、TIM1 和 DMA 联动3.1 用 Python 生成 400 点正弦占空比表下面的 Python 脚本在 PC 上运行生成的是 STM32 工程里直接能使用的 C 语言常量数组。脚本里的参数和上面推导的公式一一对应修改时只要注意 N、ARR、M 三个变量即可。import math N 400 # 一个正弦周期内的 PWM 周期数 f_carrier / f_out ARR 1800 # 中心对齐模式72 MHz 时钟下对应 20 kHz 载波 M 0.88 # 调制比限制正弦峰值处的最大占空比 lut [] for i in range(N): theta 2.0 * math.pi * i / N ccr int((1.0 M * math.sin(theta)) * ARR / 2.0) ccr max(4, min(ARR - 4, ccr)) # 留出最小脉宽余量避免极端占空比 lut.append(ccr) print(const uint16_t sine_lut[%d] { % N) for i in range(0, N, 8): row , .join(str(v) for v in lut[i:i8]) print( %s, % row) print(};)脚本逻辑说明i 是采样点序号从 0 到 N-1 正好旋转一整圈 2πsin 值乘以调制比 M再叠加直流偏置 1经 ARR/2 换算成 CCR 的整数输出范围在 0 到 ARR 之间。那一行“max / min”的裁剪很重要——如果 M 接近 1正弦峰值处的 CCR 会逼近 ARR占空比接近 100%死区插入后根本来不及关断所以实际工程里 M 一般不超过 0.92留出的余量给死区和开关损耗。参数修改经验N 决定的是“一个正弦周期内的电压台阶数”N 越大输出越平滑但 DMA 每级台阶的保持时间越短低占空比处的脉冲宽度越窄栅极驱动 IC 可能来不及开通。常见做法是把 N 控制在 200 到 600 之间对应载波比也是同样的数量级。M 低于 0.70 时输出电压偏低高于 0.92 时线性度变差且死区影响明显推荐从 0.85 起步。3.2 TIM1 初始化中心对齐、预分频、互补通道和死区配置以 STM32F103 的 72 MHz APB2 时钟为例TIM1 计数时钟就是 72 MHz预分频 PSC 设为 0。中心对齐模式下 ARR 1800 时载波频率正好 20 kHz计算公式为 72 MHz / (2 × 1800) 20 kHz。下面的初始化代码使用 STM32Cube HAL 库结构在 F1、F4 系列上都通用。TIM_OC_InitTypeDef sConfigOC {0}; TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeCfg {0}; htim1.Instance TIM1; htim1.Init.Prescaler 0; /* 72 MHz 不分频 */ htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period 1800; /* 20 kHz 载波 */ htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; HAL_TIM_PWM_Init(htim1); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 900; /* 初始 50% 占空比DMA 启动后被表覆盖 */ sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); sBreakDeadTimeCfg.OffStateRunMode TIM_OSSR_DISABLE; sBreakDeadTimeCfg.OffStateIDLEMode TIM_OSSI_DISABLE; sBreakDeadTimeCfg.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeCfg.DeadTime 36; /* DTG 值72 MHz 下约 500 ns */ sBreakDeadTimeCfg.BreakState TIM_BREAK_DISABLE; sBreakDeadTimeCfg.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeCfg.BreakFilter 0; HAL_TIMEx_ConfigBreakDeadTime(htim1, sBreakDeadTimeCfg); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); /* 主输出 CH1 */ HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); /* 互补输出 CH1N */参数说明Prescaler 为 0 时定时器直接吃 72 MHzARR 为 1800中心对齐模式下输出对称 PWM比边沿对齐的谐波表现更干净。CounterMode 用 CENTERALIGNED1指的是计数器向上计数时在 CCR 匹配点输出有效电平向下计数时在反向匹配点翻转形成中心对称波形。Pulse 初值 900 对应 50% 占空比这只是上电后到 DMA 启动前的一个安全值避免引脚悬空或驱动桥误动作。DeadTime 字段写入 36对应关系是 DTG 值乘以计数时钟周期72 MHz 下一个计数周期约 13.9 ns36 个周期约 500 ns。这个初始死区比较保守烧到实际板子上以后还要用示波器重新校准。Open drain 的 GPIO 配置这里省略了但有一条提示CH1N 对应引脚必须在 GPIO 模式下复用为 AF2且 IMP 内部上拉不要打开否则互补输出在死区期间的电平会被外部上下拉电阻破坏。3.3 用 DMA 循环搬运正弦表释放 CPU 负载载波 20 kHz 意味着每 50 µs 就要更新一次 CCR。如果靠中断里查表再写寄存器50 µs 内要完成查表、计算地址、写外设CPU 还能做其他事但中断频繁进入会干扰实时性。更干净的做法是 DMA 循环搬运正弦表的 400 个值按顺序写入 TIM1 的 CCR1DMA 传输完成一圈后自动从头开始中间不需要 CPU 参与。DMA_HandleTypeDef hdma_tim1_ch1; static void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_tim1_ch1.Instance DMA1_Channel2; hdma_tim1_ch1.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_tim1_ch1.Init.PeriphInc DMA_PINC_DISABLE; hdma_tim1_ch1.Init.MemInc DMA_MINC_ENABLE; hdma_tim1_ch1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_tim1_ch1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_tim1_ch1.Init.Mode DMA_CIRCULAR; hdma_tim1_ch1.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_tim1_ch1); __HAL_LINKDMA(htim1, hdma[TIM_DMA_ID_CC1], hdma_tim1_ch1); }逻辑说明Direction 是内存到外设PeriphInc 关闭代表外设地址始终指向 TIM1 的 CCR1MemInc 打开代表每传一个表项源内存地址自动加一。数据宽度选 HALFWORD16 位因为 CCR 寄存器和 uint16_t 表完全对齐若错用 BYTE 或 WORDDMA 会把两个表项拼成一个错误值。Mode 选 CIRCULAR 后传输 400 次地址自动回到表头形成无限循环。初始化顺序有讲究必须先初始化 DMA再初始化定时器最后调用 HAL_TIM_PWM_Start_DMA。顺序反了比如先启动 PWM 再配 DMA定时器会用一个固定的 Pulse 值跑好几个周期在驱动桥上表现为直流偏置电机启动瞬间可能抖动一下。启动 DMA 时外设地址由 HAL_TIM_PWM_Start_DMA 内部从定时器句柄绑定不需要在 MX_DMA_Init 里手动指定。3.4 改变输出频率时三个参数必须捆绑重算很多从 50 Hz 出发改参数的人会把输出频率直接写进一个变量期望 40 Hz 就是变量改成 40 这么简单。实际不是。上一节已经提到表长度 N 载波频率 / 输出频率。让输出变成 60 Hz若保持载波 20 kHzN 应该是 333.33 这样一个非整数。整数表长度无法精确表达这个比例常见做法有两种一是用 333 点表接受 0.1% 频偏适合毕业设计验证二是用相位累加器在第 5 章展开。另外运行时改动 ARR 也有讲究。中心对齐模式下 ARR 修改要到下一个更新事件才生效而且切换瞬间计数器可能处在上升或下降段的任意位置输出会先跳出一个宽度异常的脉冲。对电机负载来说这一下相当于给绕组加了一个直流瞬态。安全顺序是先关断 PWM 输出修改 DMA 表长度和 ARR清空计数器再重新启动。对变频器这类连续调速场景一般不用改 ARR而是借助 DDS 相位累加器平滑改变采样点的步进这一做法的事情在第 5 章细说。4. SPWM 死区时间、载波频率与逆变桥驱动的配合4.1 死区时间是怎么插入的DTG 计算公式与寄存器配置死区的必要性来自功率器件的关断延迟MOSFET 和 IGBT 的关断时间通常比开通时间长如果互补 PWM 的上桥和下桥在同一时刻切换上桥还没完全关断、下桥就已经导通母线电压直接对地短路几十安培电流瞬间烧毁管子。死区的本质是在上桥下降沿到下桥上升沿之间插入一段“两路都关断”的窗口。STM32 高级定时器的死区由 BDTR 寄存器低字节 DTG 字段控制。沿用上一章 500 ns 的配置推导过程是t_DTS 1 / 72 MHz ≈ 13.9 ns DTG 500 ns / 13.9 ns ≈ 36写入寄存器就是把这个十进制 36 直接填进 DTG 位段。注意 DTG 到死区时间的映射不是全程线性的当 DTG 大于 127 时参考手册里分成了几段不同的倍率此时不能简单地用“目标时间除以 t_DTS”计算要对照 RM0008 的表格查映射关系目标死区时间t_DTS 周期数DTG 写入值实际死区时间0.5 µs3636约 500 ns1.0 µs7272约 1.0 µs2.0 µs1441442 × t_DTS × (64 低 5 位)5.0 µs360256按分段公式换算配置完成后用示波器双通道同时抓 CH1 和 CH1N放大下降沿处可以看到一方拉低后另一方才拉高的缺口缺口宽度就是死区时间。比配置值略大是正常的因为还有栅极驱动 IC 的传播延迟通常 IR2104 这类芯片会把死区再撑大几十纳秒。4.2 载波频率20 kHz 是最稳的起点但不是越高越好选择载波频率本质是在三个因素间折中电流纹波、开关损耗、最小脉宽裕量。载波频率越高电感绕组里的电流脉动越小电机噪音越低沉甚至超出人耳范围但 MOSFET 的开关损耗随频率线性上升25 kHz 相对于 10 kHz单管开关损耗也大约翻倍。另一个容易被忽略的是最小脉宽问题。SPWM 在正弦过零点附近占空比接近于零CCR 最小值如果只剩几个计数周期对应的高电平宽度可能比栅极驱动 IC 的最小脉宽还短输出级的脉冲直接被吞掉造成波形失真。所以调制比 M 不要选太大而且 LUT 里最好把 CCR 最小值钳位在 4 个计数周期以上。下面这张表是逆变器实验里比较常用的参数组合供参照载波频率ARR72 MHz 中心对齐正弦台阶数开关损耗适用场景10 kHz3600200较低大功率 IGBT散热条件有限16 kHz2250320中等通用变频器常见选择20 kHz1800400中等推荐起步值24 kHz1500480较高静音要求高的电机驱动20 kHz 恰好越过人耳最敏感频段噪音小同时 ARR 还有 1800 的占空比分辨率配合 400 点表已经做到每 0.9° 一个采样THD 实测通常在 1% 以下。先在这个点调通再根据散热和输出谐波测试结果决定要不要升频。4.3 与半桥驱动电路连接时最容易漏掉的四个点把 TIM1 的 CH1 和 CH1N 接到 IR2104、EG2104 之类的半桥驱动芯片时硬件和软件边界最容易出错的有四处我按排查优先级列出来常见错误故障现象检查方法只启动了 CH1忘记启动 CH1N桥臂只有上管有信号输出波形整体只有半周确认调用了 HAL_TIMEx_PWMN_Start死区时间设成 0上电瞬间桥臂直通电流采样电阻发烫断开母线单测 T1/T2 门极波形调制比 M 超过 0.95正弦峰值处脉冲削顶输出电压接近方波降低 M或观察 CCR 最大值是否贴到 ARR互补引脚接反上桥开关和下桥开关逻辑对调用示波器对比 CH1 与 CH1N 极性最后一条讲一下自举电容半桥驱动芯片的高侧供电通常靠自举电容从低侧开通时充电SPWM 低频输出且调制比很低时低侧连续多个周期不动作自举电压下降高侧驱动会间歇失效。现象是示波器上高侧波形每隔一段就丢失一两个脉冲。解决办法是提高载波频率或在调制策略里保证最低脉宽不小于自举电容一个开关周期所需的电荷补充时间。5. SPWM 波形验证与进阶频率合成FFT 和相位累加器5.1 示波器上怎么判断 SPWM 是否真正合格先从简单看起。把示波器探头接到逆变桥输出端不加 LC 滤波直接抓 PWM 波观察脉冲宽度的包络一个正弦周期内两端最窄、中间最宽包络呈完整正弦轮廓并且正半周和负半周完全对称。如果包络左右不对称问题可能在正弦表首尾不连续或者 DMA 传输长度和表长度对不上。想定量评估把示波器切到 FFT 模式在 PWM 信号上做频谱测量。正常的 SPWM 频谱应该有三个特征基波 50 Hz 幅度最高载波频率 20 kHz 处出现成对的边带峰边带随远离载波而衰减。如果基波附近出现明显的 100 Hz 或 150 Hz 杂散峰说明正弦表本身含有偶次谐波常见原因是 N 取了偶数但 ARR 计算时把中心对齐公式记成边沿对齐。THD 的近似计算可以只取前 10 次谐波THD 谐波能量和的平方根/基波幅度。低于 2% 就说明这套 SPWM 程序在驱动普通阻感性负载时已经够用。5.2 用相位累加器做 40 Hz 到 60 Hz 连续可调固定 400 点表只能精确输出 50 Hz因为 400 个点正好对应一个正弦周期。想连续调频业内标准做法是 DDS 相位累加器正弦表只存一个固定长度的相位样本比如 1024 点每次 PWM 中断时给累加器加上一个与目标频率成正比的增量取累加器高位作为表索引。phase (uint32_t)(f_out * 4294967296.0 / f_switch); idx (phase 22) 0x3FF; TIM1-CCR1 lut[idx];f_out 50.5 Hz 时相位增量是 50.5 × 2³² / 20000 ≈ 10 831 282每次中断相位前进约 0.0025 圈查表输出自然形成 50.5 Hz 正弦不需要重建表。改变频率时直接改增量相位保持连续波形不会产生阶跃。代价是每次 PWM 周期都要中断一次去更新 CCR20 kHz 载波下 CPU 负担可以接受。这种方案的输出频率精度取决于相位累加器的位数32 位到 1 mHz 级别已经远高于模拟变频器常用的电位器调频精度。有条件的话最后再做一步验证把连续整周期的 CCR 序列通过串口导出在 PC 上做一次离散傅里叶变换用谐波能量占比替代示波器上的肉眼判断这是确认 SPWM 程序质量最快、最能定量对比死区与调制比参数的方法。本文还有配套的精品资源点击获取
返回列表