一、前言
在单片机中,USART通信是最常用也是最先去接触的串口外设,在小数据量应用中一般不需要考虑USART串口(以下简称为串口)的高负载能力,比如打印一下log,接收几个其他设备的指令或者发送几个指令控制其他设备。但是在高速的大数据量的通信场合,串口可能会承载较高的数据负载,如果不合理地进行单片机的资源利用,则有可能造成各种问题。比如使用串口接收中断接收大量的数据,频繁地进入中断,会占用太多的CPU资源。这时可能会想到【空闲中断+DMA传输完成中断】的方式接收大量数据,但是这是一个极具风险的行为,假设一下,DMA数据传输结束之后,此时CPU开始读取DMA缓存中的数据,此时又有新的数据进来,新的数据就会覆盖之前的数据导致异常。
二、如何启用串口的DMA功能
在讨论如何实现串口的高负载通信之前,我们得先明白如何启用串口的DMA通信。
DMA(DirectMemoryAccess)直接储存器访问,是一个CPU用于数据从一个地址空间到另一个地址空间的搬运组件,该过程无需CPU的干预,不占用CPU的资源,可以使单片机这种单线程CPU实现“伪多线程”。只需在数据搬运结束后通知CPU即可。
在国民技术的资料中是有串口+DMA的例程的,但是官方为了用户调试方便,例程相对简单,就是实现了两个MCU串口间的DMA通信,在开发时具有一定借鉴意义,但是不具备高负载能力,同时移植性不是很好,这里我在例程的基础上进行简化,同时例程不具备的功能也会一一展开。
1.串口+DMA发送
#defineTxBufferSize1 (countof(TxBuffer1) - 1) #definecountof(a) (sizeof(a) / sizeof(*(a))) USART_InitTypeUSART_InitStructure; uint8_tTxBuffer1[20] ={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a};
首先是定义一些相关的变量,数据和结构体啥的,TxBufferSize1发送数量,TxBuffer1[20]发送的数组。
/** *[url=home.php?mod=space&uid=247401]@brief[/url] Configures thedifferent system clocks. */ voidRCC_Configuration(void) { /*DMA clock enable */ RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_DMA,ENABLE); /*Enable GPIO clock */ RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB,ENABLE); /*Enable USARTy and USARTz Clock */ RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_USART1,ENABLE); } /** *[url=home.php?mod=space&uid=247401]@brief[/url] Configures thedifferent GPIO ports. */ voidGPIO_Configuration(void) { GPIO_InitTypeGPIO_InitStructure; /*Initialize GPIO_InitStructure */ GPIO_InitStruct(&GPIO_InitStructure); /*Configure USARTy Tx as alternate function push-pull */ GPIO_InitStructure.Pin = GPIO_PIN_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Alternate= GPIO_AF0_USART1; GPIO_InitPeripheral(GPIOB,&GPIO_InitStructure); /*Configure USARTy Rx as alternate function push-pull and pull-up */ GPIO_InitStructure.Pin = GPIO_PIN_7; GPIO_InitStructure.GPIO_Pull = GPIO_Pull_Up; GPIO_InitStructure.GPIO_Alternate= GPIO_AF0_USART1; GPIO_InitPeripheral(GPIOB,&GPIO_InitStructure); }
对相关的时钟和串口的引脚进行初始化,这里是直接用的官方例程,只不过将官方例程的宏定义换成了实际的值,便于看代码,不然还需跳转,但是官方的例程这方面的可移植性会更好。
voidDMA_Configuration(void) { DMA_InitTypeDMA_InitStructure; /*USARTy TX DMA1 Channel (triggered by USARTy Tx event) Config */ DMA_DeInit(DMA_CH4); DMA_StructInit(&DMA_InitStructure); DMA_InitStructure.PeriphAddr = (USART1_BASE + 0x04); DMA_InitStructure.MemAddr = (uint32_t)TxBuffer1; DMA_InitStructure.Direction = DMA_DIR_PERIPH_DST; DMA_InitStructure.BufSize = TxBufferSize1; DMA_InitStructure.PeriphInc = DMA_PERIPH_INC_DISABLE; DMA_InitStructure.DMA_MemoryInc = DMA_MEM_INC_ENABLE; DMA_InitStructure.PeriphDataSize= DMA_PERIPH_DATA_SIZE_BYTE; DMA_InitStructure.MemDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.CircularMode = DMA_MODE_NORMAL; DMA_InitStructure.Priority = DMA_PRIORITY_VERY_HIGH; DMA_InitStructure.Mem2Mem = DMA_M2M_DISABLE; DMA_Init(DMA_CH4,&DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_TX,DMA, DMA_CH4, ENABLE); }
DMA的初始化采用NORMAL模式,即只发送一次,当计数器为0时便不再搬运数据。
voidUART_Init(USART_Module* USARTx,uint32_t BaudRate) { /*USARTy and USARTz configuration ---------------------------*/ USART_StructInit(&USART_InitStructure); USART_InitStructure.BaudRate = BaudRate; USART_InitStructure.WordLength = USART_WL_8B; USART_InitStructure.StopBits = USART_STPB_1; USART_InitStructure.Parity = USART_PE_NO; USART_InitStructure.HardwareFlowControl= USART_HFCTRL_NONE; USART_InitStructure.Mode = USART_MODE_RX | USART_MODE_TX; /*Configure USARTy and USARTz */ USART_Init(USARTx,&USART_InitStructure); /*Enable USARTy DMA Rx and TX request */ USART_EnableDMA(USARTx,USART_DMAREQ_RX | USART_DMAREQ_TX, ENABLE); /*Enable the USARTy and USARTz */ USART_Enable(USARTx,ENABLE); }
串口的初始化。
voidDMA_send(uint8_t* pBuffer,uint16_t BufferLength) { DMA_EnableChannel(DMA_CH4,DISABLE); DMA_SetCurrDataCounter(DMA_CH4,BufferLength); DMA_EnableChannel(DMA_CH4,ENABLE); while(USART_GetFlagStatus(USART1, USART_FLAG_TXDE) == RESET) { } }
DMA的发送函数,先失能DMA通道,再重新设置传输长度,再使能DMA通道,这里是检测while是检测串口的发送完成编制位,在官方的demo中检测的是DMA的通道完成标志,这个在这里面是不可以的,因为DMA的搬运速度是远大于串口的通信速度的,如果检测DMA通道完成标志,会导致DMA已经将数据搬运到串口的数据寄存器,但是因为串口的速度不够,导致此时数据还未送出,而因为例程只循环一次,在测试例程时看不出问题,但是这里会出问题。
intmain(void) { /*System Clocks Configuration */ RCC_Configuration(); /*Configure the GPIO ports */ GPIO_Configuration(); /*Configure the DMA */ DMA_Configuration(); UART_Init(USART1,115200); while(1) { DMA_send(TxBuffer1,20); Delay(10000000); } }
最后在主函数调用各初始化函数,在while(1)中循环发送便可实现最简单的串口+DMA发送。
2.串口+DMA接收
在上面发送的基础上我们加上DMA的接收功能,此处需要解释一下下面的操作:为了对应手册,上面的串口发送DMA通道原来是CH4,我下面全部改成CH1。
uint8_tRxBuffer1[20];
定义一个数组用于接收串口数据。
USART_ConfigInt(USARTx,USART_INT_IDLEF, ENABLE);
添加串口中断定义。
voidNVIC_Configuration(void) { NVIC_InitTypeNVIC_InitStructure; /*Enable the USARTz Interrupt */ NVIC_InitStructure.NVIC_IRQChannel= USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority= 0; NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE; NVIC_Init(&NVIC_InitStructure); }
添加NVIC配置。
voidDMA_Configuration(void) { DMA_InitTypeDMA_InitStructure; /*USARTy TX DMA1 Channel (triggered by USARTy Tx event) Config */ DMA_DeInit(DMA_CH1); DMA_StructInit(&DMA_InitStructure); DMA_InitStructure.PeriphAddr= (USART1_BASE + 0x04); DMA_InitStructure.MemAddr= (uint32_t)TxBuffer1; DMA_InitStructure.Direction= DMA_DIR_PERIPH_DST; DMA_InitStructure.BufSize= TxBufferSize1; DMA_InitStructure.PeriphInc= DMA_PERIPH_INC_DISABLE; DMA_InitStructure.DMA_MemoryInc= DMA_MEM_INC_ENABLE; DMA_InitStructure.PeriphDataSize= DMA_PERIPH_DATA_SIZE_BYTE; DMA_InitStructure.MemDataSize= DMA_MemoryDataSize_Byte; DMA_InitStructure.CircularMode= DMA_MODE_NORMAL; DMA_InitStructure.Priority= DMA_PRIORITY_VERY_HIGH; DMA_InitStructure.Mem2Mem= DMA_M2M_DISABLE; DMA_Init(DMA_CH1,&DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_TX,DMA, DMA_CH1, ENABLE); DMA_DeInit(DMA_CH2); DMA_InitStructure.PeriphAddr= (USART1_BASE + 0x04); DMA_InitStructure.MemAddr= (uint32_t)RxBuffer1; DMA_InitStructure.Direction= DMA_DIR_PERIPH_SRC; DMA_InitStructure.BufSize= TxBufferSize1; DMA_Init(DMA_CH2,&DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_RX,DMA, DMA_CH2, ENABLE); }
添加DMA的接收,并将通道设置为CH2。
voidDMA_Revice(uint16_t BufferLength) { DMA_EnableChannel(DMA_CH2,DISABLE); DMA_SetCurrDataCounter(DMA_CH2,BufferLength); DMA_EnableChannel(DMA_CH2,ENABLE); }
添加DMA接收函数
voidUSART1_IRQHandler(void) { if(USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET) { /*软件先读USART_STS,再读USART_DAT清除空闲中断标志。*/ USART1->STS; USART1->DAT; for(inti=0;i<20;i++) { TxBuffer1[i]= RxBuffer1[i]; } DMA_send(20); DMA_Revice(20); } }
添加串口中断函数,在串口中断函数中将接收的数据传给DMA发送数组,再通过DMA的方式发送出来用于校验结果。
通过串口助手可观测数据正确。至此,常见的串口+DMA的发送与接收完成。后文将实现高负载的通信。
三、高负载情况下的DMA如何实现
在串口数据量较大时,一般使用双BUF,很多单片机有硬件双缓冲,DMA的目标储存区域有两个,当一次完整的数据传输结束后,也就是counter值变为0时,DMA会自动将数据指向另一块区域。这样用户就有时间去处理刚存满的buf,而不会被覆盖。就是“乒乓缓存”。
普通DMA
DMA双缓冲
大致流程如下:
1.串口有数据到来,DMA现将数据储存在内存1,完成后通知CPU过来处理数据。
2.此时DMA不停下,开始将后续的数据搬运到内存2。
3.内存2的数据搬运完成,通知CPU开始处理内存2中的数据。
4.如果数据传输还未结束,此时DMA会将数据储存在内存1。如此循环,直至没有数据到来。
但是遗憾的是N32G435这块芯片不具备双缓冲模式,那么我们可以主动控制DMA跳转内存区域。利用“传输过半中断”来模拟双缓冲模式。
大致流程如下:
1.DMA完成搬运一半的数据时,产生一个传输过半中断,此时我们让CPU来处理上一半数据。
2.DMA数据搬运未停止,此时继续搬运后一半数据,此操作不会影响前面一半的数据处理。
3.DMA数据搬运完,触发传输完成中断,这时CPU可以处理后半数据。
4.如果数据传输还未结束,DMA继续将数据向前半搬运,如此循环。
代码讲解如下:
以下代码完整流程如下:
1.配置串口波特率2.5M,DMA的BufSize设置为40,开启传输过半中断,传输完成中断,串口空闲中断。
2.启动DMA接收。
3.通过串口助手发送80个数据到串口。
4.当DMA接收数组接收到20个数据触发传输过半中断,跳转中断函数将20个数据存放到数组中。
5.此时DMA仍在运行,但是数据存放在DMA接收数组的后20个地址空间。
6.当DMA接收数组填满,触发DMA传输完成中断,跳转中断函数将后20个数据保存,此时DMA一共搬运了40个数据。
7.DMA继续搬运数据到接收数组里,此时会覆盖之前的前二十个数据,跳转到步骤4.
8.接收完80个数据,此时触发串口空闲中断,将接收到的数据打印出来。
在上面代码基础上做如下操作:
1.将DMACH2通道设置为循环模式,测试阶段将BufSize设置为40,开启传输过半中断和传输完成中断。同时为了测试高速场景,串口波特率设置为2.5M:
DMA_DeInit(DMA_CH2); DMA_InitStructure.PeriphAddr= (USART1_BASE + 0x04); DMA_InitStructure.MemAddr= (uint32_t)buffer; DMA_InitStructure.Direction= DMA_DIR_PERIPH_SRC; DMA_InitStructure.BufSize= 40; DMA_InitStructure.CircularMode= DMA_MODE_CIRCULAR; DMA_Init(DMA_CH2,&DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_RX,DMA, DMA_CH2, ENABLE); DMA_ConfigInt(DMA_CH2,DMA_INT_HTX,ENABLE);//半传输中断 DMA_ConfigInt(DMA_CH2,DMA_INT_TXC,ENABLE);//传输完成中断 DMA_ClearFlag(DMA_FLAG_HT2,DMA);//清除标志位,避免第一次传输出错 DMA_ClearFlag(DMA_FLAG_TC2,DMA); DMA_ClrIntPendingBit(DMA_INT_HTX2,DMA); DMA_ClrIntPendingBit(DMA_INT_TXC2,DMA);
2.NVIC设置DMA通道中断
voidNVIC_Configuration(void) { NVIC_InitTypeNVIC_InitStructure; /*Enable the USARTz Interrupt */ NVIC_InitStructure.NVIC_IRQChannel= USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority= 1; NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel= DMA_Channel2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority= 0; NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE; NVIC_Init(&NVIC_InitStructure); }
3.添加DMA的CH2中断函数,num为全局变量,目的是将所有的数据保存进buf数组:
voidDMA_Channel2_IRQHandler(void) { //传输半满 if(DMA_GetIntStatus(DMA_INT_HTX2,DMA)== SET) { DMA_ClrIntPendingBit(DMA_INT_HTX2,DMA); DMA_ClearFlag(DMA_FLAG_HT2,DMA); for(inti=0;i<20;i++) { buf[num]= buffer[i]; num++; } } //传输满 if(DMA_GetIntStatus(DMA_INT_TXC2,DMA)== SET) { DMA_ClrIntPendingBit(DMA_INT_TXC2,DMA); DMA_ClearFlag(DMA_FLAG_TC2,DMA); for(inti=20;i<40;i++) { buf[num]= buffer[i]; num++; } } }
4.在串口空闲中断中将收到的数据全部打印出来。
voidUSART1_IRQHandler(void) { if(USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET) { /*软件先读USART_STS,再读USART_DAT清除空闲中断标志。*/ USART1->STS; USART1->DAT; for(inti=0;i<80;i++) { TxBuffer1[i]= buf[i]; } DMA_send(80); num=0; } }
5.测试结果如下,在2.5M波特率的情况下保持数据完整。
写在最后
这次主要讨论了一种高负载情况下如何缓解CPU压力的方法,所言所写不尽完善,例如不定数据接收,就可以通过DMA_GetCurrDataCounter(DMA_CH2);函数进行传输数据的统计计算,这点大家可以自由发挥,现实可能遇到的问题是多种多样的,主要在于关键能力的拓展。更多的还需要根据实际情况灵活配置。
阅读原文:https://bbs.21ic.com/icview-3209220-1-1.html
来源:Nations加油站
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。