STM32 实战教程:USART3 + DMA +IDLE中断实现高效不定长收发

68 阅读7分钟

1. 方案概述

在嵌入式开发中,串口通信是最常用的功能。在前面博客中讲述了通过轮询和中断实现方式,但是都有各自的缺点,在高速或大数据量时会被频繁打断CPU,效率低下。

本教程使用STM32F103实现以下功能:

  • DMA接收:数据直接由DMA搬运到内存缓冲区,不占用CPU。
  • IDLE中断:利用串口的空闲中断检测一帧数据的结束,实现不定长数据接收。
  • DMA发送:发送数据同样使用DMA,发送完成后CPU才会介入(或完全不介入)。
  • 回环测试:将接收到的数据通过DMA原样发回(ECHO)。

2. 硬件与引脚分配

以 STM32F103为例:使用USART3:

功能引脚模式DMA通道
TXPB10复用推挽输出DMA1_Channel2
RXPB11浮空输入DMA1_Channel3

3. 代码核心详解

我们将代码分为三个部分:配置头文件、驱动实现、主逻辑

3.1 头文件配置(usart3.h)

  • volatile 关键字很重要,因为 g_u3_rx_flag 和 g_u3_rx_len 会在中断函数中被修改,并在主循环中被读取,防止编译器优化。
#ifndef __USART3_H
#define __USART3_H#include "stm32f10x.h"                  // Device header
#include <string.h>#define USART3_BUFFER_SIZE      256     // 定义最大接收、发送长度extern uint8_t  usart3_rx_buffer[USART3_BUFFER_SIZE];   // 接收缓冲
extern uint8_t  usart3_tx_buffer[USART3_BUFFER_SIZE];   // 发送缓冲
extern volatile uint8_t g_u3_rx_flag;                   // 接收完成标志(1:完成;0:未完成)
extern volatile uint16_t g_u3_rx_len;                   // 接收到的数据长度// 函数声明
void usart3_dma_init(void);
void usart3_dma_tx_data(uint8_t *buffer, uint16_t len);
void usart3_resume_rx(void);
​
#endif

3.2 核心驱动实现(usart3.c)

A. 初始化函数 usart3_dma_init

这个函数完成了时钟初始化、GPIO、USART和DMA的全部配置

关键点解析:

  1. 时钟开启:注意USART3挂载在APB1,而GPIO和DMA挂载在APB2和AHB。

  2. IDLE中断:USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);是检测不定长数据的核心。当总线空闲,一帧数据发完了时,触发中断。

  3. DMA配置:

    • RX(Channel3):DMA_Mode_Normal(普通模式),因为我们需要在中断中手动处理完数据后,再重新开启DMA,这样更可控。
    • TX(Channel2):初始化时不开启,只有发送函数调用时才开启。
uint8_t usart3_rx_buffer[USART3_BUFFER_SIZE];
uint8_t usart3_tx_buffer[USART3_BUFFER_SIZE];
volatile uint8_t g_u3_rx_flag   =   0;
volatile uint16_t g_u3_rx_len   =   0;
​
/**
 * @brief 初始化 usart3 + dma + nvic
 */
 
void usart3_dma_init(void) {
    GPIO_InitTypeDef    GPIO_InitStructure;
    USART_InitTypeDef   USART_InitStructure;
    DMA_InitTypeDef     DMA_InitStructure;
    NVIC_InitTypeDef    NVIC_InitStructure;
    
    // 1.开启时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    // 2.配置 GPIO (PB10 Tx,PB11 Rx)
    // TX:复用推挽
    GPIO_InitStructure.GPIO_Pin     =   GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode    =   GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed   =   GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    // RX:浮空输入
    GPIO_InitStructure.GPIO_Pin     =   GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Mode    =   GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // 3.配置 USART3
    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(USART3, &USART_InitStructure);
    
    // 开启IDLE(空闲)中断,用于检测不定长数据接收完成
    USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
    
    // 开启USART3 的DMA请求
    USART_DMACmd(USART3, USART_DMAReq_Tx | USART_DMAReq_Rx, ENABLE);
    
    // 4. 配置NVIC(中断优先级)
    NVIC_InitStructure.NVIC_IRQChannel  =   USART3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd   =   ENABLE;
    NVIC_Init(&NVIC_InitStructure);
    
    // 5. 配置DMA1_Channel3(用于接收RX)
    DMA_DeInit(DMA1_Channel3);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART3->DR;           //  外设地址(数据寄存器)
    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)usart3_rx_buffer;      //  内存地址(接收缓冲区)
    DMA_InitStructure.DMA_DIR                =  DMA_DIR_PeripheralSRC;          //  外设-》内存
    DMA_InitStructure.DMA_BufferSize         =  USART3_BUFFER_SIZE;             
    DMA_InitStructure.DMA_PeripheralInc      =  DMA_PeripheralInc_Disable;      //  DR地址不变
    DMA_InitStructure.DMA_MemoryInc          =  DMA_MemoryInc_Enable;           //  内存地址递增
    DMA_InitStructure.DMA_PeripheralDataSize =  DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize     =  DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode               =  DMA_Mode_Normal;                //  单次模式(非循环)
    DMA_InitStructure.DMA_Priority           =  DMA_Priority_VeryHigh;
    DMA_InitStructure.DMA_M2M                =  DMA_M2M_Disable;
    DMA_Init(DMA1_Channel3, &DMA_InitStructure);
    
    // 立即开启接收 DMA
    DMA_Cmd(DMA1_Channel3, ENABLE);
    
    // 6. 配置 DMA1_Channel2(用于发送TX)
    DMA_DeInit(DMA1_Channel2);
    DMA_InitStructure.DMA_PeripheralBaseAddr    =   (uint32_t)&USART3->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr        =   (uint32_t)usart3_tx_buffer;
    DMA_InitStructure.DMA_DIR                   =   DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize            =   0;
    DMA_Init(DMA1_Channel2, &DMA_InitStructure);
    // 注意:TX DMA先不开启,要等到数据发送时再开
    
    // 7. 最后使能串口
    USART_Cmd(USART3, ENABLE);
}

B.DMA 发送函数 usart3_dma_tx_data

void usart3_dma_tx_data(uint8_t *buffer, uint16_t len) {
    // 1. 等待上一次发送完成 (防止覆盖数据)
    while(DMA_GetCurrDataCounter(DMA1_Channel2) != 0);
    
    // 2. 关闭 DMA 才能修改配置
    DMA_Cmd(DMA1_Channel2, DISABLE);
    
    // 3. 更新数据源地址和长度
    DMA1_Channel2->CMAR = (uint32_t)buffer;     // 设置内存地址
    DMA_SetCurrDataCounter(DMA1_Channel2, len); // 设置发送长度
    
    // 4. 启动发送
    DMA_Cmd(DMA1_Channel2, ENABLE);
}

C.重启接收函数 usart3_resume_rx

主函数处理完数据后,需要调用此函数让DMA准备好接收下一包数据

void usart3_resume_rx(void) {
    DMA_Cmd(DMA1_Channel3, DISABLE); // 必须先关
    DMA_SetCurrDataCounter(DMA1_Channel3, USART3_BUFFER_SIZE); // 重置接收长度计数器
    // 清除所有中断标志,防止误触发
    DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_HT3 | DMA1_FLAG_TE3 | DMA1_FLAG_GL3);
    DMA_Cmd(DMA1_Channel3, ENABLE);  // 重新开
}

D.中断服务函数

void USART3_IRQHandler(void) {
    uint32_t temp;
    
    // 判断是否是空闲中断 (IDLE)
    if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) {
        // 1. 清除 IDLE 标志位
        // 查阅手册:清除 IDLE 需要先读 SR 寄存器,再读 DR 寄存器
        temp = USART3->SR;
        temp = USART3->DR; 
        (void)temp; // 防止编译器警告未使用变量
        
        // 2. 关闭 DMA,防止数据处理期间有新数据进来导致混乱
        DMA_Cmd(DMA1_Channel3, DISABLE);
        
        // 3. 计算接收到的数据长度
        // DMA_GetCurrDataCounter 返回的是“剩余”需要传输的数量
        // 实际接收 = 总大小 - 剩余大小
        g_u3_rx_len = USART3_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel3);
        
        // 4. 设置标志位,通知 main()
        if (g_u3_rx_len > 0) {
            g_u3_rx_flag = 1; 
        } else {
            // 如果是异常触发,直接恢复接收
            usart3_resume_rx();
        }
    }
}

3.3 主逻辑(main.c)、

int main(void)
{
    // ... 初始化代码 ...
    usart3_dma_init();
    
    // 发送开机欢迎语
    char *msg = "System Init OK. Echo Mode Start...\r\n";
    strcpy((char*)usart3_tx_buffer, msg);
    usart3_dma_tx_data(usart3_tx_buffer, strlen(msg));
    
    while(1) {
        // LED 心跳,证明系统未死机
        bsp_led_toggle();
        delay_ms(500); 
        
        // 轮询标志位
        if (g_u3_rx_flag == 1) {
            // 1. 处理数据 (这里是回显操作)
            // 将接收 buffer 拷贝到发送 buffer
            memcpy(usart3_tx_buffer, usart3_rx_buffer, g_u3_rx_len);
            
            uint16_t current_len = g_u3_rx_len;
            
            // 2. 清除标志位
            g_u3_rx_flag = 0;
            
            // 3. 关键:先恢复接收 DMA,再处理发送
            // 这样能最大程度减少“盲区时间”,防止漏接下一包数据
            usart3_resume_rx();
            
            // 4. 发送数据
            usart3_dma_tx_data(usart3_tx_buffer, current_len);
        }
    }
}

4. 注意事项:

  1. 清除 IDLE 标志位: 代码中 temp = USART3->SR; temp = USART3->DR; 是标准库清除 IDLE 标志的序列。如果只调用 USART_ClearITPendingBit 可能无法清除 IDLE 标志,导致一直进入中断。
  2. DMA 接收长度计算: DMA 寄存器 CNDTR 实际上是倒计数的。初始化时设为 256,接收到 10 个字节后,它会变成 246。所以 RxLen = 256 - 246 = 10。
  3. 中断优先级: 如果在使用了 FreeRTOS 等系统,或者有其他高频中断(如 ADC、定时器),务必合理设置 NVIC 的优先级。串口通信通常优先级设置为中等即可。
  4. Cache 一致性 (STM32F1) : 在 STM32F1 系列(Cortex-M3)上没有 D-Cache,所以不用担心 DMA 和 CPU 访问内存不一致的问题。如果是 F7 或 H7 系列,则需要考虑 Cache Clean/Invalidate 操作。