STM32F103 TIM1三相SPWM互补PWM死区与调频调压设计

发布时间:2026/10/26 8:43:30
STM32F103 TIM1三相SPWM互补PWM死区与调频调压设计 简介面向STM32电机控制初学者与进阶开发者这份资料包围绕STM32F103C8T6高级定时器TIM1查表法三相六路互补SPWM输出展开重点解决相位互差120°、死区插入、调频调压及恒压频比驱动三相电机的实现问题。工程使用KEIL5编写并配有详细注解可在在线仿真中观察SPWM波形便于理解互补输出与死区设置。压缩包共149个文件约46.67MB以C/H源文件、编译中间文件及工程配置文件为主另含PDF学习文档、正弦表生成工具与死区计算器等辅助材料便于在调试过程中随时查阅原理说明。已有8068人学习下载读者可据此搭建三相逆变驱动实验掌握查表法SPWM参数配置、死区计算和调压调频调试思路也能借助配套工具快速生成正弦表并验证波形。1. 从一台三相逆变实验台的需求说起某实验室搭一个小功率三相逆变实验台指标列得很直白输出频率 550Hz 连续可调线电压有效值 036V 可调六路互补 PWM 必须带硬件死区主控用 STM32F103C8T6并且要求在 Keil5 里纯软件仿真就能看到六路波形学生不上电也能先把时序验证一遍。真正卡住人的地方不在 SPWM 算法而在引脚不少人第一反应是拿三个通用定时器各出一对互补信号可 F103C8T6 上只有 TIM1 是高级定时器带 CHxN 互补输出和硬件死区TIM2/3/4 压根没有互补引脚用软件取反加延时函数凑出来的死区会随主频漂移上下管直通只是时间问题。所以这套方案的重心从一开始就压死在 TIM1 的三对互补通道上调频调压、死区配置、正弦表刷新全部围绕这一个外设展开外围只是陪衬。2. 总体架构与资源分配三对互补通道怎么落到 LQFP48 的引脚上整套东西按四层切功率主电路三相全桥 母线电容 采样电阻、栅极驱动层半桥驱动 自举电路 栅极电阻、控制层STM32F103C8T6 出六路 PWM、采母线电压和相电流、人机层按键或串口下发频率和电压指令。四层里只有控制层是软件能改的其余三层在方案阶段就要把边界划清楚否则代码写得再漂亮硬件一处没对上照样炸管。控制层的资源分配是这套方案第一个要定死的东西。STM32F103C8T6 是 LQFP48 封装可用引脚本来就紧TIM1 的三对互补通道又只能在固定的几个脚上选错了就得飞线。2.1 三相六路互补输出的通道映射与引脚锁定先把映射关系摆出来这张表是整个硬件原理图的输入桥臂上管驱动下管驱动定时器通道备注A 相PA8PB13TIM1_CH1 / TIM1_CH1N主输出高有效B 相PA9PB14TIM1_CH2 / TIM1_CH2N主输出高有效C 相PA10PB15TIM1_CH3 / TIM1_CH3N主输出高有效刹车输入PB12—TIM1_BKIN过流比较器输出高电平封锁同步/调试PA11—TIM1_CH4可输出与中断同频的方波方便示波器触发这张表有两个容易忽略的代价。第一PA9/PA10 同时也是 USART1 的 TX/RX如果调试串口还挂在 USART1 上就得挪到 USART2PA2/PA3否则串口一发数据B 相和 C 相的上管占空比就跟着变。第二PB13/PB14/PB15 是 SPI2 的引脚这套方案里没有外部 SPI 器件所以不冲突但如果后面要加一块外部 ADC 或屏就得重新权衡。引脚配置上六个脚全部设成复用推挽输出速度开 50MHz/* PA8/PA9/PA10三相上管复用推挽 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* PB13/PB14/PB15三相下管同样复用推挽 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOB, GPIO_InitStructure);这里不要图省事配成普通推挽再软件翻转那样互补关系和死区全得自己维护一旦中断优先级被别的外设抢占上下管重叠几个微秒就够炸一批管子了。硬件互补的价值就在于这部分时序由定时器自己保证CPU 挂了它也不会错。2.2 载波频率、基波频率与调制度三个参数先定死SPWM 里最容易越调越乱的是三个量之间的换算关系方案阶段就得写成公式贴在墙上。中心对齐模式下计数器从 0 数到 ARR 再数回 0一个完整的 PWM 周期占 2×ARR 个计数时钟所以计数时钟 f_CK 72MHz / (PSC 1)开关频率 f_pwm f_CK / (2 × ARR)载波比 N f_pwm / f_out调制度 m 参考波峰值 / (V_dc / 2)按实验台的指标参数取成这样参数取值说明系统时钟72 MHz8MHz 外部晶振经 PLL 九倍频PSC0计数时钟等于 72MHz分辨率最高ARR3599中心对齐对应 10kHz 开关频率f_pwm10 kHz小功率实验台的常用区间f_out550 Hz基波通过相位步长在线改载波比2002000低频段载波比大波形更细目标死区1.0 μs对应 DTG 72调制度范围00.95留 5% 余量防过调制开关频率为什么定 10kHz 而不是更高往高走开关损耗和驱动损耗同步上涨而且死区在一个周期里占的比例变大低压输出时失真明显往低走电流纹波变大电机或滤波电感会发出可听噪声。1020kHz 是小功率三相逆变最舒服的一段10kHz 配合 3600 的 ARR占空比分辨率能到 0.03%调压时步进足够细。2.3 主电路与驱动的接口边界硬件这边有三条边界必须在方案里写清楚否则软件调不出来会先怀疑代码。第一条是母线电压和调制度的关系。驱动采用自举式半桥驱动上管靠自举电容供电这就要求下管每个周期必须有足够的导通时间给自举电容充电所以占空比不能顶到 100%调制度上限锁在 0.95 就是这个原因之一。第二条是过流保护的通路。母线负端串一个小阻值采样电阻经比较器整形后直接接到 PB12TIM1_BKIN一旦过流硬件在不到一个微秒内把六路全部拉低不经过任何中断。软件里再配一个中断去记录故障标志用来区分“过流跳闸”和“正常停机”这个区分在答辩时经常被问到。第三条是死区的物理下限。驱动芯片的传输延时、光耦或隔离器的延时、管子关断的拖尾时间加起来通常已经有几百纳秒所以死区不能拍脑袋设 0.2μs1.0μs 是这套实验台上比较稳妥的起点后面按实测波形再微调。3. SPWM 波形生成正弦表、相位累加器与中断里的一次刷新波形生成这块有两条路线一条是“一个基波周期固定 N 个点每个载波周期取一个点”另一条是“相位累加器 查表”。前者实现简单但输出频率和载波比绑死改频率就得重新算表后者用 32 位累加器输出频率可以连续改代价只是多一次加法。这套方案选后者因为调频调压是核心指标不能在这上面将就。3.1 正弦表的离线生成与定点化表在 PC 上生成好直接以 C 数组的形式编进固件运行时不产生任何三角函数运算。生成脚本用 Python256 点、Q15 格式import math N 256 # 一个基波周期的点数 PEAK 32767 # Q15 满量程 tab [int(round(PEAK * math.sin(2 * math.pi * i / N))) for i in range(N)] # 每行 8 个输出成可以直接贴进 C 文件的数组 for i in range(0, N, 8): row , .join(f{v:6d} for v in tab[i:i8]) print(f {row},)生成出来是一张 512 字节的int16_t sintab[256]范围 -3276732767。为什么是 256 点相位分辨率 360/256 ≈ 1.4°对 50Hz 基波来说相当于 78μs 的时间误差在实验台上完全看不出来点数再翻倍表占的 Flash 翻倍收益却很小。三相之间差 120°索引上分别加 256/3 取整也就是 85 和 171。这里有个小误差要说清楚256 不能被 3 整除85 点对应 119.53°171 点对应 240.47°三相互差有 0.47° 的固定偏差。做实验台、做课设这个偏差无所谓真要做到并网或者对相位精度有要求的场合把表长改成 384 或 7683 的整数倍就能消掉代价是索引计算要从移位改成乘除中断里多几个周期。这个取舍在答辩时讲出来比闷头做完更有说服力。3.2 TIM1 初始化中心对齐、预装载与互补通道使能初始化代码不长但每一段都对应一个会翻车的开关逐段过一遍void TIM1_PWM_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); /* 六个引脚复用推挽代码同 2.1 节此处省略 */ TIM_TimeBaseStructure.TIM_Prescaler psc; /* 072MHz 计数 */ TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_CenterAligned1; TIM_TimeBaseStructure.TIM_Period arr; /* ARR 3599 */ TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; /* t_DTS 1/72MHz */ TIM_TimeBaseStructure.TIM_RepetitionCounter 1; /* 每两个计数周期产生一次 UEV */ TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; /* 主输出 CHx */ TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; /* 互补输出 CHxN */ TIM_OCInitStructure.TIM_Pulse arr / 2; /* 上电默认 50% 占空比 */ TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Reset; /* 刹车时主输出拉低 */ TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; /* 刹车时互补输出拉低 */ TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); TIM_OC3Init(TIM1, TIM_OCInitStructure); /* 比较值必须开预装载否则中断里写 CCR 会在计数器中途生效出现丢脉冲 */ TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); /* 死区与刹车 */ 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 72; /* 1.0μs */ TIM_BDTRInitStructure.TIM_Break TIM_Break_Enable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); /* MOE 置位少了这一句六路全哑 */ TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); TIM_Cmd(TIM1, ENABLE); }几个参数要单独解释。TIM_RepetitionCounter 1是因为中心对齐模式下计数器上下溢出都会产生更新事件RCR 取 0 的话中断频率会是 PWM 频率的两倍相位累加器跑快一倍输出频率直接翻番这个坑在下面还会再提一次。TIM_OCIdleState和TIM_OCNIdleState都设成 Reset意思是刹车信号一到上下管全部拉低而不是只封上管否则下管持续导通会把母线短死。3.3 一次更新中断里完成三相比较值刷新中断服务函数只做三件事推进相位、查表、写三个 CCR。全部定点运算不出现浮点也不调用任何库函数volatile uint32_t phase_acc 0; /* 32 位相位累加器 */ volatile uint32_t phase_step 0; /* 每个 PWM 周期的相位增量 */ volatile int32_t spwm_amp 0; /* 幅度单位是计数值 */ void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) RESET) return; TIM_ClearITPendingBit(TIM1, TIM_IT_Update); phase_acc phase_step; uint8_t i0 (uint8_t)(phase_acc 24); /* 取高 8 位做表索引 */ uint8_t i1 (uint8_t)(i0 85); /* B 相滞后 120° */ uint8_t i2 (uint8_t)(i0 171); /* C 相超前 120° */ int32_t mid (int32_t)(TIM1-ARR 1); int32_t ca mid ((sintab[i0] * spwm_amp) 15); int32_t cb mid ((sintab[i1] * spwm_amp) 15); int32_t cc mid ((sintab[i2] * spwm_amp) 15); uint16_t top TIM1-ARR - 1; TIM1-CCR1 (uint16_t)(ca 1 ? 1 : (ca top ? top : ca)); TIM1-CCR2 (uint16_t)(cb 1 ? 1 : (cb top ? top : cb)); TIM1-CCR3 (uint16_t)(cc 1 ? 1 : (cc top ? top : cc)); }逻辑上phase_acc 24取的是 32 位累加器的高 8 位正好映射到 256 点的表索引溢出自动回绕不需要取模。三相互差靠的是索引加固定偏移不额外存三张表省 Flash 也省初始化时间。sintab[i]是 Q15 的正弦值乘上spwm_amp单位是计数值再右移 15 位得到相对中点的偏移量加回mid就是最终的比较值。最后那三行限幅不能省。调制度接近上限、或者表里正好是 ±32767 时算出来的值可能落到 0 或超过 ARRCCR 等于 0 时该相上管整个周期不导通等于 ARR 时下管整个周期不导通波形上就是一个明显的缺口。限到 1 和 ARR-1保证每个周期每相都有最小的导通时间自举电容也才有充电窗口。中断本身的执行时间要心里有数三次查表、三次乘加、三次限幅、三次写寄存器在 72MHz 下大约 23μs相对 100μs 的 PWM 周期占比不到 5%留给主循环的空间很充裕。如果后面要在这个中断里再加 ADC 采样或者串口协议解析就得重新算一遍别让中断时间悄悄超过周期。4. 死区与调频调压从 TDTS 算到 DTG从调制比算到相位步长这一章是整套方案里最容易被“差不多就行”糊弄过去的部分。死区差 0.2μs轻则波形失真重则直通调制度不限幅轻则电流畸变重则母线泵升。参数都得算出来不能试出来。4.1 死区时间TDTS 与 DTG 的换算关系死区发生器的时钟是 t_DTS由 TIM1_CR1 的 CKD[1:0] 决定跟预分频器 PSC 没有半点关系。CKD 00 时 t_DTS 等于计数时钟周期72MHz 下就是 13.89ns。DTG[7:0] 的编码分四段DTG[7:5]死区计算公式可覆盖范围72MHz0xxDT DTG[7:0] × t_DTS0 1.76 μs10xDT (64 DTG[5:0]) × 2 × t_DTS1.78 3.56 μs110DT (32 DTG[4:0]) × 8 × t_DTS3.56 14.2 μs111DT (32 DTG[4:0]) × 16 × t_DTS7.1 28.4 μs1.0μs 落在第一段DTG 1000ns / 13.89ns ≈ 72写进 BDTR 的低 8 位即可。要写成通用函数的话注意取整方向/* 输入目标死区时间ns返回 BDTR 的 DTG[7:0] */ uint8_t DT_Calc(uint32_t ns) { const uint32_t t_dts 14; /* 13.89ns 向上取整宁可死区偏大不可偏小 */ uint32_t n ns / t_dts; if (n 128) return (uint8_t)n; /* 0xx */ if (n / 2 64 n / 2 128) return (uint8_t)(0x80 | (n / 2 - 64));/* 10x */ if (n / 8 32 n / 8 64) return (uint8_t)(0xC0 | (n / 8 - 32));/* 110 */ if (n / 16 32) return (uint8_t)(0xE0 | (n / 16 - 32));/* 111 */ return 0xFF; }t_dts取 14 而不是 13是故意往上凑的算出 71.4 取 72实际死区 1.0008μs比目标略大。死区这东西宁可大一点损失几个百分点的有效占空比也不能小一点冒直通的风险这是血泪经验换来的取整方向。改完 DTG 之后一定要重新上电或者复位 TIM1因为 BDTR 里的 LOCKLevel 设了 1 级锁一旦写入当前上电周期内 DTG 就改不动了这是硬件防误写机制调试时容易误以为是代码没生效。4.2 调压调制比在线缩放与过调制限幅调压就是改调制度 m。三相双极性 SPWM 下相电压峰值等于 m × V_dc / 2线电压有效值和 m 的关系是V_ll_rms (√3 / (2√2)) × m × V_dc ≈ 0.612 × m × V_dc反过来用这个式子定 m母线 48V、要 24V 线电压m 24 / (0.612 × 48) ≈ 0.817。方案里把这个换算做成一个函数按键或串口下发的是目标电压内部换算成 m用起来就不用每次手算了。#define MOD_MAX 0.95f /* 留 5% 余量防止过调制和自举电容亏电 */ static float mod_index 0.0f; void SPWM_SetModulation(float m) { if (m MOD_MAX) m MOD_MAX; if (m 0.0f) m 0.0f; mod_index m; /* 只在设置时算一次中断里不做浮点运算 */ spwm_amp (int32_t)((TIM1-ARR 1) * m); }限到 0.95 不是为了保守是双极性 SPWM 的线性区本来就到 1.0 为止超过就是过调制正弦波顶部被削平5 次、7 次谐波突然窜上来电流波形上能直接看到平顶同时母线电容电压会往上泵。做过一次把 m 拉到 1.1 的测试输出电流THD从 3% 跳到 12%电机发出明显的低频啸叫从那以后这个限幅就再没去掉过。还有一点spwm_amp是中断里要读的变量声明成volatile int32_t32 位对齐写入在 Cortex-M3 上是原子的不需要关中断保护如果换成一个 64 位或者结构体就得加临界区了。4.3 调频改 ARR 还是改相位步长调频有两条路选错了后面全是麻烦方案做法优点缺点改 ARR改变 PWM 周期来改变每周期的相位推进载波比恒定谐波分布固定滤波器好设计开关频率跟着变死区占比变电感设计要按最低频率留余量ARR 在计数中途改动容易丢拍改相位步长PWM 频率不动只改每周期累加的相位增量开关频率恒定死区固定代码只改一个变量载波比随输出频率变化低频时载波比很大这套方案选改相位步长理由是开关频率恒定对死区和滤波电感的設計友好得多而且代码改动量最小void SPWM_SetFreq(uint32_t f_out) { /* f_pwm 72MHz / (2 × (ARR 1))ARR 3599 时为 10kHz */ uint32_t f_pwm 72000000UL / (2UL * (TIM1-ARR 1)); /* 每个 PWM 周期相位累加 2^32 × f_out / f_pwm */ phase_step (uint32_t)(((uint64_t)f_out 32) / f_pwm); }代入 f_out 50、f_pwm 10000算出来 phase_step 21474836一秒累加一万次正好是 50 个完整周期误差来自整数除法50Hz 下不到 0.001Hz可以忽略。f_out 5 时载波比 2000一个基波周期有两千个 PWM 周期正弦表 256 个点每个被查将近 8 次波形依然平滑。频率切换时还有一个细节phase_step直接改相位是连续的但频率是阶跃的电流会有一个暂态冲击。要求不高的场合直接切就行要求平滑的话加一个斜坡每次中断让phase_step朝目标值逼近一小步几十毫秒内完成过渡电流上看不到明显冲击。这个斜坡用几行代码就能加上答辩时是个加分项。5. 避坑与排查六路互补输出最容易翻车的五个点这套东西调起来出问题的地方高度集中。下面五条是实际项目里出现频率最高的按“现象 → 原因 → 解决”写对着排查能省掉大半天。第一坑六路里只有三路有波形另外三路始终是低电平。现象很直观示波器量 PA8/PA9/PA10 有 PWM量 PB13/PB14/PB15 是一条直线。原因通常有三个TIM_CtrlPWMOutputs(TIM1, ENABLE)没调用MOE 位是 0主输出和互补输出全被封死或者 OC 初始化里TIM_OutputNState只设了 Enable 但三路TIM_OCxInit漏掉了某一路那一路的 CHxN 就没被使能再或者 BDTR 里的 OSSI/OSSR 配成了 Disable空闲状态下互补输出被强制拉低。排查顺序就按这个来先看 BDTR 的 MOE 位再看 CCER 里对应的 CCxNE 位最后看 OSSI/OSSR。第二坑死区实测值和设计值对不上改了 PSC 之后死区跟着变。现象是示波器量出来死区 1.4μs而代码里明明算的是 1.0μs或者把 PSC 从 0 改成 1死区翻了一倍。原因是把死区时钟当成了计数时钟。死区发生器用的是 t_DTS只由 CKD[1:0] 决定PSC 改的是计数器时钟两者互不影响。解决方法是把死区计算函数里的 t_dts 固定成 13.89ns或向上取整的 14不要用1/(72MHz/(PSC1))去算。另外记得 BDTR 有 LOCK 锁写完之后当次上电周期内改不了改参数要复位重来。第三坑调制度提到 1.0 以上电流波形顶上出现平顶母线电压往上飘。现象是示波器看相电流峰值附近被削平同时直流母线电压比设定值高出一截。原因是过调制参考波峰值超过了三角载波峰值比较器长时间饱和输出基波不再随 m 线性增长低次谐波急剧变大电机侧的能量回灌到母线导致泵升。解决办法是在SPWM_SetModulation里做硬限幅上限 0.95同时在人机层做一次范围检查串口下发本文还有配套的精品资源点击获取