一、前言
在单片机中,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;ists;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);函数进行传输数据的统计计算,这点大家可以自由发挥,现实可能遇到的问题是多种多样的,主要在于关键能力的拓展。更多的还需要根据实际情况灵活配置。
来源:nations加油站
人工智能和网络安全防范新出现的威胁
基于比较器的过压保护电路设计方案
环网柜共箱和不共箱的区别 环网柜基本组成 环网柜和母联柜的区别
华硕无畏Pro15 2024轻薄本发布,酷睿Ultra和2.8K OLED屏助力卓越PG游戏性能
单电源运算放大器的设计考虑
基于N32G435的高负载串口通信
空气加湿器的特征、注意事项以及工作原理
PLC和变频器进行通讯的接线图详细资料讲解
电线电缆制造企业恒飞电缆已同招商证券签署上市辅导协议
骏晔ChirpLAN™网关套件重磅出击,携磐启共创物联新时代
数字幅频均衡功率放大器中FPGA的应用
利用智能功率转换技术利用太阳能
陆芯精密切割——精密划片机优势及工艺介绍
最近国内新能源汽车市场中的大事件
未来机器人与人类该如何相处
螺杆泵智能直接驱动装置的优势
特斯拉Model 3新增允许汽车拨打911功能
台积电将与博通合作强化晶圆级封装平台
AMD RDNA2 GPU架构扩展技术详解
音圈模组加持的3D打印又有新的突破啦