memcpy函数优化及DMA对比

一、背景与目的

优化算法结构,提高芯片的使用效率,挖掘芯片的潜在能力,对提高产品质量,降低产品成本有着重要意义,在性能受限的嵌入式设备更加重要。
在使用C语言编程时,我们常用memcpy来复制内存数据,最简单的memcpy功能实现如下:

void *memcpy(void *desc,const void * src,size_t size)
{
	if((desc == NULL) && (src == NULL))
	{
		return NULL;
	}
	unsigned char desc1 = (unsigned char)desc;
	unsigned char src1 = (unsigned char)src;
	while(size-- >0)
	{
		*desc1 = *src1;
		desc1++;
		src1++;
	}
	return desc;
}

这是一个标准通用的memcpy函数的实现,满足memcpy的功能,但性能非常低,因为while每一次循环只能复制一个字节。
如果要进一步的优化,就需要用到更多的知识,例如CPU位宽、数据对齐、汇编指令等等,学过计算机原理应该知道CPU字长、寄存器位宽等概念。
现在常见的CPU通常为32/64位,今天我们以32位Arm Cortex -M4来讲解。

二、数据对齐

32位CPU字长为32Bit,即它的每个通用寄存器包含32个位,占4个字节,一个内存访问周期可以完成4个字节的读写。
如果按照标准memcpy函数的实现,每次while循环只能复制1个字节,会浪费大量的内存访问周期。那我们能否按照32位CPU位宽,即4个字节为单位进行内存复制呢?CPU从内存取数据的过程,对齐存放的数据可加快CPU处理的速度,因为在同一个时钟周期内,CPU访问的数据总是按32位对齐访问的。(一些CPU能够非对齐访问,如Arm Cortex -M4支持半字的非对齐访问,和双字的字节对齐访问Arm Cortex -M4内存对齐问题)
例如:

  1. CPU寄存器从内存0x20000001开始取32位数据需要两次访问内存:第一次取3字节0x20000001~0x20000003,第二次取1字节0x20000004。
  2. CPU寄存器从内存0x20000002开始取32位数据需要两次访问内存:第一次取3字节0x20000002~0x20000003,第二次取1字节0x20000004~0x20000005。
  3. CPU寄存器从内存0x20000004开始取32位数据需要一次访问内存:一次取4字节0x20000004~0x20000007。
    在这里插入图片描述

参考上图,如果需要按对齐方式将0x20000001到0x2000002C中总共44字节的数据拷贝到0x20000041、0x20000082,0x200000C3,0x20000104这几个目标位置,最少的复制过程包括哪些步骤呢?

  • 0x20000001拷贝到0x20000041:四字节对齐。先按字节复制前3个字符,需要循环3次,再按4字节对齐复制0x20000004到0x2000002B之间的数据,共需要循环10次,最后一个字节复制1次,共计14次内存访问。

  • 0x20000001拷贝到0x20000082:因为源和目标无法同时对齐,只能按照字节复制,需要访问内存44次。

  • 0x20000001拷贝到0x200000C3:两字节对齐。先按字节复制前1个字符,需要循环1次,再按2字节对齐复制0x20000002到0x2000002B之间的数据,共需要循环21次,最后一个字节复制1次,共计23次内存访问。

  • 0x20000001拷贝到0x20000100:因为源和目标无法同时对齐,只能按照字节复制,需要访问内存44次。

通过上述分析,我们发现:

  1. 32位CPU中,4字节对齐的数据拷贝能够对齐,性能提升接近4倍;2字节对齐的数据拷贝能够对齐,性能提升接近2倍。

  2. 32位CPU中,源地址和目标地址对4取模的结果一致时为,4字节对齐;源地址和目标地址对2取模的结果一致时为,2字节对齐。

void* memcpy1(void* dst, const void* src, size_t len) {
    int* d = (int*) dst;
    const int* s = (const int*) src;
    
    for (size_t i=0; i < len; i += 4) {
        *d++ = *s++;
    }
    return d;
}

例如以上代码示例,按int的字长进行拷贝以提高效率。(使用时dst和src都需要4字节对齐,len需要被4整除)

三、指令流水线

指令流水线的作用是将一条指令分割成多个步骤,并由不同的部件顺序完成,
在同一时刻,每个部件可以同时执行多个指令的不同步骤,尽可能保证每个时钟周期都能输出一条指令结果。利用CPU流水线处理的指令的能力,解除数据依赖,(原理是前一条指令在译码,后一条语句在取指,充分压榨CPU处理器)。
在这里插入图片描述

void *memcpy2(void *desc,const void * src,size_t size)
{
	if((desc == NULL) && (src == NULL))
	{
		return NULL;
	}
	unsigned char desc1 = (unsigned char)desc;
	unsigned char src1 = (unsigned char)src;
	unsigned char count = size/4;
	while(count -- >0)
	{
		*desc1++ = *src1++;
		*desc1++ = *src1++;
		*desc1++ = *src1++;
		*desc1++ = *src1++;
	}
	count = size%4; 
	while(count --)
	{
		*desc1++ = *src1++;
	}
	return desc;
}

例如以上代码,将拷贝循环展开的跳转次数大幅减少,跳转次数最多减少到原来的1/4。

四、汇编版本优化

  1. 利用Arm的汇编指令集中的 LDMIA STMIA指令,通过这两条指令,一次就可以读取/写入多个字节(一次最大读取40字节),极大的加速了拷贝过程。rt_memcpy Cortex-M 汇编加速版
  2. 加载读取的汇编流水线优化
    加载和写入流水线细节优化

四、DMA

DMA:全称Direct Memory Access(直接存储器访问),把一个地址空间的值“复制”到另一个地址空间,使用DMA传输方式无需CPU直接控制传输,通过硬件为RAM和IO设备开辟一条直接传输数据的通道,使得CPU的效率大大提高。
本文使用STM32L4R9在主频120M的情况下进行对比实验。其DMA可以配置为传输的类型大小(字节、半字、字)。传输长度32K。

五、对比测试及结果

在这里插入图片描述
通过上述测试结果,我们发现:

  1. 按字节、半字、字传输,其效率也随单次拷贝字宽成倍增加。
  2. 汇编的memcpy效率比DMA32也要高2.65倍(后续研究了一下,应该时LDM指令对AHB总线的突发访问长度的配置了较大的数,大幅提高了效率比DMA的还要高。)

🎉🎉🎉完结撒花🎉🎉🎉
  • 9
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是使用STM32F103的USART1,通过DMA方式接收和发送数据的配置及实现函数: 1. 配置USART1 首先需要配置USART1的引脚、波特率、数据位、停止位等参数。在这里我们假设USART1的引脚已经正确连接。以下是典型的USART1配置代码: ```c // 使能USART1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // USART1 GPIO 配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // USART1 配置 USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); // 使能USART1 USART_Cmd(USART1, ENABLE); ``` 2. 配置DMA 接下来需要配置DMA,以便能够使用DMA方式进行数据传输。在这里我们使用DMA1的通道4来接收数据,使用通道5来发送数据。以下是DMA配置代码: ```c // 使能DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 配置DMA接收 DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buf; // 接收缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = RX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); // 配置DMA发送 DMA_DeInit(DMA1_Channel4); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buf; // 发送缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 0; // 初始发送长度为0 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 发送模式为正常模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel4, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE); // 使能发送完成中断 ``` 3. 编写DMA中断服务函数 在使用DMA进行数据传输时,需要编写DMA中断服务函数。在这里我们需要编写发送完成中断服务函数和接收完成中断服务函数。以下是中断服务函数的代码: ```c // DMA1 Channel4 发送完成中断服务函数 void DMA1_Channel4_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC4) != RESET) { // 清除中断标志位 DMA_ClearITPendingBit(DMA1_IT_TC4); // 关闭DMA发送通道 DMA_Cmd(DMA1_Channel4, DISABLE); // 发送完成回调函数 if (tx_complete_callback != NULL) tx_complete_callback(); } } // DMA1 Channel5 接收完成中断服务函数 void DMA1_Channel5_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC5) != RESET) { // 清除中断标志位 DMA_ClearITPendingBit(DMA1_IT_TC5); // 接收完成回调函数 if (rx_complete_callback != NULL) rx_complete_callback(); } } ``` 4. 实现发送和接收函数 最后需要实现发送和接收函数,以便能够使用DMA方式进行数据传输。以下是典型的发送和接收函数代码: ```c // 发送函数 void usart1_dma_send(uint8_t *buf, uint16_t len) { if (len == 0) return; // 等待DMA发送通道空闲 while (DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); // 设置发送长度 DMA_SetCurrDataCounter(DMA1_Channel4, len); // 设置发送缓冲区地址 DMA_SetMemoryBaseAddr(DMA1_Channel4, (uint32_t)buf); // 使能DMA发送通道 DMA_Cmd(DMA1_Channel4, ENABLE); } // 接收函数 uint16_t usart1_dma_receive(uint8_t *buf, uint16_t len) { uint16_t rx_len = RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); if (rx_len == 0) return 0; if (rx_len > len) rx_len = len; // 复制接收缓冲区数据 memcpy(buf, rx_buf, rx_len); // 重新启动DMA接收通道 DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUF_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); return rx_len; } ``` 以上就是使用STM32F103的USART1,通过DMA方式接收和发送数据的配置及实现函数。注意,以上代码仅供参考,具体实现需要结合实际应用场景进行相应的修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值