0%

STM32——定时器外部时钟:让GPIO脉冲驱动计数器,给传送带测距测速

开篇

上一篇文章我们学会了定时器的核心:定时器就是计数器。预分频器 PSC、计数器 CNT、自动重装载寄存器 ARR 三者配合,数的是 APB 总线送来的内部脉冲——频率稳定,所以能精确地”数出时间”。

既然定时器本质是个计数器,那它能不能去数外面的脉冲呢?

比如传送带上贴一圈黑白条纹,让传感器扫过条纹产生方波,数一数方波的脉冲个数——数了多少个脉冲,就知道传送带走了多远。这就是今天的主题:定时器的外部时钟。

上篇:STM32——定时器入门:定时器就是计数器

一、硬件原理:外部信号怎么进入定时器

在 STM32F103 上,每个通用/高级定时器都有四个输入通道,输入信号叫 TI1~TI4(TI3、TI4 不进入触发控制器,本篇用不到)。

1. 通道输入的”预处理”

每个输入通道前面都有两道工序:输入滤波器(滤掉微小抖动)和边缘检测器(检测上升/下降沿,输出一个脉冲)。

  • 通道1 的边沿检测器输出两个信号:TI1FP1 和 TI1FP2
  • 通道2 的边沿检测器输出两个信号:TI2FP1 和 TI2FP2
  • 通道1 还单独输出一个 TI1ED 信号——它比较特殊,只能由双边沿触发产生(上升沿和下降沿都会产生脉冲)

其中 TI1FP1、TI2FP2、TI1ED 三个信号进入触发器(一个信号选择器),选择其中一路接入定时器的从模式控制器。

2. ETR:独立的外部触发输入

除了四个输入通道,定时器还有一个独立的引脚——ETR(外部触发输入)。ETR 在进入触发控制器前,要经过四道工序:

  1. 极性选择:可以把上升沿和下降沿身份互换(等效于选上升沿或下降沿触发)
  2. 边缘检测:检测上升沿输出脉冲
  3. 预分频器:因为 ETR 信号最快只能是内部时钟频率的 1/4,频率太快时可以降速
  4. 输入滤波器:和通道的一样,滤掉抖动

3. 两条路径:外部时钟模式1 和 模式2

用一张图总结整个结构:image-20260809110017391

  • 外部时钟模式1:信号(TI1FP1 / TI2FP2 / TI1ED / ETR)经过触发器,进入从模式控制器,再驱动计数
  • 外部时钟模式2:ETR 的专属特权——不经过触发器,直接接入触发控制器驱动计数

ETR 为什么独辟蹊径,自己还有一条直连路径?未来能感受到这个设计的绝妙之处(比如编码器模式、触发同步等场景)。本篇先记住:模式2 = ETR 直连,最简单直接。

二、循迹模块:红外反射光电开关

实战装置需要一个能产生脉冲的传感器。视频用的是循迹模块(也叫黑白线模块、寻迹模块),学名红外反射光电开关——就是循迹小车上探测赛道黑线的那种:

image-20260809110124092

模块上有两个”小灯泡”:一个透明蓝色的负责发射红外光,一个黑色的负责接收反射回来的红外光。

两个输出引脚,一个电位器:

部件 作用
AO 引脚 把接收到的红外光强度以模拟量输出(本实战用不到)
DO 引脚 数字量输出:光强与阈值比较后的电平
电位器 用小螺丝刀调节检测阈值(光强超过阈值才算”有反射”)

DO 输出的逻辑(阈值已用电位器调好):

模块面前的情况 红外反射强度 模块小灯 DO 引脚
有物体反射(白色/浅色) > 阈值 🔴 亮 低电平
无物体 / 黑色物体(吸光) < 阈值 ⚫ 灭 高电平

所以:在传送带边缘铺一圈黑白间隔条纹,模块架在条纹上方——传送带一转,黑白条纹依次经过,DO 引脚就输出一条方波。数方波的脉冲个数,就能统计传送带走的距离。✓

三、CubeMX 配置

1. 新建工程(注意:保持 8MHz)

新建 STM32F103C8T6 工程,命名为 TIM_ExternalInput。

主频先不要改 72MHz,保持默认 8MHz——这是本实战的关键伏笔,原因在”踩坑”部分揭晓(与输入滤波器有关)。

2. 打开 TIM2,时钟源选 ETR

先看定时器选哪个:F103C8T6 引脚有限,TIM3、TIM4 没有引出 ETR 引脚,只有 TIM2 的 ETR 在 PA0 上。所以选 TIM2。

上篇用 TIM4 时勾选的是”内部时钟”(Internal Clock),而 **TIM2 的时钟源下拉里多了一个选项:External Trigger Source (ETR2)**——选中它

image-20260809110614652

选中的瞬间,芯片图上 PA0 被标记为 TIM2_ETR——循迹模块 DO 引脚输出的方波,就走这条路进定时器。

这一下就直接开启了外部时钟模式2(ETR 直连触发控制器,不经过触发器)。

3. 定时器参数设置

image-20260809110704705

设置项 值 含义
Prescaler(预分频) 0 不分频——我们要的是准确计数,一个脉冲都不能丢
Counter Mode Up 向上计数
Counter Period(ARR) 10 故意调小——每数 11 个脉冲就溢出一次,方便暴露”计数器溢出”问题(后面用更新中断解决)
Clock Filter 0(后面改 15) ETR 输入滤波器强度(0~15)
Clock Polarity 不翻转 ETR 极性:默认不翻转 = 上升沿计数
Clock Prescaler 不分频 ETR 信号不快,不需要降速

注意:这三个 Clock 设置是 ETR 专属的——正是上面硬件原理里说的”极性选择 + 预分频器 + 输入滤波器”。

4. 打开 TIM2 更新中断(NVIC)

溢出问题稍后会出现,这里先把配置做好:System Core → NVIC → 勾选 TIM2 global interrupt。

计数器 CNT 数到上限溢出时,硬件会置位更新标志 UIF 并触发这个中断——后面就用它在回调里”记圈数”,解决计数器溢出丢数的问题。

5. 打开 I2C1(OLED 用)

本次用 OLED 屏幕显示计数,不用串口了,打开 I2C1。

6. 为每个外设生成独立的 .c/.h

Project Manager → Code Generator → 勾选 为每个外设生成独立的 .c/.h 文件——这样 OLED 驱动代码才能 include i2c.h 拿到 I2C 操作句柄。

保存,生成代码。

四、接线

波特律动学习板的 PA0 预留给电机驱动板了,所以要用杜邦线从 PA0 引出来接模块。

循迹模块 开发板 说明
DO PA0 方波信号 → TIM2_ETR
VCC 预留 VCC 给模块供电(5V)
GND 预留 GND 必须共地!
AO 不接 模拟量用不到

image-20260809110808990

五、核心代码

驱动代码沿用波特律动 OLED 库(oled.c/h + font.c/h 拖入工程),main.c 如下(改动都在 USER CODE 区域):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "oled.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PM */
#define Period 10 // 要与在CubeMX中设置的Counter Period相同
/* USER CODE END PM */

/* USER CODE BEGIN PV */
int loop = -1; // 记录更新中断次数;初始化为-1,抵消STM32配置TIM后立即触发的一次中断
/* USER CODE END PV */

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim == &htim2) {
loop += 1; /* 计数器每溢出一次,圈数+1 */
}
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
HAL_Delay(20);
OLED_Init();
HAL_TIM_Base_Start_IT(&htim2); /* 启动定时器计数(中断模式):PA0每个上升沿CNT+1,溢出触发更新中断 */
int counter=0;
char message[20]="";
/* USER CODE END 2 */

/* Infinite loop */
while (1)
{
OLED_NewFrame();

// 总计数值 = 溢出圈数×每圈脉冲数(ARR+1) + 当前计数值
counter = loop * (Period + 1) + __HAL_TIM_GET_COUNTER(&htim2);
sprintf(message,"counter:%d",counter); /* 拼成字符串 */
OLED_PrintString(0, 0, message, &font16x16, OLED_COLOR_NORMAL); /* 写入帧缓冲 */
OLED_ShowFrame(); /* 刷屏 */
HAL_Delay(100);
}

六、代码讲解

代码 作用
OLED_Init() 初始化 OLED 屏幕(沿用波特律动驱动库,前面 I2C 篇讲过)
HAL_TIM_Base_Start_IT(&htim2) 启动定时器2(中断模式)。除了把 CEN 使能位置 1,还使能更新中断——CNT 溢出时触发回调。注意:即使时钟源是外部的,计数器本身也需要启动,否则计数器不转,读出来永远是 0
HAL_TIM_PeriodElapsedCallback 更新中断回调:CNT 数到上限溢出归零时,HAL 库自动调用它,在这里给圈数 loop +1
loop * (Period + 1) + __HAL_TIM_GET_COUNTER(&htim2) 总计数 = 圈数 × 每圈脉冲数(ARR+1)+ 当前计数值——计数器溢出后计数不丢失(详见”计数器溢出”章节)
__HAL_TIM_GET_COUNTER(&htim2) 底层宏,直接读 CNT 寄存器——每来一个 ETR 上升沿,CNT 就 +1
sprintf(message,"counter:%d",counter) 把数字拼进字符串(%d 是整数占位符)
OLED_NewFrame() / OLED_PrintString() / OLED_ShowFrame() 波特律动库的帧缓冲三件套:清帧 → 写字符 → 刷屏

代码和上篇几乎一模一样区别只是定时器句柄从 htim4 换成 htim2、显示从串口换成 OLED。这就是 HAL 库的魅力——底层时钟源怎么接是 CubeMX 的事,上层代码逻辑完全不用变。

七、解读初始化代码(CubeMX 生成)

1. MX_TIM2_Init() —— 定时器本体配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void MX_TIM2_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};

htim2.Instance = TIM2;
htim2.Init.Prescaler = 0; /* 不分频:一个脉冲都不能丢 */
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; /* 向上计数 */
htim2.Init.Period = 10; /* ARR = 10:故意调小便于观察溢出 */
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
{
Error_Handler();
}

/* 外部时钟源配置:关键! */
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_ETRMODE2; /* 外部时钟模式2 */
sClockSourceConfig.ClockPolarity = TIM_CLOCKPOLARITY_NONINVERTED; /* 极性不翻转 = 上升沿计数 */
sClockSourceConfig.ClockPrescaler = TIM_CLOCKPRESCALER_DIV1; /* ETR 不分频 */
sClockSourceConfig.ClockFilter = 0; /* 滤波器(改成 15 后这里会变成 0x0F) */
if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
...
}

HAL_TIM_ConfigClockSource() 在外部时钟模式2 时,本质是写 SMCR(从模式控制)寄存器:

代码参数 对应 SMCR 位 含义
TIM_CLOCKSOURCE_ETRMODE2 ECE=1 使能 ETR 外部时钟(ETR 直连计数)
ClockPolarity = NONINVERTED ETP=0 极性不翻转,上升沿计数
ClockPrescaler = DIV1 ETPS=00 ETR 不分频
ClockFilter = 15 ETF=1111 采样滤波强度

2. HAL_TIM_Base_MspInit() —— 引脚配置

1
2
3
4
5
__HAL_RCC_GPIOA_CLK_ENABLE();        /* PA0 所在 GPIOA 时钟 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; /* PA0 作为输入 */
GPIO_InitStruct.Pull = GPIO_NOPULL; /* 外部信号驱动,不需要上下拉 */
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

注意 PA0 配置的是普通输入模式——F1 的定时器复用输入不需要配置成 AF 复用模式,外设直接读取引脚电平,所以普通输入即可(外部中断篇里配置 EXTI 时才需要特殊模式)。

3. 配置对应关系总表

CubeMX 设置 生成的代码 底层寄存器 本实战的值
时钟源 = ETR2 ClockSource = ETRMODE2 SMCR.ECE 模式2 直连
Clock Polarity ClockPolarity = NONINVERTED SMCR.ETP 上升沿计数
Clock Prescaler ClockPrescaler = DIV1 SMCR.ETPS 不分频
Clock Filter ClockFilter = 0x0F SMCR.ETF 15(抗抖)
引脚 PA0 MSP 里 GPIO 输入 MODE/CNF 寄存器 普通输入、无上下拉

八、效果展示

效果视频:

https://www.bilibili.com/video/BV1j7u26SE3b/?vd_source=5a111e8d49f70e532d36ddb73be3d0aa

九、实战测试 + 踩坑:抖动脉冲 → 输入滤波器

现象

编译烧录,拿起循迹模块随便动动,屏幕上的数字快速增加——但移动一格,数字就增加几十甚至上百!幅度离谱。

原因

循迹模块经过黑白分隔边缘时,由于各种原因(传感器响应抖动、反射不稳定等)会产生非常多的抖动脉冲。这些抖动脉冲的上升沿同样会被定时器计数——数一个黑白格,实际上数了几十上百个脉冲。

解决方案:输入滤波器

还记得原理图里那个一直没细讲的输入滤波器吗?STM32 早就准备好了应对方案:

  • 滤波器按照一定的采样频率 F 去检测每个经过的脉冲
  • 如果连续 N 次检测后脉冲还没有结束 → 判定为有效脉冲,输出给定时器
  • 如果 N 次中某一次发现脉冲已结束 → 说明脉宽太窄,是抖动,直接忽略

抖动脉冲宽度远窄于真正的脉冲信号,所以这个”确认 N 次”的机制能精准地放行真信号、滤掉抖动。image-20260809111015897

image-20260809111055809

CubeMX 设置

在 TIM2 的 Clock Filter 填入 15(0~15 可选,每个数字对应的 F 和 N 能在参考手册中查到,绝大多数情况下直接填 15 就好):

image-20260809111204556

保存生成代码,main.c 一行都不用改,重新编译烧录——再扫黑白条纹,一格一个数,完美。✓

为什么主频要保持在 8MHz?(伏笔揭晓)

从手册可以看出,采样频率 F 与内部时钟有关——就是 APB 总线送来的那个内部时钟:

  • 内部时钟越快 → F 越大 → 能过滤的脉宽上限变窄 → 很多抖动脉冲会逃过滤波器的”审判”
  • 这就是为什么本实战刻意不改 72MHz 主频的原因:8MHz 下滤波器能滤掉的抖动更宽,效果更好

如果你确实想要 72MHz 主频,也不是不行:可以调节 APB 预分频器,让输入到定时器的时钟频率不要那么快(滤波器的采样频率 F 跟着降下来),抖动就又能被滤掉了。

十、计数器溢出:用更新中断解决

现象

抖动脉冲被滤波器干掉后,一格一个数,计数很准——但注意:Counter Period 我们故意设成了 10。每数 11 个脉冲,CNT 就从 10 溢出归零,OLED 上的数字在 0~10 之间循环,数到上限就再也上不去了。

__HAL_TIM_GET_COUNTER 只读 CNT 的当前值,CNT 溢出归零后,溢出的部分直接丢了。

思路:更新中断记圈数

定时器在 CNT 溢出时会做两件事:

  1. 置位更新标志 UIF(SR 寄存器第 0 位)
  2. 如果使能了更新中断,CPU 进入 TIM2_IRQHandler → HAL 库最终调用 HAL_TIM_PeriodElapsedCallback() 回调

所以思路是:每溢出一次,回调里把”圈数” loop +1,读取时用公式:

1
2
3
总计数 = 圈数 × 每圈脉冲数 + 当前计数值
= loop × (Period + 1) + CNT
= loop × 11 + CNT

一圈到底几个脉冲? ARR=10 只代表 CNT 能数到的最大值是 10——CNT 的取值范围是 010 共 11 个状态。数到 10(第 10 个脉冲)时还没溢出,第 11 个脉冲到来才溢出归零并触发更新中断。就像 09 的十进制计数器进一次位需要 10 个脉冲一样,0~10 的计数器进一次位需要 11 个。

keysking 视频里用的是简化公式 loop × Period + CNT(Period=10),每圈会少算 1 个脉冲;本篇统一使用严格版 loop × (Period + 1) + CNT。

圈数不丢,计数就不丢——上限从”一个 ARR”变成”loop 个 ARR”,随便数。

为什么 loop 初始化为 -1? 回看第五章的代码:STM32 在初始化定时器(MX_TIM2_Init)时,更新标志 UIF 会被意外置位——第一次使能中断后,这个”残留”的更新事件会立刻触发一次回调。loop 初始化为 -1 正好把这多出来的 1 次抵消掉,从 0 开始记。

效果

编译烧录再扫条纹:计数突破 10 继续累加,不再循环——计数上限从 10 扩展到了任意大。这也是后面做电机测速、编码器计数的基础。✓

十一、数据流全链路

从黑白条纹到屏幕上的数字,数据一路是这样走的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌────────── 硬件侧 ──────────┐          ┌────────── 芯片内部 ──────────┐          ┌── 软件侧 ──┐
黑白条纹 → 红外光反射 → 接收管 → 光强阈值比较(电位器)→ DO 电平跳变
│
PA0 引脚 ──► ETR 输入 ──► 极性选择(不翻转) ──► 输入滤波器(15次确认) ──► 边沿检测(上升沿)
│
│ 每检测到一个有效上升沿,CNT+1
▼
TIM2 计数器 CNT
│ │
CNT溢出(数到上限)│ │ 主循环读取
▼ ▼
更新中断 → 回调 loop += 1 __HAL_TIM_GET_COUNTER(&htim2) 读 CNT
│ │
└──► 总计数 = loop×(Period+1) + CNT
│
sprintf 拼接成字符串 "counter:123"
│
OLED_PrintString 写入帧缓冲
│
OLED_ShowFrame 通过 I2C1 刷屏
▼
OLED 屏幕显示数字

一句话串起来:黑白条纹产生方波 → ETR 引脚进芯片 → 滤波+边沿检测 → CNT 逐脉冲递增 → 溢出时更新中断记圈数 → 软件读 CNT+圈数 → 拼字符串 → 刷 OLED。

十二、不同的配置方式

前面全程用的是模式2(ETR 直连)。但也可以配置成模式1——信号经过触发器再进从模式控制器。下面把各种配置方式全部列举。

方式1:外部时钟模式2(本篇主力)

项 配置
CubeMX 操作 TIM2 的 Clock Source 选 ETR2(时钟源选项)
信号路径 ETR 直连触发控制器
引脚 PA0
触发方式 上升沿(极性不翻转)

方式2:外部时钟模式1 + ETR

项 配置
CubeMX 操作 ① Clock Source 改回 Disable(关掉时钟源选项)② Slave Mode 选 External Clock Mode 1(从模式)③ Trigger Source 选 ETR
信号路径 ETR → 触发器 → 从模式控制器
引脚 还是 PA0(不用换线)
触发方式 上升沿

注意:选模式1 后,下面的 Clock 设置变成了 Trigger 设置,但三个选项(Filter / Polarity / Prescaler)一模一样,把 Filter 设回 15 即可。

代码完全不用改动——模式1/模式2 只是底层硬件连接的差异,CubeMX 生成的 MX_TIM2_Init 会自己变化,main.c 逻辑不变。编译下载,效果和模式2 完全一样。✓

image-20260809111702799

方式3:外部时钟模式1 + TI1ED(双边沿)

项 配置
CubeMX 操作 Slave Mode = External Clock Mode 1,Trigger Source 选 TI1ED
信号路径 TI1 通道 → 边沿检测(双边沿)→ 触发器 → 从模式控制器
引脚 还是 PA0(TI1 通道1 也在 PA0,不用换线!)
触发方式 双边沿(上升沿 + 下降沿都计数)

TI1ED 只能双边沿触发,所以 Trigger 设置里只剩滤波器一个选项(没有极性、没有预分频),设回 15。

注意效果差异:ETR 只数上升沿(白→黑计一次),TI1ED 双边沿(白→黑计一次、黑→白再计一次)——同样一段距离,计数是 ETR 方式的两倍!

方式4:外部时钟模式1 + TI2FP2(可自定义边沿)

项 配置
CubeMX 操作 Slave Mode = External Clock Mode 1,Trigger Source 选 TI2FP2
信号路径 TI2 通道2 → 滤波 + 边沿检测 → 触发器 → 从模式控制器
引脚 PA1(TI2 通道2 的引脚,和 PA0 不是同一个!需要换杜邦线)
触发方式 上升沿 / 下降沿 / 双边沿 三选一(比 TI1ED 灵活)

TI2FP2 是”边缘可自定义”版本,Trigger 设置里多了一个边沿选择选项。

方式5:外部时钟模式1 + TI1FP1

与 TI2FP2 类似,通道1 输出、边缘可自定义。视频留给观众自己尝试——配置方法照葫芦画瓢:Slave Mode = 模式1,Trigger Source = TI1FP1。

所有路径对比总表

触发路径 引脚 触发方式 可配置项 特点
ETR(模式2直连) PA0 上升沿(极性可翻) 滤波/极性/预分频 最直接,本篇主力
ETR(模式1经触发器) PA0 上升沿(极性可翻) 滤波/极性/预分频 效果同模式2,路径不同
TI1ED PA0 双边沿(固定) 仅滤波 黑白各计一次,计数翻倍
TI1FP1 PA0 上升/下降/双边沿 滤波/边沿 和 TI2FP2 类似,留作练习
TI2FP2 PA1 上升/下降/双边沿 滤波/边沿 需换线到 PA1,边缘最灵活

共同点:无论选哪条路径,main.c 的代码都一行不用改——这就是 CubeMX 的意义:底层连接全由它生成,我们专注业务逻辑。

十三、实操步骤

  1. CubeMX 新建工程:F103C8T6,命名 TIM_ExternalInput,主频保持 8MHz
  2. 配置 TIM2:Clock Source = ETR2,Prescaler=0,Period=10(故意调小,便于观察溢出),Clock Filter 设 15,Polarity 不翻转,Prescaler 不分频(TIM3/TIM4 没引出 ETR 引脚,只能用 TIM2)
  3. 打开 TIM2 更新中断:System Core → NVIC → 勾选 TIM2 global interrupt
  4. 打开 I2C1,Project Manager → Code Generator 勾选”为每个外设生成独立 .c/.h”
  5. 生成代码,把波特律动 CH1116 驱动工程的 oled.c/h、font.c/h 拖入工程 Src/Inc
  6. main.c:include 段加 oled.h + stdio.h;PM 区加 #define Period 10;PV 区加 int loop = -1;;0 区写更新中断回调;USER CODE 2 写 HAL_Delay(20); OLED_Init(); HAL_TIM_Base_Start_IT(&htim2);
  7. while 循环:OLED_NewFrame() → counter = loop*(Period+1) + __HAL_TIM_GET_COUNTER(&htim2) → sprintf 拼串 → OLED_PrintString → OLED_ShowFrame() → HAL_Delay(100)
  8. 接线:模块 DO→PA0,VCC/GND→预留电源,AO 不接
  9. 编译烧录,拿黑白条纹纸来回扫,观察计数(一张黑白条纹图可以去 keysking 公众号的最新资料包获取);扫满一圈(11 个脉冲)溢出后计数继续累加,说明更新中断生效
  10. (可选)体验模式1:把 Slave Mode 改为 External Clock Mode 1,Trigger Source 依次换成 ETR / TI1ED / TI2FP2,对比效果差异

十四、总结

新知识点 一句话
外部时钟模式2 ETR 引脚信号直连触发控制器驱动计数(CubeMX:时钟源选 ETR2)
外部时钟模式1 信号经触发器 + 从模式控制器再驱动计数(CubeMX:从模式设置)
ETR 引脚 TIM2 的 ETR 在 PA0;TIM3/TIM4 没引出
输入滤波器 采样 F 次确认脉冲有效,滤掉抖动;CubeMX Clock Filter 填 15
边缘检测器 上升沿/下降沿/双边沿输出脉冲
TI1ED 只能双边沿,黑白交界计两次 → 计数翻倍
更新中断(UIF) CNT 溢出时置位 UIF,触发 HAL_TIM_PeriodElapsedCallback 回调
loop 记圈数 每溢出一次 loop+1,总计数 = loop×(Period+1) + CNT,计数器溢出不再丢数
主频与滤波 滤波采样频率依赖内部时钟,8MHz 下滤抖效果更好

对比上篇:上篇数内部 APB 时钟脉冲实现定时;这篇数外部 GPIO 脉冲实现测距计数。

参考资源