资讯动态

STM32F103 TIM1三相互补SPWM死区与调频调压实现

发布时间:2026/10/26 17:06:55 来源:尧图企业网站定制
简介面向STM32嵌入式学习与三相电机控制入门的实践资料包围绕STM32F103C8T6高级定时器TIM1采用查表法生成三相六路互补SPWM信号相位互差120°并加入死区与调频调压按恒压频比方式驱动三相电机。工程基于KEIL5编写注释较细支持在线仿真观察SPWM波形适合希望从原理到代码理解互补输出、死区设置与变频调速的读者。压缩包共149个文件、约46.67MB以C/H源文件与工程配置为主另含PDF原理文档、HEX固件、压缩资料及正弦表生成工具、死区计算器等辅助文件便于对照学习与快速验证。已有8068人学习下载。读者可获得一套可仿真的完整工程、SPWM正弦表与死区计算工具、生成原理说明及调试观察思路用于课程设计、电机驱动实验和嵌入式定时器应用练习。1. 一块 F103C8T6 撑起三相 SPWM六路互补、死区、调频调压都在这一个定时器里手头只有一块 STM32F103C8T6 最小系统板却要驱动三相全桥去带小功率电机这种需求在项目里出现的频率比想象中高得多。这份资源解决的正是这条链路上最关键的几件事用 TIM1 高级定时器输出三相六路互补 SPWM把死区、调频、调压三件事都落到寄存器层面并且在 KEIL5 环境下不接硬件也能把波形调出来看。它适合两类人一类是刚接触电机驱动想从“电机能转”走到“波形能看”的初学者另一类是手上已经有 F103 工程、要把固定占空比改成可调频调压的开发者。整套东西没有复杂的算法栈核心就是一个正弦表、一个更新中断加一个 BDTR 寄存器但每一处都藏着能把桥臂烧掉的细节。2. 硬件与工程骨架从引脚分配走到 KEIL5 能编译的第一版先把“这六路 PWM 到底从芯片的哪几个脚出来”这件事定死后面所有代码才有落脚点。C8T6 只有 TIM1 一个高级定时器没有 TIM8也就意味着三相六路互补输出只能由 TIM1 的 CH1/CH2/CH3 加三个互补通道承担这是硬件层面无法绕过的限制也是选型时先要看清楚的一点。2.1 六个 PWM 通道怎么落到具体引脚上默认复用映射下TIM1_CH1/CH2/CH3 分别在 PA8、PA9、PA10互补通道 CH1N/CH2N/CH3N 在 PB13、PB14、PB15。这六个脚刚好组成三相桥的六个驱动信号不需要做重映射也不需要额外占用别的定时器。我在画板子的时候会优先把这三个高边、三个低边引脚排在一起走线短一点后面测波形也方便。需要注意 PB13/PB14/PB15 在上电复位后默认是普通 IO也同时是 JTAG 的部分引脚。如果调试口用的是 SWDSWDIO/SWCLK 在 PA13/PA14那这三个脚可以放心当 PWM 用如果还留着 JTAG就得先关掉 JTAG 功能否则波形怎么配都出不来这是新手最容易卡住的一步。另外要留意的是一路上桥臂和它对应的下桥臂必须成对配置。CH1 和 CH1N 天然是一对硬件上就在同一个定时器通道里联动你不能把 CH1 配到 TIM1、把 CH1N 配到别的定时器去凑那样死区就完全不受控了。2.2 工程目录和几个必须盯住的文件资源里的 KEIL5 工程结构大体是这种分法我拆开后第一件事就是按这个顺序读SPWM_3PH_F103/ ├── CORE/ # 启动文件、core_cm3 ├── STM32F10x_FWLib/ # 标准外设库 ├── USER/ │ ├── main.c # 初始化顺序、主循环里的调频调压接口 │ ├── stm32f10x_it.c # TIM1_UP_IRQHandler 在这里 │ └── spwm.c / spwm.h # 正弦表、相位累加、CCR 计算 ├── HARDWARE/ │ ├── timer/ # TIM1 的 GPIO、时基、BDTR 配置 │ └── usart/ # 可选串口下发频率和调制度 └── OBJ/ # 编译输出Keil 自动生成读这个工程的顺序我建议是先看timer.c里 TIM1 的初始化把 ARR、PSC、BDTR 三个值抄下来再看spwm.c里的正弦表和相位累加逻辑最后回到stm32f10x_it.c看更新中断里到底算了什么。反过来读会很容易陷在中断里出不来因为你不知道 ARR 是多少就看不懂那些移位和除法。spwm.h一般是整个工程对外暴露参数的地方调制比、输出频率、载波频率、死区时间这几个宏都在这改参数基本不用动别的文件。我习惯把这一层叫做“人机接口层”它的值一变整个输出就跟着变。2.3 时钟、死区与中断优先级的初始化顺序初始化的顺序错了现象会非常隐蔽定时器在跑引脚也有电平但就是没有互补、没有死区。下面这段是我按工程实际配置抽出来的核心部分省略了 GPIO 的复用推挽配置/* 72MHz 主频TIM1 挂在 APB2时钟也是 72MHz */ void TIM1_PWM_Init(uint16_t arr, uint16_t psc) { TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; TIM_BDTRInitTypeDef bdtr; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); tb.TIM_Prescaler psc; /* 预分频一般给 0 */ tb.TIM_CounterMode TIM_CounterMode_CenterAligned1; /* 中心对齐 */ tb.TIM_Period arr; /* 载波周期由 ARR 决定 */ tb.TIM_ClockDivision TIM_CKD_DIV1; /* tDTS 1/72MHz */ tb.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM1, tb); 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 arr / 2; /* 初始 50% 占空比 */ TIM_OC1Init(TIM1, oc); TIM_OC2Init(TIM1, oc); TIM_OC3Init(TIM1, oc); /* 死区 刹车 主输出使能三件事都在这一个结构体里 */ TIM_BDTRStructInit(bdtr); bdtr.TIM_DeadTime 72; /* 72 * 13.89ns ≈ 1us */ bdtr.TIM_Break TIM_Break_Disable; bdtr.TIM_OSSRState TIM_OSSRState_Enable; bdtr.TIM_OSSIState TIM_OSSIState_Enable; bdtr.TIM_LOCKLevel TIM_LOCKLevel_1; bdtr.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, bdtr); TIM_CtrlPWMOutputs(TIM1, ENABLE); /* MOE 位不写这句六路全哑 */ TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); TIM_Cmd(TIM1, ENABLE); }这段里有三个参数值得单独说明。arr决定 PWM 载波频率中心对齐模式下f_pwm 72MHz / (2 * (arr1))取 arr3599 就是 10kHz 载波。psc保持 0因为后面调频靠改相位步进而不是改分频改分频会让载波和采样率的关系乱掉。TIM_DeadTime72是 DTG 寄存器的原始值不是纳秒单位是 tDTS1/72MHz 约等于 13.89ns所以 72 个 tDTS 落在 1us 附近。TIM_CtrlPWMOutputs这一句是很多人漏掉的。BDTR 里的 MOE 主输出使能位不置起来前面配得再对六个脚也全是静默电平示波器上什么都看不到。我第一次调这个工程时就在这儿卡了半天血泪经验。3. 核心链路拆解正弦表、查表更新与六路互补的落地初始化的部分只是把地板铺平真正决定波形长什么样的是“正弦表 → 相位累加 → CCR 写入”这条链路。它跑在 TIM1 的更新中断里属于整份资源里最值得逐行读的部分因为调频调压的精度和波形的平滑度几乎全部由这里的实现方式决定。3.1 正弦表的生成方式与 Q15 定点化F103C8T6 没有 FPU中断里做浮点运算是自找麻烦所以正弦表一定是预先算好的定点常量表。常见的点数有 128 点和 256 点两种128 点对小容量 Flash 更友好一个周期 128 个采样点配上 10kHz 载波输出 50Hz 时每个周期有 200 个载波周期点数够用。生成脚本我一般用 Python 跑一次把结果直接贴进 C 文件import math N 128 AMP 32767 # Q15对应 1.0 lines [] for i in range(N): v int(round(AMP * math.sin(2 * math.pi * i / N))) lines.append(f{v:6d}) # 每行 8 个方便贴进 C 数组 for i in range(0, N, 8): print( , .join(lines[i:i8]) ,)生成的表在 C 里长这样注意类型必须是int16_t并且加const否则会被放进 RAM20KB 的 RAM 里放一张表不算大事但养成放 Flash 的习惯没坏处#define SIN_TAB_LEN 128 #define PHASE_B_OFF (SIN_TAB_LEN / 3) /* 120 度 */ #define PHASE_C_OFF (2 * SIN_TAB_LEN / 3) /* 240 度 */ static const int16_t g_sin_tab[SIN_TAB_LEN] { 0, 1608, 3212, 4808, 6393, 7962, 9512, 11039, 12540, 14010, 15447, 16846, 18205, 19520, 20788, 22006, /* ... 中间省略实际是完整 128 点 ... */ -1608, -3212, -4808, -6393, -7962, -9512, -11039, };三相的关键就在这两个偏移量上。同一个表A 相从 0 开始取B 相从 42 点开始取C 相从 85 点开始取取出来的就是严格相差 120 度的三条正弦波。这种“共用一张表 相位偏移”的做法比做三张表省 2/3 的 Flash而且三相幅值天然一致不会出现因为表的精度不同导致三相不平衡的问题。3.2 更新中断里做的三件事查表、缩放、写 CCR中断函数的骨架是整份资源的中心。它每个更新事件触发一次做三件事相位累加器前进、查三相的值、按调制比缩放后写进三个 CCR。写法如下volatile uint32_t g_phase_acc 0; /* Q16 相位累加器范围 [0, N16) */ volatile uint32_t g_phase_step 1 16; /* 每个中断前进 1 个表点 */ volatile uint16_t g_mod_index 30000; /* 调制比 MQ1530000 ≈ 0.915 */ void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) RESET) return; TIM_ClearITPendingBit(TIM1, TIM_IT_Update); /* 1. 相位累加越界回绕 */ g_phase_acc g_phase_step; if (g_phase_acc ((uint32_t)SIN_TAB_LEN 16)) g_phase_acc - ((uint32_t)SIN_TAB_LEN 16); /* 2. 取整数索引加 120/240 度偏移 */ uint16_t idx (uint16_t)(g_phase_acc 16); uint16_t ia idx; uint16_t ib (idx PHASE_B_OFF) (SIN_TAB_LEN - 1); uint16_t ic (idx PHASE_C_OFF) (SIN_TAB_LEN - 1); /* 3. 缩放中点 ARR/2幅值 ARR/2 * M */ uint16_t arr (uint16_t)TIM1-ARR; uint16_t center arr / 2; uint16_t amp (uint16_t)(((uint32_t)center * g_mod_index) 15); TIM1-CCR1 spwm_duty(g_sin_tab[ia], amp, center); TIM1-CCR2 spwm_duty(g_sin_tab[ib], amp, center); TIM1-CCR3 spwm_duty(g_sin_tab[ic], amp, center); }g_phase_acc用一个 32 位无符号数存相位高 16 位当查表索引低 16 位是小数部分。这样即使g_phase_step不是整数个表点相位也能平滑推进不会出现步进不均匀导致的低频啸叫这是定点相位累加器最实用的地方。g_phase_step是调频的旋钮。它等于 65536 的时候每个中断正好走一个表点改成 32768 就是走半个表点。因为中断里只做加法和移位整个频率调整过程没有除法也不会进入浮点库。g_mod_index是调压的旋钮Q15 格式0 到 32767 对应调制比 0 到 1。30000 大约是 0.915留了一点余量给死区和最小脉宽。spwm_duty这个函数负责把有符号的正弦值映射到 CCR 的合法区间它的实现比看上去重要static inline uint16_t spwm_duty(int16_t s, uint16_t amp, uint16_t center) { int32_t v ((int32_t)s * (int32_t)amp) 15; /* Q15 乘还原成计数值 */ v center; /* 抬到中点 */ if (v 0) v 0; /* 下限钳位 */ if (v (int32_t)(2 * center)) v 2 * center; /* 上限钳位 */ return (uint16_t)v; }这两个钳位不是防御性编程的客套话。调制度推到 1.0 再加上中断延迟s * amp 15完全可能让结果越过 [0, ARR] 区间一旦越界写进 CCR比较值超出计数器范围该通道要么恒高要么恒低表现出来就是波形削顶或者桥臂直通。钳位这两行就是防止这种情况的保险。3.3 互补输出与 BDTR死区到底是谁在管很多人以为死区是靠软件在中断里错开写入时间实现的实际上在 STM32 上完全不是。只要 CH1 和 CH1N 成对使能并配置好 BDTR 的 DTG硬件会在每次电平翻转时自动插入一段两路都为低的空档软件不需要参与也来不及参与。DTG 是 8 位编码不是线性的一串数四档分辨率对应四段范围DTG[7:5]死区计算式覆盖范围tDTS13.89ns0xxDTG[7:0] × tDTS0 ~ 1.76us10x(64 DTG[5:0]) × 2tDTS1.78us ~ 3.52us110(32 DTG[4:0]) × 8tDTS3.56us ~ 7.00us111(32 DTG[4:0]) × 16tDTS7.11us ~ 14.0us工程里我之前写了个小函数做这个换算比手动查表靠谱/* 输入目标死区时间ns返回 DTG 寄存器原始值 */ static uint8_t dead_ns_to_dtg(uint32_t dead_ns) { uint32_t n (dead_ns * 72u 999u) / 1000u; /* 向上取整到 tDTS 个数 */ if (n 127u) return (uint8_t)n; /* 0xx 档 */ if (n 254u) return (uint8_t)(0x80u | ((n / 2u) - 64u)); /* 10x 档 */ if (n 504u) return (uint8_t)(0xC0u | ((n / 8u) - 32u)); /* 110 档 */ if (n 1008u) return (uint8_t)(0xE0u | ((n / 16u) - 32u));/* 111 档 */ return 0xFFu; /* 超范围取最大 */ }取整方向一定要向上。宁可死区大一点也不能少一个 tDTS因为少一点就可能出现上下桥臂同时导通的瞬间。功率管关断时间通常在几百纳秒量级光耦或者驱动芯片还会再加一点延迟所以实际死区一般取 1us 到 2us具体看管子型号我一般从 1.5us 起步往上试。还有一点容易被忽略死区是在互补通道之间插入的如果只用单路输出、没开互补DTG 配了也没用。判断有没有生效最直接的办法是把示波器两个探头同时搭在 CH1 和 CH1N 上看两路下降沿和上升沿之间是不是真的有一段共同的低电平区。3.4 调频调压的两个旋钮改调制比还是改相位步进调压改的是g_mod_index也就是调制比 M。M 从 0 到 1 变化输出线电压基波幅值随之从 0 变到最大值波形本身的正弦形状不变只是整体被“压扁”或者“撑开”。这中间不需要动定时器任何一个寄存器中断里乘一下就行非常轻量。调频改的是g_phase_step。因为载波频率由 ARR 固定死输出频率只能靠“每个载波周期正弦波走多远”来控制。步长大的时候一个输出周期用的载波周期少输出频率就高步长小的时候反过来。两者的定量关系是f_out f_isr * (g_phase_step / 65536) / SIN_TAB_LEN 其中中心对齐模式下 f_isr 2 * f_pwm 72MHz / (ARR 1)举个具体数ARR 3599f_pwm 10kHzf_isr 20kHzSIN_TAB_LEN 128要输出 50Hz代入得到g_phase_step ≈ 50 × 128 × 65536 / 20000 ≈ 20971。要输出 25Hz 就取一半10486。这个数在代码里可以用宏算出来编译期就定死省掉运行时除法。提示f_isr之所以是f_pwm的两倍是因为中心对齐模式下计数到 ARR 和到 0 都会置位 UIF中断频率天然翻倍。这一点算频率公式时最容易算错算错了输出频率就整体差一倍波形看着还挺像正弦但频率表对不上。4. 调频调压的工程实现参数怎么改、边界在哪把旋钮找出来只是第一步真正上机跑的时候卡人的往往是“调制度推到多少合适”“频率最低能到多少”“改参数为什么波形会抖”这类问题。这一层没有标准答案只有一堆互相牵制的约束得挨个算清楚。4.1 调制度、死区占比与最小脉宽之间的三角关系调制比 M 越大基波幅值越高但每一相在一个载波周期里的高电平时间就越极端。以中心对齐、M1 为例正弦峰值那一点占空比接近 100%谷值接近 0%。这时候问题来了占空比接近 100% 的那一路是没问题但它的互补通道就要输出一段非常窄的低电平脉冲这段宽度可能比死区还窄结果就是被死区吞掉波形顶部变成一个平顶。工程上的做法是给调制比留上限。我一般把 M 钳在 0.9 到 0.95 之间具体值用这个经验公式估/* 返回当前 ARR 和死区下允许的最大调制比Q15 */ static uint16_t max_mod_index(uint16_t arr, uint16_t dead_cnt) { /* 峰点剩余脉宽至少要大于 2 倍死区计数值否则会被吃掉 */ uint32_t usable (uint32_t)(arr / 2) - (uint32_t)dead_cnt * 2u; uint32_t m (usable * 32767u) / (uint32_t)(arr / 2); return (uint16_t)((m 32767u) ? 32767u : m); }dead_cnt是死区时间换算成定时器计数值的量也就是dead_ns * 72 / 1000 / (psc1)。载波频率越高ARR 越小同样 1us 的死区占比就越大可用的调制比上限就越低。这就是为什么高频载波下 M 必须往回收很多人调频到 20kHz 之后发现波形顶部糊成一片就是没把这个关系算进去。还有一个隐性约束是最小脉宽。驱动芯片对输入脉宽一般有个最小值几百纳秒量级低于这个宽度可能根本不响应表现出来就是波形偶尔丢一个脉冲。这个问题在 M 很小、输出频率很低的时候最明显因为此时有效占空比本来就小再叠上死区窄脉冲就出来了。解决办法要么提高载波频率让分辨率变细要么在低调制比区间限制最低输出电压不让它无限往 0 走。4.2 频率调节走相位步进还是走 ARR调频有两条路工程里都有人用但适用场合不同。一条是改 ARR也就是改载波频率输出频率跟着变另一条是固定 ARR、改g_phase_step载波不变、只改相位推进速度。改 ARR 的问题是它会同时改变载波频率开关损耗、死区占比、采样时序全都跟着动而且 ARR 是定时器的周期寄存器在运行中修改如果时机不对会有一个周期出现计数溢出异常波形上就是一次明显的抖动。所以这条路一般只在“整体换档”时用比如从一个固定工作点切到另一个而且切换前要先关掉输出。改相位步进就没有这个问题。ARR 不动载波恒定开关损耗和死区占比都是常数只改变正弦的推进速度。平滑调速、连续变频全部走这条路代价是频率分辨率受限于 Q16 的小数位数不过一般工况下完全够用。我在工程里把两者的接口都留着但默认只用相位步进那条/* 设定输出频率单位 0.01Hz内部换算成 Q16 相位步进 */ void spwm_set_freq(uint32_t freq_x100) { /* f_isr 72MHz / (ARR1)中断频率 */ uint32_t f_isr 72000000UL / ((TIM1-ARR) 1UL); /* step f_out * N * 65536 / f_isr */ uint64_t num (uint64_t)freq_x100 * SIN_TAB_LEN * 65536ULL; uint32_t step (uint32_t)(num / ((uint64_t)f_isr * 100ULL)); if (step 64u) step 64u; /* 下限防止低频时步进太小 */ if (step (8UL 16)) step 8UL 16; /* 上限一个中断最多走 8 个点 */ g_phase_step step; }上下限这两行限制是工程上很关键的保护。步进太小相位几乎不动中断里的计算全白做而且容易和中断抖动叠加成低频噪声步进太大一个中断跨过好几个表点波形不再是正弦而是直接跳变谐波含量会变差。上限取 8 个表点是个经验值再往上就明显感觉波形变粗了。另外g_phase_step是 32 位变量在中断里被读、在主循环里被写中间没有保护的话主循环一次 32 位写入在 Cortex-M3 上是原子的倒不用担心撕裂。但如果后面换成 64 位相位累加器就必须关中断再写或者用双缓冲。4.3 一个可复用的参数下发接口实际项目里频率和调制度不会写死在代码里一般是通过串口、按键或者上位机下发。工程里留了一个简单的串口协议格式就是两个浮点数打包好处是调试时用串口助手直接敲不用重新编译。/* 协议帧0xAA 0x55 | freq(4B, 单位 0.01Hz) | mod(2B, Q15) | CRC8 */ void spwm_apply_cmd(const uint8_t *frame, uint16_t len) { if (len 9 || frame[0] ! 0xAA || frame[1] ! 0x55) return; uint32_t freq_x100 (uint32_t)frame[2] | ((uint32_t)frame[3] 8) | ((uint32_t)frame[4] 16) | ((uint32_t)frame[5] 24); uint16_t mod_q15 (uint16_t)frame[6] | ((uint16_t)frame[7] 8); if (mod_q15 max_mod_index(TIM1-ARR, 72)) /* 越限直接拒绝 */ return; spwm_set_freq(freq_x100); /* 先调频 */ g_mod_index mod_q15; /* 后调压 */ }执行顺序是先频率后调制度原因是调制度上限本身依赖当前的 ARR 和死区配置虽然这里面 ARR 不动但把频率先落下去、再校验调制比逻辑上更顺也避免了中间态出现“频率没变、调制比已经超限”的短暂组合。g_mod_index是 16 位写入是原子的不需要关中断。真要做成可在线改的先调频后调压流程最好在两次写之间不要插入别的定时器操作否则会有一个周期出现三相幅值不同步示波器上看着就是某两相突然比第三相高一点。这种瞬时不对称虽然很短但电流环里会被放大。5. 避坑与排查示波器上那些不对劲的波形这段是我自己踩过、也帮别人看过的问题基本都是“代码编译过了、板子也上电了但波形就是不对”这一类。每条按现象、原因、解决三步写对得上号的话能省下大半天。5.1 上电后六路全静默示波器什么都没有现象程序下载进去了引脚用万用表量是低电平示波器上一片平静感觉定时器压根没在跑。原因BDTR 的 MOE 主输出使能位没置起来或者TIM_CtrlPWMOutputs(TIM1, ENABLE)这一句被漏掉。高级定时器的输出比通用定时器多一道总闸闸不开通道配得再全也不出波形。另一种可能是刹车输入未关闭导致 MOE 被硬件清掉。解决先确认TIM_CtrlPWMOutputs在TIM_Cmd之前被调用再确认 BDTR 里TIM_Break是 Disable 状态。如果板上真有刹车引脚把它拉高或者干脆不用别悬空。还有一种情况是 GPIO 的复用推挽没配六个脚还是默认浮空输入那就得回到 GPIO 初始化那段查。5.2 波形有了但顶部被削平或者底部塌下去现象正弦波看着像方波峰值位置被一条水平线切掉调制度越大切得越多。原因CCR 计算越界写进去的值超过 ARR 或者小于 0。中心对齐模式下 CCR 的合法范围是 [0, ARR]一旦超了比较器在这个周期里永远不会翻转通道就保持恒定电平表现出来就是削顶。解决在spwm_duty里把上下限钳位补上同时把调制比上限用max_mod_index卡住。另外顺手检查一下amp的计算是不是用了center而不是arr用arr的话幅值会大一倍必然削顶。5.3 死区看着配了实际上没生效现象DTG 写了 72示波器上量 CH1 下降沿到 CH1N 上升沿之间的间隔结果是 0 或者只有几十纳秒。原因通常是 DEADTIME 写成纳秒了DTG 需要的是原始编码值。也可能互补通道没开TIM_OutputNState_Enable漏配或者测量的时候探头的带宽和接地弹簧没处理好把真实存在的死区看没了。解决先按 3.3 的换算表确认编码是否正确72 对应的是 0xx 档、约 1us。然后用双通道示波器同时看 CH1 和 CH1N探头地线尽量短别用长鳄鱼夹。要是实在测不出把死区先设成 5us肉眼都能看出来再往回降。5.4 KEIL5 仿真里跑不出波形现象用 Simulator 模式调试外设窗口里寄存器的值都对但逻辑分析仪窗口里加进去的引脚信号是条直线。原因Keil 的仿真器对高级定时器输出引脚的支持有限尤其是互补通道和死区这种硬件行为纯软件模型不一定能还原。很多人加了TIM1_CH1这类符号发现根本没反应就以为程序有问题。解决与其盯着引脚不如盯变量。把TIM1-CCR1/CCR2/CCR3或者自己维护的g_ccr_log[3][N]环形缓冲加到逻辑分析仪里观察它们的数值变化正弦形状一眼就能看出来。仿真器里还要确认 Dialog 参数设置对了器件选 STM32F103C8、晶振按板子实际填否则外设的时钟模型是错的一切数值都不可信。提示仿真验证的是算法逻辑真实验证还得靠示波器。我一般先在 Keil 里把 CCR 序列导出来用脚本画一遍确认是标准正弦再上板测能少走很多弯路。5.5 频率一调波形就抖一下现象通过串口改频率输出频率确实变了但切换瞬间波形会抖一个周期严重的时候能看到一两个畸形波。原因改的是 ARR 而不是相位步进或者虽然改的是步进但新值在相位累加器回绕的中间时刻写进去了导致那一个周期相位跨度不均匀。解决坚持走相位步进那条路ARR 只在停机时改。如果是步进方案还有抖动那就把g_phase_step的更新放到更新中断里做用一个 pending 标志主循环只负责置位中断里在确认刚回绕之后才替换值这样相位是连续的。6. 二次开发把它改成你自己的变频器骨架这份资源本身是个最小闭环要落到实际项目里改的第一处通常是参数层。把g_mod_index和g_phase_step收进一个结构体外面套一层 V/F 曲线让频率升高的同时调制度按预设斜率跟着走低速段做转矩提升这套东西不需要动中断里的核心循环只改spwm_apply_cmd往后的一小段。typedef struct { uint16_t freq_x100; /* 目标频率0.01Hz */ uint16_t mod_q15; /* 目标调制比 */ uint16_t boost_q15; /* 低频转矩提升量 */ } vf_point_t; /* 简易 V/Ff 从 5Hz 到 50HzM 从 0.15 线性升到 0.90 */ static void vf_update(uint16_t f_x100) { uint32_t f f_x100; uint32_t m; if (f 500u) f 500u; if (f 5000u) f 5000u; m 1500u (f - 500u) * (9000u - 1500u) / (5000u - 500u); g_mod_index (uint16_t)((m * 32767u) / 10000u); spwm_set_freq(f); }改的第二处是保护。现在的工程里刹车输入是关掉的真实设备上最好把它打开接上过流比较器的输出一旦触发硬件会立刻把六路拉低比软件中断快得多。打开之后要在刹车中断里做状态记录和恢复逻辑别让它一触发就死在那儿。第三处是采样同步。如果后面要加电流环采样时刻必须和 PWM 对齐中心对齐模式下最合适的采样点是计数器到 ARR 的那一瞬间也就是更新中断刚进来的位置。把 ADC 的触发源配成 TIM1 的 CC4 或者更新事件比软件延时靠谱得多。下面这张表是我按改造优先级排的从轻到重改造项改动位置风险V/F 曲线vf_update低只影响输出幅值串口协议扩展spwm_apply_cmd低注意长度校验刹车保护BDTR 中断中恢复逻辑要测ADC 同步采样TIM1 CC4 ADC 触发中时序要对齐载波频率切换ARR 重配高必须停机改验证方法我自己的顺序是先在 Keil 里用逻辑分析仪看三相 CCR 的数值序列确认相位差严格是 1/3 和 2/3再上板用示波器看线电压重点量死区有没有真的存在最后接上小功率电机从 5Hz 慢慢往上加观察电流波形有没有畸变。三步都过才算能拿去用。改造完记得把参数宏集中放到一个头文件里我见过太多工程把 ARR 写死在四个不同的地方改载波频率的时候漏掉一处调了一晚上才发现频率对不上。从那以后我每次动定时器参数都强制走一遍“搜索 ARR、搜索死区宏、搜索调制比”的清单再编译。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑