对比RP2040可执行内存与从闪存中加载程序到SRAM
对比RP2040可执行内存(闪存XIP空间、条带SRAM、区块SRAM)。探讨内存规划、数据冒险与结构冒险。将闪存中保存的程序载入SRAM后再执行。
树莓派Pico, RP2040, ARM, Cortex M0+, 汇编, 裸金属, 内存, 内存结构冒险, 链接脚本, 虚拟地址, 存储地址, 单片机
--by Captdam @ Mar 12, 2026Index
这篇文章的针对读者为32位RP2040与ARM Cortex-M0+新手,但是熟悉使用汇编与C语言开发8位单片裸金属应用的机开发者。
因为我们将开发裸金属应用,我们将会直接读写单片的控制寄存器,并不使用任何库。
我们将大量依靠文档。文档中包括了我们所需要知道的所有单片机控制寄存器的信息。
因为RP2040文档更新的缘故,而且他们还决定把老文档的链接直接重定向到新文档,我决定在本地保存当前版本的副本(2025-02-20)。你可以通过原始链接(取样于2026-02-10)获取该文档。
这篇文章基于我的上一篇文章:裸金属切换RP2040时钟源:ROSC,XOSC与PLL,你应该先看看那一篇文章。
内存分区的不同
(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带有16kiB启动ROM,其地址位于内存空间的开头。上电后将执行启动ROM作为一级引导程序。
启动ROM在生产时被烧录,且不可求改。因此,我们没办法将我们的的程序(用户程序)写入这个空间。就算写了也不会起作用。但是,用户程序可以调用启动ROM中的功能程序。
(映射)闪存
外部闪存没有直接和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提供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还有两个缓冲在未使用时可以用来存放程序或数据:
- XIP缓存 - 16kiB,位于0x15000000
- USB DPRAM - 4kiB,位于0x50100000(是的,可执行)
停顿
缓存未命中
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使用的CPU并没有缓存。因此,在执行位于特定地址的一条指令时,必须在执行时从该地址将这条指令读取出来。同样,在读写位于特定地址的一项数据(普通数据或是控制寄存器的值)时,必须在执行时在该地址读写这项数据。
如图,4:10 AHB-Lite交换器用于将CPU和DMA与内存空间相连。在同一时间点内,每个上行线路(如CPU0到AHB-Lite交换器)和每个下行线路(如SRAM0到AHB-Lite交换器)最多只能有一项读写操作。
所以,内存读写操作如ldr和str需要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中执行。
- 要避免结构冒险:我们将SRAM区块设定为核心独占。
这个例子中我将会使用以下模式:
- SRAM区块0和1:CPU0独占数据储存。(128kiB)
- SRAM区块2和3:CPU1独占数据储存。(128kiB)
- SRAM区块4:CPU0的向量表、只读数据(常量)、栈和程序。(4kiB)
- SRAM区块5:CPU1的向量表、只读数据(常量)、栈和程序。(4kiB)
- 闪存:不是马上需要的数据或程序,在之后快要用时再载入SRAM。
因为读写SRAM总需要2个周期,所以把一个核心的程序与数据放在同一个SRAM区块也没问题。
大部分情况下,程序都很小,数据却很大。所以,我决定使用小的SRAM区块来储存程序,大的SRAM区块来储存数据。另外,我也不爱用递归,所以栈也不会很大。当然,使用大的区块来装程序,或是把栈和程序放在不同的区块也完全没问题,仁者见仁智者见智。
我将不使用条纹内存。条纹内存只在程序员不给每个上线线路分配独占区块时才有用。条纹区块不提供任何保证,只会整得时序乱七八糟。
USB启动双核程序
USB启动双核程序的工程文件在这里。
三级启动引导程序
首先,我们将创建一个汇编程序叫做boot3(三级启动引导程序)。这个程序用来启动核心1执行核心1的程序,然后开始核心0的程序。将这个文件保存为boot3.s:
.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的向量表:
- 第一个向量为初始SP。设置为SRAM区块4的顶端。
- 第一个向量为核心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。我们将:
- 调用
boot3中创建的boot3_clearInterprocessorMailboxRx来清空邮箱。 - 配置GPIO允许输出。
- 结构冒险测试。这个可选的部分用来对核心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,使用如下属性:
section (".c1_text")- 将该方程放在.c1_text段落,不使用默认的程序(指令文本)的.text段落。naked- 这是主程序且永远不会返回。因此,没有必要保存母程序的栈。默认情况下,链接寄存器lr和被使用的通用寄存器会被入栈以保留其值,因为母程序不期望这些寄存器被修改。将主程序定义为naked可以节约一些栈空间。
这个方程中我们将:
- 调用
boot3中定义的boot3_clearInterprocessorMailboxRx来清空邮箱。 - 在一个死循环中翻转输出。
核心1向量表
uint32_t c1_vector[48] __attribute__ ((section(".c1_vector"))) = {
0x20042000,
(uint32_t)c1_reset
};
创建向量表,并将其放在.c1_vector中。该向量表包括:
- 第一个向量为初始SP。设置为SRAM区块5顶端。
- 第一个向量为核心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级启动引导程序boot3与boot3_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_static0与c0_static1紧接在c0_vector之后,地址0x20040008,仍然是SRAM区块4。
c0_static0和c0_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为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}。在这个循环中,我们:
- 获得
c1_static0的地址,该变量和核心1的程序指令保存在同一个SRAM区块上。 - 该地址装入r0。(1周期)
- 使用r0作为指针读8个数据,导致核心1使用的SRAM区块结构冒险。(9周期,8读取)
- 回到第2步并重复。(2周期)
综上,核心0每12个周期将造成核心1停顿8周期。
编译、链接、下载uf2文件。然后,使用示波器测量输出波形。
可以看到,频率降低了,表示核心1停顿了。
因为AHB-Lite交换器使用轮盘形式分配访问权,有时核心1使核心0停顿(得到和之前相同的波长),有时核心0使核心1停顿(更长的波长)。
Flash启动双核程序
在上一个例子中,我们成功地将一个双核程序下载到SRAM区块4和5中,并使用各个区块作为各个核心的独占资源,以避免结构冒险。
但是,只要一掉电,程序就没了。有没有办法将程序保存在闪存中,启动时再将程序从闪存载入SRAM区块呢?
在电脑上,当我们编译一个程序,程序不仅仅是一堆CPU指令,还包括有关于设置运行环境和准备数据的信息,才能让这个程序可执行。当我们执行一个程序时:
- 操作系统(OS)开机。
- OS将程序从硬盘中载入内存中。
- OS读取程序文件中的“信息”部分,设置运行环境,并将程序内容(程序与数据)放在需要的地址。
- 程序进入点载入PC,开始执行。
我们在RP2040上的操作相似:
- 片上启动引导程序在条纹SRAM中执行二级启动引导程序。接下来,二级启动引导程序将配置SSI中的XIP功能。这类似于启动OS。
- 二级启动引导程序启动我们的三级启动引导程序。该三级启动引导程序在闪存(XIP)空间中执行。
- 我们的三级启动引导程序将向量表、数据、程序指令载入SRAM区块中。另外,该三级启动引导程序还会进行其它设置,例如时钟源、内存保护、中断等……
- 我们的三级启动引导程(运行在核心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二级启动引导程序)的需求,我们必须把向量表在最开头。该向量表包括:
- 该程序使用的初始SP。我们这里设置为SRAM区块4的顶端。
- 进入点。我们设置为程序
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级启动引导程序中要做的项目。我们在之前的文章中都详细讨论过了,所以,这里我只会概括以下:
- 启动XOSC。
- 启动PLL,PLL需要使用XOSC作为参考时钟。
- 将系统时钟源切换到PLL,允许系统以最高频率(133MHz)运行。
- 将程序(包括指令、数据、向量表)从闪存中复制到SRAM中。具体地址将在链接时解析。
- 启动核心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的向量表:
- 第一个向量为初始SP。设置为SRAM区块4的顶端。
- 第一个向量为核心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。我们将:
- 调用
boot3中创建的boot3_clearInterprocessorMailboxRx来清空邮箱。 - 给寄存器赋值地址和要写入的值。
- 在死循环中翻转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,使用如下属性:
section (".c1_text")- 将该方程放在.c1_text段落,不使用默认的程序(指令文本)的.text段落。naked- 这是主程序且永远不会返回。因此,没有必要保存母程序的栈。默认情况下,链接寄存器lr和被使用的通用寄存器会被入栈以保留其值,因为母程序不期望这些寄存器被修改。将主程序定义为naked可以节约一些栈空间。
这个方程中我们将:
- 调用
boot3中定义的boot3_clearInterprocessorMailboxRx来清空邮箱。 - 配置GPIO输出。
核心1向量表
uint32_t c1_vector[48] __attribute__ ((section(".c1_vector"))) = {
0x20042000,
(uint32_t)c1_reset
};
创建向量表,并将其放在.c1_vector中。该向量表包括:
- 第一个向量为初始SP。设置为SRAM区块5顶端。
- 第一个向量为核心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_copyText、boot3_clearInterprocessorMailboxRx和boot3。全都是在闪存中。
最后是汇编代码中使用的常量,包括:
.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_static0与c0_static1紧接在c0_vector之后,地址0x20040008,仍然是SRAM区块4。
c0_static0和c0_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文件。然后,使用示波器来测量输出波形。
示波器显示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
我们有如下参数:
r2- 使用的闪存缓存策略。0x10000000为可缓存可分配,0x13000000为不缓存不分配(永不命中)。r3- 读取闪存步进。r4- 读取闪存顶端(使用掩码)。达到后归零。
全命中 - 缓存空间足够大
我们使用以下参数:
r2- 0x10000000r3- 0x00000004r4- 0x100000FF
也就是说,我们将读取以下地址(共64个):
0x100000000x100000040x100000080x1000000C0x100000100x10000014- ...
0x100000F80x100000FC- 跳回
0x10000000
我们在闪存空间读取了多个数据,区间大小为256字节。该区间小于XIP的缓存大小,因此,所有的数据都能被缓存。
有时命中 - 频繁地重载缓存
我们使用以下参数:
r2- 0x10000000r3- 0x00000400r4- 0x1000FFFF
也就是说,我们将读取以下地址(共64个):
0x100000000x100004000x100008000x10000C000x100010000x10001400- ...
0x1000F8000x1000FC00- 跳回
0x10000000
我们在闪存空间读取了多个数据,区间大小为64 kiB,步进为1 kiB。该步进小于XIP的缓存大小但区间大于XIP的缓存大小,因此,偶尔命中。
全不命中 - 步进大于缓存大小
我们使用以下参数:
r2- 0x10000000r3- 0x00004000r4- 0x100FFFFF
也就是说,我们将读取以下地址(共64个):
0x100000000x100040000x100080000x1000C0000x100100000x10014000- ...
0x100F80000x100FC000- 跳回
0x10000000
们在闪存空间读取了多个数据,步进为16 kiB。该步进大于XIP的缓存大小,因此,不能命中。
全命中 - 双路缓存
我们使用以下参数:
r2- 0x10000000r3- 0x00080000r4- 0x100FFFFF
也就是说,我们将读取以下地址(共2个):
0x100000000x10080000- 跳回
0x10000000
虽然步进很大,但是只读了两个地址。因为XIP使用双路缓存,所以能全命中。