STM32F103ZET6的USART 中断配置详解
在串口通信中,除了轮询方式,中断方式能更高效地处理数据收发,尤其适合需要实时响应的场景。本文将详细介绍如何在 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. 配置USART1USART_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. 使能USART1USART_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
)提高灵活性。 -
中断优先级的设置:
根据系统需求调整抢占优先级和响应优先级,确保关键中断优先执行。
0voice · GitHub