在串口通信中,除了轮询方式,中断方式能更高效地处理数据收发,尤其适合需要实时响应的场景。本文将详细介绍如何在 STM32 中配置 USART 中断(包括接收中断和空闲中断),并通过中断处理函数实现数据的实时接收与命令解析。
一、为什么需要 USART 中断
轮询方式虽然简单,但在等待数据时会阻塞 CPU,导致系统效率低下。而中断方式让 CPU 可以在没有数据时处理其他任务,只有当数据到来或满足特定条件(如总线空闲)时,才会触发中断并暂停当前任务去处理串口数据。这种方式能显著提高系统的实时性和资源利用率。
对于串口通信,常用的中断有两种:
- 接收中断(RXNE):当接收缓冲区有数据时触发,用于实时接收单个字节。
- 空闲中断(IDLE):当串口总线在数据传输后处于空闲状态时触发,通常用于判断一帧数据(如一个字符串)接收完成。
二、开启 USART 中断(接收中断与空闲中断)
要使用中断,首先需要开启对应的中断使能位。以下是寄存器和库函数两种实现方式:
1. 寄存器方式
// 打开接收中断(RXNEIE,CR1寄存器第5位)和空闲中断(IDLEIE,CR1寄存器第4位)
USART1->CR1 |= (1 << 5); // 使能接收缓冲区非空中断(RXNE)
USART1->CR1 |= (1 << 4); // 使能空闲线路检测中断(IDLE)
2. 库函数方式
// 打开接收中断和空闲中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能RXNE中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 使能IDLE中断
三、配置中断控制器 NVIC
STM32 的中断由 NVIC(嵌套向量中断控制器)管理,需要配置中断优先级和使能中断通道。
1. 中断优先级分组
STM32 的中断优先级由抢占优先级和响应优先级组成,通过优先级分组设置两者的位数。常用的分组方式是 “2+2”(2 位抢占优先级,2 位响应优先级),共支持 16 级优先级。
// 设置中断优先级分组为2(2位抢占优先级,2位响应优先级)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
2. 配置 USART1 中断通道
// 定义NVIC初始化结构体
NVIC_InitTypeDef nvic;
// 指定中断通道为USART1
nvic.NVIC_IRQChannel = USART1_IRQn;
// 使能该中断通道
nvic.NVIC_IRQChannelCmd = ENABLE;
// 设置抢占优先级(1级)
nvic.NVIC_IRQChannelPreemptionPriority = 1;
// 设置响应优先级(1级)
nvic.NVIC_IRQChannelSubPriority = 1;
// 初始化NVIC
NVIC_Init(&nvic);
- 抢占优先级决定中断的嵌套能力(高抢占优先级的中断可以打断低抢占优先级的中断)。
- 响应优先级决定同抢占优先级中断的执行顺序(数值越小,优先级越高)。
四、实现中断处理函数
中断处理函数是中断发生时的执行逻辑,STM32 规定了固定的函数名(如USART1_IRQHandler
)。我们需要在函数中区分中断类型(接收中断 / 空闲中断),并进行相应处理。
1. 全局变量定义
首先定义用于缓存数据和命令的全局变量:
uint8_t buffer[255] = {0}; // 接收缓冲区(最大255字节)
uint8_t data; // 临时变量,用于清除中断标志
int cnt = 0; // 缓冲区计数
// 命令定义
uint8_t cmd0[] = "关灯"; // 关灯命令
uint8_t cmd1[] = "开灯"; // 开灯命令
uint8_t cmd2[] = "放歌"; // 放歌命令
2. 寄存器方式的中断处理函数
void USART1_IRQHandler(void)
{
// 接收中断(RXNE:接收缓冲区非空)
if (USART1->SR & (0X1 << 5)) // 判断SR寄存器第5位(RXNE标志)
{
buffer[cnt++] = USART1->DR; // 读取数据寄存器,保存到缓冲区
// 注意:读取DR会自动清除RXNE标志
}
// 空闲中断(IDLE:总线空闲)
if (USART1->SR & (0X1 << 4)) // 判断SR寄存器第4位(IDLE标志)
{
// 清除空闲中断标志(必须先读SR,再读DR)
data = USART1->SR; // 读SR寄存器
data = USART1->DR; // 读DR寄存器(数据无用,仅用于清除标志)
// 命令匹配与执行
if (strcmp((char*)cmd0, (char*)buffer) == 0)
{
GPIOB->ODR |= (0X1 << 5); // PB5置高,关灯
}
else if (strcmp((char*)cmd1, (char*)buffer) == 0)
{
GPIOB->ODR &= ~(0X1 << 5); // PB5置低,开灯
}
else if (strcmp((char*)cmd2, (char*)buffer) == 0)
{
play_two_tigers(); // 调用放歌函数(需提前实现)
}
// 清空缓冲区,准备下次接收
memset(buffer, 0, cnt);
cnt = 0;
}
}
3. 库函数方式的中断处理函数
void USART1_IRQHandler(void)
{
// 接收中断(RXNE)
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
buffer[cnt++] = USART_ReceiveData(USART1); // 读取数据到缓冲区
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除接收中断标志
}
// 空闲中断(IDLE)
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
// 清除空闲中断标志(先读SR,再读DR)
data = USART1->SR; // 读SR寄存器
data = USART_ReceiveData(USART1); // 读DR寄存器
// 命令匹配与执行
if (strcmp((char*)cmd0, (char*)buffer) == 0)
{
GPIO_SetBits(GPIOB, GPIO_Pin_5); // PB5置高,关灯
}
else if (strcmp((char*)cmd1, (char*)buffer) == 0)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_5); // PB5置低,开灯
}
else if (strcmp((char*)cmd2, (char*)buffer) == 0)
{
play_two_tigers(); // 调用放歌函数
}
// 清空缓冲区
memset(buffer, 0, cnt);
cnt = 0;
}
}
4. 关键逻辑说明
- 接收中断(RXNE):每次收到一个字节就触发,将数据存入
buffer
并递增计数cnt
。 - 空闲中断(IDLE):当串口在数据传输后空闲(无数据达一定时间)时触发,此时认为一帧数据接收完成。我们需要:
- 清除空闲中断标志(必须先读
SR
再读DR
,否则标志无法清除); - 对比缓冲区数据与预设命令,执行对应操作(如开灯、关灯);
- 清空缓冲区,重置计数,准备下一次接收。
- 清除空闲中断标志(必须先读
五、完整代码示例(库函数版)
将上述配置整合,完整代码如下:
#include "stm32f10x.h"
#include <string.h>
// 全局变量
uint8_t buffer[255] = {0};
uint8_t data;
int cnt = 0;
uint8_t cmd0[] = "关灯";
uint8_t cmd1[] = "开灯";
uint8_t cmd2[] = "放歌";
// 函数声明
void USART1_Init(void);
void GPIO_Init_LED(void);
void play_two_tigers(void); // 假设已实现
int main(void)
{
// 初始化LED(PB5)
GPIO_Init_LED();
// 初始化USART1(含中断配置)
USART1_Init();
while (1)
{
// 主循环可处理其他任务,中断会实时响应
}
}
// USART1初始化(含GPIO、USART配置和中断使能)
void USART1_Init(void)
{
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 2. 配置GPIO(PA9=TX,PA10=RX)
GPIO_InitTypeDef gpio;
// PA9:复用推挽输出
gpio.GPIO_Pin = GPIO_Pin_9;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);
// PA10:浮空输入
gpio.GPIO_Pin = GPIO_Pin_10;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);
// 3. 配置USART1
USART_InitTypeDef usart;
usart.USART_BaudRate = 9600;
usart.USART_WordLength = USART_WordLength_8b;
usart.USART_StopBits = USART_StopBits_1;
usart.USART_Parity = USART_Parity_No;
usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
usart.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &usart);
// 4. 配置中断优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef nvic;
nvic.NVIC_IRQChannel = USART1_IRQn;
nvic.NVIC_IRQChannelCmd = ENABLE;
nvic.NVIC_IRQChannelPreemptionPriority = 1;
nvic.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&nvic);
// 5. 使能USART中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
// 6. 使能USART1
USART_Cmd(USART1, ENABLE);
}
// LED初始化(PB5推挽输出)
void GPIO_Init_LED(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef gpio;
gpio.GPIO_Pin = GPIO_Pin_5;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &gpio);
}
// 中断处理函数
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
buffer[cnt++] = USART_ReceiveData(USART1);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
if (USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)
{
data = USART1->SR;
data = USART_ReceiveData(USART1);
if (strcmp((char*)cmd0, (char*)buffer) == 0)
{
GPIO_SetBits(GPIOB, GPIO_Pin_5); // 关灯
}
else if (strcmp((char*)cmd1, (char*)buffer) == 0)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_5); // 开灯
}
else if (strcmp((char*)cmd2, (char*)buffer) == 0)
{
play_two_tigers(); // 放歌
}
memset(buffer, 0, cnt);
cnt = 0;
}
}
六、总结与注意事项
中断标志的清除:
- 接收中断(RXNE):读取
DR
寄存器后自动清除。 - 空闲中断(IDLE):必须先读
SR
寄存器,再读DR
寄存器才能清除,否则会重复触发中断。
- 接收中断(RXNE):读取
缓冲区溢出处理:
示例中未处理缓冲区溢出(cnt
超过 255),实际应用中需添加判断(如if (cnt >= 255) cnt = 0;
)。命令匹配的局限性:
示例使用strcmp
匹配命令,要求输入与命令完全一致(包括长度)。实际应用中可使用字符串查找函数(如strstr
)提高灵活性。中断优先级的设置:
根据系统需求调整抢占优先级和响应优先级,确保关键中断优先执行。