ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103四路90°相位差方波:TIM6+DMA实现无中断生成

STM32F103四路90°相位差方波:TIM6+DMA实现无中断生成 简介面向嵌入式开发者的 STM32F103 四通道90度相位差方波生成工程适用于电机四象限驱动、正交编码器激励及无线通信正交调制等场景。资源包含完整可编译的 Keil 工程共73个文件以C与H源码为主穿插启动汇编文件、Hex固件、工程配置和清理脚本总体积仅277KB便于直接导入工程查看已有943人学习下载。工程重点展示了基于定时器PWM的多通道相位差实现方法包括预分频器配置、比较寄存器设置、输出极性控制、多通道同步更新及异常保护策略代码基于标准外设库编写覆盖定时器初始化、PWM通道配置、相位偏移调节等关键环节各通道比较值按定时器周期的四分之一递增从而得到准确的90度相位差并通过更新事件保证四路输出同步。涉及的初始化思路与代码结构清晰适合需要快速掌握STM32F103定时器正交信号生成的嵌入式开发者参考同时也可作为后续二次开发和功能移植的基础模板。1. 4通道90°相位差方波一个经常被做成“抖动脉冲”的STM32F103需求如果你的 STM32F103 最小系统板要做的是正交编码器信号模拟、四相步进驱动前级或者给伺服驱动器灌一组可调速的 A/B/Z 脉冲那标题里的“4通道90°相位差方波”不是让你开 4 个定时器同时输出 PWM。真正的难点在“相位差”三个字四路方波同频、占空比 50%相邻通道边沿错开严格 90°且运行时能改频率、换方向不能靠 CPU 一条一条挤。搜这个标题的人大多在用标准外设库 V3.5.0 的老工程所以下面这套方案基于标准库实现把 STM32F103 的定时器更新事件当成“节拍器”用 DMA 在固定相位点刷新 GPIO不需要进中断相位误差只来自定时器计数和 DMA 响应速度。适合想一次性把信号做干净、不想反复调延时的工程师。2. 为什么中断翻转和 PWM 都做不出四路 90° 方波2.1 软件翻转的抖动上限中断延迟和相位累积误差最常见的做法是开一个定时器中断每次进入中断翻转 4 个 GPIO。可一旦频率到几十 kHz问题立刻暴露F103 的中断入口本身有固定延迟以及 NVIC 抢占、串口或其他外设中断的竞争都会让每次翻转时基偏移几十到几百纳秒。四路信号之间的 90° 关系一旦靠“翻转次数”数出来任何一次抖动都会累积成相位漂移。示波器上看上升沿间距不是恒定的 T/4而是忽大忽小。把中断优先级推到最高也救不回来因为 Flash 取指、写 ODR 的读改写、以及编译器优化都会引入不确定周期。低频 1kHz 以下可能看不出来但编码器模拟常用于 10kHz~500kHz这种方案基本不可用。2.2 PWM 只控制占空比控制不了 90° 绝对相位差有人会想用 TIM2 的 CH1~CH4设四个不同的 CCR让每个通道电平翻转时刻不同。这个思路在边沿对齐 PWM 下有一个前提性错误PWM1 模式里通道输出高电平的起点在计数器 0 处四个通道的上升沿天然对齐CCR 只能错开下降沿。要让两路 50% 占空比方波上升沿相差 90°单一 PWM 通道做不到。中心对齐模式可以改变上升/下降两个边沿的位置但 CCR 从根本上决定的是高电平窗口宽度也就是占空比。把 CCR 平移实现相位移动的同时占空比会跟着变不再是 50%。你要么接受占空比偏差要么用两套定时器主从触发做相位粗调代码复杂度和调试成本会迅速超过一个 DMA 方案。2.3 TIM6 DMA GPIO ODR 的方案原理更新事件代替中断正确思路是改变“更新对象”不用定时器中断去翻转引脚而是让定时器更新事件直接触发 DMA由 DMA 把一个相位状态字写到 GPIO 的 ODR。每来一次更新事件DMA 从内存取一个 16 位数据写到 GPIOB四路引脚的电平组合随之切换。一次 DMA 传输只需要几个 AHB 周期不占用 CPU也不会被中断延迟打扰。拿 STM32F103 的典型配置举例定时器使用基本定时器 TIM6它的更新事件映射到 DMA1 的通道 3这是官方库和大量 DAC 波形例程用过的路径。DMA 设成循环模式内存数组放 4 个相位状态定时器每计满一次就推进一个状态。方波频率和 90° 相位被同一个时基锁定不存在累积误差。2.4 三种方案对比表方案相位精度CPU 占用频率上限代码复杂度适用场景定时器中断翻转 GPIO中低受中断抖动影响高ISR 频繁几十 kHz 以下低低频演示、非关键脉冲PWM 中心对齐无法同时保证 50% 占空比和移相低较高中占空比可变的调速信号TIM6 DMA 循环写 ODR高边沿由硬件时基决定极低初始化后不用管可达数百 kHz 至 MHz 级中正交编码器模拟、四相方波源选型结论很直接只要你的负载对“四路之间的相位关系”有硬性要求就放弃中断方案把定时器和 DMA 绑定在一起。3. STM32F103 标准外设库下的最小实现TIM6DMA1 生成四路方波3.1 引脚与 DMA 资源分配这里用一个最小系统板最常见的外部引脚组合PB0、PB1、PB2、PB3 分别作为 CH1(0°)、CH2(90°)、CH3(180°)、CH4(270°) 输出。选择 PB0~PB3 的原因是它们在 GPIOB ODR 的低四位DMA 写一个半字即可一次更新四路电平后续验证也直观。资源占用如下TIM6仅作为时基不占用任何输出引脚DMA1_Channel3TIM6 更新事件触发源内存到外设循环模式GPIOB ODRDMA 外设目标地址TIM6 是基本定时器不带输出比较通道正好适合“只用来定时”的场景。如果你手头只有通用定时器比如 TIM2/TIM3也可以做同样的事但需要确认对应 DMA 通道映射因为不同定时器的更新事件挂在不同的 DMA 通道上。3.2 相位状态表推导90° 一个状态4 个半字四路方波占空比 50%周期划分成 4 个 90° 区间。每过 90°四路电平组合状态如下表相位区间CH1 (0°)CH2 (90°)CH3 (180°)CH4 (270°)GPIOB ODR值0° ~ 90°10000x000190° ~ 180°11000x0003180° ~ 270°01100x0006270° ~ 360°00110x000C这个状态表的关键在于任意相邻两个状态之间至少有一条边沿发生状态循环一圈正好是 4 次更新也就是一个完整方波周期。DMA 循环读取这 4 个半字输出就是持续不断的四路 90° 方波。3.3 可直接抄的标准库初始化代码下面的代码按标准外设库 V3.5.0 风格写硬件为 72MHz 主频的 STM32F103C8T6。#include stm32f10x.h #define PHASE_TABLE_SIZE 4 /* 相位表低四位对应 PB0/PB1/PB2/PB3 */ volatile uint16_t phase_tbl[PHASE_TABLE_SIZE] { 0x0001, /* 0°: CH11 CH20 CH30 CH40 */ 0x0003, /* 90°: CH11 CH21 CH30 CH40 */ 0x0006, /* 180°: CH10 CH21 CH31 CH40 */ 0x000C /* 270°: CH10 CH20 CH31 CH41 */ }; static void QuadWave_Init(uint32_t square_freq_hz) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; DMA_InitTypeDef dma; uint32_t timer_clk 72000000UL; uint32_t update_freq square_freq_hz * 4UL; /* 每 90° 更新一次 */ uint32_t div, psc, arr; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /* PB0~PB3 推挽输出速度必须配置为最高档 */ gpio.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); GPIO_Write(GPIOB, phase_tbl[0]); /* 计算分频先保证 ARR 不超过 16 位 */ div timer_clk / update_freq; psc 0; while (div / (psc 1) 0xFFFFUL) { psc; } arr div / (psc 1) - 1; /* TIM6 作为时基更新频率 72MHz / ((PSC1)*(ARR1)) */ TIM_TimeBaseStructInit(tim); tim.TIM_Prescaler psc; tim.TIM_Period arr; TIM_TimeBaseInit(TIM6, tim); TIM_ARRPreloadConfig(TIM6, ENABLE); /* DMA1_Channel3 对应 TIM6 的更新请求 */ DMA_DeInit(DMA1_Channel3); dma.DMA_PeripheralBaseAddr (uint32_t)GPIOB-ODR; dma.DMA_MemoryBaseAddr (uint32_t)phase_tbl; dma.DMA_DIR DMA_DIR_PeripheralDST; /* 内存 - 外设 */ dma.DMA_BufferSize PHASE_TABLE_SIZE; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; /* 循环发送相位表 */ dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, dma); /* 先使能 DMA再打开定时器避免丢第一个状态 */ DMA_Cmd(DMA1_Channel3, ENABLE); TIM_DMACmd(TIM6, TIM_DMA_Update, ENABLE); TIM_Cmd(TIM6, ENABLE); }这段代码做了三件事初始化 PB0~PB3 为推挽输出按照目标方波频率计算 TIM6 的分频参数把 phase_tbl 数组配置成 DMA 的循环源。请注意DMA_DIR被设置为DMA_DIR_PeripheralDST标准库这里语义是“外设作为目的地”实际方向是内存到外设命名容易看反。3.4 频率公式与常见取值输出更新频率和方波频率的关系是更新频率 72MHz / ((PSC1) * (ARR1)) 方波频率 更新频率 / 4也就是说 TIM6 每触发一次 DMA四路方波整体推进 90°。四个更新才走完一个 360° 周期。下面给出几个目标频率下的参数参考代码中已经按 div 自动计算这里用来验证你的手算结果方波频率PSCARR实际方波频率100 Hz179999100 Hz1 kHz179991 kHz10 kHz189910 kHz100 kHz0179100 kHz500 kHz035500 kHz4. 参数调整目标频率计算、串口调速和相位反向4.1 运行时修改频率PSC 和 ARR 一起改上面代码里的分频参数是一次性算好的。实际设备通常要能在运行中调速比如用串口发一个频率值。常见做法是把计算逻辑封装成独立函数调用时直接修改 TIM6 的 PSC 和 ARR。void QuadWave_SetFreq(uint32_t square_freq_hz) { uint32_t timer_clk 72000000UL; uint32_t update_freq square_freq_hz * 4UL; uint32_t div, psc, arr; if (square_freq_hz 0) { TIM_Cmd(TIM6, DISABLE); return; } div timer_clk / update_freq; psc 0; while (div / (psc 1) 0xFFFFUL) { psc; } arr div / (psc 1) - 1; TIM_PrescalerConfig(TIM6, psc, TIM_PSCReloadMode_Update); TIM_SetAutoreload(TIM6, arr); }注意TIM_PrescalerConfig的更新方式是TIM_PSCReloadMode_Update也就是等 TIM6 当前周期结束后新分频值才生效。这样改频率时不会切断正在输出的半周期四路相位关系也不会因为改参数而错位。4.2 用串口把频率和控制指令接进来只做开环输出意义不大我一般会把串口 1 或串口 3 的中断接进来收一个简单文本指令。串口 1 在 APB2 总线上有独立的 USART1_IRQn串口 3 挂在 APB1 上初始化时钟和中断向量不同但解析逻辑完全一样。/* 假设串口中断里收到一行文本F1000\n */ void Parse_Command(const char *cmd) { uint32_t freq; if (cmd[0] F) { freq 0; sscanf(cmd 1, %lu, freq); QuadWave_SetFreq(freq); } }sscanf在 F103 上通常能从标准 C 库直接调用注意小内存工程要检查链接器是否打入了scanf系列函数。如果不想引入格式化库可以自己写一个strtoul手工解析这也顺带避开了热词里常被问到的strcmp使用差异。4.3 反转方向phase 数组倒序四路方波的方向对编码器模拟非常重要。正转时相位状态按0°→90°→180°→270°推进反转时应该反过来走0°→270°→180°→90°。对应的 phase_tbl 重排为phase_tbl[0] 0x0001; /* 0° */ phase_tbl[1] 0x000C; /* 反向的下一状态270° */ phase_tbl[2] 0x0006; /* 180° */ phase_tbl[3] 0x0003; /* 90° */直接改数组在 DMA 循环模式下最终会生效但有可能在切换瞬间产生一个错误状态。更稳的做法在最后一章讲先记住方向反转的本质不是翻转某个 GPIO而是改变状态表的循环顺序。4.4 ODR 直接写的注意事项DMA 把数据写到GPIOB-ODR这是整个方案的唯一敏感点。ODR 是读-改写寄存器DMA 写入的是一个 16 位半字如果其他代码也通过GPIO_WriteBit操作 GPIOB 的其它引脚理论上可能覆盖掉 DMA 刚写入的低位状态。避免方法有两种一是把四路方波引脚占用的低四位从所有业务逻辑中隔离CPU 只操作 BSRR/BRR二是将方波输出放到独立 GPIO 端口上比如 PORTA 的低四位DMA 写GPIOA-ODR其他功能绕开这个端口。实际项目中我更推荐第二个办法端口隔离能减少很多无法复现的偶发问题。5. 逻辑分析仪验证与排错边沿间隔 25% 周期才算对5.1 状态表对照法验证四路时序代码下进去以后不要直接看频率先抓四路波形。正常情况下应该看到四条频率相同的方波CH1 相对 CH2、CH2 相对 CH3、CH3 相对 CH4 的上升沿之间正好差一个方波周期的四分之一。以 1kHz 方波为例相邻上升沿间隔应为 250μs。如果图上看到四路频率正确但间隔明显不是 T/4大概率是相位表顺序错了。用逻辑分析仪把四路解析成总线值对照 3.2 节的状态表正转应该循环出现1, 3, 6, C这 4 个值。顺序一旦是1, 6, 3, C说明数组元素放混了。5.2 用 CH1 XOR CH2 做 2 倍频验证90° 相位差有一个非常实用的物理特征把 CH1 和 CH2 接进一个异或门输出端的频率刚好是原方波的 2 倍。如果你的板子上没有额外逻辑门可以用示波器的数学通道做 CH1 XOR CH2。这个 2 倍频信号的高电平宽度应该严格等于原方波的半周期。这个验证方法比肉眼量边沿快很多因为它把相位误差转化成了脉宽误差。任何一个通道的相位不对异或结果的脉宽就会变成 1/4 周期或 3/4 周期一眼就能看出来。5.3 常见坑对照表现象原因处理方式四路无输出TIM6 和 DMA 使能顺序反了先DMA_Cmd再TIM_Cmd输出频率减半或翻倍更新频率与方波频率关系搞错确认公式方波 更新频率 / 4高频时波形边沿不陡GPIO_Speed 配成了 2MHz改成GPIO_Speed_50MHzDMA 只发一次就停DMA_Mode 没有配为 Circular检查DMA_Mode是否为循环模式频率改动后出现毛刺PSC 立即生效导致计数周期不完整使用TIM_PSCReloadMode_Update个别引脚被其他外设干扰ODR 被 CPU 读改写覆盖将输出引脚挪到独立端口还有一个容易被忽略的点TIM6 的时钟来自 APB1。如果 APB1 分频系数不是 1定时器时钟会自动倍频到 72MHz。用标准库RCC_GetClocksFreq读一次PCLK1_Timer_Frequency确认你代码里的 timer_clk 没算错。6. 动态换向不产生毛刺的 DMA 完成中断技巧直接在图 4.3 那种“改数组”的做法里换向DMA 可能正好处于 phase_tbl 的中间位置切换后会先读到新数组的后续元素导致状态跳变不连续。要让换向在相位表的边界处发生可以利用 DMA 循环模式自带的传输完成中断DMA 每传完一轮 4 个状态会触发一次 TC 中断此时 DMA 内部地址已经回到数组首地址但下一次更新还没有开始。在这个中断里改写 phase_tbl[1] 到 phase_tbl[3]就能保证反向从 0° 状态之后才开始不会出现半个错误状态。volatile uint8_t dir_change_pending 0; void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3)) { DMA_ClearITPendingBit(DMA1_IT_TC3); if (dir_change_pending) { dir_change_pending 0; /* 反向状态表从 0° 之后进入 270° */ phase_tbl[1] 0x000C; phase_tbl[2] 0x0006; phase_tbl[3] 0x0003; } } }调用换向时不需要停定时器只需要置dir_change_pending 1TC 中断会在下一次周期边界执行替换。phase_tbl[0] 在正转和反转状态下都是 0x0001所以第一个状态不需要改。这个技巧同时适用于编码器方向切换和步进电机正反转控制省掉了外部方向引脚和额外逻辑。TC 中断本身只执行几个数组赋值时间很短不会影响正常 DMA 时序。如果后续还想扩展单步脉冲输出可以把dir_change_pending换成模式变量比如在 DMA 完成中断里再判断是否执行一次状态推进后暂停这样就能顺手得到一个可单步的正交脉冲发生器输出精度和连续模式完全一致。本文还有配套的精品资源点击获取
返回列表