0%

STM32——定时器从模式:复位、门控、触发,三种方式控制定时器

开篇

上篇让 TIM2 数外部脉冲,走的是 ETR 直连的外部时钟模式二(结尾顺带介绍了从模式下的模式一)。除了外部时钟模式一,从模式还有另外三个选项:

从模式选项 作用
复位模式 触发信号让定时器复位
门控模式 电平控制定时器跑/停
触发模式 触发信号启动定时器

外部时钟决定”数什么”,这三个模式决定”怎么数”。下面分开讲。

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

一、复位模式:触发信号让计数器清零重来

1. 原理

① 触发信号怎么走:本篇走 TI1 通道

上篇的 PA0 走 ETR 直连(外部时钟模式二)。这篇把 PA0 换回输入通道 TI1 的身份,走的是上篇末尾”不同的配置方式”里介绍过的那条路——TI1FP1:

1
PA0(TI1) → 输入滤波 → 边沿检测 → TI1FP1 → 触发器 → 从模式控制器

和 ETR 直连相比,这条路径多经过一道触发器做信号选择;更关键的区别在从模式控制器收到触发脉冲后干什么——上篇是”把它当计数脉冲数一下”,这次是”执行一次复位”:image-20260817135100512

1
2
3
4
5
PA0 ──► [输入滤波×15] ──► [边沿检测:上升沿] ──► TI1FP1 ──► [触发器] ──► 从模式控制器
│
从模式 = 复位模式
▼
CNT 立刻清零 + 产生更新事件

② 复位做了什么:相当于触发了一次更新事件

触发沿一到,从模式控制器执行的动作,相当于定时器自己触发了一次更新事件——和数满 ARR 自动重载产生的是同一个事件。这次更新带来三件事:

  1. CNT 立刻归零——不管它刚才数到哪(比如 6789),直接回到 0 重新开始
  2. PSC、ARR 的新值立刻生效——运行中改了预分频或重载值,触发瞬间就生效,不用等这一轮数完
  3. 产生更新事件——开了更新中断,就会进 HAL_TIM_PeriodElapsedCallback

注意第 3 点:复位同样会进更新中断,回调里并不知道这次是”复位”还是”自动重载”带来的——这正是第二章 TRIGGER 标志区分法的由来。

③ 和自动重载的本质区别:归零的时机不同

自动重载 复位模式
谁发起 定时器自己(CNT 数到 ARR) 外部触发信号
什么时刻 时间固定,一轮接一轮 说不准,信号一到就复位
PSC/ARR 生效时 数满这一轮 任何一次触发瞬间

那有了自动重载,为什么还需要复位模式?差别在于归零的时机:

  • 自动重载:清零跟着时间走——CNT 数满 ARR 就归零,跟传送带转到哪没关系
  • 复位模式:清零跟着物理位置走——转够一整圈才清零,清零的瞬间正好对应”走完一圈”

还是传送带测距这个场景,两种做法对比:

只用自动重载 复位模式
什么时候清零 数满 10000 下就清零(时间固定),圈可能还没转完 每转一整圈、触发点扫到传感器时
读到的 CNT 是什么 说不清——可能是圈中某个位置,也可能已经跨圈 “这一圈从起点走了多远”

2. CubeMX 配置

新建工程 Tim_SlaveMode,TIM2 配置如下(与上篇的差别:时钟源改回内部、从模式改复位):

设置项 值 说明
Clock Source Internal Clock 不做外部计数,改回内部 8MHz 时钟
Slave Mode Reset Mode(复位模式) /
Trigger Source TI1FP1 PA0 上的 TI1,滤波后输出 TI1FP1 进触发器
Trigger Polarity Rising(上升沿) 上升沿触发复位
Trigger Filter 15 沿用上篇经验,滤掉循迹模块抖动
Prescaler 8000-1 8MHz÷8000=1kHz,1 毫秒计数一次
Counter Period 10000-1 数 10000 下,10 秒自动重载一次
Counter Mode Up 从 0 数到 9999
NVIC → TIM2 global interrupt 勾选 开更新中断,观察自动重载

image-20260817114230539

串口用阻塞发送 HAL_UART_Transmit 就够——延时要求不严,不需要中断或 DMA。

3. 代码:复位模式

CubeMX 生成后,MX_TIM2_Init 自带 sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET。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
/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdio.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
char updataMsg[] = "自动重装载"; /* 自动重载提示 */
char triggerMsg[] = "从模式触发"; /* 复位触发提示 */
/* USER CODE END PV */

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim2)
{
/* 触发器标志置位了:说明这次更新是外部复位带进来的 */
if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_TRIGGER) == SET)
{
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_TRIGGER); /* 手动清掉,留着会误判 */
HAL_UART_Transmit(&huart2, (uint8_t*)triggerMsg, strlen(triggerMsg), 100); /* 从模式触发 */
}
else
{
HAL_UART_Transmit(&huart2, (uint8_t*)updataMsg, strlen(updataMsg), 100); /* 自动重载 */
}
}
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2); /* 启动定时器(中断模式) */
int counter = 0;
char message[20] = "";
/* USER CODE END 2 */

主循环:

1
2
3
4
5
6
7
while (1)
{
counter = __HAL_TIM_GET_COUNTER(&htim2); /* 读 CNT 当前值 */
sprintf(message, "counter:%d", counter); /* 拼成字符串 */
HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), 100);
HAL_Delay(2000); /* 每 2 秒印一次 */
}
代码 作用
HAL_TIM_Base_Start_IT(&htim2) 置 CEN 并开更新中断
__HAL_TIM_GET_COUNTER(&htim2) 读 CNT 寄存器的值
HAL_UART_Transmit 阻塞发字符串,串口 115200

二、怎么区分”复位触发”和”自动重载”?

两种事件都会触发更新中断,代码里怎么分辨?看定时器的触发器中断标志位:

标志位 宏定义 谁置位 谁清
更新事件标志 TIM_FLAG_UPDATE 自动重载 / 复位 HAL 中断里自动清
触发器标志 TIM_FLAG_TRIGGER 从模式控制器收到触发信号 HAL 不清,要手动清

关键在最后一行:HAL 库只清更新标志,触发器标志得自己清。所以更新中断里先读 TRIGGER:置位说明本次是外部触发(复位模式),没置位就是自然重载。

上面代码里的那个 if 拆开看就是这两步:

1
2
3
4
5
6
7
8
9
10
11
/* ① 触发器标志置位了,说明这次更新是外部复位带进来的 */
if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_TRIGGER) == SET)
{
/* ② 先清掉标志(不清下次会误判),再打印"从模式触发" */
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_TRIGGER);
HAL_UART_Transmit(&huart2, (uint8_t*)triggerMsg, strlen(triggerMsg), 100);
}
else
{
HAL_UART_Transmit(&huart2, (uint8_t*)updataMsg, strlen(updataMsg), 100); /* 触发器标志没有置位,自然重载 */
}
HAL 宏/函数 作用
__HAL_TIM_GET_FLAG(htim, TIM_FLAG_TRIGGER) 读触发器标志是否置位,SET 表示置位(为 1)
__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_TRIGGER) 清掉这个标志;不清的话它一直留着,下次会误判

TRIGGER 读完立刻清,否则下一次自动重载进来,标志还是置位状态,会被误判。

三、初始化代码解读

三种模式在代码里的差别,只有从模式那一行的取值(必要时再勾单脉冲)。下面这段代码是触发模式的配置——触发模式 + 单脉冲,如果是复位或者门控模式,只需要变下面提到的一行代码

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
static void MX_TIM2_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_SlaveConfigTypeDef sSlaveConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};

htim2.Instance = TIM2;
htim2.Init.Prescaler = 8000-1; /* PSC:8MHz÷8000=1kHz */
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; /* 向上计数 */
htim2.Init.Period = 10000-1; /* ARR=9999: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_INTERNAL; /* 内部时钟(上篇是 ETR) */
if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) { Error_Handler(); }

if (HAL_TIM_OnePulse_Init(&htim2, TIM_OPMODE_SINGLE) != HAL_OK) { Error_Handler(); } /* 单脉冲:数满一轮自动停 */

sSlaveConfig.SlaveMode = TIM_SLAVEMODE_TRIGGER; /* 从模式 = 触发模式(CubeMX 的 Trigger Mode) */
sSlaveConfig.InputTrigger = TIM_TS_TI1FP1; /* 触发源 = TI1FP1(PA0) */
sSlaveConfig.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING; /* 上升沿触发 */
sSlaveConfig.TriggerFilter = 15; /* 输入滤波器 15 档,滤掉循迹模块抖动 */
if (HAL_TIM_SlaveConfigSynchro(&htim2, &sSlaveConfig) != HAL_OK) { Error_Handler(); }

sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; /* 主模式默认:更新事件输出 TRGO,本工程用不到 */
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) { Error_Handler(); }
}

MX_TIM2_Init 里的配置,对应到寄存器的关键位如下:

代码字段 对应寄存器的位 含义
HAL_TIM_OnePulse_Init(TIM_OPMODE_SINGLE) CR1.OPM=1 单脉冲模式:数满一轮自动停,等下一次触发
TIM_SLAVEMODE_TRIGGER SMCR.SMS[2:0]=100 从模式:触发(检测到沿 → 启动计数)
InputTrigger = TI1FP1 SMCR.TS[4:0] 触发信号选 TI1 滤波后的输出
TriggerPolarity CCER.CC1P 上升沿有效
TriggerFilter=15 SMCR.TF[3:0] 采样 15 次确认(滤抖动)
TIM_TRGO_RESET CR2.MMS[2:0]=000 主模式默认:更新事件时 TRGO 复位,本工程用不到

复位、门控、触发三种模式,CubeMX 里只改一个下拉,落到寄存器上就是 SMCR 的 SMS[2:0] 三位:

CubeMX 的 Slave Mode 代码里的宏 SMS[2:0] 行为
Reset Mode(复位) TIM_SLAVEMODE_RESET 010 触发信号清零重数
Gated Mode(门控) TIM_SLAVEMODE_GATED 011 电平开门计数、关门暂停
Trigger Mode(触发) TIM_SLAVEMODE_TRIGGER 100 触发边沿启动计数
(触发时另勾 One-Pulse) TIM_OPMODE_SINGLE CR1.OPM=1 数满一轮自动停止

所以三种模式只是 CubeMX 改一个下拉 + 必要时勾单脉冲,生成代码自动对应上述宏,上层代码几乎不用动。

四、门控模式:高电平就数,低电平就停

1. 原理

① “门”是什么:用电平控制计数器的时钟

前面学过的复位、外部计数里,触发信号都是一闪而过的沿;门控模式不一样,它把触发信号看成一把持续的电平闸门,直接控制计数器的时钟通断:

1
2
3
4
5
6
7
触发信号(持续电平)──► 从模式控制器(门控模式)
│
┌────────────┴────────────┐
▼ ▼
高电平 = 门打开 低电平 = 门关上
内部时钟送进计数器 时钟被掐断
CNT 照常 +1 CNT 停在原值(不归零、不清零)

门打开时计数器照常数,门关上时计数暂停——CNT 拿住当前值一动不动,等门再开,接着数。所以门控的效果是暂停/继续,跟复位那种”清零重来”是两码事。

② 门沿:置位 TRIGGER,但不产生更新事件

开关门的一瞬间(电平跳变沿),从模式控制器同样会置位 TRIGGER 标志——但它不做复位、不产生更新事件(更新事件只在计数溢出时才有)。

所以门控模式下,在更新中断里判断 TRIGGER 没有意义:门沿根本不触发更新中断,回调只在计数溢出时进来;而门沿留下的 TRIGGER 标志,又可能被下一次溢出”带进”回调,造成误判。要判断门的开关,得在主循环里轮询 TRIGGER(见下节代码)。

③ 极性反转

CubeMX 里如果把 Trigger Polarity 改成 Falling(下降沿有效),高低电平对门的控制就反过来:低电平 = 开门计数,高电平 = 关门暂停。

④ 能干什么

门控最有用的场景是测信号的时间宽度:门开多久,计数器就数多久——门的开启时间 = 计数增量 × 计数周期(本篇 1ms),由此反推出一个电平持续了多久。也常用于”条件计数”:只有某个条件满足(门开)时才累计。

2. 配置

CubeMX 只改一处:Slave Mode 从 “Reset Mode” 改成 **”Gated Mode(门控)”**,其余不变。

image-20260817153025816

3. 代码:门控模式

代码改动就一处:TRIGGER 判断从更新中断挪到主循环。原因上一节说了——门沿只置标志、不触发更新中断,放主循环轮询,门沿后最多 2 秒就能抓到,和自动重载互不干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 更新中断只发"自动重载"提示,TRIGGER 判断移到主循环 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim2)
{
HAL_UART_Transmit(&huart2, (uint8_t*)updataMsg, strlen(updataMsg), 100);
}
}

while (1)
{
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_TRIGGER) == SET)
{
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_TRIGGER); /* 门沿到了,清标志 */
HAL_UART_Transmit(&huart2, (uint8_t*)triggerMsg, strlen(triggerMsg), 100);
}
counter = __HAL_TIM_GET_COUNTER(&htim2);
sprintf(message, "counter:%d", counter);
HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), 100);
HAL_Delay(2000);
}

五、触发模式:边沿一到才开始数

1. 原理

① 触发模式:只负责启动,不负责停止

触发模式只管一件事:检测到触发沿,让定时器开始计数。触发沿过去之后,定时器自己接着数,不需要持续控制:

1
2
3
4
5
6
7
8
触发信号 ──► 从模式控制器(触发模式)
│
沿一到 → CEN 置 1 → 计数器开跑
│
┌───────┴───────┐
▼ ▼
触发只负责启动 数满一轮自动重载,继续循环
(再来沿也不停) (CEN 保持 1,不会自己停)

所以触发模式只启动、不停止。配置好后即使调了 HAL_TIM_Base_Start_IT,计数器也不会自己跑起来——CNT 停在 0,直到触发沿到来。串口一直打印 counter:0,就是还没触发的状态。

② 单脉冲模式:数满一轮自动停

光”启动”还不够,很多场景要”触发一次、数一轮、就停”。这时勾上 One-Pulse Mode(单脉冲模式),硬件的动作变成:

1
2
3
触发沿 → CEN=1 → 开始计数 → 数满 ARR → 更新事件 + 硬件自动清 CEN → 停止
▲ │
└──────── 下一次触发沿 → CEN=1 → 再数一轮 → 再自动停 ◄────────┘

单脉冲模式下,计数器数满溢出、产生更新事件的同时,硬件自动把 CEN 清零,定时器随之停止,等下一次触发沿再来。

常规模式 单脉冲模式
数满之后 自动重载,接着循环 自动停止,等下一次触发
应用 连续计数 “一次触发、一轮计数”

③ 能干什么

触发模式最典型的场景:外部事件来了才开始计时。比如光电开关扫到一下 → 定时器启动 → 数满一轮(固定时长)→ 自动停;再来一下再数一轮,每轮之间互不干扰。配合单脉冲,就是一个触发脉冲 → 自动数一轮 → 自动停。

2. 配置

CubeMX 改两处:

设置项 值
Slave Mode Trigger Mode(触发模式)
One-Pulse Mode 勾选(Single Pulse)

image-20260817153200760

这个配置下,定时器默认不会自己启动计数——HAL_TIM_Base_Start_IT 打开的是”等待触发”的状态,没有触发信号时 CNT 一直停在 0。

3. 代码:触发模式 + 单脉冲

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
/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim2)
{
HAL_UART_Transmit(&huart2, (uint8_t*)updataMsg, strlen(updataMsg), 100);
}
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2); /* 启动等待触发:没触发时 CNT 是 0 */
int counter = 0;
char message[20] = "";
/* USER CODE END 2 */

while (1)
{
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_TRIGGER) == SET)
{
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_TRIGGER);
HAL_UART_Transmit(&huart2, (uint8_t*)triggerMsg, strlen(triggerMsg), 100);
}
counter = __HAL_TIM_GET_COUNTER(&htim2);
sprintf(message, "counter:%d", counter);
HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), 100);
HAL_Delay(2000);
}

六、三种模式对比总表

复位模式 门控模式 触发模式
干的事 触发 = 清零重来 门开就数、门关就停 触发 = 开跑(配单脉冲:跑完一轮停)
对计数器 复位到 0 暂停/继续(不丢) 启动计数
门沿 / 触发沿 产生更新事件 只置 TRIGGER,无更新事件 只置 TRIGGER,无更新事件
计数溢出 有更新事件 有更新事件 有更新事件
TRIGGER 标志 复位时置位 门沿置位 触发沿置位
实用场景 每圈归零测距 选通计数、测门时间 触发后一轮计时

怎么选:归零用复位;暂停/继续用门控;触发后跑一轮用触发 + 单脉冲。

七、数据流全链路

以触发模式为例,信号从光电开关一路走到串口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
光电开关 DO ──► PA0(TI1) ──► 输入滤波(15次) ──► 上升沿检测
│
触发脉冲 TI1FP1 进触发器(三选一:TI1FP1/TI2FP2/TI1ED)
│
┌─────────────────────┴────────────────────┐
▼ ▼
从模式控制器(触发模式): 置位 TRIGGER 标志
检测到沿 → 启动 CEN │
(单脉冲:一轮后自动停) 主循环轮询 → 打印"从模式触发"
│
内部时钟 8MHz → PSC 分频 1kHz → CNT 每毫秒 +1
│
▼
CNT 数到 9999 溢出 → 更新事件 → 更新中断
│
▼
HAL_TIM_PeriodElapsedCallback 打印"自动重装载"
│
主循环每 2 秒读 CNT(__HAL_TIM_GET_COUNTER)
│
▼
sprintf → USART2 串口(115200)显示

八、上电时多出的第一次”自动重装载”

烧录后上电,还没到 10 秒周期,串口往往先打出一条”自动重装载”。原因是:MX_TIM2_Init 初始化时会把更新标志置位(和上篇 loop 初始化为 -1 抵消的是同一件事),中断一开,CPU 立刻响应一次更新中断。解决:启动前先清一次更新标志。

1
2
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);   /* 启动前清一次更新标志 */
HAL_TIM_Base_Start_IT(&htim2);

清标志有两套写法,效果一样——都是清同一个更新标志:

  • HAL_TIM_ClearFlag(&htim2, TIM_FLAG_UPDATE)
  • HAL_TIM_ClearIT(&htim2, TIM_IT_UPDATE)

清这一次之后重启,就不会再多打一条。

九、效果展示

https://www.bilibili.com/video/BV19Db86DEvk?vd_source=5a111e8d49f70e532d36ddb73be3d0aa&p=2&spm_id_from=333.788.videopod.episodes

十、实操步骤

  1. CubeMX 新建工程 Tim_SlaveMode,主频保持 8MHz(理由见上篇滤波章节)
  2. TIM2:Clock = Internal Clock、PSC=8000-1、Period=10000-1、向上计数
  3. Slave Mode = Reset Mode、Trigger = TI1FP1、极性 Rising、Filter=15
  4. 勾选 TIM2 global interrupt(NVIC);USART2 115200
  5. 生成代码,补齐 main.c 的 include / PV / 回调 / 启动 / 主循环(见上文)
  6. 编译烧录,接串口线(USART2 对应调试口),波特率 115200
  7. 接线:光电开关 DO → PA0,VCC/GND 接 3.3V/GND(与上篇一致,PA0 复用 TI1 或 ETR)
  8. 编译烧录,对照”效果展示”一节观察复位模式现象
  9. 依次把 Slave Mode 改成门控、触发(+单脉冲),对照”效果展示”观察三档表现差异

总结

新知识点 记住这点
复位模式 触发信号清零计数器并触发更新中断(TRIGGER 标志可以区分)
门控模式 高电平计数 / 低电平暂停,门沿只置 TRIGGER 不触发更新中断
触发模式 触发信号让计数开始跑,跑满一轮停(配单脉冲用)
TRIGGER 标志 从模式控制器置位,HAL 不自动清,记得手动清
单脉冲模式 重载一次后自动停止,配合触发模式使用

对比上篇:上篇把从模式当成外部脉冲的”计数源”;这篇把从模式当成定时器的”开关闸门”。

参考资源