☰
STM32F103C8T6实现三相SPWM:TIM1互补PWM与死区配置详解
2026/10/16 7:00:26 网站建设 项目流程

简介:针对STM32F103C8T6的SPWM三相六路互补输出工程,基于高级定时器TIM1查表法实现,适合电机驱动、电力电子及嵌入式控制学习者参考,可作为三相电机调速控制开发的参考模板。资源包体积约46.67MB,共149个文件,核心包括C源码与头文件、KEIL5工程配置、可烧录调试的hex/axf,以及PDF原理文档、正弦表生成工具和死区计算器等辅助内容。工程输出相位互差120°的三相六路带死区SPWM信号,采用恒压频比方式调频调压,可直接驱动三相电机;程序注释详细,支持KEIL5在线仿真直接观察输出波形。另配套SPWM生成原理学习资料和实用工具,能帮助理解查表法、死区设置以及恒压频比调频调压的实现细节。目前已有8067人学习下载,适合需要完整参考工程或入门三相SPWM驱动开发的读者。

1. 一颗 F103C8T6 做三相 SPWM:别等专用驱动板到手才动手

很多开发者第一次接触三相逆变或电机驱动,第一反应是“得上 DSP 或者专用电机控制芯片”。实际上,一块普通的 STM32F103C8T6 系统板,靠片上一颗高级定时器 TIM1,就能生成三相逆变器需要的六路互补 PWM,死区由硬件插入,调频调压靠一张正弦表和相位累加器完成,不需要外挂 PWM 芯片。这套工程就是围绕这个思路组织的 KEIL5 工程,编译之后可以直接在模拟器里跑,用逻辑分析仪观察六路波形和死区宽度,确认波形形态后再上真板。想做三相 SPWM、想搞清楚高级定时器互补输出怎么配的开发者,这份工程能省下不少找资料的时间。

2. 方案与架构:一颗 TIM1 如何撑起三相六路互补输出

先把这个工程拆成三个层次看:功率层、脉冲层、调制层。功率层是三相全桥的六个开关管;脉冲层由 TIM1 负责,产生六路带死区的互补 PWM;调制层在 CPU 里,由相位累加器和正弦表决定每个开关周期的占空比。

很多教程一上来就把代码塞给你,结果看到“TIM_CtrlPWMOutputs”“TIM_BDTRInit”这些函数完全不知道在干嘛。实际上只要把架构理顺,后面每个寄存器都是可以倒推出来的。

2.1 三相全桥和互补 PWM:六路信号与六个开关管的对应关系

三相逆变器的功率部分,本质上是三个半桥并联,每个半桥有上管和下管。要让电机或负载得到正弦变化的电压,控制逻辑就是让上下管以互补方式导通:上管开的时间比例按正弦变化,下管在剩下时间里开。三路正弦彼此相差 120°,于是就有了 U、V、W 三相。

在 STM32F103C8T6 上,TIM1 恰好提供三对互补通道。普通定时器也有 PWM 输出,但只有 TIM1 这类高级定时器能把“互补”和“死区”用硬件做出来。工程里常用的引脚映射关系如下:

输出信号定时器通道引脚对应半桥
U 相上管TIM1_CH1PA8U 半桥上臂
U 相下管TIM1_CH1NPB13U 半桥下臂
V 相上管TIM1_CH2PA9V 半桥上臂
V 相下管TIM1_CH2NPB14V 半桥下臂
W 相上管TIM1_CH3PA10W 半桥上臂
W 相下管TIM1_CH3NPB15W 半桥下臂

注意 TIM1 的 CH4 没有互补输出,所以三相 PWM 必须用 CH1、CH2、CH3 这前三路。工程里的初始化代码也只开了这三个通道,而不是把 CH4 也配成普通 PWM。

2.2 死区为什么要由硬件做:软件插死区在逆变器场景下不现实

上下管互补,意味着翻转瞬间存在一个危险窗口:如果上管还没完全关断,下管已经导通,直流母线就相当于被短路。这个现象做硬件的人叫“直通”,轻则波形畸变,重则炸管子。

软件能不能在翻转沿之间手动插入一段死区?可以,但工程上没人这么做。原因有两个:一是死区宽度通常是微秒级,而中断响应存在延迟和抖动,软件翻转很难稳定到几百纳秒;二是每次都要关中断、算延时、再翻转,会把简单逻辑拖得很复杂。TIM1 的做法是让 OCx 和 OCxN 的输出本身错开一段时间,这段错开的时间由死区寄存器配置,硬件自动完成,CPU 不需要参与每次翻转。

这个设计还带来一个额外好处:如果需要紧急刹车,可以直接把刹车输入接到 TIM1 的刹车引脚,硬件会在一个计数周期内封锁全部输出。工程里把刹车功能关掉,是为了先让波形跑起来;后面做电动工具这类有急停需求的项目时,再把它打开即可。

2.3 调频调压的结构选择:固定载波加 DDS 相位累加器

SPWM 的本质很简单:用高频三角波或锯齿波作为载波,用一个频率较低的正弦波作为调制波,两者比较得到占空比随时间变化的 PWM。载波频率一般固定,而正弦波的频率和幅度需要可调。

工程里我选的是“固定载波 + 相位累加器”这种方式。载波固定为 10kHz,正弦表用 1024 个点表示一个完整的 360°。要让输出是 50Hz,那么每 1ms 的载波周期里,正弦表指针应该走 1024 × 50 / 10000 = 5.12 个点。问题来了,指针不能走小数步,于是工程里用了一个 32 位相位累加器来累积相位增量,每次中断只取相位的高 10 位作为表格索引。这样输出频率就和 DDS 一样连续可调,不会有“只能出 19.5Hz、39Hz、58Hz 这种跳变”的问题。

调压的方式则是乘一个调制比。正弦表保存的是 0 到 32767 之间的无符号值,实际计算 CCR 时先根据目标电压算出幅度 amp,再把表值换算到 [0, amp] 区间。若做电机驱动,还要让调制比跟随频率变化,这就是常说的恒压频比,低频时再叠一点电压补偿。这套逻辑都集中在 SPWM 核心文件里,和硬件寄存器解耦。

3. 核心代码拆解:从定时器寄存器到六路波形输出的完整链路

这一章是整个工程最容易照抄的部分。我会按“先看工程目录,再配定时器,再算死区,最后写调制算法”的顺序把代码过一遍。代码可能和你手头工程略有出入,但原理一致。

3.1 工程目录速览:KEIL5 工程文件职责

这份工程不是把所有代码都塞进 main.c,而是按标准外设库的习惯分了几层。目录大致长这样:

spwm_f103 ├── SPL │ ├── inc # 标准外设库头文件 │ └── src # 标准外设库源码 ├── USER │ ├── main.c # 主流程、按键调频调压 │ ├── stm32f10x_it.c # TIM1 更新中断入口 │ └── system_stm32f10x.c # 系统时钟初始化 ├── BSP │ ├── bsp_tim.c # TIM1 GPIO、PWM、死区初始化 │ ├── bsp_tim.h │ ├── spwm_core.c # 正弦表、相位累加器、CCR 计算 │ └── spwm_core.h └── Project ├── spwm.uvprojx # KEIL5 工程文件 └── spwm.uvopt # 调试和仿真配置

main.c 只负责调用初始化函数和响应按键;真正干活的逻辑在三个文件里:bsp_tim.c 管硬件,spwm_core.c 管波形算法,stm32f10x_it.c 做中断回调。这样分的好处是,以后要移植到 HAL 库或换到 F4,只需要改 bsp_tim.c,正弦表和相位累加器可以原样搬走。

3.2 TIM1 初始化:三对互补通道和主输出使能的坑

先用标准外设库写一遍 TIM1 初始化。代码里每一步都和寄存器一一对应,注释标清楚:

void SPWM_TIM1_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tbase; TIM_OCInitTypeDef oc; TIM_BDTRInitTypeDef bdtr; /* 开启 TIM1、GPIOA、GPIOB、AFIO 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); /* CH1/CH2/CH3 主输出引脚:PA8/PA9/PA10 */ GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); /* CH1N/CH2N/CH3N 互补输出引脚:PB13/PB14/PB15 */ gpio.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOB, &gpio); /* 时基配置:72MHz / (psc+1),向上计数,周期 arr */ tbase.TIM_Prescaler = psc; tbase.TIM_CounterMode = TIM_CounterMode_Up; tbase.TIM_Period = arr; tbase.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, &tbase); /* PWM1 模式,打开主输出和互补输出 */ oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_OutputNState = TIM_OutputNState_Enable; oc.TIM_Pulse = 0; oc.TIM_OCPolarity = TIM_OCPolarity_High; oc.TIM_OCNPolarity = TIM_OCNPolarity_High; oc.TIM_OCIdleState = TIM_OCIdleState_Reset; oc.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, &oc); TIM_OC2Init(TIM1, &oc); TIM_OC3Init(TIM1, &oc); /* 死区寄存器,后面单独讲 */ bdtr.TIM_OSSRState = TIM_OSSRState_Enable; bdtr.TIM_OSSIState = TIM_OSSIState_Disable; bdtr.TIM_LOCKLevel = TIM_LOCKLevel_OFF; bdtr.TIM_DeadTime = 0x96; /* 约 2.39us */ bdtr.TIM_Break = TIM_Break_Disable; bdtr.TIM_BreakPolarity = TIM_BreakPolarity_High; bdtr.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRInit(TIM1, &bdtr); /* 预装载:让 CCR/ARR 修改在更新事件统一生效,避免中途跳变 */ TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); /* 主输出使能,也就是 MOE 位 */ TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }

这段代码里最容易被忽略的是TIM_CtrlPWMOutputs(TIM1, ENABLE)。高级定时器里有一个主输出使能位,官方手册里叫 MOE,它不使能的话,哪怕 CH1 已经工作,引脚上也不会有输出波形。很多人第一次调这种工程,看到寄存器都对了但示波器没信号,翻车基本都是栽在这一句上。

参数说明:arr = 7199, psc = 0时,定时器时钟 72MHz,周期 7200,载波频率正好是 72MHz / 7200 = 10kHz。如果想让载波变成 20kHz,可以把 arr 改成 3599。

3.3 死区时间计算:BDTR 寄存器里填的不是纳秒数

TIM1 的死区由 BDTR 寄存器的低 8 位 DTG 控制。新手最容易踩坑的地方就是直接把这 8 位当成死区计数,以为填多少就是多少微秒,结果实际差了一大截。

这 8 位的含义是分组的,DTG 的高三位决定步长,低五位决定计数:

DTG[7:5]死区计算公式最细步长
0xxDT = DTG[7:0] × Tdts1 × Tdts
10xDT = (64 + DTG[4:0]) × 2 × Tdts2 × Tdts
110DT = (32 + DTG[4:0]) × 8 × Tdts8 × Tdts
111DT = (32 + DTG[4:0]) × 16 × Tdts16 × Tdts

Tdts 等于定时器计数时钟的周期。工程里TIM_ClockDivision = TIM_CKD_DIV1,所以 Tdts = 1 / 72MHz ≈ 13.9ns。

代码里填的0x96,二进制是 1001 0110。高三位是 100,属于第二组公式,低五位是 0x16 = 22。那么死区时间就是 (64 + 22) × 2 × 13.9ns ≈ 2.39us。这个值对常用 MOS 管驱动来说比较合适,实际使用时要根据你的功率管栅极电阻和驱动芯片调整,一般 IGBT 会用到 2us 以上,低压 MOS 可以收到 1us。

验算方式很简单:先把 DTG 拆成高三位和低五位,确定用哪一档公式,再代入 Tdts 计算。我一般会在代码里把死区值做成宏,比如#define PWM_DEADTIME_NS 2390,然后在调试器里直接看最终写入 BDTR 的寄存器值,省得每次手算。

3.4 SPWM 核心:正弦表、相位累加器和 CCR 更新

波形生成放在 spwm_core.c 里。首先准备一张 1024 点的正弦表,每个点保存 0 到 32767 的幅值,这样后面可以直接用移位代替除法:

#include <math.h> #define TABLE_SIZE 1024 #define CARRIER_HZ 10000 uint16_t sin_table[TABLE_SIZE]; void SinTableInit(void) { for (uint16_t i = 0; i < TABLE_SIZE; i++) { float s = sinf(2.0f * 3.1415926535f * i / TABLE_SIZE); sin_table[i] = (uint16_t)((s + 1.0f) * 32767.0f / 2.0f); } }

这里把负半周也平移成正的,因为定时器比较寄存器只能给正数。相位处于 180° 时表值接近 0,占空比最小;相位 0° 时表值约 16383,一半占空比;90° 时表值接近 32767,占空比最大。

接下来是调频调压的核心,32 位相位累加器:

static uint32_t phase_acc; volatile uint32_t g_phase_inc; volatile uint16_t g_volt_m = 1000; /* 调制比,1000 表示 100% */ void SetSpwmFrequency(uint32_t fs_hz) { /* fs_hz 是正弦波目标频率,例如 50Hz */ g_phase_inc = (uint32_t)(((uint64_t)fs_hz << 32) / CARRIER_HZ); } void SetSpwmVoltage(uint16_t volt_per_thousand) { g_volt_m = volt_per_thousand; /* 0~1000 */ }

频率设定采用 DDS 思路:累加器是 32 位,累加满 2^32 次计数相当于跑完一个正弦周期。每个载波周期做一次累加,所以写入 50Hz 时,相位增量是 50 × 2^32 / 10000 ≈ 21474836,这个数在 32 位整数里正好对应 49.9999Hz 左右,精度足够。

中断里做查表和 CCR 更新:

void TIM1_UP_IRQHandler(void) { uint32_t idx, arr, amp; uint16_t ccr_u, ccr_v, ccr_w; if (TIM_GetITStatus(TIM1, TIM_IT_Update) == RESET) { return; } TIM_ClearITPendingBit(TIM1, TIM_IT_Update); phase_acc += g_phase_inc; idx = phase_acc >> (32 - 10); /* 取高 10 位,正好映射 1024 点 */ arr = TIM1->ARR; /* 当前载波周期值,10kHz 时是 7199 */ amp = arr * g_volt_m / 1000U; /* 调压后的最大比较值 */ /* U 相直接用当前相位,V/W 相分别移 120° 和 240° */ ccr_u = (uint16_t)(((uint32_t)sin_table[idx] * amp) >> 15); ccr_v = (uint16_t)(((uint32_t)sin_table[(idx + 341) & 1023] * amp) >> 15); ccr_w = (uint16_t)(((uint32_t)sin_table[(idx + 682) & 1023] * amp) >> 15); TIM_SetCompare1(TIM1, ccr_u); TIM_SetCompare2(TIM1, ccr_v); TIM_SetCompare3(TIM1, ccr_w); }

注意三点:第一,1024 / 3 不是整数,所以 V 相偏移取 341,W 相偏移取 682,误差只有 0.12° 左右,对 SPWM 来说完全可接受;第二,正弦表最高值是 32767,右移 15 位相当于除以 32768,所以当表值最大时,ccr 就是 amp;第三,TIM1->ARR直接读寄存器,比读缓存变量更保险,因为预装载会让 ARR 在边界处更新,两者一旦不同步就可能产生一个周期的异常占空比。

3.5 载波、调制比和主循环:按键调频调压怎么接进来

主循环里只需要处理按键和参数更新:

int main(void) { /* 系统时钟、GPIO、串口等基础初始化省略 */ SinTableInit(); SPWM_TIM1_Init(7199, 0); /* 10kHz 载波 */ SetSpwmFrequency(50); SetSpwmVoltage(800); /* 开 TIM1 更新中断 */ TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); NVIC_EnableIRQ(TIM1_UP_IRQn); while (1) { /* 扫描按键,改频率时先调 SetSpwmFrequency,再调 SetSpwmVoltage */ } }

调压和调频是两套独立参数,互不干扰。做电机驱动时,通常把恒压频比写成一个函数,比如set_vf(fs_hz),内部先算目标电压,再同时调 g_phase_inc 和 g_volt_m。低频启动时还需要加一个 boost 电压,这个后面在二次开发里说。

4. 在 KEIL5 里跑工程并观察六路波形

这一章讲怎么把工程在模拟器里跑起来。很多人拿到一个能编译的工程,却卡在“点开 Debug 后没有任何波形”这一步,这里把配置细节都过一遍。

4.1 导入工程和芯片型号配置

在 KEIL5 里打开工程的 uvprojx 文件,第一次编译前要做三件事。第一,确认工程设置里的芯片型号是 STM32F103C8,如果 pack 没装,在 Pack Installer 里补齐对应的器件包;第二,检查 C/C++ 选项里的 Include Path 是否包含了 SPL 的 inc 目录和 USER 目录,否则会报找不到 stm32f10x.h;第三,在 Target 标签页里把 Xtal 设置成板子实际晶振频率,比如 8MHz,这样 SystemInit 算出的 72MHz 和模拟器里的时间基准才对得上。

工程默认配置了“Use Simulator”调试方式。这里有个很容易忽略的选项:如果你之前用过真机调试,Keil 可能还停留在“Use ULINK”这类状态,需要在 Options for Target 的 Debug 标签页切换到 Use Simulator,并且在右边窗口勾选 Run to main,否则一进调试就是汇编入口,新手容易懵。

4.2 逻辑分析仪设置:从哪里看六路波形

进入 Debug 后,从菜单打开 View → Analysis Windows → Logic Analyzer。逻辑分析仪可以显示 GPIO 引脚状态,所以只需要观察 PA8、PA9、PA10、PB13、PB14、PB15 六个引脚。

在 Logic Analyzer 窗口里添加信号时,不同版本的 KEIL 语法不太一样。新版本可以直接输入PORTB.13,如果输入不了,就写PORTB & 0x2000,然后在 Setup 里把 Bit Mask 设成对应值。添加完成后,设置信号颜色,连续运行一段时间再暂停,就能看到六个方波。

要确认死区,比较同一组主通道和互补通道。比如观察 PA8 和 PB13,放大到单周期,能看到两者同时为低的一小段电平,那段宽度就是死区。用光标测量这个低电平时间,对照 BDTR 寄存器算出的 2.39us,能验证配置是否正确。

4.3 模拟器里“占空比不动”的常见原因

如果运行后波形是固定的,不像正弦变化,先检查 TIM1 更新中断有没有被触发。在中断函数入口打断点,运行一次看是否进入。常见的原因有几个:一是没有调用TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE),中断根本没开;二是 NVIC 优先级没配,但默认情况下也有响应;三是模拟器对高级定时器的支持有时不完整,更新事件和死区生成未必完全复现真机行为。

遇到这种情况,我一般会加一个辅助变量g_int_count,每进一次中断加一,在 Watch 窗口观察它是否在运行中增长。如果计数值在涨,说明中断链路通了,问题出在 CCR 更新或者逻辑分析仪的采样设置上;如果计数值为零,问题就在中断配置。

4.4 真板运行前的最小验证思路

模拟器能确认逻辑,但没办法验证功率级的接线和驱动芯片的极性。上真板之前,建议先断开直流母线,用低压 12V 或 24V 给逆变桥供电,输出端接一个几十欧的功率电阻或者白炽灯,再给定时器引脚接上示波器。这一步只验证开关管的栅极波形和死区是否正常,不要直接接三相电机和高压母线。

我习惯把真板验证分成两步:先看栅极波形,确认六个引脚开关时序正确;再接电机空载测试,确认输出频率和电压可调。如果第一步栅极波形有重叠导通,立刻断电查极性配置,这是整个工程里最不能侥幸的环节。

5. 避坑记录:死区寄存器、MOE 和仿真波形的五个踩坑现场

这里把开发过程中最容易翻车的五个问题整理出来,每条按现象、原因、解决来写,方便你对照排查。

5.1 死区时间怎么调都差一个数量级

现象:在 BDTR 里填了 0x32,预期死区是 50 × 13.9ns ≈ 0.7us,但示波器量出来明显偏大或偏小,怎么都不对。

原因:0x32 的二进制是 0011 0010,高三位是 001,属于第一组公式,确实可以按 DTG × Tdts 算。但很多人把 0x96、0xF0 这类值当成相同含义来填,没有意识到高三位不同的时候,计算公式完全不同。死区寄存器不是一个线性计数器,而是一个分段映射表。

解决:先拆高三位确定用哪一组公式,再算低五位系数。例如想要 2.4us,先选择 100 组,公式是 (64 + DTG[4:0]) × 2 × Tdts,代回去算出 DTG[4:0] 是 22,即 0x96。不要把记忆中的“0x96 对应 2us”直接套到不同芯片上,关键看 Tdts 是多少。

5.2 寄存器全对,引脚就是没有输出

现象:模拟器和真板上都能看到定时器计数在跳,CCR 也在更新,但 PA8、PB13 这些引脚就是没有 PWM 波形。

原因:TIM1 这类高级定时器有一个主输出使能位 MOE,它位于 BDTR 寄存器中。标准外设库的TIM_CtrlPWMOutputs(TIM1, ENABLE)就是控制这个位。如果没有调用,整个定时器的输出级被封锁,引脚只能保持复位状态。这个位在日常的普通 PWM 例程里不涉及,所以很多人第一次用高级定时器时会漏掉。

解决:确认初始化代码里调用了TIM_CtrlPWMOutputs(TIM1, ENABLE),并且它要放在TIM_Cmd之前还是之后都不是关键,关键是必须执行。真机调试时可以直接看 BDTR 寄存器的 MOE 位是否为 1。

5.3 主引脚有波形,互补引脚却是死的

现象:PA8 有正常的 PWM,PB13 一直保持低电平,怎么看都不像互补输出。

原因:互补引脚和主引脚虽然属于同一个定时器通道,但 GPIO 复用的是两套引脚。如果只初始化了 GPIOA,没有初始化 GPIOB,那么 PB13、PB14、PB15 根本不会被复用成定时器输出功能。芯片内部定时器确实在输出,但这些引脚仍作为普通 GPIO 保持默认电平。

解决:在初始化里把 GPIOB 的时钟打开,并把 PB13/PB14/PB15 配置成复用推挽输出。检查引脚一定要对着参考手册的复用功能表,不要凭直觉以为 CH1N 也在 PA 口上。

5.4 PWM 极性配反,上真板一开就炸

现象:模拟器里看到的波形很正常,但接到驱动板上,母线一旦上电就过流,甚至烧掉功率管。

原因:OCPolarity 和 OCNPolarity 决定的是输出信号有效电平,而这个有效电平必须和你的栅极驱动电路匹配。如果驱动芯片是低电平导通,而代码里配成高电平有效,那关断指令会变成导通指令。换句话说,你以为是互补 PWM,实际输出变成了同相 PWM,上下管一起导通。

解决:上真板之前,先把驱动原理图的逻辑搞清,确认栅极是高电平有效还是低电平有效,再决定极性。这一步在模拟器里是看不出来的,必须用数据手册核对。验证时先用低压小功率测试,严禁空手去摸母线段。

5.5 输出频率跳档,50Hz 变成 39Hz 或 58Hz

现象:程序里写了 50Hz,实际测出来却不是整 50Hz,波形频率会在某些值之间跳变。

原因:直接用整数步进去走正弦表,比如每周期前进 2 步,1024 点表在 10kHz 载波下只能得到 19.5Hz 的倍数,没有 50Hz 这个档。这就是没有用相位累加器导致的频率量化问题。

解决:把“正弦表索引”改成 32 位相位累加器,用phase_inc = (uint64_t)fs << 32 / carrier来设定频率,每个载波周期取相位的高 10 位作为索引。这样频率分辨率能做到接近 0.001Hz,调频才是真正连续的。

6. 二次开发:把三相 SPWM 核心改造成可移植的电机驱动基础

这套工程跑通之后,下一步通常是把主循环里的按键调频调压改成串口或上位机下发指令。改造时最值得保留的是 spwm_core.c,它不应该依赖任何 STM32 外设库。为此,我一般会再把 CCR 计算封装成纯函数:

void spwm_get_ccr(uint32_t phase, uint16_t arr, uint16_t volt_m, uint16_t *ccr_u, uint16_t *ccr_v, uint16_t *ccr_w) { uint32_t amp = (uint32_t)arr * volt_m / 1000U; *ccr_u = (uint16_t)(((uint32_t)sin_table[phase] * amp) >> 15); *ccr_v = (uint16_t)(((uint32_t)sin_table[(phase + 341) & 1023] * amp) >> 15); *ccr_w = (uint16_t)(((uint32_t)sin_table[(phase + 682) & 1023] * amp) >> 15); }

这样 spwm_core.c 甚至可以在 PC 上做单元测试。喂入不同的 phase 数组,直接打印输出曲线,检查三相相位差和调制比线性度,不用上板就能完成算法验证。中断里调用这个函数,整个 ISR 逻辑会变得很短,也更容易审查。

另一个值得做的改动是增加恒压频比启动流程。现在的工程把频率和电压分开设置,但电机驱动里低频时电压如果太低,电流可能推不动转子。通常做法是加一个 boost 系数:频率低于 20Hz 时,电压不是按线性比例下降,而是保留一个最小占空比。这个逻辑可以做成查表,放在主循环里,每次设置频率后自动更新电压。

如果后续想向 SVPWM 演进,这套架构也能平滑过渡。SVPWM 的本质是六个基本电压矢量的时间组合,它最终输出的依然是三路互补 PWM,只是 CCR 计算方式从正弦表改成了扇区矢量作用时间。替换掉 spwm_get_ccr 内部实现即可,TIM1 初始化完全不动。

从那以后,我每次移植到新板子都会先做同一个动作:把死区寄存器按分档公式算一遍,在初始化函数里打印出最终写入的 BDTR 值,再用模拟器量一次波形。这个“算、写、量”的循环看起来笨,但正因为养成了这个习惯,后来再没在死区和极性问题上前功尽弃过。希望这个工程能帮你少踩几个我踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询