让 MT7621 等不支持 SPI-NAND 的路由器直接从 SPI-NAND 上启动

转载请注明原帖链接和作者!

在现在看来,现今比较老的一批路由器芯片,以 Atheros/QCA/MediaTek 等的 MIPS 平台的芯片为代表,基本都只支持从 SPI-NOR 启动。
如果要使用 SPI-NAND,就必须使用双 Flash 方案,即从 SPI-NOR 上启动,再从 SPI-NAND 上加载固件。
对于现有的方法,从单 Flash 改为双 Flash 就很不容易。如果是新设计的方案,双 Flash 会增加额外的物料成本和开发成本,不划算。

说到底,这还是因为 SPI-NOR 跟 SPI-NAND 使用的读指令的时序不同:

SPI-NOR 的 Read data (03h) 指令时序:

总结为 1个字节的 Opcode + 3字节的地址 + 输出数据

SPI-NAND 的 Read from cache x1 (03h) 指令时序:

总结为 1个字节的 Opcode + 2字节的地址 + 1字节的无用时钟 + 输出数据

可以看到这两者差异还是很明显的。而 CPU 读取数据的时序是固定不变的,即 SPI-NOR 的时序。因此这类 CPU 是不能从 SPI-NAND 上启动的。


但是,Micron 在他们的 SPI-NAND 中,提供了一个功能,可以将自身的 Read from cache x1 (03h) 和 Fast read from cache x1 (0Bh) 的时序转换为 SPI-NOR 的时序。
并且绝大部分 SPI-NAND 在上电之后都会将其第一个 page 的数据加载至 cache 里面,Micron 的当然也不例外。
这样,CPU 就能够直接访问到 SPI-NAND 第一个 page 的数据了。虽然一个 page 普遍都是 2KB 大小,少部分是 4KB。

根据描述,这个功能需要一系列复杂的操作才能开启,而且这个功能是不会因为断电就失效的。
但是,Micron 只提到了怎么开启这个功能,而没有提到怎么关闭这个功能。事实上我测试和发现,这个功能一旦开启就不能关闭了。


那么,能否在有限的 2KB 大小内,写一份代码让 MT7621 直接从 SPI-NAND 上启动呢?
显然我做到了。我的设计如下:

整个 Bootloader 分为3个部分:

  1. 第一阶段,此时没有内存可用。需要将第二阶段的代码从 SPI-NAND 读取出来并执行。
  2. 第二阶段,初始化 CPU 内存等,并加载第三阶段
  3. 第三阶段,即常规的 Bootlaoder,例如 Breed 或者 U-Boot

虽然 MT7621 有 24KB 的 SRAM 可用,但是考虑到第二阶段大小必定大于 24KB,因此第二阶段只能放在 cache 上执行。
MT7621 有 256KB 的 L2 cache,4-way。MIPS 规定最多 3 way 可以被锁定,也就是最多 192KB 的 L2 cache 可以被用于加载第二阶段,完全足够了。

因此,第一阶段需要完成如下操作:

  • 初始化 MIPS CPS,以便于能够进行 cache 初始化操作
  • 初始化 cache,并锁定一部分用于加载第二阶段
  • 初始化 spi-nand 驱动,并加载第二阶段到 cache
  • 执行第二阶段

第二阶段主要就是加载并执行 MTK SDK U-Boot 中提供的初始化内存的 mt7621_stage_sram.bin,然后完成其余必要的初始化操作。
最后执行 spi-nand 驱动,加载第三阶段到内存,并执行

所以开发难点在于第一阶段,如何将这么多功能塞进 2KB 以内。
首先当然是精简代码,只保留必要的部分。
然后就是利用 CPU 和编译器的特性:
MIPS CPU 支持 mips16 压缩指令,因此这个选项必须要开启
再一个是 gcc 编译器的链接时优化 (Link-time optimization, LTO) 功能。通常的优化都只能在一个源代码中进行优化。
这个 LTO 功能可以跨越源代码文件边界,将全部源代码看成一个整体进行进一步优化,减小体积。
只不过要注意的是 LTO 一直存在 BUG,因此只给第一阶段使用,不能给后续阶段使用。

最终我成功将第一阶段的大小控制在了 1.88KB 左右。这里面还有一个本可以去掉的串口输出功能。


然后就是 Bootloader / Kernel 适配了。

要能支持 SPI-NAND,U-Boot 最好选用 Upstream的,因为它已经支持 spi-nand framework 了。
但是 Upstream U-Boot 并不支持 MT7621 因此作罢。
而给 MTK SDK U-Boot 增加 SPI-NAND 支持工程量更大,因此不考虑。
最后就只有 Breed 可以胜任了。

而 Kernel 正式支持 SPI-NAND 需要版本 4.18 及之后了,当然 4.14 版本也可也轻松 backport。
Kernel 的 spi-nand 驱动需要进行修改,识别 SPI-NAND 是否开启了 NOR Read 功能,如果开启了,就要修改 spi-mem op template,调整其 read_cache 的时序。
另一点是,开启 NOR Read 之后, 03h 和 0Bh 这两个指令的时钟频率不能高于 20MHz,否则会得到错误的数据。因此 dts 里面要限制其 spi master的速率。

另外,开启了 NOR Read 的 SPI-NAND 存在一个跟 SPI-NOR 32MB 类似的软重启问题。
跟 SPI-NOR 直接读取存储的数据不同,SPI-NAND 的 03h/0Bh 指令是读取的内部 cache 的数据,而真实存储的数据需要预先在 SPI-NAND 内部读取到 cache 后,才能通过 03h/0Bh 读出。
而要将数据写入 SPI-NAND,也是先将数据加载到 cache,SPI-NAND 内部才会将数据实际写入。
这样会带来的一个显而易见的问题就是,cache 内的数据会随着对 SPI-NAND 的操作而不断变化,而软重启后,cache 的数据就是未知的了,且99.99%的概率不是第一个 page 的数据。
这会导致软重启无法开机。因此解决办法自然是在每次完整的读写操作之后,都执行一个加载第一个 page 的数据到 cache 的命令,以保证正常操作性,软重启能正常执行。


实际测试结果

第一二阶段 + Breed 输出:

Kernel 内 spi-nand 相关的输出:


我将第一二阶段的源代码放在 github 上:

https://github.com/hackpascal/mt7621-boot-spi-nand

对 kernel 的修改:

https://github.com/hackpascal/openwrt-dev/commits/mt7621-boot-spi-nand

测试用的 bootloader 和固件:

https://pan.baidu.com/s/17aWbKr7QzKWw8zw7fog_nw
提取码:8iqj

loader-no-payload.bin 不含第三阶段,可以自行拼接。
loader-breed.bin 的第三阶段是 Breed。

发表评论

电子邮件地址不会被公开。 必填项已用*标注