发新帖本帖赏金 200.00元(功能说明)我要提问
返回列表
[N32G43x]

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

[复制链接]
5956|149
手机看帖
扫描二维码
随时随地手机跟帖
呐咯密密|  楼主 | 2022-3-31 16:49 | 显示全部楼层 |阅读模式
本帖最后由 呐咯密密 于 2022-4-23 15:20 编辑

#申请原创#@21小跑堂

一、前言

在单片机中,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发送。

微信图片_20220331164501.png

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的方式发送出来用于校验结果。

微信图片_202203311645011.png

通过串口助手可观测数据正确。至此,常见的串口+DMA的发送与接收完成。后文将实现高负载的通信。



三、高负载情况下的DMA如何实现

在串口数据量较大时,一般使用双BUf,很多单片机有硬件双缓冲,DMA的目标储存区域有两个,当一次完整的数据传输结束后,也就是counter值变为0时,DMA会自动将数据指向另一块区域。这样用户就有时间去处理刚存满的buf,而不会被覆盖。就是“乒乓缓存”。

普通DMA

微信图片_20220331164502.png

DMA双缓冲

微信图片_202203311645021.png


大致流程如下:

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继续将数据向前半搬运,如此循环。

微信图片_202203311645022.png

代码讲解如下:

以下代码完整流程如下:

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波特率的情况下保持数据完整。

微信图片_202203311645023.png





写在最后:

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

工程放在后面,回帖下载。

游客,如果您要查看本帖隐藏内容请回复

如果觉的帖子对您有所帮助,麻烦大佬们点击头像给个关注呗。感谢大家!


  

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 200.00 元 2022-04-21
理由:恭喜通过原创奖文章审核!请多多加油哦!

七毛钱| | 2022-4-1 09:51 | 显示全部楼层
想看看具体的工程

使用特权

评论回复
asmine| | 2022-4-1 16:44 | 显示全部楼层
高负载指的是数据量么

使用特权

评论回复
呐咯密密|  楼主 | 2022-4-1 17:02 | 显示全部楼层
asmine 发表于 2022-4-1 16:44
高负载指的是数据量么

不止是数据量,还有数据的传输速度

使用特权

评论回复
廖为情| | 2022-4-6 10:02 | 显示全部楼层
国产进口MCU代理,有需要的小伙伴加我微信了解,L18121451280  廖**

使用特权

评论回复
asmine| | 2022-4-6 14:58 | 显示全部楼层
呐咯密密 发表于 2022-4-1 17:02
不止是数据量,还有数据的传输速度

确实,现在常规功能基本各大家族的芯片都具备了。
比拼的就是速度稳定性了

使用特权

评论回复
bioe| | 2022-4-22 17:33 | 显示全部楼层
谢谢分享  学习学习

使用特权

评论回复
fzy_666| | 2022-4-23 13:07 | 显示全部楼层
非常不错的资料,谢谢

使用特权

评论回复
arima| | 2022-4-23 18:10 | 显示全部楼层
是技术类**就学习收藏了。

使用特权

评论回复
lfc315| | 2022-4-27 17:51 | 显示全部楼层
收藏备用。。。。。。。。

使用特权

评论回复
2695877352| | 2022-4-27 23:45 | 显示全部楼层
谢谢分享  学习学习

使用特权

评论回复
rwj1226| | 2022-4-28 17:59 | 显示全部楼层
感谢大佬,刚好遇到大量数据收发是导致收发出问题。

使用特权

评论回复
cisco777| | 2022-5-7 09:20 | 显示全部楼层
想看看具体的工程

使用特权

评论回复
Angel_YY| | 2022-5-9 11:34 | 显示全部楼层
正需要,多谢分享

使用特权

评论回复
fuqinyyy| | 2022-5-10 07:54 | 显示全部楼层
应该叫高吞吐量吧,负载指的是功耗。

使用特权

评论回复
allbut| | 2022-5-18 22:26 | 显示全部楼层
看内容

使用特权

评论回复
laipeng101| | 2022-5-31 16:25 | 显示全部楼层
谢谢分享

使用特权

评论回复
olivem55arlowe| | 2022-6-2 09:11 | 显示全部楼层
这个是串口通信的吗?

使用特权

评论回复
everyrobin| | 2022-6-2 10:12 | 显示全部楼层
485通信?

使用特权

评论回复
xiaoyaozt| | 2022-6-2 10:46 | 显示全部楼层
最大的传输波特率是多少呢?

使用特权

评论回复
发新帖 本帖赏金 200.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:苏州澜宭自动化科技嵌入式工程师
简介:本人从事磁编码器研发工作,负责开发2500线增量式磁编码器以及17位、23位绝对值式磁编码器,拥有多年嵌入式开发经验,精通STM32、GD32、N32等多种品牌单片机,熟练使用单片机各种外设。

344

主题

2691

帖子

38

粉丝