恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

STM32定时器寄存器白话手册:PWM与输入捕获实战解析

  • 首页
  • 资讯中心
  • /
  • STM32定时器寄存器白话手册:PWM与输入捕获实战解析

相关资讯

AUTOSAR以太网协议栈仿真套件:从SOME/IP到TSN的可验证开发实践 2026/10/21 10:33:40
NVDLA硬件映射指南:从PyTorch卷积到Cycle级流水线 2026/10/21 10:28:39
STM32F103硬件I2C驱动AT24C02 EEPROM读写实战指南 2026/10/21 10:28:39

最新资讯

数智金融用户偏好智能画像构建全解析:从数据底座到算法落地
RAG技术实战指南:从架构拆解到生产落地的完整路径
Ubuntu安装NVIDIA驱动报错Building kernel modules的排查与解决
Spring Boot人脸识别数据管理实战:表设计、特征存储与检索优化
基于STM32的红外空调控制方案:从编码协议到状态机实现
SVN Checkout 命令详解:从基础原理到实战排错全攻略

今日推荐

SAP 屏幕上的金额未必存在数据库表里,从 WB2R_SV 的 Net Amount 看懂运行时计算
具身智能中‘指哪打哪’的视觉语言动作联合建模
AI时代人工翻译技术博客的价值与实践指南

本周热门

基于GMM的语音识别系统原理与MATLAB实现详解
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

STM32定时器寄存器白话手册:PWM与输入捕获实战解析

发布时间:2026/10/21 10:33:40
STM32定时器寄存器白话手册:PWM与输入捕获实战解析 STM32寄存器白话手册第二章来了。上一章我们把时钟树和GPIO寄存器捋了一遍这一章直接上硬菜定时器TIM。我本来想把串口教程放在第二章后来想了想定时器才是大多数实际项目的骨架PWM输出、输入捕获、编码器测速、超声波回波计时全都挂在定时器上面。你如果只是想点个灯GPIO确实够了但想控制电机转速、做一个呼吸灯、测量外部脉冲频率绕不开定时器的几个核心寄存器。这篇还是沿用“白话手册”的写法不贴大段英文手册原文只看CNT、PSC、ARR、CCR这些寄存器到底在干什么以及为什么那么多人配完定时器发现波形不对、频率翻倍、中断乱跳。适合刚把寄存器操作入门、想真正吃透底层逻辑的开发者也适合用库函数调完项目但心里没底、想回头补课的朋友。1. 定时器在嵌入式里的地位为什么第二章必须是它很多人学完GPIO之后会有一个错觉嵌入式好像就是置位和清位。点灯拉高拉低按键读电平。直到有一天你想让LED以一定亮度亮着或者想精确测量一个脉冲信号的频率才会意识到一个问题靠主循环里软件延时去翻转IOCPU被占得死死的精度还不稳定。这时候定时器就该登场了。定时器说白了就是芯片内部的硬件秒表加定时闹钟而且还不止一个每个定时器还带好几个通道能同时干几件事。我见过不少项目一个定时器做PWM输出驱动蜂鸣器另一个定时器做输入捕获测量外部转速脉冲还有一个定时器做系统tick基准。要是没有硬件定时器这些功能全得靠CPU死等系统根本没空干别的。从学习路径上看GPIO是嵌入式的地基定时器是第二层楼。能点亮LED只能算刚进门能输出一路PWM、能测一个频率才算真正开始做“控制”和“信号处理”这类带技术含量的活。上一章我们点亮了LED这一章就用它做呼吸灯再顺带着把一个外部脉冲频率测出来。呼吸灯刷存在感比较简单但背后的PWM原理是所有电机驱动、电源调压、舵机控制的基础测频率看着不起眼但超声波测距、霍尔测速、红外解码全是同一套玩法。这一章我会用F1系列作为例子讲其他系列只是外设挂载的时钟总线不同寄存器的核心逻辑几乎是一致的。看完这一章你再翻参考手册里定时器那一两百页至少知道它在说什么。2. 看不懂定时器先别慌先搞清家族和时钟2.1 定时器家族高级、通用、基本到底差在哪大多数人第一次打开参考手册看到TIM1、TIM2、TIM3一直到TIM8还有TIM6、TIM7当场就懵了怎么这么多定时器是不是每个都要学一遍其实不用。定时器按功能从高到低分三档记住差异就够用。高级定时器比如TIM1、TIM8能干的事最多。它们除了普通定时还有互补输出、死区插入、刹车输入这些电机控制专属功能。什么叫互补输出就是两个引脚同时输出一正一反的波形用来驱动全桥电路里的上下两个功率管。死区是防止上下管同时导通造成短路而故意插入的一段延迟。做工业控制、无刷电机驱动这些功能是刚需做普通LED呼吸灯、蜂鸣器完全用不上。通用定时器TIM2、TIM3、TIM4、TIM5这几位是日常项目的主力。PWM输出、输入捕获、输出比较、编码器接口这些常用功能它们全支持。绝大多数开发板上看到的“TIM3_CH1”“TIM4_CH2”这类引脚指的就是通用定时器的通道。不是所有型号都有四个通用定时器具体看选型手册但F1里TIM2、TIM3、TIM4、TIM5是比较齐全的。基本定时器TIM6、TIM7只有一个功能定时。它没有输入输出引脚不能输出PWM也不能捕获外部信号最大的用途就是产生周期性的更新中断或者触发DAC转换。你可以把它理解成一个只能自己默默计时的跑表到了时间喊一声仅此而已。一句话记忆法高级定时器就是“通用定时器加上电机控制专用功能”基本定时器就是“通用定时器砍掉了全部通道”。学的时候把通用定时器吃透其他两类自然就能融会贯通。2.2 定时器时钟从哪来APB1和APB2那个经典大坑如果说定时器是秒表那它得有个“滴答滴答”的时钟源在驱动。这个时钟源不是CPU主频直接进的而是经过时钟树的一路分频之后才到定时器。问题就出在这条路上很多人配完定时器发现定时时间不对、PWM频率翻倍十有八九是这里算错了。拿F1系列最典型的主频配置来说外部晶振经过PLL倍频到72MHz这是CPU主频。往下分配的时候挂在APB1总线上的定时器时钟和挂在APB2总线上的定时器时钟不是简单等于总线频率。这里有一个非常关键的规则如果APB1的预分频系数不是1那么定时器时钟是APB1的2倍如果APB1预分频系数是1定时器时钟才等于APB1。APB2那边也是同理。默认的库配置里APB1预分频系数是2所以APB1总线是36MHz但挂在APB1上的TIM2、TIM3、TIM4、TIM5、TIM6、TIM7实际时钟是72MHz。APB2预分频系数是1APB2总线是72MHz所以TIM1、TIM8也是72MHz。你看了一圈发现所有定时器都是72MHz但如果有人只记住“APB1是36MHz”那他后面算定时器时间就会全都慢一半。我当时第一次遇到这个问题是在调一个1ms定时中断理论算好要配的PSC和ARR结果实测中断间隔变成了2ms。折腾半天最后逐行看了RCC配置才发现APB1分频那块有个x2的倍频器。所以你现在看到这里等于提前避开了这个坑。只要涉及定时器第一步永远先确认APB1、APB2的预分频系数再决定你的定时器时钟到底是多少不要凭感觉。另外使能定时器时钟的时候也要注意挂在哪个总线。RCC-APB1ENR管的是TIM2到TIM7这一批RCC-APB2ENR管的是TIM1、TIM8以及GPIO、ADC这些高速外设。写错使能寄存器定时器寄存器读出来永远是0因为根本就没上电。2.3 参考手册里的时基单元一张框图看懂CNT、PSC、ARR现在随便打开一本参考手册翻到定时器章节你会看到一张时基单元框图里面有CNT、PSC、ARR三个方框中间画着各种箭头。很多人被这张图劝退了我来把它翻译成人话。时钟源进来的脉冲不能直接给计数器用得先过一道预分频器也就是PSC。PSC可以理解为“脉冲降速器”它能决定你是每来一个时钟脉冲计一次数还是每来一堆脉冲才计一次数。经过PSC之后真正驱动计数器CNT的时钟信号频率是原始时钟除以PSC加1。然后CNT计数器开始干活。它每一个时钟边沿就加1向上计数模式从0开始慢慢涨。涨到多少算一圈呢由ARR决定。CNT的值达到ARR时定时器会做两件事一是产生一个“更新事件”相当于跑表到点响铃二是把CNT清零重新开始下一圈。简单说就是一句话PSC决定计数速度ARR决定计数长度CNT是那个正在数数的家伙。三个寄存器配合起来就得到了你想要的定时周期。比如时钟源是72MHzPSC设为71那么计数时钟变成1MHz也就是每微秒数一下再设ARR为999那么从0数到999正好需要1000微秒也就是1毫秒这样定时周期就是1ms。这张图看着复杂但实际你只需要关心这三兄弟其他部件都是围绕着它们做衍生功能的。比如后面要讲的PWM就是在CNT数数的过程中额外拿一个叫CCR的寄存器在旁边做比较CNT到了CCR的值就翻转输出。理解了时基单元整个定时器就有了锚点。3. 核心寄存器逐个说CNT、PSC、ARR到底谁管谁3.1 CNT正在跑着的秒表CNT寄存器是当前计数值它是一个可读可写的寄存器。你可以随时读它看计数器跑到了多少也可以直接往里面写一个数让计数器从指定位置开始跑。这个特性在调试时特别有用我发现定时器不工作时第一件事就是读CNT看看它动没动。停在某个固定值没变化说明时钟没进来或者定时器没启动一直在跳说明计数功能本身是好的问题出在其他地方。CNT的值范围取决于定时器是16位还是32位。F1系列大部分通用定时器是16位也就是最大值65535少数几个定时器在处理一些特殊应用时能到32位。如果你要做长时间计时16位寄存器在72MHz下最多计900多微秒就溢出了必须配合溢出中断做软件扩展。3.2 PSC给时钟降速的那个分频器PSC叫预分频器寄存器它的作用是把定时器输入时钟按比例降频。注意写入PSC的值和实际分频系数差1分频系数等于PSC加1。PSC写0就是不分频PSC写71就是72分频72MHz变成1MHzPSC写65535就是65536分频72MHz变成约1098Hz。PSC为什么要设计成写值加1才等于分频系数因为这样可以从1开始连续调节PSC0对应分频1PSC1对应分频2以此类推。你要是把寄存器值直接当分频系数反而没法表达“不分频”这个状态。有一个非常容易忽略的细节PSC寄存器是带预装载的意味着你写入PSC的值并不会立刻生效而是要等到定时器产生更新事件时才会被真正装载进去。这个机制的本意是避免在计数过程中突然改变计数频率导致当前计数周期混乱。但新手往往忽略了这一点改完PSC觉得没反应其实是没触发更新事件。3.3 ARR数到多少算一圈ARR叫自动重装载寄存器它决定CNT计数的终点。CNT从0开始向上加加到一个特殊时刻这个时刻不是等于ARR本身而是等于ARR时触发更新事件同时CNT被清零重新开始。所以在向上计数模式下一圈实际包含ARR加1个计数时钟周期。这个“加1”很多人会算错。要产生一个1kHz的PWM计数时钟是1MHzARR应该写999因为从0数到999刚好是1000个时钟周期。如果你写1000那周期就是1001个计数算出来频率是999Hz虽然只差了0.1%但在要求严格的场合就会有明显误差。ARR同样存在预装载问题。参考手册里有个ARPE位专门控制ARR寄存器是否启用预装载。ARPE置1ARR的写入延迟到更新事件才生效ARPE清零写入立即生效。后面调PWM频率时这个位的选择直接影响波形质量我会在实操部分专门说。3.4 影子寄存器和预装载运行中改参数不翻车的关键上面提到“预装载”这个概念很多人第一次看到会懵。我换个方式解释定时器内部其实有两套寄存器一套是你直接写入的“预装载寄存器”另一套是真正参与硬件工作的“影子寄存器”。你的写入先到预装载寄存器等到了某个时机预装载的值才被复制到影子寄存器硬件开始用新值工作。为什么要这么设计因为定时器在工作时CNT每秒都在变。如果ARR的值在CNT数到一半时突然变了那这一圈的终点到底是按新ARR算还是按旧ARR算如果没有预装载机制就会出现一圈长一圈短的怪异波形输出频率不稳定。预装载机制保证了一个周期内的参数是固定的所有改变都等到周期边界才生效波形自然平滑。PSC和ARR的预装载行为不完全一样。ARR可以由ARPE位控制要不要预装载而PSC几乎没有选择空间默认就是预装载的只能在更新事件时生效。这也解释了一个常见现象你在定时器运行中改了PSC回读寄存器发现值已经变了但CNT还在按旧频率跑要等下一个更新事件才切换。理解了影子寄存器后面第四章里运行中改CCR做呼吸灯以及调PWM参数时遇到的“波形乱跳”问题你都能自己找到答案。寄存器位宽白话职责常见易错点CNT16位/32位正在数数可读可写溢出后归零需要软件扩展计数PSC16位给时钟降速实际分频写入值1预装载更新事件才真正生效ARR16位数到多少算一圈一圈ARR1个时钟ARPE控制是否预装载计算周期别忘1CCR16位比较值用于PWM翻转和捕获快照写入顺序和极性设置影响输出方向4. 手把手写PWM做一个呼吸灯4.1 从软件翻转IO到硬件PWM差的可不止是性能在写硬件PWM之前很多人有过这种经历想做个呼吸灯用软件延时循环控制GPIO翻转降低占空比就是增加某个延时。这招能让LED亮起来但代价很大。一是CPU被一个死循环死死占住什么别的活都干不了二是波形精度极低软件延时的误差叠加起来呼吸效果看起来一顿一顿的三是你无法同时输出多路不同频率的PWM。硬件PWM的思路完全不同定时器自己数数另外拿一个叫CCR的比较寄存器设一个“翻转点”CNT每增加一次就把CNT和CCR比一比。CNT还没到CCR时输出一种电平CNT到了CCR时输出另一种电平CNT跑到ARR自动清零重来如此往复一个方波就这样被硬件自动生成了。整个过程CPU完全不用管你只需要预先设置好几个寄存器就能持续输出频率和占空比都精确的波形。4.2 PWM模式1和极性的选择为什么占空比会反过来PWM有一个常见困惑为什么我设置CCR等于半个ARR占空比却是75%而不是50%这多半是极性位的问题。对照参考手册里的PWM模式1定义向上计数模式下当CNT小于CCR时输出有效电平当CNT大于或等于CCR时输出无效电平。这里的“有效电平”和“无效电平”取决于CCER寄存器里对应通道的极性位CCxP。CCxP为0有效电平是高电平CCxP为1有效电平反而是低电平。绝大多数情况下我们用默认极性也就是CCxP等于0高电平有效。这时候如果ARR是999、CCR是500那么CNT从0数到499期间输出高电平从500数到999期间输出低电平高低各500个计数周期占空比才是50%。但如果你手滑把CCxP设成了1那输出波形就反了看起来占空比变成50%的“低电平版本”如果只看示波器波形可能发现不了直到接上负载才发现方向不对。还有一个容易算错的地方严格来说占空比等于CCR除以ARR加1因为高电平持续了CCR个计数周期总周期是ARR加1个计数周期。当ARR比较大的时候CCR除ARR加1和直接除ARR差别很小但严谨计算时还是应该用后者。4.3 完整配置代码TIM3_CH2输出1kHz PWM这里用F1系列做一个可复现的配置例子输出通道是TIM3_CH2对应引脚PA7。目标是输出1kHz频率、50%占空比的PWM。下面这段代码每行对应一个必须操作的环节我顺手在注释里写了它解决什么问题。#include stm32f1xx.h void TIM3_PWM_Init(void) { // 1. 打开定时器3和GPIOA的时钟 // TIM3挂在APB1上GPIOA挂在APB2上两个总线都要使能 RCC-APB1ENR | RCC_APB1ENR_TIM3EN; RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 2. 配置PA7为复用推挽输出50MHz // PA7属于低端口在CRL寄存器中第7引脚对应的4位是[31:28] // CNF10表示复用推挽输出MODE11表示输出速度50MHz GPIOA-CRL ~(0xF (7 * 4)); GPIOA-CRL | (0xB (7 * 4)); // 0xB 1011CNF10 MODE11 // 3. 配置时基单元 // 定时器时钟是72MHzPSC71 计数时钟 72MHz / (711) 1MHz // ARR999 计数一圈 1000个计数 1ms TIM3-PSC 71; TIM3-ARR 999; // 4. 配置通道2为PWM模式1使能预装载 // CCMR1的高8位控制通道2OC2M位于[14:12]写入110表示PWM模式1 // OC2PE位于第11位置1表示CCR2使用预装载 TIM3-CCMR1 ~(0x7 12); TIM3-CCMR1 | (0x6 12) | TIM_CCMR1_OC2PE; // 5. 设置比较值决定占空比 // ARR999CCR500 占空比 500/1000 50% TIM3-CCR2 500; // 6. 配置输出极性和输出使能 // CC2P清0高电平有效CC2E置1使能通道2输出 TIM3-CCER ~TIM_CCER_CC2P; TIM3-CCER | TIM_CCER_CC2E; // 7. 生成更新事件让PSC和ARR的值立即装载 TIM3-EGR | TIM_EGR_UG; // 8. 使能自动重装载预装载然后启动定时器 TIM3-CR1 | TIM_CR1_ARPE; TIM3-CR1 | TIM_CR1_CEN; }这段代码编译烧录后PA7引脚上就能量到1kHz、占空比约50%的PWM波形。整个过程中定时器没有占用CPU你在主循环里想干什么都行。4.4 参数计算过程从目标频率倒推PSC和ARR配置PWM之前先做一道小学算术题这是调定时器最核心的步骤。目标频率1kHz已知定时器时钟72MHz。计数时钟频率的计算公式是计数时钟 定时器时钟 / (PSC 1)。PWM输出频率的计算公式是PWM频率 计数时钟 / (ARR 1)。这两个公式合并起来PWM频率 72MHz / ((PSC 1) * (ARR 1))。现在要凑出1000我先选PSC71得到计数时钟1MHz。然后要得到1kHzARR 1必须等于1000所以ARR999。CCR决定占空比50%占空比就是高电平时间占一半CCR取500。这套计算里最关键的是最后别忘了“寄存器值减1”这一步。PSC要写71而不是72ARR要写999而不是1000。我见过有人直接把ARR写成想要的分频值结果频率差一点花半天时间排查。如果目标PWM频率需要很低比如10Hz的呼吸灯你会发现一个问题ARR最大只能到6553516位定时器PSC最大也是65535两者都取最大值时最低频率是72MHz除以65536再除以65536大概是0.0167Hz理论上够用。但实际中如果ARR取太大一个周期内计数非常密集精度会受影响。我的建议是尽量把PSC设大让计数时钟降下来ARR保持在几百到几千的量级这样每步的占空比调节精度足够计数次数也不至于多到溢出。4.5 呼吸灯效果让CCR连续变化有了固定PWM呼吸灯就是让CCR在每个周期变化一点点LED亮度跟着变化形成从亮到暗再到亮的循环。最简单的写法是在主循环里延时几毫秒改一次CCR的值。int main(void) { uint8_t dir 1; // 1表示CCR递增0表示递减 TIM3_PWM_Init(); while (1) { if (dir) { TIM3-CCR2; if (TIM3-CCR2 999) dir 0; } else { TIM3-CCR2--; if (TIM3-CCR2 1) dir 1; } for (volatile uint32_t i 0; i 100000; i); // 简单的软件延时 } }这里有个细节CCR的范围最好不要到0。CCR等于0时整个周期里CNT都大于等于CCR输出一直处于无效电平LED完全灭。CCR等于ARR加1时也就是1000CNT永远小于CCR输出一直有效LED全亮。呼吸灯到两端留一点余量看起来更自然。更进阶的写法是让定时器更新中断自动改CCR这样主循环完全解放。我后面第6章会详细说中断的配置方法这里先不展开。4.6 高级定时器TIM1/TIM8的隐藏坑MOE位如果你用的是TIM1或者TIM8输出PWM配置流程走到上面第6步会发现一个问题波形出不来。引脚电平要么一直低要么只有一丁点异常波形。不是代码抄错也不是GPIO配错而是高级定时器比通用定时器多了一个主输出使能位MOE它位于BDTR寄存器里。高级定时器的设计初衷是驱动电机为了安全输出级默认是关闭的必须软件明确打开MOE位通道才能把波形送到引脚上。很多人用通用定时器习惯了从来没有这个步骤换到高级定时器就卡住了。解决方式很简单在启动定时器前加一行TIM1-BDTR | TIM_BDTR_MOE;顺便说一句高级定时器里还有死区时间配置也在这个BDTR寄存器里。如果你只是做普通PWM输出不需要死区但MOE是无论如何都必须置1的。这个坑太经典了凡是玩过高级定时器的人十有八九都栽过这一回。5. 输入捕获让定时器反过来“听”外部信号5.1 捕获原理外部边沿给CNT拍一张快照PWM是定时器输出波形输入捕获则是另外一面定时器在外面某个引脚上监听信号边沿一旦检测到指定的上升沿或下降沿立刻把当前CNT的值“快照”到捕获寄存器CCR里然后继续计数。打个比方CNT是一个一直在跑的秒表你要测量一个人跑一圈的时间不需要手动按停。在起跑线装一个传感器人经过时秒表读数被自动记下来第二圈再经过时又记一次。两次读数之差就是一圈的时间。输入捕获干的就是这个活信号边沿到达的那个瞬间CNT的读数会自动存进CCR精确且不打断定时器的工作。这个功能用途极广。测量外部脉冲频率只需要捕获相邻两个上升沿记下两次CCR的差值就知道一个周期包含多少个计数时钟。测量PWM波的脉宽只需要捕获上升沿后把边沿改成下降沿再捕获一次差值乘以计数周期就是高电平持续时间。超声波测距用的回波时间测量本质上也一样。5.2 配置输入捕获以TIM3_CH1为例还是以F1系列为例用TIM3_CH1捕获PA6引脚上的上升沿。先看通道输入和引脚的对应关系。然后配置流程比PWM稍微多几步因为要指定捕获边沿、信号映射和滤波方式。注意和PWM配置最大的区别PWM时要配置复用推挽输出输入捕获时GPIO要配置成输入模式一般是浮空输入或者上拉输入取决于外部信号驱动能力。如果你的信号源比较弱建议开上拉。void TIM3_Capture_Init(void) { // 1. 打开时钟 RCC-APB1ENR | RCC_APB1ENR_TIM3EN; RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 2. 配置PA6为浮空输入 // PA6属于低端口第6引脚对应的4位是[27:24] // CNF01表示浮空输入MODE00表示输入模式 GPIOA-CRL ~(0xF (6 * 4)); GPIOA-CRL | (0x4 (6 * 4)); // 0x4 0100 // 3. 配置时基 // 设PSC71计数时钟1MHz即每1us计数器加1 TIM3-PSC 71; TIM3-ARR 0xFFFF; // 16位定时器最大计数值防止过早溢出 // 4. 配置通道1为输入捕获 // CC1S位于CCMR1的[1:0]01表示IC1映射到TI1 // IC1F位于[7:4]这里设为0不滤波 // IC1PSC位于[3:2]这里设为0不分频 TIM3-CCMR1 ~(0x3 0); TIM3-CCMR1 | (0x1 0); // 5. 配置捕获边沿上升沿 // CC1P位于CCER的第1位清0表示上升沿捕获 // CC1E置1使能通道1输入捕获 TIM3-CCER ~TIM_CCER_CC1P; TIM3-CCER | TIM_CCER_CC1E; // 6. 使能捕获中断和定时器通道中断线 // CC1IE位于DIER的第1位置1后发生捕获会触发中断 TIM3-DIER | TIM_DIER_CC1IE; // 7. NVIC使能TIM3全局中断 NVIC_EnableIRQ(TIM3_IRQn); // 8. 启动定时器 TIM3-CR1 | TIM_CR1_CEN; }这里关键的一步是CC1S的配置。它决定捕获通道的输入源怎么映射01表示直接映射到TI1也就是把这个定时器通道的输入引脚信号作为捕获源。如果配置成00CCR1就只是一个普通的输出比较寄存器完全不会去捕获外部信号。很多人捕获不到数据排查半天发现是漏了这一步。5.3 捕获中断里的处理读CCR算周期算频率配置完成后每次PA6引脚出现一个上升沿CCR1寄存器就会被自动写入当前CNT的值同时硬件会把SR寄存器里的CC1IF标志位置1触发捕获中断。中断服务函数里要做的事情很简单把连续的两次捕获值相减得到信号周期。拿1MHz计数时钟举例如果相邻两次上升沿的CCR差值恰好是1000说明信号一个周期占1000个计数时钟周期是1ms频率是1kHz。注意第一次捕获和第二次捕获之间如果CNT绕过了ARR发生溢出直接相减就错了这个我在下一节单独说。下面是一段常用的双捕获测频代码框架。它用两个变量保存上一次和当前的CCR值捕获两次后才计算一次周期避免刚开始只有一个数据没法做差。volatile uint16_t capt_prev 0; volatile uint16_t capt_curr 0; volatile uint32_t capt_result 0; void TIM3_IRQHandler(void) { if (TIM3-SR TIM_SR_CC1IF) { // 读取CCR1同时清除捕获中断标志 capt_prev capt_curr; capt_curr TIM3-CCR1; if (capt_curr capt_prev) { capt_result capt_curr - capt_prev; } else { // 说明计数器溢出过一次结果需要加65536 capt_result (uint32_t)capt_curr 0x10000 - capt_prev; } TIM3-SR ~TIM_SR_CC1IF; } }代码里那个if结构是在处理溢出情况如果当前捕获值比上一次还小说明CNT不是“倒回去”了而是从65535绕到了0继续走。这时候结果要加上0x10000才是真实的时间间隔。这种做法对许多信号测频场景已经足够用但如果信号周期特别长导致CNT绕了好几次就需要配合更新中断再扩展一层溢出计数器我在第五章第四节说。5.4 低频信号的坑计数器溢出必须处理用16位定时器捕获信号最大的坑就是溢出。ARR最大65535计数时钟1MHz时CNT从0跑到65535只花约65.5ms。如果你要测的信号周期大于这个时间比如一个只有几赫兹的低频脉冲那两次上升沿之间CNT可能绕了好几圈只靠两次CCR相减根本得不到正确结果。解决办法是开启更新中断。CNT每次到达ARR溢出时产生更新中断我们在中断服务函数里用一个软件变量对溢出次数加1。这样计算周期时总时间间隔 溢出次数 × 65536 当前捕获值 - 上一次捕获值。如果溢出次数本身超过65535再用32位变量继续加实际能测的频率下限会低到难以想象。不过要注意一点更新中断和捕获中断同时发生时先后顺序会影响计算。我的习惯是捕获中断里先读CCR再读溢出次数这样即使更新中断晚了一步也能拿到一个相对新一点的溢出值。更稳的做法是在临界区里短暂关闭中断读完再打开虽然会损失一点性能但保证数据一致性。5.5 滤波和噪声给信号加一道保险真实的单片机项目环境往往不像实验台那么干净外部信号可能带毛刺、抖动。如果捕获边沿检测到噪声引起的假沿测出来的频率就会忽高忽低。参考手册里ICxF这一位段就是干这个用的它叫输入滤波器。滤波的原理不复杂内部用时钟对输入信号持续采样只有连续N个采样点都读到同一个电平才认为真正的边沿到来。它本质上是“延时确认”用少量时间换取抗干扰能力。N值越大滤波越强但对窄脉冲信号的风险是可能漏掉真实的边沿所以滤波器设置要和被测信号的最小脉宽匹配。另外还有一个IC1PSC位段可以配置捕获分频。如果把捕获分频设为1/8那么每8个边沿才捕获一次。这不会改变信号本身也没法增强抗噪性但可以降低捕获中断的频率在需要连续测速时减少CPU负担。比如电机测速转一圈输出多个脉冲你没必要每次脉冲都进中断捕获分频后中断次数成倍减少。5.6 测脉宽的思路先捕获上升沿再改成下降沿测频率只要捕获同一个边沿两次就行但测脉宽需要把边沿切换一下。思路是先配置上升沿捕获等到上升沿触发中断读到了起始时间点然后在中断里立刻把CCER的CC1P位配置成下降沿捕获下一次边沿也就是下降沿到来时再触发一次中断读到的CCR值就是高电平结束时间点。两次差值乘以计数周期就是高电平脉宽。这个过程中要注意切换边沿必须在第一次捕获中断里尽快完成否则如果信号脉宽很短切换还没生效下降沿就已经过去了就会漏掉第二次捕获。更保险的做法是用两个通道同时捕获一个上升沿一个下降沿但普通开发中用单通道动态切换边沿已经够用。这个思路是许多超声波测距模块实现里的核心发超声波启动定时器收到回波读取时间距离就是时间乘声速除以2。6. 定时器中断更新中断、捕获中断怎么配合6.1 先分清中断源头更新中断和捕获中断不是一回事定时器中断看起来都叫“中断”实际上有不同的触发源头。更新中断在CNT到ARR溢出/清零时产生捕获中断在输入捕获事件发生时产生输出比较中断在CNT等于CCR时产生。它们共用一个中断号但通过SR寄存器里不同的标志位区分。很多人刚学定时器中断时会在中断服务函数里写一个if判断却不知道这个if该查哪个标志。这里给一个简单判断方法如果只是定时看更新标志UIF如果是捕获外部信号看捕获标志CC1IF、CC2IF这些。中断标志位的对应关系在SR寄存器里写得清清楚楚配置DIER时开了哪个中断源中断服务函数里就清哪个标志。有一个非常经典的低级错误中断服务函数里读到了标志也执行了业务逻辑但忘记在退出前清除标志位。结果就是中断退出后标志仍然置1硬件立刻再次触发同一次中断系统表现为死循环式地卡在中断里主循环完全不跑。你如果遇到“程序进中断出不来的情况”优先检查是不是忘了清标志。6.2 用更新中断做呼吸灯主循环解放出来前面第四章的呼吸灯是用主循环软件延时修改CCR的CPU被捆死在一个循环里。如果想在呼吸灯的同时让主循环去扫描按键、刷屏幕就得把CCR的修改搬到中断里。先把定时器的更新中断打开然后在中断服务函数里每来一次更新事件就把CCR加一或者减一。做法很简单在上述PWM初始化的基础上加上几行配置DIER的UIE位使能更新中断NVIC使能对应中断通道。中断服务函数里处理逻辑和之前主循环里几乎一样只是现在放在中断上下文。改CCR的时候有一个额外的注意事项如果CCR的预装载位没有使能你在中断里修改CCR的值是立即生效的。如果恰好此时CNT已经跑过了新的CCR值这一轮输出会立刻跳变一下到下一轮才正常。使能预装载位以后CCR的修改会等到计数器溢出才生效波形切换更平滑。呼吸灯效果要求不高可能看不出差别但做电机驱动时这种瞬态跳变可能带来电流冲击。6.3 中断服务函数里最容易犯的错别在里面干耗时的事中断服务函数的第一原则是快进快出。我见过有人在中断里写了一个软件延时函数目的是让LED闪烁慢一点结果整个系统的定时全部错乱捕获测频的误差大得离谱。原因很简单中断期间定时器的硬件还在继续计数可你的软件已经停止响应新的中断事件了。在中断里耗时等于变相缩短了主循环的运行时间还可能导致后续中断堆积。正确的做法是中断里只做必须立刻做的事。比如捕获中断里读CCR、清标志、把数据存到全局变量比如更新中断里给溢出计数器加一。这些操作都是微秒级的足够快。至于要不要根据这个数据去计算频率、更新界面、调节参数这些事情放到主循环里处理。这里还要提醒一个优先级问题。多个外设的中断如果都用同一优先级同时触发时响应顺序就不好控制了。做捕获测频这种对时间敏感的功能建议把捕获中断优先级调高不让别的中断插队。定时器中断优先级一旦配置错低频信号可能白丢一次捕获波形没丢是中断优先级把这次中断饿死了。7. 调试定时器时我常用的三板斧7.1 第一板斧串口打印寄存器值不要靠猜寄存器操作有个好处所有状态都写在寄存器里能读。遇到定时器不工作我习惯在关键节点用串口把相关寄存器的值打出来看一眼就知道问题出在哪。最先看三个数CR1的CEN位是不是1确认定时器启动了SR的更新标志是不是在周期性变化确认时钟和计数正常CNT是不是在跳确认时基单元活着。之前有个项目PWM输出死活没波形我逐个读寄存器发现CCER里CC2E是0等于通道输出根本没使能。代码里明明写了使能但被另一行初始化代码在之后又把整个CCER清零了。这种问题靠眼睛盯着代码很难发现读寄存器一眼就看到了。养成读寄存器排错的习惯比反复改参数碰运气高效得多。7.2 第二板斧把频率降到肉眼可见的范围内验证没有示波器或者逻辑分析仪的时候验证PWM最简单的方法是把频率降下来。把PWM频率调到1Hz占空比50%接一个LED你会看到LED以每秒一次的频率闪烁亮半秒灭半秒。如果调CCR后亮灭时间随之变化说明PWM逻辑是对的。再把频率调回去功能应该依然是好的。这个看起来特别土的办法实战中救了我很多次。有时候波形异常不是寄存器配置的问题而是计算出来的参数本身就是错的。把频率降到人能感知的区间相当于给定时器做了一次“低倍率检波”靠肉眼的反馈把问题定位出来。等波形肉眼看起来没问题了再调回原频率用示波器细看占空比和频率的精度。7.3 常见问题速查表先对号入座再动手下面这个表是根据我实际踩过的坑整理的出现问题时先对照现象找原因通常能省掉大半盲目排查的时间。现象可能原因解决办法PWM完全没有波形GPIO不是复用推挽模式检查CRL/CRH里的CNF位PWM完全没有波形CCER里对应通道没使能CCxE置位CCxEPWM完全没有波形用TIM1/TIM8时没置MOE在BDTR里置位MOEPWM完全没有波形RCC时钟没使能检查APB1ENR/APB2ENRPWM频率比预期大一倍APB1分频系数不是1定时器时钟是2倍没算进去按实际时钟重新算PSC和ARRPWM频率比预期小一倍ARR或PSC忘了“加1再计算”校准公式频率时钟/((PSC1)*(ARR1))占空比完全反了CCER极性位CCxP设反了把CCxP清零或置1切换运行中改ARR波形乱ARPE预装载位没开ARR改动立即生效置位CR1的ARPE运行中改PSC没反应PSC更新事件才生效触发UG位或等下一个更新事件捕获中断完全没触发CCMR里的CCxS没配置成输入映射配置CCxS为01捕获中断完全没触发NVIC没使能或者DIER中断位没开检查DIER和NVIC捕获测频偶尔偏低信号带毛刺触发了多余边沿配置ICxF进行输入滤波低频信号测不准16位计数器溢出了没处理溢出次数开启更新中断记录溢出程序卡死在中断里中断标志位没清除退出中断前清SR对应标志7.4 配合库函数定位问题的小技巧还有一个很实用的调试技巧不是让大家抛弃寄存器反而能帮你更好地理解寄存器。当你用封装好的库函数配置完定时器发现行为不对可以先不去猜库内部做了什么而是把对应的寄存器值读出来和你自己算出来的预期值做比对。库函数配置的本质也是写寄存器只是帮你封装了常用组合。读出来的寄存器值和你预期不一致那就顺着这个差值去翻库函数源码看看究竟是哪一步修改了它。这个技巧在移植别人代码时特别管用。拿到一段别人写的定时器初始化代码先跑起来看波形把PSC、ARR、CCR的实际值打出来对照你手算应该填的值。如果对上说明这段代码适合你的时钟配置如果对不上几乎可以断定对方用的主频或APB分频和你不一样。把这套思路学会你以后看任何人的定时器代码都不会慌。我个人在实际调试里的体会是定时器出错超过一半都发生在时钟树的某个分频系数和寄存器“写入值减1”这个细节上。高手的寄存器配置看起来和你没什么区别但他们对每一个数值的由来都心里有数。这章讲的内容说白了就是一个目标让你每次填PSC、ARR、CCR之前能自己把数字推算出来而不是照抄例程。做到这一步PWM、输入捕获这些功能离彻底吃透就不远了。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号