串口1中断移植
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中断移植检查表:
uart.c/h中定义并声明huart1;- USART1、GPIOA 时钟与 PA9/PA10 配置正确;
- NVIC 设置 USART1_IRQn 优先级并使能;
stm32f1xx_it.c声明句柄并实现USART1_IRQHandler();- 初始化后调用一次
HAL_UART_Receive_IT(); - 应用实现
HAL_UART_RxCpltCallback(); - 回调末尾重新装载接收;
- 处理 ORE 等错误并考虑错误恢复。
空闲事件移植还会增加 DMA 时钟、通道句柄关联、DMA IRQ 和 HAL_UARTEx_RxEventCallback(),基本的“硬件事件→向量→IRQ Handler→HAL→应用回调”链路保持一致。