ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103生成三路120°相位差方波的两种实现方案

2026/9/16 14:50:39 拓冰建站 浏览量
STM32F103生成三路120°相位差方波的两种实现方案 简介基于STM32F103的3通道120°相位差方波生成工程面向嵌入式开发与电机控制场景利用定时器PWM输出及捕获/比较寄存器配置实现三路严格120°相位差的方波信号可用于无刷直流电机驱动、三相功率变换等。资源共73个文件以C源码和H头文件为主体34个h、33个c另有启动文件、工程配置及hex固件整体仅279KB已有588人学习下载。压缩包内包含完整Keil工程、时钟与延时驱动、定时器PWM初始化、串口调试模块及清理脚本目录结构清晰可直接编译烧录验证。阅读源码可掌握STM32定时器多通道相位差输出原理为实际项目提供可复用参考模板。1. 3通道120°相位差方波这个标题在解决什么问题做三相无刷电机驱动、三相全桥逆变前级或者步进模拟器时经常要拿到一组频率相同、彼此错开120°电角度的方波。三个方波各自是50%占空比波形本身不复杂难点在“相位差精确且不漂移”B相比A相滞后120°C相较B相再滞后120°六个波形的上升沿和下降沿把一个完整周期均匀切成了六段60°扇区。只要某一个沿的时序偏了电机力矩就会抖动测量出来的换相点也会跟着错。这个标题落到STM32F103上最实用、最省成本的做法不是外挂PLC或CPLD而是让芯片自带的定时器配合GPIO直接生成。F103主频72MHzGPIO单条BSRR写指令可以在几十纳秒内完成电平翻转定时器更新事件又能以硬件方式触发DMA也就是说从10kHz到几十kHz的方波频率都能做到完全不占用CPU主循环。下面给出的两版方案一版用定时器中断BSRR适合快速验证一版用定时器DMA循环适合产线和低抖动场景。最后还会讲怎么用输入捕获把相位差量出来避免示波器上看着“好像对了”实际差半个扇区的问题。2. 用STM32F103的定时器中断把3通道120°相位差方波铺到GPIO上2.1 先把120°换算成定时器的计数刻度生成三路120°相位差方波时我习惯先不直接写“相位差120°”这个角度而是把整个周期拆成六个60°扇区。原因很简单三个方波在0°、60°、120°、180°、240°、300°各有一个电平跳变沿六个跳变沿恰好等间隔。只要定时器每到下一个60°边界进一次中断按扇区表刷新一次三个GPIO的电平相位关系就自动满足了。如果定时器时钟是72MHzPSC分频系数是PSCARR重载值是ARR目标方波频率是f_sq那么事件间隔满足ARR 1 f_TCLK / ( (PSC 1) × 6 × f_sq )这里的6就是六个60°扇区。比如要输出10kHz三路方波PSC设为0ARR 1 72000000 / (6 × 10000) 1200所以ARR填1199。定时器每1200个计数产生一次更新事件也就是每16.67us刷新一次扇区一个完整电周期正好是100us对应10kHz。这个公式比直接去算“120°等于几个计数”更不容易错因为ARR值本身就代表“一个扇区占多少计数”而不是“一个周期占多少计数”。当目标频率低到ARR 1超过65535时就要先加大PSC把定时器计数时钟降下来。至于PSC和ARR的取舍放到第4章再细说。2.2 六个扇区的状态表与BSRR更新假设三个输出分别接到PA0、PA1、PA2分别代表A相、B相、C相。以A相上升沿作为0°起点各相逻辑如下A相从0°到180°为高B相从120°到300°为高C相从240°到下一个60°为高。把六个扇区列成表扇区电气角度范围PA2(C)PA1(B)PA0(A)ODR目标值00°-60°0010x0001160°-120°1010x00052120°-180°1110x00073180°-240°1100x00064240°-300°0100x00025300°-360°0000x0000这张表的关键是每一行不是上一个扇区的“变化量”而是下一个扇区结束后三个引脚应该保持的绝对状态。中断里用BSRR寄存器来写BSRR低16位是置位某位写1对应引脚输出高高16位是复位某位写1对应引脚输出低。这样一次32位写入就能完成“先清旧位、再置新位”中间不会有读改写竞争。/* 定时器4更新中断每60°电角度触发一次 */ void TIM4_IRQHandler(void) { static const uint16_t sq_table[6] { 0x0001, /* 0° -60° : A1 */ 0x0005, /* 60° -120° : A1 C1 */ 0x0007, /* 120°-180° : A1 B1 C1 */ 0x0006, /* 180°-240° : B1 C1 */ 0x0002, /* 240°-300° : B1 */ 0x0000 /* 300°-360° : 全低 */ }; static uint8_t idx 0; static uint16_t prev 0; uint16_t target, set_mask, clr_mask; if (TIM4-SR TIM_SR_UIF) { TIM4-SR (uint16_t)~TIM_SR_UIF; target sq_table[idx] 0x0007; set_mask target (uint16_t)~prev; clr_mask prev (uint16_t)~target; /* 一次BSRR写入完成置位和复位 */ GPIOA-BSRR set_mask | ((uint32_t)clr_mask 16); prev target; idx; if (idx 6) idx 0; } }这段代码里set_mask是本次扇区需要拉高的引脚clr_mask是本次扇区需要拉低的引脚。用prev记录上一个扇区的电平才能算出哪些引脚发生了跳变。如果直接写目标ODR值也能工作但BSRR方案的好处是不影响PA3以上的其他引脚比如PA4还连着SPI片选不会被这个中断意外清掉。初始化部分按标准流程走先开GPIOA时钟把PA0-PA2配成50MHz推挽输出再开TIM4时钟并设置PSC和ARR。/* GPIOA时钟和TIM4时钟使能 */ RCC-APB2ENR | RCC_APB2ENR_IOPAEN; RCC-APB1ENR | RCC_APB1ENR_TIM4EN; /* PA0-PA2 设为推挽输出50MHz */ GPIOA-CRL ~0x00000FFF; GPIOA-CRL | 0x00000BBB; /* TIM4: 72MHz / (01) / (11991) / 6 10kHz */ TIM4-PSC 0; TIM4-ARR 1199; TIM4-EGR TIM_EGR_UG; /* 清除中断标志开NVIC */ TIM4-SR 0; NVIC_EnableIRQ(TIM4_IRQn); NVIC_SetPriority(TIM4_IRQn, 1); /* 使能更新中断和计数器 */ TIM4-DIER | TIM_DIER_UIE; TIM4-CR1 | TIM_CR1_CEN;代码里CRL的0xBBB是三个引脚各占4位0xB表示50MHz推挽输出。很多人在这里只配置了MODE忘记清CNF结果引脚变成开漏或模拟输入方波一直拉不高这是最常遇到的一个坑。2.3 为什么用TIM4而不是TIM1F103里TIM1是高级定时器带互补输出和死区适合后续做三相PWM。但这里只生成三路普通方波不需要互补通道也不需要刹车输入用TIM1反而要多配一堆BDTR和MOE相关的寄存器。TIM2、TIM3、TIM4都是通用定时器行为几乎一致我一般选TIM4因为它的更新事件可以映射到DMA1的通道6和后面要讲的DMA方案能复用同一个定时器初始化代码。还有一个实际考虑是中断延迟。TIM4的更新中断在NVIC里占的位置和USART、DMA通道不冲突把优先级设成一个中间值就好。如果放在和SysTick一样的最高优先级上外部通信中断可能会被卡住如果设太低其他中断频繁打断扇区切换就开始抖动。对于纯方波输出中断里只有一次查表、两次位运算、一次BSRR写入在72MHz下执行时间不到1us即使偶尔被高优先级中断打断几十个周期相位误差也在0.1°以内多数应用都能接受。3. 去掉中断定时器DMA循环刷新三路相位3.1 从UIF中断到DMA请求的思路中断方案能跑但有一个隐藏问题每次扇区切换都要进一次ISR中断入口、压栈、判断标志、出栈这些开销虽然每个只有几百纳秒在高频方波下累积起来还是会影响主循环的实时性。如果要输出20kHz甚至50kHz的三相方波每秒中断次数达到120kHz到300kHz主循环被频繁打断放在主循环里的通信协议、按键扫描就会明显卡顿。更干净的方案是用定时器更新事件直接触发DMA搬运把六扇区状态表放在内存里每次TIM4计数溢出时DMA自动从表里取一个数写到GPIO的ODR寄存器。表走完一圈后DMA循环模式自动回到表头三个引脚的电平就按扇区序列一直轮转。整个过程不需要中断CPU零参与相位切换时刻由总线仲裁决定抖动比中断方案还要小。在STM32F103上DMA1的每个通道有固定的外设请求映射。TIM4的更新事件对应DMA1的通道6这一点在不同的F103型号上一致但如果你换到其他定时器或换到F105/F107还是要以参考手册里的DMA请求映射表为准。3.2 直接写DMA1通道的配置代码DMA版的状态表内容和中断版完全一样只是从uint16_t数组变成DMA的源地址。为了让DMA写ODR时只动低16位表元素用半字。/* 六扇区目标ODR值顺序与中断版一致 */ const uint16_t sq_dma_table[6] { 0x0001, 0x0005, 0x0007, 0x0006, 0x0002, 0x0000 }; void dma_square_wave_start(void) { /* GPIO和TIM4配置先完成再开关DMA时钟 */ RCC-AHBENR | RCC_AHBENR_DMA1EN; /* 确保通道关闭 */ DMA1_Channel6-CCR (uint16_t)~DMA_CCR_EN; /* 内存地址状态表外设地址GPIOA-ODR */ DMA1_Channel6-CMAR (uint32_t)sq_dma_table; DMA1_Channel6-CPAR (uint32_t)GPIOA-ODR; DMA1_Channel6-CNDTR 6; /* 方向内存-外设外设地址固定内存地址自增 半字半字传输循环模式优先级中 */ DMA1_Channel6-CCR DMA_CCR_DIR | DMA_CCR_MINC | DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_CIRC; /* 允许TIM4更新事件触发DMA */ TIM4-DIER | TIM_DIER_UDE; /* 先手动把首扇区写到ODR避免上电到第一个更新之间电平不确定 */ GPIOA-ODR sq_dma_table[0]; /* 最后使能DMA通道 */ DMA1_Channel6-CCR | DMA_CCR_EN; }这段配置里PSIZE_0和MSIZE_0都是半字也就是每次传输16位数据。MINC表示源地址自动加2外设地址GPIOA-ODR不递增。CIRC是循环模式CNDTR计数到0后自动恢复为6这样DMA表就能一直循环下去。TIM4的配置和中断版一样PSC0、ARR1199对应10kHz三路方波。区别只是不再开TIM_DIER_UIE而是开TIM_DIER_UDE。另外要让GPIOA-CRL的配置和中断版保持一致不然DMA一启动输出引脚还是浮空状态。两个方案放一起对比会更清楚对比项TIM中断BSRRTIMDMA循环相位刷新时刻更新中断触发更新事件硬件触发CPU负担每扇区一次完整中断几乎为零抖动主要来源中断延迟和压栈DMA总线仲裁GPIO写入方式BSRR原子置位/复位ODR整字覆盖适合场景快速验证、引脚复用的板子批量运行、高频方波3.3 ODR整字覆盖的边界与规避DMA方案最大的限制是它写的是整个ODR的16位不是只改PA0-PA2。sq_dma_table里每个元素只有低3位有效其他位都是0所以每次DMA传输都会把PA3-PA15全部清零。如果PA3以上还有别的功能引脚比如SPI片选、串口TX那就会被这个方案搞挂。规避方法有三种。第一种最简单把三个方波输出挪到一个完全没有其他输出的端口比如PB0-PB2代价和PA0-PA2一样低。第二种是保留中断版BSRR方案BSRR能按位操作只影响三个目标引脚。第三种是上电初始化时关掉DMA等系统其他外设配置完成后再启动DMA让被清零的引脚在运行期间本来就不参与业务。对大多数只输出三路方波的板子第一种做法最省心。另一个边界是DMA的源表放在const区也就是Flash里。F103的DMA可以直接从Flash读取数据不需要先把表拷到RAM但要注意数组的16位对齐。C编译器对uint16_t数组默认按2字节对齐DMA半字传输正好满足要求。如果你自己定义了某种打包结构体或者把表声明成uint8_t数组DMA传输时可能出现源地址非对齐触发总线错误。4. 参数化与选型频率、预分频与方波/PWM边界4.1 频率公式和3个必调参数把前面两个方案抽象一下最后落到命令行或上位机上需要调的其实只有三个参数PSC、ARR、输出引脚映射。频率公式再写一遍ARR 1 f_TCLK / ( (PSC 1) × 6 × f_sq )f_TCLK是定时器输入时钟F103里TIM2-TIM4挂在APB1上当APB1预分频不为1时定时器时钟一般是72MHz如果APB1分频系数是4定时器时钟也可能只有36MHz。所以上板后第一步先确认RCC-CFGR里的PPRE1位不要理所当然认为定时器就是72MHz。下面这张表按72MHz定时器时钟给出几组可以直接用的参数目标方波频率PSCARR实际频率说明20kHz059920.000kHz完整周期50us10kHz0119910.000kHz扇区16.67us1kHz0119991.000kHz扇区166.67us50Hz713333约49.99Hz适合电机对拖测试注意50Hz那行PSC71把计数时钟降到1MHzARR13334时事件频率约300Hz折算方波频率49.99Hz。这里出现非整数是因为50Hz不能被1MHz等分为六个60°扇区实际频率会有不到0.02%的偏差对大多数电机测试没有影响。如果一定要精确50Hz可以让PSC取其他值找到一个更接近整除的组合或者用锁相环倍频。4.2 相位方向和输出映射怎么调现实中经常碰到“波形频率对了但B相该滞后却超前了”的情况。这不是定时器配错而是扇区表的输出顺序和你的电机相序顺序不一致。如果想让B相滞后A相120°就用第2章那张表。如果现场发现电机反转或者测量结果变成A相滞后B相最简单的方法不是改接线而是把扇区表倒过来也就是把原来的扇区5放到表头依次倒序/* 反转相序B滞后A变成A滞后B */ const uint16_t reverse_table[6] { 0x0000, /* 原扇区5 */ 0x0002, /* 原扇区4 */ 0x0006, /* 原扇区3 */ 0x0007, /* 原扇区2 */ 0x0005, /* 原扇区1 */ 0x0001 /* 原扇区0 */ };如果你把A、B、C分别接到了PA2、PA1、PA0而不是PA0、PA1、PA2也不需要改硬件只需要把表里每一行的位做相应位移。比如原来0x0007表示三个都高换引脚后这个值变成0x000E。这种映射关系建议在一个宏定义或一个结构体里统一维护不要每个扇区手改很容易错。4.3 什么时候该换成TIM1高级定时器PWM上面两套方案输出的是纯方波每个引脚要么全高要么全低。但真实的三相电机驱动里半桥上下管不能同时导通需要死区低速时还需要调节占空比来控制平均电压这时纯方波就不够用了应该换成TIM1的高级PWM模式。TIM1自带三组互补输出CH1/CH1N、CH2/CH2N、CH3/CH3N可以配置死区时间和刹车输入。六步换相时不是三路都是50%占空比方波而是每相导通120°一个周期内每个管子只工作120°通过互补PWM的占空比调节电压。这就是常见的无感方波驱动中“外部比较器法无感方波”的基本形态反电动势过零点通过比较器送回到TIM1的捕获通道软件根据捕获值更新换相时刻。如果你的最终波形要经过LC滤波转成正弦波比如方波转正弦波的应用那么前面的定时器扇区表仍然可以作为相位基准但真正的功率级应该用高频率PWM配合滤波网络而不是让IGBT或MOS管直接开关10kHz方波。一句话总结这个边界标题里说的是“相位差方波”只管时序功率级要不要PWM占空比调制是另一层需求。5. 上板验证与排错波形、相位差与初相5.1 用输入捕获量两个通道的相位差示波器看三路方波最直观但很多调试现场只有逻辑分析仪甚至只有万用表。这时可以借用“计数器测量两个时钟的相位差方法”让STM32自己把相位差算出来。方法是把A相接TIM2的CH1B相接TIM2的CH2都用上升沿捕获让TIM2自由运行。A相上升沿到来时TIM2-CCR1被锁存B相上升沿到来时TIM2-CCR2被锁存。假定A相在前、B相在后相位差就是两个捕获值的差在TIM2整个计数周期中的占比uint32_t a_edge TIM2-CCR1; uint32_t b_edge TIM2-CCR2; uint32_t diff (b_edge a_edge) ? (b_edge - a_edge) : (b_edge (TIM2-ARR 1) - a_edge); float angle (float)diff * 360.0f / (float)(TIM2-ARR 1);120°相位差对应angle ≈ 120.0f也就是diff ≈ (ARR 1) / 3。如果TIM2的PSC被设置过公式里的ARR 1要换成实际计数周期。这个方法的好处是即使不知道方波频率只要两个通道用同一个TIM结论就不会受时钟精度影响。5.2 最小系统与输出检查如果下载都进不去先查最小系统。BOOT0和BOOT1要拉到确定的电平外部晶振起振后PA8会有时钟输出才说明振荡器工作。STM32F103的DAP下载失败最常见原因是复位电路里电容漏电或者调试线太长把SWDIO和SWCLK连到板子之前先确认这两个引脚没有被其他外设复用。输出侧检查顺序是先看GPIO CRL配置其次看定时器时钟是否真的72MHz再看ODR是否被DMA或中断之外的代码改动。很多人把PA0-PA2配置完就开始调扇区表结果量出来只有一路有波形一查是GPIOA-CRL的低12位被上一段代码改了。5.3 波形不对时的三处优先排查第一个优先查扇区表顺序。把表打印出来对照六个扇区状态重点看第2项和第3项。表里如果出现两个相邻扇区完全相同说明二极管箝位、代码误改或数组越界把表内容覆盖了。第二个优先查相位超前还是滞后。如果A、B、C三路频率都对但B相明显超前A相120°那就是相序反了用第4章的反转表替换即可。出现这种情况不需要怀疑定时器因为同一颗TIM4出来的三个沿间隔是固定不变的。第三个优先查跳变沿位置。用示波器把A相上升沿作为触发看B相上升沿是否落在A相周期的三分之一处。如果B相上升沿一路抖动说明DMA和GPIO之间有总线竞争或者电源纹波太大导致IO翻转不稳定。此时把DMA优先级提高一级或者在GPIO输出端加一个RC滤波吸收过冲波形就会干净很多。本文还有配套的精品资源点击获取