开篇
上一篇文章我们学会了定时器的核心:定时器就是计数器。预分频器 PSC、计数器 CNT、自动重装载寄存器 ARR 三者配合,数的是 APB 总线送来的内部脉冲——频率稳定,所以能精确地”数出时间”。
既然定时器本质是个计数器,那它能不能去数外面的脉冲呢?
比如传送带上贴一圈黑白条纹,让传感器扫过条纹产生方波,数一数方波的脉冲个数——数了多少个脉冲,就知道传送带走了多远。这就是今天的主题:定时器的外部时钟。
一、硬件原理:外部信号怎么进入定时器
在 STM32F103 上,每个通用/高级定时器都有四个输入通道,输入信号叫 TI1~TI4(TI3、TI4 不进入触发控制器,本篇用不到)。
1. 通道输入的”预处理”
每个输入通道前面都有两道工序:输入滤波器(滤掉微小抖动)和边缘检测器(检测上升/下降沿,输出一个脉冲)。
- 通道1 的边沿检测器输出两个信号:TI1FP1 和 TI1FP2
- 通道2 的边沿检测器输出两个信号:TI2FP1 和 TI2FP2
- 通道1 还单独输出一个 TI1ED 信号——它比较特殊,只能由双边沿触发产生(上升沿和下降沿都会产生脉冲)
其中 TI1FP1、TI2FP2、TI1ED 三个信号进入触发器(一个信号选择器),选择其中一路接入定时器的从模式控制器。
2. ETR:独立的外部触发输入
除了四个输入通道,定时器还有一个独立的引脚——ETR(外部触发输入)。ETR 在进入触发控制器前,要经过四道工序:
- 极性选择:可以把上升沿和下降沿身份互换(等效于选上升沿或下降沿触发)
- 边缘检测:检测上升沿输出脉冲
- 预分频器:因为 ETR 信号最快只能是内部时钟频率的 1/4,频率太快时可以降速
- 输入滤波器:和通道的一样,滤掉抖动
3. 两条路径:外部时钟模式1 和 模式2
用一张图总结整个结构:
- 外部时钟模式1:信号(TI1FP1 / TI2FP2 / TI1ED / ETR)经过触发器,进入从模式控制器,再驱动计数
- 外部时钟模式2:ETR 的专属特权——不经过触发器,直接接入触发控制器驱动计数
ETR 为什么独辟蹊径,自己还有一条直连路径?未来能感受到这个设计的绝妙之处(比如编码器模式、触发同步等场景)。本篇先记住:模式2 = ETR 直连,最简单直接。
二、循迹模块:红外反射光电开关
实战装置需要一个能产生脉冲的传感器。视频用的是循迹模块(也叫黑白线模块、寻迹模块),学名红外反射光电开关——就是循迹小车上探测赛道黑线的那种:
模块上有两个”小灯泡”:一个透明蓝色的负责发射红外光,一个黑色的负责接收反射回来的红外光。
两个输出引脚,一个电位器:
| 部件 | 作用 |
|---|---|
| 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)**——选中它
选中的瞬间,芯片图上 PA0 被标记为 TIM2_ETR——循迹模块 DO 引脚输出的方波,就走这条路进定时器。
这一下就直接开启了外部时钟模式2(ETR 直连触发控制器,不经过触发器)。
3. 定时器参数设置
| 设置项 | 值 | 含义 |
|---|---|---|
| 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 | 不接 | 模拟量用不到 |
五、核心代码
驱动代码沿用波特律动 OLED 库(oled.c/h + font.c/h 拖入工程),main.c 如下(改动都在 USER CODE 区域):
1 | /* USER CODE BEGIN Includes */ |
六、代码讲解
| 代码 | 作用 |
|---|---|
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 | void MX_TIM2_Init(void) |
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 | __HAL_RCC_GPIOA_CLK_ENABLE(); /* PA0 所在 GPIOA 时钟 */ |
注意 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 次”的机制能精准地放行真信号、滤掉抖动。

CubeMX 设置
在 TIM2 的 Clock Filter 填入 15(0~15 可选,每个数字对应的 F 和 N 能在参考手册中查到,绝大多数情况下直接填 15 就好):
保存生成代码,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 溢出时会做两件事:
- 置位更新标志 UIF(SR 寄存器第 0 位)
- 如果使能了更新中断,CPU 进入
TIM2_IRQHandler→ HAL 库最终调用HAL_TIM_PeriodElapsedCallback()回调
所以思路是:每溢出一次,回调里把”圈数” loop +1,读取时用公式:
1 | 总计数 = 圈数 × 每圈脉冲数 + 当前计数值 |
一圈到底几个脉冲? ARR=10 只代表 CNT 能数到的最大值是 10——CNT 的取值范围是 0
10 共 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 | ┌────────── 硬件侧 ──────────┐ ┌────────── 芯片内部 ──────────┐ ┌── 软件侧 ──┐ |
一句话串起来:黑白条纹产生方波 → 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 完全一样。✓
方式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 的意义:底层连接全由它生成,我们专注业务逻辑。
十三、实操步骤
- CubeMX 新建工程:F103C8T6,命名
TIM_ExternalInput,主频保持 8MHz - 配置 TIM2:Clock Source = ETR2,Prescaler=0,Period=10(故意调小,便于观察溢出),Clock Filter 设 15,Polarity 不翻转,Prescaler 不分频(TIM3/TIM4 没引出 ETR 引脚,只能用 TIM2)
- 打开 TIM2 更新中断:System Core → NVIC → 勾选
TIM2 global interrupt - 打开 I2C1,Project Manager → Code Generator 勾选”为每个外设生成独立 .c/.h”
- 生成代码,把波特律动 CH1116 驱动工程的
oled.c/h、font.c/h拖入工程 Src/Inc - 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); - while 循环:
OLED_NewFrame()→counter = loop*(Period+1) + __HAL_TIM_GET_COUNTER(&htim2)→sprintf拼串 →OLED_PrintString→OLED_ShowFrame()→HAL_Delay(100) - 接线:模块 DO→PA0,VCC/GND→预留电源,AO 不接
- 编译烧录,拿黑白条纹纸来回扫,观察计数(一张黑白条纹图可以去 keysking 公众号的最新资料包获取);扫满一圈(11 个脉冲)溢出后计数继续累加,说明更新中断生效
- (可选)体验模式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 脉冲实现测距计数。





