嵌入式裸机&RTOS开发杂谈系列
第二章 串口空闲中断原理与用DMA实现不定长数据接收(STM32F4)
文章目录
前言
在串口实际应用中,经常会碰到不定长数据的接收。本文讨论用DMA+串口的空闲中断,实现不定长数据接收。
一、串口空闲中断原理
第一次接触这个概念的人可能会有一个疑问,在没有数据时会不会触发空闲中断?答案:不会。
先来抽象个实际接收不定长数据的情况,总线第一次空闲,总线有一帧数据在传输,总线第二次空闲…按照这一规律循环直到传输结束。
实际上总线第一次空闲不会产生空闲中断,总线第二次空闲才会产生空闲中断,简单点来说:一直没有接收到数据是不会产生空闲中断的,只有当已经开始接收数据了,然后数据停止传输一段时间(一个字节传输的时间)之后才会触发空闲中断。
1.触发条件
当串口接收数据后,检测到RX引脚空闲时间超过1个字符帧传输时间时触发空闲中断。例如:波特率9600时,1字符帧(8位数据+1停止位)传输时间为1.041ms,若空闲时间超过此值则触发中断。
2.中断特性
空闲中断标志(IDLE)需手动清除,通过先读USART_SR再读USART_DR实现。
仅在一次接收数据后检测空闲状态,避免误触发。
二、DMA+空闲中断
在STM32F4的串口通信中,DMA(直接内存访问)与空闲中断的结合为数据的收发提供了一种高效且灵活的方式。
1.接收不定长数据
思路:DMA搬运串口接收的数据到缓冲区,开启串口空闲中断,当数据停止传输一段时间(一个字符传输所需的时间)时就会触发串口空闲中断(此时一帧不定长的数据也接收完毕了,没接收完毕不会停止一段时间),在串口空闲中断的服务函数中对缓冲区的数据进行处理。
// 配置步骤(以USART1为例)
void USART1_Init() {
// 1. 使能GPIO、USART、DMA时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_DMA2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
// 2. 配置GPIO为复用推挽输出(TX)和浮空输入(RX)
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
// 3. 配置USART参数(波特率115200,8N1)
USART_InitTypeDef USART_InitStruct = {115200, USART_WordLength_8b, ...};
USART_Init(USART1, &USART_InitStruct);
// 4. 配置DMA接收(外设→内存)
DMA_InitTypeDef DMA_InitStruct = {
.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR,
.DMA_Memory0BaseAddr = (uint32_t)rx_buffer,
.DMA_BufferSize = RX_BUF_SIZE,
.DMA_DIR = DMA_DIR_PeripheralToMemory,
.DMA_Mode = DMA_Mode_Circular, // 循环模式适配不定长数据
.DMA_Priority = DMA_Priority_High
};
DMA_Init(DMA2_Stream5, &DMA_InitStruct);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
// 5. 使能空闲中断
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
NVIC_EnableIRQ(USART1_IRQn);
}
// 中断服务函数
void USART1_IRQHandler() {
// 串口空闲中断
if (USART_GetITStatus(USART1, USART_IT_IDLE)) {
USART_ReceiveData(USART1); // 清除IDLE标志
DMA_Cmd(DMA2_Stream5, DISABLE);
// 计算接收数据长度
uint16_t data_len = RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA2_Stream5);
process_data(rx_buffer, data_len); // 处理数据
// 重启DMA接收
DMA_SetCurrDataCounter(DMA2_Stream5, RX_BUF_SIZE);
DMA_Cmd(DMA2_Stream5, ENABLE);
}
}
2. DMA发送数据
思路:直接调用发送函数,数据大小自由定义。
void USART1_SendData_DMA(uint8_t *data, uint16_t len) {
// 配置DMA发送(内存→外设)
DMA_InitTypeDef DMA_InitStruct = {
.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR,
.DMA_Memory0BaseAddr = (uint32_t)data,
.DMA_BufferSize = len,
.DMA_DIR = DMA_DIR_MemoryToPeripheral,
.DMA_Mode = DMA_Mode_Normal // 单次传输模式
};
DMA_Init(DMA2_Stream7, &DMA_InitStruct);
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
// 等待发送完成(或使用DMA传输完成中断)
while (DMA_GetFlagStatus(DMA2_Stream7, DMA_FLAG_TCIF7) == RESET);
}
3.关键点说明
1. DMA模式选择
接收:使用普通模式(DMA_Mode_Circular),接收完成后需重新配置。
发送:使用普通模式(DMA_Mode_Normal),发送完成后需重新配置。
2. 中断优化
接收时仅开启空闲中断,关闭RXNE中断以减少CPU负担。
发送完成后可通过DMA传输完成中断(DMA_IT_TC)通知CPU。