输入捕获实验代码1

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

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.CC2IFCH2 已发生捕获
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 到来的边沿只记录为丢帧或直接忽略。