我有一个能够与外部闪存芯片接口的 ARM 处理器。写入芯片的是为 ARM 架构编译的程序,准备执行。我需要知道如何从外部闪存获取这些数据到 ARM 处理器上执行。
我可以提前运行某种复制例程,将数据复制到可执行内存空间中吗?我想我可以,但是 ARM 处理器正在运行一个操作系统,我没有大量的闪存空间可供使用。我还希望能够一次安排两个甚至三个程序的执行,一次将多个程序复制到内部闪存中是不可行的。一旦程序在可访问的内存空间内,操作系统就可以用来启动程序,所以任何需要事先完成的事情都可以。
我有一个能够与外部闪存芯片接口的 ARM 处理器。写入芯片的是为 ARM 架构编译的程序,准备执行。我需要知道如何从外部闪存获取这些数据到 ARM 处理器上执行。
我可以提前运行某种复制例程,将数据复制到可执行内存空间中吗?我想我可以,但是 ARM 处理器正在运行一个操作系统,我没有大量的闪存空间可供使用。我还希望能够一次安排两个甚至三个程序的执行,一次将多个程序复制到内部闪存中是不可行的。一旦程序在可访问的内存空间内,操作系统就可以用来启动程序,所以任何需要事先完成的事情都可以。
通过阅读@FiddlingBits 和@ensc 的现有答案,我认为我可以提供不同的方法。
你说你的Flash芯片不能进行内存映射。这是一个很大的限制,但我们可以使用它。
是的,您可以提前运行复制程序。只要将其放入 RAM 中,您就可以执行它。
DMA使其更快:
如果您有一个外设 DMA 控制器(如 Atmel SAM3N 系列上可用的那个),那么您可以使用 DMA 控制器复制出大块内存,而您的处理器确实可以做一些有用的事情。
MMU 使其更简单:
如果您有可用的 MMU,那么您可以轻松地做到这一点,只需选择您希望代码执行的 RAM 区域,将代码复制到其中并在每个页面错误时,将正确的代码重新加载到同一区域。但是,这已经由@ensc 提出,所以我还没有添加任何新内容。
注意:如果不清楚,MMU 与 MPU 不同
没有 MMU 解决方案,但有 MPU 可用:
如果没有 MMU,任务会有点棘手,但仍然可以完成。您将需要了解您的编译器如何生成代码并阅读有关Position Independent Code (PIC)的信息。然后,您需要在 RAM 中分配一个区域,您将从该区域执行外部闪存芯片代码,并在其中复制部分代码(确保从正确的位置开始执行它)。需要将 MPU 配置为在任务尝试访问其分配区域之外的内存时生成故障,然后您将需要获取正确的内存(这可能成为一个复杂的过程)、重新加载并继续执行。
没有可用的 MMU 和 MPU:
如果您没有 MMU,现在这项任务将变得非常困难。在这两种情况下,您对外部代码的大小都有严格的限制。基本上,您现在存储在外部闪存芯片上的代码必须能够完全适合您将执行它的 RAM 中分配的区域。如果您可以将该代码拆分为彼此不交互的单独任务,那么您可以这样做,否则您不能。
如果您正在生成 PIC,那么您只需编译任务并将它们按顺序放置在内存中。否则,您将需要使用链接描述文件来控制代码生成,以便将存储在外部闪存中的每个已编译任务将从 RAM 中相同的预定义位置执行(这将需要您了解ld 覆盖或编译它们分别地)。
概括:
要更完整地回答您的问题,我需要知道您使用的是什么芯片和什么操作系统。有多少可用 RAM 也将帮助我更好地了解您的限制。
但是,您询问是否可以一次加载多个任务来运行。如果您像我建议的那样使用 PIC,应该可以这样做。如果没有,那么您需要提前决定每个任务将在哪里运行,并且可以同时加载/运行一些组合。
最后,根据您的系统和芯片,这可能容易或困难。
编辑1:
给出的附加信息:
提出的其他问题:
是的,可以逐条执行程序指令(但这种方法也有一个限制,我将在几秒钟内完成)。您将首先在内存中分配一个(4 字节对齐的)地址,您的单条指令将执行该地址。它是 32 位(4 字节)宽,紧随其后您将放置第二条永远不会更改的指令。第二条指令将是一个主管调用 (SVC),它会引发一个中断,允许您获取下一条指令,将其放入内存并重新开始。
虽然可能不建议这样做,因为您将花费更多时间进行上下文切换而不是执行代码,您实际上不能使用变量(您需要为此使用 RAM),您不能使用函数调用(除非您手动处理分支指令,哎哟!)并且您的闪存将被写入如此多的内容,以至于它将很快变得无用。最后一个,关于 Flash 变得无用,我假设你想从 RAM 中逐条执行指令。除了所有这些限制之外,您仍然需要为堆栈、堆和全局变量使用一些 RAM(有关详细信息,请参阅我的附录)。所有从外部闪存运行的任务都可以共享该区域,但您需要为此编写自定义链接描述文件,否则会浪费您的 RAM。
让您更清楚的是了解 C 代码是如何编译的。即使您使用 C++,首先要问自己这个问题,我的设备上的变量和指令编译到哪里?
基本上你在尝试之前必须知道的是:
编辑2:
如何使用外设 DMA 控制器:
对于我正在使用的微控制器,DMA 控制器实际上没有连接到嵌入式闪存进行读取或写入。如果您也是这种情况,您将无法使用它。但是,您的数据表在这方面不清楚,我怀疑您需要使用串行端口运行测试以查看它是否真的可以工作。
除此之外,我担心使用 DMA 控制器时的写入操作可能比您手动执行的操作更复杂,因为缓存页面写入。您需要确保只在页面内进行 DMA 传输,并且 DMA 传输永远不会跨越页面边界。另外,我不确定当您告诉 DMA 控制器从闪存写回同一位置时会发生什么(您可能需要这样做以确保只覆盖正确的部分)。
关于可用闪存和 RAM 的担忧:
我担心您之前关于一次执行一条指令的问题。如果是这样的话,那么你还不如写一个解释器。如果您没有足够的内存来包含您需要执行的任务的全部代码,那么您需要将任务编译为 PIC,并将全局偏移表 (GOT) 连同所有所需的内存一起放置在 ram 中任务的全局变量。这是解决整个任务没有足够空间的唯一方法。您还必须为其堆栈分配足够的空间。
如果您没有足够的 RAM(我怀疑您不会),您可以在每次需要在外部闪存芯片上的任务之间进行更改时将您的 RAM 内存换出并将其转储到闪存中,但我再次强烈建议您不要编写到您的闪存多次。这样,您可以使外部闪存上的任务为其全局共享一块 RAM。
对于所有其他情况,您将编写解释器。我什至做了不可思议的事情,我试图想出一种方法来使用微控制器内存控制器的中止状态(数据表中的第 18.3.4 节中止状态)作为 MPU,但甚至没有找到一种远程聪明的方法来用它。
编辑3:
我建议阅读数据表中的第 40.8.2 节非易失性存储器 (NVM) 位,这表明您的闪存最多有 10,000 次写入/擦除周期(我花了一段时间才找到它)。这意味着当您写入和擦除闪存区域时,您将在其中进行上下文切换任务的 10,000 次,该部分闪存将变得无用。
附录
在继续阅读我下面的评论之前,请先简短阅读此博客条目。
C 变量存在于嵌入式 ARM 芯片上的位置:
我不是从抽象概念而是从具体的例子中学到最好的东西,所以我会尝试给你代码来使用。基本上所有的魔法都发生在你的链接器脚本中。如果您阅读并理解它,您将看到您的代码会发生什么。现在让我们剖析一个:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
SEARCH_DIR(.)
/* Memory Spaces Definitions */
MEMORY
{
/* Here we are defining the memory regions that we will be placing
* different sections into. Different regions have different properties,
* for example, Flash is read only (because you need special instructions
* to write to it and writing is slow), while RAM is read write.
* In the brackets after the region name:
* r - denotes that reads are allowed from this memory region.
* w - denotes that writes are allowed to this memory region.
* x - means that you can execute code in this region.
*/
/* We will call Flash rom and RAM ram */
rom (rx) : ORIGIN = 0x00400000, LENGTH = 0x00040000 /* flash, 256K */
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00006000 /* sram, 24K */
}
/* The stack size used by the application. NOTE: you need to adjust */
STACK_SIZE = DEFINED(STACK_SIZE) ? STACK_SIZE : 0x800 ;
/* Section Definitions */
SECTIONS
{
.text :
{
. = ALIGN(4);
_sfixed = .;
KEEP(*(.vectors .vectors.*))
*(.text .text.* .gnu.linkonce.t.*)
*(.glue_7t) *(.glue_7)
*(.rodata .rodata* .gnu.linkonce.r.*) /* This is important, .rodata is in Flash */
*(.ARM.extab* .gnu.linkonce.armextab.*)
/* Support C constructors, and C destructors in both user code
and the C library. This also provides support for C++ code. */
. = ALIGN(4);
KEEP(*(.init))
. = ALIGN(4);
__preinit_array_start = .;
KEEP (*(.preinit_array))
__preinit_array_end = .;
. = ALIGN(4);
__init_array_start = .;
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array))
__init_array_end = .;
. = ALIGN(0x4);
KEEP (*crtbegin.o(.ctors))
KEEP (*(EXCLUDE_FILE (*crtend.o) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*crtend.o(.ctors))
. = ALIGN(4);
KEEP(*(.fini))
. = ALIGN(4);
__fini_array_start = .;
KEEP (*(.fini_array))
KEEP (*(SORT(.fini_array.*)))
__fini_array_end = .;
KEEP (*crtbegin.o(.dtors))
KEEP (*(EXCLUDE_FILE (*crtend.o) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*crtend.o(.dtors))
. = ALIGN(4);
_efixed = .; /* End of text section */
} > rom /* All the sections in the preceding curly braces are going to Flash in the order that they were specified */
/* .ARM.exidx is sorted, so has to go in its own output section. */
PROVIDE_HIDDEN (__exidx_start = .);
.ARM.exidx :
{
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
} > rom
PROVIDE_HIDDEN (__exidx_end = .);
. = ALIGN(4);
_etext = .;
/* Here is the .relocate section please pay special attention to it */
.relocate : AT (_etext)
{
. = ALIGN(4);
_srelocate = .;
*(.ramfunc .ramfunc.*);
*(.data .data.*);
. = ALIGN(4);
_erelocate = .;
} > ram /* All the sections in the preceding curly braces are going to RAM in the order that they were specified */
/* .bss section which is used for uninitialized but zeroed data */
/* Please note the NOLOAD flag, this means that when you compile the code this section won't be in your .hex, .bin or .o files but will be just assumed to have been allocated */
.bss (NOLOAD) :
{
. = ALIGN(4);
_sbss = . ;
_szero = .;
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = . ;
_ezero = .;
} > ram
/* stack section */
.stack (NOLOAD):
{
. = ALIGN(8);
_sstack = .;
. = . + STACK_SIZE;
. = ALIGN(8);
_estack = .;
} > ram
. = ALIGN(4);
_end = . ;
/* heap extends from here to end of memory */
}
这是一个为 SAM3N 自动生成的链接描述文件(你的链接描述文件应该只在内存区域定义上有所不同)。现在,让我们来看看您的设备在关机后启动时会发生什么。
发生的第一件事是 ARM 内核读取存储在闪存向量表中的地址,该地址指向您的复位向量。重置向量只是一个函数,对我来说它也是由 Atmel Studio 自动生成的。这里是:
void Reset_Handler(void)
{
uint32_t *pSrc, *pDest;
/* Initialize the relocate segment */
pSrc = &_etext;
pDest = &_srelocate;
/* This code copyes all of the memory for "initialised globals" from Flash to RAM */
if (pSrc != pDest) {
for (; pDest < &_erelocate;) {
*pDest++ = *pSrc++;
}
}
/* Clear the zero segment (.bss). Since it in RAM it could be anything after a reset so zero it. */
for (pDest = &_szero; pDest < &_ezero;) {
*pDest++ = 0;
}
/* Set the vector table base address */
pSrc = (uint32_t *) & _sfixed;
SCB->VTOR = ((uint32_t) pSrc & SCB_VTOR_TBLOFF_Msk);
if (((uint32_t) pSrc >= IRAM_ADDR) && ((uint32_t) pSrc < IRAM_ADDR + IRAM_SIZE)) {
SCB->VTOR |= 1 << SCB_VTOR_TBLBASE_Pos;
}
/* Initialize the C library */
__libc_init_array();
/* Branch to main function */
main();
/* Infinite loop */
while (1);
}
现在,请稍等片刻,我将解释您编写的 C 代码如何适合所有这些。
考虑以下代码示例:
int UninitializedGlobal; // Goes to the .bss segment (RAM)
int ZeroedGlobal[10] = { 0 }; // Goes to the .bss segment (RAM)
int InitializedGlobal[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 11 }; // Goes to the .relocate segment (RAM and FLASH)
const int ConstInitializedGlobal[10] = { 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 }; // Goes to the .rodata segment (FLASH)
void function(int parameter)
{
static int UninitializedStatic; // Same as UninitializedGlobal above.
static int ZeroedStatic = 0; // Same as ZeroedGlobal above.
static int InitializedStatic = 7; // Same as InitializedGlobal above.
static const int ConstStatic = 18; // Same as ConstInitializedGlobal above. Might get optimized away though, lets assume it doesn't.
int UninitializedLocal; // Stacked. (RAM)
int ZeroedLocal = 0; // Stacked and then initialized (RAM)
int InitializedLocal = 7; // Stacked and then initialized (RAM)
const int ConstLocal = 91; // Not actually sure where this one goes. I assume optimized away.
// Do something with all those lovely variables...
}
这取决于闪存和/或 CPU 的种类。NOR 闪存通常映射到内存中,因此您可以直接跳转到其中。必须将 NAND 闪存(取决于 SOC)读入本地内存(SRAM、DRAM(--> 需要额外的初始化!))。
编辑:
SPI 也不能映射到 RAM。您必须对 SOC 的 SPI 控制器和 SPI 闪存进行编程。用于 SPI 闪存的协议通常在其手册中有所描述;它很可能是一个通用协议,因此您可以重用现有驱动程序。