【单片机】给STM32F10x编写内存到内存DMA把ROM中的数据段复制到RAM里
光是把数据段里面的已初始化变量复制到RAM里是不行的,你还要让所有引用数据的代码都引用RAM里的数据,而不是ROM里的。这就需要了解一下链接器脚本的编写了。
这有个代码链接相关的概念要介绍一下:VMA和LMA。
VMA,Virtual Memory Address,和LMA,Load Memory Address。
LMA是你的ROM被加载的地址,而VMA则是你的ROM运行的时候,你的指令里面各种指针引用数据或者代码时的地址。
通常这两个地址在PC平台上,尤其是把代码加载到RAM里运行的平台,比如各种桌面版Linux,或者安卓手机、各种派等,LMA和VMA一般都是相同的。
Windows的PE在加载的时候,为了提高运行效率和内存管理效率,代码段和数据段的位置有时候会做出一些调整,比如调整到4K对齐的页面上,再运行。而它们在磁盘上以文件的形式存储的时候,为了节省存储空间、加快读取速度,它是一个段紧挨着另一个段的。所以此时的LMA相当于PE文件里面用到的地址,而VMA相当于PE文件被加载到内存里面以后用到的地址。
我们需要实现的效果是:单片机启动的时候,我们把数据段的数据从ROM(0x08000000到0x0800FFFF,LMA)的位置复制出来到RAM里(0x20000000到0x20004FFF),并且所有使用了这些变量的代码都是使用它们对应于RAM里的地址而不是ROM里的地址。
首先从链接器指令开始写。我们要让数据段的数据存储在ROM的位置,然后让代码引用它的时候,按照RAM的地址来引用它。ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) :ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.isr_vector : ALIGN(4)
{
KEEP(*(.isr_vector))
} > FLASH
.text : ALIGN(2)
{
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
*(.glue_7)
*(.glue_7t)
*(.vfp11_veneer)
*(.ARM.extab* .gnu.linkonce.armextab.*)
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
PROVIDE(_etext = .);
PROVIDE(_sidata = .);
} > FLASH
.stack : ALIGN(4)
{
PROVIDE(__stack_start__ = .);
. = 0x400;
PROVIDE(__stack_end__ = .);
} > RAM
.data : ALIGN(4)
{
PROVIDE(_sdata = .);
*(.data)
*(.data*)
PROVIDE(_edata = .);
} > RAM AT > FLASH
.bss : ALIGN(4)
{
PROVIDE(__bss_start__ = .);
*(.bss)
*(.bss*)
PROVIDE(__bss_end__ = .);
} > RAM
}这里面的“AT > FLASH”是关键,它告诉链接器,数据段和代码段一起存储到ROM里,然后引用的时候从RAM引用。
此时我们的目标已经完成了一半,因为数据它仍然存储在ROM里,需要我们手动把它拷贝出来,它才会到RAM里。
接下来就是如何拷贝的问题了。
因为不想为了这个占用DMA1通道1的中断,我不使用中断。extern char __bss_start__;
extern char __bss_end__;
extern char _sidata;
extern char _sdata;
extern char _edata;
static void _dma_copy_dataseg()
{
if(&_sdata == &_edata) return;
uint32_t regval = DMA1_Channel1->CCR & 0xFFFF8000;
regval |=
DMA_DIR_PeripheralSRC |
DMA_PeripheralInc_Enable |
DMA_MemoryInc_Enable |
DMA_PeripheralDataSize_Word |
DMA_MemoryDataSize_Word |
DMA_Mode_Normal |
DMA_Priority_Medium |
DMA_M2M_Enable;
DMA1_Channel1->CCR = regval;
DMA1_Channel1->CNDTR = ((uint32_t)&_edata - (uint32_t)&_sdata - 1) / 4 + 1;
DMA1_Channel1->CPAR = (uint32_t)&_sidata;
DMA1_Channel1->CMAR = (uint32_t)&_sdata;
DMA1_Channel1->CCR = regval | DMA_CCR1_EN;
}
static void _dma_copy_wait()
{
if(&_sdata == &_edata) return;
while(DMA1_Channel1->CNDTR);
}
static void _clear_bss()
{
if(&__bss_start__ == &__bss_end__) return;
uint32_t *ptr = (uint32_t*)&__bss_start__;
while(ptr < (uint32_t*)&__bss_end__) *ptr++ = 0;
}
void Reset_Handler()
{
do
{
_dma_copy_dataseg();
_clear_bss();
_dma_copy_wait();
main();
}while(1);
}其中Reset_Handler()是真·入口点,而main()则是以此为模板时使用的入口点。
我们用extern关键字把链接器脚本里生成的那些符号引用进来,来定位数据段在ROM中的位置,以及在RAM中的对应位置。
_dma_copy_dataseg()函数开启了一次内存到内存DMA,_clear_bss()函数使用循环来把bss段的RAM清零。最后通过一个忙等待来等待DMA的计数器归零。
DMA计数器归零后DMA传输就结束了,因此可以不用管它。
这里有个要注意的地方。_clear_bss()这个函数是使用循环来把一个地址上的内存清零。使用gcc编译器的时候,如果你把优化开到了O3,它就会检测到你的这个行为,并且把你的循环写内存操作替换为一个memset()调用。在编译参数上,使用-fno-tree-loop-distribute-patterns可以禁用这一类优化。
参考资料:
https://www.embeddedrelated.com/showthread/comp.arch.embedded/77071-1.php
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/4/html/Using_ld_the_GNU_Linker/index.html
https://stackoverflow.com/questions/46996893/gcc-replaces-loops-with-memcpy-and-memset
页:
[1]