ARTICLE DETAIL

资讯详情

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

STM32F0 TIM1三通道PWM相位配置与互补死区实现详解

STM32F0 TIM1三通道PWM相位配置与互补死区实现详解 简介这是一款基于STM32F0系列微控制器的三通道PWM输出工程源码面向嵌入式开发、电机控制与电源调节等应用场景解决多通道PWM相位和占空比任意配置的实际需求。代码采用C语言编写并配有电路原理或效果展示图片可直观查看定时器通道配置与相位对齐逻辑。压缩包共含3个文件包括2张PNG示意图和1个C语言源文件整体仅12KB内容精简适合快速移植与参考学习。已有1832人学习下载。通过该工程可掌握STM32F0定时器TIM2的PWM模式设置、CCRx比较寄存器控制相位偏移的方法以及更新事件、中断/DMA动态调节占空比等关键知识点为多路PWM协同输出提供一套可运行的参考实现。适合有一定STM32基础、希望深入理解定时器多通道输出机制的开发者可直接对照源码理解配置流程并迁移到自主项目中。1. 三通道PWM相位任意配置要过的坎不止占空比做无刷电机控制器时三路PWM必须错开120°换相逻辑才不烧驱动做全桥电源时上下管互补输出必须插入死区否则一瞬间就直通炸管。很多STM32F0开发者在HAL里只启动了单个通道占空比能改就觉得学会了真需要三路相位差、互补输出和死区一起上时卡在CCR和CCER寄存器上好几天。这里从TIM1高级定时器的通道复用关系入手拆解三通道相位任意配置的C语言源码给出互补PWM死区时间的寄存器配置参数表再补一个动态改相位导致波形畸变的坑。源码包里的Untitled-1.c拿过来能直接编译跑适合用C或C做电机驱动、数字电源和PWM合成信号的工程师。2. STM32F0定时器架构与PWM生成原理2.1 TIM1高级定时器三通道相位的硬件基础STM32F0系列多数型号只有一个高级定时器TIM1它拥有CH1、CH2、CH3三个主输出通道和CH1N、CH2N、CH3N三个互补输出通道这才是能够支撑三相驱动和全桥的硬件前提。通用定时器TIM2、TIM3也有多个捕获/比较通道能配置多路PWM但不支持互补输出BDTR寄存器在通用定时器上不存在所以做电机控制时不要在第一轮选型中就跳过TIM1。TIM1同时支持刹车输入和可编程死区死区插入完全由硬件完成不需要在中断里做软件延时这是高级定时器和通用定时器最核心的差异。通道复用关系也要提前确认。TIM1的CH1可以映射到PA8CH2映射到PA9CH3映射到PA10小封装芯片上映射可能有差异。画板时如果三个PWM输出选成了同一端口的连续引脚往往还要检查AFIO重映射。F0系列每个引脚的可选功能在数据手册的Alternate Function Mapping表里配置GPIO时使用GPIO_AF1_TIM1。另外TIM1的时钟来自APB2在F0上APB2预分频为1时TIM1时钟与系统时钟一致如果开了APB2分频定时器时钟还会翻倍这个变量在计算PWM频率时容易漏掉。2.2 边沿对齐与中心对齐对相位差计算的影响PWM模式由TIMx_CCMRx寄存器的OCxM位决定PWM模式1和模式2只是有效电平的相对关系不同。这里要抠一个关键点在边沿对齐向上计数模式下计数器从0增加到ARRPWM模式1且极性为高有效时输出在CNT CCRx期间为高电平CNT CCRx期间为低电平。也就是说上升沿固定在计数器更新事件也就是周期起点CCRx的实际物理意义是下降沿位置而不是脉冲中心或起始点。因此边沿对齐模式下不同通道只能错开下降沿上升沿永远对齐在周期起点。这一点在计算“相位差”时最容易出偏差。中心对齐模式CMS01或10下计数器先向上再从ARR向下一个完整周期计数长度是2×ARR。CCRx控制的是计数器在上下两个斜坡上各自产生比较翻转的时刻输出脉冲关于周期中心对称这时改变CCRx会同时移动上升沿和下降沿比边沿对齐更接近通常理解的相位移动。但代价是更新事件频率减半且三通道共用一个计数器相位差仍然只能通过CCR差值表达只是同样的CCR差值对应的真实时间差要按半周期重新换算实际相位差计数等于边沿对齐模式下的CCR差值除以2。如果项目需要真正“整体平移”的三路PWM优先考虑中心对齐模式如果只是让下降沿错开比如合成阶梯波或错相整流边沿对齐就够用。2.3 PWM频率、占空比与相位分辨率的参数表PWM频率由预分频器和自动重载值共同决定f_PWM TIM_CLK / ((PSC 1) × (ARR 1))。这里PSC和ARR都是16位寄存器最大65535F0的TIM1时钟最高可以到48MHz。下表是几组实测可用的参数组合场景TIM_CLKPSCARRPWM频率边沿对齐下CCR单步对应的相位分辨率低频验证48MHz479999100Hz0.036°电机载波48MHz0239920kHz0.15°高分辨率信号48MHz099994.8kHz0.036°相位分辨率取决于计数器步长。不要为了追求16位分辨率把ARR拉到65535再用PSC压频率PSC过大会让计数器时钟抖动对输出边沿的影响放大实测相位噪声会变差。一般做法是让计数时钟尽量接近TIM_CLK通过ARR调整频率必要时牺牲一点相位分辨率换取更高载波频率。下面这段代码把角度转换成CCR偏移在边沿对齐模式下对应下降沿位置偏移// 边沿对齐PWM模式下把相位角(0-360度)换算成CCR偏移量 // base_ccr是参考通道的下降沿计数位置arr是自动重载值 uint32_t phase_to_ccr(uint32_t base_ccr, float angle_deg, uint32_t arr) { int32_t delta (int32_t)((angle_deg / 360.0f) * (arr 1)); int32_t result ((int32_t)base_ccr delta) % (arr 1); if (result 0) result (arr 1); return (uint32_t)result; }逻辑说明函数先按角度在周期中的占比求出CCR增量delta再与参考CCR相加并取模得到目标通道的CCR值。取模的意义是当相位偏移跨过ARR边界时自动回绕。这种实现适合初始化阶段调用如果在高频中断里每周期调用浮点除法会占不少CPU周期Cortex-M0没有FPU建议用整数乘除法预先算好每度对应的步长或直接查表。3. 三通道相位与占空比任意配置的C实现3.1 相位差如何转换成CCR偏移量“相位和占空比任意设置”的前提是明确参考点。在TIM1的三通道结构中计数器只有一个周期起点在所有通道的上升沿边沿对齐模式处是相同的。设定CH1为参考通道其CCR1由目标占空比决定duty CCR1 / (ARR 1)高有效。CH2、CH3的占空比如果要求与CH1一致则它们的脉冲宽度也应当是CCR1如果要求不同的占空比则各自有独立的脉冲宽度值。此时相位差体现为下降沿的间隔即目标CCR值 参考下降沿位置 相位差对应的计数值。通用配置流程分三步第一步确定ARR和PSC得到频率第二步根据目标占空比设置各通道的Pulse值第三步把各通道Pulse值加上对应的相位偏移量并取模。三步都完成后才算是“占空比和相位分别可控”。注意占空比计算在边沿对齐和中心对齐下公式不同中心对齐模式下占空比仍可近似为CCR/(ARR1)但相位偏移要按半周期折算。以100Hz、ARR9999为例120°对应的CCR增量为(10000 × 120) / 360 3333240°对应6666这就是后面代码里魔法数字的来源。3.2 从Untitled-1.c提取的三通道PWM初始化代码源码包里的Untitled-1.c核心是初始化TIM1和三个通道。整理后的完整配置如下#include stm32f0xx_hal.h TIM_HandleTypeDef htim1; TIM_OC_InitTypeDef sConfigOC; void MX_TIM1_ThreePhase_Init(void) { __HAL_RCC_TIM1_CLK_ENABLE(); htim1.Instance TIM1; htim1.Init.Prescaler 47; // 48MHz / (471) 1MHz 计数时钟 htim1.Init.CounterMode TIM_COUNTERMODE_UP; // 边沿对齐向上计数 htim1.Init.Period 9999; // PWM周期 10000个计数100Hz htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim1); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 2500; // 占空比 25%下降沿在2500 sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; // CH1 参考通道下降沿在 2500 HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); // CH2 相对CH1下降沿滞后120度偏移 10000 * 120 / 360 3333 sConfigOC.Pulse (2500 3333) % 10000; // 5833 HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_2); // CH3 相对CH1滞后240度偏移 6666 sConfigOC.Pulse (2500 6666) % 10000; // 9166 HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_3); // 启动三个主通道高级定时器必须先使能MOE主输出 __HAL_TIM_MOE_ENABLE(htim1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_3); }参数说明Prescaler和Period在寄存器里都是减1存储的HAL库的Init结构体直接给47和9999库函数内部会做减1。Pulse设置为5833意味着CH2的高电平从0持续到5833下降沿比CH1晚3333个计数即三分之一周期。三个通道的上升沿都对齐在计数器溢出点所以这种配置产生的是“同起始、不同终止沿”的PWM适合需要阶梯边沿的合成信号。如果确实要完整的波形整体平移请改用中心对齐模式并在初始化时将CounterMode改为TIM_COUNTERMODE_CENTERALIGNED1然后把相位偏移按半周期折算。3.3 运行时动态修改相位与占空比的机制实际项目中往往要在运行中改变相位例如电机换相、PWM移相调压。直接调用__HAL_TIM_SET_COMPARE会立刻修改CCR但当修改发生在当前周期内时输出会提前或延后翻转产生半个畸形脉冲。TIM1提供了预装载影子寄存器机制当OC1PE位为1时写入CCR1的值先进入预装载寄存器计数器溢出更新事件到来时才真正复制到影子寄存器这个特性保证一个周期内CCR值不会中途变化。HAL库在TIM_OC_InitTypeDef中没有暴露这个位需要在配置通道后手动设置// 使能三个通道的CCR预装载避免运行时修改造成波形畸变 htim1.Instance-CCMR1 | TIM_CCMR1_OC1PE | TIM_CCMR1_OC2PE; htim1.Instance-CCMR2 | TIM_CCMR2_OC3PE;设置预装载后动态修改相位和占空比可以封装成这样// 运行时更新某一通道的比较值下一周期生效 void pwm_set_pulse(uint32_t channel, uint16_t pulse) { if (pulse htim1.Init.Period) pulse htim1.Init.Period; if (channel TIM_CHANNEL_1) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, pulse); } else if (channel TIM_CHANNEL_2) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_2, pulse); } else if (channel TIM_CHANNEL_3) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_3, pulse); } }逻辑说明预装载使能后写入CCR不会立刻生效所以不需要关中断来保证三个通道的一致性更新。三个通道在同一个更新事件时刷新因此需要在更新事件到来之前把所有新值写入预装载寄存器这给DMA或定时器中断提供了同步窗口。如果追求极低延迟可以关闭预装载位但就要在更新中断内一次性写完三个寄存器否则先写入的通道会先改变边沿破坏三者之间的相位关系。4. 互补PWM输出、死区插入与C封装4.1 互补输出通道与BDTR死区寄存器互补输出的价值在于全桥驱动一个通道控制上管互补通道控制下管二者逻辑电平相反但硬件会保证在翻转瞬间插入一段两者同时关断的时间也就是死区。STM32F0里这部分逻辑由TIM1的BDTR寄存器完成死区时长由DTG[7:0]位段编码死区时钟T_DTS由CK_INT和DTS位决定。以下是常见编码对应关系DTG[7:5]死区计算公式当T_DTS1us时DTG值对应的死区0xxDTG[7:0] × T_DTSDTG2 → 2us10x(64 DTG[5:0]) × 2 × T_DTSDTG128 → 128us110(32 DTG[5:0]) × 8 × T_DTSDTG192 → 256us111(32 DTG[5:0]) × 16 × T_DTSDTG224 → 512us这个表在调节不同死区范围时很有用。死区不是越大越好死区过大占用了有效占空比区间输出能力下降还会引起电流波形畸变死区过小则上下管有共同导通风险。功率MOS管的栅极电荷和驱动器延迟通常需要至少几百纳秒到1微秒死区具体要拿示波器量实际切换沿来确定最终值。4.2 HAL库使能互补输出和死区的正确姿势HAL库没有把BDTR封装成完整的初始化函数需要在通道配置完成后手动写寄存器。同时要明确两个函数的职责HAL_TIM_PWM_Start启动主输出通道HAL_TIMEx_PWMN_Start启动互补输出通道后者会同时使能对应主通道的互补输出。只调用前者不会得到CH1N信号。下面是一段同时带死区和互补输出的配置void MX_TIM1_Complementary_Init(void) { // 死区时间 2 * T_DTST_DTS1usDTG2 htim1.Instance-BDTR ~TIM_BDTR_DTG_Msk; htim1.Instance-BDTR | (2 TIM_BDTR_DTG_Pos); // 使能主输出MOE高级定时器输出通路的总开关 __HAL_TIM_MOE_ENABLE(htim1); // 配置CH1主通道和互补通道的极性 sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 2500; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); // 分别启动主输出和互补输出 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); }参数说明OCPolarity控制主通道有效极性OCNPolarity控制互补通道有效极性两者都设为HIGH时正常工作状态互补输出是反相的死区期间都为低。如果驱动电路是高电平关断可能需要把OCNPolarity设为LOW并按需调整空闲状态位。BDTR的DTG位段在寄存器里紧邻MOE位用位段宏操作比直接给整个寄存器赋0xFFF更安全。调试时如果发现互补输出一直没有波形优先检查MOE是否被置位以及是否有刹车输入把输出强制拉低。4.3 把三通道PWM封装成C类用C语言写裸机函数已经足够但在一个固件里同时管理多组PWM参数时C的封装能把“频率、占空比、相位偏移”以及“何时更新”这些状态集中起来。下面这个类把三通道的相位偏移和占空比抽象成两个方法组件内部持有HAL定时器句柄class ThreePhasePWM { public: ThreePhasePWM(TIM_HandleTypeDef* htim, uint32_t period) : htim_(htim), period_(period), base_pulse_(period / 4) {} void SetDuty(float duty_cycle) { base_pulse_ static_castuint32_t(duty_cycle * period_); UpdateAll(); } void SetPhaseOffset(uint32_t channel, float angle_deg) { if (channel TIM_CHANNEL_2) phase2_ angle_deg; else if (channel TIM_CHANNEL_3) phase3_ angle_deg; UpdateAll(); } void UpdateAll() { __HAL_TIM_SET_COMPARE(htim_, TIM_CHANNEL_1, base_pulse_); __HAL_TIM_SET_COMPARE(htim_, TIM_CHANNEL_2, (base_pulse_ PhaseToTicks(phase2_)) % (period_ 1)); __HAL_TIM_SET_COMPARE(htim_, TIM_CHANNEL_3, (base_pulse_ PhaseToTicks(phase3_)) % (period_ 1)); } private: uint32_t PhaseToTicks(float deg) { return static_castuint32_t((deg / 360.0f) * (period_ 1)); } TIM_HandleTypeDef* htim_; uint32_t period_; uint32_t base_pulse_; float phase2_ 120.0f; float phase3_ 240.0f; };说明构造函数的period对应HAL的Period参数duty由调用方传入0到1之间的浮点数。SetDuty修改基础脉冲宽度三个通道的占空比同时变化SetPhaseOffset单独修改后两通道的相位角保持占空比不变。注意这个类的前提是三个通道都使用PWM模式1并且共用同一个计数器因此无法实现各自独立频率只适合固定PWM周期、独立相位和占空比的应用。在C代码中所有HAL函数仍然可以直接调用因为HAL库本身是C语言实现并导出extern C符号。5. 用逻辑分析仪验证相位关系与死区时间的实测技巧5.1 示波器触发设置三通道的边沿关系用逻辑分析仪比示波器更好看。把CH1接到参考通道触发方式设为下降沿触发因为边沿对齐模式下下降沿在各通道间是错开的以CH1下降沿为基准能直接读出CH2下降沿到它的时间差。计算期望时间差120度对应的计数差3333除以计数时钟1MHz等于3.333ms。如果在逻辑分析仪上看到的时间差与3.333ms偏差大于一个计数周期先检查PSC和Period是否按减1配置这是最常见的误差来源。5.2 三个高频踩坑点第一个坑是输出极性配置反了。PWM模式1配合OCPOLARITY_HIGH在这个例子里是计数器小于比较值时输出高如果误配成POLARITY_LOW占空比会变成1 - CCR/(ARR1)相位关系仍然保持边沿错开但整个波形反向逻辑分析仪上会多出很多低电平区间。第二个坑是互补输出启动不全只调用了HAL_TIM_PWM_Start而忘记HAL_TIMEx_PWMN_StartCH1N引脚保持空闲电平不动。第三个坑是刹车输入悬空造成的不稳定F0的TIM1_BKIN引脚如果复用了外部信号但没有配置环境噪声可能触发刹车导致所有输出被强制为无效电平。排查时先读BDTR的MOE位如果自动清0多半是刹车或故障输入进入了。5.3 用DMA预装CCR表生成连续任意波形调试相位配置时手工改寄存器能验证静态关系但验证动态性能最好用DMA连续更新CCR看看相位是否在周期边界干净切换。实现思路是把要输出的CCR值做成一张表用定时器更新事件触发DMA把表里的值依次写到TIM1_CCR2。初始化代码可以这样做先在内存里准备好长度为N的相位表再用HAL_DMA_Start_IT把表首地址、目标寄存器地址和传输长度填好使能TIM1更新DMA请求。这样每个PWM周期自动从表里取下一个比较值相位按预设轨迹连续变化不需要CPU介入。在移相调压项目里用这个办法生成了线性扫频的PWM波形CPU占用率几乎为零而且验证了预装载寄存器在多周期更新下没有丢帧。这个技巧同样适用于三相SPWM的定时器查表输出。本文还有配套的精品资源点击获取
返回列表