资讯动态

STM32 TIM1重复计数寄存器RCR实现指定个数PWM脉冲输出

发布时间:2026/10/19 16:28:21 来源:尧图企业网站定制
简介面向STM32学习者的TIM1高级定时器指定个数PWM中断输出资料包解决PWM脉冲数量精确控制与中断程序设计中的常见问题。包内基于标准固件库实现核心代码覆盖定时器初始化、PWM通道配置、更新中断与捕获比较中断处理、计数控制等关键环节适合需要掌握高级定时器实用技法的嵌入式开发者对照学习。资源共374个文件以C源文件、H头文件、目标文件及依赖文件为主另含汇编启动文件、Keil工程文件与调试信息压缩包约8.34MB目录结构完整便于快速定位定时器相关程序也可作为工程移植和排错时的参考。已有657人学习浏览整体内容具有较强的实践参考价值。1. 为什么说TIM1才是真正能“数着个数”输出的定时器STM32的TIM1高级定时器要输出指定个数的PWM脉冲很多人第一反应是开更新中断然后在中断里自己数脉冲数到N就关。这个方案在10kHz以下勉强能跑一旦PWM频率到50kHz、100kHz每周期一次中断会让CPU大半时间耗在处理进中断和清标志上稍有延迟还会多出半个脉冲。TIM1硬件里其实藏着一个重复计数寄存器RCR可以把N个PWM周期打包成一次更新事件脉冲个数交给硬件去数CPU只需要在中断里完成“停止”这一个动作。下面的内容围绕这个功能展开从RCR的选型原理、初始化、中断方式输出指定个数PWM到N255时的拆批处理最后列几个我调过的坑和一条验证技巧。适合正在做步进驱动、激光触发和伺服位置控制的工程师也适合把中断负载看成隐患的固件开发者。## 2. TIM1重复计数器RCR与普通PWM的差别一次更新中断能打包N个脉冲2.1 普通定时器为什么数不准指定个数普通定时器输出PWM本身没问题难点在于“什么时候停”。常见做法是更新中断里软件计数每次溢出countcount等于N时关闭输出。这个方案的麻烦在于中断次数与PWM频率直接挂钩而且中断响应本身有延迟硬件计数溢出发生在计数器从ARR回到0的瞬间但CPU进入中断服务还要压栈、取地址等真正执行到“关闭输出”这条指令时计数器可能已经又跑了十几个周期。负载一高中断再被更高优先级任务抢一下多一个或少一个脉冲就变成玄学问题。我调某个步进驱动方案时就有过一次印象很深的翻车PWM跑到100kHz软件数300个脉冲单独跑没问题电机启动瞬间电流一大数量变成299或301查了整整一个下午才发现是中断响应延迟。TIM1高级定时器能把这个“数脉冲”的活从软件搬到硬件里靠的正是普通定时器没有的重复计数寄存器。RCR是8位的写入0时行为和普通定时器一样每次上溢都产生更新事件写入N-1时要连续溢出N次才产生一次更新事件。更新事件同时置UIF标志、重新装载RCR影子寄存器、并把CNT清零。在PWM模式下一次溢出就是一个PWM周期所以一个更新事件天然对应N个PWM周期CPU可以在N个周期内完全不管PWM只在第N个周期结束时被唤醒一次。2.2 时钟树、频率公式与那三个绕不开的寄存器先算清楚PWM频率这一步错了后面全白搭。TIM1挂在APB2总线上F103这类芯片里APB2时钟等于系统时钟所以定时器时钟是72MHz。如果你把APB2分了频定时器时钟会变成APB2的两倍这个容易搞混。PWM频率公式为Fpwm TIMxCLK / ((PSC 1) * (ARR 1))想输出8kHz PWM72MHz下PSC0、ARR8999就行。占空比则由CCR1与ARR的比值决定比如ARR8999、CCR14500就是50%占空比。写ARR和CCR这些值前要确认CR1的ARPE位和CCMR1的OC1PE位已置1否则运行时修改会直接进影子寄存器波形容易出毛刺。高级定时器和普通定时器的输出结构还有一个关键区别TIM1的通道输出受两级开关控制第一级是CCER里的CC1E第二级是BDTR寄存器里的MOE主输出使能位。只置CC1E不动MOE引脚上是看不到波形的。MOE还能被刹车信号自动清零出现刹车事件后即使计数器还在跑输出也被封锁。这个位是好多新手查半天没波形的最常见原因。如果只做单端PWM而不需要互补输出死区寄存器可以不配置主输出使能必须置位。2.3 TIM1 PWM初始化代码与必须有的强制更新下面给出一个寄存器版本的TIM1 PWM初始化函数芯片以F103为例输出通道用CH1对应PA8void TIM1_PWM_Config(uint16_t psc, uint16_t arr, uint16_t ccr) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_TIM1EN; GPIOA-CRH ~0x0F; GPIOA-CRH | 0x0B; // PA8 复用推挽输出50MHz TIM1-PSC psc; TIM1-ARR arr; TIM1-CCR1 ccr; TIM1-RCR 0; // 重复计数先关闭 TIM1-CCMR1 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM模式1 TIM1-CCMR1 | TIM_CCMR1_OC1PE; // 比较预装载 TIM1-CR1 | TIM_CR1_ARPE; // 自动重载预装载 TIM1-CCER | TIM_CCER_CC1E; // 使能OC1通道 TIM1-BDTR | TIM_BDTR_MOE; // 主输出使能 TIM1-EGR | TIM_EGR_UG; // 立即产生一次更新事件 TIM1-SR ~TIM_SR_UIF; // 清掉UG产生的更新标志 }代码里最后两行是容易忽略的细节。RCR、ARR、PSC这些寄存器都有影子缓冲写入后并不会立刻生效要等一次更新事件才会装载。直接置UG位能强制产生一次更新事件让这些值立即生效但UG也会把UIF标志置1所以后面马上清掉。如果不做这一步后面配置RCR再启动时可能第一次更新中断来的时机就不是预想的N个脉冲之后。PA8的复用推挽配置是0x0B对应CNF10复用推挽、MODE1150MHz如果你的硬件引脚不在PA8按具体引脚改这一行即可。## 3. 中断方式输出指定个数PWM为什么先停计数器再关输出3.1 单批场景下RCR打包的启动函数先看最简单的场景要输出的脉冲数N不超过255。这时直接设置RCR N - 1一个更新中断结束后关停即可。启动函数如下volatile uint8_t pwm_state 0; #define PWM_STATE_DONE 0 #define PWM_STATE_RUN 1 void TIM1_Start_N_Pulses(uint16_t n) { if (n 0) { pwm_state PWM_STATE_DONE; return; } TIM1-CR1 ~TIM_CR1_CEN; // 确保定时器先停 if (n 255) n 255; // 单批上限后续章节处理 pwm_state PWM_STATE_RUN; TIM1-RCR n - 1; TIM1-CNT 0; TIM1-EGR | TIM_EGR_UG; // 强制更新使RCR立即生效 TIM1-SR ~TIM_SR_UIF; // 清掉UG产生的UIF TIM1-CCER | TIM_CCER_CC1E; TIM1-BDTR | TIM_BDTR_MOE; TIM1-CR1 | TIM_CR1_CEN; // 最后启动计数 }启动顺序是刻意的先停计数器再装载RCR用UG强制更新并清标志然后开输出通道和主输出最后开计数器。如果先开CEN再配置RCR计数器可能已经在跑了第一批脉冲数就不对。注意这里的上限限制只是临时保护实际要输出超过255个脉冲时不能用它。3.2 中断服务函数里的关停顺序更新中断服务函数写起来很简单但关停顺序值得展开讲void TIM1_UP_IRQHandler(void) { if (TIM1-SR TIM_SR_UIF) { TIM1-SR ~TIM_SR_UIF; if (pwm_state PWM_STATE_RUN) { TIM1-CR1 ~TIM_CR1_CEN; // 1. 先停计数 TIM1-CCER ~TIM_CCER_CC1E; // 2. 再关通道 TIM1-BDTR ~TIM_BDTR_MOE; // 3. 最后关主输出 pwm_state PWM_STATE_DONE; } } }为什么要按这个顺序关关键在于更新中断触发时计数器虽然在溢出瞬间清零了但如果你不关CEN它会继续往上跑。此时如果先清CCER比较电路可能正好处在输出有效电平的区间引脚电平会被硬拉成无效状态最后的波形会缺半个周期更麻烦的是计数器还在走跑到下一个比较点又会输出一个不完整脉冲。先清CEN让计数器停住输出电平就锁在某个状态再关通道从物理上切断这个通道最后关MOE是把整个输出级封锁防止残留电平。三步做完脉冲个数就锁定在N不会多也不会少。清UIF标志放在最前面避免中断还没退出又有新标志进来造成重入。3.3 状态机与CPU负载对比上面代码里的pwm_state建议用volatile修饰因为中断和主循环都会读写它不加volatile的话编译器可能把变量优化到寄存器里主循环读到的永远是旧值。状态机本身不复杂IDLE不做处理RUN运行批次DONE表示批次完成。应用层查询到DONE后可以做下一步动作比如启动另一个定时器或通知控制任务。用普通定时器软件数脉冲和TIM1RCR的差距可以看下面这个对比输出脉冲数普通定时器中断次数TIM1RCR中断次数50501255255160060035000500020中断次数按每批255个往上拆5000个脉冲拆成20批中断负载是原来的二百五十分之一。CPU节省下来的时间可以用来处理控制算法、通信协议栈而不是被PWM中断淹没。这个对比只针对“数脉冲”普通定时器如果配合主从定时器或DMA也能做类似效果但那套方案的调试成本和灵活性是另一个话题。## 4. 输出超过255个脉冲拆批处理的完整状态机与重装时序4.1 拆批思路与完整启动函数RCR只有8位单批最多255个脉冲。要输出500、600甚至几万个脉冲就得把任务拆成多个批次每批最多255个批次之间在更新中断里衔接。我的习惯是把“剩余脉冲数”保存在一个全局变量里启动时先装载第一批中断到来时判断是否还有剩余有就切下一批没有就停止。完整启动函数如下volatile uint16_t pwm_remaining; volatile uint8_t pwm_state; void TIM1_Start_N_Pulses(uint16_t n) { TIM1-CR1 ~TIM_CR1_CEN; if (n 0) { pwm_state PWM_STATE_DONE; return; } pwm_remaining n; pwm_state PWM_STATE_RUN; TIM1_Apply_Next_Batch(); // 装载第一批RCR TIM1-CNT 0; TIM1-EGR | TIM_EGR_UG; TIM1-SR ~TIM_SR_UIF; TIM1-CCER | TIM_CCER_CC1E; TIM1-BDTR | TIM_BDTR_MOE; TIM1-CR1 | TIM_CR1_CEN; } void TIM1_Apply_Next_Batch(void) { if (pwm_remaining 255) { TIM1-RCR 254; // 一次输出255个脉冲 pwm_remaining - 255; } else { TIM1-RCR pwm_remaining - 1; pwm_remaining 0; } }TIM1_Apply_Next_Batch每调用一次就从pwm_remaining里扣除一批并把RCR设成对应值。比如总数为600第一次进来remaining600设置RCR254remaining变成345输出255个后中断第二次再进来设置RCR254remaining变成90第三次设置RCR89remaining清0。启动前用UG强制更新保证第一批RCR生效。这里TIM1-CNT 0放在UG之前UG本身也会清CNT双保险避免意外残留。4.2 批次切换的中断服务与边界说明批次切换逻辑放在更新中断里和停止逻辑分开判断void TIM1_UP_IRQHandler(void) { if (!(TIM1-SR TIM_SR_UIF)) return; TIM1-SR ~TIM_SR_UIF; if (pwm_state ! PWM_STATE_RUN) return; if (pwm_remaining 0) { TIM1-CR1 ~TIM_CR1_CEN; // 暂停计数 TIM1_Apply_Next_Batch(); // 装载下一批RCR TIM1-EGR | TIM_EGR_UG; // 强制更新让新RCR立即生效 TIM1-SR ~TIM_SR_UIF; // 清掉UG产生的UIF TIM1-CR1 | TIM_CR1_CEN; // 继续计数 } else { TIM1-CR1 ~TIM_CR1_CEN; TIM1-CCER ~TIM_CCER_CC1E; TIM1-BDTR ~TIM_BDTR_MOE; pwm_state PWM_STATE_DONE; } }批次切换时先暂停计数调用TIM1_Apply_Next_Batch写入新RCR再用UG强制更新。这一步很关键如果只写RCR不触发UG新值要等下一次更新事件才会被硬件装载而下一次更新的间隔取决于旧的RCR这样会出现一个255脉冲的过渡批次数量完全对不上。UG会立即重装所有影子寄存器并清CNT所以在中断里UG之后紧接着清UIF然后重新开CEN下一批就从CNT0开始。批次切换有一个边界要讲清从暂停CEN到重新启动CEN之间输出引脚会保持当时的电平这段“保持”的宽度就是中断服务函数执行时间。PWM周期是1ms时几十微秒的保持几乎看不出来PWM周期到1us时这段保持可能相当于几十个脉冲周期波形上明显缺了一块。如果应用要求脉冲之间严格等间隔拆批方案不适用只能用RCR0逐脉冲软件计数或者考虑主从定时器、DMA等更复杂的方案。拆批方案适合脉冲周期相对长、或者批次间隙不影响控制精度的场景判断标准就是中断服务耗时与PWM周期之比。## 5. 避坑检查TIM1指定脉冲调试中五个高频翻车点5.1 最后一个脉冲只有半截现象示波器看最后一段波形本该完整的第N个脉冲只出现了一半或者尾部多了一个很窄的毛刺。原因中断服务里先关了CCER没停CEN。计数器在中断到来后仍在继续跑关闭通道的时刻恰好处于高电平区间电平被强制拉低就切出了半截脉冲。反过来如果关闭动作发生在比较事件之后计数器又跑完一段就会多输出一小段电平。解决固定关闭顺序先清CEN停计数器再清CCER封通道最后清MOE封主输出。三步在中断服务里连续执行中间不要插入任何函数调用或延时确保关闭动作在最短时间内完成。5.2 RCR超过255后脉冲数完全不对现象想输出500个脉冲直接写TIM1-RCR 499结果是255个或256个而且行为不稳定。原因RCR寄存器只有8位写入499实际只保留了低8位相当于写入了243。加上部分情况下RCR影子值还没更新批次个数变成了随机数。解决N超过255时不要硬塞RCR用拆批方案。写RCR前先判断N上限最好做一个断言或饱和保护至少让开发者知道参数越界。单批值取min(剩余数, 255)换算成RCR时记得减1。5.3 初始化后引脚一直在高阻量不到波形现象寄存器配置看起来都对PWM频率也算了占空比也写了但引脚上量不到信号。原因TIM1的高级输出通道有主输出使能位MOE它默认是0。MOE为0时即使CCER和CCMR都配置正确输出引脚也不输出PWM波形。这个位还可能在刹车事件后被硬件自动清零所以运行中途波形突然消失也要回来检查它。解决初始化时把TIM1-BDTR | TIM_BDTR_MOE加上。出现调试问题先读BDTR寄存器确认MOE是否还在。使用刹车输入功能时要设计好刹车事件恢复流程因为重新置MOE不能简单地在中断里写一下就完事还要考虑当前相位。5.4 修改周期或占空比时波形抖动一次现象运行中修改ARR或CCR下一次输出周期出现一次明显不正常的宽度之后又恢复正常。原因ARPE和OC1PE没有开启。这两个预装载位控制ARR和CCR的写入是否随更新事件同步生效。没开时写入值会立刻进入影子寄存器改变频率或占空比的时刻随机打乱了PWM节拍。解决初始化时置位TIM_CR1_ARPE和TIM_CCMR1_OC1PE。运行时修改这些值等一个更新中断或标志后再做下一步判断。批量脉冲期间尽量不要中途改ARR如果非改不可需要锁定在更新中断里同步切换。5.5 高PWM频率下拆批后波形序列出现缺口现象PWM频率很高时拆批输出几百个脉冲用示波器数发现批次之间有明显间隙间隙宽度超过一个周期甚至像少了好几个脉冲。原因批次切换在中断里暂停CEN、装载RCR、UG、再恢复CEN这几条指令本身耗时几十个周期。PWM周期短的时候中断服务耗时相当于几十个脉冲周期输出电平保持不动视觉上就是缺口。解决先测中断服务实际耗时再判断拆批是否可行。如果缺口不可接受改用RCR0逐脉冲中断方式虽然中断次数多但每个周期都能及时响应不会累积批次间隙。或者把PWM频率降到拆批可承受范围。这个坑没有通用解只能按控制精度要求取舍。## 6. 验证技巧用更新中断翻转一个空闲IO来数脉冲调试指定个数PWM最怕的是“软件说对了示波器数错了”或者反过来。靠眼睛在示波器上数几百个脉冲太累这里有个实用技巧在更新中断服务里翻转一个调试用的IO把“批次开始”和“批次结束”变成可以测量的电平变化。具体做法是选一个不冲突的GPIO比如PB0配置成推挽输出。在TIM1_UP_IRQHandler入口处加一行翻转void TIM1_UP_IRQHandler(void) { GPIOB-ODR ^ (1 0); // 每次更新中断翻转一次PB0 if (TIM1-SR TIM_SR_UIF) { TIM1-SR ~TIM_SR_UIF; // 原有PWM停止/批次切换逻辑 } }这样第一批启动时PB0变高中断处理结束变低用示波器CH1看PA8的PWM波形CH2看PB0就能直接把“软件是否在正确时机停止”这一判断可视化。PB0的高电平宽度近似等于批次总耗时除以PWM周期就得到脉冲数如果高电平出现的位置比预期早说明中断漏了或标志没清干净。调试中断耗时也靠同一根线进入中断写高退出中断写低测高电平宽度就是中断服务函数真实耗时。做拆批方案前先测这个宽度再除以PWM周期就能估算批次间隙对波形的影响程度。我后来每次调TIM1批次输出都会先做这套检查CNT清零、清SR、装RCR、UG、再开CEN这条启动序列一步不落然后看PB0翻转是否干净利落。检查完再动业务流程比对着示波器数脉冲高效得多。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑