嵌入式裸机&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。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐