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

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,  //使用串口1
	19200,						/* 通讯波特率默认值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不定长数据的接收与发送的一种探讨,方式不止一种,本文是我的一种愚见罢了。其次,单片机开发并不难,我们只是要要了解到有这么一种方式可以实现想要的功能,那么就基本离成功不远了,耐心看手册耐心调试程序,没有不能完成的事情的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小谦·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值