STM32F103x 串口使用DMA实现收发不定长数据的方法【附源码】
目录
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》即可获取整个源码的工程文件,希望朋友们都能成为技术大牛,生活愉快~

更多推荐



所有评论(0)