输入捕获代码移植到Keil

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

142.5.14输入捕获代码移植到Keil

/*
 * 整体目的:把 CubeIDE/08IC 的 TIM2_CH2 输入捕获功能手工移植到 Keil/5TIM_IC。
 * 移植边界:业务 main.c 之外,还必须同步加入 tim.c/tim.h、HAL TIM 驱动、
 *           TIM2_IRQHandler()、USART1 输出接口以及 stm32f1xx_hal_conf.h 模块开关。
 * 名称关系:Keil 工程使用 Init_TIM2()、Init_UART1(),函数声明、定义和调用必须一致;
 *           htim2 则通过 extern 在多个 C 文件间共享。
 * 技术修正:课程源码跨溢出乘数写成 0xFFFF;ARR=0xFFFF 时正确模数为 ARR+1=0x10000。
 * 代码范围:以下保留移植时必须理解的工程层接口与调用,HAL 库内部不逐行展开。
 */

/* ==================== Core/Inc/tim.h:对外声明 ==================== */

#ifndef __TIM_H__                          /* 防止 tim.h 被同一编译单元重复展开。 */
#define __TIM_H__                          /* 定义头文件保护宏。 */
#include "main.h"                          /* 引入 TIM_HandleTypeDef 等 HAL 公共类型。 */
extern TIM_HandleTypeDef htim2;            /* 声明 TIM2 句柄实体位于 tim.c。 */
void Init_TIM2(void);                      /* 声明 Keil 工程自定义命名的 TIM2 初始化函数。 */
#endif /* __TIM_H__ */                     /* 结束 tim.h 的头文件保护。 */

/* ==================== Core/Src/tim.c:句柄和输入捕获配置 ==================== */

#include "tim.h"                           /* 引入本文件对应的声明并检查接口一致性。 */
TIM_HandleTypeDef htim2;                   /* 定义全局 TIM2 句柄,只能在一个 C 文件中定义一次。 */

void Init_TIM2(void)                       /* 配置 TIM2 为 1 MHz、CH2 上升沿输入捕获。 */
{                                           /* TIM2 初始化函数体开始。 */
    TIM_ClockConfigTypeDef clock_cfg = {0}; /* 清零时钟源配置结构体。 */
    TIM_IC_InitTypeDef ic_cfg = {0};       /* 清零输入捕获通道配置结构体。 */

    htim2.Instance = TIM2;                 /* 绑定 TIM2 寄存器块。 */
    htim2.Init.Prescaler = 71U;            /* 72 MHz 除以 72,CNT 每 1 μs 加 1。 */
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP; /* 选择向上计数。 */
    htim2.Init.Period = 0xFFFFU;           /* 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();                    /* 失败时进入统一错误处理。 */
    }                                       /* 基础初始化结果判断结束。 */

    clock_cfg.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /* 使用内部时钟树驱动 TIM2。 */
    if (HAL_TIM_ConfigClockSource(&htim2, &clock_cfg) != HAL_OK) /* 应用时钟源。 */
    {                                       /* 时钟源配置失败分支开始。 */
        Error_Handler();                    /* 配置失败时停止正常业务。 */
    }                                       /* 时钟源配置结果判断结束。 */

    if (HAL_TIM_IC_Init(&htim2) != HAL_OK) /* 初始化输入捕获功能状态。 */
    {                                       /* 输入捕获初始化失败分支开始。 */
        Error_Handler();                    /* 失败时进入错误处理。 */
    }                                       /* 输入捕获初始化结果判断结束。 */

    ic_cfg.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; /* 第一次捕获上升沿。 */
    ic_cfg.ICSelection = TIM_ICSELECTION_DIRECTTI; /* CH2 直接连接 TI2/PA1。 */
    ic_cfg.ICPrescaler = TIM_ICPSC_DIV1;   /* 每一个有效边沿都捕获。 */
    ic_cfg.ICFilter = 0U;                  /* 关闭数字滤波。 */
    if (HAL_TIM_IC_ConfigChannel(&htim2, &ic_cfg, TIM_CHANNEL_2) != HAL_OK) /* 配置 CH2。 */
    {                                       /* 通道配置失败分支开始。 */
        Error_Handler();                    /* 失败时进入错误处理。 */
    }                                       /* 通道配置结果判断结束。 */
}                                           /* TIM2 初始化函数结束。 */

/* ==================== Core/Src/main.c:移植后的调用关系 ==================== */

#include "main.h"                          /* 引入工程公共 HAL 配置。 */
#include "gpio.h"                          /* 引入 Init_GPIO()。 */
#include "uart.h"                          /* 引入 Init_UART1()、huart1 和 printf 宏。 */
#include "tim.h"                           /* 引入 Init_TIM2() 与 htim2。 */

volatile uint8_t ic_state = 0U;            /* 保存捕获状态,由中断修改、主循环读取。 */
volatile uint32_t period_count = 0U;       /* 保存高电平期间的 TIM2 溢出次数。 */
volatile uint32_t ic_count = 0U;           /* 保存下降沿锁存的 CCR2 值。 */
uint32_t us_count = 0U;                   /* 保存换算后的微秒数。 */

int main(void)                             /* Keil 工程入口,按依赖顺序初始化和启动外设。 */
{                                           /* main() 函数体开始。 */
    HAL_Init();                            /* 初始化 HAL 与 SysTick。 */
    SystemClock_Config();                  /* 配置 72 MHz 系统时钟。 */
    Init_GPIO();                           /* 初始化开发板通用 GPIO。 */
    Init_UART1();                          /* 初始化结果打印使用的 USART1。 */
    Init_TIM2();                           /* 初始化 TIM2_CH2、PA1 与 TIM2 NVIC。 */
    HAL_TIM_Base_Start_IT(&htim2);        /* 启动 TIM2 更新中断以统计溢出。 */
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); /* 启动 CH2 边沿捕获中断。 */

    while (1)                              /* 主循环等待中断产生完整测量结果。 */
    {                                       /* 一次主循环开始。 */
        if (ic_state == 2U)               /* 状态2表示下降沿已捕获。 */
        {                                   /* 结果处理开始。 */
            us_count =                    /* 组合完整计数器周期与最后一圈 CCR2。 */
                ic_count +                /* 加上下降沿捕获的最后一圈计数。 */
                period_count * 0x10000UL; /* 每个 ARR=0xFFFF 的完整周期是 65536 拍。 */
            printf("High level duration: %luus\r\n", /* 经 USART1 输出高电平宽度。 */
                   (unsigned long)us_count); /* 显式匹配 %lu 的参数类型。 */
            ic_state = 0U;                /* 处理完成后重新等待下一次上升沿。 */
        }                                   /* 结果处理结束。 */
    }                                       /* 返回 while 顶部继续等待。 */
}                                           /* main() 语法边界。 */

一、定时器输入捕获代码移植到Keil

1.1 复制工程

  • 输入捕获通过检测上升沿和下降沿测量高电平持续时间:上升沿到来时清零计数器,下降沿到来时读取捕获值。
  • 定时器计数过程中若发生溢出,需要记录溢出次数。ARR=0xFFFF 时,一个完整计数周期为 0x10000 个计数,而不是 0xFFFF 个计数。
  • 复制一个已经能下载、复位并通过串口输出的 Keil HAL 基础工程,再添加输入捕获相关文件。这样可以把问题限定在移植的定时器代码上。

1.2 简单看工程

  • 基础工程通常包含系统时钟、HAL 库、GPIO 和串口初始化;移植前先确认这些部分能够独立运行。
  • 本课使用 USART1 输出结果,PA9 是 TX,PA10 是 RX。开发板 TX 接 USB 转串口模块 RX,开发板 RX 接模块 TX,并连接公共地线。
  • 函数名、句柄名和实际外设必须一致。例如使用 USART1 时应统一使用 huart1、Init_UART1() 等名称,避免名称与硬件不一致。

1.3 修改H文件

  • 在 uart.h 中把 USART2 的外部句柄声明、初始化函数声明和相关宏统一为 USART1 对应的名称。
  • 头文件提供给其他 .c 文件使用的内容包括 extern UART_HandleTypeDef huart1;、发送缓冲区声明和初始化函数声明;函数定义仍放在 .c 文件中。
  • 修改函数名后,函数声明、函数定义和调用处必须同时更新,否则编译器会报告未声明或未定义的函数。

1.4 修改C文件

  • uart.c 中将实例改为 USART1,并开启 USART1 与 GPIOA 时钟;PA9 配置为复用推挽输出,PA10 配置为输入。
  • 可使用编辑器的替换功能批量替换 huart2、USART2 等标识符,但替换后必须逐项核对引脚、时钟使能和注释。
  • 外设寄存器地址、时钟和引脚不应凭名称直接套用。USART1 挂在 APB2,USART2 挂在 APB1;从 USART2 改为 USART1 时,相关时钟使能代码也必须改变。

1.5 编译并解决问题

  • 编译器提示函数未声明时,先检查对应头文件是否被包含;链接器提示函数未定义时,检查 .c 文件是否加入 Keil 工程。
  • 每处理一个错误就重新编译,直到没有错误和与本实验有关的警告。烧写前先用简短字符串验证串口,例如 printf("test\r\n");。
  • 编译通过只表示程序能够生成,仍需要下载到开发板后确认串口能够正常输出。

1.6 串口实验现象

  • 串口输出应使用 \r\n 换行,例如 printf("UART1 ready\r\n");,便于串口助手逐行显示。
  • 下载后若必须手动按复位键才运行,应在 Keil 下载设置中启用下载后复位并运行。
  • 串口没有输出时,先确认 COM 口、115200 波特率、PA9 与串口模块 RX 的连接以及公共地线,再检查程序本身。

1.7 定时器的代码移植

1.7.1 定时器的C和H文件
  • 输入捕获模块至少需要 tim.c 和 tim.h。tim.c 保存 TIM2 初始化、回调和中断相关实现,tim.h 保存句柄与初始化函数声明。
  • 本例把 TIM2 配置为向上计数、PSC=71、ARR=0xFFFF。TIM2 的实际计数时钟为 72 MHz 时,计数频率为:

$ f_{CNT}=\frac{72\ \text{MHz}}{71+1}=1\ \text{MHz} $

  • 因而每个计数是 $1\ \mu s$,最长一次未溢出的计时范围为 65.536 ms。
1.7.2 添加文件到工程
  • 在 Keil 的源文件组中使用 Add Existing Files to Group 添加 tim.c。文件只存在于磁盘上并不会参与编译。
  • tim.h 所在目录还必须加入 C/C++ 的 Include Paths;main.c 要包含 tim.h。
  • 定时器所用的 HAL 模块必须在 stm32f1xx_hal_conf.h 中启用:
#define HAL_TIM_MODULE_ENABLED
1.7.3 打开H文件
  • tim.h 应包含 main.h,并声明 extern TIM_HandleTypeDef htim2; 与 void Init_TIM2(void);。
  • 源文件中使用而不在本文件定义的符号,要通过头文件声明或 extern 正确声明;不要靠复制局部变量定义来消除报错。
  • tim.c、main.c 和中断文件使用的是同一个 htim2 句柄,不能分别定义多个同名或不同名的定时器句柄。
1.7.4 主函数修改
  • 在 main.c 中包含 tim.h,初始化顺序为串口初始化完成后执行 Init_TIM2(),然后启动更新中断与输入捕获中断:
Init_TIM2();
HAL_TIM_Base_Start_IT(&htim2);
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2);
  • HAL_TIM_Base_Start_IT() 用于处理计数器溢出,HAL_TIM_IC_Start_IT() 用于接收 CH2 的边沿捕获事件,二者缺一不可。
1.7.5 编译并解决问题
  • 首次移植出现错误时,按“头文件声明、源文件是否加入工程、HAL 模块开关、中断入口”顺序处理,不要一次改动大量无关代码。
  • 启用 TIM2 中断后,stm32f1xx_it.c 中必须有中断入口:
void TIM2_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim2);
}
  • HAL_TIM_IRQHandler() 会分发更新事件和捕获事件,并调用相应 HAL 回调。缺少该入口时,发生中断后程序会进入默认中断处理,表现为程序停止运行。
1.7.6 串口调试
  • 用串口打印短标记可以定位代码运行位置,例如分别在 Init_TIM2() 前后、启动中断前后打印不同标记。
  • printf 的发送缓冲区必须足够大。本工程中 u_buf[256] 可容纳本实验的普通输出;格式化长字符串时仍要按最长输出长度检查数组边界。
  • 检查输出时,先验证串口本身,再验证输入捕获模块,避免把串口故障误判为定时器故障。

二、移植代码到Keil中运行

  • 定时器文件、头文件搜索路径、HAL 模块开关和中断文件均配置完成后再编译、下载。
  • 程序运行时,主循环只在 ic_state == 2 时读取测量结果并打印;没有输入脉冲时,它会等待下一次上升沿,这是正常状态。
  • 使用 ST-Link 下载时选择复位后运行,串口助手选择实际枚举的 COM 口和 115200 波特率。

三、寻找问题

  • 串口正常而加入定时器后程序停止运行,优先怀疑 TIM2 初始化、NVIC 配置或 TIM2_IRQHandler()。
  • 可临时不调用 Init_TIM2()、不启动更新中断或不启动输入捕获中断,逐步缩小问题范围;每次只改变一个条件。
  • 捕获不到信号时,要同时检查软件配置和接线,不能只查看代码。

四、修改串口初始化位置

  • 将串口初始化放在定时器初始化之前,才能在定时器初始化前后输出调试标记。
  • 在关键位置打印不同内容,根据最后一条可见输出判断程序停止的位置;不要只打印相同字符串,否则无法定位执行进度。
  • 这类打印只用于定位初始化顺序和中断入口问题,确认后可删除无关测试输出。

五、定时器初始化问题

  • TIM2 使用内部时钟,PA1 作为 TIM2_CH2 输入。TIM2 的时钟、GPIOA 时钟和 TIM2 中断都必须使能。
  • 上升沿被配置为首次捕获边沿;回调中清零计数器并改为捕获下降沿,下降沿到来后再改回上升沿。
  • APB1 配置为 36 MHz 且预分频不为 1 时,STM32F103 的 TIM2 时钟自动倍频为 72 MHz,因此 PSC=71 得到 1 MHz 计数频率。

六、查看主函数

  • 主函数初始化完成后启动 TIM2,再进入循环读取结果。不要在中断回调里执行耗时的串口发送,回调只负责保存捕获值和切换状态。
  • 上升沿到来后清零 CNT,下降沿到来时硬件将计数值锁存到 CCR2;串口打印的执行时间不会直接叠加到已锁存的脉宽值上。
  • 未跨越溢出时,高电平宽度为:

$ t_{high}=CCR2\times1\ \mu s $

  • 跨越 $N$ 次溢出时,高电平宽度为:

$ t_{high}=(N\times(ARR+1)+CCR2)\times1\ \mu s $

  • 从通用计数器机理看,跨溢出计算的模数是 ARR+1,本例为 0x10000。课程工程中的测试信号是 SG90 的毫秒级控制脉冲,正常情况下 period_count 为 0,因此该乘数不会参与本次测量结果;测量可能跨越溢出的长脉冲时,再结合当时的计数方式核对这一项。

七、加上打印语句

  • 在 Init_TIM2()、HAL_TIM_Base_Start_IT() 和 HAL_TIM_IC_Start_IT() 前后加打印标记,可以确认程序是否在某个初始化调用或中断触发后停止。
  • 启用中断却没有编写 TIM2_IRQHandler() 是程序停止运行的常见原因;入口函数中必须调用 HAL_TIM_IRQHandler(&htim2)。
  • printf 输出字符串应带 \r\n,并确保格式化后的长度不超过发送缓冲区容量。

八、检测信号

  • 本实验测量的是 51 单片机 P2.0 输出给 SG90 舵机的控制脉冲,不是舵机的 5 V 电源线。P2.0 同时接 SG90 的信号线和 STM32 PA1(TIM2_CH2)。
MCU51 P2.0  -----------+---------->  SG90 信号线
                         |
                         +---------->  STM32 PA1(TIM2_CH2)

MCU51 GND  ------------+---------->  SG90 GND
                         |
                         +---------->  STM32 GND

独立 5 V 电源  --------------------->  SG90 VCC
  • 两块开发板和舵机必须共地,否则 STM32 没有共同的 0 V 参考,输入捕获会出现随机触发或始终高、低电平等现象。
  • 舵机供电应由能够提供足够电流的 5 V 电源承担,不能从 STM32 的 3.3 V 引脚给舵机供电。若 P2.0 是 5 V 逻辑,先确认 STM32 具体 PA1 引脚是否允许 5 V 输入;不允许时必须加电平转换或分压。
  • 舵机控制信号通常周期约为 20 ms,高电平约为 0.5 ms 到 2.5 ms。串口显示约 2065 us,表示当前控制脉冲的高电平宽度约为 2.065 ms。