输入捕获代码移植到Keil
142.5.14输入捕获代码移植到Keil
/* main.c */
#include "main.h" // 提供 HAL 类型、错误处理函数和开发板引脚定义
#include "gpio.h" // 声明开发板 GPIO 初始化函数
#include "uart.h" // 声明 USART1 初始化函数和课程使用的 printf 宏
#include "tim.h" // 声明 TIM2 句柄与输入捕获初始化函数
uint8_t recv_buff[5]; // 保留的串口接收缓冲区,本课输入捕获流程未使用它
uint32_t ic_state = 0, period_count = 0, ic_count = 0, us_count; // 捕获状态、溢出次数、CCR2 值和最终微秒数
void SystemClock_Config(void); // 声明 72 MHz 系统时钟配置函数
int main(void) // 程序入口:初始化外设并等待输入捕获结果
{
HAL_Init(); // 初始化 HAL 库、SysTick 和中断优先级基础配置
SystemClock_Config(); // 使用外部晶振和 PLL 配置系统时钟
Init_GPIO(); // 初始化开发板按键、LED、蜂鸣器等通用 GPIO
Init_UART1(); // 初始化 USART1,供调试信息输出
Init_TIM2(); // 初始化 TIM2_CH2 输入捕获与更新中断
HAL_TIM_Base_Start_IT(&htim2); // 启动 TIM2 更新中断,用于记录计数器溢出
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); // 启动 TIM2 通道 2 输入捕获中断
printf("Please connect MCU51 P2.0,and let SG90 run to detect signal!\n"); // 提示连接待测信号
while (1) // 主循环等待中断回调完成一次高电平测量
{
if (ic_state == 2) // 状态 2 表示下降沿已经到达,捕获值可以使用
{
us_count = ic_count + period_count * 0xFFFF; // 按课程工程公式合成高电平计数值
printf("High level duration: %ldus \n", us_count); // 输出高电平持续时间
ic_state = 0; // 复位状态,等待下一次上升沿
}
}
}
void SystemClock_Config(void) // 配置 72 MHz 系统时钟及 APB1/APB2 分频
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0}; // 保存 HSE、HSI 和 PLL 振荡器配置
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 保存系统总线时钟与分频配置
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 选择外部高速晶振
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 开启 HSE
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; // HSE 不预分频
RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 保持内部高速时钟开启
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 开启 PLL
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // PLL 输入选择 HSE
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8 MHz 乘 9 得到 72 MHz
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) // 写入振荡器配置并检查结果
{
Error_Handler(); // 时钟源配置失败后进入错误处理
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | // 指定要配置的时钟域
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; // 同时配置 APB1 与 APB2
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 系统时钟取自 PLL
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // HCLK = 72 MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // PCLK1 = 36 MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // PCLK2 = 72 MHz
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // 设置总线时钟与 Flash 等待周期
{
Error_Handler(); // 总线时钟配置失败后进入错误处理
}
}
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) // 输入边沿捕获完成后的 HAL 回调
{
if (TIM2 == htim->Instance) // 只处理 TIM2 产生的输入捕获回调
{
if (ic_state == 0) // 状态 0:当前捕获到本次脉冲的上升沿
{
__HAL_TIM_SET_COUNTER(htim, 0); // 从上升沿开始重新计数
__HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_2, // 设置 CH2 下一次捕获极性
TIM_INPUTCHANNELPOLARITY_FALLING); // 下一次改为捕获下降沿
ic_state = 1; // 进入正在测量高电平的状态
period_count = 0; // 清零本次测量的溢出次数
ic_count = 0; // 清零上一次捕获值
}
else // 状态非 0:当前捕获到本次脉冲的下降沿
{
ic_count = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_2); // 读取 CCR2 锁存值
__HAL_TIM_SET_CAPTUREPOLARITY(&htim2, TIM_CHANNEL_2, // 恢复 CH2 捕获极性
TIM_INPUTCHANNELPOLARITY_RISING); // 恢复为捕获下一次上升沿
ic_state = 2; // 通知主循环本次测量结束
}
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) // TIM2 计数器溢出后的 HAL 回调
{
if (TIM2 == htim->Instance) // 只统计 TIM2 的更新事件
{
if (ic_state == 1) // 仅在上升沿与下降沿之间统计溢出
{
if (period_count == 0XFFFF) // 溢出计数达到课程代码设定的上限
{
ic_state = 2; // 强制结束当前测量,防止无限等待
ic_count = 0XFFFF; // 将捕获计数置为上限值
}
else // 溢出计数尚未达到上限
{
period_count++; // 记录一次 TIM2 计数器溢出
}
}
}
}
void Error_Handler(void) // HAL 初始化失败时统一进入的错误处理函数
{
__disable_irq(); // 发生不可恢复错误时关闭全局中断
while (1) // 停留在错误状态,便于调试定位
{
}
}
/* main.h */
#ifndef __MAIN_H // 防止 main.h 被重复包含
#define __MAIN_H // 定义 main.h 的头文件保护宏
#ifdef __cplusplus // 使用 C++ 编译器时按 C 语言规则链接这些符号
extern "C" { // 开始 C 链接声明
#endif // C++ 兼容处理结束
#include "stm32f1xx_hal.h" // 引入 STM32F1 HAL 库类型与接口
#define KEY1_Pin GPIO_PIN_3 // KEY1 使用 PE3
#define KEY1_GPIO_Port GPIOE // KEY1 所在端口为 GPIOE
#define KEY0_Pin GPIO_PIN_4 // KEY0 使用 PE4
#define KEY0_GPIO_Port GPIOE // KEY0 所在端口为 GPIOE
#define JDQ_Pin GPIO_PIN_10 // 继电器控制脚使用 PB10
#define JDQ_GPIO_Port GPIOB // 继电器所在端口为 GPIOB
#define SUN_Pin GPIO_PIN_11 // 光照数字输入使用 PB11
#define SUN_GPIO_Port GPIOB // 光照输入所在端口为 GPIOB
#define LED1_Pin GPIO_PIN_5 // LED1 使用 PB5
#define LED1_GPIO_Port GPIOB // LED1 所在端口为 GPIOB
#define BEEP_Pin GPIO_PIN_8 // 蜂鸣器使用 PB8
#define BEEP_GPIO_Port GPIOB // 蜂鸣器所在端口为 GPIOB
void Error_Handler(void); // 声明统一错误处理函数
#ifdef __cplusplus // 与前面的 extern "C" 配对
} // 结束 C 链接声明
#endif // C++ 兼容处理结束
#endif /* __MAIN_H */ // main.h 头文件保护结束
/* gpio.c */
#include "gpio.h" // 引入 GPIO 初始化函数声明和引脚宏
void Init_GPIO(void) // 初始化开发板上与本工程共用的通用 GPIO
{
GPIO_InitTypeDef GPIO_InitStruct = {0}; // 创建并清零 GPIO 配置结构体
__HAL_RCC_GPIOE_CLK_ENABLE(); // 开启按键所在 GPIOE 时钟
__HAL_RCC_GPIOB_CLK_ENABLE(); // 开启 LED、蜂鸣器等所在 GPIOB 时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 开启 TIM2_CH2 和 USART1 所在 GPIOA 时钟
HAL_GPIO_WritePin(GPIOB, JDQ_Pin | LED1_Pin, GPIO_PIN_SET); // 预置继电器和 LED 输出电平
HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET); // 预置蜂鸣器输出电平
GPIO_InitStruct.Pin = KEY1_Pin | KEY0_Pin; // 同时选择两个按键引脚
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 按键引脚配置为输入
GPIO_InitStruct.Pull = GPIO_PULLUP; // 启用内部上拉,按下时通常读到低电平
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); // 将按键配置写入 GPIOE
GPIO_InitStruct.Pin = JDQ_Pin | LED1_Pin | BEEP_Pin; // 选择三个控制输出
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 配置为推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 输出脚不启用内部上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速输出足以控制这些器件
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 将输出配置写入 GPIOB
GPIO_InitStruct.Pin = SUN_Pin; // 选择光照数字输入引脚
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 配置为普通输入
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不启用 MCU 内部上下拉
HAL_GPIO_Init(SUN_GPIO_Port, &GPIO_InitStruct); // 将配置写入对应端口
}
/* gpio.h */
#ifndef __GPIO_H__ // 防止 gpio.h 被重复包含
#define __GPIO_H__ // 定义 gpio.h 的头文件保护宏
#ifdef __cplusplus // 使用 C++ 编译时启用 C 链接
extern "C" { // 开始 C 链接声明
#endif
#include "main.h" // 获取 HAL 类型和引脚宏
void Init_GPIO(void); // 声明开发板 GPIO 初始化函数
#ifdef __cplusplus // 与前面的 extern "C" 配对
}
#endif
#endif /* __GPIO_H__ */ // gpio.h 头文件保护结束
/* tim.c */
#include "tim.h" // 引入 TIM2 句柄声明和 HAL 定时器接口
TIM_HandleTypeDef htim2; // 保存 TIM2 的实例和初始化参数
void Init_TIM2(void) // 配置 TIM2_CH2 上升沿输入捕获和 1 us 计数
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0}; // 配置 TIM2 的计数时钟源
TIM_MasterConfigTypeDef sMasterConfig = {0}; // 配置主从同步与触发输出
TIM_IC_InitTypeDef sConfigIC = {0}; // 配置输入捕获通道
htim2.Instance = TIM2; // 选择 TIM2 外设
htim2.Init.Prescaler = 71; // 72 MHz 除以 72,计数周期为 1 us
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 使用向上计数
htim2.Init.Period = 0xffff; // 自动重装值设为 16 位最大值
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 不额外分频定时器采样时钟
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 启用 ARR 预装载
if (HAL_TIM_Base_Init(&htim2) != HAL_OK) // 初始化 TIM2 基本计数单元
{
Error_Handler(); // TIM2 基本初始化失败
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; // 使用内部定时器时钟
if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) // 写入时钟源配置
{
Error_Handler(); // TIM2 时钟源配置失败
}
if (HAL_TIM_IC_Init(&htim2) != HAL_OK) // 初始化输入捕获功能
{
Error_Handler(); // 输入捕获初始化失败
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; // 不向其他外设输出特殊触发
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; // 不使用定时器主从同步
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) // 写入主模式配置
{
Error_Handler(); // 主模式配置失败
}
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; // 第一次先捕获上升沿
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // CH2 直接连接 TI2 输入
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 每个有效边沿都参与捕获
sConfigIC.ICFilter = 0; // 不启用数字输入滤波
if (HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_2) != HAL_OK) // 配置 TIM2_CH2
{
Error_Handler(); // 输入捕获通道配置失败
}
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *tim_baseHandle) // 配置 TIM2 时钟、PA1 与 NVIC
{
GPIO_InitTypeDef GPIO_InitStruct = {0}; // 保存 PA1 输入配置
if (tim_baseHandle->Instance == 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; // 配置为数字输入
GPIO_InitStruct.Pull = GPIO_PULLUP; // 按课程工程启用内部上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 应用 PA1 配置
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); // 设置 TIM2 中断优先级
HAL_NVIC_EnableIRQ(TIM2_IRQn); // 在 NVIC 中使能 TIM2 中断
}
}
/* tim.h */
#ifndef __TIM_H__ // 防止 tim.h 被重复包含
#define __TIM_H__ // 定义 tim.h 的头文件保护宏
#include "main.h" // 获取 HAL 定时器句柄类型
extern TIM_HandleTypeDef htim2; // 引用 tim.c 中唯一的 TIM2 句柄
void Init_TIM2(void); // 声明 TIM2 输入捕获初始化函数
#endif /* __TIM_H__ */ // tim.h 头文件保护结束
/* uart.c */
#include "uart.h" // 引入 USART1 句柄、缓冲区和初始化声明
uint8_t u_buf[256]; // 保存 printf 格式化后的待发送文本
UART_HandleTypeDef huart1; // 保存 USART1 的实例和配置参数
void Init_UART1(void) // 配置 USART1 为 115200-8-N-1
{
huart1.Instance = USART1; // 选择 USART1 外设
huart1.Init.BaudRate = 115200; // 波特率设为 115200 bit/s
huart1.Init.WordLength = UART_WORDLENGTH_8B; // 每帧 8 位数据
huart1.Init.StopBits = UART_STOPBITS_1; // 使用 1 位停止位
huart1.Init.Parity = UART_PARITY_NONE; // 不使用奇偶校验
huart1.Init.Mode = UART_MODE_TX_RX; // 同时使能发送和接收
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 不使用 RTS/CTS 硬件流控
huart1.Init.OverSampling = UART_OVERSAMPLING_16; // 使用 16 倍过采样
if (HAL_UART_Init(&huart1) != HAL_OK) // 初始化 USART1 并检查返回值
{
Error_Handler(); // USART1 初始化失败
}
}
void HAL_UART_MspInit(UART_HandleTypeDef *uartHandle) // 配置 USART1 时钟与 PA9/PA10
{
GPIO_InitTypeDef GPIO_InitStruct = {0}; // 保存 USART1 引脚配置
if (uartHandle->Instance == USART1) // 只为 USART1 执行底层初始化
{
__HAL_RCC_USART1_CLK_ENABLE(); // 开启 USART1 外设时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 开启 PA9、PA10 所在 GPIOA 时钟
GPIO_InitStruct.Pin = GPIO_PIN_9; // PA9 对应 USART1_TX
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // TX 配置为复用推挽输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 使用高速输出模式
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 应用 PA9 配置
GPIO_InitStruct.Pin = GPIO_PIN_10; // PA10 对应 USART1_RX
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // RX 配置为输入
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不启用内部上下拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 应用 PA10 配置
}
}
/* uart.h */
#ifndef __UART_H__ // 防止 uart.h 被重复包含
#define __UART_H__ // 定义 uart.h 的头文件保护宏
#include "main.h" // 获取 HAL 串口句柄类型
#include <stdio.h> // 声明 sprintf()
extern uint8_t u_buf[256]; // 引用 uart.c 中的格式化缓冲区
extern UART_HandleTypeDef huart1; // 引用 uart.c 中的 USART1 句柄
// 将可变参数先格式化到 u_buf,再通过 USART1 阻塞发送。
#define printf(...) /* 接收与标准 printf 相同的可变参数 */ \
HAL_UART_Transmit(&huart1, (uint8_t *)u_buf, /* 用 USART1 发送 u_buf */ \
sprintf((char *)u_buf, __VA_ARGS__), 0xffff) /* sprintf 返回发送长度 */
void Init_UART1(void); // 声明 USART1 初始化函数
#endif /* __UART_H__ */ // uart.h 头文件保护结束
/* stm32f1xx_it.c:与 TIM2 相关的中断入口 */
#include "main.h" // 获取 HAL 基础类型
#include "stm32f1xx_it.h" // 获取中断服务函数声明
extern TIM_HandleTypeDef htim2; // 引用 tim.c 中的 TIM2 句柄
void TIM2_IRQHandler(void) // TIM2 在中断向量表中的硬件入口
{
HAL_TIM_IRQHandler(&htim2); // 交给 HAL 处理标志位并调用更新/捕获回调
}
一、定时器输入捕获代码移植到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。