ARTICLE DETAIL

资讯详情

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

STM32定时器输出固定数量PWM脉冲的三种实现方案

STM32定时器输出固定数量PWM脉冲的三种实现方案 简介一份面向STM32定时器PWM脉冲数量控制场景的工程资源包适合嵌入式开发者在电机控制、灯光调光、信号发生等项目中快速上手。整个包体内容完整共计214个文件以C源码.c/.h、STM32启动汇编文件.s、编译生成的目标文件.o及Keil工程配置.uvproj为主另有hex烧录文件和map映射文件压缩包大小约3.27MB可直接打开Keil工程查看寄存器配置与中断逻辑。资源基于STM32F10x系列深入讲解了定时器预分频器、自动重载寄存器ARR、捕获比较寄存器CCR的作用以及通过更新事件动态调整PWM周期和脉冲数量的方法同时覆盖多通道输出、死区时间等实用细节便于从原理到代码理解定时器PWM的底层机制。已有826人学习下载适合有一定单片机基础、希望掌握STM32定时器PWM精确控制的开发者参考与二次开发。1. 定时器控PWM脉冲数量核心不是占空比而是计数时机在嵌入式项目里“输出N个PWM脉冲然后停止”这个需求比看起来要棘手很多。比如步进电机要精确走200步伺服驱动器需要外部给定一串固定数量的脉宽调制信号又或者在批量信号发生应用中要求每帧脉冲数严格一致。很多人习惯把定时器配置成PWM模式后只关心ARR决定频率、CCR决定占空比结果发现脉冲会无限发下去停不下来。拆开这个标准外设库工程stm32f10x_tim.c 搭配 gpio.c、logic.c会发现真正的功夫在“什么时候数完N个脉冲”上软件更新中断、重复计数寄存器、主从定时器级联是三条最常用的路径。下面我们就沿着这三个方向把硬件行为、参数计算和坑都过一遍适合需要精确控制PWM数量的工程师。2. 先看懂stm32f10x_tim.c定时器时钟与PWM模式的寄存器级配置2.1 从工程文件反推定时器资源分配压缩包里出现的 power_manage.__i、logic.__i、gpio.__i、AllInOne.axf、AllInOne_uvproj.bak 和 stm32f10x_tim.c其实是一个典型的 Keil 标准外设库工程。.__i后缀是 Keil 在预处理后生成的中间文件.axf是编译链接出来的最终映像.uvproj.bak则是工程备份。看到 stm32f10x_tim.c 出现基本可以确定它走的是标准外设库Standard Peripheral Library而不是 LL 或 HAL。在这种库下面定时器的初始化由TIM_TimeBaseInitTypeDef、TIM_OCInitTypeDef这些结构体完成寄存器层面的控制则直接落在stm32f10x_tim.c的函数里。从文件名还能看出模块划分power_manage 管时钟和供电logic 管脉冲计数和关断逻辑gpio 管引脚复用。这种划分很合理因为定时器 PWM 应用至少要关心三件事定时器本身的时钟是否打开、引脚是否复用对了、以及谁在负责“数到N就停”。在标准库下第一个问题由RCC_APB1PeriphClockCmd或RCC_APB2PeriphClockCmd解决第二个问题由GPIO_Init解决第三个问题则需要自己写逻辑这也是我们后面几章的重点。2.2 初始化代码PWM模式、预分频与重载值的计算以 TIM3 为例它挂在 APB1 总线上。系统主频 72MHz 时APB1 分频器为 2但定时器时钟不是 36MHz而是 36MHz 的 2 倍频也就是 72MHz。这个倍频来自 RCC 的 APB1 预分频配置当 APB1 预分频不等于 1 时定时器时钟是 APB1 的两倍。很多人在算频率的时候直接用SystemCoreClock / (psc1) / (arr1)在默认时钟树下的确成立但改过 RCC 配置后就要重新核实。下面是一段完整的 TIM3 输出 PWM 的初始化代码使用标准外设库走的是边沿对齐模式#include stm32f10x_tim.h #include stm32f10x_gpio.h #include stm32f10x_rcc.h TIM_TimeBaseInitTypeDef tim_base; TIM_OCInitTypeDef tim_oc; GPIO_InitTypeDef gpio; void TIM3_PWM_Init(uint16_t prescaler, uint16_t period, uint16_t pulse) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_StructInit(gpio); gpio.GPIO_Mode GPIO_Mode_AF_PP; /* 复用推挽输出 */ gpio.GPIO_Pin GPIO_Pin_6; /* TIM3_CH1 映射到 PA6 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); TIM_TimeBaseStructInit(tim_base); tim_base.TIM_Prescaler prescaler; tim_base.TIM_Period period; /* ARR */ tim_base.TIM_ClockDivision 0; tim_base.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, tim_base); TIM_OCStructInit(tim_oc); tim_oc.TIM_OCMode TIM_OCMode_PWM1; tim_oc.TIM_Pulse pulse; /* CCR默认占空比 */ tim_oc.TIM_OutputState TIM_OutputState_Enable; TIM_OC1Init(TIM3, tim_oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); }参数里prescaler范围是 0~65535对应预分频器 PSCperiod写入 ARRpulse写入 CCR1。在 PWM1 模式下CNT 小于 CCR 时输出有效电平大于等于 CCR 时输出无效电平。因此 PWM 频率为定时器时钟 / (prescaler1) / (period1)占空比为pulse / (period1)。注意 ARR 和 CCR 都减 1 计算因为计数器是从 0 开始的。如果pulse大于period输出会一直保持有效这是常见的配置错误。2.3 不同定时器的边界基本、通用、高级定时器差异STM32F10x 系列里的定时器资源并不是都适合做 PWM更不是都有重复计数能力。选型时首先要分清三类定时器总线通道数PWM输出特殊功能TIM1 / TIM8APB24互补支持死区、刹车、RCR重复计数TIM2 ~ TIM5APB14支持通用可做编码器接口TIM6 / TIM7APB10不支持基本时基只能做内部触发从这张表能看出TIM1/TIM8 是唯一带重复计数寄存器 RCR 的定时器这对于硬件计数 N 个脉冲非常关键。通用定时器没有 RCR只能靠软件在更新中断里计数或者通过主从级联借用另一个定时器计数。基本定时器连 PWM 输出都不行只能做内部 DAC 或时基触发。另外高级定时器的 PWM 输出默认是关的必须调用TIM_CtrlPWMOutputs(TIM1, ENABLE)打开主输出 MOE否则引脚上没有任何电平变化这一点经常被忽略。3. 实现定长脉冲从“一直发”到“发N个停”3.1 方案一更新中断里做软件计数最简单、最容易理解的做法是让 PWM 自由运行并在定时器的更新中断里对脉冲周期计数。所谓更新事件就是计数器溢出从 ARR 回到 0 的时刻。每一次更新事件意味着一个完整的 PWM 周期结束。在更新中断里累加一个变量当达到目标值pulse_target时关闭定时器同时把输出引脚拉到一个确定电平防止最后留下一个悬空的半波。volatile uint32_t pwm_counter 0; volatile uint32_t pwm_target 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); pwm_counter; if (pwm_counter pwm_target) { TIM_Cmd(TIM3, DISABLE); GPIO_ResetBits(GPIOA, GPIO_Pin_6); /* 拉低确保没有毛刺 */ pwm_counter 0; } } } void Start_PWM_Burst(uint32_t n) { pwm_target n; pwm_counter 0; TIM_Cmd(TIM3, ENABLE); /* 启动PWM开始计数 */ }这段代码有三个地方要解释清楚。第一更新中断需要在启动 PWM 之前通过TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE)打开同时NVIC中使能TIM3_IRQn否则中断不执行。第二中断里关闭定时器后CNT 会停在当前值下一行GPIO_ResetBits把输出拉到低电平避免定时器虽然停了但引脚仍然维持高电平。第三pwm_target如果设置为 0那么第一次更新事件就会关闭定时器但此时已经发出了一个完整周期所以调用Start_PWM_Burst时要注意业务上对 0 脉冲的定义。对于步进电机控制这个方案足够用因为电机加减速时间远大于中断响应时间几十微秒的关断延迟可以接受。3.2 方案二重复计数寄存器RCR硬件计数如果不想让 CPU 频繁进入中断或者 PWM 频率很高软件计数会大量占用中断时间。这时候高级定时器 TIM1/TIM8 的 RCR 就有用了。RCR 的作用是“连续溢出 N1 次才产生一次更新事件”。假设要输出 200 个完整脉冲在启动前把TIM1-RCR 199那么第 200 次计数溢出时才会触发更新中断中断里只需关一次定时器CPU 开销几乎可以忽略。void TIM1_Start_Burst(uint32_t n) { TIM_CtrlPWMOutputs(TIM1, DISABLE); /* 先关主输出安全配置 */ TIM1-RCR n - 1; /* RCR 目标脉冲数 - 1 */ TIM_ClearFlag(TIM1, TIM_FLAG_Update); TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); } void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); TIM_Cmd(TIM1, DISABLE); TIM_CtrlPWMOutputs(TIM1, DISABLE); } }RCR 是一个 16 位寄存器单次最大只能数 65536 个脉冲。如果需要更多可以在更新中断里用一个静态变量继续累加例如每触发一次中断表示 65536 个脉冲到头再决定是否关闭。另外RCR 的预装载行为和 ARR 一样修改后不是立即生效而是等到当前更新事件结束才装载。所以在启动前设置 RCR 是最稳妥的。这个方案的问题在于 TIM1 和 TIM8 通常还要兼顾互补 PWM 和死区控制如果项目里已经用掉了主输出就需要把脉冲计数逻辑放到软件层或者改用主从级联。3.3 方案三主从定时器级联用另一个计数器做闸门当 N 很大或者 CPU 不希望在脉冲中间处理任何中断时可以用两个定时器级联。主定时器负责产生 PWM从定时器工作在外部时钟模式 1把主定时器的更新事件当作计数时钟。从定时器溢出时它自己产生中断然后关闭主定时器。这样 CPU 只在最终完成时进一次中断期间完全不用管。void MasterSlave_Burst_Config(TIM_TypeDef* master, TIM_TypeDef* slave, uint16_t n) { /* 主定时器更新事件作为 TRGO 输出 */ TIM_SelectOutputTrigger(master, TIM_TRGOSource_Update); /* 从定时器选择对应的 ITR 输入具体值要看芯片手册的触发矩阵 */ /* 在 STM32F10x 上TIM_TS_ITR0 ~ TIM_TS_ITR3 对应不同的定时器组合 */ TIM_SelectInputTrigger(slave, TIM_TS_ITR2); TIM_SelectSlaveMode(slave, TIM_SlaveMode_External1); /* 从定时器计数到 n 个主更新事件后溢出 */ slave-PSC 0; slave-ARR n - 1; TIM_Cmd(slave, ENABLE); }这段代码的关键是TIM_TS_ITR2不是固定配置需要查芯片的数据手册确定哪一个从定时器输入对应哪个主定时器。例如在 STM32F103 中TIM2 的 ITR0 对应 TIM1ITR1 对应 TIM8ITR2 对应 TIM3ITR3 对应 TIM4。不同定时器组合的映射不一样写代码前先把手册里的“Timer Input Selection”表格找出来。从定时器溢出后在它的更新中断里关闭主定时器即可。这个方案的优点是脉冲期间完全没有中断缺点是占用了两个定时器并且要理清触发矩阵适合脉冲数量大、频率高的场景。4. 中心对齐模式、死区与动态调速工程里真正的坑4.1 中心对齐对脉冲数量统计的影响中心对齐模式下计数器的运动方式是先向上计数到 ARR再向下计数到 0因此一个 PWM 周期里可能存在两个“更新事件点”具体取决于TIM_CounterMode_CenterAligned1/2/3三个模式。模式 1 只在向下计数到 0 时产生更新事件模式 2 只在向上计数到 ARR 时产生模式 3 则两个时刻都产生。对软件计数来说模式 3 会导致每个 PWM 周期触发 2 次更新中断如果直接沿用 3.1 节的代码统计出来的脉冲数会翻倍。更隐蔽的是RCR 在中心对齐模式下的行为也会跟着变。RCR 数的是“更新事件次数”不是“PWM 周期数”。所以在中心对齐模式 3 下要输出 N 个完整 PWM 周期RCR 需要设置为2N - 1而且关闭动作发生在哪个更新事件点还取决于计数方向。这样的代码调试起来非常绕。我的一般做法是如果项目不是必须用中心对齐来减少谐波或整定死区脉冲数量控制就坚持用边沿对齐模式。如果一定要中心对齐就在更新中断里判断计数方向标志只在方向为“从顶向下”时计数这样每个周期只计一次。4.2 高级定时器死区时间计算与配置电机控制或 H 桥驱动里经常用到互补 PWM 和死区。死区时间的配置由 TIM1 的BDTR寄存器提供标准库把它封装成了TIM_BDTRInitTypeDef。死区时间不是简单地写一个微秒值而是与定时器时钟周期Tdtg相关。以 72MHz 定时器时钟为例Tdtg约等于 13.89ns。当TIM_DeadTime设为 50 时死区约为 694ns设为 100 时因为 DTG 编码进入第二段计算公式会变成(64 低位) x 2 x Tdtg所以要按参考手册的公式分段计算。TIM_BDTRInitTypeDef bdtr; bdtr.TIM_OSSIState TIM_OSSIState_Enable; /* 空闲时输出生效 */ bdtr.TIM_OSSRState TIM_OSSRState_Enable; /* 运行状态互补输出 */ bdtr.TIM_LOCKLevel TIM_LOCKLevel_OFF; bdtr.TIM_DeadTime 50; /* 约694ns 72MHz */ bdtr.TIM_Break TIM_Break_Disable; bdtr.TIM_BreakPolarity TIM_BreakPolarity_Low; bdtr.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, bdtr); TIM_CtrlPWMOutputs(TIM1, ENABLE);这里注意一个细节高级定时器的TIM_OSSIState和TIM_OSSRState决定关闭高级定时器主输出后引脚是强制低电平还是保持最后的有效状态。对于脉冲数量控制我通常把两个状态都打开对应“电机停转时输出低”比配置成高阻更安全。如果需要更精确的死区时间建议先把时钟源踩准如果定时器时钟不是 72MHz所有死区时间都会成比例偏移。用示波器量一次实际死区再反推 DTG 值比死磕手册公式快得多。4.3 影子寄存器与预装载运行中改ARR和CCR的安全姿势在 3.1 节的代码里如果下一次又调用Start_PWM_Burst改变脉冲目标同时还想改频率和占空比就要注意 ARR 和 CCR 的影子寄存器机制。启用预装载后写入 ARR 和 CCR 的新值不会立刻生效而是等到下一个更新事件到来时一次性装载。这样做的目的是防止计数器运行到一半时重载值变化导致当前周期出现异常宽的脉冲。/* 在PWM运行过程中调整占空比和周期 */ TIM_SetCompare1(TIM3, new_ccr); /* 只写影子寄存器 */ TIM_SetAutoreload(TIM3, new_arr); /* 更新事件后才生效 */ /* 如果下一批脉冲要改变频率建议先停止再修改 */ TIM_Cmd(TIM3, DISABLE); TIM_SetCompare1(TIM3, 500); TIM_SetAutoreload(TIM3, 999); TIM_Cmd(TIM3, ENABLE);后一种“先停止再修改”的方式更适合定长脉冲场景。因为如果在更新中断里改 ARR 和 CCR中断本身已经发生在周期边界虽然理论上安全但如果new_arr比当前CNT还小计数器会立刻溢出可能产生一次意外更新事件。所以批量输出时我习惯在关闭定时器之后调整这些参数等下一次启动时再一起生效。5. 验证与调试用逻辑分析仪数脉冲而不是用眼睛看5.1 用调试器和测试IO确认脉冲边界定长 PWM 的验证不能只看 LED 亮度或电机转不转必须确认“最后一个脉冲的下降沿”与“定时器关闭动作”发生在同一个周期边界。我常用的办法是找一个空余 GPIO在关闭 PWM 的同时拉高这个测试脚然后用双通道示波器同时看 PWM 输出和测试脚。正常的波形是第 N 个 PWM 周期结束后测试脚电平跳变并且 PWM 停在确定的低电平上。如果 PWM 还在输出半个周期说明关闭动作发生在更新事件以外也就是软件计数提前或延迟了半个周期。还可以利用调试器的寄存器窗口。程序停在中断里时读取TIM1-CNT。如果关闭动作发生在更新事件之后CNT应该是 0 或接近 0。如果停在其他值说明中断优先级导致响应延迟或者 PWM 是中心对齐模式关闭点不在 0 处。这个验证方法不需要额外硬件看到 CNT 的值就能判断边界是否准确。5.2 空闲定时器做硬件计数器验证对于 RCR 方案更彻底的验证是把 PWM 输出引脚飞线到另一个空闲定时器的输入捕获通道用输入捕获中断统计上升沿数量。这样可以完全独立于发送端做一次交叉验证。在调试阶段我经常写一段临时代码void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); captured_count; } }把captured_count的最终值与发送目标放在同一个断点里比较。如果两者相等说明发送端的停止点确实落在第 N 个完整脉冲之后。如果差 1通常是第一个占空比周期从定时器使能开始就算而捕获端是从第一个上升沿开始算起点不同导致偏移。这种验证比单纯看波形更客观尤其当 PWM 频率在几百 kHz 以上时肉眼已经无法区分最后一个脉冲是否完整硬件计数器的结果才是最终标准。本文还有配套的精品资源点击获取
返回列表