输入捕获代码移植到Keil
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 的信号线和 STM32PA1(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。