Lichee RV 裸机编程,将自己写的裸机程序烧录至 SD 卡,实现上电启动。
实现这一步之后,就可以继续完善自己的裸机程序,写自己的 SPL,Bootloader,Kernel 等。
当然,写自己的 Bootloader 和 Kernel 或许并不一定需要依赖自己的 SPL,使用固件中通用的 OpenSBI,U-Boot 也可以做,但是让板子从头到尾都运行自己写的代码也是一件很有趣的事情。
D1 这款 SoC 启动流程为:
-
上电之后
pc
寄存器重置为reset vector: 0x0
,并开始加载指令执行,这个地址对应于 SoC 内置BROM
; -
BROM 检查
FEL
引脚状态。如果用户按了 FEL 按钮,则跳转到 FEL 模块并执行,这种模式下可以使用 xfel 工具操作 Soc; -
否则 BROM 会:
- 寻找启动设备;
- 在启动设备的特定位置,检查是否存在
eGON.BT0
标记来判断是否存在BOOT0
; - 加载
BOOT0
到SRAM: 0x20000
,并对BOOT0
进行完整性校验,然后执行;
如果未找到设备、未找到 BOOT0,或者校验不通过,则跳转到 FEL 模块执行;
对于 Lichee RV 板子来说:
- 启动设备也就是 SD 卡;
- 可以把裸机程序作为 BOOT0;
- 关键在于按照规范生成 BOOT0,并写到 SD 卡的指定位置;
这样一来,Lichee RV 上电之后,就会从 SD 卡加载自己写入的 BOOT0,完成校验并执行。
SD 卡的第 16 个扇区,也就是 0x2000
处。
- BOOT0 区域由 header 和 boot0 程序组成,先是 header,然后是 boot0 程序,header 和 boot0 都需要适当对齐,因此可能存在一些 padding;
- 具体来说,BOOT0 包括:
- header
- boot0 程序
- 为对齐而填充的 padding
header 定义如下:
typedef struct __attribute__((packed)) boot_file_head
{
u32 jump_instruction; // one intruction jumping to real code
u8 magic[8]; // ="eGON.BT0", not C-style string.
u32 check_sum; // generated by PC
u32 length; // generated by PC
u32 pub_head_size; // the size of boot_file_head_t
u8 pub_head_vsn[4]; // the version of boot_file_head_t
u8 file_head_vsn[4]; // the version of boot0_file_head_t or boot1_file_head_t
u8 Boot_vsn[4]; // Boot version
u8 eGON_vsn[4]; // eGON version
u8 platform[8]; // platform information
} boot_file_head_t;
boot_file_head_t
中,
jump_instruction
是一条跳转指令,用于跳转到 boot0 程序。
这是因为 BROM 加载 BOOT0 时,并非只加载程序代码,而是第 16 扇区起始处开始读取 header 和 boot0 程序,并放置到 SRAM 起始处0x20000
,校验通过后从0x20000
开始执行,由于此处是一个 header,所以起始处有一个跳转指令,跳过 header 内容,至 boot0 程序继续执行。magic[8]
即为eGON.BT0
,BROM 会初步检查是否存在此标记来判断是否存在 BOOT0;check_sum
是整个 BOOT0 区域的校验和,BROM 会计算校验和是否正确来校验 BOOT0 的合法性。校验和生成逻辑为:// STEP-1:初始校验和为 0 check_sum = 0 // STEP-2: 计算校验和之前,赋给 BOOT0.header 中的 check_sum 一个给定的初值 BOOT0.header.check_sum = 0x5F0A6C39 // STEP-3: 将 BOOT0 视为 u32 数组,依次读取其中每一个 u32_val // BOOT0 的长度由 BOOT0.header.length 确定 for u32_val in BOOT0: // 将 u32_val 加到 check_sum 上 check_sum += u32_val // STEP-4: 此处得到正确的 check_sum BOOT0.header.check_sum = check_sum
length
存储整个 BOOT0 区域的长度,包括 header、boot0、padding,上述校验和计算所涵盖的 BOOT0 范围即由length
限定;- 其它字段似乎并不是很重要;
项目包含一个示例裸机程序和一个 BOOT0 生成工具。
C 语言版本在 c-language 分支。
- yuan_spl.s : 该程序将 PB_0 引脚设置为 OUTPUT 模式,并将其状态设为 1,如果连接 LED 灯,就能看到灯亮起
- yuan_spl.ld : 链接脚本,其中指定了程序加载地址,在 BOOT0 header 之后,即
0x20030
# mkdir -p bin
riscv64-linux-gnu-as yuan_spl.s -o bin/yuan_spl.o
riscv64-linux-gnu-ld -T yuan_spl.ld bin/yuan_spl.o -o bin/yuan_spl.elf
riscv64-linux-gnu-objcopy -O binary -S bin/yuan_spl.elf bin/yuan_spl.bin
mksunxiboot.c
文件可单独编译成可执行文件,它可以读取 flat binary,生成 BOOT0 header,并将 header 和 flat binary 拼装起来输出为可写入的 BOOT0 数据。
# mkdir -p tools
# gcc -o tools/mksunxiboot mksunxiboot.c
./tools/mksunxiboot bin/yuan_spl.bin bin/BOOT0.bin
将生成的 BOOT0 写入 SD 卡第 16 扇区,即可上电执行:
sudo dd if=bin/BOOT0.bin of=${your_sdcard} bs=512 seek=16 conv=sync
使用 xxd 查看 SD 卡第16扇区的内容,即为 BOOT0 的内容,可以看到 eGON.BT0
标记。
riscv-server:~$ sudo xxd -s 8192 -l 512 /dev/sdb
00002000: 6f00 0003 6547 4f4e 2e42 5430 2b5d 86ea o...eGON.BT0+]..
00002010: 0002 0000 3000 0000 0000 0000 0000 0000 ....0...........
00002020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002030: b701 0002 9b02 1000 9392 0202 9382 12ff ................
00002040: 23a8 5102 9302 1000 23a0 5104 6f00 0000 #.Q.....#.Q.o...
00002050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000020a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000020b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000020c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000020d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000020e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000020f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002100: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002110: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002120: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002130: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002140: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002150: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002160: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002170: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002180: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002190: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000021a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000021b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000021c0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000021d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000021e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000021f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
使用 objdump 反编译 BOOT0 ,可以看到起始位置的 jump 指令能够正确跳过 header,至 0x30 处执行 BOOT0 程序代码。
riscv-server:~$ riscv64-linux-gnu-objdump -b binary -m riscv:rv64 -D bin/BOOT0.bin
bin/BOOT0.bin: file format binary
Disassembly of section .data:
0000000000000000 <.data>:
0: 0300006f j 0x30
4: 4765 li a4,25
6: 422e4e4f fnmadd.d ft8,ft8,ft2,fs0,rmm
a: 3054 fld fa3,160(s0)
c: ea865d2b 0xea865d2b
10: 0200 addi s0,sp,256
12: 0000 unimp
14: 0030 addi a2,sp,8
...
2e: 0000 unimp
30: 020001b7 lui gp,0x2000
34: 0010029b addiw t0,zero,1
38: 02029293 slli t0,t0,0x20
3c: ff128293 addi t0,t0,-15
40: 0251a823 sw t0,48(gp) # 0x2000030
44: 00100293 li t0,1
48: 0451a023 sw t0,64(gp)
4c: 0000006f j 0x4c
...