W25Q闪存与裸金属RP2040 SDK引导程序

以8位单片机开发者的视角分析W25Q闪存通讯与反汇编RP2040 SDK二级引导程序。

--by Captdam @ Feb 14, 2026 Feb 10, 2026

Index

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

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

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

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

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

树莓派Pico开发板带有一块W25Q系列的闪存芯片用于储存用户程序。注意,W25Q系列下有多个型号。所有型号都是用相同的SPI通讯协议,但是使用不同的继续码。所谓继续码是跟在数据地址后的8位代码,其用处为在之后的交互中省略读取指令,以减少发送指令带来的额外带宽消耗。

虽然Pico文档复制于官方链接(取样于2026-02-10))指出该板载闪存为Winbond W25Q16JV,该闪存使用0b1111xxxx(x为任意电平)作为继续码。但是我通过反汇编发现SDK中二级引导程序发现使用的继续码是0b10101010。在浏览了其它一些型号的文档后,我发现W25Q80EW使用的继续码0bxx01xxxx可以和SDK中使用的继续码匹配。因此,本文章我将使用这一款闪存的文档作为参考。

W25Q版权信息
W25Q版权信息

这篇文章使用的工程文档可以在这里找到

这篇文章基于我的上一篇文章:树莓派Pico(RP2040)SRAM与闪存编程,你应该先看看那一篇文章。

引导程序

什么是引导程序?

引导程序是一个小程序,用来用来载入主程序,使主程序可被执行。

正如我的上一篇文章中我们讨论到,片上引导程序只会从闪存读取256字节的数据(包括4字节校验和),也就是说,126条Thumb指令(每条指令2字节)。显然,这完全不够存放一个正常的应用。

因此,我们将引入二级引导程序(boot2),一个保存在闪存中的256字节以内的程序。该程序的目标只有一个:为执行主程序做准备。

总结来说,片上引导程序(厂家提供,保存在片上ROM)将加载二级引导程序(我们提供,保存在闪存),其再加载主程序(我们提供,保存在闪存)。

RP2040外围设备

我觉得有必要先讨论一下引导程序使用到的外围设备。它们和8为单片机上的外围设备有不少区别。

SSI(Synchronous Serial Interface,同步串行接口)

RP2040引脚
RP2040引脚56-51支持QSPI并用作SSI端口 ©RPI2040

RP2040引脚56-51支持QSPI并用作SSI端口,和板载闪存相连。

以8位单片机开发者的视角来看,SSI就是一个升级版SPI。最重要的特点包括:

  • 不仅支持标准SPI(一个输入端,一个输出端),还支持DSPI和QSPI(2或4个双向接口)。因为QSPI使用4个双向接口,自然比同向只有1根数据线的SPI要快。
  • 自动拉高/拉低从机选择信号。不像是8位机上,我们需要手动操作。
  • 使用更深的缓冲。我们可以一次性发送多个字,然后再读取多个字。不像是8位机上只有1个字的缓冲或者根本没有缓冲,我们必须在发送下一个字前读取当前收到的字,不然就有数据被覆写的风险。
  • 读写都是32位宽。因此,只需一次写就能发送32位的字。不像是8位机上,我们要写4次。
  • 可以设置帧大小。如果帧小于32位,将丢弃高位比特。
  • 内置EEPROM逻辑。在配置好XIP模式后,内部逻辑将自动发送EEPROM指令与地址,接收EEPROM数据。

XIP(Execute in Place,片内执行)

RP2040 XIP
XIP将闪存缓存到内存空间中以便CPU直接访问 ©RPI2040

RP2040提供了一个特殊的外围设备,即XIP。

通常来说,程序需要被放在CPU执行单元可以访问的地方。

  • 对AVR和8051这类8位单片机来说,CPU的执行单元直接和程序ROM相连。
  • 对家用电脑来说,程序被保存在硬盘上。当我们需要执行一个程序,操作系统会将程序从硬盘读到内存中,然后,CPU才能执行这个程序。虽然逻辑上我们可以mmap()映射一个硬盘上的文件到内存中,实际上数据必须从硬盘中复制到内存中。
  • 对于Pico上的RP2040单片机,程序被保存在与SSI相连的板载闪存中。要执行这个程序,必须把它通过SSI从闪存复制到内存中。

我们将需要写一个引导程序来通过SSI从闪存复制程序到内存中。我们可以手动从闪存复制程序到内存中,也可以将闪存映射到RP2040的内存空间中。

RP2040提供了一个特殊的外围设备,即XIP。它将外部闪存映射到一个特殊的内存区间,即XIP缓存(地址0x10000000到0x1FFFFFFF),允许CPU直接执行里面的程序。

XIP让闪存在逻辑上成为内存的一部分。于是,CPU就可以像是执行物理内部SRAM一样执行这个XIP缓存内的程序了。XIP底层硬件会自动将外部闪存内的程序复制入XIP缓存。

SDK引导程序

要搞明白二级引导程序,我们可以先看看官方SDK二级引导程序的内容,源码可以在这里找到。但是,这份源码并不好读。到处都是依赖文件,我个人反正不喜欢这份源码。简直就是依赖地狱。

相较而言,我倾向于直接反汇编可执行文件。(如果你经常写8位单片机,你应该也喜欢直接反汇编二进制文件而不是去啃源码。)我选择从pico-example/blink这个闪光程序入手,使用arm-none-eabi-objdump --disassembler-options=force-thumb -j .boot2 -Dxs blink.elf进行反汇编。

SDK二级引导程序被用来配置SSI为XIP(片内执行)模式,假设主程序存放在紧接着二级引导程序的位置,即地址0x10000100。

Let's analysis the disassembled code line by line to understant it.

进入点


__boot2_start__:
10000000:	b500      	push	{lr}
	

ARM CPU的子程序呼叫不使用栈,在进入子程序时不会将(PC,程序计数器中的)返回地址入栈,在返回时也不会将出栈(以获得返回地址改写PC)。相反,我们使用bl(branch and link)到子程序,返回地址将会写入lr(链接寄存器)。要返回,我们bx rnrn可以是包括lr在内的任意寄存器),把rn的值写入PC,也就变相将rn内的返回地址变成下一步执行的指令的地址了。详细的细节可以参考这里

在之后,我们将会使用这个值判断谁调用了这个SDK引导程序。

因为我们将在二级引导程序中调用子程序,我们需要把lr入栈备份(non-leaf function)。否则,就不用备份(leaf function)。

电气特性

设置连接板载闪存的SSI端口的电气特性。

RP2040 PADS_QSPI_GPIO_QSPI_SCLK
Pad control register @ 0x40020004 ©RPI2040

10000002:	4b32      	ldr	r3, =0x40020000	@ PADS_QSPI_BASE
10000004:	2021      	movs	r0, #0x21	@ 2 (8mA) << DRIVE | 1 << SLEWFAST
10000006:	6058      	str	r0, [r3, #4]	@ PADS_QSPI_BASE + GPIO_QSPI_SCLK
			

对于时钟引脚,使用快速信号切换,8mA电流强度。

RP2040 PADS_QSPI_GPIO_QSPI_SD0
Pad control registers @ 0x40020008, 0C, 10, 14 ©RPI2040

10000008:	6898      	ldr	r0, [r3, #8]	@ PADS_QSPI_BASE + GPIO_QSPI_SD0
1000000a:	2102      	movs	r1, #2		@ 1 << Schmitt
1000000c:	4388      	bics	r0, r1
1000000e:	6098      	str	r0, [r3, #8]	@ PADS_QSPI_BASE + GPIO_QSPI_SD0
10000010:	60d8      	str	r0, [r3, #12]	@ PADS_QSPI_BASE + GPIO_QSPI_SD1
10000012:	6118      	str	r0, [r3, #16]	@ PADS_QSPI_BASE + GPIO_QSPI_SD2
10000014:	6158      	str	r0, [r3, #20]	@ PADS_QSPI_BASE + GPIO_QSPI_SD3
			

对于所有数据引脚,保留当前配置(或默认配置),但禁用施密特触发器。这可以提高传输速度,但降低可靠性。

配置SSI为标准SPI

On power-up, the W25Q flash is in standrad SPI mode, we have to temporarily switch the SSI to standard SPI mode for flash configuration.

设置SSI波特率

RP2040 SSI_SSIERN
SSI Enable @ 0x18000008 ©RPI2040
RP2040 SSI_BAUDR
Baud rate @ 0x18000014 ©RPI2040

10000016:	4b2e      	ldr	r3, =0x18000000	@ XIP_SSI_BASE
10000018:	2100      	movs	r1, #0
1000001a:	6099      	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
1000001c:	2102      	movs	r1, #2
1000001e:	6159      	str	r1, [r3, #20]	@ XIP_SSI_BASE + BAUD
			

禁用SSI进行配置。

设置SSI波特率,使用时钟差分值为2。

r3中保存XIP控制寄存器的基地址,0x18000000。该值在整个二级引导程序都不会被改变,且会在接下来的操作与子程序中复用。

设置SSI采样延迟

RP2040 RX_SAMPLE_DLY
RX sample delay @ 0x180000f0 ©RPI2040

10000020:	2101      	movs	r1, #1
10000022:	22f0      	movs	r2, #0xf0
10000024:	5099      	str	r1, [r3, r2]	@ XIP_SSI_BASE + RX_SAMPLE_DLY
			

设置SSI采样延迟为1个时钟周期以补偿信号传输延迟。

信号传输
信号传输 ©RPI2040

如上图所示,信号从主机传输到从机、从机响应、从从机传输到主机,都会产生延迟。

设置SSI模式

RP2040 SSI_CTRLR0
Control register 0 @ 0x18000000 ©RPI2040
RP2040 SSI_SSIERN
SSI Enable @ 0x18000008 ©RPI2040

10000026:	492b      	ldr	r1, =0x00070000	@ 0 (STD) << SPI_FRF | 7 << DFS32 | 0 (TX_AND_RX) << TMOD
10000028:	6019      	str	r1, [r3, #0]	@ XIP_SSI_BASE + CTRLR0
1000002a:	2101      	movs	r1, #1
1000002c:	6099      	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
			

设置SSI模式:

  • SPI_FRF - 0 = 标准SPI。
  • DFS_32 - 7 = 8位数据帧。
  • TMOD - 0 = 启动发送和接收。

重启SSI。

初始化闪存

在以QSPI使用W25Q闪存前,我们需要确保它已经准备好了。不然,我们就要配置它。

检查Flash模式

W25Q Read Status Register
Read Status Register 2 0x35 ©W25Q
W25Q Status Register 2
Status Register 2 ©W25Q

1000002e:	2035      	movs	r0, #53
10000030:	f000 f844 	bl	ssi_writeread
10000034:	2202      	movs	r2, #2
10000036:	4290      	cmp	r0, r2
10000038:	d014      	beq.n	set_qspi
			

发送53(0x35)到W25Q80闪存来读取状态寄存器2。使用ssi_writeread来发送一个字并接收一个字,详见子程序 - SSI发送并接受1个字

这里应该返回2(0b00000010),也就是说:

  • SUS = 0b0 - 闪存编程(写入)/擦除状态没有挂起。
  • CMP = 0b0 - 反向SECTBBP设置的保护区域。W25Q闪存支持对顶端/底端区域的写保护。因为我们不使用保护功能,因此这里应该得到默认值0。
  • LB = 0b000 - 安全寄存器锁。W25Q闪存有3个特殊的当安全寄存器写入后就再也不能修改的256字节区间。则可以用来储存单次编程的数据,例如序列号。因为我们不使用安全功能,因此这里应该得到默认值0。
  • QE = 0b1 - 四联接口(QSPI)启用。
  • SRL = 0b0 - 状态寄存器未锁定。W25Q闪存的状态寄存器可以被锁定,直到断电。

如果为真,则说明闪存已经准备好以QSPI模式工作,跳转到set_qspi,见设置SSI为QSPI。否则,继续初始化闪存。

取决于订单,W25Q闪存可以是出厂默认启用QSPI的。

注意这些比特是非易失性的。重新上电后状态仍然保持不变。

因此,基本上我们都会跳转到set_qspi

允许闪存写入

RP2040 SSI_DR0
Data Register (0 of 36) @ 0x18000060 ©RPI2040
W25Q Write Enable
Write Enable 0x06 ©W25Q

1000003a:	2106      	movs	r1, #6
1000003c:	6619      	str	r1, [r3, #96]	@ XIP_SSI_BASE + DR0
1000003e:	f000 f834 	bl	ssi_waitsend
10000042:	6e19      	ldr	r1, [r3, #96]
			

在对W25Q闪存进行任何修改前,我们需要先允许写入。为此,我们需要发送6(0x06)。

将字写入发送FIFO就可以发送该字。调用子程序ssi_waitsend来等待发送完成,详见子程序 - 等待SSI发送

因为SPI是双向同步的,在主机的发送端发送数据时,主机的接收端同时也在采样接收数据,即使从机并没有驱动SPI总线。虽然收到的是垃圾数据,我们仍然需要将其读出来,以释放接收端FIFO。

设置闪存模式

W25Q Write Status Register
Write Status Register 1 0x1 ©W25Q
W25Q Status Register 1
Status Register 1 ©W25Q
W25Q Status Register 2
Status Register 2 ©W25Q

10000044:	2101      	movs	r1, #1
10000046:	6619      	str	r1, [r3, #96]	@ XIP_SSI_BASE + DR0
10000048:	2000      	movs	r0, #0
1000004a:	6618      	str	r0, [r3, #96]
1000004c:	661a      	str	r2, [r3, #96]
1000004e:	f000 f82c 	bl	ssi_waitsend
10000052:	6e19      	ldr	r1, [r3, #96]
10000054:	6e19      	ldr	r1, [r3, #96]
10000056:	6e19      	ldr	r1, [r3, #96]
			

发送1(0x01)到W25Q80闪存来写状态寄存器1。如股发送给了2个字节,状态寄存器2也会被写入。我们将对状态寄存器1写入0(0b00000000,保存在r0,在程序地址0x10000048时设定),对状态寄存器2写入2(0b00000010,保存在r2,在程序地址0x10000034时设定):

  • SRP = 0b0 - 使用软件保护。W25Q闪存的第3引脚可以作为写保护(硬件保护)或数据IO2。
  • SEC = 0bx - BP使用扇面(sector)或块(block)保护。我们的应用不会使用。
  • TB = 0bx - BP保护指顶端还是底端。我们的应用不会使用。
  • BP = 0b000 - 不要使用区块保护。
  • WEL = 0bx - 允许写。只读,在允许闪存写入中修改。
  • BUSY = 0bx - 闪存正在忙于编程/擦除。只读。
  • SUS = 0bx - 闪存编程/擦除状态挂起。只读。
  • CMP = 0bx - 反向SECTBBP设置的保护区域。我们的应用不会使用。
  • LB = 0b000 - 不要使用安全寄存器锁。(可能只允许单词编程)
  • QE = 0b1 - 启用四联接口(QSPI)。
  • SRL = 0b0 - 不要保护状态寄存器。

调用子程序ssi_waitsend来等待发送完成,详见子程序 - 等待SSI发送。接下来,销毁返回的垃圾数据。

等待闪存模式设置完成

W25Q Read Status Register
Read Status Register 1 0x05 ©W25Q
W25Q Status Register 1
Status Register 1 ©W25Q

check_flash_busy:
10000058:	2005      	movs	r0, #5
1000005a:	f000 f82f 	bl	ssi_writeread
1000005e:	2101      	movs	r1, #1
10000060:	4208      	tst	r0, r1
10000062:	d1f9      	bne.n	check_flash_busy
			

发送5(0x05)到W25Q80闪存来读取状态寄存器1。使用ssi_writeread来发送一个字并接收一个字,详见子程序 - SSI发送并接受1个字

持续轮询W25Q闪存状态寄存器1,直到其值为1(0b00000001),也就是说:

  • SRP = 0b0 - 使用软件保护。
  • SEC = 0b0 - BP使用块(block)保护。因为我们不使用保护功能,因此这里应该得到默认值0。
  • TB = 0b0 - BP保护指顶端。因为我们不使用保护功能,因此这里应该得到默认值0。
  • BP = 0b000 - 不使用区块保护。
  • WEL = 0b1 - 允许写。
  • BUSY = 0b0 - 闪存不忙于编程/擦除。

设置SSI为QSPI

W25Q闪存现已就绪,我们可以设置SSI为QSPI模式。

设置SSI模式

RP2040 SSI_SSIERN
SSI Enable @ 0x18000008 ©RPI2040
RP2040 SSI_CTRLR0
Control register 0 @ 0x18000000 ©RPI2040

set_qspi:
10000064:	2100      	movs	r1, #0
10000066:	6099      	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
10000068:	491b      	ldr	r1, =0x005f0300	@ 2 (QUAD) << SPI_FRF | 31 << DFS_32 | 3 (EEPROM_READ) << TMOD
1000006a:	6019      	str	r1, [r3, #0]	@ XIP_SSI_BASE + CTRLR0
			

禁用SSI进行配置。

设置SSI模式:

  • SPI_FRF - 2 = QSPI。(这个设置之后会用)
  • DFS_32 - 31 = 32位数据帧。
  • TMOD - 3 = 使用EEPROM_READ模式。硬件将使用为读EEPROM量身定制的内部逻辑。

Set SSI Data Size

RP2040 SSI_CTRLR1
Master Control register 1 @ 0x18000004 ©RPI2040

1000006c:	2100      	movs	r1, #0
1000006e:	6059      	str	r1, [r3, #4]	@ XIP_SSI_BASE + CTRLR1
			

展示设置数据大小为0字。也就是说,我们现在不会接收闪存返回的任何数据。

注意,和同时在两条线上发送与接收的标准SPI不同,QSPI是半双工的。SSI会使用所有线发送,然后释放数据总线(,但仍然驱动时钟信号),然后接收从机数据。

当设置数据大小为0后,SSI会在发送完数据后立刻终止传输。因此,从机将无法返回任何数据。

设置SSI的SPI模式

RP2040 SSI_SPI_CTRLR0
SPI control @ 0x180000f4 ©RPI2040
RP2040 SSI_SSIERN
SSI Enable @ 0x18000008 ©RPI2040

10000070:	491a      	ldr	r1, =0x00002221	@ 4 << WAIT_CYCLES | 2 (8B) << INST_L | 8 (x4) << ADDR_L | 1 (1C2A) << TRANS_TYPE
10000072:	481b      	ldr	r0, =0x180000f4	@ XIP_SSI_BASE + SPI_CTRLR0 
10000074:	6001      	str	r1, [r0, #0]
10000076:	2101      	movs	r1, #1
10000078:	6099      	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
			

依据闪存的时序设置SSI的SPI模式:

  • WAIT_CYCLES - 4 = 在发送和接收之间等待4个周期。这给闪存设备时间来处理指令、解码地址、设置IO、读取数据。
  • INST_L - 2 = 8位指令。
  • ADDR_L - 8 = 32位地址。
  • TRANS_TYPE - 1 = 使用SPI发送指令,使用FRF的设置(QSPI)发送地址。

重启SSI。

XIP配置

设置闪存与SSI位XIP模式。

设置闪存模式

W25Q Fast Read Quad I/O
Fast Read Quad I/O 0xEB ©W25Q

1000007a:	21eb      	movs	r1, #235
1000007c:	6619      	str	r1, [r3, #96]	@ XIP_SSI_BASE + DR0
1000007e:	21a0      	movs	r1, #160
10000080:	6619      	str	r1, [r3, #96]
10000082:	f000 f812 	bl	ssi_waitsend
			

要使用QSPI从W25Q80闪存读取数据,发送8位指令0xEB后接32位地址。

地址帧中前24位(A23-A0)为数据的实际地址,后8位为继续码。对于这个型号(W25Q80EW),如果状态码的第5和第4比特(第0比特为LSB)不为0b10,闪存就会进入继续模式。也就是说,之后的之后的交互中将可以省略指令。这能够减少发送指令带来的额外消耗。

我们将发送32位地址160,其中包含24位实际地址0x000000与8位继续码0b10100000。

调用子程序ssi_waitsend来等待发送完成,详见子程序 - 等待SSI发送。接下来,销毁返回的垃圾数据。

设置SSI的SPI模式

RP2040 SSI_SSIERN
SSI Enable @ 0x18000008 ©RPI2040
RP2040 SSI_SPI_CTRLR0
SPI control @ 0x180000f4 ©RPI2040

10000086:	2100      	movs	r1, #0
10000088:	6099      	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
1000008a:	4916      	ldr	r1, =0xa0002022	@ 0xA0 << XIP_CMD | 4 << WAIT_CYCLES | 0 (0B) << INST_L | 8 (x4) << ADDR_L | 2 (2C2A) << TRANS_TYPE
1000008c:	4814      	ldr	r0, =0x180000f4	@ XIP_SSI_BASE + SPI_CTRLR0 
1000008e:	6001      	str	r1, [r0, #0]
10000090:	2101      	movs	r1, #1
10000092:	6099      	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
			

禁用SSI进行配置。

依据闪存的时序设置SSI的SPI模式:

  • XIP_CMD - 0xA0 = 地址后接的继续码。
  • WAIT_CYCLES - 4 = 在发送和接收之间等待4个周期。
  • INST_L - 0 = 无指令。使用继续码。
  • ADDR_L - 8 = 32位地址。
  • TRANS_TYPE - 2 = 使用FRF的设置(QSPI)发送指令和地址。

重启SSI。

进入主程序

判断返回点


10000094:	bc01      	pop	{r0}
10000096:	2800      	cmp	r0, #0
10000098:	d000      	beq.n	boot_launch
1000009a:	4700      	bx	r0
boot_launch:
			

通过在二级引导程序第一行就保存的调用者地址来判断是谁调用了引导程序。如果该值为:

  • 零 - 一个程序调用了二级引导程序:返回该程序。
  • 非零 - 由片上引导程序进入 二级引导程序:载入主程序。

向量表

RP2040 SSI_SPI_CTRLR0
The VTOR holds the vector table offset address @ 0xe000ed08 ©RPI2040
ARM向量表
ARM向量表 ©ARM Cortex-M0+ Devices Generic User Guide

1000009c:	4812      	ldr	r0, =0x10000100
1000009e:	4913      	ldr	r1, =0xe000ed08	@ PPB_BASE + VTOR
100000a0:	6008      	str	r0, [r1, #0]
100000a2:	c803      	ldmia	r0, {r0, r1}	@ r0 <= [r0, #0], r1 <= [r0, #4]
100000a4:	f380 8808 	msr	MSP, r0
100000a8:	4708      	bx	r1
			

将向量表的地址写入CPU中相应的寄存器。在这里,我们假设向量表在紧接着二级引导程序的地方。注意向量表地址需要256字节对齐。

第一个向量(向量表地址+0)含有初始化栈的地址。我们需要将它装入MSP(主程序栈指针)。

第二个向量(向量表地址+4)含有重置地址,也就是主程序的入口。我们将使用bx(branch and exchange)来跳转到该地址。

子程序

子程序 - 等待SSI发送

RP2040 SSI_SR
Status register @ 0x18000028 ©RPI2040

ssi_waitsend:
100000aa:	b503      	push	{r0, r1, lr}
ssi_waitsend_loop:
100000ac:	6a99      	ldr	r1, [r3, #40]	@ XIP_SSI_BASE + SR
100000ae:	2004      	movs	r0, #4		@ 1 << TFE
100000b0:	4201      	tst	r1, r0
100000b2:	d0fb      	beq.n	ssi_waitsend_loop
100000b4:	2001      	movs	r0, #1		@ 1 << BUSY
100000b6:	4201      	tst	r1, r0
100000b8:	d1f8      	bne.n	ssi_waitsend_loop
100000ba:	bd03      	pop	{r0, r1, pc}
			

首先,将所有会使用的寄存器入栈以备份其内容。调用程序并不期望这些值被覆写。

持续轮询SSI状态寄存器直到TFEBUSY比特清零,也就是说:

  • TFE = 0b1 - 发送端FIFO为空。
  • BUSY = 0b0 - SSI空闲。

因为SPI是双向同步的传输协议,数据发送和接收都在同时发生。

恢复被污染的寄存器并返回。

子程序 - SSI发送并接受1个字

RP2040 SSI_DR0
Data Register (0 of 36) @ 0x18000060 ©RPI2040

ssi_writeread:
100000bc:	b502      	push	{r1, lr}
100000be:	6618      	str	r0, [r3, #96]	@ XIP_SSI_BASE + DR0
100000c0:	6618      	str	r0, [r3, #96]
100000c2:	f7ff fff2 	bl	ssi_waitsend
100000c6:	6e18      	ldr	r0, [r3, #96]
100000c8:	6e18      	ldr	r0, [r3, #96]
100000ca:	bd02      	pop	{r1, pc}
			

首先,将所有会使用的寄存器入栈以备份其内容。调用程序并不期望这些值被覆写。

要发送的数据通过r0传入。我们通过将这个字写入发送端FIFO的方式来发送它。这个字将会用来发送指令给从机。因为SPI是双向同步的,主机在发送的同时也会采样一个字。此时,从机并没有驱动SPI总线,采样到的数据为垃圾数据。

再次发送这个字。这用只是为了驱动SPI时钟,以允许从机返回数据。发送的数据会被从机忽略。

当2个字都发送后,接收端的FIFO中也有2个字了。第一个字是垃圾数据,但是我们仍需要将其读入r0来释放FIFO。接收端的FIFO中第二个字是实际数据,我们将其读入r0,覆盖刚才读的垃圾数据。

SSI_DR0寄存器代表发送端与接收端FIFO最顶层。我们只要直接读写这个寄存器就可以把数据加入和移出FIFO。SSI的内部指针将追踪寄存器读写与收发端口活动。

恢复被污染的寄存器并返回。

常数表


100000cc:	0000 4002
100000d0:	0000 1800
100000d4:	0000 0007
100000d8:	0300 005f
100000dc:	2221 0000
100000e0:	00f4 1800
100000e4:	2022 a000
100000e8:	0100 1000
100000ec:	ed08 e000
			

Thumb指令集使用16位指令。对于ldr(读取立即数)来说,立即数只由5比特空间。显然,这没办法用来装入32位常量。

相反,我们需要将32位常量存放在程序中,然后使用ldr rd, pc+offset,通过程序计数器作为读取指针外加一个偏移量。该常数和该指令不能离得太远,因为ldr指令支持的偏移量是有限的。

以上为二级引导程序中使用的常数。

自定义C语言程序和SDK引导程序

创建SDK引导程序

现在,让我们把SDK引导程序里面的东西放在一起:


.cpu cortex-m0plus
.thumb
.align 2
.section .boot2, "ax"

boot2:
	push	{lr}

@ Pad setup
	ldr	r3, =0x40020000	@ PADS_QSPI_BASE
	movs	r0, #0x21	@ 2 (8mA) << DRIVE | 1 << SLEWFAST
	str	r0, [r3, #4]	@ PADS_QSPI_BASE + GPIO_QSPI_SCLK
	ldr	r0, [r3, #8]	@ PADS_QSPI_BASE + GPIO_QSPI_SD0
	movs	r1, #2		@ 1 << Schmitt
	bic	r0, r1
	str	r0, [r3, #8]	@ PADS_QSPI_BASE + GPIO_QSPI_SD0
	str	r0, [r3, #12]	@ PADS_QSPI_BASE + GPIO_QSPI_SD1
	str	r0, [r3, #16]	@ PADS_QSPI_BASE + GPIO_QSPI_SD2
	str	r0, [r3, #20]	@ PADS_QSPI_BASE + GPIO_QSPI_SD3

@ Use standard SPI for
	ldr	r3, =0x18000000	@ XIP_SSI_BASE
	movs	r1, #0
	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
	movs	r1, #2
	str	r1, [r3, #20]	@ XIP_SSI_BASE + BAUD
	movs	r1, #1
	movs	r2, #0xf0
	str	r1, [r3, r2]	@ XIP_SSI_BASE + RX_SAMPLE_DLY
	ldr	r1, =0x00070000	@ 0 (STD) << SPI_FRF | 7 << DFS32 | 0 (TX_AND_RX) << TMOD
	str	r1, [r3, #0]	@ XIP_SSI_BASE + CTRLR0
	movs	r1, #1
	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR

@ Check flash in QSPI mode
	movs	r0, #53
	bl	ssi_writeread
	movs	r2, #2
	cmp	r0, r2
	beq	set_qspi

@ Enable falsh write
	movs	r1, #6
	str	r1, [r3, #96]	@ XIP_SSI_BASE + DR0
	bl	ssi_waitsend
	ldr	r1, [r3, #96]

@ Setup flash QSPI mode
	movs	r1, #1
	str	r1, [r3, #96]	@ XIP_SSI_BASE + DR0
	movs	r0, #0
	str	r0, [r3, #96]
	str	r2, [r3, #96]
	bl	ssi_waitsend
	ldr	r1, [r3, #96]
	ldr	r1, [r3, #96]
	ldr	r1, [r3, #96]

@ Wait flash ready
check_flash_busy:
	movs	r0, #5
	bl	ssi_writeread
	movs	r1, #1
	tst	r0, r1
	bne	check_flash_busy

@ Use QSPI and send fast read command to flash
set_qspi:
	movs	r1, #0
	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
	ldr	r1, =0x005f0300	@ 2 (QUAD) << SPI_FRF | 31 << DFS_32 | 3 (EEPROM_READ) << TMOD
	str	r1, [r3, #0]	@ XIP_SSI_BASE + CTRLR0
	movs	r1, #0
	str	r1, [r3, #4]	@ XIP_SSI_BASE + CTRLR1
	ldr	r1, =0x00002221	@ 4 << WAIT_CYCLES | 2 (8B) << INST_L | 8 (x4) << ADDR_L | 1 (1C2A) << TRANS_TYPE
	ldr	r0, =0x180000f4	@ XIP_SSI_BASE + SPI_CTRLR0 
	str	r1, [r0, #0]
	movs	r1, #1
	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR

@ Setup insturction bypass
	movs	r1, #235
	str	r1, [r3, #96]	@ XIP_SSI_BASE + DR0
	movs	r1, #160
	str	r1, [r3, #96]
	bl	ssi_waitsend

@ Setup QSPI with insturction bypass
	movs	r1, #0
	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR
	ldr	r1, =0xa0002022	@ 0xA0 << XIP_CMD | 4 << WAIT_CYCLES | 0 (0B) << INST_L | 8 (x4) << ADDR_L | 2 (2C2A) << TRANS_TYPE
	ldr	r0, =0x180000f4	@ XIP_SSI_BASE + SPI_CTRLR0 
	str	r1, [r0, #0]
	movs	r1, #1
	str	r1, [r3, #8]	@ XIP_SSI_BASE + SSIENR

@ Exit 2nd stage bootloader
	pop	{r0}
	cmp	r0, #0
	beq	boot_launch
	bx	r0
	boot_launch:
	ldr	r0, =0x10000100
	ldr	r1, =0xe000ed08	@ PPB_BASE + VTOR
	str	r0, [r1, #0]
	ldmia	r0, {r0, r1}	@ r0 <= [r0, #0], r1 <= [r0, #4]
	msr	MSP, r0
	bx	r1

@ Wait SPI sent
ssi_waitsend:
	push	{r0, r1, lr}
ssi_waitsend_loop:
	ldr	r1, [r3, #40]	@ XIP_SSI_BASE + SR
	movs	r0, #4		@ 1 << TFE
	tst	r1, r0
	beq	ssi_waitsend_loop
	movs	r0, #1		@ 1 << BUSY
	tst	r1, r0
	bne	ssi_waitsend_loop
	pop	{r0, r1, pc}

@ SPI send 1 command and receive 1 data
ssi_writeread:
	push	{r1, lr}
	str	r0, [r3, #96]	@ XIP_SSI_BASE + DR0
	str	r0, [r3, #96]
	bl	ssi_waitsend
	ldr	r0, [r3, #96]
	ldr	r0, [r3, #96]
	pop	{r1, pc}
	

使用之前使用的链接脚本,见文件boot2_src.ld


MEMORY {
	BOOT2(rx) : ORIGIN = 0x10000000, LENGTH = 256
}

SECTIONS {
	. = ORIGIN(BOOT2);
	.text : {
		KEEP(*(.boot2))
	} >BOOT2
}
	

汇编、链接、添加校验和,保存为文件boot2.o


arm-none-eabi-as --warn --fatal-warnings -g boot2_src.s -o boot2_src.o
arm-none-eabi-ld -nostdlib -nostartfiles -T boot2_src.ld boot2_src.o -o boot2_src.elf

arm-none-eabi-objcopy -O binary boot2_src.elf boot2_src.bin
pico-pad_checksum -s 0xFFFFFFFF boot2_src.bin boot2.s
arm-none-eabi-as --warn --fatal-warnings -g boot2.s -o boot2.o
	

现在,我们的二级引导程序就准备好了。注意该引导程序:

主程序

让我们来写一个和我的上一篇文章中相同的闪光程序。但是,这次我们使用C语言,并保存在文件main.c中:


#include <stdint.h>

void reset();

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

void reset() {
	*(uint32_t volatile * const)(0x4000f000) = (1<<5);
	*(uint32_t volatile * const)(0x400140cc) = 5;
	*(uint32_t volatile * const)(0xd0000020) = (1<<25);

	for (;;) {
		*(uint32_t volatile * const)(0xd000001c) = (1<<25);
		for (uint32_t i = 30000; i; i--) { __asm("nop\n\t"); }
	}
}
	
AARM向量表
ARM向量表 ©ARM Cortex-M0+ Devices Generic User Guide

在最开始,我们需要定义向量表(一些列地址):

  • 第一个向量为初始化栈指针。在这个程序中,我们想要使用SRAM区块5(0x20041000到0x20041FFF)作为栈。于是,将顶端地址+1写入该向量。
  • 第二个向量为主程序进入点。我们使用符号reset。在链接时,连接器会将该符号替换为方程reset的地址。
  • 在这个程序中,我们还不需要考虑其它向量。不用定义。
  • RP2040的文档指出,Cortex-M0+ CPU有34 WIC (Wake-up Interrupt Controller) lines (32 IRQ and NMI, RXEV)。也就是说,一共16 + 32 = 48个向量。

    给向量表取名.vector以便在链接时使用。

    我的上一篇文章中,我们已经讨论过单片机控制寄存器的地址。我们可以使用*(uint32_t volatile * const)(0x4000f000) = data这种特殊语法来直接写该地址。

    针对Cortex-M0+ CPU进行编译:

    
    arm-none-eabi-gcc -mcpu=cortex-m0plus -c -O3 main.c -o main.o
    	

    注意我们使用了-c,这告诉GCC只编译,不要链接。不然,GCC将会:

    因为我们在创建裸金属应用,我们不想要任何库或是环境,并且我们希望能手动链接程序。

    结果保存在文件main.o中。

    链接

    创建链接脚本flash.ld

    
    MEMORY {
    	FLASH(rx) : ORIGIN = 0x10000000, LENGTH = 2048k
    	SRAM(rwx) : ORIGIN = 0x20000000, LENGTH = 264k
    }
    
    SECTIONS {
    	.text : {
    		. = ORIGIN(FLASH);
    		KEEP(*(.boot2))
    		KEEP(*(.vector))
    		KEEP(*(.text))
    	} >FLASH
    }
    		

    在这个链接脚本中,按照片上引导程序的需求,我们把boot2(带有校验和的二级引导程序)放在闪存的最开头

    紧接着boot2的是vector向量表。也就是说,向量表位于地址0x10000100。

    向量表后为文本,即主程序。

    链接并创建uf2文件:

    
    arm-none-eabi-ld -nostdlib -nostartfiles -T flash.ld boot2.o main.o -o flash.elf
    pico-elf2uf2 flash.elf flash.uf2
    			

    现在,生成的文件flash.uf2可以用来对Pico编程了。