STM32F103C8T6六路互补PWM驱动三相电机SPWM实现

发布时间:2026/10/26 17:10:35
STM32F103C8T6六路互补PWM驱动三相电机SPWM实现

简介:面向嵌入式初学者与电机控制方向学习者的STM32实战资源,围绕STM32F103C8T6高级定时器TIM1,采用查表法生成三相六路互补SPWM信号,相位互差120°,支持死区插入与调频调压,并以恒压频比方式驱动三相电机。资源包共149个文件、约46.67MB,以C源文件与头文件为主体,配合Keil工程文件、HEX与AXF固件、MAP映射文件及PDF学习文档,另附SPWM正弦表生成工具、死区计算器和原理说明,便于对照理解波形生成过程。程序在KEIL5下编写并带详细注解,可在线仿真观察六路互补输出波形,帮助读者把定时器配置、死区时间与调压调频逻辑对应到实际代码。目前已有8068人学习下载,适合作为电机驱动入门与课程设计的参考范例。

1. 一块十几块钱的芯片,怎么把三相电机转起来

手头有个小功率三相电机要驱动,控制板预算压到几十块,方案选型绕了一圈最后还是落回 STM32F103C8T6。原因很直接:这颗芯片带三组高级定时器,能同时吐出六路互补 PWM,硬件死区可配,主频 72MHz 跑 SPWM 载波绰绰有余。这份工程做的就是这件事——用 TIM1 的六路互补输出驱动三相逆变桥,正弦表查表生成 SPWM,死区时间写进寄存器,频率和幅值都能在线调。KEIL5 打开就能编译,工程里带了仿真配置,没有示波器和功率板也能先在软件里看波形。适合正在做电机驱动、逆变电源、变频器小样,又不想从寄存器手册第一页啃起的人。

2. 工程结构与定时器资源分配:六路 PWM 到底从哪来

2.1 目录怎么摆,每个文件管什么

拿到工程先别急着点编译,把目录过一遍,后面改代码才知道往哪下手。这类 STM32 标准库工程的结构基本是固定的,我按实际拆过的习惯给你捋一遍:

SPWM_3Phase/ ├── CORE/ │ ├── core_cm3.c/h # 内核访问层,别动 │ └── startup_stm32f10x_md.s # 启动文件,中断向量表在这 ├── HARDWARE/ │ ├── timer.c/h # TIM1 六路互补 PWM 初始化,核心 │ ├── spwm.c/h # 正弦表 + 调频调压接口 │ └── led.c/h # 状态指示,调试用 ├── STM32F10x_FWLib/ # 标准外设库 ├── USER/ │ ├── main.c # 主循环,参数在这里改 │ ├── stm32f10x_it.c # 中断服务函数 │ └── system_stm32f10x.c # 时钟树配置,72M 在这 └── OBJ/ # 编译输出,含 .hex 和仿真配置

真正要盯的只有三个文件:timer.c决定六路波形从哪个引脚出来、死区多长;spwm.c决定正弦表怎么算、调频调压怎么落地;main.c决定上电后跑什么参数。其余都是脚手架。我一般拿到新工程先看system_stm32f10x.c里的时钟配置,确认SYSCLK_FREQ_72MHz是打开的,不然定时器算出来的频率全是错的,这个坑后面避坑章节还会提。

2.2 TIM1 的通道映射:为什么偏偏是这三个引脚

STM32F103C8T6 的高级定时器 TIM1 有四个通道,每个通道带一个互补输出,正好凑出 CH1/CH1N、CH2/CH2N、CH3/CH3N 六路,驱动三相桥的六个管子。引脚是硬件定死的,改不了:

通道主输出引脚互补引脚驱动对象
CH1PA8PB13A 相上管 / 下管
CH2PA9PB14B 相上管 / 下管
CH3PA10PB15C 相上管 / 下管
CH4PA11无本工程未用,可留作触发

这里有个容易翻车的点:PA9、PA10 默认是 USART1 的 TX/RX,如果你工程里还开了串口打印调试信息,引脚就打架了。要么把串口换到 USART2(PA2/PA3),要么干脆调试阶段别开串口。我一般调试期用 SWD 看变量,省得抢引脚。

2.3 时钟与载波频率的换算关系

TIM1 挂在 APB2 上,72MHz。PWM 载波频率由预分频PSC和自动重装ARR共同决定:

f_pwm = 72MHz / ((PSC + 1) * (ARR + 1))

工程里常见配置是PSC = 0、ARR = 719,算下来载波 100kHz。这个值不是随便定的:载波太低(比如 10kHz 以下)电机啸叫明显、电流纹波大;太高又会让开关损耗和死区占比上升。小功率场合 10kHz~20kHz 比较稳妥,人耳听不见,管子也扛得住。你要改载波,只动ARR就行,但记住 SPWM 的采样点数要跟着调,不然一个正弦周期里的点数太少,波形台阶感很重。

3. 核心代码拆解:正弦表、死区与调频调压的实现

3.1 正弦表怎么生成,点数取多少合适

SPWM 的本质是用一系列等幅不等宽的方波去逼近正弦。最省算力的做法是提前把正弦值算成表,运行时查表赋给比较寄存器。表长取多少是个权衡:点数多波形细腻但占 Flash、查表步进小;点数少省空间但谐波大。工程里用 256 点,一个正弦周期采 256 次,对 50Hz 输出、10kHz 载波来说,每个载波周期更新一次比较值,够用了。

#include "spwm.h" #include <math.h> #define SPWM_POINTS 256 // 正弦表点数,2 的幂方便位运算取模 #define SPWM_ARR 719 // 与 timer.c 中 ARR 保持一致 uint16_t sin_table[SPWM_POINTS]; /* 上电时调用一次,生成 0~2π 的归一化正弦表 */ void SPWM_Table_Init(void) { for (uint16_t i = 0; i < SPWM_POINTS; i++) { /* sin 值域 [-1,1],映射到 [0, SPWM_ARR] 的比较值区间 */ float s = sinf(2.0f * 3.1415926f * i / SPWM_POINTS); sin_table[i] = (uint16_t)((s + 1.0f) * 0.5f * SPWM_ARR); } }

这段代码的关键在映射公式:(s + 1) * 0.5 * ARR。正弦值从 -1 到 1,加 1 变成 0 到 2,乘 0.5 归一到 0 到 1,再乘ARR落到比较寄存器的合法区间。这样占空比 50% 对应正弦过零点,占空比 0% 和 100% 对应波峰波谷。注意sinf是浮点运算,只在初始化时跑一次,运行时绝不进浮点,这是裸机 SPWM 的基本纪律——中断里做浮点,时序必崩。

3.2 三相互补输出与死区寄存器配置

死区是这份工程的重点。三相桥同一相的上下管绝不能同时导通,否则母线直接短路,管子当场炸。死区就是强制上下管都关断的一小段时间。STM32 的BDTR寄存器专门管这个,DTG[7:0]八位按公式换算成纳秒级延时。

void TIM1_PWM_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; TIM_BDTRInitTypeDef bdtr; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); /* PA8/PA9/PA10 复用推挽,主输出 */ 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); /* PB13/14/15 互补输出 */ gpio.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOB, &gpio); tb.TIM_Period = arr; // ARR,决定载波频率 tb.TIM_Prescaler = psc; // PSC tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, &tb); /* 三通道统一配成 PWM1 模式,互补使能 */ oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_OutputNState = TIM_OutputNState_Enable; // 关键:开互补 oc.TIM_OCPolarity = TIM_OCPolarity_High; oc.TIM_OCNPolarity = TIM_OCNPolarity_High; oc.TIM_Pulse = 0; TIM_OC1Init(TIM1, &oc); TIM_OC2Init(TIM1, &oc); TIM_OC3Init(TIM1, &oc); /* 死区配置:DTG=0x60 约对应 1us 量级,按实际管子开关时间调 */ TIM_BDTRStructInit(&bdtr); bdtr.TIM_DeadTime = 0x60; bdtr.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, &bdtr); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须开主输出 TIM_Cmd(TIM1, ENABLE); }

几个参数必须说清楚。TIM_OutputNState_Enable是互补输出的开关,漏了这句只有主输出、没有反相波形,桥臂根本推不起来。TIM_CtrlPWMOutputs是高级定时器特有的总闸,不开它前面配得再对引脚也没波形,这个坑我踩过不止一次。死区值0x60是按 DTG 编码表估的,实际要拿示波器量上下管驱动波形,保证死区大于管子的关断时间(常见 MOS 在几百纳秒),又别大到明显畸变输出。没有示波器就先用保守值,宁可大一点。

3.3 调频调压:改 ARR 还是改比较值

调频和调压是两套独立机制,别混。调频改的是载波频率,动ARR;调压改的是调制比,动查表后的比较值缩放系数。工程里把两者拆成两个接口:

static uint16_t phase_a = 0, phase_b = 0, phase_c = 0; static uint16_t amp_scale = SPWM_ARR; // 调压系数,满幅时等于 ARR /* 调压:0~100 对应 0~满幅 */ void SPWM_SetAmplitude(uint8_t percent) { if (percent > 100) percent = 100; amp_scale = (uint16_t)((uint32_t)SPWM_ARR * percent / 100); } /* 调频:改正弦表步进,步进越大输出频率越高 */ void SPWM_SetFrequency(uint16_t step) { /* step 由目标频率反算:step = f_out * SPWM_POINTS / f_pwm */ /* 这里只存步进,实际更新在载波中断里做 */ spwm_step = step; } /* 在 TIM1 更新中断里调用,每个载波周期推进一次相位 */ void SPWM_Update(void) { phase_a = (phase_a + spwm_step) & (SPWM_POINTS - 1); phase_b = (phase_b + spwm_step + SPWM_POINTS / 3) & (SPWM_POINTS - 1); phase_c = (phase_c + spwm_step + 2 * SPWM_POINTS / 3) & (SPWM_POINTS - 1); TIM_SetCompare1(TIM1, (uint32_t)sin_table[phase_a] * amp_scale / SPWM_ARR); TIM_SetCompare2(TIM1, (uint32_t)sin_table[phase_b] * amp_scale / SPWM_ARR); TIM_SetCompare3(TIM1, (uint32_t)sin_table[phase_c] * amp_scale / SPWM_ARR); }

逻辑说明:三相相位差固定 120 度,即表长的 1/3,用SPWM_POINTS / 3偏移实现,不用再算三角函数。& (SPWM_POINTS - 1)是取模的位运算写法,前提是表长必须是 2 的幂,这也是前面强调 256 点的原因。调压用amp_scale缩放查表值,本质是改调制比,输出线电压有效值随之变化;调频用spwm_step控制相位推进速度,步进越大一个载波周期跨过的正弦角度越多,输出频率越高。两个参数互不干扰,这就是调频调压能独立控制的原理。注意SPWM_Update放在更新中断里,执行时间要短,全是整数运算,符合裸机实时性要求。

4. 编译、仿真与上板:从 KEIL5 到真实波形

4.1 KEIL5 工程配置要点

打开工程先确认三件事。第一,Options for Target里 Device 选的是STM32F103C8,Flash 算法选STM32F10x Med-density,选错容量下载会报错。第二,C/C++选项卡的宏定义里要有USE_STDPERIPH_DRIVER, STM32F10X_MD,少了标准库编不过。第三,Debug选项卡选Use Simulator就能纯软件仿真,选ST-Link Debugger才是上板。仿真模式下在Debug里打开Logic Analyzer,把TIM1->CCR1、CCR2、CCR3加进去,直接看三路比较值的波形,不用示波器就能验证相位差和调制深度对不对。

4.2 仿真里看什么,上板前怎么验

仿真阶段重点看三样:三相波形相位是否严格差 120 度、调压时幅值是否线性变化、调频时周期是否跟着变。Logic Analyzer 里把三个 CCR 信号叠一起,正常应该看到三条正弦错开三分之一周期。如果某两相重合,八成是相位偏移算错了,回去查SPWM_Update里的偏移量。仿真通过再上板,上板第一步别接电机,先空载用示波器量 PA8 和 PB13,确认互补关系和死区间隙,这一步省不得。

4.3 上板接线与安全顺序

接线顺序有讲究:先接控制板电源和 SWD,确认 MCU 正常跑;再接驱动板逻辑电源,量六路驱动信号;最后才上母线高压。母线电容要先预充电,别直接合闸,浪涌能把管子送走。第一次带电机用限流电源,电流限制设小一点,观察空载电流是否正常,再逐步加载。这套顺序是我带小功率样机时的固定流程,跳过任何一步都可能烧板子。

5. 避坑与排查:那些让波形消失或管子冒烟的地方

5.1 上电六路都没波形

现象:编译下载都成功,示波器量 PA8 到 PB15 全是低电平。原因大概率是TIM_CtrlPWMOutputs(TIM1, ENABLE)没调用,高级定时器的主输出默认关闭,这是它和通用定时器最大的区别。解决:检查初始化末尾有没有这句,以及BDTR里TIM_AutomaticOutput是否使能。另外确认TIM_Cmd也调了,两个开关缺一不可。

5.2 互补波形有了但死区看不见

现象:上下管波形几乎无缝衔接,死区像不存在。原因通常是TIM_DeadTime设得太小,或者 DTG 编码理解错了——DTG 不是直接写纳秒,是按分段公式换算的,写个0x60和写 96 完全不是一回事。解决:查参考手册的 DTG 编码表,按目标死区时间反查编码值,再用示波器实测上下管下降沿到上升沿的间隔,以实测为准。

5.3 调频时波形抖动甚至丢步

现象:改spwm_step后输出频率跳变,波形不连续。原因多半是spwm_step在中断外被改写,主循环和中断抢同一个变量,读到了半更新的值。解决:把spwm_step声明为volatile,改写时先关更新中断、写完再开,或者用双缓冲,中断只读、主循环只写备用变量,在中断里切换。

5.4 带电机后管子发热严重

现象:空载正常,一加载管子烫手。原因可能是死区偏小导致上下管直通瞬间电流尖峰,也可能是载波频率过高开关损耗大,还可能是正弦表点数太少谐波电流大。解决:先加大死区实测温升变化,再降载波看是否缓解,最后考虑提高表点数。三个方向逐个排除,别一次改多个参数,否则不知道是哪个起的作用。

5.5 仿真正常上板异常

现象:Simulator 里波形完美,实际板子上乱七八糟。原因常见于时钟没真正跑到 72MHz——仿真器不严格校验时钟树,实际晶振没起振或 PLL 配置错,定时器频率全偏。解决:上板先点个 LED 用示波器量翻转周期,反推系统时钟对不对,确认时钟没问题再查 PWM。这个习惯能省掉大量瞎猜。

6. 二次开发:把固定参数改成可在线配置的驱动框架

工程现在参数是写死的,main.c里改完重新编译。真要拿去做产品,得让上位机或按键能在线调频调压。改造思路是把SPWM_SetAmplitude和SPWM_SetFrequency挂到一个命令解析层后面,串口收到指令就调这两个函数,参数范围做钳位保护。下面是个最小可用的串口命令处理骨架:

/* 串口收到 "F1000" 设频率步进,"A80" 设幅值 80% */ void Cmd_Parse(char *buf) { if (buf[0] == 'F') { uint16_t step = atoi(&buf[1]); if (step > 0 && step < SPWM_POINTS / 2) // 上限防混叠 SPWM_SetFrequency(step); } else if (buf[0] == 'A') { uint8_t pct = atoi(&buf[1]); SPWM_SetAmplitude(pct); // 内部已钳位 0~100 } }

验证方法很直接:串口发A50,Logic Analyzer 里三路幅值应减半;发F系列不同步进,量输出周期是否线性变化。改造时有个原则我吃过亏——所有在线可改的参数都要有上下限,频率步进超过表长一半会混叠,幅值超 100 会溢出比较寄存器,这些边界不守住,现场调参分分钟把波形搞崩。另外中断里只做查表和赋值,命令解析放主循环,别把atoi这种耗时操作塞进中断。

从那以后我每次改这类驱动工程,都强制先把时钟和主输出开关这两处单独验证一遍再动别的,血泪经验换来的顺序。希望帮到你。

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