对比RP2040可执行内存与从闪存中加载程序到SRAM

对比RP2040可执行内存(闪存XIP空间、条带SRAM、区块SRAM)。探讨内存规划、数据冒险与结构冒险。将闪存中保存的程序载入SRAM后再执行。

--by Captdam @ Mar 12, 2026 Mar 8, 2026

Index

这篇文章的针对读者为32位RP2040与ARM Cortex-M0+新手,但是熟悉使用汇编与C语言开发8位单片裸金属应用的机开发者。

因为我们将开发裸金属应用,我们将会直接读写单片的控制寄存器,并不使用任何库。

我们将大量依靠文档。文档中包括了我们所需要知道的所有单片机控制寄存器的信息。

因为RP2040文档更新的缘故,而且他们还决定把老文档的链接直接重定向到新文档,我决定在本地保存当前版本的副本(2025-02-20)。你可以通过原始链接(取样于2026-02-10)获取该文档。

RP2040文档版权页
RP2040文档版权页

这篇文章基于我的上一篇文章:裸金属切换RP2040时钟源:ROSC,XOSC与PLL,你应该先看看那一篇文章。

内存分区的不同

Cortex-M0+内存分区
Cortex-M0+内存分区 ©RP2040
RP2040内存分区
RP2040内存分区 ©RP2040

(RP2040中使用的)ARM Cortex M0+ CPU使用冯费鲁曼架构,也就是说只有一个通用的内存空间,既可以用作程序指令,也可以用于数据,甚至包括设备(控制寄存器)。

该列表展示了不同分区用于不同目的。其中,地址0x00000000到0x3FFFFFFF和地址0x60000000到0x9FFFFFFF可以用作程序。

RP2040进一步将这些可执行分区切分为更小的区块:

  • 0x00000000 - 0x00FFFFFF - 启动ROM。16kiB大小。不能用来储存用户程序。但是,可以用来执行启动程序并提供一些功能程序。
  • 0x10000000 - 0x1FFFFFFF - (映射)XIP闪存。最大支持16MiB空间(24位地址)。可以通过不同的基地址使用不同的缓存策略。
  • 0x20000000 - 0x2FFFFFFF - SRAM。256kiB + 8kiB大小。可以通过不同的基地址使用不同的访问模式。

启动ROM

RP2040启动ROM地址
RP2040启动ROM地址 ©RP2040

RP2040带有16kiB启动ROM,其地址位于内存空间的开头。上电后将执行启动ROM作为一级引导程序。

启动ROM在生产时被烧录,且不可求改。因此,我们没办法将我们的的程序(用户程序)写入这个空间。就算写了也不会起作用。但是,用户程序可以调用启动ROM中的功能程序。

(映射)闪存

RP2040 XIP地址
RP2040 XIP地址 ©RP2040

外部闪存没有直接和RP2040的内存空间相连接。不过,RP2040提供了XIP,通过SSI来讲外部闪存缓存到XIP内存分区。这使得外部闪存在逻辑上成为了RP2040内部存储的一部分。

最大支持1616MiB闪存,或者说是0xFFFFFF字节,也就是24位地址。闪存将被镜像4次以使用不同的缓存策略:

  • 0x10000000 - 0x10FFFFFF - 可缓存可分配。读取时会先检查缓存。如果未命中,则更新缓存。
  • 0x11000000 - 0x11FFFFFF - 可缓存不分配。读取时会先检查缓存。如果未命中,则读取闪存但是不更新缓存。
  • 0x12000000 - 0x12FFFFFF - 不缓存可分配。不检查缓存,直接读闪存并更新缓存。
  • 0x13000000 - 0x13FFFFFF - 不缓存不分配。跳过缓存,直接读闪存,不更新缓存。

XIP针对双核顺序执行和附近读取的情况优化,使用16 KiB双路缓存。命中为1周期。

如果缓存未命中,XIP将从闪存读取需要的数据。将造成停顿。

Pico板载闪存大小为2MiB。

SRAM

RP2040 SRAM地址
RP2040 SRAM地址 ©RP2040

RP2040提供264 kiB的SRAM,分6个区块。SRAM区块0到3各64 kiB,区块4和5各64 kiB。读写都只需要1周期。

4个64k内存区块可以被组合为一个256 kiB的条纹区块。这个条纹区块的地址为0x20000000 - 0x2003FFFF。其中,0x20000000指向SRAM区块0的第0个词,0x20000004指向SRAM区块1的第0个词,0x20000010指向SRAM区块0的第1个词,以此类推。

条纹区块拥有最高的随机访问效率。当一个核心在访问某个区块时,另外一个核心访问同一个区块的可能性只有1/4。也就是说,25%的结构冒险机率。

2个64k的内存区块不能条纹化。SRAM区块4的地址为0x20040000 - 0x20040FFF,SRAM区块5的地址为address 0x20041000 - 0x20041FFF。

我们可以把条纹化的四个64k区块和两个4k区块看作是一个264 kiB大小的整体。

如果我们不想使用条纹化的SRAM区块0到3,我们可以通过地址0x21000000 - 0x2103FFFF来访问它们。其中,区块0为0x21000000 - 0x2100FFFF,区块1为0x21010000 - 0x2101FFFF,以此类推。

非条纹化的SRAM在我们将SRAM区块用作核心独占的空间时最有效。如果我们可以保证一个区块只会同时被同一个核心访问,另一个核心(或是DMA)不会来凑热闹的话,这就不会有任何结构冒险。

我们不能将两个4k区块和四个非条纹化的64k区块看作是一个整体,因为这是不连贯的。地址0x20042000 - 0x20FFFFF为空。但是,这四个非条纹化的64k区块可以看作一个256 kiB大小的整体。

其它内存

RP2040还有两个缓冲在未使用时可以用来存放程序或数据:

停顿

缓存未命中

RP2040 XIP
XIP对闪存进行缓存以便CPU直接读取 ©RPI2040

CPU的运行速度比外部设备要快得多。为了防止外部设备拖慢CPU,外部设备的数据将被缓存进CPU内部的一块小但快(和CPU一样快或是慢一丁点)的内存。当CPU需要从外部设备读取数据时,不需要等待外部设备返回数据,而是从从缓存读取。如果需要的数据在缓存中,CPU将读取,这个过程不会有任何延迟(或者只有很低的延迟)。如果需要的数据不在缓存中,CPU就会停下来,直到这个数据被载入缓存。除了会拖慢速度外缓存未命中不会造成危害。

这个名词被用在现代电脑CPU和内存。CPU(运行在GHz数量级)比内存(运行在100MHz数量级,注意内存的数据传输速度和内存时钟速度是两个概念)快成十上百倍。因此,CPU会内置数据与指令缓存。有时,CPU还会有多级缓存(L1、L2、L3)。

RP2040使用的CPU并没有缓存,因为SRAM和CPU运行速度一样,访问SRAM只需要1个周期。但是,用于储存程序的外部闪存被缓存在XIP缓存中。当执行闪存中的程序时,CPU将从XIP地址空间中读取特定地址的指令(或数据)。XIP硬件会先检查需要的指令(或数据)是否被缓存。如果命中,该指令(或数据)将被立即返回。反之,CPU就会被停下来,直到XIP硬件将需要的指令(或数据)从外部闪存读入XIP缓存中。

XIP使用16 kiB双路缓存。

结构冒险

RP2040 AHB-Lite交换器
RP2040 AHB-Lite交换器有4个上行接口和10个下行接口 ©RP2040
RP2040 SIO连接
SIO直接和两个CPU相连 ©RP2040

结构冒险发生在同一个资源被多个设备访问的情况下,但是该资源只能同时响应一个设备。因此,其它设备就需要暂时停下来(等待)。除了会拖慢速度外结构冒险不会造成危害。

RP2040使用的CPU并没有缓存。因此,在执行位于特定地址的一条指令时,必须在执行时从该地址将这条指令读取出来。同样,在读写位于特定地址的一项数据(普通数据或是控制寄存器的值)时,必须在执行时在该地址读写这项数据。

如图,4:10 AHB-Lite交换器用于将CPU和DMA与内存空间相连。在同一时间点内,每个上行线路(如CPU0到AHB-Lite交换器)和每个下行线路(如SRAM0到AHB-Lite交换器)最多只能有一项读写操作。

所以,内存读写操作如ldrstr需要2个周期。第一个周期用于读取指令,第二个周期用于读写数据。

另外,如果两个CPU(或是一个CPU和DMA)同时访问同一个下行线路,其中一个必须被停下来,直到另一个访问完成。

我们可以设置4个上行线路(CPU0、CPU1、DMA读、 DMA写)的访问的优先级。如果相同优先级的两个上行线路同时访问同一个下行内存或设备,则根据转轮的方式分配优先权。

另一方面,读写SIO只需要1个CPU周期,因为SIO和CPU直接相连,不需要绕道AHB-Lite交换器。也就是说,我们可以在一个周期内访问SIO和通过AHB-Lite交换器读取指令。

如果两个CPU同时写同一个SIO(GPIO输出相关),CPU0的操作先被执行,然后立刻执行CPU1的操作。换言之,只有CPU1的输出有效。

内存策略

在这个例子中,设想我们在创建一个时序很重要的双核应用,因此我们想要避免任何缓存未命中或结构冒险的情况。也就是说:

这个例子中我将会使用以下模式:

因为读写SRAM总需要2个周期,所以把一个核心的程序与数据放在同一个SRAM区块也没问题。

大部分情况下,程序都很小,数据却很大。所以,我决定使用小的SRAM区块来储存程序,大的SRAM区块来储存数据。另外,我也不爱用递归,所以栈也不会很大。当然,使用大的区块来装程序,或是把栈和程序放在不同的区块也完全没问题,仁者见仁智者见智。

我将不使用条纹内存。条纹内存只在程序员不给每个上线线路分配独占区块时才有用。条纹区块不提供任何保证,只会整得时序乱七八糟。

USB启动双核程序

USB启动双核程序的工程文件在这里

三级启动引导程序

首先,我们将创建一个汇编程序叫做boot3(三级启动引导程序)。这个程序用来启动核心1执行核心1的程序,然后开始核心0的程序。将这个文件保存为boot3.s

RP2040 SIO_FIFO_*
跨核心信箱FIFO状态与读写 @ 0xD0000050 - 0xD000005B ©RPI2040

.cpu cortex-m0plus
.thumb
.align 2
.thumb_func

.section .boot, "ax"

.global boot3
boot3:
	@ Start core 1 main program
	ldr	r7, =c1_vector
	ldr	r6, =0xd0000050		@ SIO_FIFO_ST
	mov	r0, #1
	str	r0, [r6, #0x04]		@ SIO_FIFO_WR = 1
	str	r7, [r6, #0x04]		@ SIO_FIFO_WR = c1_vector
	ldr	r0, [r7, #0x00]
	str	r0, [r6, #0x04]		@ SIO_FIFO_WR = c1_vector[0] = SP
	ldr	r0, [r7, #0x04]
	str	r0, [r6, #0x04]		@ SIO_FIFO_WR = c1_vector[1] = c0_reset
	sev

	@ Enter core 0 main program
	ldr	r7, =c0_vector
	ldr	r0, [r7, #0x00]		@ c1_vector[0] = SP
	mov	sp, r0
	ldr	r0, [r7, #0x04]		@ c1_vector[1] = c1_reset
	bx	r0
	
.global boot3_clearInterprocessorMailboxRx
boot3_clearInterprocessorMailboxRx:
	push	{r0, r1}
	ldr	r1, =0xd0000050		@ SIO_FIFO_ST
1:	ldr	r0, [r1, #0x00]		@ SIO_FIFO_ST
	lsr	r0, #1			@ VLD
	bcc	2f
	ldr	r0, [r1, #0x08]		@ SIO_FIFO_RD
	b	1b
2:	mov	r0, #0b1100
	str	r0, [r1, #0x00]		@ SIO_FIFO_ST
	pop	{r0, r1}
	bx	lr
			

程序boot3中没有什么新知识,我们在之前启动核心1的那篇文章中都讨论过了。

我们新建了一个子程序用于清空跨核心信箱,即boot3_clearInterprocessorMailboxRx。这个子程序将一直读FIFO直到FIFO不在有效(说明FIFO空了)。这个子程序应该在核心0与核心1的程序开始后立刻执行。

当两个核心都启动并清空了各自的邮箱后,启动引导程序就不再被需要了。程序可以使用SRAM区块0到3并覆盖该启动引导程序。

将这段代码命名为.boot并设置为可执行可分配,这样链接器才会分配空间。同时,将两个程序都设置为全局,以便在其它文件中使用。

核心0程序

接下来,我们将创建核心0运行的汇编代码程序。将该程序保存为main.s

核心0独占数据


.section .c0_data, "aw"

c0_static0:	.space	3
c0_static1:	.byte	0x15
		

创建一个新段落.c0_data作为核心0的数据:

  • c0_static0 - 保留3个字节。初始值未定(,汇编器会填充0)。
  • c0_static1 - 1字节长,初始值0x15。

我将不会使用这些变量。这里只是为了验证核心0独占数据将被与保存在核心0程序相同的SRAM区块。

核心0向量表


.section .c0_vector, "aw"
.global c0_vector
c0_vector:
	.word	0x20041000
	.word	c0_reset + 1
		

创建一个新段落.c0_vector作为核心0的向量表:

  1. 第一个向量为初始SP。设置为SRAM区块4的顶端。
  2. 第一个向量为核心0的进入点。设置为c0_reset。另外,因为Thumb要求指令地址LSB为1,于是我们+1。

虽然向量表应该包含48个32位向量,这个例子中我们不使用中断,所以省略它们。

核心0程序


.section .c0_text, "awx"

.global c0_reset
c0_reset:
	bl	boot3_clearInterprocessorMailboxRx

	ldr	r7, =0x4000f000		@ RESETS_RESET  + 0x3000
	mov	r0, #(1<<5)		@ IO_BANK0
	str	r0, [r7, #0x00]
	ldr	r7, =0x400140cc		@ IO_BANK0_GPIO25_CTRL
	mov	r0, #5			@ SIO
	str	r0, [r7, #0x00]
	ldr	r7, =0xd0000020		@ SIO_GPIO_OE
	ldr	r0, =(1<<25)		@ GPIO25
	str	r0, [r7, #0x00]

	ldr	r0, =c1_static0
	mov	r8, r0
1:
	mov	r0, r8
@	ldm	r0, {r0, r1, r2, r3, r4, r5, r6, r7}	@ Structural hazard test 
	b	1b
		

创建一个新段落.c0_text作为核心0的程序:

这里只有一个程序为c0_reset。我们将:

  1. 调用boot3中创建的boot3_clearInterprocessorMailboxRx来清空邮箱。
  2. 配置GPIO允许输出。
  3. 结构冒险测试。这个可选的部分用来对核心1造成结构冒险。我们暂时注释掉这部分。

核心1程序

接下来,我们将创建核心1运行的C语言程序。将该程序保存为main.c

核心1程序


#include <stdint.h>

extern void boot3_clearInterprocessorMailboxRx();

void c1_reset() __attribute__((section (".c1_text"))) __attribute__((naked));
void c1_reset() {
	boot3_clearInterprocessorMailboxRx();
	for (;;) {
		*(uint32_t volatile * const)(0xd000001c) = (1<<25); // GPIO XOR
	}
}
		

声明外部方程boot3_clearInterprocessorMailboxRx。这告诉编译器方程boot3_clearInterprocessorMailboxRx是外部的。所以,当编译时,编译器就不会抱怨找不到这个程序了。(链接时再找)

创建核心1的主程序c1_reset,使用如下属性:

  1. section (".c1_text") - 将该方程放在.c1_text段落,不使用默认的程序(指令文本)的.text段落。
  2. naked - 这是主程序且永远不会返回。因此,没有必要保存母程序的栈。默认情况下,链接寄存器lr和被使用的通用寄存器会被入栈以保留其值,因为母程序不期望这些寄存器被修改。将主程序定义为naked可以节约一些栈空间。

这个方程中我们将:

  1. 调用boot3中定义的boot3_clearInterprocessorMailboxRx来清空邮箱。
  2. 在一个死循环中翻转输出。

核心1向量表


uint32_t c1_vector[48] __attribute__ ((section(".c1_vector"))) = {
	0x20042000,
	(uint32_t)c1_reset
};
		

创建向量表,并将其放在.c1_vector中。该向量表包括:

  1. 第一个向量为初始SP。设置为SRAM区块5顶端。
  2. 第一个向量为核心1的进入点。设置为c1_reset。在C语言中,编译器自动设置好了地址的LSB。

这个例子中我们不使用中断,所以不定义这些向量。

核心1独占数据


__attribute__((section (".c1_data"))) volatile uint32_t c1_static0 = 5;
		

创建一个新段落.c1_data作为核心1的数据:

定义一个变量c1_static0,该变量:

  • 这个变量为核心1独占。所以我们将它放在.c1_data中。
  • 初始值为5。
  • Make it volatile, this prevents the compiler from optimizing it into a register-save variable.

我将不会在核心1中使用这些变量。这里只是为了验证核心1独占数据将被与保存在核心1程序相同的SRAM区块。

链接与编译

现在,创建一个链接脚本main.ld


MEMORY {
	SRAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k
	SRAM_4(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
	SRAM_5(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
	SRAM_0(rwx) : ORIGIN = 0x21000000, LENGTH = 64k
	SRAM_1(rwx) : ORIGIN = 0x21010000, LENGTH = 64k
	SRAM_2(rwx) : ORIGIN = 0x21020000, LENGTH = 64k
	SRAM_3(rwx) : ORIGIN = 0x21030000, LENGTH = 64k
}

ENTRY(boot3)

SECTIONS {
	.text : {
		*(boot3)
		*(boot3_clearInterprocessorMailboxRx)
	} > SRAM

	.core0 : {
		. = ALIGN (256);
		*(.c0_vector)
		*(.c0_data)
		*(.c0_text)
	} > SRAM_4

	.core1 : {
		. = ALIGN (256);
		*(.c1_vector)
		*(.c1_data)
		*(.c1_text)
	} > SRAM_5
}
		

首先提供各个内存区块的地址。

将我们的3级启动引导程序boot3boot3_clearInterprocessorMailboxRx放在SRAM(条纹化内存)中。设置进入点为boot3

对于USB启动,进入点必须在条纹化内存的开头,地址0x20000000(Thumb 0x20000001)。另外,必须使用.text作为段落名。

将核心0的向量表.c0_vector、数据.c0_data、程序.c0_text放在SRAM区块4中。向量表在最开头。

将核心1的向量表.c1_vector、数据.c1_data、程序.c1_text放在SRAM区块5中。向量表在最开头。

使用. = ALIGN (256)是冗余的,因为各个内存区块的起始地址已经4k对其了。

接下来,编译这个项目:


arm-none-eabi-as --warn --fatal-warnings -g boot3.s -o boot3.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs boot3.o > boot3.s.list

arm-none-eabi-as --warn --fatal-warnings -g main.s -o main.s.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs main.s.o > main.s.list

arm-none-eabi-gcc -mcpu=cortex-m0plus -c -O3 main.c -o main.c.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs main.c.o > main.c.list

arm-none-eabi-ld -nostdlib -nostartfiles -T main.ld boot3.o main.s.o main.c.o -o main.elf
arm-none-eabi-objdump --disassembler-options=force-thumb -dxs main.elf > main.list
pico-elf2uf2 main.elf main.uf2
	

验证输出

现在,我们可以通过反汇编的代码来验证我们的程序,见main.list

boot


Disassembly of section .boot:

20000000 <boot3>:
20000000:	4f0c      	ldr	r7, [pc, #48]	; (20000034 <boot3_clearInterprocessorMailboxRx+0x16>)
20000002:	4e0d      	ldr	r6, [pc, #52]	; (20000038 <boot3_clearInterprocessorMailboxRx+0x1a>)
20000004:	2001      	movs	r0, #1
20000006:	6070      	str	r0, [r6, #4]
20000008:	6077      	str	r7, [r6, #4]
2000000a:	6838      	ldr	r0, [r7, #0]
2000000c:	6070      	str	r0, [r6, #4]
2000000e:	6878      	ldr	r0, [r7, #4]
20000010:	6070      	str	r0, [r6, #4]
20000012:	bf40      	sev
20000014:	4f09      	ldr	r7, [pc, #36]	; (2000003c <boot3_clearInterprocessorMailboxRx+0x1e>)
20000016:	6838      	ldr	r0, [r7, #0]
20000018:	4685      	mov	sp, r0
2000001a:	6878      	ldr	r0, [r7, #4]
2000001c:	4700      	bx	r0

2000001e <boot3_clearInterprocessorMailboxRx>:
2000001e:	b403      	push	{r0, r1}
20000020:	4905      	ldr	r1, [pc, #20]	; (20000038 <boot3_clearInterprocessorMailboxRx+0x1a>)
20000022:	6808      	ldr	r0, [r1, #0]
20000024:	0840      	lsrs	r0, r0, #1
20000026:	d301      	bcc.n	2000002c <boot3_clearInterprocessorMailboxRx+0xe>
20000028:	6888      	ldr	r0, [r1, #8]
2000002a:	e7fa      	b.n	20000022 <boot3_clearInterprocessorMailboxRx+0x4>
2000002c:	200c      	movs	r0, #12
2000002e:	6008      	str	r0, [r1, #0]
20000030:	bc03      	pop	{r0, r1}
20000032:	4770      	bx	lr

20000034:	20041000 	.word	0x20041000
20000038:	d0000050 	.word	0xd0000050
2000003c:	20040000 	.word	0x20040000
		

程序boot3位于条纹SRAM的开头,地址0x20000000。该程序将在启动执行。

程序boot3_clearInterprocessorMailboxRx紧接在boot3之后,地址0x2000001e,仍然是条纹SRAM之中。

接下来是汇编代码中使用的常量,包括:

  • .word 0x20041000 @ 20000034 - 核心1的向量表地址。
  • .word 0x20040000 @ 2000003C - 核心0的向量表地址。

core0


Disassembly of section .core0:

20040000 <c0_vector>:
20040000:	20041000 	.word	0x20041000
20040004:	2004000d 	.word	0x2004000d

20040008 <c0_static0>:
20040008:	0000      	.short	0x0000
	...

2004000b <c0_static1>:
2004000b:	15          	.byte	0x15

2004000c <c0_reset>:
2004000c:	f7c0 f807 	bl	2000001e <boot3_clearInterprocessorMailboxRx>
20040010:	4f06      	ldr	r7, [pc, #24]	; (2004002c <c0_reset+0x20>)
20040012:	2020      	movs	r0, #32
20040014:	6038      	str	r0, [r7, #0]
20040016:	4f06      	ldr	r7, [pc, #24]	; (20040030 <c0_reset+0x24>)
20040018:	2005      	movs	r0, #5
2004001a:	6038      	str	r0, [r7, #0]
2004001c:	4f05      	ldr	r7, [pc, #20]	; (20040034 <c0_reset+0x28>)
2004001e:	4806      	ldr	r0, [pc, #24]	; (20040038 <c0_reset+0x2c>)
20040020:	6038      	str	r0, [r7, #0]
20040022:	4806      	ldr	r0, [pc, #24]	; (2004003c <c0_reset+0x30>)
20040024:	4680      	mov	r8, r0
20040026:	4640      	mov	r0, r8
20040028:	e7fd      	b.n	20040026 <c0_reset+0x1a>
2004002a:	0000      	.short	0x0000

2004002c:	4000f000 	.word	0x4000f000
20040030:	400140cc 	.word	0x400140cc
20040034:	d0000020 	.word	0xd0000020
20040038:	02000000 	.word	0x02000000
2004003c:	200410c0 	.word	0x200410c0
		

核心0的向量表c0_vector被放在SRAM区块4的开头,地址0x20040000。该地址已256字节对齐。

核心0独占数据c0_static0c0_static1紧接在c0_vector之后,地址0x20040008,仍然是SRAM区块4。

c0_static0c0_static1是byte数据,不用对其。

核心0的程序c0_reset紧接在c0_static1之后,地址0x2004000c,仍然是SRAM区块4。

接下来是汇编代码中使用的常量。

core1


Disassembly of section .core1:

20041000 <c1_vector>:
20041000:	20042000 200410c5 00000000 00000000     . . ... ........
	...

200410c0 <c1_static0>:
200410c0:	00000005                                ....

200410c4 <c1_reset>:
200410c4:	f7be ffab 	bl	2000001e <boot3_clearInterprocessorMailboxRx>
200410c8:	2380      	movs	r3, #128	; 0x80
200410ca:	4a02      	ldr	r2, [pc, #8]	; (200410d4 <c1_reset+0x10>)
200410cc:	049b      	lsls	r3, r3, #18
200410ce:	6013      	str	r3, [r2, #0]
200410d0:	6013      	str	r3, [r2, #0]
200410d2:	e7fc      	b.n	200410ce <c1_reset+0xa>
200410d4:	d000001c 	.word	0xd000001c
		

核心1的向量表c1_vector被放在SRAM区块5的开头,地址0x20041000。该地址已256字节对齐。

核心1独占数据c1_static0紧接在c1_vector之后,地址0x200410c0,仍然是SRAM区块5。

核心1的程序c1_reset紧接在c1_static1之后,地址0x200410c4,仍然是SRAM区块5。

不知道为啥编译器决定在循环中翻转输出两次。我们看到两行str r3, [r2, #0]

接下来是汇编代码中使用的常量。

测量输出

下载uf2文件。然后,使用示波器来测量输出波形。

使用ROSC且没有冒险时的输出波形
使用ROSC且没有冒险时的输出波形

默认使用ROSC作为系统时钟源。我的这块芯片的ROSC为5.8MHz。

写SIO消耗1周期,跳转消耗2周期。因此,我们可以看到输出波形频率为5.8MHz / 4 = 1.45MHz,占空比1/4。

结构冒险的效果

为了验证两个核心同时访问同一个内存区块会造成核心停顿,我们将在核心0的程序中添加一个死循环。在这个死循环中,我们将尝试从保存核心1程序指令的SRAM区块读取数据。这样,该SRAM区块就必须要同时响应核心1的指令读取和核心0的数据读取。


	ldr	r0, =c1_static0
	mov	r8, r0
1:
	mov	r0, r8
	ldm	r0, {r0, r1, r2, r3, r4, r5, r6, r7}	@ Structural hazard test 
	b	1b
		

我们在c0_reset中加入:ldm r0, {r0, r1, r2, r3, r4, r5, r6, r7}。在这个循环中,我们:

  1. 获得c1_static0的地址,该变量和核心1的程序指令保存在同一个SRAM区块上。
  2. 该地址装入r0。(1周期)
  3. 使用r0作为指针读8个数据,导致核心1使用的SRAM区块结构冒险。(9周期,8读取)
  4. 回到第2步并重复。(2周期)

综上,核心0每12个周期将造成核心1停顿8周期。

编译、链接、下载uf2文件。然后,使用示波器测量输出波形。

使用ROSC且有冒险时的输出波形
使用ROSC且有冒险时的输出波形

可以看到,频率降低了,表示核心1停顿了。

因为AHB-Lite交换器使用轮盘形式分配访问权,有时核心1使核心0停顿(得到和之前相同的波长),有时核心0使核心1停顿(更长的波长)。

Flash启动双核程序

在上一个例子中,我们成功地将一个双核程序下载到SRAM区块4和5中,并使用各个区块作为各个核心的独占资源,以避免结构冒险。

但是,只要一掉电,程序就没了。有没有办法将程序保存在闪存中,启动时再将程序从闪存载入SRAM区块呢?

在电脑上,当我们编译一个程序,程序不仅仅是一堆CPU指令,还包括有关于设置运行环境和准备数据的信息,才能让这个程序可执行。当我们执行一个程序时:

  1. 操作系统(OS)开机。
  2. OS将程序从硬盘中载入内存中。
  3. OS读取程序文件中的“信息”部分,设置运行环境,并将程序内容(程序与数据)放在需要的地址。
  4. 程序进入点载入PC,开始执行。

我们在RP2040上的操作相似:

  1. 片上启动引导程序在条纹SRAM中执行二级启动引导程序。接下来,二级启动引导程序将配置SSI中的XIP功能。这类似于启动OS。
  2. 二级启动引导程序启动我们的三级启动引导程序。该三级启动引导程序在闪存(XIP)空间中执行。
  3. 我们的三级启动引导程序将向量表、数据、程序指令载入SRAM区块中。另外,该三级启动引导程序还会进行其它设置,例如时钟源、内存保护、中断等……
  4. 我们的三级启动引导程(运行在核心0上)启动核心1执行核心1主程序,并跳转到核心0的主程序。

闪存启动双核程序的工程文件在这里

三级引导程序

首先,我们将创建一个汇编程序叫做boot3(三级引导程序)。和上一个例子类似,这个程序用来启动核心1执行核心1的程序,然后开始核心0的程序。此外,这个程序还会设置系统时钟为133MHz PLL并将程序从闪存中复制到SRAM区块中。将这个文件保存为boot3.s

Boot section


.cpu cortex-m0plus
.thumb
.align 2
.thumb_func

.section .boot3, "awx"
		

将整个文件都放在.boot3段中,并设置为可执行可分配,这样链接器才会分配空间。

Vector Table


	.word	0x20041000
	.word	boot3 + 1
		

根据我们使用的二级启动引导程序(SDK二级启动引导程序)的需求,我们必须把向量表在最开头。该向量表包括:

  1. 该程序使用的初始SP。我们这里设置为SRAM区块4的顶端。
  2. 进入点。我们设置为程序boot3。因为是Thumb指令程序,所以设置地址的LSB为1。

boot3_copyText


boot3_copyText:
	ldrh	r3, [r1, #0]		@ r1 = src, use halfword because insturction is 16-bit wide, code block may not be 4-byte aligned
	strh	r3, [r0, #0]		@ r0 = dest
	add	r1, #2
	add	r0, #2
	cmp	r1, r2			@ r2 = src_top
	bls	boot3_copyText		@ Continue if src less than src_top
	bx	lr
		

创建一个子程序boot3_copyText。这个子程序用于将程序指令从闪存复制到SRAM。

这个子程序有三个参数:

  • r0 - 目标初始地址。
  • r1 - 源初始地址。
  • r2 - 源结束(但不包括)地址。
  • r3 - 将被修改。

这个过程为:


for (
	uint16_t* src = program_in_flash_begin, * dest = program_in_sram_begin;
	src < program_in_flash_end;
	src++
) {
	*(dest++) = *(src++);
}
			

因为Thumb为16位指令集,因此内容(程序、向量表、数据)都是2字节对齐的。另外,汇编器和编译器将给没有2字节对齐的内容添0对齐。因此,我们使用半词的读写指令ldrh / strh

这个子程序只在启动引导程序中使用,所以不需要全局化。

子程序和数据可以放在3级启动引导程序主程序前面,因为2级启动引导程序通过向量表得到进入点,且进入点可以在任何地址。

boot3_clearInterprocessorMailboxRx


.global boot3_clearInterprocessorMailboxRx
boot3_clearInterprocessorMailboxRx:
	push	{r0, r1}
	ldr	r1, =0xd0000050		@ SIO_FIFO_ST
1:	ldr	r0, [r1, #0x00]		@ SIO_FIFO_ST
	lsr	r0, #1			@ VLD
	bcc	2f
	ldr	r0, [r1, #0x08]		@ SIO_FIFO_RD
	b	1b
2:	mov	r0, #0b1100
	str	r0, [r1, #0x00]		@ SIO_FIFO_ST
	pop	{r0, r1}
	bx	lr
		

新建了一个子程序用于清空跨核心信箱,即boot3_clearInterprocessorMailboxRx。我们在上个例子中讨论过了。

这个子程序会在主程序(位于其它文件中)中调用,将其设置为全局,以便链接。

boot3


.global boot3
boot3:
	@ Start XOSC
	ldr	r7, =0x40024000			@ XOSC_BASE
	ldr	r0, =0x00FABAA0			@ Enable 1-15MHz
	str	r0, [r7, #0x00]			@ XOSC_CTRL
1:	ldr	r0, [r7, #0x04]			@ XOSC_STATUS
	lsr	r0, #32				@ Stable
	bcc	1b

	@ Start PLL
	ldr	r7, =0x4000f000			@ RESETS_RESET  + 0x3000
	ldr	r0, =(1<<12)			@ PLL_SYS
	str	r0, [r7, #0x00]

	ldr	r7, =0x40028000			@ PLL_SYS_BASE
	ldr	r6, =0x40028000 + 0x3000	@ PLL_SYS_CLEAR
	mov	r0, #(1<<0)			@ RefDiv = 1
	str	r0, [r7, #0x00]			@ PLL_SYS_CS
	mov	r0, #63				@ 12MHz * 63 = 756MHz
	str	r0, [r7, #0x08]			@ PLL_SYS_FBDIV_INT
	mov	r0, #((1<<5) | (1<<0))		@ VO and main powerdown
	str	r0, [r6, #0x04]			@ PLL_SYS_CLEAR_PWR
1:	ldr	r0, [r7, #0x00]			@ PLL_SYS_CS
	lsr	r0, r0, #32			@ Lock
	bcc	1b
	ldr	r0, =((6 << 16) | (1 << 12))	@ 756MHz / (6*1) = 126MHz
	str	r0, [r7, #0x0C]			@ PLL_SYS_PRIM
	mov	r0, #(1<<3)			@ PostDiv powerdown
	str	r0, [r6, #0x04]			@ PLL_SYS_CLEAR_PWR

	@ Switch to PLL
	ldr	r7, =0x40008000			@ CLOCKS_BASE
	mov	r0, #((0<<5) | (0<<0))		@ Set Aux src to CLKSRC_PLL_SYS (need wait) and switch Src to CLK_REF (on-the-fly), for safe
	str	r0, [r7, #0x3C]			@ CLOCKS_CLK_SYS_CTRL
	nop
	nop
	mov	r0, #((0<<5) | (1<<0))		@ Keep Aux src but switch Src to CLKSRC_CLK_SYS_AUX (on-the-fly)
	str	r0, [r7, #0x3C]

	@ Copy Code for core 0
	ldr	r0, =_core0_dest
	ldr	r1, =_core0_start
	ldr	r2, =_core0_end
	bl	boot3_copyText

	@ Copy code for core 1
	ldr	r0, =_core1_dest
	ldr	r1, =_core1_start
	ldr	r2, =_core1_end
	bl	boot3_copyText

	@ Start core 1 main program
	ldr	r7, =c1_vector
	ldr	r6, =0xd0000050		@ SIO_FIFO_ST
	mov	r0, #1
	str	r0, [r6, #0x04]		@ SIO_FIFO_WR = 1
	str	r7, [r6, #0x04]		@ SIO_FIFO_WR = c1_vector
	ldr	r0, [r7, #0x00]
	str	r0, [r6, #0x04]		@ SIO_FIFO_WR = c1_vector[0] = SP
	ldr	r0, [r7, #0x04]
	str	r0, [r6, #0x04]		@ SIO_FIFO_WR = c1_vector[1] = c0_reset
	@ Delay core 1 start until core 0 ready

	@ Enter core 0 main program
	ldr	r7, =c0_vector
	ldr	r0, [r7, #0x00]		@ c1_vector[0] = SP
	mov	sp, r0
	ldr	r0, [r7, #0x04]		@ c1_vector[1] = c1_reset
	sev				@ Core 1 start
	bx	r0
		

这里是问哦们在3级启动引导程序中要做的项目。我们在之前的文章中都详细讨论过了,所以,这里我只会概括以下:

  1. 启动XOSC。
  2. 启动PLL,PLL需要使用XOSC作为参考时钟。
  3. 将系统时钟源切换到PLL,允许系统以最高频率(133MHz)运行。
  4. 将程序(包括指令、数据、向量表)从闪存中复制到SRAM中。具体地址将在链接时解析。
  5. 启动核心1,跳转核心0。

核心0程序

接下来,我们将创建核心0运行的汇编代码程序。这次,核心0将翻转输出。将该程序保存为main.s

核心0独占数据


.section .c0_data, "aw"

c0_static0:	.space	3
c0_static1:	.byte	0x15
		

创建一个新段落.c0_data作为核心0的数据:

  • c0_static0 - 保留3个字节。初始值未定(,汇编器会填充0)。
  • c0_static1 - 1字节长,初始值0x15。

我将不会使用这些变量。这里只是为了验证核心0独占数据将被与保存在核心0程序相同的SRAM区块。

核心0向量表


.section .c0_vector, "aw"
.global c0_vector
c0_vector:
	.word	0x20041000
	.word	c0_reset + 1
		

创建一个新段落.c0_vector作为核心0的向量表:

  1. 第一个向量为初始SP。设置为SRAM区块4的顶端。
  2. 第一个向量为核心0的进入点。设置为c0_reset。另外,因为Thumb要求指令地址LSB为1,于是我们+1。

虽然向量表应该包含48个32位向量,这个例子中我们不使用中断,所以省略它们。

核心0程序


.section .c0_text, "awx"

.global c0_reset
c0_reset:
	ldr	r0, =(boot3_clearInterprocessorMailboxRx+1)
	blx	r0

	ldr	r1, =0xd000001c		@ GPIO XOR
	ldr	r0, =(1<<25)		@ GPIO25

	ldr	r2, =0x10000000		@ Flash pointer
	ldr	r3, =0x00000004		@ Flash step (must be 4x for word read)
	ldr	r4, =0x100000FF		@ Flash mask

1:	str	r0, [r1, #0]
	ldr	r5, [r2, #0]
	add	r2, r2, r3
	and	r2, r2, r4
	b	1b
		

创建一个新段落.c0_text作为核心0的程序:

这里只有一个程序为c0_reset。我们将:

  1. 调用boot3中创建的boot3_clearInterprocessorMailboxRx来清空邮箱。
  2. 给寄存器赋值地址和要写入的值。
  3. 在死循环中翻转GPIO并读闪存(缓存未命中测试)。

注意在这个例子中,当调用子程序boot3_clearInterprocessorMailboxRx时,我们不能使用bl boot3_clearInterprocessorMailboxRx指令,因为bl label指令只支持PC偏移量+/- 16MiB。但是,这个子程序在闪存空间0x10000000中,而我们的程序运行在SRAM空间0x20000000中,中间的距离有0x10000000,也就是256MiB,明显大于允许的偏移量。因此,我们需要将子程序的地址装入一个寄存器(方程指针),加1(因为Thumb),再使用blx rx指令来调用该子程序。

我们将在本文之后章节具体讨论缓存未命中测试。

核心1程序

接下来,我们将创建核心1运行的C语言程序。这次,核心1将配置GPIO。将该程序保存为main.c

核心1程序


#include <stdint.h>

__attribute__((long_call)) extern void boot3_clearInterprocessorMailboxRx();

void c1_reset() __attribute__((section (".c1_text"))) __attribute__((naked));
void c1_reset() {
	(boot3_clearInterprocessorMailboxRx + 1)();
	
	*(uint32_t volatile * const)(0x4000f000) = (1<<5);	// RESETS_RESET + 0x3000 <-- IO_BANK0
	*(uint32_t volatile * const)(0x400140cc) = (5);		// IO_BANK0_GPIO25_CTRL <-- SIO
	*(uint32_t volatile * const)(0xd0000020) = (1<<25);	// SIO_GPIO_OE <-- GPIO25

	for(;;);
}
		

声明外部方程boot3_clearInterprocessorMailboxRx。另外,为其添加long_call属性。这允许编译器使用blx rx来调用距离当前地址超过16 MiB的子程序。但是,该属性并没有告诉编译器目标方程使用Thumb还是ARM。我们需要在调用时手动给自个符号加1。

创建核心1的主程序c1_reset,使用如下属性:

  1. section (".c1_text") - 将该方程放在.c1_text段落,不使用默认的程序(指令文本)的.text段落。
  2. naked - 这是主程序且永远不会返回。因此,没有必要保存母程序的栈。默认情况下,链接寄存器lr和被使用的通用寄存器会被入栈以保留其值,因为母程序不期望这些寄存器被修改。将主程序定义为naked可以节约一些栈空间。

这个方程中我们将:

  1. 调用boot3中定义的boot3_clearInterprocessorMailboxRx来清空邮箱。
  2. 配置GPIO输出。

核心1向量表


uint32_t c1_vector[48] __attribute__ ((section(".c1_vector"))) = {
	0x20042000,
	(uint32_t)c1_reset
};
		

创建向量表,并将其放在.c1_vector中。该向量表包括:

  1. 第一个向量为初始SP。设置为SRAM区块5顶端。
  2. 第一个向量为核心1的进入点。设置为c1_reset。在C语言中,编译器自动设置好了地址的LSB。

这个例子中我们不使用中断,所以不定义这些向量。

核心1独占数据


__attribute__((section (".c1_data"))) volatile uint32_t c1_static0 = 5;
		

Create a new section .c1_data for core 1's data:

Define a variable c1_static0, that:

  • This variable is dedicated to core 1. So, let's put it in section .c0_data.
  • Its initial value is 5.
  • Make it volatile, this prevents the compiler from optimizing it into a register-save variable.

I am not gonna use these variables in core 1. I just want to show that data dedicated to core 1 is stored in the same SRAM bank as the program for core 1.

链接与编译

现在,创建一个链接脚本main.ld


MEMORY {
	FLASH(rwx) : ORIGIN = 0x10000000, LENGTH = 2048k
	SRAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k
	SRAM_4(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
	SRAM_5(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
	SRAM_0(rwx) : ORIGIN = 0x21000000, LENGTH = 64k
	SRAM_1(rwx) : ORIGIN = 0x21010000, LENGTH = 64k
	SRAM_2(rwx) : ORIGIN = 0x21020000, LENGTH = 64k
	SRAM_3(rwx) : ORIGIN = 0x21030000, LENGTH = 64k
}

ENTRY(_boot_start)

SECTIONS {
	.boot : {
		*(.boot2)
		*(.boot3)
	} > FLASH
	_boot_start = ORIGIN(FLASH);
	_boot_end = _boot_start + SIZEOF(.boot);

	.core0 : {
		. = ALIGN (256)
		*(.c0_vector)
		*(.c0_data)
		*(.c0_text)
	} > SRAM_4 AT > FLASH
	_core0_dest = ORIGIN(SRAM_4);
	_core0_start = _boot_end;
	_core0_end = _core0_start + SIZEOF(.core0);

	.core1 : {
		. = ALIGN (256)
		*(.c1_vector)
		*(.c1_data)
		*(.c1_text)
	} > SRAM_5 AT > FLASH
	_core1_dest = ORIGIN(SRAM_5);
	_core1_start = _core0_end;
	_core1_end = _core1_start + SIZEOF(.core1);

	.unspecified : {
		*(.text)
		*(.data)
		*(.bss)
	}
	ASSERT(!(SIZEOF(.unspecified)), "Unspecified text, data, and/or bss section")
}
		

这个例子中使用的链接脚本要复杂一些。

要使用和保存地址不同的地址执行程序,我们将需要提供段落的虚拟地址(VMA)加载地址(LMA),使用如下语法:


.section_name : {
	*(.subsection_name)
	*(.subsection_name)
	*(.subsection_name)
	*(symbol_name)
	*(symbol_name)
} > virtual_memory_addr AT > load_memory_addr
			

也就是说:

  • 使用SRAM区块4和5作为VMA。在解析符号(变量与程序)时使用这个地址。
  • 使用闪存作为LMA。保存程序与数据时使用该地址。

此外,我们还需要定义如下符号来表示程序和数据的地址。这些符号在我们在三级启动引导程序中将程序、向量表、数据从闪存复制到SRAM时使用。

  • _boot_start - 闪存起始地址。
  • _boot_end - .boot段落在闪存中的结束地址。
  • _core0_start - 和_boot_end相同。.core0段落(程序、向量表、数据)紧接在.boot段落之后。
  • _core0_end - .core0段落在闪存中的结束地址。
  • _core1_start - 和_core0_end相同。.core1段落(程序、向量表、数据)紧接在.core0段落之后。
  • _core1_end - .core1段落在闪存中的结束地址。
  • _core0_dest - .core0的虚拟地址,我们的例子中使用SRAM区块4。
  • _core1_dest - .core1的虚拟地址,我们的例子中使用SRAM区块5。

在我们的设计中,程序、向量表、数据必须被放在.boot.core0.core1段落中。我们创建了一个特殊的.unspecified段落用来包含没有被设置段落的程序、向量表、数据。如果.unspecified有任何内容,链接就会失败,提醒我们忘记了设置段落。


首先提供各个内存区块的地址。

将SDK二级启动引导程序.boot2与我们的三级启动引导程序.boot3(保留顺序)放在闪存中。

闪存启动不需要进入点。

将核心0的向量表.c0_vector、数据.c0_data、程序.c0_text放闪存中,但是使用SRAM区块4作为虚拟地址。向量表在最开头。

将核心1的向量表.c1_vector、数据.c1_data、程序.c1_text放闪存中,但是使用SRAM区块5作为虚拟地址。向量表在最开头。

使用. = ALIGN (256)是冗余的,因为各个内存区块的起始地址已经4k对其了。另外,这里的对齐只对VMA有效。

接下来,编译这个项目:


arm-none-eabi-as --warn --fatal-warnings -g boot3.s -o boot3.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs boot3.o > boot3.s.list

arm-none-eabi-as --warn --fatal-warnings -g main.s -o main.s.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs main.s.o > main.s.list

arm-none-eabi-gcc -mcpu=cortex-m0plus -c -O3 main.c -o main.c.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs main.c.o > main.c.list

arm-none-eabi-ld -nostdlib -nostartfiles -T main.ld boot3.o main.s.o main.c.o boot2.o -o main.elf
arm-none-eabi-objdump --disassembler-options=force-thumb -dxs main.elf > main.list
pico-elf2uf2 main.elf main.uf2
	

验证输出 - 虚拟地址

现在,我们可以通过反汇编的代码来验证我们的程序,见main.list。注意文件中地址都是虚拟地址:

boot


Disassembly of section .boot:

10000000 <_boot_start>:
10000000:	4b32b500 	.word	0x4b32b500
10000004:	60582021 	.word	0x60582021
10000008:	21026898 	.word	0x21026898
1000000c:	60984388 	.word	0x60984388
10000010:	611860d8 	.word	0x611860d8
10000014:	4b2e6158 	.word	0x4b2e6158
10000018:	60992100 	.word	0x60992100
1000001c:	61592102 	.word	0x61592102
10000020:	22f02101 	.word	0x22f02101
10000024:	492b5099 	.word	0x492b5099
10000028:	21016019 	.word	0x21016019
1000002c:	20356099 	.word	0x20356099
10000030:	f844f000 	.word	0xf844f000
10000034:	42902202 	.word	0x42902202
10000038:	2106d014 	.word	0x2106d014
1000003c:	f0006619 	.word	0xf0006619
10000040:	6e19f834 	.word	0x6e19f834
10000044:	66192101 	.word	0x66192101
10000048:	66182000 	.word	0x66182000
1000004c:	f000661a 	.word	0xf000661a
10000050:	6e19f82c 	.word	0x6e19f82c
10000054:	6e196e19 	.word	0x6e196e19
10000058:	f0002005 	.word	0xf0002005
1000005c:	2101f82f 	.word	0x2101f82f
10000060:	d1f94208 	.word	0xd1f94208
10000064:	60992100 	.word	0x60992100
10000068:	6019491b 	.word	0x6019491b
1000006c:	60592100 	.word	0x60592100
10000070:	481b491a 	.word	0x481b491a
10000074:	21016001 	.word	0x21016001
10000078:	21eb6099 	.word	0x21eb6099
1000007c:	21a06619 	.word	0x21a06619
10000080:	f0006619 	.word	0xf0006619
10000084:	2100f812 	.word	0x2100f812
10000088:	49166099 	.word	0x49166099
1000008c:	60014814 	.word	0x60014814
10000090:	60992101 	.word	0x60992101
10000094:	2800bc01 	.word	0x2800bc01
10000098:	4700d000 	.word	0x4700d000
1000009c:	49134812 	.word	0x49134812
100000a0:	c8036008 	.word	0xc8036008
100000a4:	8808f380 	.word	0x8808f380
100000a8:	b5034708 	.word	0xb5034708
100000ac:	20046a99 	.word	0x20046a99
100000b0:	d0fb4201 	.word	0xd0fb4201
100000b4:	42012001 	.word	0x42012001
100000b8:	bd03d1f8 	.word	0xbd03d1f8
100000bc:	6618b502 	.word	0x6618b502
100000c0:	f7ff6618 	.word	0xf7ff6618
100000c4:	6e18fff2 	.word	0x6e18fff2
100000c8:	bd026e18 	.word	0xbd026e18
100000cc:	40020000 	.word	0x40020000
100000d0:	18000000 	.word	0x18000000
100000d4:	00070000 	.word	0x00070000
100000d8:	005f0300 	.word	0x005f0300
100000dc:	00002221 	.word	0x00002221
100000e0:	180000f4 	.word	0x180000f4
100000e4:	a0002022 	.word	0xa0002022
100000e8:	10000100 	.word	0x10000100
100000ec:	e000ed08 	.word	0xe000ed08
	...
100000fc:	7a4eb274 	.word	0x7a4eb274
10000100:	20041000 	.word	0x20041000
10000104:	1000012d 	.word	0x1000012d

10000108 <boot3_copyText>:
10000108:	880b      	ldrh	r3, [r1, #0]
1000010a:	8003      	strh	r3, [r0, #0]
1000010c:	3102      	adds	r1, #2
1000010e:	3002      	adds	r0, #2
10000110:	4291      	cmp	r1, r2
10000112:	d9f9      	bls.n	10000108 <boot3_copyText>
10000114:	4770      	bx	lr

10000116 <boot3_clearInterprocessorMailboxRx>:
10000116:	b403      	push	{r0, r1}
10000118:	4920      	ldr	r1, [pc, #128]	; (1000019c <boot3+0x70>)
1000011a:	6808      	ldr	r0, [r1, #0]
1000011c:	0840      	lsrs	r0, r0, #1
1000011e:	d301      	bcc.n	10000124 <boot3_clearInterprocessorMailboxRx+0xe>
10000120:	6888      	ldr	r0, [r1, #8]
10000122:	e7fa      	b.n	1000011a <boot3_clearInterprocessorMailboxRx+0x4>
10000124:	200c      	movs	r0, #12
10000126:	6008      	str	r0, [r1, #0]
10000128:	bc03      	pop	{r0, r1}
1000012a:	4770      	bx	lr

1000012c <boot3>:
1000012c:	4f1c      	ldr	r7, [pc, #112]	; (100001a0 <boot3+0x74>)
1000012e:	481d      	ldr	r0, [pc, #116]	; (100001a4 <boot3+0x78>)
10000130:	6038      	str	r0, [r7, #0]
10000132:	6878      	ldr	r0, [r7, #4]
10000134:	0800      	lsrs	r0, r0, #32
10000136:	d3fc      	bcc.n	10000132 <boot3+0x6>
10000138:	4f1b      	ldr	r7, [pc, #108]	; (100001a8 <boot3+0x7c>)
1000013a:	481c      	ldr	r0, [pc, #112]	; (100001ac <boot3+0x80>)
1000013c:	6038      	str	r0, [r7, #0]
1000013e:	4f1c      	ldr	r7, [pc, #112]	; (100001b0 <boot3+0x84>)
10000140:	4e1c      	ldr	r6, [pc, #112]	; (100001b4 <boot3+0x88>)
10000142:	2001      	movs	r0, #1
10000144:	6038      	str	r0, [r7, #0]
10000146:	203f      	movs	r0, #63	; 0x3f
10000148:	60b8      	str	r0, [r7, #8]
1000014a:	2021      	movs	r0, #33	; 0x21
1000014c:	6070      	str	r0, [r6, #4]
1000014e:	6838      	ldr	r0, [r7, #0]
10000150:	0800      	lsrs	r0, r0, #32
10000152:	d3fc      	bcc.n	1000014e <boot3+0x22>
10000154:	4818      	ldr	r0, [pc, #96]	; (100001b8 <boot3+0x8c>)
10000156:	60f8      	str	r0, [r7, #12]
10000158:	2008      	movs	r0, #8
1000015a:	6070      	str	r0, [r6, #4]
1000015c:	4f17      	ldr	r7, [pc, #92]	; (100001bc <boot3+0x90>)
1000015e:	2000      	movs	r0, #0
10000160:	63f8      	str	r0, [r7, #60]	; 0x3c
10000162:	46c0      	nop			; (mov r8, r8)
10000164:	46c0      	nop			; (mov r8, r8)
10000166:	2001      	movs	r0, #1
10000168:	63f8      	str	r0, [r7, #60]	; 0x3c
1000016a:	4815      	ldr	r0, [pc, #84]	; (100001c0 <boot3+0x94>)
1000016c:	4915      	ldr	r1, [pc, #84]	; (100001c4 <boot3+0x98>)
1000016e:	4a16      	ldr	r2, [pc, #88]	; (100001c8 <boot3+0x9c>)
10000170:	f7ff ffca 	bl	10000108 <boot3_copyText>
10000174:	4815      	ldr	r0, [pc, #84]	; (100001cc <boot3+0xa0>)
10000176:	4916      	ldr	r1, [pc, #88]	; (100001d0 <boot3+0xa4>)
10000178:	4a16      	ldr	r2, [pc, #88]	; (100001d4 <boot3+0xa8>)
1000017a:	f7ff ffc5 	bl	10000108 <boot3_copyText>
1000017e:	4f16      	ldr	r7, [pc, #88]	; (100001d8 <boot3+0xac>)
10000180:	4e06      	ldr	r6, [pc, #24]	; (1000019c <boot3+0x70>)
10000182:	2001      	movs	r0, #1
10000184:	6070      	str	r0, [r6, #4]
10000186:	6077      	str	r7, [r6, #4]
10000188:	6838      	ldr	r0, [r7, #0]
1000018a:	6070      	str	r0, [r6, #4]
1000018c:	6878      	ldr	r0, [r7, #4]
1000018e:	6070      	str	r0, [r6, #4]
10000190:	4f12      	ldr	r7, [pc, #72]	; (100001dc <boot3+0xb0>)
10000192:	6838      	ldr	r0, [r7, #0]
10000194:	4685      	mov	sp, r0
10000196:	6878      	ldr	r0, [r7, #4]
10000198:	bf40      	sev
1000019a:	4700      	bx	r0

1000019c:	d0000050 	.word	0xd0000050
100001a0:	40024000 	.word	0x40024000
100001a4:	00fabaa0 	.word	0x00fabaa0
100001a8:	4000f000 	.word	0x4000f000
100001ac:	00001000 	.word	0x00001000
100001b0:	40028000 	.word	0x40028000
100001b4:	4002b000 	.word	0x4002b000
100001b8:	00061000 	.word	0x00061000
100001bc:	40008000 	.word	0x40008000
100001c0:	20040000 	.word	0x20040000
100001c4:	100001e0 	.word	0x100001e0
100001c8:	1000021c 	.word	0x1000021c
100001cc:	20041000 	.word	0x20041000
100001d0:	1000021c 	.word	0x1000021c
100001d4:	1000030c 	.word	0x1000030c
100001d8:	20041000 	.word	0x20041000
100001dc:	20040000 	.word	0x20040000
		

SDK二级启动引导程序_boot_start位于闪存的开头,地址0x10000000。该程序将在启动执行。

我们的三级启动引导程序的向量表紧接在SDK二级启动引导程序之后,地址0x10000100。接下里是boot3_copyTextboot3_clearInterprocessorMailboxRxboot3。全都是在闪存中。

最后是汇编代码中使用的常量,包括:

  • .word 0x20040000 @ 100001c0 - 复制核心0内容的目标地址。
  • .word 0x100001e0 @ 100001c4 - 复制核心0内容的源地址开头。注意boot3的最后一个项目的地址为0x100001dc,又因为core0紧接在boot3之后,其地址为0x100001e0。
  • .word 0x1000021c @ 100001c8 - 复制核心0内容的源地址顶端。
  • ...

core0


Disassembly of section .core0:

20040000 <c0_vector>:
20040000:	20041000 	.word	0x20041000
20040004:	2004000d 	.word	0x2004000d

20040008 <c0_static0>:
20040008:	0000      	.short	0x0000
	...

2004000b <c0_static1>:
2004000b:	15          	.byte	0x15

2004000c <c0_reset>:
2004000c:	4805      	ldr	r0, [pc, #20]	; (20040024 <c0_reset+0x18>)
2004000e:	4780      	blx	r0
20040010:	4905      	ldr	r1, [pc, #20]	; (20040028 <c0_reset+0x1c>)
20040012:	4806      	ldr	r0, [pc, #24]	; (2004002c <c0_reset+0x20>)
20040014:	4a06      	ldr	r2, [pc, #24]	; (20040030 <c0_reset+0x24>)
20040016:	4b07      	ldr	r3, [pc, #28]	; (20040034 <c0_reset+0x28>)
20040018:	4c07      	ldr	r4, [pc, #28]	; (20040038 <c0_reset+0x2c>)
2004001a:	6008      	str	r0, [r1, #0]
2004001c:	6815      	ldr	r5, [r2, #0]
2004001e:	18d2      	adds	r2, r2, r3
20040020:	4022      	ands	r2, r4
20040022:	e7fa      	b.n	2004001a <c0_reset+0xe>

20040024:	10000117 	.word	0x10000117
20040028:	d000001c 	.word	0xd000001c
2004002c:	02000000 	.word	0x02000000
20040030:	10000000 	.word	0x10000000
20040034:	00000004 	.word	0x00000004
20040038:	100000ff 	.word	0x100000ff
		

核心0的向量表c0_vector被放在SRAM区块4的开头,地址0x20040000。该地址已256字节对齐。

核心0独占数据c0_static0c0_static1紧接在c0_vector之后,地址0x20040008,仍然是SRAM区块4。

c0_static0c0_static1是byte数据,不用对其。

核心0的程序c0_reset紧接在c0_static1之后,地址0x2004000c,仍然是SRAM区块4。

接下来是汇编代码中使用的常量。

core1


Disassembly of section .core1:

20041000 <c1_vector>:
20041000:	20042000 200410c5 00000000 00000000     . . ... ........
	...

200410c0 <c1_static0>:
200410c0:	00000005                                ....

200410c4 <c1_reset>:
200410c4:	4b06      	ldr	r3, [pc, #24]	; (200410e0 <c1_reset+0x1c>)
200410c6:	4798      	blx	r3
200410c8:	2220      	movs	r2, #32
200410ca:	4b06      	ldr	r3, [pc, #24]	; (200410e4 <c1_reset+0x20>)
200410cc:	601a      	str	r2, [r3, #0]
200410ce:	4b06      	ldr	r3, [pc, #24]	; (200410e8 <c1_reset+0x24>)
200410d0:	3a1b      	subs	r2, #27
200410d2:	601a      	str	r2, [r3, #0]
200410d4:	2280      	movs	r2, #128	; 0x80
200410d6:	4b05      	ldr	r3, [pc, #20]	; (200410ec <c1_reset+0x28>)
200410d8:	0492      	lsls	r2, r2, #18
200410da:	601a      	str	r2, [r3, #0]
200410dc:	e7fe      	b.n	200410dc <c1_reset+0x18>
200410de:	46c0      	nop			; (mov r8, r8)

200410e0:	10000117 	.word	0x10000117
200410e4:	4000f000 	.word	0x4000f000
200410e8:	400140cc 	.word	0x400140cc
200410ec:	d0000020 	.word	0xd0000020
		

核心1的向量表c1_vector被放在SRAM区块5的开头,地址0x20041000。该地址已256字节对齐。

核心1独占数据c1_static0紧接在c1_vector之后,地址0x200410c0,仍然是SRAM区块5。

核心1的程序c1_reset紧接在c1_static1之后,地址0x200410c4,仍然是SRAM区块5。

接下来是汇编代码中使用的常量。

验证输出 - 加载地址

我们可以通过反汇编文件的头来验证LMA:


Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .boot         000001e0  10000000  10000000  00000094  2**2
                  CONTENTS, ALLOC, LOAD, CODE
  1 .core0        0000003c  20040000  100001e0  00000274  2**2
                  CONTENTS, ALLOC, LOAD, CODE
  2 .core1        000000f0  20041000  1000021c  000002b0  2**2
                  CONTENTS, ALLOC, LOAD, CODE
	

其中VMA为虚拟地址,LMA为加载地址。

可以看到,.core0.core1的VMA都在SRAM空间,但LMA都在闪存空间。

我们也可以通过查看uf2文件中各个区块的地址信息(下图高亮部分)来验证LMA:

uf2文件中加载地址
uf2文件中加载地址

测量输出

下载uf2文件。然后,使用示波器来测量输出波形。

使用PLL且缓存始终命中时的输出波形
使用PLL且缓存始终命中时的输出波形

示波器显示9MHz的方波。一切正常!我们的程序成功运行。

缓存未命中的效果

为了验证缓存未命中会造成核心停顿,我们可以调整核心0程序中读取闪存相关的参数。在一个死循环中,我们将读取一定数量的闪存空间地址。如果读取命中,我们将看到输出波形为最高频率;反之,频率将会降低。


	ldr	r2, =0x10000000		@ Flash pointer
	ldr	r3, =0x00000004		@ Flash step (must be 4x for word read)
	ldr	r4, =0x100000FF		@ Flash mask

1:	str	r0, [r1, #0]
	ldr	r5, [r2, #0]
	add	r2, r2, r3
	and	r2, r2, r4
	b	1b
		

我们有如下参数:

  1. r2 - 使用的闪存缓存策略。0x10000000为可缓存可分配,0x13000000为不缓存不分配(永不命中)。
  2. r3 - 读取闪存步进。
  3. r4 - 读取闪存顶端(使用掩码)。达到后归零。

全命中 - 缓存空间足够大

小区间闪存读取的输出波形
小区间闪存读取的输出波形

我们使用以下参数:

  1. r2 - 0x10000000
  2. r3 - 0x00000004
  3. r4 - 0x100000FF

也就是说,我们将读取以下地址(共64个):

  1. 0x10000000
  2. 0x10000004
  3. 0x10000008
  4. 0x1000000C
  5. 0x10000010
  6. 0x10000014
  7. ...
  8. 0x100000F8
  9. 0x100000FC
  10. 跳回0x10000000

我们在闪存空间读取了多个数据,区间大小为256字节。该区间小于XIP的缓存大小,因此,所有的数据都能被缓存。

有时命中 - 频繁地重载缓存

大区间小步进闪存读取的输出波形
大区间小步进闪存读取的输出波形

我们使用以下参数:

  1. r2 - 0x10000000
  2. r3 - 0x00000400
  3. r4 - 0x1000FFFF

也就是说,我们将读取以下地址(共64个):

  1. 0x10000000
  2. 0x10000400
  3. 0x10000800
  4. 0x10000C00
  5. 0x10001000
  6. 0x10001400
  7. ...
  8. 0x1000F800
  9. 0x1000FC00
  10. 跳回0x10000000

我们在闪存空间读取了多个数据,区间大小为64 kiB,步进为1 kiB。该步进小于XIP的缓存大小但区间大于XIP的缓存大小,因此,偶尔命中。

全不命中 - 步进大于缓存大小

大区间大步进闪存读取的输出波形
大区间大步进闪存读取的输出波形

我们使用以下参数:

  1. r2 - 0x10000000
  2. r3 - 0x00004000
  3. r4 - 0x100FFFFF

也就是说,我们将读取以下地址(共64个):

  1. 0x10000000
  2. 0x10004000
  3. 0x10008000
  4. 0x1000C000
  5. 0x10010000
  6. 0x10014000
  7. ...
  8. 0x100F8000
  9. 0x100FC000
  10. 跳回0x10000000

们在闪存空间读取了多个数据,步进为16 kiB。该步进大于XIP的缓存大小,因此,不能命中。

全命中 - 双路缓存

Output waveform with 2-location read window
Output waveform with 2-location read window

我们使用以下参数:

  1. r2 - 0x10000000
  2. r3 - 0x00080000
  3. r4 - 0x100FFFFF

也就是说,我们将读取以下地址(共2个):

  1. 0x10000000
  2. 0x10080000
  3. 跳回0x10000000

虽然步进很大,但是只读了两个地址。因为XIP使用双路缓存,所以能全命中。