1. STM32串口中断接收基础
串口通信是嵌入式开发中最常用的外设之一,而中断接收模式则是处理不定长数据的黄金方案。我用STM32做过十几个项目,几乎每个都会用到串口中断接收,这里分享最实用的经验。
STM32的USART外设支持多种工作模式,我们需要重点关注异步通信和中断机制。配置一个基础的中断接收流程需要5个关键步骤:
- 使能USART和GPIO时钟(RCC寄存器配置)
- 初始化GPIO为复用功能(AF模式)
- 配置USART参数(波特率、数据位、停止位等)
- 设置NVIC中断优先级并使能中断
- 编写中断服务函数(ISR)
具体到代码实现,以STM32F103为例:
// 1. 时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. GPIO初始化 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; // TX GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; // RX GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. USART参数配置 USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate = 115200; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStruct); // 4. NVIC配置 NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); // 5. 使能中断和USART USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE);1.1 中断服务函数实现
当中断触发时,硬件会自动跳转到中断服务函数。一个典型的中断服务函数应该包含:
- 检查中断来源(RXNE标志)
- 读取接收数据寄存器(USART_DR)
- 清除中断标志(自动或手动)
void USART1_IRQHandler(void) { uint8_t data; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { data = USART_ReceiveData(USART1); // 读取数据 // 这里可以添加数据处理逻辑 } }常见坑点:我曾遇到过中断频繁触发的问题,后来发现是忘记清除中断标志。虽然读取USART_DR会自动清除RXNE标志,但在复杂场景下建议手动清除:
USART_ClearITPendingBit(USART1, USART_IT_RXNE);2. 自定义协议设计实战
裸机环境下处理不定长数据帧是个技术活。正点原子例程中的状态变量法(USART_RX_STA)非常经典,但实际项目中我们需要更灵活的方案。
2.1 协议帧结构设计
常见的串口协议帧包含以下要素:
- 帧头:1-2字节固定值(如0xAA、0x55)
- 长度字段:指示数据域长度(1-2字节)
- 数据域:有效载荷
- 校验字段:CRC8/CRC16等
- 帧尾:可选(如0x0D 0x0A)
示例协议帧:
[帧头0xAA][长度][数据...][CRC8][帧尾0x0D 0x0A]2.2 状态机实现
在中断中实现协议解析状态机是最可靠的方式。我常用的状态定义如下:
typedef enum { STATE_HEADER1, STATE_HEADER2, STATE_LENGTH, STATE_DATA, STATE_CRC, STATE_TAIL } ParserState;对应的中断服务函数实现:
#define BUF_SIZE 256 uint8_t rx_buf[BUF_SIZE]; uint16_t rx_index = 0; ParserState state = STATE_HEADER1; void USART1_IRQHandler(void) { uint8_t data = USART_ReceiveData(USART1); switch(state) { case STATE_HEADER1: if(data == 0xAA) state = STATE_HEADER2; break; case STATE_HEADER2: if(data == 0x55) { state = STATE_LENGTH; rx_index = 0; } else { state = STATE_HEADER1; } break; case STATE_LENGTH: pkg_length = data; state = STATE_DATA; break; case STATE_DATA: rx_buf[rx_index++] = data; if(rx_index >= pkg_length) state = STATE_CRC; break; case STATE_CRC: // 校验处理 state = STATE_TAIL; break; case STATE_TAIL: if(data == 0x0D) { // 完整帧接收完成 process_packet(rx_buf, pkg_length); } state = STATE_HEADER1; break; } }优化技巧:在实际项目中,我会使用环形缓冲区作为接收缓存,避免数据覆盖问题。下面是简化实现:
#define BUF_SIZE 256 typedef struct { uint8_t buffer[BUF_SIZE]; uint16_t head; uint16_t tail; } RingBuffer; RingBuffer rx_ring; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(USART1); uint16_t next = (rx_ring.head + 1) % BUF_SIZE; if(next != rx_ring.tail) { // 缓冲区未满 rx_ring.buffer[rx_ring.head] = data; rx_ring.head = next; } } }3. 数据帧处理与错误恢复
数据完整性是通信协议的核心。我在实际项目中遇到过各种异常情况,总结出以下处理经验:
3.1 超时机制实现
使用定时器实现帧接收超时检测:
// 在中断中重置计时器 void USART1_IRQHandler(void) { // ...接收数据处理... TIM_SetCounter(TIM4, 0); // 重置计时器 TIM_Cmd(TIM4, ENABLE); } // 定时器中断中处理超时 void TIM4_IRQHandler(void) { if(TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); TIM_Cmd(TIM4, DISABLE); // 超时处理逻辑 state = STATE_HEADER1; // 重置状态机 } }3.2 校验算法选择
CRC校验比简单的累加和更可靠。以下是CRC8的实现:
uint8_t crc8(const uint8_t *data, uint16_t len) { uint8_t crc = 0xFF; while(len--) { crc ^= *data++; for(uint8_t i=0; i<8; i++) { crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1); } } return crc; }3.3 错误恢复策略
当检测到以下情况时应重置状态机:
- 帧头不匹配
- 数据长度超过最大值
- 校验失败
- 接收超时
void reset_parser(void) { state = STATE_HEADER1; rx_index = 0; TIM_Cmd(TIM4, DISABLE); }4. 性能优化与实战技巧
高效稳定的串口通信需要综合考虑多方面因素。以下是几个关键优化点:
4.1 中断优先级配置
根据系统需求合理设置中断优先级:
NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级 NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; // 子优先级 NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct);经验法则:串口中断优先级应高于非实时任务,但低于硬件关键任务(如电机控制)。
4.2 DMA结合中断的混合模式
对于高速数据传输,可以采用DMA+中断的混合模式:
// DMA配置 DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)rx_buffer; DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStruct.DMA_BufferSize = BUF_SIZE; DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; DMA_InitStruct.DMA_Priority = DMA_Priority_High; DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &DMA_InitStruct); // 使能DMA USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); // 配置空闲中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);在空闲中断中处理数据:
void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { USART_ReceiveData(USART1); // 清除IDLE标志 uint16_t remain = DMA_GetCurrDataCounter(DMA1_Channel5); uint16_t received = BUF_SIZE - remain; process_data(rx_buffer, received); DMA_SetCurrDataCounter(DMA1_Channel5, BUF_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }4.3 标志位使用注意事项
STM32的USART有多个关键标志位,容易混淆:
| 标志位 | 全称 | 触发条件 | 清除方式 |
|---|---|---|---|
| RXNE | Read data register not empty | 接收数据寄存器非空 | 读DR寄存器或手动清除 |
| TC | Transmission complete | 一帧数据发送完成 | 读SR+写DR或手动清除 |
| TXE | Transmit data register empty | 发送数据寄存器空 | 写DR寄存器 |
常见问题:我曾遇到过TC标志未及时清除导致发送卡死的情况,后来发现需要在发送最后字节后等待TC置位:
void USART_SendData(USART_TypeDef* USARTx, uint8_t data) { USARTx->DR = (data & 0xFF); while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); }