输入捕获代码移植到Keil

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

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 时应统一使用 huart1Init_UART1() 等名称,避免名称与硬件不一致。

1.3 修改H文件

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

1.4 修改C文件

  • uart.c 中将实例改为 USART1,并开启 USART1 与 GPIOA 时钟;PA9 配置为复用推挽输出,PA10 配置为输入。
  • 可使用编辑器的替换功能批量替换 huart2USART2 等标识符,但替换后必须逐项核对引脚、时钟使能和注释。
  • 外设寄存器地址、时钟和引脚不应凭名称直接套用。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.ctim.htim.c 保存 TIM2 初始化、回调和中断相关实现,tim.h 保存句柄与初始化函数声明。
  • 本例把 TIM2 配置为向上计数、PSC=71ARR=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.cmain.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_count0,因此该乘数不会参与本次测量结果;测量可能跨越溢出的长脉冲时,再结合当时的计数方式核对这一项。

七、加上打印语句

  • 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 PA1TIM2_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。