首页 / 资讯中心 / 文章详情

STM32定时器寄存器剖析:从PSC/ARR到PWM与捕获

STM32定时器寄存器剖析:从PSC/ARR到PWM与捕获 ★ FEATURED ARTICLE
1. 定时器到底是个什么东西前四章我们把GPIO和时钟树聊完了我以为大家的寄存器之路已经顺了大半。结果第五章定时器一来好多人又开始打退堂鼓。我不止一次看到有人问我为什么我的延时函数在跑但是PWM死活没输出或者定时器中断进不去是不是我烧录姿势不对先给你吃颗定心丸STM32的定时器玩来玩去就是几个核心寄存器在互相配合——预分频器PSC、自动重装载寄存器ARR、计数器CNT、控制寄存器CR1。把这四兄弟搞明白你就已经掌握大半边天了。这一章我用白话带你把这些寄存器挨个过一遍。你先放下对计数溢出比较匹配这些术语的恐惧。它们本质上是同一件事让一个计数器在特定条件下产生一个事件然后让这个事件去触发后续动作。这个动作可以是中断、可以是翻转引脚、也可以是启动一次DMA搬运。1.1 从数数开始理解定时器定时器最朴素的本质是一个数数的东西。你给它一个时钟源它每个时钟沿把计数值加一。这个值存在CNT里。你可以拿它当厨房定时器到了设定时间就响铃产生更新事件。也可以拿它当停车场的人肉计数器每进来一辆车数字加一外部脉冲计数。还可以拿它当高速公路测速仪两段固定距离中间测时间输入捕获。STM32芯片里一般有好几类定时器基本定时器比如TIM6、TIM7、通用定时器TIM2~TIM5、高级定时器TIM1、TIM8。它们的寄存器大同小异只是能力不同。基本定时器只有计数功能通用定时器加了输入捕获和输出比较高级定时器又额外带了互补输出和刹车功能。你先玩好通用定时器其他的都是锦上添花。1.2 先背出这张寄存器地图在翻寄存器之前先把下面这张表存进脑子里。这章后面每一个字都在围绕这张表说事。寄存器中文外号核心作用PSC预分频器寄存器把输入时钟先分频相当于把面包切成片再数ARR自动重装载寄存器计数的终点溢出周期的决定者CNT计数器寄存器当前数到哪了实时可读可写CR1控制寄存器1总开关CEN位、计数方向、对齐模式SR状态寄存器各种事件标志位UIF是最常用的更新标志DIER中断使能寄存器决定哪些事件能进中断CCMR捕获/比较模式寄存器通道工作在捕获还是输出模式、PWM模式选择CCER捕获/比较使能寄存器通道输出/捕获的总开关CCRx捕获/比较寄存器比较值或捕获到的瞬间计数器快照好地图有了。下面挨个解释它们为什么那样设计。2. 核心寄存器逐个击破——白话版释义2.1 PSC与ARR预分频和自动重装载的联合作战先说你平时用得最多的两个PSC和ARR。PSCPrescaler预分频器。它把定时器驱动的原始时钟信号先放慢。比如你定时器时钟是72MHz你希望它每1微秒数一次数那就用72分频。关键是实际分频系数是PSC寄存器的值加1。你写PSC71那就是72分频。这个加1的坑几乎每个新手都要踩一次。ARRAuto-Reload Register自动重装载寄存器。它决定了计数器数到哪里算到头。这个值同样是加1思维你写ARR999计数器会从0数到999一共数1000个计数值。到头的瞬间产生一个更新事件UIF然后计数器自动跳回0重新开始。我当时学的时候用了这个类比才彻底记住PSC是决定一格刻度是多少秒ARR是决定表盘上一共有多少个刻度。一格的秒数乘以刻度总数就是整个计时的周期。算一笔账时钟72MHzPSC71分频后1MHz周期1微秒ARR999。定时器周期 1000 × 1微秒 1毫秒也就是1ms进一次更新中断。这个公式你值得钉在桌前定时器溢出周期 (PSC 1) × (ARR 1) / 定时器输入时钟频率为什么PSC和ARR都要加1再乘因为寄存器从0开始计。你写1代表的是2个计数值你写999代表的是1000个计数值。这个思维如果不扭转过来后面算PWM频率、算捕获时间都会出错。2.2 CNT、SR、DIER看看现在数到哪了、发生了什么事CNT寄存器不需要你写太多东西它更多是被读的。调试的时候你可以在调试器里实时观察CNT的跳动来确认定时器时钟到底有没有跑起来。它是一个24位或16位的寄存器具体看你用的芯片型号数到ARR就回零。SR寄存器全称状态寄存器。它里面每一位代表一种事件UIF更新事件、CC1IF捕获/比较事件、TIF触发事件等等。你要在中断服务函数里清除UIF标志方法永远是往UIF位写0。很多人写成读SR寄存器然后清标志其实正确做法是直接给SR的对应位写0或者先读SR再读CNT也能走走硬件路径清除部分标志。DIER是中断使能寄存器。这玩意儿是个门卫SR里的标志产生了但如果DIER对应的位没打开中断请求根本不会发到NVIC那里。所以排查定时器为什么进不了中断的时候先检查两个地方DIER对应位有没有置1NVIC里定时器中断通道有没有使能。2.3 CR1与CCMR总开关和通道工作模式CR1寄存器对初学者来说需要关注的其实就三位CENbit 0定时器总开关。置1启动计数清0停止。DIRbit 1计数方向。0是向上计数1是向下计数。做标准定时用向上就对了。CMSbit 5~6对齐模式。做中心对齐PWM的时候选模式1或模式2或模式3不用PWM就别管它。还有一个很容易被忽略的ARPE位bit 7。这个位决定ARR是否带影子寄存器。如果ARPE1你对ARR的修改会在下一次更新事件才真正生效如果ARPE0改动立即生效。预分频器PSC有同样的影子机制。影子寄存器这个设计是为了防止你在计数器运行途中改参数导致一个周期突然变得不伦不类。如果你只是初始化阶段配置一次这个位可以不关心但你要在运行中动态调PWM频率就得明白它。CCMR寄存器是通道模式寄存器。这个寄存器的名字取得特别容易误导人——它既管捕获Capture也管比较Compare。你可以通过它的OCxM位输出比较模式位来选择PWM模式1还是模式2也可以通过ICxPSC位输入捕获分频器来设置捕获边沿分频。后面讲到PWM和输入捕获的时候我们再细抠这两个寄存器的位。2.4 配置序列一个规范到能背下来的顺序寄存器学到这里我猜你已经手痒想动手了。但在写代码之前先把配置顺序捋顺。我总结了一个五步走照着写基本不会漏打开RCC里对应定时器的时钟比如TIM2的时钟使能位。设置PSC和ARR确定定时周期。配置DIER决定哪些事件能进中断或者不依赖中断轮询SR。配置NVIC使能这个定时器的中断通道并设置优先级。最后把CR1的CEN置1启动定时器。为什么CEN放最后因为你前面所有配置都是在给定时器上膛“开保险”的动作必须放到最后一步。如果先启动定时器再改参数可能会让第一个计数周期使用错误的PSC/ARR值。3. 实操演示从零配置一个1ms定时中断我拿某个开发板上最常见的TIM2来演示芯片主频72MHz。目标每1ms进一次定时器中断中断里翻转一个IO口实现LED以500ms周期闪烁即翻转LED引脚亮250ms灭250ms就近似1Hz如果每次中断翻转一次则翻转周期2ms亮500ms灭500ms需要翻转500次之后你再根据自己的需求延长。3.1 先算分频系数和重装载值已知条件定时器输入时钟72MHz。目标周期1ms。先用前面那条公式反推(PSC 1) × (ARR 1) / 72,000,000 0.001秒选项太多了怎么选我建议先把PSC定到让计数器时钟变得好算。把PSC设为71那计数时钟就是72MHz/72 1MHz。1MHz下数1000个计数值就是1ms。所以PSC 71ARR 999中断周期 (71 1) × (999 1) / 72,000,000 1ms如果你想让PWM的频率刚好是20kHz你就要反过来先定ARR和PSC的组合。这就是为什么我反复强调先理解公式再碰寄存器。3.2 直接操作寄存器的初始化代码下面这段代码全部用寄存器操作不依赖标准外设库。/* 使用TIM2输出比较/中断方式实现1ms定时 */ void TIM2_Init(void) { /* 1. 开启TIM2时钟 */ RCC-APB1ENR | RCC_APB1ENR_TIM2EN; /* 2. 配置预分频器72MHz / (711) 1MHz */ TIM2-PSC 71; /* 3. 自动重装载值计数1000个得到1ms周期 */ TIM2-ARR 999; /* 4. 更新事件产生时影子寄存器才加载PSC/ARR的新值可选 */ TIM2-CR1 | TIM_CR1_ARPE; /* 5. 清一次更新标志位防止刚初始化就进中断 */ TIM2-SR ~TIM_SR_UIF; /* 6. 使能更新中断 */ TIM2-DIER | TIM_DIER_UIE; /* 7. 配置NVIC使能TIM2全局中断抢占优先级设为2 */ NVIC_SetPriority(TIM2_IRQn, 2); NVIC_EnableIRQ(TIM2_IRQn); /* 8. 启动定时器 */ TIM2-CR1 | TIM_CR1_CEN; }配置完之后中断服务函数长这样void TIM2_IRQHandler(void) { /* 先判断是不是更新中断 */ if (TIM2-SR TIM_SR_UIF) { /* 这里必须清除标志位注意是写0 */ TIM2-SR ~TIM_SR_UIF; /* 用户代码翻转LED引脚 */ GPIOA-ODR ^ GPIO_ODR_ODR0; } }3.3 中断标志位清除顺序的讲究上面代码里有一行看着不起眼却极其重要TIM2-SR ~TIM_SR_UIF;这行必须在用户代码之前清掉还是之后清我的建议是先清再执行用户逻辑。原因很简单如果用户逻辑耗时较长而期间定时器又产生了新的UIF标志你在末尾清标志可能会把新产生的那一次事件也清掉导致丢一次中断。我自己就吃过这个亏——把标志清除放在中断函数最后结果用户代码里有个延时操作中断到来时标志已经被置位最后清标志把新的一并清掉了导致中断周期看起来忽长忽短。改到前面之后问题立刻消失。还有一个小技巧有些芯片手册提到读SR寄存器再读CNT硬件会自动清除UIF标志。但这个行为跟具体的芯片版本有关手动写0清除才是最保险的写法。千万别依赖自动清除这个彩蛋。4. PWM输出和输入捕获两大高频需求的寄存器差异定时中断只是定时器最基础的用法。真正让定时器发光发热的是它的**输出比较PWM和输入捕获测量脉冲**功能。这两个场景同样只需要改几个寄存器位。4.1 从纯定时到输出PWM只需动四个位我们已经能产生1ms中断了。如果想输出一个频率1kHz、占空比75%的PWM波怎么改先理清思路PWM要的不是到了就中断而是当计数器CNT达到某个特定值CCR的时候翻转/设置输出引脚电平。所以你从定时器中断切换到PWM输出模式本质上做了四件事在CCMR里把通道设为PWM模式1OCxM位110。在CCER里把对应通道的输出使能打开CCxE1。设置CCR寄存器的值占空比 CCR / (ARR 1)。配置对应的GPIO引脚为复用功能输出。以TIM2的CH1为例产生20kHz PWM占空比50%void TIM2_PWM_Init(void) { RCC-APB1ENR | RCC_APB1ENR_TIM2EN; RCC-AHB1ENR | RCC_AHB1ENR_GPIOAEN; /* PA0 配置为复用输出以某个具体芯片为例 */ GPIOA-MODER ~GPIO_MODER_MODER0_Msk; GPIOA-MODER | (GPIO_MODER_MODER0_1); // 0b10: 复用功能 GPIOA-AFR[0] ~GPIO_AFRL_AFSEL0_Msk; GPIOA-AFR[0] | (0x01) 0; // 复用功能AF1对应TIM2_CH1 /* 预分频72MHz / (11) 36MHz太宽了不适合20kHz下面重新算 */ /* 如果你坚持用PSC1则计数时钟36MHzARR1799时频率20kHz */ TIM2-PSC 1; TIM2-ARR 1799; /* 清计数器 */ TIM2-CNT 0; /* 通道1输出比较模式PWM模式1 */ TIM2-CCMR1 | (0x6 4); // OC1M 110 /* 使能CH1输出 */ TIM2-CCER | TIM_CCER_CC1E; /* 设置50%占空比CCR (ARR1)/2 900 */ TIM2-CCR1 900; /* 启动 */ TIM2-CR1 | TIM_CR1_CEN; }注意我这个例子里的PSC1是为了让你看清楚PSC和ARR如何搭配出频率实际工程中我更推荐先用PSC把计数周期设置为微秒量级比如PSC71再让ARR去精确控制频率。这样调起来更直观。4.2 输入捕获怎么用寄存器抓到频率与脉宽再说输入捕获。这个功能可以测量外部信号的频率、周期、脉宽。它的原理很简单通道引脚出现指定边沿上升沿/下降沿时硬件会瞬间把CNT当前的数值复制到CCRx寄存器并置位CCxIF标志。你只需要做三件事在CCMR里设置输入捕获滤波器和分频——这其实是用ICxPSC位设置要计数1个边沿、2个边沿还是4个边沿才触发捕获。在CCER里设置捕获极性也就是捕获上升沿还是下降沿。在CCER里使能捕获通道CCxE1。测量频率的思路捕获到第一个上升沿时读CCR1记为t0捕获到第二个上升沿时读CCR1记为t1。把两次的差值换算成时间乘以计数时钟周期倒数就是频率。关键前提是两次捕获之间CNT不能被重置否则差值就没有意义了。这里有个读寄存器的坑在32位的定时器上CCR寄存器是32位的。但在某些16位定时器上CCR寄存器是16位。如果你在捕获中断里先读高16位再读低16位在两次读之间如果发生了捕获数据就会错位。解决方法是读两次比较结果是否一致或者直接用一次32位读操作。4.3 高级定时器那些进阶位要心里有数如果你用的是TIM1或者TIM8这类高级定时器本章的配置序列依旧适用但有三个额外注意点第一它们的重复计数寄存器RCR。默认情况下RCR0也就是每次更新事件都产生中断。如果你把RCR设置为N那定时器要溢出N1次才产生一次更新事件。这在做长定时时很有用。第二刹车寄存器BDTR。高级定时器的PWM输出在默认情况下是被禁止的你必须设置BDTR的MOE主输出使能位为1PWM信号才能真正输出到引脚。我见过有人配置TIM1的PWM查了半天查不出问题就是漏了这个MOE位。第三高级定时器的通道通常有互补输出CHxN用来驱动全桥电路。如果你用不到完全不用管它。但如果你有一天要做一个带刹车功能的电机驱动这套互补输出和死区插入的寄存器位就很值钱了。5. 常见问题速查定时器调试避坑实录5.1 定时器完全没反应优先级最高的检查项永远是RCC时钟有没有开。我见过太多人把NVIC配好了、中断函数写了结果忘了给RCC-APB1ENR对应位置1。定时器连时钟都没有后面全是白搭。再检查CR1的CEN是否为1。这个位常常被调试工具复位成0或者你在别的代码里误操作把整个CR1覆盖了。我建议在初始化函数末尾加一句读回校验打印CR1的值确认CEN1。5.2 改了ARR和PSC但新参数不生效这个现象十有八九是影子寄存器引起的。PSC和ARR都有影子缓存只有当更新事件到来的时候新值才会被加载到真正参与的寄存器里。如果你在跑起来之后动态改ARR却没等到下一个更新事件就急着去读读到的还是旧值。解决方式两种要么在修改前先产生一次软件更新事件EGR寄存器的UG位要么等到中断服务函数里同步修改。我习惯在中断里改ARR做变频控制因为这个时机天然就对齐了更新边界。5.3 中断一直进来根本停不下来先看一眼SR寄存器是不是UIF一直被置1。如果是说明清除标志的代码没跑到或在清除之前又有新的更新事件产生。我的排查顺序是这样的中断函数开头第一行就是清标志别的什么都不做看中断频率是否恢复正常。如果恢复正常说明用户代码阻塞太长把清标志的时机往后拖了。如果还是疯狂触发检查DIER有没有误配置比如不光是UIE连CCIE也一起开了。还有一个很隐蔽的问题最后启动定时器之前忘了清一次SR。初始化时SR里的UIF位可能是个随机值一使能中断就会立刻进一次中断。我写代码的习惯就是CEN之前必须清一次UIF。下面这张速查表是我每次帮人远程调定时器问题都会发过去的那张现象优先检查项常见坑定时器不计数RCC时钟、CR1.CEN忘了开时钟定时周期不对PSC/ARR计算忘了加1中断不触发DIER、NVIC中断通道没使能PWM无输出CCER.CCxE、GPIO复用、BDTR.MOE高级定时器GPIO没配复用更新中断风暴SR清标志顺序用户逻辑耗时太长5.4 读CNT或CCR的值偶尔跳变如果你在应用中需要高精度测量建议把CNT读两次取一致的那次作为结果。因为你在读一个正在自增的寄存器时完全可能遇到读低16位的前一瞬间刚好进位到高16位这种边界情况。虽然概率很低但做工业控制的时候一次误读可能导致整个控制环跳变这个代价实在不值得。6. 给自己的一个学习路径建议这章内容不少但回头想想整个定时器真正需要你动手配置的位其实不超过20个。比GPIO只多一点点只是它们组合在一起时能力就完全不同了。我强烈建议你今天找一块板子打开一个空的工程手写一遍这段初始化代码。不要复制粘贴不要用CubeMX生成。你先算好PSC和ARR然后逐行写寄存器配置最后在中断里把LED点亮、熄灭。完成这一步你对定时器的信心会上一个台阶。写完这章我突然想起自己当初第一次点亮LED时用的是别人封装好的函数三行代码调完就跑了完全没搞懂背后发生了什么。后来被一个问题逼到不得不翻开参考手册才意识到外设库帮你省掉的每一行都是你之后理解问题的坎。现在你把这些坎一个一个迈过去以后再看任何基于定时器的例程你会觉得他们写的全是废话因为你连寄存器层面都懂透透的了。动手去改不要怕烧板子。板子烧不了知识不烧可学不会。
阅读完成 · 觉得有帮助?
咨询建站