资讯动态

STM32定时器输出三路相差120°方波的原理与实现

发布时间:2026/9/16 18:29:51 来源:尧图企业网站定制
简介面向嵌入式开发与电机控制应用场景提供基于STM32F103的三通道120°相位差方波生成方案。工程围绕定时器PWM输出展开通过预分频器、自动重载值及三个捕获比较寄存器的配合实现精确相位差可用于无刷直流电机或步进电机控制等三相驱动场景。压缩包共73个文件其中34个h头文件与33个c源文件构成完整固件工程另含启动文件、Keil工程配置uvprojx/uvoptx以及可直接烧录的hex固件整体仅279KB轻量易用。已有588人学习参考。代码中除核心定时器配置外还附带了串口通信模块、系统延时及单片机基础外设初始化内容预览显示其目录结构清晰便于快速定位并理解相位差产生的完整流程。无论是入门三相方波生成原理还是在实际项目中移植应用这份工程都能提供直观的代码参考与调试基础。1. 三个相差 120° 的方波为什么值得翻工程做无刷电机驱动或者三相逆变的人迟早会遇到一个需求从单片机引脚上直接输出三路占空比相同、相位依次相差 120° 的方波。有人用定时器中断里翻转 GPIO 来做结果频率一高中断里还没翻完下一次中断又来了有人用三路独立定时器拼相位结果硬同步问题一大堆码盘一转波形就漂。实际拆过这个 STM32F103 工程会发现它用的是硬件定时器直接输出三路 PWM把 120° 相位差落实在捕获比较寄存器 CCR 的数值差上主循环几乎不参与波形生成。这个方案适合三相方波无刷电机驱动、基础三相信号源、以及对相位一致性有要求但不想上复杂控制算法的场景。下面直接从定时器时钟树讲起把相位差是怎么从寄存器里“算”出来的拆开看。2. 定时器时钟、分频与重装值先把周期为什么是 ARR 说了2.1 APB1/APB2 预分频对定时器时钟的决定性影响STM32F103 的定时器时钟不是直接等于系统主频它经过了 APB 预分频器的处理。常见的 72 MHz 主频配置下挂在 APB1 上的 TIM2~TIM4 在 APB1 分频系数不等于 1 时定时器时钟会翻倍到 72 MHz挂在 APB2 上的 TIM1、TIM8 同理。工程里如果用的是标准库SystemInit()默认配置外部晶振 8 MHz、倍频到 72 MHzAPB1 一般是 36 MHz分频系数 2所以 TIM2~TIM4 的时钟是 36×272 MHz。这个数字很重要因为它直接决定 PSC 和 ARR 怎么取。RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);这里开的是 TIM1 和 GPIOA、AFIO 时钟AFIO 时钟的作用稍后在引脚重映射部分说。TIM1 在 APB2 上主频 72 MHz 时它的时钟就是 72 MHz不需要像 TIM2 那样考虑 APB1 分频倍频的问题。要确认实际定时器时钟标准库可以直接读RCC-CFGR里的 PPRE1/PPRE2 位段或者简单点先用RCC_GetClocksFreq()看PCLK1和PCLK2APB1 分频为 2 时 TIM2~TIM4 时钟 2×PCLK1这个换算关系常被人忽略。2.2 PSC 与 ARR 的乘积决定频率CCR 的位置决定相位方波输出的周期由(PSC1) × (ARR1) / TIM_CLK决定。假设要输出 10 kHz 方波定时器时钟 72 MHz可以取 PSC71、ARR99这样计数频率是 1 MHz周期 100 µs正好 10 kHz。PSC 和 ARR 不是随便取的PSC 越小计数分辨率越高相位调节步进越细ARR 则决定一个周期内有多少个计数点120° 相位差就靠它来量化。uint16_t arr_val 99; // 周期计数点决定 PWM 频率 uint16_t psc_val 71; // 分频决定计数时钟 // 120° 相差 一个周期的 1/3 uint16_t ccr1 arr_val / 3; // 通道1 比较值占空比 2/3 时对应下降沿位置有人把 CCR1、CCR2、CCR3 理解为“占空比字典”但它本质是“计数到什么位置翻转输出”的阈值。PWM 模式 1 下计数器 CNT 小于 CCR 时输出有效电平大于 CCR 时输出无效电平。所以 CCR 就是方波从高到低的那个位置点。把三个通道的 CCR 依次错开 ARR 的 1/3输出波形就自然相差 120°前提是三个通道共用一个定时器、同一个 CNT。这是整个方案的核心三路共用计数基准相位误差只取决于 CCR 数值是否精确而不是中断响应速度。2.3 相位偏差一个计数点会偏多少度实际配置时ARR 不一定是 3 的整数倍。比如 ARR99 时99/333CCR133、CCR266、CCR399刚好每个通道相差 33 个计数点每计数点对应 360/1003.6°120° 精确成立。但如果 ARR 换成 100100/3 取整是 33CCR133、CCR266、CCR3100第三路的相位实际比第一路多了 67 个计数点而不是 66相位差变成 67×3.564°−66×3.564°多出约 3.5° 的误差。这个误差在电机驱动里可能表现为转矩波动在要求不高的信号源里可以接受但如果要严格 120°ARR 值必须先想好常见做法是直接把 ARR 设成 3 的倍数或者用浮点算好 CCR 之后四舍五入写入寄存器。#define PWM_PERIOD (3000) // ARR 取 3 的倍数 #define PHASE_120 (PWM_PERIOD / 3) TIM_SetCompare1(TIM1, PWM_PERIOD * 2 / 3); // 占空比 2/3上升沿在 0下降沿在 2/3 周期 TIM_SetCompare2(TIM1, PWM_PERIOD / 3); // 下降沿在 1/3 周期和通道1 相差 120° TIM_SetCompare3(TIM1, 0); // 下降沿在 0即 1/3 周期处下降三个通道的“上升沿”其实都在 CNT0 处因为 PWM 模式 1 下 CNT 从 0 开始计数凡是 CNT 小于 CCR 的输出都是高电平。如果三个 CCR 都小于 ARR 且不为 0三路波形的上升沿是重叠的下降沿依次错开这才是三相 PWM 常见的工作方式。真正的三相方波要求高电平宽度占 120°即占空比 1/3那么 CCR1、CCR2、CCR3 分别是周期的 2/3、1/3、0从同一时刻看三路高电平依次错开。上边代码用的就是这个逻辑。3. GPIO 重映射与 PWM 模式寄存器配置3.1 引脚选择TIM1_CH1/CH2/CH3 在哪几个脚TIM1 的三路 PWM 通道默认映射在 PA8、PA9、PA10。这个工程如果用的是标准库且没有调用GPIO_PinRemapConfig()默认就是这三个脚。但 PA9、PA10 同时是 USART1 的 TX/RX如果项目还要跑串口打印很容易冲突。常见绕过方案是启用 TIM1 的部分重映射把 CH1、CH2、CH3 挪到 PE9、PE11、PE13但这是 100 脚以上的封装才有的选项LQFP48 或者 LQFP64 的 STM32F103C8T6 没有这些引脚所以很多工程直接把串口换到 USART2/USART3。这个工程的文件列表里同时出现了 usart1、usart2、usart3大概率就是做这个隔离。GPIO_InitTypeDef GPIO_InitStructure; 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);推挽复用输出是 PWM 的标准 GPIO 形态。速度选 50 MHz 是因为方波沿很陡GPIO 翻转速率不够会把边沿拉圆示波器上看上升沿变成斜坡电机驱动里还会增加开关损耗。10 MHz 模式不是不行只是 10 kHz 方波、上升沿要求不高的场景够用但我一般直接 50 MHz避免后续调频率时还要回来改这里。3.2 TIM1 的预装载、PWM 模式与输出极性位TIM1 是高级定时器和 TIM2/TIM3 有个关键区别它默认输出的是带死区插入的互补波形用的有些位必须显式配置否则输出不正常。比如TIM_CtrlPWMOutputs(TIM1, ENABLE)是 MOE 主输出使能位普通定时器没有这一位TIM1 不开它CC1E、CC2E、CC3E 配置得再对也没波形。这是新手从 TIM3 迁到 TIM1 最容易踩的坑。TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseStructure.TIM_Prescaler psc_val; TIM_TimeBaseStructure.TIM_Period arr_val; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse ccr1; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC2Init(TIM1, TIM_OCInitStructure); TIM_OC3Init(TIM1, TIM_OCInitStructure);上面这段是“三通道同一个占空比配置”的偷懒写法TIM_OC1Init 之后把同一个结构体直接喂给 OC2、OC3。结构体里的 TIM_Pulse 是初始值后续用 TIM_SetCompare1/2/3 单独改所以三个通道初始化成同一个初始 CCR 没关系。TIM_OCMode_PWM1 在向上计数模式下表示 CNTCCR 输出高电平如果需要反极性波形改成 TIM_OCPolarity_Low 并把 PWM 模式换成 PWM2相位关系会反转 180°这个容易在调试时绕晕。3.3 TIM_CtrlPWMOutputs 和 ARPE 位为什么必须一起处理高级定时器特有的位还有一个TIM_CR1.ARPE自动重装载预装载使能。如果 ARPE0TIM_Period 的写入会立即生效这就产生一个风险程序运行中修改周期时CNT 已经计到 80 了ARR 突然从 99 变成 50CNT 会立刻归零造成周期抖动。如果 ARPE1ARR 的新值要等 CNT 计数到当前 ARR 发生更新事件后才生效所有通道同步切换波形不会有毛刺。标准库TIM_TimeBaseInit默认把 ARPE 置 1但如果有人手动清过 TIM_CR1就要注意这个行为差异。TIM_ARRPreloadConfig(TIM1, ENABLE); // ARPE1周期变化时等更新事件 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_CtrlPWMOutputs(TIM1, ENABLE); // MOE 主输出使能TIM1 专用 TIM_Cmd(TIM1, ENABLE);CCR 预装载的道理和 ARR 一样没有 CC1PE 时写入 CCR1 的值立刻被比较器采用如果刚好在 CNT 越过旧 CCR 的瞬间改值这一周期里输出脉宽就会突然变化一次。对三相相位精确性要求高的场景这个瞬间误差会传导到电机电流上。所以上面四行配置缺一不可。注意顺序先开预装载再使能输出最后TIM_Cmd反过来配置偶尔会出现第一个周期输出异常。4. 主循环调度、串口参数修改与失效保护4.1 主循环只改 CCR相位更新的安全窗口标准库工程里主循环通常是 while(1) 里轮询按键或接收串口指令然后调用TIM_SetCompare1(TIM1, new_ccr1)类似的接口。这个调用本身没有保护写入动作可能发生在 CNT 任意位置。虽然有 CC1PE 预装载新值只会在更新事件时被锁存但 CCR1、CCR2、CCR3 三个值的更新不是原子的用户可能改了 CCR1又改了 CCR2在两次写入之间定时器已经发生了一次更新事件导致 CCR1 用新值、CCR2 用旧值跑完一个完整周期。一个周期后 CCR2 才跟上表现出来就是相位突然跳变一下。if (new_phase_valid) { TIM-CCR1 new_ccr1; // 直接访问寄存器也无法避免非原子 TIM-CCR2 new_ccr2; TIM-CCR3 new_ccr3; }规避办法有两种一是先关更新中断或者用TIM_GenerateEvent(TIM1, TIM_EventSource_Update)之后立刻写三个 CCR但这会改变波形的绝对相位。二是三个 CCR 同时写入后人为产生一个更新事件让它们一起生效代价是这一个更新的瞬间周期会有微小扰动。如果对相位连续性要求极高可以改用 DMA 把 CCR1/CCR2/CCR3 的地址按 4 字节间隔排好用一次 DMA 突发传输写入但标准库工程里少见这么干我一般会接受“修改瞬间有一周期过渡”这个瑕疵关键是不要频繁改。4.2 串口指令改频率和相位的结构设计既然工程里带了 usart1/usart2/usart3最常见的玩法就是串口下发“频率、相位、占空比”三项参数主循环解析后更新 PSC/ARR/CCR。这里有一个优先级问题先改 ARR 还是先改 CCR如果先改 ARR旧 CCR 可能大于新 ARRPWM 会变成 100% 或 0% 电平一个周期如果先改 CCRARR 旧值可能小于新 CCR同样先出现异常电平。正确顺序是提前算好全部参数停掉定时器全部写入再启动TIM_Cmd(TIM1, DISABLE); TIM_SetAutoreload(TIM1, new_arr); TIM_SetCompare1(TIM1, new_arr * 2 / 3); TIM_SetCompare2(TIM1, new_arr / 3); TIM_SetCompare3(TIM1, 0); TIM_Cmd(TIM1, ENABLE);停定时器会直接让 PWM 输出停在当前电平对电机驱动来说这等于瞬间撤掉驱动信号可能引起电流冲击。所以这种做法只适合“信号源”类应用。电机驱动中如果要在运行中调速应该保持 ARR 不变、只改 PSC因为 PSC 预分频器同样有缓冲区但它影响的是计数时钟频率周期变化的瞬间相位跳变是不可避免的通常靠软件斜坡来缓解。4.3 MN 引脚、BKN 输入和三相互锁思路TIM1 的刹车功能在工程里经常被忽略但它其实非常适合做三相输出的硬件保护。把三路 PWM 的使能信号引到硬件电路后如果有一路电源或逻辑异常代码还没反应过来引脚已经输出错误电平。TIM1 的TIM_BKIN引脚默认 PB12拉低时MOE 位会被硬件清掉所有 PWM 输出立即进入空闲态这个响应是纳秒级的比中断快一个量级。TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Disable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime 0x00; TIM_BDTRInitStructure.TIM_Break TIM_Break_Enable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_Low; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Disable; TIM_BDTRConfig(TIM1, TIM_BDTRInitStructure);这段不是必须的但如果板子上有硬件过流信号接到 BKIN 比接外部中断靠谱得多。注意的是死区时间TIM_DeadTime设为 0x00 表示不插入死区方波信号源确实不需要死区如果是给三相半桥直接驱动功率管死区是必须算的否则上下桥直通。这个工程标题没有提死区我判断它就是逻辑信号输出而非直接驱动功率管。5. 示波器量相差三路波形怎么看才是 120°把三路 PWM 输出接到示波器三个通道先用自动量程看到三路占空比相同、周期相同的方波然后测量它们的相位差。相位差的定义不是看下降沿的绝对距离而是用同一周期的上升沿或下降沿之时间差除以周期再乘以 360°。具体操作打开示波器的两个光标分别放在通道 1 和通道 2 的下降沿读Δt再读出周期T相位差 Δt / T × 360°。如果 ARR3000、计数频率 1 MHz一个周期是 3ms120° 对应的 Δt 应该是 1ms测量误差一般不超过 1µs换算成相位就是 0.12° 以内这是示波器光标测量的极限。// 输出 10kHz 时一个周期 100us // 120° 对应时间 100us * 120 / 360 33.33us // 实测 delta_t 落在 33us ~ 33.7us 都算正常示波器采样率 100MSa/s 时相位误差的真实来源不是 GPIO 翻转速度GPIO 翻转延迟只有几十纳秒三路是同一个定时器同时翻转误差可以忽略。真正的问题是 CCR 量化误差和 ARR 是否可被 3 整除。如果 ARR10001/3 周期是 333.33 个计数点寄存器只能写 333误差 0.33 个计数点计数时钟 1MHz 时对应 0.33µs10kHz 周期换算相位误差约 1.2°电机驱动里勉强可用但严格方案建议把 ARR 设成 3000 这样的 3 的倍数。还有一个从示波器上看不出来的问题三路 PWM 的占空比如果刚好是 50% 附近CCR 的微小误差会同时被误读成相位误差所以测量时应先确认占空比再把光标对准同一跳变沿方向不要上升沿对下降沿那会无端加入 180°。高级验证方法是把三路信号异或后观察脉冲宽度或者用逻辑分析仪的协议解析功能直接读每路上升沿时间戳。但手头只有示波器时有个更快的检查技巧把通道 2 的触发电平调成和通道 1 完全一致然后把通道 2 的边框向右移动 120° 对应的时间刻度如果三路波形完全重合说明相位差已经满足要求。这个做法不需要逐个量 Δt判断效率高一截。配合频闪法测瞬时相位的时候把时基调到周期的三倍三路方波的低电平区间会刚好在屏幕上排成相切但不交叠的形态肉眼扫一眼就能识别相位是否漂移。相位校正方面如果实测发现通道 2 落后了 1 个计数点直接给 CCR2 加 1 就行但要注意方向PWM 模式 1 下 CCR 增大会让下降沿后移即相位滞后增加CCR 减小则相位提前。改完再用示波器确认一般两个来回就能把三路误差修到 0.1° 以内。运行频率不变时这个偏差是固定的修一次就能长期稳定。本文还有配套的精品资源点击获取

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

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

免费获取报价