串口通信USART-2:深入解析STM32的USART中断机制与实战应用
1. 什么是USART中断?为什么需要它?
大家好,我是熊哥。今天我们来聊聊STM32的USART中断机制,这是串口通信中非常实用的技术。如果你之前用过轮询方式处理串口数据,可能会发现当数据量大时,CPU大部分时间都在等待数据,效率很低。而中断方式就像有个"门铃",数据到达时才会通知CPU处理,大大提高了效率。
USART中断的工作原理其实很简单:当特定事件发生时(比如接收到数据或发送完成),USART外设会向CPU发送一个中断请求,CPU暂停当前工作去处理这个事件,处理完再回到原来的任务。这种方式特别适合实时性要求高的场景,比如接收传感器数据、控制执行器等。
我在实际项目中就遇到过这样的情况:需要同时处理多个传感器的数据,如果使用轮询方式,很可能会丢失数据。改用中断后,不仅数据接收稳定,CPU还有余力处理其他任务。接下来,我会详细讲解如何配置和使用USART中断。
2. USART中断的配置步骤
2.1 硬件准备与引脚配置
首先,我们需要确定使用哪个USART外设和对应的GPIO引脚。以STM32F103系列为例,USART1的TX引脚是PA9,RX引脚是PA10。配置GPIO时,TX引脚需要设置为复用推挽输出模式,RX引脚设置为上拉输入模式。
记得先使能对应的时钟,USART1挂载在APB2总线上,所以需要使能APB2时钟和GPIOA的时钟。这里有个小技巧:如果使用其他USART(如USART2、USART3),它们可能挂载在APB1总线上,时钟使能函数会有所不同,这点要特别注意。
// 使能USART1和GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置TX引脚(PA9)为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置RX引脚(PA10)为上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
2.2 USART参数初始化
接下来配置USART的基本参数,包括波特率、数据位、停止位、校验位等。常用的配置是115200波特率、8位数据位、1位停止位、无校验位。这里要注意波特率的计算,特别是当使用非标准时钟频率时,需要仔细计算分频系数。
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
2.3 中断配置与NVIC设置
这是中断配置的核心部分。我们需要使能接收中断(USART_IT_RXNE),这样每当接收到一个字节时就会触发中断。然后配置NVIC(嵌套向量中断控制器),设置中断优先级和使能中断通道。
我建议将USART中断的抢占优先级设置为中等,这样既能保证及时响应,又不会影响更高优先级的中断。实际项目中,要根据整个系统的中断安排来合理设置优先级。
// 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 最后使能USART
USART_Cmd(USART1, ENABLE);
3. 编写中断服务函数
3.1 中断服务函数的基本结构
中断服务函数是中断处理的核心,它需要完成以下工作:检查中断类型、处理数据、清除中断标志。函数名必须与启动文件中的中断向量表一致,对于USART1,函数名必须是USART1_IRQHandler。
在函数内部,首先要检查是否是接收中断(USART_IT_RXNE),因为USART可能产生多种中断(发送完成、校验错误等)。只有确认是接收中断后才进行数据处理,处理完成后必须清除中断标志,否则会不断进入中断。
void USART1_IRQHandler(void)
{
uint8_t received_data;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 读取接收到的数据
received_data = USART_ReceiveData(USART1);
// 处理数据...
// 清除中断标志
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
3.2 数据处理与缓冲区管理
在中断服务函数中直接处理复杂逻辑是不推荐的,因为中断应该尽快执行完毕。更好的做法是将数据存入缓冲区,在主循环中处理。我通常使用环形缓冲区(FIFO)来存储接收到的数据。
环形缓冲区的实现需要两个指针:读指针和写指针。当写入数据时,写指针递增;读取数据时,读指针递增。当指针到达缓冲区末尾时,回绕到开头。这种设计可以高效地处理连续数据流。
#define BUFFER_SIZE 256
typedef struct {
uint8_t buffer[BUFFER_SIZE];
uint16_t head;
uint16_t tail;
} ring_buffer;
ring_buffer rx_buffer;
// 在中断中将数据存入缓冲区
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 将数据存入环形缓冲区
uint16_t next = (rx_buffer.head + 1) % BUFFER_SIZE;
if(next != rx_buffer.tail) { // 缓冲区未满
rx_buffer.buffer[rx_buffer.head] = data;
rx_buffer.head = next;
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
4. 实战应用:通过串口控制LED状态
4.1 项目需求与硬件连接
现在我们来实现一个实际应用:通过串口发送指令控制LED灯的开关。当接收到字符'1'时点亮LED,接收到'0'时熄灭LED。这个例子虽然简单,但包含了中断处理的基本要素,可以扩展到更复杂的控制场景。
硬件连接很简单:USART1的TX、RX引脚通过USB转串口模块连接到电脑,同时连接一个LED到某个GPIO引脚(如PB5)。记得给LED串联一个限流电阻(通常220Ω-1kΩ)。
// LED初始化
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
4.2 完整代码实现
下面是完整的代码实现,包括初始化、中断处理和主循环。在主循环中,我们检查缓冲区中是否有数据,如果有就处理LED控制指令。这种设计将中断处理和数据 processing 分离,保证了系统的实时性和稳定性。
我在实际测试中发现,即使高速发送数据,这种架构也能稳定工作,不会丢失数据。你可以根据需要调整缓冲区大小,平衡内存使用和性能。
#include "stm32f10x.h"
#include <stdio.h>
#define BUFFER_SIZE 256
typedef struct {
uint8_t buffer[BUFFER_SIZE];
uint16_t head;
uint16_t tail;
} ring_buffer;
ring_buffer rx_buffer;
void USART1_Init(void);
void LED_Init(void);
void Buffer_Put(uint8_t data);
uint8_t Buffer_Get(uint8_t *data);
int main(void)
{
USART1_Init();
LED_Init();
while(1)
{
uint8_t data;
if(Buffer_Get(&data))
{
if(data == '1')
{
GPIO_SetBits(GPIOB, GPIO_Pin_5); // LED亮
}
else if(data == '0')
{
GPIO_ResetBits(GPIOB, GPIO_Pin_5); // LED灭
}
}
}
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
Buffer_Put(data);
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
void Buffer_Put(uint8_t data)
{
uint16_t next = (rx_buffer.head + 1) % BUFFER_SIZE;
if(next != rx_buffer.tail) {
rx_buffer.buffer[rx_buffer.head] = data;
rx_buffer.head = next;
}
}
uint8_t Buffer_Get(uint8_t *data)
{
if(rx_buffer.head == rx_buffer.tail) {
return 0; // 缓冲区空
}
*data = rx_buffer.buffer[rx_buffer.tail];
rx_buffer.tail = (rx_buffer.tail + 1) % BUFFER_SIZE;
return 1;
}
5. 常见问题与调试技巧
5.1 中断不触发的问题排查
在实际使用中,可能会遇到中断不触发的情况。根据我的经验,这通常是由以下几个原因造成的:首先检查NVIC配置是否正确,中断通道和优先级设置是否正确;其次确认USART中断是否使能;最后检查总中断是否开启(__enable_irq())。
还有一个常见的坑是忘记清除中断标志,这会导致中断不断触发,系统卡死在中断中。建议在调试时使用printf输出调试信息,但要避免在中断中频繁使用printf,因为这可能引起阻塞。
5.2 数据丢失与缓冲区溢出
当数据接收速度超过处理速度时,会发生缓冲区溢出,导致数据丢失。解决方法包括:增大缓冲区大小、提高数据处理效率、使用流控制(RTS/CTS)等。
我通常会在缓冲区接近满时点亮一个警告LED,或者发送流控制信号让发送方暂停。在实际项目中,还可以使用DMA配合中断来高效处理大量数据。
// 缓冲区状态检查
uint16_t Buffer_FreeSize(void)
{
if(rx_buffer.head >= rx_buffer.tail) {
return BUFFER_SIZE - (rx_buffer.head - rx_buffer.tail) - 1;
} else {
return rx_buffer.tail - rx_buffer.head - 1;
}
}
// 在中断中检查缓冲区状态
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
if(Buffer_FreeSize() > 10) { // 保留一定余量
uint8_t data = USART_ReceiveData(USART1);
Buffer_Put(data);
} else {
// 缓冲区快满了,可以触发流控制或丢弃数据
// 读取数据但不存储,避免卡死
USART_ReceiveData(USART1);
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
5.3 波特率误差与通信稳定性
波特率误差过大会导致通信错误,特别是当使用非标准波特率时。STM32的USART波特率发生器支持小数分频,可以较精确地产生各种波特率。
计算波特率时要注意时钟源的选择,不同USART可能挂载在不同总线上,时钟频率也不同。USART1在APB2上,最高72MHz,其他USART在APB1上,最高36MHz。使用标准波特率(如9600、115200)可以减少误差。
我在长期使用中发现,加入简单的协议格式可以提高通信可靠性,比如添加帧头、帧尾和校验和。即使偶尔出现误码,也能通过校验发现并重传。
更多推荐


所有评论(0)