
简介配套源码包面向 STM32F103C8T6 的输入捕获实验场景适合正在学习定时器输入捕获、想通过完整工程快速上手的嵌入式开发者。源码覆盖 RCC 时钟使能、定时器模式设置、GPIO 输入通道选择、捕获中断配置以及预分频器与自动重装载值调整等关键代码并包含串口调试输出逻辑可直接编译烧录观察捕获数据。压缩包共一百三十六份文件以 c 和 h 源码、Keil 工程配置为主同时包含编译生成的 hex、axf、map 文件及少量备份文档整体大小约一点四六兆字节目录划分清楚方便按模块查找。目前已有二百五十人浏览学习尤其适合对照芯片手册验证配置流程深入理解输入捕获如何测量脉宽和频率。通过这份源码还可以学习到定时器捕获寄存器的读取方法、中断服务程序的编写思路为电机测速、PWM 解码等实际应用打下基础。1. 外部中断测脉宽会抖动输入捕获才是定时器该干的活做电机调速或者红外解码时很多人第一反应是用外部中断信号跳变沿触发中断进中断里读一个计数器或者翻转一个 IO然后用主循环算时间差。真去测 1kHz 以上的方波就会发现这套做法并不可靠——中断响应有延迟、GPIO 翻转有抖动、高优先级中断一打断脉宽数据就跳得没法看。输入捕获不一样它是定时器硬件在边沿到来那一刻直接把计数器快照到捕获寄存器软件层面零干预。这个实验包里正好是一份完整的 STM32F103C8T6 标准外设库工程用 TIM2 的通道 1 做输入捕获配合 USART1 把捕获结果打印出来工程里包含了time2_capture.c、main.c、usart1.c的源码和 Keil 工程备份文件。新手可以照着手册把寄存器配上老手则可以直接跳过初始化部分把注意力放在捕获中断里数据怎么处理这件事上。2. 输入捕获的时基结构TIM2 时钟、PSC 与 ARR 怎么配合2.1 为什么输入捕获必须有时基概念输入捕获表面上是边沿来了存个值但存下来的值本身没有意义有意义的是两个值之间的差。这个差换算成时间靠的是定时器的计数频率。计数频率又由两件事决定定时器输入时钟和预分频器 PSC。STM32F103C8T6 的系统时钟是 72MHzAPB1 总线的最高频率是 36MHz但这里有个容易踩坑的点——APB1 预分频系数不为 1 时定时器时钟是 APB1 的两倍。也就是说虽然 APB1 是 36MHzTIM2 的时钟源却是 72MHz。很多人在计算捕获分辨率时直接拿 36MHz 算结果和实际测量值差了整整一倍这个问题在调试阶段非常隐蔽。PSC 和 ARR 的分工要分清楚。PSC 决定计数频率也就是分辨率ARR 决定计数上限也就是能测多宽的信号。PSC71 时72MHz 被分频到 1MHz计数器每 1us 加 116 位计数器最大计到 65535对应约 65.5ms。这意味着单次捕获最多能测 65.5ms 的周期再宽就只能等 ARR 溢出时用更新中断来补计数。如果 PSC0计数频率 72MHz分辨率到 13.89ns但同样 16 位计数器量程只有约 910us。量程和分辨率永远是矛盾的PSC 这个参数就是用来在两者之间找平衡点的。2.2 定时器通道映射与引脚选择F103C8T6 的 TIM2 有 4 个输入捕获通道默认映射下通道 1 对应 PA0。PA0 同时也是 WKUP 引脚和 ADC 的输入通道实际使用时要注意有没有其他外设占用这个引脚。有些板子把 PA0 接了按键或者 LED直接连信号源进去会互相干扰。如果 PA0 被占用TIM2_CH1 可以通过部分重映射挪到 PA15但 PA15 默认是 JTDI 引脚重映射之前先要把 JTAG 关闭否则引脚不生效。对于实验性质的代码我一般建议直接用 PA0省去重映射那些额外配置。通道选择还牵扯到一个输入滤波的问题。TIMx_CCMR1 寄存器的 IC1F 位段是输入捕获滤波器它可以对输入信号做数字滤波原理是连续采样 N 次电平一致才算有效边沿。这个 N 值可以在 0 到 15 之间选对应不同的采样频率和采样次数。环境干扰比较大的场合比如电机驱动板附近信号线上有毛刺是常态。滤波器的采样频率和采样次数直接决定了它能滤掉多窄的毛刺采样频率越高滤掉的毛刺越宽但也会引入更大的边沿延迟影响捕获精度。2.3 从模式复位另一种测频思路常规做输入捕获是在中断里读捕获寄存器然后自己做差。但 STM32 定时器还有一个从模式功能设置 SMS100复位模式同时把 TI1FP1 作为触发源每次上升沿到来时计数器自动清零。这样 CCR1 里存的值直接就是周期不用额外做差也省掉了计数溢出的处理。这种做法的好处是主循环里直接读 CCR1 就能得到周期值非常适合低频信号的轮询测量缺点是无法同时测占空比因为计数器已经被清零了。两种方式本质上没有优劣之分取决于你要的是周期还是脉宽。中断做差适合脉宽测量从模式复位适合周期测量这个选择会影响后续代码的整体结构。3. 标准外设库实现从 GPIO 到捕获中断的完整初始化3.1 时钟使能要分清 APB1 和 APB2工程文件里出现了time2_capture.c和time2_capture.h说明作者把定时器初始化封装成了独立模块。初始化第一步是开时钟TIM2 挂在 APB1 总线上GPIOA 挂在 APB2 总线上两者用的时钟使能函数不一样。APB1 外设时钟用RCC_APB1PeriphClockCmdAPB2 外设时钟用RCC_APB2PeriphClockCmd。把 TIM2 时钟挂在 APB2 上开编译不会报错但运行起来定时器完全不动这个错误排查起来很费时间。// time2_capture.c void TIM2_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 开启 TIM2 时钟TIM2 挂在 APB1 总线上 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开启 GPIOA 时钟PA0 是 TIM2_CH1 的默认映射引脚 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0 配置为浮空输入因为外部信号源自己驱动电平 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); }这段代码里有两个细节值得注意。GPIO 的模式用的是浮空输入外部信号必须能主动拉高拉低如果信号源是开漏输出就必须加外部上拉否则边沿根本检测不到。另外输入捕获模式下 GPIO 不需要配置复用功能这一点和 PWM 输出不一样。PWM 输出要把引脚配成复用推挽输入捕获只需要普通输入模式很多人在这一步搞混导致捕获值一直是 0。3.2 时基与捕获通道参数时基初始化要设置 PSC、ARR、计数模式和对齐模式。PSC 直接决定了计数分辨率这里配置为 71 就是 1MHz 计数频率每个计数单位 1us。计数模式选择向上计数ARR 设为 0xFFFF 让计数器跑满 16 位范围。时钟分频 CKD 和重复计数寄存器 RCR 在输入捕获场景用不到保持默认值即可。// 时基配置72MHz / (711) 1MHz每 1us 计一次数 TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // 输入捕获通道 1 配置上升沿捕获不分频无滤波 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x00; TIM_ICInit(TIM2, TIM_ICInitStructure);TIM_ICSelection_DirectTI的意思是通道 1 直接映射到 TI1也就是 PA0 上的信号。如果选TIM_ICSelection_IndirectTI那就变成了交叉映射捕获的是另一个通道的信号这种配置在测量占空比时用来做一个引脚触发、另一个引脚捕获的场景。TIM_ICPSC_DIV1是不分频每次有效边沿都触发捕获。TIM_ICFilter设为 0 表示不过滤如果是电机驱动这种干扰大的环境可以把这个值调到 4 到 8 之间用几微秒的延迟换掉高频毛刺的误触发。3.3 中断服务函数里读数据捕获中断使能要走两步第一步打开 TIM2 的 CC1 中断源第二步在 NVIC 里打开 TIM2 的中断通道。TIM2 的中断通道号是 29优先级分组建议在 main 函数最开头调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)设置否则中断优先级配置不生效。// 使能捕获比较中断通道 1 对应 TIM_IT_CC1 TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); NVIC_InitStructure.NVIC_IRQChannel TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);中断服务函数写在stm32f10x_it.c里比较规范这个实验工程里应该有现成的结构。函数名必须是TIM2_IRQHandler这是启动文件里定义好的中断向量名。进中断后先判断中断标志位再读取捕获值。如果判断标志位和读取捕获值的顺序反了或者在读取之前就把标志位清掉了会出现读取到旧值的竞态问题。// stm32f10x_it.c 或 time2_capture.c 中 void TIM2_IRQHandler(void) { static uint16_t last_capture 0; uint16_t current_capture; uint32_t period; if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { // 先读取捕获值再清中断标志 current_capture TIM_GetCapture1(TIM2); TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); // 两次上升沿之间的差值就是信号周期 if (current_capture last_capture) { period current_capture - last_capture; } else { period 0xFFFF - last_capture current_capture; } last_capture current_capture; // 周期换算频率计数频率 1MHz频率 1000000 / period // 串口打印结果便于直接在终端观察 printf(Period%d us, Freq%d Hz\r\n, period, 1000000 / period); } }这段中断处理代码处理了计数器回绕的情况。如果两次捕获之间计数器从 0xFFFF 回到了 0直接用当前值减上次值得出负数所以要做分段处理。分之二和分之一久准备读值但输入捕获中断里千万不能做浮点运算1000000 / period是整数除法结果直接截断对于显示用途足够了。如果要在中断里做精度更高的浮点运算会拖慢中断响应高频信号下会丢捕获事件。串口打印方面工程里的usart1.c应该已经实现了重定向fputc到 USART1这样printf才能工作这部分在调试时非常有价值。4. 捕获值异常排查中断丢失、溢出和极性反转4.1 捕获值恒为 0先查 GPIO 再查时钟输入捕获最常见的故障是捕获寄存器一直读到 0。第一步用万用表量 PA0 引脚上的电平是否在翻转如果信号本身没有进到芯片引脚后面全部白搭。第二步查 GPIO 模式有没有配成输入之前有人把 PA0 配成了复用推挽输出信号压根进不去。第三步确认RCC_APB1PeriphClockCmd传入的是RCC_APB1Periph_TIM2如果误写成RCC_APB2Periph_TIM2TIM2 时钟没开寄存器写什么都没反应。第四步查TIM_Cmd(TIM2, ENABLE)有没有执行这个函数控制的是定时器使能位漏掉之后计数器根本不跑。还有一种特殊场景是开启了 JTAG 调试口而 PA0 刚好被复用。F103C8T6 里 PA0 默认不是调试引脚但如果芯片是 LQFP48 封装且复用了调试功能PA0 可能与调试口冲突。看实验板原理图如果 PA0 同时连了 ST-Link 的 SWIO调试器会干扰捕获信号。这时候要么换通道要么去 boot 配置里禁用调试口。4.2 极性配置反了上升沿还是下降沿TIM_ICPolarity_Rising和TIM_ICPolarity_Falling决定捕获边沿。测高电平脉宽的正规做法是通道 1 配上升沿捕获进中断后把通道 1 的极性翻转成下降沿等下降沿到来再捕获一次同一个通道交替测两个边沿。如果只配了一次上升沿那么连续两次捕获之间的差值其实是整个周期不是脉宽。有些工程为了简化用两个通道分别配上升沿和下降沿TIM2_CH1 测上升沿、TIM2_CH2 测下降沿然后两个通道的捕获值做差。这种双通道方案要注意两个通道不能同时触发否则捕获寄存器的更新会有竞争。判断极性是否正确的最快手段是观察打印值如果打印的周期值跟信号源设定的频率对不上但数值稳定大概率是边沿选错了。比如 1kHz 方波配成上升沿捕获两次捕获的间隔是一个完整周期 1000us这个值是对的。但如果配成下降沿对标准方波来说还是 1000us此时无法区分。这时候改变信号源的占空比比如从 50% 改成 30%如果捕获值变了就说明捕获边沿之间混入了不该发生的边沿事件。4.3 中断标志位没有清除导致重复进入TIM_GetITStatus返回 SET 后必须调用TIM_ClearITPendingBit清除标志位。如果漏了这一句中断标志一直为 SETCPU 会不停进入中断看起来像死循环。这种问题时中断服务函数末尾加一个 LED 翻转如果 LED 全速闪烁停不下来就是中断反复触发的典型症状。反过来如果在读取捕获值之前就清标志位理论上不会有问题因为捕获值已经锁存到 CCR 寄存器里了但在多通道捕获场景下可能会丢数据所以规范做法永远是先读值再清标志。还有一种情况是定时器更新中断也开了但服务函数只写了捕获中断的逻辑。当计数器溢出时产生 UPDATE 事件如果使能了TIM_IT_Update却没有对应处理标志位同样挂起后续中断排队堆积捕获中断会被延时执行。解决方法是要么不开更新中断要么在TIM2_IRQHandler里同时判断TIM_IT_Update并清标志。4.4 极性翻转的正确时机要在中断里动态翻转极性不能直接改写TIM_ICPolarity对应的 CCMR 寄存器位标准做法是调用TIM_OC1PolarityConfig或者直接修改CCER寄存器的 CC1P 位。常见的坑是只改了捕获极性但没重新初始化输入通道结果捕获通道失效。稳妥的顺序是先TIM_Cmd(TIM2, DISABLE)停定时器再改极性再TIM_Cmd(TIM2, ENABLE)重新开启。虽然这样做会丢失一个计数周期但比起改寄存器改出一堆隐藏 bug 要值得。5. 用回环验证捕获精度PWM 自测与量程扩展5.1 用定时器产生一个已知信号来验证验证输入捕获是否可靠最直接的办法是让另一个定时器产生标准 PWM 信号接到 PA0 上然后对比捕获测量值和设定值。这样不需要信号发生器纯靠芯片自己就能完成闭环测试。思路是TIM3_CH1 复用推挽输出 PB0配置 PWM 模式输出 100kHz、50% 占空比的方波用杜邦线把 PB0 连到 PA0输入捕获测出来的频率应该落在 99.9kHz 到 100.1kHz 之间脉宽应该在 5us 加减一个计数单位。// 用 TIM3 输出 100kHz PWM 作为校准信号源 // 72MHz / (720-1) 100kHz占空比 50% TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_Period 720 - 1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 360; // 高电平时间 360 个计数周期 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE);代码里TIM_Pulse设为 360占空比刚好是一半。跑这个回环测试的时候要注意 TIM3 的 PWM 输出频率不能太高因为输入捕获中断里还有printf操作串口打印本身耗时如果每次捕获都打印中断处理时间加上串口发送时间会超过信号周期直接导致捕获丢失。实际测试 100kHz 信号时两次捕获间隔只有 10us中断里做整数除法加串口发送远远不够必须在中断里只做简单的标志置位和值存储主循环里再统一打印。5.2 量程扩展计数溢出补偿PSC71 时 16 位计数器最大只能覆盖 65.535ms。如果需要测更低频的信号比如 1Hz 的转速脉冲计数器必然溢出。这时要开 TIM2 的更新中断在更新中断里用一个变量累加溢出次数每次捕获时把溢出次数乘以 65536 加到差值上就得到完整周期的计数值。这种方式下需要注意捕获值和溢出计数的同步问题如果捕获发生时更新中断还没来得及处理两个数值会错位。标准做法是在捕获中断里同时读取TIM_GetCounter和溢出计数变量但这需要临界区保护。低频测量的另一个思路是降低计数频率。PSC 改到 7199计数频率变成 10kHz一个计数单位 100us16 位计数器最远能覆盖 6.5535 秒大多数转速测量场景都够用了。降低计数频率牺牲的是高频信号的分辨率10kHz 计数频率下测 100kHz 信号一个周期只有 0.1 个计数单位结果几乎不可用。所以低频测量配低计数频率、高频测量配高计数频率这是一个取舍没有参数能同时兼顾。扩量程的正确做法是先根据信号的大致频率范围选 PSC把 ARR 用到接近满量程再决定是否需要溢出补偿不要一上来就无脑堆溢出代码。5.3 多通道同时捕获与应用扩展TIM2 的四个通道都可以做输入捕获而且各自有独立的捕获寄存器。这意味着可以同时测量两路或四路信号的周期和脉宽。一个典型应用是电机测速霍尔传感器 A 相接 TIM2_CH1B 相接 TIM2_CH2中断里分别记录两路信号的边沿时刻通过两个通道捕获值的先后关系判断正反转。TIM2_CH1 和 TIM2_CH2 的捕获分别对应 CCR1 和 CCR2中断标志位分别是TIM_IT_CC1和TIM_IT_CC2服务函数里要分别处理。这个实验包的代码量不大但把输入捕获的初始化、中断处理和串口输出全部打通了。建议把它作为一个基础模板保存下来后续做超声波测距、红外遥控解码、RC 接收机信号解析这些项目时直接复制time2_capture.c的初始化部分只改 GPIO 引脚和 PSC 参数就能复用。输入捕获本身不复杂真正的复杂度在对边界情况的处理比如计数器溢出、极性切换、多通道竞争、低频信号测量这些场景把这些case 想清楚定时器才算真正玩明白了。本文还有配套的精品资源点击获取