STM32F103生成三路120°相位差方波的两种实现方案
2026/9/16 14:50:38 网站建设 项目流程

简介:基于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主频72MHz,GPIO单条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的电平,相位关系就自动满足了。

如果定时器时钟是72MHz,PSC分频系数是PSC,ARR重载值是ARR,目标方波频率是f_sq,那么事件间隔满足:

ARR + 1 = f_TCLK / ( (PSC + 1) × 6 × f_sq )

这里的6就是六个60°扇区。比如要输出10kHz三路方波,PSC设为0,ARR + 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°0010x0001
160°-120°1010x0005
2120°-180°1110x0007
3180°-240°1100x0006
4240°-300°0100x0002
5300°-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° : A=1 */ 0x0005, /* 60° -120° : A=1 C=1 */ 0x0007, /* 120°-180° : A=1 B=1 C=1 */ 0x0006, /* 180°-240° : B=1 C=1 */ 0x0002, /* 240°-300° : B=1 */ 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 / (0+1) / (1199+1) / 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;

代码里CRL0xBBB是三个引脚各占4位,0xB表示50MHz推挽输出。很多人在这里只配置了MODE忘记清CNF,结果引脚变成开漏或模拟输入,方波一直拉不高,这是最常遇到的一个坑。

2.3 为什么用TIM4而不是TIM1

F103里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_0MSIZE_0都是半字,也就是每次传输16位数据。MINC表示源地址自动加2,外设地址GPIOA->ODR不递增。CIRC是循环模式,CNDTR计数到0后自动恢复为6,这样DMA表就能一直循环下去。

TIM4的配置和中断版一样,PSC=0ARR=1199对应10kHz三路方波。区别只是不再开TIM_DIER_UIE,而是开TIM_DIER_UDE。另外要让GPIOA->CRL的配置和中断版保持一致,不然DMA一启动,输出引脚还是浮空状态。

两个方案放一起对比会更清楚:

对比项TIM中断+BSRRTIM+DMA循环
相位刷新时刻更新中断触发更新事件硬件触发
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个必调参数

把前面两个方案抽象一下,最后落到命令行或上位机上需要调的其实只有三个参数:PSCARR、输出引脚映射。

频率公式再写一遍:

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完整周期50us
10kHz0119910.000kHz扇区16.67us
1kHz0119991.000kHz扇区166.67us
50Hz713333约49.99Hz适合电机对拖测试

注意50Hz那行,PSC=71把计数时钟降到1MHz,ARR+1=3334时,事件频率约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的CH1,B相接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滤波吸收过冲,波形就会干净很多。

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

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

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

立即咨询