ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DMA+串口空闲中断实现RS485不定长数据接收和发送

2026/8/9 6:36:10 拓冰建站 浏览量
DMA+串口空闲中断实现RS485不定长数据接收和发送

目录

    • 1、环境说明
    • 2、实现不定长数据接收需要做哪些事?
      • 2.1、数据的接收与缓存
      • 2.2、数据帧的结束判断
      • 2.3、数据帧的长度计算
    • 3、RS485串口实现不定长数据发送
    • 4、代码实现
    • 结语:

1、环境说明

单片机型号;Cortex-M4架构,AT32F437
说明:实现只是一个思路,只要了解到单片机存在这些功能,对照着官方手册和例程,实现并不难。

2、实现不定长数据接收需要做哪些事?

  • 数据的接收与缓存
  • 数据帧的结束判断
  • 数据帧的长度计算

解决以上几点,基本上就可以实现不定长数据的接收了。下面将依次给出应对措施。

2.1、数据的接收与缓存

数据的接收通常是通过单片机的usart–>dt(数据寄存器)来完成的,每次能够存储一个字节。单片机可以通过定时查询、接收中断、DMA等方式进行数据的缓存工作。其中DMA作为效率最高的方式,是本次讲解的重点。
在这里插入图片描述

什么是DMA?在每一个32位单片机的手册里应该都会有说明
DMA是单片机集成在芯片内部的一个数据搬运工,它可以代替单片机对数据进行传输、存储。举一个简单的例子:
要发一个快递,我们需要填写快递的大致体积大小、从哪里发走、在哪里收货。DMA就像快递员,我们只需要负责填写信息,等待快递员上门取货、发货就可以了,我们只需考虑要填的信息和签收工作。
DMA不仅仅是串口能用,它能满足单片机绝大多数的数据传输工作,比如支持定时器、ADC、DAC、SDIO、I²S、SPI、I²C、DVP、QSPI和USART。无需了解太多,我认为嵌入式工程师对于这一类的工具能做到快速上手使用即可。
因此我们可以派出DMA对usart->dt中存储的数据进行一个接收和缓存工作。我们只需要知道什么时候数据都送到了,然后取出处理即可。
在这里插入图片描述

2.2、数据帧的结束判断

在串口通信中,通常我们对一帧数据的结束判定是在一定时间后,没有下一个数据到来。这一段时间在单片机内认为是一个空闲时刻。如果串口接收到一个数据后,隔了一个空闲时刻仍然没有数据继续到达,则串口的状态寄存器就会对空闲位进行一个置起。当单片机触发该中断后,就可以认为本次串口接收已经结束。在2.1我们已经让DMA对数据进行接收缓存了,因此我们就可以直接去DMA中取出这段数据。
在这里插入图片描述

2.3、数据帧的长度计算

在串口通信中,数据的长度是一个非常重要的信息,它可以帮助我们对数据帧进行解析。那具体要怎么知道呢?
在2.1中我们知道,在使用DMA时,需要预先填写一些信息。比如“快递”的大致体积。比如DMA最大支持是65536个字节,我们预估本次接收最大可能会用到x个字节。实际我们接收到了y个字节。那么DMA的长度寄存器就会记录下本次接收剩余的一个容量z。那么我们实际上接收到的数据长度y=x-z(x<=65536)。
在这里插入图片描述

3、RS485串口实现不定长数据发送

熟悉RS485的朋友都知道,RS485是一种半双工通信,发送和接收必须分时进行,因此对于收发转换控制的就非常重要,既要保证数据能够全部接收到,也必须保证数据能够全部发出,否则会造成数据的丢包。因此我们必须知道我们何时结束数据的发送,将RS485由发送切换到接收状态,切换早了会导致数据发送不完整,晚了会导致数据接收不及时不完整。
在单片机的串口状态寄存器里存在一个发送数据完成寄存器,当数据发送结束时,会将此位置起,产生一个发送结束中断。因此在此中断产生后,我们就可以安心切换至接收状态了。
在这里插入图片描述

4、代码实现

#include "rs485.h"
#include "stdio.h"
#include "comm_protocol.h"
/*
*********************************************************************************************************
*                                           与FreeRTOS相关的一些变量
*********************************************************************************************************
*/
TaskHandle_t rs485_handler;/*RS485数据处理任务句柄*/
SemaphoreHandle_t BinarySemphore;/*创建用于串口中断与任务同步的信号量*/
/*
*********************************************************************************************************
*                                            该文件内调用的全局变量
*********************************************************************************************************
*/
static uint8_t		Uart1RecBuffer[MaxNumOfUart1RecBuffer];		/* 接收缓冲区*/
static uint8_t		Uart1SendBuffer[MaxNumOfUart1SendBuffer];	/* 发送缓冲区*///数据接收标志位,
volatile uint8_t usart1_rx_dma_status = 0;
volatile uint8_t usart1_tx_dma_status=1;
/*
*********************************************************************************************************
*                                           部分函数声明
*********************************************************************************************************
*/
void TestProtocol(stCommBufBlock *BufBlockPtr);
/*
*********************************************************************************************************
*                                            驱动配置(外部比如MAIN.C , APP_HOOKS.c可能引用该全局变量)
*********************************************************************************************************
*/
#define PRINT_UART USART1
//RS485 1 用
stCommBSPInfo	RS485BSPInfo1 = {USART1,  //使用串口119200,						/* 通讯波特率默认值19200*/0,							 /* 485通讯地址,在系统初始化的时候,会重新赋值*/DMA1_CHANNEL2,   //串口1接收数据使用的DMA地址DMA1MUX_CHANNEL2,DMAMUX_DMAREQ_ID_USART1_RX,			/* 通讯模块接收的源号*/Uart1RecBuffer,				/* 接收缓存指针*/MaxNumOfUart1RecBuffer,		/* 接收缓存大小*/DMA1_CHANNEL1,DMA1MUX_CHANNEL1,DMAMUX_DMAREQ_ID_USART1_TX,			/* 通讯模块发送 的源号*/Uart1SendBuffer,			/* 发送缓存指针*/MaxNumOfUart1SendBuffer,		/* 发送缓存大小*/0,							/* 接收计数器*/5,							/* 接收间隔,单位5ms*/5,							/* 发送间隔,单位5ms ,RS485用*/TestProtocol,				/* 通信协议函数指针*/BSPTYPE_485,					/* 接口选项,比如RS232,485,SPI等*/GPIO_PINS_11,							/* 控制485芯片的IO 序号0~31,232 不用,RS485 用*/GPIOA,0,
};
void usartConfiguration(void) {gpio_init_type gpio_init_struct;/* enable the usart1 and gpio clock */crm_periph_clock_enable(CRM_USART1_PERIPH_CLOCK, TRUE);crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);gpio_default_para_init(&gpio_init_struct);/* configure the usart2 tx, rx pin */gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;gpio_init_struct.gpio_out_type  = GPIO_OUTPUT_PUSH_PULL;gpio_init_struct.gpio_mode = GPIO_MODE_MUX;gpio_init_struct.gpio_pins = GPIO_PINS_9 | GPIO_PINS_10;gpio_init_struct.gpio_pull = GPIO_PULL_NONE;gpio_init(GPIOA, &gpio_init_struct);gpio_pin_mux_config(GPIOA, GPIO_PINS_SOURCE9, GPIO_MUX_7);gpio_pin_mux_config(GPIOA, GPIO_PINS_SOURCE10, GPIO_MUX_7);/* config usart nvic interrupt */nvic_priority_group_config(NVIC_PRIORITY_GROUP_4);nvic_irq_enable(USART1_IRQn, 2, 0);/* configure usart1 param */usart_init(USART1, RS485BSPInfo1.BaudRate, USART_DATA_8BITS, USART_STOP_1_BIT);usart_transmitter_enable(USART1, TRUE);usart_receiver_enable(USART1, TRUE);/* enable usart1 and usart2 and usart3 interrupt */usart_enable(USART1, TRUE);usart_dma_transmitter_enable(USART1, TRUE);usart_dma_receiver_enable(USART1, TRUE);usart_interrupt_enable(USART1, USART_IDLE_INT, TRUE);//使能空闲中断usart_interrupt_enable(USART1, USART_TDC_INT, TRUE); //使能发送完成中断
}
//初始化DMA的时钟
void dmaInit(void) {/* enable dma1 clock */crm_periph_clock_enable(CRM_DMA1_PERIPH_CLOCK, TRUE);dmamux_enable(DMA1, TRUE);/*一旦开启,就不要关闭,*///配置通道1,用于串口1的发送dma_reset(RS485BSPInfo1.Send_Dma_Chn);dmamux_init(RS485BSPInfo1.Send_Dmamux_Chn, RS485BSPInfo1.CommSendSlotNumber);//配置通道2,用于串口1的接收dma_reset(RS485BSPInfo1.Rec_Dma_Chn);dmamux_init(RS485BSPInfo1.Rec_Dmamux_Chn, RS485BSPInfo1.CommRecSlotNumber);
}//PA11 PD3 PD11
void usartCtrlIOInit(void) {gpio_init_type gpio_init_struct;/* enable the usart1 and gpio clock */crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);gpio_default_para_init(&gpio_init_struct);/* configure the usart2 tx, rx pin */gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;gpio_init_struct.gpio_out_type  = GPIO_OUTPUT_PUSH_PULL;gpio_init_struct.gpio_mode =  GPIO_MODE_OUTPUT;gpio_init_struct.gpio_pins = GPIO_PINS_11;gpio_init_struct.gpio_pull = GPIO_PULL_NONE;gpio_init(GPIOA, &gpio_init_struct);gpio_init_struct.gpio_pins = GPIO_PINS_3;
//默认接收状态gpio_bits_set(RS485BSPInfo1.gpio_x, RS485BSPInfo1.IoNumber);
}
//DMA最大传输量为65536
//用于串口接收和发送
void dmaUsartTXorRX(dma_dir_type dir, uint8_t* buf, uint16_t size, dma_channel_type *dmax_channely, usart_type* usart_x) {dma_init_type dma_init_struct;dma_reset(dmax_channely);//重置此通道if (dir == SEND) {if (usart_x == USART1) {while(!usart1_tx_dma_status){}usart1_tx_dma_status=0;gpio_bits_reset(GPIOA, GPIO_PINS_11);} }/* dma1 channel1 for usart2 tx configuration */dma_default_para_init(&dma_init_struct);dma_init_struct.buffer_size = size;dma_init_struct.direction = dir;dma_init_struct.memory_base_addr = (uint32_t)buf;dma_init_struct.memory_data_width = DMA_MEMORY_DATA_WIDTH_BYTE;dma_init_struct.memory_inc_enable = TRUE;dma_init_struct.peripheral_base_addr = (uint32_t)&usart_x->dt;dma_init_struct.peripheral_data_width = DMA_PERIPHERAL_DATA_WIDTH_BYTE;dma_init_struct.peripheral_inc_enable = FALSE;dma_init_struct.priority = DMA_PRIORITY_MEDIUM;dma_init_struct.loop_mode_enable = FALSE;dma_init(dmax_channely, &dma_init_struct);dma_channel_enable(dmax_channely, TRUE); //一旦开启,就会立刻开始传输,不受MCU控制
}
/*
*********************************************************************************************************
*                                            TestProtocol()
*
* 描述: 测试用例协议
*
* 参数: BufBlockPtr-- 接收数据信息块
*
* 附注: 无
*
*********************************************************************************************************
*/void TestProtocol(stCommBufBlock *BufBlockPtr)
{CPU_INT32U i;// 以同样的字节数将接收到的数据返回BufBlockPtr->SendByteNum = BufBlockPtr->RecByteNum;// 将返回的数据都扩大两倍for (i = 0; i < BufBlockPtr->SendByteNum; i++){*(BufBlockPtr->SendBufPtr + i) = *(BufBlockPtr->RecBufPtr + i);}
}
/*
*********************************************************************************************************
*                                            CommProcess()
*
* 描述: 串口(232,485)通讯进程,改为任务函数,当有数据完成接收后,及时处理该函数
*
* 参数: CBinfo--通信端口配置信息
*
* 附注: 无
*
*********************************************************************************************************
*/
void CommProcess(stCommBSPInfo *CBinfo) {stCommBufBlock BufBlock;uint16_t sendnum;//先关闭相应的DMA接收dma_channel_enable(CBinfo->Rec_Dma_Chn, FALSE);//调用通信协议,RecCount 为接收数据个数参数,CBinfo.RecBuff 为接收数组地址CBinfo->RecCntTemp = CBinfo->RecBuffLength - dma_data_number_get(CBinfo->Rec_Dma_Chn); //根据接收的数据剩余量得出BufBlock.RecByteNum = CBinfo->RecCntTemp;BufBlock.RecBufPtr = CBinfo->RecBuff;BufBlock.SendBufPtr = CBinfo->SendBuff;BufBlock.Address = CBinfo->Address;//跳转至CBinfo.ProtocolFunPtr 指向的通信协议函数//该函数处理BufBlock.RecBufPtr 所指向的BufBlock.RecByteNum个字节的数据,//然后将处理出来的BufBlock.SendByteNum个字节的数据存放在BufBlock.SendBufPtr//所指向的地方(*CBinfo->ProtocolFunPtr)(&BufBlock);//防止要发送的数据个数超过SENDBUF最大值if (BufBlock.SendByteNum <= CBinfo->SendBuffLength) {sendnum = BufBlock.SendByteNum;} else {sendnum = CBinfo->SendBuffLength;}//如果地址不对,就不会有数据需要发送if (sendnum > 0) {dmaUsartTXorRX(SEND, BufBlock.SendBufPtr, sendnum, CBinfo->Send_Dma_Chn, CBinfo->usart_x);}
}
//建立一个任务,用于处理通信进程
void communication_485_task(void *pvParameters) { BaseType_t semaphore485 = pdFALSE;dmaInit();dmaUsartTXorRX(RECEIVE, RS485BSPInfo1.RecBuff, RS485BSPInfo1.RecBuffLength, USART1RX, USART1); //开启DMA接收while (1) {//还需要判断当前协议。//等待信号量if (BinarySemphore != NULL) {//获取信号量semaphore485 = xSemaphoreTake(BinarySemphore, portMAX_DELAY);if (semaphore485 == pdTRUE) { //获取信号量成功if (usart1_rx_dma_status == 1) { //串口1,数据接收完成usart1_rx_dma_status = 0;CommProcess(&RS485BSPInfo1);//进入通信进程dmaUsartTXorRX(RECEIVE, RS485BSPInfo1.RecBuff, RS485BSPInfo1.RecBuffLength, USART1RX, USART1); //开启DMA接收}}}}}
}
void USART1_IRQHandler(void) {BaseType_t xHigherPriorityTaskWoken = pdFALSE;if (usart_flag_get(USART1, USART_IDLEF_FLAG) != RESET) { //发生了空闲中断//一帧数据接收结束usart1_rx_dma_status = 1;usart_flag_clear(USART1, USART_IDLEF_FLAG);xSemaphoreGiveFromISR(BinarySemphore, &xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}if (usart_flag_get(USART1, USART_TDC_FLAG) != RESET) { //发送完成中断//一帧数据接收结束gpio_bits_set(GPIOA, GPIO_PINS_11);usart1_tx_dma_status=1;usart_flag_clear(USART1, USART_TDC_FLAG);}
}
/*end file*/

.h文件

#ifndef _RS485_H_
#define _RS485_H_#include "config.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"/*
*********************************************************************************************************
*                                               接口定义
*********************************************************************************************************
*/
#define	BSPTYPE_485			2
/*
*********************************************************************************************************
*                                               与RS485接口相关定义
*********************************************************************************************************
*/
#define SEND    DMA_DIR_MEMORY_TO_PERIPHERAL //发送
#define RECEIVE DMA_DIR_PERIPHERAL_TO_MEMORY //接收
#define USART1TX DMA1_CHANNEL1 //串口1发送
#define USART1RX DMA1_CHANNEL2 //串口1接收
#define MaxNumOfUart1RecBuffer 128
#define MaxNumOfUart1SendBuffer 256
/*
*********************************************************************************************************
*                                           自定义数据结构
*********************************************************************************************************
*/typedef	struct
{uint32_t		Address;			/* 该端口地址*/uint32_t		RecByteNum;		/* 接收数组字节个数*/uint8_t		*RecBufPtr;			/* 接收指针指向缓冲数组*/uint32_t		SendByteNum;		/* 发送数组字节个数*/uint8_t		*SendBufPtr;		/* 发送指针指向缓冲数组*/
}stCommBufBlock;typedef void (*tCommProtocolFunPtr)(stCommBufBlock*);		/* 通信协议函数指针*/typedef	struct
{usart_type*		usart_x;          /* 串口外设 */uint32_t		BaudRate;					/* 通讯波特率*/uint32_t		Address;					/* 通讯波地址*/dma_channel_type * Rec_Dma_Chn;  /* 用于串口接收的DMA外设 */dmamux_channel_type* Rec_Dmamux_Chn; /* 用于串口接收的DMA通道号 */dmamux_requst_id_sel_type		CommRecSlotNumber;		/* 用于串口接收的接收的源号*/uint8_t		*RecBuff;					/* 接收缓存指针*/uint16_t		RecBuffLength;		/* 接收缓存大小,由接收最大值减去当前DMA剩余接收量得到 */dma_channel_type* Send_Dma_Chn;  /* 用于串口发送的DMA外设 */dmamux_channel_type* Send_Dmamux_Chn; /* 用于串口发送的DMA通道号 */dmamux_requst_id_sel_type		CommSendSlotNumber;		/* 用于串口发送的接收的源号*/uint8_t		*SendBuff;					/* 发送缓存指针*/uint16_t		SendBuffLength;				/* 发送缓存大小,由协议给出*/uint32_t		RecCntTemp;				/* 接收计数器,现在使用的是串口空闲中断来判断帧数据是否结束,该变量可以省去*/uint32_t		RecTimeInterval;			/* 接收间隔,单位5ms*/uint32_t		SendTimeInterval;			/* 发送间隔,单位5ms,RS485用*/tCommProtocolFunPtr	ProtocolFunPtr;				/* 通信协议函数指针*/uint32_t		BSPType;					/* 接口选项,比如RS232,485,SPI等*/uint32_t		IoNumber;					/* 控制485芯片的IO 序号0~31*/gpio_type* 		gpio_x;							/*PORT地址*/uint8_t		CommCnt;
}stCommBSPInfo;extern TaskHandle_t rs485_handler;
extern SemaphoreHandle_t BinarySemphore;/*创建用于串口中断与任务同步的信号量*/extern  stCommBSPInfo	RS485BSPInfo1;
extern volatile uint8_t usart1_tx_dma_status;
void communication_485_task(void *pvParameters);
void usartConfiguration(void);
void dmaInit(void);
void usartCtrlIOInit(void);
void dmaUsartTXorRX(dma_dir_type dir,uint8_t* buf,uint16_t size,dma_channel_type *dmax_channely,usart_type* usart_x);
#endif

结语:

首先此篇文章仅作为对RS485不定长数据的接收与发送的一种探讨,方式不止一种,本文是我的一种愚见罢了。其次,单片机开发并不难,我们只是要要了解到有这么一种方式可以实现想要的功能,那么就基本离成功不远了,耐心看手册耐心调试程序,没有不能完成的事情的。