0xAA55 发表于 2018-4-3 00:08:13

【单片机】给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]
查看完整版本: 【单片机】给STM32F10x编写内存到内存DMA把ROM中的数据段复制到RAM里