1. 为什么还要啃寄存器手册1.1 从“能跑就行”到“必须搞懂”的转折点很多人玩STM32是从库函数或者图形化配置工具入门的点几下鼠标、生成一堆初始化代码编译烧录灯亮了觉得已经掌握了。但真正做项目的时候你会发现有些问题库函数根本帮不了你中断响应慢了几个时钟周期、某个外设死活不工作、低功耗模式下电流降不下去、时序要求严格时信号总是差那么一点。这些问题追到根上往往就是寄存器配置的问题。我刚开始接触STM32的时候也是走库函数路线直到有一次做一个需要精确控制PWM死区时间的项目库函数封装的参数怎么调都达不到要求最后硬着头皮翻参考手册直接操作寄存器才搞定。从那以后我就养成了一个习惯库函数可以用但关键外设的寄存器必须心里有数。这个系列写到第四章前面三章分别讲了GPIO、时钟树和中断系统这一章我们聚焦一个非常核心但又容易被忽视的话题——定时器TIM的寄存器级配置。为什么定时器这么重要因为它几乎是所有嵌入式项目的“心跳”。延时、PWM输出、输入捕获、编码器接口、触发ADC采样全都离不开它。你可以不用DMA可以不用CAN但定时器你绕不过去。而定时器的寄存器又特别多光一个通用定时器就有CR1、CR2、SMCR、DIER、SR、EGR、CCMR1/2、CCER、CNT、PSC、ARR、CCR1~4、BDTR等等初学者一看就头大。所以这一章的目标很明确把定时器最常用的几个寄存器讲透让你能脱离库函数直接配置出PWM和定时中断。1.2 本章覆盖的范围和适用人群这一章主要讲通用定时器以TIM2~TIM5为代表和高级定时器TIM1、TIM8的寄存器操作。基本定时器TIM6、TIM7比较简单顺带提一下。内容会覆盖定时器的时钟来源和计数模式预分频器PSC和自动重装载寄存器ARR的配合计算定时中断的完整配置流程PWM输出的寄存器级实现输入捕获的基本原理和配置高级定时器特有的刹车和死区寄存器适合的读者是已经用过STM32库函数、能点亮LED和配置基本中断但想进一步理解底层机制的人。如果你连GPIO的寄存器都没碰过建议先翻回去看第二章。这一章不会逐位解释每个寄存器的每一个bit而是挑最常用的位结合具体场景讲清楚“为什么要这样配”。2. 定时器的核心寄存器拆解2.1 时钟来源定时器到底跑多快在配置任何定时器之前你必须先搞清楚它的时钟从哪来。STM32的定时器时钟不是直接等于系统时钟的中间要经过APB预分频器。以常见的STM32F103为例系统时钟72MHzAPB1预分频系数为2APB1总线时钟是36MHz。但定时器有个特殊规则如果APB预分频系数不为1定时器时钟等于APB总线时钟的2倍。所以TIM2~TIM5的时钟是72MHz而不是36MHz。这个规则很多人第一次看到会懵为什么反而翻倍了其实这是芯片设计时的考虑APB总线为了省电跑得慢但定时器需要更高的分辨率所以单独给它翻倍。你在计算定时时间的时候如果搞错了这个时钟源算出来的时间会差一倍。怎么确认自己芯片的定时器时钟最靠谱的方法是查参考手册的时钟树图找到对应定时器的时钟分支。以STM32F4系列为例TIM2~TIM7挂在APB1上TIM1、TIM8~TIM11挂在APB2上规则类似但具体频率不同。我一般会在代码里先算好一个全局变量TIMER_CLK后面所有计算都基于它避免每次都要翻手册。2.2 PSC和ARR定时时间的两个关键参数定时器的基本工作原理其实就是一个计数器在数数。时钟每来一个脉冲计数器加一或减一。数到某个值的时候产生事件然后重新开始。这里有两个关键寄存器PSC预分频器把输入时钟分频降低计数频率ARR自动重装载寄存器决定计数器的上限数到这个值就溢出定时时间的计算公式是定时时间 (PSC 1) × (ARR 1) / 定时器时钟频率注意这里PSC和ARR都要加1因为寄存器值是从0开始计的。比如PSC71实际分频系数是72ARR999实际计数1000次。举个例子定时器时钟72MHz想要1ms的定时中断。1ms 0.001s对应频率1000Hz。72MHz / 1000Hz 72000。所以(PSC1)×(ARR1)72000。可以取PSC71分频72ARR999计数100072×100072000正好。这里有个经验PSC和ARR的分配要合理。如果PSC太大计数频率太低定时精度会下降如果ARR太大计数器溢出周期太长修改定时时间时不够灵活。一般来说PSC尽量小一些让计数器跑得快一点ARR根据需要的定时范围来定。对于1ms这种常用定时我习惯用PSC71、ARR999这样计数器每1微秒加一次ARR直接对应微秒数改起来很直观。2.3 CR1控制寄存器的关键位CR1是定时器的控制寄存器1里面有几个位几乎每次配置都要用到CENbit 0计数器使能。置1开始计数清0停止。这个位是最基本的开关。ARPEbit 7自动重装载预装载使能。置1时写入ARR的值不会立即生效要等到下次更新事件才生效。这个位在需要动态修改定时周期时很重要可以避免中途改值导致异常。OPMbit 3单脉冲模式。置1时计数器溢出一次后自动停止适合做精确的单次延时。DIRbit 4计数方向。0是向上计数1是向下计数。CMSbit 6:5中心对齐模式选择。做电机控制时常用中心对齐PWM可以减少谐波。我刚开始学的时候配置完PSC和ARR就直接把CEN置1了结果发现定时器不工作。后来才发现忘了先产生一个更新事件把PSC的值加载进去。这个坑后面会详细讲。2.4 DIER和SR中断相关的两个寄存器DIERDMA/中断使能寄存器和SR状态寄存器是配合使用的。DIER里面对应各种事件的使能位SR里面对应各种事件的标志位。常用的中断事件有UIEDIER bit 0更新中断使能对应SR的UIF位CC1IE~CC4IEDIER bit 1~4捕获/比较中断使能对应SR的CC1IF~CC4IFTIEDIER bit 6触发中断使能BIEDIER bit 7刹车中断使能高级定时器配置中断的流程是先在DIER里使能对应的中断位然后在NVIC里使能对应的中断通道最后在中断服务函数里判断SR的标志位并清除。这里有个容易忽略的点SR的标志位必须手动清除。比如更新中断发生后UIF位会置1你必须在中断服务函数里写TIMx-SR ~TIM_SR_UIF来清除否则中断会一直触发。我见过有人忘了清标志结果程序卡在中断里出不来。2.5 CCMR和CCERPWM输出的核心配置要做PWM输出CCMR捕获/比较模式寄存器和CCER捕获/比较使能寄存器是必须搞懂的。CCMR分为CCMR1和CCMR2每个管两个通道。以CCMR1为例OC1Mbit 6:4输出比较1模式。PWM模式1是110PWM模式2是111。OC1PEbit 3输出比较1预装载使能。置1时CCR1的修改要等更新事件才生效。CC1Sbit 1:0捕获/比较1选择。00是输出01是输入。CCER里面主要是CC1Ebit 0捕获/比较1输出使能CC1Pbit 1输出极性。0是高电平有效1是低电平有效PWM模式1和模式2的区别在于模式1下计数器小于CCR时输出有效电平模式2下计数器小于CCR时输出无效电平。一般用模式1比较多。配置PWM的典型流程是设置ARR决定PWM周期设置CCR决定占空比CCMR里选PWM模式CCER里使能输出。占空比 CCR / (ARR1)。3. 从零配置一个定时中断3.1 完整配置步骤和代码假设我们用TIM2做一个1ms的定时中断定时器时钟72MHz。步骤如下第一步使能TIM2时钟。TIM2挂在APB1上对应的RCC使能位是RCC_APB1ENR的bit 0。RCC-APB1ENR | RCC_APB1ENR_TIM2EN;第二步配置PSC和ARR。前面算过PSC71ARR999。TIM2-PSC 71; TIM2-ARR 999;第三步产生一个更新事件把PSC的值加载到影子寄存器。TIM2-EGR | TIM_EGR_UG;这一步非常关键。PSC寄存器有影子寄存器你写入的值不会立即生效要等更新事件才加载。如果不做这一步第一个计数周期会使用旧的PSC值导致第一个定时周期不准。第四步清除更新标志。TIM2-SR ~TIM_SR_UIF;因为上一步产生更新事件后UIF会被置1如果不清楚后面使能中断后会立即进一次中断。第五步使能更新中断。TIM2-DIER | TIM_DIER_UIE;第六步配置NVIC。NVIC_SetPriority(TIM2_IRQn, 1); NVIC_EnableIRQ(TIM2_IRQn);第七步使能计数器。TIM2-CR1 | TIM_CR1_CEN;中断服务函数void TIM2_IRQHandler(void) { if (TIM2-SR TIM_SR_UIF) { TIM2-SR ~TIM_SR_UIF; // 用户代码 } }3.2 为什么必须先产生更新事件前面提到了EGR的UG位这里展开说一下。STM32的定时器里PSC和ARR都有影子寄存器。你写入的值先存到预装载寄存器然后在更新事件发生时才会加载到影子寄存器。这样设计的好处是你可以在运行时安全地修改PSC和ARR不会因为中途改值导致计数异常。但初始化的时候如果你不主动产生一个更新事件PSC的值就不会加载定时器会用默认值PSC0开始计数。结果就是第一个周期的定时时间完全不对。我当初调这个的时候用示波器看波形发现第一个脉冲特别短后面才正常查了半天才发现是这个问题。注意产生更新事件后SR的UIF位会被置1一定要手动清除否则会误触发中断。3.3 动态修改定时周期的正确姿势有时候需要在运行时改变定时周期比如做呼吸灯或者变频控制。这时候ARPE位就派上用场了。如果ARPE0你写入ARR的值立即生效。如果计数器已经超过了新的ARR值可能会直接溢出导致一个异常短的周期。如果ARPE1写入的值要等下次更新事件才生效周期切换更平滑。我的建议是初始化时就把ARPE置1这样后面动态改ARR不会有问题。TIM2-CR1 | TIM_CR1_ARPE;改ARR的时候直接写就行TIM2-ARR new_value;新值会在当前周期结束后生效不会打断正在进行的计数。3.4 中断服务函数的编写要点中断服务函数里要注意几点第一判断标志位。虽然这个中断通道只使能了更新中断但保险起见还是判断一下UIF。第二清除标志位。一定要在中断服务函数里清除否则会反复进入中断。第三中断服务函数要尽量短。不要在中断里做耗时操作比如浮点运算、长循环、串口打印。如果确实需要处理复杂逻辑可以置一个标志位在主循环里处理。第四注意中断优先级。如果系统里有多个中断要合理分配优先级。定时中断一般设为中等优先级不要太高也不要太低。我踩过的一个坑在中断里调用了printf结果程序跑着跑着就卡死了。后来查出来是printf内部有锁中断里调用可能导致死锁。所以中断里千万不要做输出操作。4. PWM输出的寄存器级实现4.1 PWM的基本原理和寄存器配置PWM的本质就是一个计数器在0到ARR之间循环同时有一个比较值CCR。当计数器小于CCR时输出一种电平大于CCR时输出另一种电平。通过改变CCR的值就能改变占空比。配置PWM的步骤如下以TIM3的通道1为例PA6引脚第一步使能TIM3时钟和GPIO时钟。RCC-APB1ENR | RCC_APB1ENR_TIM3EN; RCC-APB2ENR | RCC_APB2ENR_IOPAEN;第二步配置PA6为复用推挽输出。CRL寄存器对应PA6的是bit 27:24设为0xB复用推挽50MHz。GPIOA-CRL ~(0xF 24); GPIOA-CRL | (0xB 24);第三步配置定时器基础参数。假设要1kHz的PWM72MHz时钟PSC71ARR999。TIM3-PSC 71; TIM3-ARR 999;第四步配置通道1为PWM模式1。TIM3-CCMR1 ~TIM_CCMR1_OC1M; TIM3-CCMR1 | (0x6 4); // PWM模式1 TIM3-CCMR1 | TIM_CCMR1_OC1PE; // 预装载使能第五步使能通道1输出设置极性。TIM3-CCER | TIM_CCER_CC1E; TIM3-CCER ~TIM_CCER_CC1P; // 高电平有效第六步设置初始占空比。假设50%CCR1500。TIM3-CCR1 500;第七步产生更新事件使能计数器。TIM3-EGR | TIM_EGR_UG; TIM3-SR ~TIM_SR_UIF; TIM3-CR1 | TIM_CR1_CEN;4.2 占空比和频率的计算PWM频率 定时器时钟 / ((PSC1) × (ARR1))占空比 CCR / (ARR1)以刚才的配置为例频率 72MHz / (72 × 1000) 1kHz。CCR1500时占空比 500/1000 50%。如果要改频率改PSC或ARR。如果要改占空比改CCR。注意CCR的值不能超过ARR否则占空比一直是100%。这里有个细节CCR有预装载功能。如果CCMR的OC1PE置1写入CCR的值要等更新事件才生效。这样改占空比时不会出现毛刺。如果OC1PE0写入立即生效可能会在周期中间产生一个异常脉冲。4.3 高级定时器的额外配置高级定时器TIM1、TIM8比通用定时器多了几个寄存器最重要的是BDTR刹车和死区寄存器。要让高级定时器输出PWM必须设置BDTR的MOE位主输出使能否则引脚不会有输出。TIM1-BDTR | TIM_BDTR_MOE;这个坑我踩过用TIM1配置PWM所有寄存器都配好了示波器就是没波形。查了半天才发现高级定时器默认MOE是0输出被关掉了。BDTR里还有死区时间设置DTG位做电机驱动时用来防止上下桥臂直通。死区时间的计算和具体时钟频率有关需要查手册的表格。4.4 PWM输出的实测和调试配置完之后用示波器或者逻辑分析仪看波形。如果没有波形按以下顺序排查时钟使能了吗RCC的对应位有没有置1GPIO配置对吗是不是复用推挽MOE位高级定时器置1了吗CCER的CC1E置1了吗CEN置1了吗CCR值是不是0或者大于ARR我一般会先在代码里把CCR设成ARR的一半这样示波器上应该看到50%的方波。如果看到的是直线说明输出没使能如果频率不对检查PSC和ARR的计算。提示调试PWM时先把频率设低一点比如1kHz这样示波器容易捕捉。等确认工作了再改到目标频率。5. 输入捕获和常见问题排查5.1 输入捕获的基本配置输入捕获用来测量外部信号的频率、脉宽等参数。基本原理是当引脚上检测到边沿时把当前计数器的值锁存到CCR寄存器并产生中断。配置步骤以TIM3通道1为例// 1. 配置PA6为浮空输入 GPIOA-CRL ~(0xF 24); GPIOA-CRL | (0x4 24); // 2. 配置定时器基础参数 TIM3-PSC 71; TIM3-ARR 0xFFFF; // 3. 配置通道1为输入映射到TI1 TIM3-CCMR1 ~TIM_CCMR1_CC1S; TIM3-CCMR1 | 0x1; // CC1S01IC1映射到TI1 // 4. 设置输入滤波和预分频 TIM3-CCMR1 ~TIM_CCMR1_IC1F; TIM3-CCMR1 | (0x3 4); // 滤波 TIM3-CCMR1 ~TIM_CCMR1_IC1PSC; // 不分频 // 5. 设置捕获边沿 TIM3-CCER ~TIM_CCER_CC1P; // 上升沿捕获 TIM3-CCER | TIM_CCER_CC1E; // 使能捕获 // 6. 使能捕获中断 TIM3-DIER | TIM_DIER_CC1IE; NVIC_EnableIRQ(TIM3_IRQn); // 7. 使能计数器 TIM3-CR1 | TIM_CR1_CEN;中断服务函数里读取CCR1的值两次捕获的差值就是信号周期。5.2 常见问题速查表问题现象可能原因排查方法定时器不计数CEN没置1检查CR1的bit 0第一个周期不准没产生更新事件检查EGR的UG位中断一直触发没清SR标志中断里清UIFPWM无输出MOE没置1高级定时器检查BDTRPWM无输出CC1E没置1检查CCERPWM频率不对时钟源算错查时钟树确认定时器时钟占空比不对CCR超过ARR检查CCR值捕获值跳动滤波不够增大IC1F动态改ARR异常ARPE没置1检查CR1的bit 75.3 几个容易踩的坑坑一忘了使能AFIO时钟。有些引脚需要重映射才能用作定时器输出这时候要开AFIO时钟。比如TIM3的通道1默认在PA6如果要重映射到PB4就需要配置AFIO_MAPR寄存器。坑二PSC和ARR的1。公式里是(PSC1)×(ARR1)不是PSC×ARR。我见过有人算出来差一倍就是忘了加1。坑三中断优先级分组。STM32的中断优先级有分组设置如果分组不对抢占优先级和子优先级的分配会出问题。建议在程序初始化时统一设置一次分组。坑四CCR的预装载。如果OC1PE0在计数器运行过程中改CCR可能会产生一个异常宽的脉冲。做电机控制时这个问题很致命一定要使能预装载。坑五高级定时器的重复计数器。TIM1和TIM8有RCR重复计数器寄存器更新事件发生的频率是RCR1次溢出。如果RCR没设对中断频率会不对。一般设RCR0就行。5.4 调试工具和技巧调试定时器问题示波器是最直接的。如果没有示波器可以用以下方法在中断里翻转一个GPIO用逻辑分析仪看频率用调试器实时查看CNT、SR、CCR等寄存器的值在关键位置设置断点观察程序是否执行到我个人的习惯是配置完定时器后先不接外部电路直接在中断里翻转一个LED或者GPIO确认定时周期正确了再接后续的PWM或者捕获逻辑。这样可以把问题隔离避免多个因素混在一起。另外STM32的参考手册是最好的老师。遇到不确定的位直接翻手册看描述。虽然手册很厚但每个寄存器的每一位都有详细说明比任何教程都准确。我一般会把常用寄存器的关键位截图保存配置的时候对照着看避免记错。6. 寄存器操作的经验总结6.1 直接操作寄存器的利与弊直接操作寄存器最大的好处是透明和高效。你知道每一行代码在做什么没有中间层执行效率最高。对于时序敏感的场景比如高速PWM、精确捕获寄存器操作是必须的。但缺点也很明显可读性差、容易出错、移植性差。不同型号的STM32寄存器地址和位定义可能不同换一个芯片就要重新查手册。而且寄存器操作没有类型检查写错了编译器也不报错。我的建议是关键外设用寄存器普通外设用库函数。比如定时器、ADC、DMA这些对时序和性能要求高的直接操作寄存器GPIO、串口这些用库函数就行没必要什么都自己写。6.2 如何高效查阅参考手册参考手册几百页不可能从头看到尾。我的方法是第一先看目录找到对应外设的章节。每个外设的章节结构都差不多功能描述、寄存器列表、寄存器详细说明。第二先看功能描述理解外设的工作原理。这部分通常有框图配合框图看更容易理解。第三看寄存器列表找到你需要的寄存器。然后翻到对应的详细说明看每一位的定义。第四配置的时候把关键寄存器的位定义整理成表格贴在代码旁边。这样下次改的时候不用再翻手册。我一般会用Excel做一个寄存器配置表列出每个寄存器的值、每一位的含义、为什么这样配。这样项目做完之后回头看代码一目了然。6.3 从寄存器到库函数的映射理解了寄存器之后再看库函数就会觉得很清晰。比如TIM_TimeBaseInit这个函数内部就是在配置PSC、ARR、CR1等寄存器。TIM_OC1Init就是在配置CCMR1、CCER、CCR1。知道底层之后用库函数时心里有底出了问题也知道去哪里查。而且有些库函数没封装的功能你可以直接操作寄存器来实现不用等库更新。6.4 后续可以深入的方向定时器这块还有很多可以深挖的内容编码器接口模式、霍尔传感器接口、触发ADC采样、DMA突发传输、多定时器同步等等。这些高级功能在实际项目中很有用但都需要对寄存器有深入理解。我个人的学习路径是先用库函数把功能跑通然后翻手册看对应的寄存器配置最后试着自己用寄存器重写一遍。这样既保证了项目进度又真正掌握了底层原理。下一章我会讲ADC的寄存器配置那个比定时器更复杂但掌握了方法之后也是一样的套路。
阅读完成 · 觉得有帮助?