STM32F103定时器寄存器深度解析:从计数原理到PWM与输入捕获实战

发布时间:2026/10/19 1:46:20
STM32F103定时器寄存器深度解析:从计数原理到PWM与输入捕获实战
1. 为什么第八章值得单独拎出来讲搞STM32F103的人前面几章大概率已经把GPIO、时钟树、中断向量表这些啃得差不多了。到了第八章很多人会突然卡住——因为这一章开始碰的是定时器。不是那种“配个延时”的简单活儿而是从基本定时器到通用定时器、高级定时器从计数模式到PWM输出、输入捕获、编码器接口一整套东西全砸过来。我见过太多人学到这一章就开始抄代码能跑就行问他为什么这么配答不上来。结果换个需求比如要输出一个占空比可调、频率可变的PWM或者要测一个外部脉冲的宽度立马抓瞎。所以第八章我打算用“白话”的方式把寄存器层面的逻辑掰开揉碎讲清楚。你不需要死记硬背每个位的定义但得明白为什么这个位要这么设以及不这么设会出什么问题。这一章的核心就一句话STM32F103的定时器本质上是一个带预分频器的计数器所有花里胡哨的功能都是围绕这个计数器衍生出来的。你把这句话吃透了后面什么PWM、输入捕获、输出比较全都是同一个底层逻辑的不同用法。适合谁看适合已经能点灯、能配中断但一看到TIMx_CR1、TIMx_CCMR1这些寄存器就头大的嵌入式初学者也适合工作几年但一直用库函数、想回头补寄存器底子的人。2. 定时器的整体架构与核心思路拆解2.1 三个定时器家族的分工逻辑STM32F103的定时器不是一种而是分了三档基本定时器TIM6/TIM7、通用定时器TIM2~TIM5、高级定时器TIM1/TIM8。很多人一开始不理解为什么要分这么细其实从寄存器角度看就是功能模块的堆叠。基本定时器最简单只有一个16位计数器、一个预分频器、一个自动重装载寄存器主要用来做时基比如给DAC触发或者做简单的定时中断。它没有外部引脚不能输出PWM也不能捕获外部信号。你可以把它理解成一个“纯内部计时器”。通用定时器在基本定时器的基础上加了四个独立通道每个通道可以配置成输出比较、PWM输出、输入捕获、输出比较单脉冲模式。还多了编码器接口、霍尔传感器接口。TIM2~TIM5都是这一类其中TIM2和TIM5是32位计数器TIM3和TIM4是16位。注意32位和16位在寄存器操作上有个坑32位的TIM2/TIM5在读写CNT寄存器时需要分两次读或者用特定顺序否则可能读到不一致的值。这个后面会细说。高级定时器TIM1和TIM8在通用定时器基础上又加了互补输出、死区插入、刹车输入、重复计数器。这些功能是给电机控制、数字电源这类场景准备的。比如你驱动一个三相无刷电机需要三对互补PWM还要防止上下桥臂直通死区时间就是干这个的。重复计数器RCR则允许你设置每N个计数周期才产生一次更新事件这在做高频PWM时能减轻CPU中断负担。从寄存器映射上看基本定时器只有CR1、CR2、DIER、SR、EGR、CNT、PSC、ARR这几个。通用定时器在此基础上多了CCMR1/2、CCER、CCR1~4、SMCR。高级定时器再多BDTR、RCR、CCR5/6用于互补通道的额外比较。所以你学的时候先把基本定时器的寄存器吃透后面每加一个功能就是多操作几个寄存器而已。2.2 预分频器与自动重装载的配合逻辑这是第八章最核心的一个计算也是很多人配错的地方。STM32F103的定时器时钟源通常来自APB1或APB2。注意一个细节当APB预分频系数为1时定时器时钟等于APB时钟当APB预分频系数大于1时定时器时钟等于APB时钟的2倍。这个规则在参考手册的时钟树章节有写但很多人忽略。假设系统时钟72MHzAPB1预分频系数为2那么APB1时钟是36MHz但TIM2~TIM7的时钟是72MHz。APB2预分频系数为1那么TIM1和TIM8的时钟就是72MHz。这个点如果不搞清楚你算出来的定时时间会差一倍。定时器的计数频率公式是计数频率 定时器时钟 / (PSC 1)其中PSC是预分频寄存器的值实际分频系数是PSC1。比如PSC71那么分频系数是7272MHz/721MHz也就是计数器每1微秒加一。然后定时周期公式是定时周期 (ARR 1) / 计数频率ARR是自动重装载寄存器的值。比如ARR999计数频率1MHz那么定时周期就是1000微秒也就是1毫秒。这里注意ARR1是因为计数器从0计到ARR一共ARR1个计数。所以完整的定时时间公式是定时时间 (ARR 1) * (PSC 1) / 定时器时钟频率这个公式你最好记在脑子里因为后面配PWM频率、输入捕获超时全都要用。我见过有人把PSC和ARR搞反结果定时时间差了十万八千里。还有个坑PSC和ARR寄存器都有影子寄存器你写入的值不会立即生效而是在下一个更新事件时才被加载到实际寄存器。如果你在运行中修改PSC或ARR需要手动产生一个更新事件置位EGR的UG位才能立即生效。这个在动态调频调占空比时特别重要。2.3 计数模式的选择与适用场景STM32F103的通用定时器支持三种计数模式向上计数、向下计数、中央对齐向上/向下交替。基本定时器只支持向上计数。向上计数是最常用的计数器从0加到ARR溢出后回到0产生更新事件。向下计数是从ARR减到0下溢后回到ARR产生更新事件。中央对齐模式则是先向上到ARR-1再向下到0如此反复。中央对齐模式主要用于电机控制中的PWM生成因为它的输出波形是对称的能减少谐波。在寄存器层面计数模式由CR1的CMS位和DIR位控制。CMS[1:0]选择中央对齐模式DIR选择向上或向下。注意在中央对齐模式下DIR位是只读的由硬件自动切换。如果你试图写DIR会被忽略。这里有个实操心得如果你只是做普通的定时中断用向上计数就够了别没事儿选中央对齐因为中央对齐的更新事件频率是向上计数的两倍中断里处理的东西会翻倍。我早期做的一个项目用中央对齐模式做1ms定时结果中断频率变成2kHzCPU负载直接上去了后来改成向上计数才降下来。3. 核心寄存器逐位拆解与实操要点3.1 CR1控制寄存器的关键位CR1是定时器最核心的控制寄存器位定义如下只挑最关键的几个CEN位0计数器使能。置1开始计数清0停止。这个位在初始化时最后置1避免配置过程中计数器乱跑。UDIS位1更新禁止。置1时禁止更新事件产生但影子寄存器仍然会被加载。这个位用得少一般保持0。URS位2更新请求源。置1时只有计数器溢出/下溢才产生更新中断而软件置位UG不产生中断。这个在需要手动更新影子寄存器但不想触发中断时很有用。OPM位3单脉冲模式。置1时计数器在下一个更新事件后自动停止。做单次延时很方便。DIR位4计数方向。0向上1向下。中央对齐模式下只读。CMS位6:5中央对齐模式选择。00边沿对齐01/10/11分别对应不同的中央对齐模式。ARPE位7自动重装载预装载使能。置1时ARR有影子寄存器更新事件时才加载置0时ARR写入立即生效。这个位很关键做PWM动态调频时如果ARPE0你改ARR会立即影响当前周期可能导致波形畸变如果ARPE1则在下个周期生效波形平滑。CKD位9:8时钟分频。用于给输入捕获做数字滤波一般设00。实操中我通常这样配CR1 0x0000然后根据需要置CEN、ARPE、CMS。注意不要一上来就置CEN先把PSC、ARR、CCMR、CCER都配好最后再置CEN。3.2 CCMR1/CCMR2通道模式寄存器这两个寄存器控制四个通道的模式每个通道占8位。以CCMR1为例低8位控制通道1高8位控制通道2。关键位CCxS位1:0通道方向。00输出01输入IC1映射到TI110输入IC1映射到TI211输入IC1映射到TRC。做PWM输出时设00。OCxM位6:4输出比较模式。110是PWM模式1111是PWM模式2。PWM模式1是计数器小于CCR时输出有效电平模式2相反。OCxPE位3输出比较预装载使能。置1时CCR有影子寄存器更新事件时加载。做PWM时建议置1避免改占空比时出现毛刺。OCxFE位2输出比较快速使能。一般不用。这里有个坑CCMR1的位定义在输出模式和输入模式下是不同的。比如位3在输出模式下是OCxPE在输入模式下是ICxPSC。所以配置前一定要先确定通道方向再按对应模式写位。我见过有人把输入捕获配成输出模式结果死活捕获不到查了半天才发现CCxS没设对。3.3 CCER捕获/比较使能寄存器CCER控制每个通道的使能和极性CCxE位0/4/8/12通道使能。输出模式下必须置1才能输出到引脚。CCxP位1/5/9/13极性。0高电平有效1低电平有效。做PWM时如果你希望占空比表示高电平时间就设0如果驱动的是低电平有效的LED就设1。CCxNE位2/6/10/14互补输出使能。只有高级定时器有。CCxNP位3/7/11/15互补输出极性。注意CCxE和CCxNE不能同时为0否则输出引脚会处于高阻态。高级定时器还要配BDTR的MOE位主输出使能否则即使CCxE置1引脚也不输出。这个MOE位是高级定时器独有的坑很多人配完TIM1的PWM发现没波形就是忘了置MOE。3.4 预分频器与自动重装载寄存器的写入顺序PSC和ARR都是16位寄存器但写入时要注意先写PSC再写ARR最后产生更新事件。因为PSC的影子寄存器在更新事件时加载如果你先写ARR再写PSC而ARR又比较小可能在PSC生效前就产生溢出导致第一个周期时间不对。标准流程是TIMx-PSC psc_value; TIMx-ARR arr_value; TIMx-EGR | TIM_EGR_UG; // 产生更新事件加载影子寄存器 TIMx-SR ~TIM_SR_UIF; // 清除更新标志避免立即进中断注意最后一步清标志因为置UG会同时置位UIF如果你开了更新中断会立刻进一次中断。这个在初始化时如果不处理会导致第一次中断提前到来。4. PWM输出与输入捕获的完整实操流程4.1 输出可调频率和占空比的PWM假设用TIM3的通道1PA6输出PWM要求频率1kHz占空比50%系统时钟72MHzAPB1预分频系数为2所以TIM3时钟72MHz。计算过程目标频率1kHz周期1ms。取计数频率1MHz则PSC 72MHz/1MHz - 1 71。周期1ms计数1000次ARR 1000 - 1 999。占空比50%CCR1 (9991) * 50% 500。配置步骤使能TIM3时钟RCC-APB1ENR | RCC_APB1ENR_TIM3EN。配置PA6为复用推挽输出GPIOA-CRL对应位设0xB复用推挽50MHz。配TIM3TIM3-PSC 71; TIM3-ARR 999; TIM3-CCR1 500; TIM3-CCMR1 (64) | (13); // PWM模式1预装载使能 TIM3-CCER (10); // 通道1使能高电平有效 TIM3-CR1 (17) | (10); // ARPE使能CEN使能 TIM3-EGR (10); // 产生更新事件 TIM3-SR ~(10); // 清更新标志实测下来PA6会输出1kHz、50%占空比的方波。如果你想动态改占空比直接写TIM3-CCR1即可因为OCxPE1新值会在下个更新事件生效波形不会畸变。如果想动态改频率改ARR和PSC但注意PSC改完也要产生更新事件才生效。注意改ARR时如果ARPE0会立即生效可能导致当前周期被截断。所以做动态调频时ARPE一定要置1。4.2 输入捕获测脉冲宽度用TIM4的通道1PB6捕获一个外部脉冲的高电平宽度。思路是上升沿捕获时记录CNT到CCR1同时把捕获极性改为下降沿下降沿捕获时再记录CNT两次差值就是高电平时间。配置步骤使能TIM4时钟配置PB6为浮空输入或上拉输入。配TIM4TIM4-PSC 71; // 计数频率1MHz每微秒加一 TIM4-ARR 0xFFFF; // 最大计数 TIM4-CCMR1 (10); // 通道1输入IC1映射到TI1 TIM4-CCER (10); // 通道1使能上升沿捕获 TIM4-DIER (11); // 使能通道1捕获中断 TIM4-CR1 (10); // 使能计数器中断服务函数里if (TIM4-SR (11)) { if (捕获的是上升沿) { rise TIM4-CCR1; TIM4-CCER | (11); // 改为下降沿捕获 } else { fall TIM4-CCR1; width fall - rise; // 单位微秒 TIM4-CCER ~(11); // 改回上升沿 } TIM4-SR ~(11); }这里有个坑如果脉冲宽度超过ARR计数器会溢出差值计算就错了。解决办法是开溢出中断在溢出中断里加一个溢出计数最终宽度 溢出次数 * (ARR1) fall - rise。或者把PSC调大让计数频率降低但精度会下降。我一般根据预估的最大脉宽来设PSC留20%余量。4.3 高级定时器的互补PWM与死区用TIM1的通道1和互补通道1PA8和PB13输出一对互补PWM频率10kHz死区1微秒。计算10kHz周期100微秒取计数频率1MHzPSC71ARR99。死区1微秒死区时间 DTG[7:0] * Tdtg其中Tdtg 1/72MHz ≈ 13.89ns。1us / 13.89ns ≈ 72。查表DTG0x48左右可以产生约1us死区具体查参考手册的死区时间表。配置TIM1-PSC 71; TIM1-ARR 99; TIM1-CCR1 50; // 50%占空比 TIM1-CCMR1 (64) | (13); TIM1-CCER (10) | (12); // CC1E和CC1NE都使能 TIM1-BDTR (0x480) | (115); // 死区时间MOE使能 TIM1-CR1 (17) | (10); TIM1-EGR (10); TIM1-SR ~(10);注意BDTR的MOE位位15必须置1否则输出无效。这个位在刹车事件后会被硬件清零所以如果用了刹车功能刹车解除后要重新置MOE。5. 常见问题与排查技巧实录5.1 定时器不计数或计数不对最常见的原因有三个时钟没使能、CEN没置1、PSC/ARR算错。排查时先读RCC的使能寄存器确认时钟开了再读CR1的CEN位最后用调试器看CNT是否在变。如果CNT在变但周期不对检查PSC和ARR的值注意APB预分频系数对定时器时钟的影响。还有一个隐蔽的坑如果你在调试模式下暂停了CPU定时器可能还在计数。STM32的DBGMCU模块可以配置调试时冻结定时器在DBGMCU_APB1FZ或DBGMCU_APB2FZ里置对应位即可。我早期调试PWM时一暂停就看到波形乱跳后来才知道要冻结。5.2 PWM输出没有波形或波形异常先查GPIO配置必须是复用推挽输出不能是普通推挽。再查CCER的CCxE位高级定时器还要查BDTR的MOE位。如果波形频率不对查PSC和ARR。如果占空比不对查CCR和OCxM模式。如果波形有毛刺查ARPE和OCxPE是否使能。有个特殊情况当CCR0时PWM模式1输出一直是低电平当CCRARR时输出一直是高电平。这是正常行为不是bug。如果你想要0%和100%占空比直接设CCR0或CCRARR1即可。5.3 输入捕获捕获不到或捕获值不对先确认GPIO是输入模式且外部信号确实有跳变。再查CCMR1的CCxS位是否设成了输入。然后查CCER的CCxE和CCxP极性设反了会捕获到相反的边沿。如果捕获值偶尔不对可能是信号抖动可以在CCMR1里设ICxPSC做数字滤波或者在硬件上加RC滤波。还有一个坑输入捕获和输出比较共用CCR寄存器如果你同时用同一个通道做输入和输出会冲突。所以规划功能时一个通道只做一件事。5.4 更新中断进得太频繁或太少更新中断频率 计数频率 / (ARR1)。如果进得太频繁增大PSC或ARR。如果太少减小。注意中央对齐模式的更新频率是边沿对齐的两倍。另外如果开了URS位软件置UG不会触发中断但硬件溢出会。这个在需要手动更新影子寄存器时很有用。5.5 高级定时器刹车功能误触发刹车输入引脚如果悬空可能被噪声触发导致MOE被清零输出关闭。解决办法是在BDTR里配置刹车极性或者使能刹车输入滤波或者干脆不用刹车时把刹车引脚配置成普通GPIO。我有个项目就是刹车引脚没处理电机一转就停查了两天才发现是刹车误触发。6. 寄存器操作中的几个实战心得第一个心得初始化时先写PSC和ARR再写CCMR和CCER最后写CR1和EGR。这个顺序能避免计数器在配置过程中乱跑。我见过有人先置CEN再配PSC结果第一个周期完全不对。第二个心得动态改PWM参数时利用影子寄存器。ARPE1和OCxPE1时你写ARR和CCR不会立即生效而是在下个更新事件生效。这样波形是连续的不会出现半个周期。如果你需要立即生效就置UG位但要注意会触发更新中断。第三个心得32位定时器TIM2/TIM5读CNT要小心。因为32位寄存器在16位总线上要分两次读如果两次读之间发生了进位读出来的值就错了。正确做法是先读高16位再读低16位再读一次高16位如果两次高16位相同则值有效。或者用DMA读取。这个坑在高速计数时特别明显。第四个心得定时器中断里尽量少做事。定时器中断频率可能很高如果里面做浮点运算或者复杂逻辑CPU负载会很大。我一般只在中断里置标志位主循环里处理。如果非要在中断里处理用DMA或者硬件自动操作比如用DMA把CCR值批量更新实现任意波形输出。第五个心得调试时善用DBGMCU冻结定时器。在调试器里暂停CPU时如果不冻结定时器CNT会继续跑你看到的寄存器值和你暂停时的实际值不一致。在DBGMCU_APB1FZ里置对应位就能在调试时冻结TIM2~TIM7APB2FZ里冻结TIM1和TIM8。7. 从寄存器到实际项目的映射思路学完第八章你手里就有了一个非常灵活的定时器工具。实际项目中我通常这样用做系统时基用TIM6或TIM7做1ms中断给任务调度器提供心跳。基本定时器没有引脚不占用GPIO最合适。做PWM调光用TIM3或TIM4的通道输出PWM驱动LED或者电机。频率选1kHz~20kHz看负载特性。做脉冲测量用TIM2或TIM5的输入捕获测超声波回波、红外遥控信号。32位计数器不用担心中间溢出。做电机控制用TIM1或TIM8的高级定时器输出三对互补PWM配死区和刹车。这是高级定时器的主场。做编码器接口用TIM2~TIM5的编码器模式直接读正交编码器的计数不用外部中断。这个功能在寄存器层面就是配SMCR的SMS位为001或010然后读CNT即可。最后再分享一个小技巧如果你需要多个定时器同步可以用一个定时器做主输出TRGO信号另一个定时器配成从模式用ITR0~ITR3接收。这样两个定时器的计数器是同步的做多路PWM相位控制时特别有用。具体配SMCR的TS位和SMS位主定时器配CR2的MMS位。这个在第八章的末尾有提到但很多人没注意实际用起来能省不少事。