输入捕获实验代码1
140.5.12输入捕获实验代码(1)
/*
* 整体目的:用 PA1/TIM2_CH2 捕获舵机控制脉冲的上升沿和下降沿,测量高电平持续时间。
* 计时依据:TIM2CLK=72 MHz,PSC=71 后 CNT 每 1 μs 加 1;ARR=0xFFFF,
* 因此 16 位计数器每 65536 μs 溢出一次。
* 状态流程:状态0等上升沿 → CNT清零并切换下降沿 → 状态1计时
* → 下降沿锁存 CCR2 → 状态2由主循环处理结果。
* 代码范围:摘自 CubeIDE/08IC 的 tim.c 与 main.c;HAL 输入捕获库内部不展开。
*/
/* ==================== Core/Src/tim.c:TIM2_CH2 输入捕获配置 ==================== */
TIM_HandleTypeDef htim2; /* 定义 TIM2 句柄,供初始化、启动和两个回调共同使用。 */
void MX_TIM2_Init(void) /* 配置 1 μs 计数时基及 CH2 上升沿捕获。 */
{ /* TIM2 初始化函数体开始。 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0}; /* 清零定时器时钟源配置。 */
TIM_MasterConfigTypeDef sMasterConfig = {0}; /* 清零定时器主模式配置。 */
TIM_IC_InitTypeDef sConfigIC = {0}; /* 清零输入捕获通道配置。 */
htim2.Instance = TIM2; /* 让句柄指向 APB1 上的 TIM2 寄存器块。 */
htim2.Init.Prescaler = 71U; /* 实际 72 分频,把 72 MHz 变为 1 MHz。 */
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; /* CNT 从 0 向 ARR 递增。 */
htim2.Init.Period = 0xFFFFU; /* ARR 取 16 位最大值,一圈含 65536 个计数。 */
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 不额外分频输入滤波采样时钟。 */
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; /* ARR 在更新边界生效。 */
if (HAL_TIM_Base_Init(&htim2) != HAL_OK) /* 配置基本计数器并调用 MSP 初始化。 */
{ /* 基础初始化失败分支开始。 */
Error_Handler(); /* 失败时进入统一错误处理。 */
} /* 基础初始化结果判断结束。 */
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /* 选择内部定时器时钟。 */
if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) /* 应用时钟源。 */
{ /* 时钟源配置失败分支开始。 */
Error_Handler(); /* 配置失败时停止正常业务。 */
} /* 时钟源配置结果判断结束。 */
if (HAL_TIM_IC_Init(&htim2) != HAL_OK) /* 初始化 TIM2 输入捕获功能状态。 */
{ /* 输入捕获初始化失败分支开始。 */
Error_Handler(); /* 失败时进入错误处理。 */
} /* 输入捕获初始化结果判断结束。 */
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; /* 首先捕获高脉冲起点的上升沿。 */
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; /* 把 CH2 直接连接到 TI2/PA1。 */
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; /* 每个有效边沿都进行一次捕获。 */
sConfigIC.ICFilter = 0U; /* 不使用数字滤波,输入噪声可能直接触发捕获。 */
if (HAL_TIM_IC_ConfigChannel( /* 把上述输入参数应用到通道 2。 */
&htim2, /* 选择 TIM2。 */
&sConfigIC, /* 传入输入捕获配置。 */
TIM_CHANNEL_2) != HAL_OK) /* 选择连接 PA1 的 CH2 并检查返回值。 */
{ /* 通道配置失败分支开始。 */
Error_Handler(); /* 配置失败时进入错误处理。 */
} /* 通道配置结果判断结束。 */
} /* TIM2 初始化函数结束。 */
void HAL_TIM_Base_MspInit( /* HAL 初始化过程中配置 TIM2 时钟、PA1 和 NVIC。 */
TIM_HandleTypeDef *timBaseHandle) /* 参数指出当前正在初始化的定时器。 */
{ /* TIM2 底层初始化函数体开始。 */
GPIO_InitTypeDef GPIO_InitStruct = {0}; /* 清零 GPIO 配置结构体。 */
if (timBaseHandle->Instance == TIM2) /* 仅处理 TIM2。 */
{ /* TIM2 底层配置开始。 */
__HAL_RCC_TIM2_CLK_ENABLE(); /* 开启 TIM2 外设时钟。 */
__HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启 PA1 所属 GPIOA 的时钟。 */
GPIO_InitStruct.Pin = GPIO_PIN_1; /* 选择 PA1/TIM2_CH2。 */
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; /* 将 PA1 配置为输入。 */
GPIO_InitStruct.Pull = GPIO_PULLUP; /* 信号源断开时用上拉提供默认高电平。 */
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* 应用 PA1 输入配置。 */
HAL_NVIC_SetPriority(TIM2_IRQn, 0U, 0U); /* 设置 TIM2 全局中断优先级。 */
HAL_NVIC_EnableIRQ(TIM2_IRQn); /* 在 NVIC 中允许 TIM2 中断。 */
} /* TIM2 底层配置结束。 */
} /* TIM2 底层初始化函数结束。 */
/* ==================== Core/Src/main.c:状态变量、启动和捕获回调 ==================== */
volatile uint8_t ic_state = 0U; /* 0等上升沿、1等下降沿、2表示结果待处理。 */
volatile uint32_t period_count = 0U; /* 记录状态1期间 TIM2 的溢出次数。 */
volatile uint32_t ic_count = 0U; /* 保存下降沿到来时锁存在 CCR2 的计数值。 */
HAL_TIM_Base_Start_IT(&htim2); /* 启动更新中断,用于统计 CNT 溢出。 */
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); /* 启动 CH2 捕获中断,用于响应输入边沿。 */
void HAL_TIM_IC_CaptureCallback( /* HAL 识别 CH2 捕获事件后调用的应用回调。 */
TIM_HandleTypeDef *htim) /* 参数指出发生捕获的定时器。 */
{ /* 输入捕获回调开始。 */
if (htim->Instance == TIM2) /* 只处理 TIM2 的捕获事件。 */
{ /* TIM2 捕获分支开始。 */
if (ic_state == 0U) /* 状态0中的边沿是本次高脉冲上升沿。 */
{ /* 上升沿处理开始。 */
__HAL_TIM_SET_COUNTER(htim, 0U); /* 从上升沿开始把 CNT 清零。 */
__HAL_TIM_SET_CAPTUREPOLARITY( /* 把下一次捕获切换为下降沿。 */
&htim2, /* 选择 TIM2。 */
TIM_CHANNEL_2, /* 选择 PA1 对应的通道 2。 */
TIM_INPUTCHANNELPOLARITY_FALLING); /* 下一边沿应标记高电平结束。 */
ic_state = 1U; /* 进入正在测量高电平的状态。 */
period_count = 0U; /* 清除上一次测量的溢出次数。 */
ic_count = 0U; /* 清除上一次测量的捕获值。 */
} /* 上升沿处理结束。 */
else if (ic_state == 1U) /* 仅在等待下降沿时接受第二个边沿。 */
{ /* 下降沿处理开始。 */
ic_count = HAL_TIM_ReadCapturedValue( /* 读取硬件锁存的 CCR2,而非当前 CNT。 */
&htim2, /* 选择 TIM2。 */
TIM_CHANNEL_2); /* 读取通道 2 的捕获值。 */
__HAL_TIM_SET_CAPTUREPOLARITY( /* 为下一帧恢复上升沿捕获。 */
&htim2, /* 选择 TIM2。 */
TIM_CHANNEL_2, /* 选择通道 2。 */
TIM_INPUTCHANNELPOLARITY_RISING); /* 下一次重新寻找脉冲起点。 */
ic_state = 2U; /* 通知主循环结果已经就绪。 */
} /* 下降沿处理结束。 */
} /* TIM2 捕获分支结束。 */
} /* 输入捕获回调结束。 */
一、输入捕获实验
输入捕获用于测量外部数字信号的边沿时间。TIM2_CH2 接收到选定边沿时,硬件会把当时的 CNT 锁存到 CCR2,并置位捕获标志。软件读取 CCR2,即可计算两个边沿之间的时间。
flowchart LR
P[PA1/TIM2_CH2] --> M[输入选择CC2S]
M --> F[数字滤波IC2F]
F --> E[极性检测CC2P]
E --> L[边沿到来锁存CNT]
L --> C[CCR2]
L --> S[SR.CC2IF]
S --> I[DIER.CC2IE与NVIC]
I --> H[HAL_TIM_IC_CaptureCallback]
1.1 定时器配置
TIM2 的参数为 PSC=71、ARR=0xFFFF,输入时钟为 72 MHz:
计数频率 = 72 MHz / (71+1) = 1 MHz
每个计数 = 1 μs
溢出模数 = ARR+1 = 65536
溢出周期 = 65536 μs = 65.536 ms
CH2 配置为上升沿、直接输入、每个有效边沿都捕获、数字滤波值为 0。滤波值 0 表示没有数字去抖滤波,接线较长或噪声较大时可能出现误捕获,应结合信号质量选择滤波参数。
1.2 实验演示
实验把 51 单片机舵机控制信号接到 STM32 的 PA1,由 STM32 测量高电平脉宽。接线应是:
51单片机 P2.0(信号) ───── STM32 PA1 / TIM2_CH2
51单片机 GND ───────────── STM32 GND
舵机电源按原控制系统供电,信号测量必须共地
STM32 输入只测逻辑信号,不承担舵机电流。接线前应确认信号电压满足 PA1 的电气限制;需要改线时先断电,避免信号悬空、短路或热插拔造成异常。
测得约 500、1000、1500、2000、2500 μs,分别对应舵机控制中常见的 0.5~2.5 ms 高脉宽范围。
1.3 代码分析
1.3.1 main.c文件
复位后先由启动代码建立运行环境,再调用 main()。只有启动模式选择系统存储器等特殊区域时,才会进入芯片内置 Bootloader;正常从 Flash 启动不能笼统称为先运行 Bootloader。
业务变量用于保存捕获状态、溢出次数、当前捕获值和换算后的微秒数。凡是中断和主循环共同访问的变量,都应声明为 volatile,否则编译器可能缓存数值,主循环看不到中断中的最新修改。
1.3.2 配置系统时钟
SystemClock_Config() 把系统与定时器时钟配置到实验所需频率。它是 MCU 时钟树配置,不是 ST-Link 调试器配置。计算 TIM2 时钟时还要注意 APB 分频规则:当 APB 预分频不为 1 时,许多 STM32F1 定时器内核时钟是该 APB 外设时钟的 2 倍。
1.3.3 定时器二
TIM2 既承担自由运行计数,又承担 CH2 输入捕获。与测量有关的寄存器如下:
| 寄存器/位 | 作用 |
|---|---|
PSC | 把定时器时钟分到 1 MHz |
ARR | 决定 65536 个计数后溢出 |
CNT | 当前时间计数 |
CCMR1.CC2S | 把 CH2 连接到直接输入 TI2 |
CCMR1.IC2F | 输入数字滤波 |
CCER.CC2P | 选择捕获上升沿或下降沿 |
CCR2 | 保存捕获瞬间的 CNT |
DIER.CC2IE | 允许 CH2 捕获中断 |
SR.CC2IF | CH2 已发生捕获 |
SR.CC2OF | 新捕获覆盖了未及时处理的捕获 |
1.3.4 主函数
初始化顺序是 HAL、系统时钟、GPIO、TIM2、USART1。随后主循环等待 ic_state==2,计算脉宽、串口打印,再把状态恢复为 0,等待下一次测量。
这个实现只有一个结果槽。若信号边沿很快或打印阻塞很久,下一次测量可能覆盖尚未处理的数据。舵机信号约 20 ms 一帧,演示条件下通常足够;更高频的输入应使用缓冲队列、DMA 或更短的中断路径。
1.3.5 打开定时器
工程需要同时启动更新中断和输入捕获中断:
HAL_TIM_Base_Start_IT(&htim2);
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2);
第一句使能更新中断,用于累计 CNT 溢出次数;第二句使能 CH2 捕获中断。只启动其中一个,会分别失去跨溢出测量能力或边沿捕获能力。
1.3.6 配置界面
CubeMX 中 CH2 选择 Input Capture direct mode,极性先设为 Rising Edge,预分频为 DIV1,滤波为 0;PA1 配成 TIM2_CH2 输入,并开启 TIM2 全局中断。
PA1 在该工程中使用上拉输入。上拉只能在信号源断开时提供默认电平,不能替代共地,也不能修复超过电气额定值的输入电压。
1.3.7 分析代码
软件用一次上升沿作为起点、下一次下降沿作为终点,因此测到的是高电平宽度:
状态0:等待上升沿
上升沿到达 → CNT清零,改捕获下降沿,进入状态1
状态1:累计时间
CNT溢出 → period_count加1
下降沿到达 → 读CCR2,改回上升沿,进入状态2
状态2:主循环换算并打印
完成后回到状态0
1.3.7.1 ic_state定义
| 状态值 | 含义 | 允许的下一步 |
|---|---|---|
| 0 | 等待测量起始上升沿 | 清零计数并切到下降沿 |
| 1 | 正在测量高电平 | 等下降沿,期间统计溢出 |
| 2 | 一次结果已完成 | 主循环读取、打印并复位状态 |
状态变量能避免把每个边沿都按相同方式处理。中断与主循环共享时,建议定义为 volatile uint8_t。
1.3.7.2 回调函数
上升沿回调清零 CNT、把下一次捕获极性改成下降沿,并清零溢出次数;下降沿回调读取 CCR2,再把极性切回上升沿。
if (ic_state == 0U) {
__HAL_TIM_SET_COUNTER(htim, 0U);
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_2,
TIM_INPUTCHANNELPOLARITY_FALLING);
period_count = 0U;
ic_state = 1U;
} else if (ic_state == 1U) {
ic_count = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_2,
TIM_INPUTCHANNELPOLARITY_RISING);
ic_state = 2U;
}
切换极性、清状态和改状态值的顺序应尽量紧凑。如果输入在这几条语句之间再次变化,可能漏掉边沿;需要高可靠测量时可使用 PWM Input 模式、从模式复位计数器、DMA 或专门设计的双通道捕获方案。
工程中的第二个分支写成了不带条件的 else,因此主循环尚未处理状态 2 时若又来一次捕获,也会进入下降沿处理并覆盖数据。更稳妥的写法是明确使用 else if (ic_state == 1U),状态 2 到来的边沿只记录为丢帧或直接忽略。