串口1中断移植

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

128.4.18串口1中断移植

/*
 * 整体目的:把 USART1 从阻塞轮询接收改为中断接收,同时用发送中断每秒输出测试文本。
 * 事件链路:USART1 收到字节 → USART1_IRQHandler() → HAL_UART_IRQHandler()
 *           → 收满 4 字节 → HAL_UART_RxCpltCallback() → 解析 LED 命令并重新启动接收。
 * 硬件依据:板载 CH340C 连接 PA9/USART1_TX 与 PA10/USART1_RX,所以本节无需外置串口模块。
 * 代码范围:摘自 KEIL/04UART1_IT 的 main.c、uart.c 和 stm32f1xx_it.c;
 *           HAL_UART_IRQHandler() 的库内部实现不在这里逐行展开。
 */

/* ==================== Core/Src/main.c:启动中断收发 ==================== */

#include "main.h"                           /* 引入 HAL 公共定义、LED 引脚别名和错误处理声明。 */
#include "gpio.h"                           /* 引入 MDK_GPIO_Init(),用于初始化 PB5、PE5 两路 LED。 */
#include "uart.h"                           /* 引入 huart1 和 MDK_USART1_UART_Init() 的声明。 */
#include <string.h>                         /* 引入 strlen()、strcmp() 和 memset()。 */

uint8_t UART_Recv_IT[5];                    /* 4 字节命令缓冲区外加 1 字节字符串结束符,静态区初值均为 0。 */

int main(void)                              /* 程序入口,负责初始化并启动第一次中断接收。 */
{                                           /* main() 函数体开始。 */
    HAL_Init();                             /* 初始化 HAL 和 SysTick,为 HAL_Delay() 及超时管理提供时基。 */
    SystemClock_Config();                   /* 配置系统时钟;配套工程使用 72 MHz SYSCLK。 */
    MDK_GPIO_Init();                        /* 初始化命令控制的 LED1 和 LED2。 */
    MDK_USART1_UART_Init();                 /* 配置 USART1、PA9/PA10 以及 USART1 的 NVIC 通道。 */
    HAL_UART_Receive_IT(                    /* 首次挂起一个非阻塞的中断接收任务。 */
        &huart1,                            /* 指定 USART1 的 HAL 句柄。 */
        UART_Recv_IT,                       /* 收到的数据写入这个 5 字节数组的前 4 字节。 */
        4U);                                /* 收满 4 字节后才触发接收完成回调。 */

    while (1)                               /* 主循环不等待接收,CPU 可继续处理其他任务。 */
    {                                       /* 一次主循环开始。 */
        HAL_UART_Transmit_IT(               /* 尝试以中断方式发送测试字符串并立即返回。 */
            &huart1,                        /* 从 USART1/PA9 发送。 */
            (uint8_t *)"i am wfeng\n",      /* 待发送的只读测试文本;转换为 HAL 接口要求的字节指针。 */
            strlen("i am wfeng\n"));        /* 发送字符数,不包括字符串末尾的 '\0'。 */
        HAL_Delay(1000U);                   /* 主循环等待约 1 秒,避免不断重复发起发送。 */
    }                                       /* 返回 while 顶部,再尝试下一次发送。 */
}                                           /* main() 的语法边界;正常运行不会到达这里。 */

/* ==================== Core/Src/uart.c:USART1 与 NVIC 配置 ==================== */

UART_HandleTypeDef huart1;                  /* 定义 USART1 句柄,供主函数、IRQ 和回调共同使用。 */

void MDK_USART1_UART_Init(void)             /* 设置 USART1 的 115200/8N1 收发参数。 */
{                                           /* USART1 初始化函数体开始。 */
    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)   /* 让 HAL 写入配置,并调用下方 MSP 初始化。 */
    {                                       /* 初始化失败分支开始。 */
        Error_Handler();                    /* 停止正常业务,避免在错误配置下运行。 */
    }                                       /* 初始化结果判断结束。 */
}                                           /* USART1 初始化函数结束。 */

void HAL_UART_MspInit(                     /* HAL_UART_Init() 调用的芯片级资源初始化回调。 */
    UART_HandleTypeDef *uartHandle)         /* 接收当前待初始化的 UART 句柄。 */
{                                           /* MSP 初始化函数体开始。 */
    GPIO_InitTypeDef GPIO_InitStruct = {0}; /* 清零 GPIO 配置结构体。 */
    if (uartHandle->Instance == USART1)     /* 仅为 USART1 配置以下时钟、引脚和 NVIC。 */
    {                                       /* USART1 专属配置开始。 */
        __HAL_RCC_USART1_CLK_ENABLE();      /* 开启 APB2 上的 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; /* 把 PA9 配为复用推挽输出。 */
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; /* 选择高速 GPIO 输出能力。 */
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* 把 TX 配置写入 PA9。 */
        GPIO_InitStruct.Pin = GPIO_PIN_10;  /* 改为选择 PA10/USART1_RX。 */
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT; /* 把 PA10 配为输入。 */
        GPIO_InitStruct.Pull = GPIO_NOPULL; /* 不启用内部上下拉,由串口对端维持空闲高电平。 */
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* 把 RX 配置写入 PA10。 */
        HAL_NVIC_SetPriority(USART1_IRQn, 0U, 0U); /* 设置 USART1 的抢占优先级和子优先级。 */
        HAL_NVIC_EnableIRQ(USART1_IRQn);    /* 打开 NVIC 中的 USART1 中断通道。 */
    }                                       /* USART1 专属配置结束。 */
}                                           /* MSP 初始化函数结束。 */

/* ==================== Core/Src/stm32f1xx_it.c:硬件中断入口 ==================== */

extern UART_HandleTypeDef huart1;           /* 声明句柄实体位于 uart.c,当前文件只引用它。 */

void USART1_IRQHandler(void)               /* 名称必须与启动文件中 USART1 向量表入口一致。 */
{                                           /* USART1 中断服务函数开始。 */
    HAL_UART_IRQHandler(&huart1);           /* 交给 HAL 判定中断原因、搬运字节并在完成时调用应用回调。 */
}                                           /* USART1 中断服务函数结束。 */

/* ==================== Core/Src/main.c:接收完成应用回调 ==================== */

void HAL_UART_RxCpltCallback(               /* HAL 在指定长度全部接收完成后调用此函数。 */
    UART_HandleTypeDef *huart)              /* 参数指出本次完成事件属于哪个 UART 句柄。 */
{                                           /* 接收完成回调函数体开始。 */
    if (huart == &huart1)                   /* 只处理 USART1 的完成事件,避免混入其他串口。 */
    {                                       /* USART1 事件分支开始。 */
        if (strcmp("LED1",                  /* 将缓冲区与第一条 4 字节命令比较。 */
                   (char *)UART_Recv_IT) == 0) /* 返回 0 表示接收到的文本正是 "LED1"。 */
        {                                   /* LED1 命令分支开始。 */
            HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); /* 翻转 PB5,从而切换低有效 LED1。 */
            memset(UART_Recv_IT, '\0', 5U); /* 清空全部 5 字节并恢复可靠的字符串结束符。 */
        }                                   /* LED1 命令分支结束。 */
        if (!strcmp("LED2",                 /* 第二种写法用逻辑非把 strcmp 的 0 变为真。 */
                    (char *)UART_Recv_IT))  /* 判断当前缓冲区是否等于 "LED2"。 */
        {                                   /* LED2 命令分支开始。 */
            HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); /* 翻转 PE5,从而切换低有效 LED2。 */
            memset(UART_Recv_IT, '\0', 5U); /* 清空接收区,为下一条命令准备。 */
        }                                   /* LED2 命令分支结束。 */
        HAL_UART_Receive_IT(                /* 当前任务完成后必须重新挂起接收,才能持续接收命令。 */
            &huart1,                        /* 继续使用 USART1。 */
            UART_Recv_IT,                   /* 继续写入同一个接收缓冲区。 */
            4U);                            /* 下一次仍以 4 字节为一条完整命令。 */
    }                                       /* USART1 事件分支结束。 */
}                                           /* 接收完成回调结束,应尽快返回中断上下文。 */

一、串口1中断移植

1.1 复制工程

以 03UART1 轮询工程为基础复制为 04UART1_IT。保留 GPIO、系统时钟、USART1 的 115200/8N1 配置,将接收方式替换成固定长度中断接收。

1.2 分析工程

1.2.1 main函数分析

中断工程需要同时配置三层:

USART外设层:RXNEIE/错误中断允许
NVIC控制层:USART1_IRQn优先级和使能
向量与软件层:USART1_IRQHandler调用HAL_UART_IRQHandler

在 HAL_UART_MspInit() 的 USART1 分支加入:

HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);

数值越小抢占优先级越高。实际项目应结合其他中断耗时统一规划,不能把所有外设都随意设为最高优先级。

1.2.2 分析q工程

主函数定义接收缓冲区并在初始化后启动一次:

uint8_t UART_Recv_IT[5] = {0};

HAL_UART_Receive_IT(&huart1, UART_Recv_IT, 4);

缓冲区最后一字节保留给 '\0'。调用后主循环可以继续执行;若返回 HAL_BUSY 或 HAL_ERROR,接收任务并未正确启动,正式代码应检查返回值。

1.2.3 callback函数分析

应用回调:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        UART_Recv_IT[4] = '\0';

        if (strcmp("LED1", (char *)UART_Recv_IT) == 0)
            HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
        else if (strcmp("LED2", (char *)UART_Recv_IT) == 0)
            HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);

        memset(UART_Recv_IT, 0, sizeof(UART_Recv_IT));
        HAL_UART_Receive_IT(&huart1, UART_Recv_IT, 4);
    }
}

回调处于中断上下文,应短小、不可延时。重新启动接收是持续工作的必要步骤,不是多余代码。

1.2.4 中断处理函数分析

硬件收到字节后不会直接调用应用回调。调用链为:

flowchart TD
    A[USART1产生IRQ] --> B[向量表找到USART1_IRQHandler]
    B --> C[stm32f1xx_it.c函数实体]
    C --> D[HAL_UART_IRQHandler &huart1]
    D --> E[检查SR/CR1/CR3]
    E --> F[读取DR并令RxXferCount递减]
    F --> G{计数为0?}
    G -- 否 --> A
    G -- 是 --> H[HAL_UART_RxCpltCallback]

手动工程若缺少 USART1_IRQHandler(),编译不一定总能暴露问题:启动文件可能带弱默认处理器,运行后会进入默认死循环。必须明确提供正确函数实体。

1.2.5 外部声明分析

stm32f1xx_it.c 需要访问 uart.c 中定义的句柄:

extern UART_HandleTypeDef huart1;

extern 只声明“变量定义在别处”,不分配新存储。若在两个 C 文件中都写成普通定义,就会产生重复符号。

完整 IRQ:

void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart1);
}

烧录后应选择板载 CH340C 对应 COM 口,因为 USART1 固定连接 PA9/PA10。发送 LED1、LED2 验证中断接收,并观察每秒中断发送文本是否出现。

1.3 任务总结

串口1中断移植检查表:

  1. uart.c/h 中定义并声明 huart1;
  2. USART1、GPIOA 时钟与 PA9/PA10 配置正确;
  3. NVIC 设置 USART1_IRQn 优先级并使能;
  4. stm32f1xx_it.c 声明句柄并实现 USART1_IRQHandler();
  5. 初始化后调用一次 HAL_UART_Receive_IT();
  6. 应用实现 HAL_UART_RxCpltCallback();
  7. 回调末尾重新装载接收;
  8. 处理 ORE 等错误并考虑错误恢复。

空闲事件移植还会增加 DMA 时钟、通道句柄关联、DMA IRQ 和 HAL_UARTEx_RxEventCallback(),基本的“硬件事件→向量→IRQ Handler→HAL→应用回调”链路保持一致。