目录

  01  开 篇

  02  环境说明

  03  设计思路

  04  源 码

  05  总 结


  01  开 篇

串口,作为人机交互的“桥梁”,不论是对内调试,还是对外交互,几乎都有它的身影;平时最常见的一种使用场景就是:数据随时可能到来,且数据长度不固定

因此,本篇小记的内容是结合这种使用场景,分享使用DMA通道+串口中断实现收发不定长数据的方法


  02  环境说明

· MCU型号:STM32F103C8T6;

· 标准库版本:StdPeriph_Lib_V3.6.0;

· 开发环境:Keil 5,版本V5.36;


  03  设计思路

整个串口驱动分为两个部分:DMA+发送完成中断实现不定长数据的发送、DMA+接收完成中断实现不定长数据的接收

先介绍不定长数据的发送部分,这部分的核心有两个,一是使用DMA处理发送数据流,只需要使能串口的DMA发送通道,数据会被自动填充到串口的数据寄存器中,无须手动填充;二是使用串口的发送完成中断,当数据发送完成后,触发中断,在中断处理中设置发送完成标志,既可以作为状态判断是否发送完成,也能避免下一次发送与前一帧数据冲突,整个运行逻辑如下。

再介绍不定长数据的接收部分,设计思路有两个核心,一是利用串口DMA通道实现串口数据的自动转存,在转存的过程中DMA通道也会有记录下传输数量;二是利用串口的空闲中断,这个中断的含义是当一帧数据接收完成之后超过一个字节的时间,才会触发;这样一来,在整个数据接收过程中,我们除了在空闲中断中复制接收的数据、计算数据长度,全程无须手动控制,充分解放内核。


  04  源 码

整个运行逻辑已经很清楚了,接着就是源码的实现,总共涉及三个文件:串口源文件、串口头文件、中断处理源文件;这里我以USART2为例,那么我需要用到的三个文件分别是USART2.c、USART2.h、stm32f10x_it.c

第一步,先在USART2.h头文件中创建所需要的通信结构体以及相关接口函数声明,源码如下:

/********************************************
*说 明:此头文件为USART2底层驱动相关宏定义
*创 建:MaJinWen
*时 间:2023-02-02
*版 本:V_1.0.0
*修 改:none
**********************************************/

#ifndef __USART2_H__
#define __USART2_H__

#include <stdint.h>

#define USART1_DATA_NUM  256  //缓存空间,512byte

typedef struct{
    uint16_t TranSts;     //USART1发送状态
    uint16_t RecSts;      //USART1接受状态

    uint16_t TranNum;     //USART1发送数量
    uint16_t RecNum;      //USART1接受数量

    uint8_t txbuffer[USART1_DATA_NUM];  //USART1发送缓存
    uint8_t rxbuffer[USART1_DATA_NUM];  //USART1接受缓存
}tUsart2;

extern tUsart2 st_usart2_com;  //usart1通信结构体

/*RS-485(USART2)初始化*/
void RS485_init(void);

/*Configures the DMA.*/
void DMA_Configuration(void);

/*USART1 transmit data.*/
uint8_t usart2_transmit_data(uint16_t num, uint8_t *data);

/*设置发送状态标志位*/
void set_TranSts(uint16_t status);

/*获取发送状态标志位*/
uint16_t get_TranSts(void);

/*设置接收状态标志位*/
void set_RecSts(uint16_t status);

/*获取发送状态标志位*/
uint16_t get_RecSts(void);

#endif    /*__USART2_H__*/

第二步,在USART2.c源文件中实现usart2的初始化配置、DMA通道配置,以及DMA数据发送和接收的接口函数,源码如下:

/***********************************************************************************
*说 明:此C文件为USART2底层驱动,该USART2串口用于Modbus-RTU通信
*创 建:MaJinWen
*时 间:2025-11-25
*版 本:V_1.0.0
*修 改:none
************************************************************************************/

#include "USART2.h"
#include "stm32f10x_dma.h"
#include "userSystick.h"

#include <stdio.h>
#include <string.h>

#define ARRAYNUM(arr_name)   (uint32_t)(sizeof(arr_name) / sizeof(*(arr_name)))
#define USART2_DR_Base       0x40004404

tUsart2 st_usart2_com;  //usart2通信结构体

/**
  * @brief  RS-485(USART2)初始化
  * @param  None
  * @retval None
  */
void RS485_init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;					//定义一个GPIO结构体变量
    USART_InitTypeDef USART_InitStructure;				    //定义一个串口结构体
    NVIC_InitTypeDef NVIC_InitStructure;

    /*清空USART1数据结构体*/
    memset(&st_usart2_com, 0x00, sizeof(st_usart2_com));

    //使能外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);	 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);       //DMA clock enable

    NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    DMA_Configuration();

    //USART2配置
    USART_DeInit(USART2);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; 				//配置 Tx-Pin
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	   		//复用功能输出推挽
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	   	//配置端口速度为50M
    GPIO_Init(GPIOA, &GPIO_InitStructure);				   	//初始化PA2

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;               //配置 Rx-Pin
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;	//浮空输入(复位状态);	   				
    GPIO_Init(GPIOA, &GPIO_InitStructure);				   	//初始化PA3

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; 				//配置 DEN-Pin
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;	    //推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	   	//配置端口速度为50M
    GPIO_Init(GPIOA, &GPIO_InitStructure);				   	//初始化PA4

    GPIO_ResetBits(GPIOA, GPIO_Pin_4);  //set Receive mode

    USART_InitStructure.USART_BaudRate            = 115200;	             //波特率115200
    USART_InitStructure.USART_WordLength          = USART_WordLength_8b; //8位数据位
    USART_InitStructure.USART_StopBits            = USART_StopBits_1;	 //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(USART2, &USART_InitStructure);	//根据参数初始化串口寄存器

    USART_DMACmd(USART2, USART_DMAReq_Rx | USART_DMAReq_Tx, ENABLE);

    //配置 USART2 接收中断
    USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);		//使能串口中断接收
    USART_ITConfig(USART2, USART_IT_TC, ENABLE);		//使能串口中断接收

    USART_Cmd(USART2, ENABLE);     						//使能串口外设

    delay_1ms(1);
    //使能TC中断后,会进中断,所以要在这里清除一下发送标志位 --2025-12-02-11-00
    set_TranSts(0);    
}


/**
  * @brief  Configures the DMA.
  * @param  None
  * @retval None
  */
void DMA_Configuration(void)
{
    DMA_InitTypeDef DMA_InitStructure;

    /* USART2_DMA_Channel (triggered by USART2 Tx event) Config */
    DMA_DeInit(DMA1_Channel7);
    DMA_InitStructure.DMA_PeripheralBaseAddr = USART2_DR_Base;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)st_usart2_com.txbuffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize = ARRAYNUM(st_usart2_com.txbuffer);
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;      //8bits data
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel7, &DMA_InitStructure);
    /* Disable USART2 TX DMA1 Channel */
    DMA_Cmd(DMA1_Channel7, DISABLE);

    /* USARTy RX DMA1 Channel (triggered by USART2 Rx event) Config */
    DMA_DeInit(DMA1_Channel6);  
    DMA_InitStructure.DMA_PeripheralBaseAddr = USART2_DR_Base;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)st_usart2_com.rxbuffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = ARRAYNUM(st_usart2_com.rxbuffer);
    DMA_Init(DMA1_Channel6, &DMA_InitStructure);
    /* Enable USART2 Rx DMA1 Channel */
    DMA_Cmd(DMA1_Channel6, ENABLE);
}



/**
  * @brief  USART1 transmit data.
  * @param  None
  * @retval None
  */
uint8_t usart2_transmit_data(uint16_t num, uint8_t *data)
{
    uint16_t idx = 0;

    if(num == 0) 
        return 0;

    for(idx = 0; idx < num; idx++)
    {
        st_usart2_com.txbuffer[idx] = data[idx];
    }

    /* Disable USART2 TX DMA1 Channel */
    DMA_Cmd(DMA1_Channel7, DISABLE);
    //配置 DMA
    DMA_SetCurrDataCounter(DMA1_Channel7, num);
    /* Enable USART2 TX DMA1 Channel */
    DMA_Cmd(DMA1_Channel7, ENABLE);
    return 1;
}


/**
  * @brief  设置发送状态标志位
  * @param  None
  * @retval None
  */
void set_TranSts(uint16_t status)
{
    st_usart2_com.TranSts = status;
}


/**
  * @brief  获取发送状态标志位
  * @param  None
  * @retval None
  */
uint16_t get_TranSts(void)
{
    return st_usart2_com.TranSts;
}


/**
  * @brief  设置接收状态标志位
  * @param  None
  * @retval None
  */
void set_RecSts(uint16_t status)
{
    st_usart2_com.RecSts = status;
}

/**
  * @brief  获取发送状态标志位
  * @param  None
  * @retval None
  */
uint16_t get_RecSts(void)
{
    return st_usart2_com.RecSts;
}

第三步,在stm32f10x_it.c源文件中重写USART2的中断处理函数,主要实现DMA数据发送完成、数据接收完成的处理细节,源码如下:

/* Includes ------------------------*/
#include "stm32f10x_it.h"
#include "USART2.h"
#include <string.h>
#include "user_Systick.h"

/**
  * @brief  This function handles USART2 global interrupt request.
  * @param  None
  * @retval None
  */
void USART2_IRQHandler(void)
{
    /*处理串口空闲中断*/
    if(USART_GetITStatus(USART2, USART_IT_IDLE) != RESET)
    {
        /*清除 IDLE 标志:先读 SR,再读 DR*/
        volatile uint32_t tmp;
        tmp = USART2->SR;   // 读取状态寄存器
        tmp = USART2->DR;   // 读取数据寄存器,只有这样才能清除USART_IT_IDLE标志位

        /* Disable USART2 Rx DMA1 Channel */
        DMA_Cmd(DMA1_Channel6, DISABLE);
        /*获取本次接收到的字节数量*/
        st_usart2_com.RecNum = sizeof(st_usart2_com.rxbuffer) 
                               - DMA_GetCurrDataCounter(DMA1_Channel6);
        /*在此处将usart2接收的数据复制到用户数据缓存中,例如我这里复制数据到Modbus协议栈中*/
        //memcpy(&tModbus.tRec_data.rec_data_buf, &st_usart2_com.rxbuffer,st_usart2_com.RecNum);
        /*记录接收的数据长度*/
        tModbus.tRec_data.rec_length = st_usart2_com.RecNum;

        /*清除接收数量*/
        st_usart2_com.RecNum = 0;
        //配置 DMA 接收通道数量
        DMA_SetCurrDataCounter(DMA1_Channel6, sizeof(st_usart2_com.rxbuffer));
        /* Enable USART2 Rx DMA1 Channel */
        DMA_Cmd(DMA1_Channel6, ENABLE);

        /*置位数据接收完成标志*/
        set_RecSts(1);
    }

    if(USART_GetITStatus(USART2, USART_IT_TC) != RESET)
    {   
        /*设置发送完成标志*/ 
        set_TranSts(1);
        USART_ClearITPendingBit(USART2, USART_IT_TC);
    }
}

  05  总 结

以上就是串口使用DMA+中断实现不定长数据收发的方法,当然我这里使用的是ST的标准库,也可以直接操作寄存器,或者使用ST提供的STM32CubeMX软件来实现,根据个人的编程习惯选择即可;

多说一句,如果使用GD32的片子,这个设计思路依然可行,只需要将接收空闲中断换成接收超时中断即可;

最后如果朋友们需要源码的话,后台私信《USART》即可获取整个源码的工程文件,希望朋友们都能成为技术大牛,生活愉快~

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐