一文搞定国民N32G435高负载串口通信

news/2024/11/24 16:46:54/

副标题:USRAT无硬件双缓冲条件下的软件双缓冲

一、前言

在单片机中,USART的通信一般都是最常用也最先去接触的串口外设,在一般的小数据量应用中一般不需要考虑USART串口(以下简称为串口)的高负载能力,比如打印一下log,接收几个其他设备的指令或者发送几个指令控制其他设备。但是在高速的大数据量的通信场合,串口可能会承载较高的数据负载,如果不合理的进行单片机的资源利用,可能造成各种问题。比如使用串口接收中断接收大量的数据,频繁的进入中断,会占用太多的CPU资源。这时可能会想到【空闲中断+DMA传输完成中断】的方式接收大量数据,但是这是一个极具风险的行为,假设一下,DMA数据传输结束之后,此时CPU开始读取DMA缓存中的数据,此时又有新的数据进来,新的数据就会覆盖之前的数据导致异常。

二、如何启用串口的DMA功能

在讨论如何高实现串口的高负载通信之前,我们得先明白如何启用串口的DMA通信。

DMA(Direct Memory Access)直接储存器访问,是一个CPU用于数据从一个地址空间到另一个地址空间的搬运组件,该过程无需CPU的干预,不占用CPU的资源,可以使单片机这种单线程CPU实现“伪多线程”。只需在数据搬运结束后通知CPU即可。

在国民的资料中是有串口+DMA的例程的,但是官方为了用户调试方便,例程相对简单,就是实现了两个MCU串口间的DMA通信,在开发时具有一定借鉴意义,但是不具备高负载能力,同时移植性不是很好,这里我在例程的基础上进行简化,同时例程不具备的功能也会一一展开。

1.串口+DMA发送

#define TxBufferSize1 (countof(TxBuffer1) - 1)
#define countof(a) (sizeof(a) / sizeof(*(a)))
USART_InitType USART_InitStructure;
uint8_t TxBuffer1[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 the different system clocks.
*/
void RCC_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 the different GPIO ports.
*/
void GPIO_Configuration(void)
{GPIO_InitType GPIO_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);   }

对相关的时钟和串口的引脚进行初始化,这里是直接用的官方例程,只不过将官方例程的宏定义换成了实际的值,便于看代码,不然还需跳转,但是官方的例程这方面的可移植性会更好。

void DMA_Configuration(void)
{DMA_InitType DMA_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时便不再搬运数据。

void UART_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);}

串口的初始化。

void DMA_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已经将数据搬运到串口的数据寄存器,但是因为串口的速度不够,导致此时数据还未送出,而因为例程只循环一次,在测试例程时看不出问题,但是这里会出问题。

int main(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_t RxBuffer1[20];

定义一个数组用于接收串口数据。

USART_ConfigInt(USARTx, USART_INT_IDLEF, ENABLE);

添加串口中断定义。

void NVIC_Configuration(void)
{
NVIC_InitType NVIC_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配置。

void DMA_Configuration(void)
{
DMA_InitType DMA_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。

void DMA_Revice(uint16_t BufferLength)
{
DMA_EnableChannel(DMA_CH2, DISABLE);
DMA_SetCurrDataCounter(DMA_CH2,BufferLength);
DMA_EnableChannel(DMA_CH2, ENABLE);}

添加DMA接收函数

void USART1_IRQHandler(void)
{
if (USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET)
{
/*软件先读 USART_STS,再读 USART_DAT 清除空闲中断标志。*/
USART1->STS;
USART1->DAT;
for(int i=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.将DMA CH2通道设置为循环模式,测试阶段将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);
UART_Init(USART1,2500000);

2.NVIC设置DMA通道中断

void NVIC_Configuration(void)
{
NVIC_InitType NVIC_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数组:

void DMA_Channel2_IRQHandler(void)
{
//传输半满
if(DMA_GetIntStatus(DMA_INT_HTX2,DMA) == SET)
{
DMA_ClrIntPendingBit(DMA_INT_HTX2,DMA);
DMA_ClearFlag(DMA_FLAG_HT2,DMA);
for(int i=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(int i=20;i<40;i++)
{
buf[num] = buffer[i];
num++;
}
}
}

4.在串口空闲中断中将收到的数据全部打印出来。

void USART1_IRQHandler(void)
{
if (USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET)
{
/*软件先读 USART_STS,再读 USART_DAT 清除空闲中断标志。*/
USART1->STS;
USART1->DAT;
for(int i=0;i<80;i++)
{
TxBuffer1[i] = buf[i];}
DMA_send(80);
num=0;
}
}

5.测试结果如下,在2.5M波特率的情况下保持数据完整。

在这里插入图片描述

写在最后:

这次主要讨论了一种高负载情况下如何缓解CPU压力的方法,所言所写不尽完善,例如不定数据接收,就可以通过DMA_GetCurrDataCounter(DMA_CH2);函数进行传输数据的统计计算,这点大家可以自由发挥,现实可能遇到的问题是多种多样的,主要在于关键能力的拓展。更多的还需要根据实际情况灵活配置。

源码链接

N32G435串口通信


http://www.ppmy.cn/news/221543.html

相关文章

【转载】单反圣经!八大品牌镜头标识完全解析

转载来源&#xff1a;http://publish.it168.com/2009/0522/20090522021401.shtml 责任编辑&#xff1a;张凡作者&#xff1a;REX 2009-05-25 【IT168 导购】镜头型号标识&#xff0c;正如一个镜头的身份识别系统一样&#xff0c;相同焦段相同光圈的镜头只有通过镜头标识来区分。…

笔记本电源适配器的工作原理及其类型简介

随着笔记本产品和功能的多样化&#xff0c;其运行功率也相应的提高&#xff0c;这对 笔记本电源适配器(也就是笔记本电脑的充电器)提出了更高的要求。据调查85%的笔记本使用者喜欢轻巧小型&#xff0c;能立刻存电使用并不产生热量的笔记本电源适配器。设计这样的笔记本电源适配…

关于VR产品的前世今生,看这一篇文章就够了

关于VR产品的前世今生&#xff0c;看这一篇文章就够了&#xff08;转&#xff09; 文/胡勇 即使最富质疑精神最冷静的人也无法漠视现在的 VR/AR 掀起的狂潮&#xff0c;这个从科技圈蔓延到实业界最后席卷大众的想象力的狂欢正以前所未有的态势改变着我们对世界、交互方式、互联…

输入企业名称,爬取企业地址

输入企业名称&#xff0c;爬取企业地址&#xff0c;你也可以添加其他信息进去。 # -*- coding: utf-8 -*- """ Created on Tue Jul 23 14:11:50 2019author: Administrator """import requests import lxml import sys from bs4 import Beautif…

andoird TV 优化学习笔记

文章目录 1. 崩溃优化2. 内存优化2.x 内存优化工具2.x 查看内存的相关命令参考资料 3. 卡顿优化3.1 基础知识3.2 Andorid 卡顿排查工具3.3 可视化方法3.4 如何监控应用卡顿3.5 卡顿现场与卡顿分析3.6 总结3.7 参考资料 4. UI优化4.1 硬件加速4.2 Projbect Buffer相关工具参考资…

国产山寨掌机遇到强劲对手,来自开源界的Odroid Go Advance

1、评测背景 ​ 最近拿到一台来自韩国Odroid开源社区的产品&#xff0c;名字叫Odroid Go Advance&#xff0c;简称OGA。它是台开源掌上游戏机&#xff0c;基于瑞芯微RK3326处理器&#xff0c;GPU支持OpenGL。软件方面&#xff0c;操作系统基于Ubuntu 20.04&#xff0c;支持PPS…

tf卡可以自己裁剪成nm卡_这些年Surface 3用过的TF卡与购买心得

使用Surface 3已经多年,内置储存总是不够用,这些年来不断购买TF卡扩展储存。今天数了下手上已买过9张卡。替换下来的卡一些已经卖掉,一些还继续服役于手机,路由器,相机,导航等各种数码产品。这次把收集的跑分图晒晒,顺便谈谈储存卡选购心得。 购买理由 因为Surface 3是阿…

选择MediaTek,赋能 Vewd for Automotive 平台

与全球先进的半导体解决方案提供商合作 挪威奥斯陆--(美国商业资讯)--OTT 软件解决方案的领先提供商 Vewd 今日宣布&#xff0c;MediaTek的 Autus I20 (MT2712) 芯片组已选为赋能 Vewd for Automotive 的首个参考平台。Vewd for Automotive 是一款采用白色徽标、基于云管理的内…