资讯动态

用TIM1重复计数寄存器(RCR)精确输出指定个数PWM脉冲

发布时间:2026/10/19 13:34:58 来源:尧图企业网站定制
简介面向STM32嵌入式开发者的TIM1高级定时器PWM中断输出示例工程重点演示通过更新中断与计数器配合实现指定个数PWM脉冲的精确控制适合需要掌握高级定时器单脉冲/多脉冲输出、中断标志处理及占空比配置的中级开发者。压缩包为完整Keil工程共374个文件包含C源程序如stm32f10x_tim.c、os_core.c、lib_mem.c、汇编启动文件、HAL库头文件、链接脚本及uvprojx工程配置编译配置、调试记录和日志文件也一应俱全整体约8.34MB结构清晰便于直接打开工程对照学习。资源已有657人浏览下载代码中涵盖TIM1通道初始化、PWM周期与占空比设置、中断服务例程编写及脉冲计数终止逻辑并附带uC/OS-II相关文件可供参考对于想理解高级定时器中断方式输出受限场景的读者可结合工程内库函数调用和寄存器操作快速上手减少环境搭建和排错时间。1. 从「数脉冲」到「配个数」一个寄存器解决指定数量 PWM 输出台子上那台步进驱动器控制板一上电就用 PWM 给方向脉冲跑起来之后我发现一个问题——明明只想发 30 个脉冲示波器上却数出 31 个停下来之后还带一个毛刺。后来查到原因问题出在一开始就把定时器的更新中断当成计数器用每溢出一次就在中断里做一次自减时序稍有抖动个数就偏了。这不是玄学是 PWM 输出模型的边界没理清。这个项目要解决的就是用 TIM1 高级定时器的重复计数寄存器RCR配合更新中断的方式让 MCU 输出指定个数 PWM。它面向的是步进电机驱动、传感器激励、红外载波这类需要「发够 N 个就停」的场景适合已经在用定时器做 PWM、但被脉冲个数精度和中断开销卡住的开发者。2. 方案先导高级定时器与 RCR 重复计数的配合逻辑2.1 为什么是 TIM1 而不是通用定时器手头这块 MCU 里通用定时器比如 TIM2、TIM3和高级定时器TIM1最大的区别不是通道多不多而是有没有重复计数寄存器。通用定时器的更新事件在每个计数周期都会产生想输出 N 个 PWM 就得靠软件在中断里自减计数中断频率等于 PWM 频率1kHz 的 PWM 就是每秒 1000 次中断处理器相当一部分时间花在进出中断上。TIM1 这类高级定时器多了一个 RCR 寄存器它的作用是把「更新事件」和「计数溢出」解耦。RCR 不为 0 时计数器要溢出 RCR1 次才会产生一次更新事件。这意味着可以把软件里的 N 次中断压缩成 1 次CPU 只在最后收尾时被唤醒一次。这个差异在脉冲个数大、PWM 频率高的场景特别明显——我做过一个 20kHz 的超声激励信号用通用定时器的方式做中断每秒 2 万次主循环几乎被饿死换成 RCR 之后中断每秒只有几十次负载直接降了两个数量级。注意高级定时器的 RCR 在不同系列上有效位宽不一样老一代 M3/M4 内核的型号上只用了低 8 位也就是一次最多配 255。后面写代码时会单独处理这个限制。2.2 三种实现路线对比「输出指定个数 PWM」在工程里大体有三条路网上能找到的例程基本都逃不出这三类。方案实现思路CPU 占用个数精度适用场景A. 软件计数每周期溢出进中断软件自减到 0 停止高中断次数 PWM 个数受中断延迟影响临时验证、个数很少B. RCR 重复计数更新事件被稀释为 N 次溢出一次中断收尾低中断次数 1精确到完整周期常规项目首选C. 主从定时器级联用另一个定时器的更新事件驱动/门控 TIM1极低几乎零中断纯硬件行为最稳定个数大、要求无软件干预这个项目用的是方案 B。方案 A 的问题是中断延迟不可控我在 2.1 里说的「30 个脉冲变成 31 个」就是这么来的最后一次自减到 0 之后定时器还在跑下次比较匹配已经把高电平拉起来了停都来不及。方案 C 很干净但对硬件连接有要求做复杂序列的时候才值得上后面第 5 章会展开。2.3 RCR 的行为细节RCR 是影子寄存器这个细节直接影响调试方向。在标准外设库的初始化结构体里叫 TIM_RepetitionCounter运行中直接写 TIM1-RCR 是改影子值真正生效要等下一次更新事件把预装载值搬进工作寄存器。也就是说改 RCR 的时机和清更新标志的时机是有讲究的。具体行为可以记成这个对照关系RCR 配置值计数溢出次数产生更新事件的时机实际输出完整 PWM 周期数01第 1 次溢出默认每周期中断1 个12第 2 次溢出2 个N-1N第 N 次溢出N 个所以要输出 N 个 PWMRCR 写的是 N-1。这个「减一」很容易记反我在项目里第一次就写成了 N结果每个批次多了 1 个脉冲逻辑分析仪上一看包络长度永远多一个周期。这个坑后面避坑章节还会再提。提示RCR 的预装载特性对运行时改参数有影响。如果定时器正在跑直接写 TIM1-RCR要等当前这一轮计数完成才生效不是立即生效。3. 核心实现拆解初始化、启停接口与中断回调的完整代码3.1 时基与 GPIO 初始化先把工程骨架搭起来下面是标准外设库风格的初始化代码工程里我习惯按「时钟 → 引脚 → 时基 → 输出比较 → 中断」的顺序写每一步都对应芯片参考手册里的一个寄存器组排查时也按这个顺序倒着查。static void tim1_gpio_init(void) { GPIO_InitTypeDef gpio; // TIM1_CH1 默认在 PA8如果目标板引到了其他引脚查重映射表 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_8; gpio.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽PWM 输出必须用 AF 模式 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); }PA8 的模式必须配成 GPIO_Mode_AF_PP我在早期把这里配成 GPIO_Mode_Out_PP结果引脚电平始终是死的因为定时器的输出通路根本没有连到引脚上。这个错误很隐蔽GPIO 初始化本身不会报错示波器上也看不出异常就是没有波形。接着是时基初始化。注意 TIM_TimeBaseInitTypeDef 里有个 TIM_RepetitionCounter 字段它就是 RCR 的初始化入口。这里先填 0表示默认每周期溢出都产生更新事件具体个数在启动函数里动态改。static void tim1_base_init(void) { TIM_TimeBaseInitTypeDef tb; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); tb.TIM_Prescaler 71; // 72MHz / 72 1MHz 计数频率 tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_Period 999; // 1MHz / 1000 1kHz PWM tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_RepetitionCounter 0; // RCR 初值每个计数周期产生一次更新事件 TIM_TimeBaseInit(TIM1, tb); }PSC 和 ARR 的计算关系是PWM 频率 定时器时钟 / (PSC1) / (ARR1)。上面配置里 72MHz 经过 PSC71 分频后计数频率是 1MHz再除以 ARR11000得到 1kHz。占空比则由输出比较值 Pulse 决定Pulse 写 500 就是 50%写 250 就是 25%。3.2 输出比较通道与更新中断配置PWM 的输出模式、极性和占空比在输出比较结构体里配置。这里用 PWM1 模式向上计数时计数小于 Pulse 输出有效电平大于 Pulse 输出无效电平这样改变 Pulse 就能线性调占空比直觉上最好用。static void tim1_ch1_pwm_init(void) { TIM_OCInitTypeDef oc; oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; // 主通道输出使能 oc.TIM_Pulse 500; // 占空比 50% oc.TIM_OCPolarity TIM_OCPolarity_High; // 高电平为有效电平 TIM_OC1Init(TIM1, oc); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // 更新中断优先级分组设为抢占 1、子优先级 0 NVIC_InitTypeDef nvic; nvic.NVIC_IRQChannel TIM1_UP_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); }TIM_OCPolarity_High 这个字段决定了输出无效电平是低还是高。很多人只关注占空比忽略了这个极性后面会出现「停止之后引脚停在低电平还是高电平」这种问题。做步进电机脉冲输出时通常希望停止后引脚回到低电平所以极性选 High这样 PWM 周期内高电平是有效区间停止后自然回到低电平。3.3 启动函数动态配置 RCR 与主输出使能核心的启动函数是项目里最该抄的一段。它的职责是清零计数器 → 写入 RCR → 清残留中断标志 → 启动定时器 → 打开主输出。注意顺序尤其最后两步。void pwm_send_burst(uint16_t count) { if (count 0) return; TIM_SetCounter(TIM1, 0); // 计数器清零保证从完整周期起点开始 TIM1-RCR count - 1; // 关键N 个脉冲写 N-1 TIM_ClearFlag(TIM1, TIM_FLAG_Update); // 清掉上一次可能残留的更新标志 TIM_Cmd(TIM1, ENABLE); // 先启动计数 TIM_CtrlPWMOutputs(TIM1, ENABLE); // 再打开高级定时器主输出 }这里有两个点需要解释。第一TIM1-RCR 直接写寄存器而不是重新调用 TIM_TimeBaseInit因为后者会把 PSC、ARR 全部重写一遍副作用太大。运行中改 RCR 就该直接操作寄存器这也是标准外设库时代最常见的写法。第二为什么要先 TIM_Cmd 再 TIM_CtrlPWMOutputs因为高级定时器的主输出使能位MOE默认是关闭的如果先开 MOE 再启动定时器部分芯片在极端时序下会在引脚上蹦出一个异常电平。先启动计数、再放行输出能保证引脚上看到的第一个沿是正常 PWM 周期的一部分。count 参数的类型是 uint16_t但前面提过老型号 RCR 有效位只有 8 位所以传超过 256 的值会被截断。项目里我加了一层保护count 超过 255 时拆成多段发送或者改用主从定时器方案。这一点看目标芯片新系列普遍已经把 RCR 扩成 16 位可以放宽。3.4 中断服务函数与主程序调用中断里做的事很少确认更新标志 → 清标志 → 关闭主输出 → 停止计数 → 置完成标志。这里只做收尾不做任何耗时操作。volatile uint8_t burst_done 0; volatile uint32_t burst_count 0; // 记录已完成的批次用于自测 void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) RESET) return; TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 能走到这里说明 RCR 已经倒数到 0N 个 PWM 全部输出完毕 TIM_CtrlPWMOutputs(TIM1, DISABLE); // 先封锁输出停在无效电平 TIM_Cmd(TIM1, DISABLE); // 再停计数省电 burst_done 1; burst_count; }逻辑上要注意一点中断触发的那一瞬间第 N 个完整周期已经结束计数器其实已经溢出并开始进入第 N1 个周期。但因为先关了 MOE引脚不会再产生新的有效电平所以波形在包络层看是严格 N 个周期。如果反过来先停定时器再关 MOE中间几微秒的间隙里引脚状态不受控负载端就可能多收一个脉冲。主程序里的调用方式很简单启动后轮询 burst_done完成后延时再做下一批int main(void) { tim1_gpio_init(); tim1_base_init(); tim1_ch1_pwm_init(); while (1) { pwm_send_burst(50); // 输出 50 个 1kHz PWM while (!burst_done) ; // 等待批次完成 burst_done 0; delay_ms(200); // 间隔 200ms 再发下一批 } }delay_ms 只是示意实际工程里可以用状态机或者 RTOS 的信号量替代轮询。中断里把 burst_done 置 1主循环清 0这个模式简单但可靠只要别在中断里清标志就行——清晚了会丢掉下一次完成事件。4. 实战避坑记录高级定时器 PWM 输出最容易翻车的五个细节4.1 现象高级定时器完全没有波形引脚电平一动不动这是 TIM1 和通用定时器最大的区别。通用定时器初始化完就能输出高级定时器多了一道「主输出使能」的闸门库函数对应 TIM_CtrlPWMOutputs。忘了调用或者调用了 DISABLE输出级电路直接断开示波器上只有直流电平。原因高级定时器的 MOE 位默认是 0OC 信号被封锁。解决初始化完成后调用一次 TIM_CtrlPWMOutputs(TIM1, ENABLE)后续每次启动也都要带上这一句。我见过有人只在初始化里调一次然后通过 TIM_Cmd 启停定时器结果第二次启动后依然没波形——因为停止时把 MOE 关了。统一的写法是启动函数里先 TIM_Cmd 再 TIM_CtrlPWMOutputs停止函数里先 TIM_CtrlPWMOutputs 再 TIM_Cmd两个函数永远配对出现。4.2 现象脉冲个数总是比设定值多 1 或少 1早期版本我在启动函数里写 TIM1-RCR count发 50 个脉冲示波器数出来 51 个。原因很简单RCR 的工作方式是「溢出 RCR1 次产生一次更新事件」所以 N 个脉冲对应 RCRN-1写 N 自然就多了一个。反过来少 1 的情况也有一般出现在极性配置不对时PWM 极性配成 High第一个周期开始前引脚已经是高电平负载把这一小段高电平也算成了一个脉冲。解决用逻辑分析仪看整个波形的包络别靠示波器肉眼数高频脉冲再检查 TIM_OCPolarity 和刹车/空闲电平配置。严格来说中断关闭 MOE 的动作有几微秒延迟停止点与理想边界之间存在不超过一个周期的相位差这在绝大多数负载下可忽略但做计量类应用时要用方案 C 的硬件联动来彻底消除。4.3 现象第一批波形正常第二批开始第一个脉冲变窄或者畸变复现路径第一批发完后定时器停在随机计数值上下一次启动时没有清零计数器直接启动于是第一个 PWM 周期从半截开始看起来就是一个窄脉冲。原因停止时计数器的 CNT 停在哪里是不确定的。解决pwm_send_burst 函数里第一行就做 TIM_SetCounter(TIM1, 0)保证每次都是完整周期起点。同理如果需要批次间隔完全相同停止之后手动把 ARR 或比较值复位避免上一次遗留的配置污染下一次波形。4.4 现象运行中改了 RCR但输出个数没有任何变化RCR 是影子寄存器预装载值要等一次更新事件才进入工作寄存器。如果定时器正跑着写完 TIM1-RCR 后计数器还没溢出新值就一直躺在预装载里自然不生效。解决改 RCR 必须在定时器停止时进行。标准顺序是TIM_Cmd(DISABLE) → 写 TIM1-RCR → TIM_ClearFlag 清更新标志 → TIM_SetCounter 清零 → TIM_Cmd(ENABLE) → TIM_CtrlPWMOutputs(ENABLE)。我习惯把这个顺序封装进启动函数不让外部随意改 RCR避免使用者把「运行时动态改个数」想得太简单。4.5 现象中断触发频率远超预期主程序卡顿明显如果你把 RCR 写成 0 或者没配 RCRTIM1 的更新中断就是每个计数周期触发一次。1kHz 的 PWM 每秒 1000 次中断每个中断里还要清标志、置标志虽然单次耗时短但频繁打断会让主循环的实时性变得很难看。原因有两类一是 RCR 没有按需配置中断没有被「稀释」二是中断服务函数里忘了清标志导致中断反复进入。解决确认 TIM1-RCR count - 1 之后再在完成批次后调用 TIM_ITConfig(TIM1, TIM_IT_Update, DISABLE) 关掉更新中断下次启动时重新使能。这样整个批次运行期间中断只来一次收尾时关中断彻底消除风暴。注意上面几条坑有很强的关联性第 4.1 和第 4.4 往往同时出现。排查时先确认 MOE再确认 RCR最后看中断标志按这个顺序能少走很多弯路。5. 二次开发思路从固定个数到动态序列的三种改造路径这个项目的骨架搭好之后扩方向基本就三个。第一个是彻底摆脱软件收尾用另一个定时器做硬件联动把 TIM2 配置为产生指定个数的更新事件通过内部触发线接到 TIM1 的从模式让 TIM1 工作在外部时钟模式 1TIM2 每产生一个事件 TIM1 就计一个数。这样脉冲个数完全由硬件决定中断只做完成通知个数上限也从 255 扩展到 TIM2 的 ARR 位宽。配置核心就两行TIM_SelectInputTrigger(TIM1, TIM_TS_ITR1); // ITR 编号以参考手册为准 TIM_SelectSlaveMode(TIM1, TIM_SlaveMode_External1);第二个改造方向是用 DMA 批量更新 RCR做出「一串脉冲流」而不是单一脉冲批。比如步进电机的 S 形加减速曲线需要连续修改脉冲周期或者脉冲个数DMA 可以先把一组 RCR 值放在数组里TIM1 每次更新事件触发一次 DMA 搬运硬件自动切换下一批的个数。这个技巧适合做激励信号序列或者电机测试省掉主循环逐段切换的时延。第三个方向是验证手段。我每次改完都会在中断里加一个 GPIO 翻转用示波器同时看 PWM 输出和翻转引脚确认中断只在批次结束时来一次再用逻辑分析仪统计每一批的上升沿数量和设定值比对。这套自测流程让我后面做多通道同步输出时能快速定位是通道配置问题还是 RCR 共享导致的问题——毕竟 RCR 在 TIM1 里是全局的两个通道只能共用同一个个数。从那以后我每次做定时器输出类需求都会强制走一遍「先定方案再算寄存器最后用逻辑分析仪数包络」的流程特别是 RCR 这个「减一」的细节已经刻进肌肉记忆了。希望这篇拆解能帮你少踩几个我已经踩过的坑。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑