计算器的启动流程以及如何给R2s安装ArchLinux
我想装Arch Linux
最近我给自己的Nanopi-R2s上安装了Arch Linux。
如果用R2s做软路由,用OpenWRT官方固件就很好。厂商也提供了Ubuntu/Debian镜像。
虽然使用这些镜像可以更快速部署,但我更想借这个机会深入理解Linux底层架构——这正是Arch的价值,它强制用户直面底层配置。尽管这对新手而言充满挑战,不过Arch提供非常全面和详细的文档。过程具有挑战性,但也提供了很好的支持,这是最佳的学习机会。
我对在PC上安装Arch的过程很熟悉,但是在Arm设备上还是第一次。 网上虽然有一些教程可供参考,但是大多数都只提供操作步骤,如果不理解每个步骤的意义,出了问题就会很难处理。 我不打算也写一篇那样的教程,而是会对比PC架构,聊一下ARM的启动过程,然后会有具体实践过程。
这篇文章是写给还没有刷Arch之前的自己,适合那些想了解计算机启动过程的朋友,当然,对于想给自己的ARM设备刷ArchLinuxARM的朋友也会有帮助。
ARM 设备的启动流程
要成功部署系统,首先要理解设备从通电到系统就绪的全过程。我们将其简化为三个阶段:。
- 固件初始化阶段:
-
传统PC:BIOS(Basic Input Output System)完成硬件初始化、POST自检、启动设备选择
-
ARM架构:SoC(System on chip)芯片内置BootROM,执行初始化后从存储设备固定位置加载引导程序
-
- 引导加载阶段。加载系统的工具叫做Bootloader(引导程序)。它为硬件提供了一个抽象层,支持对复杂文件系统的操作,并且传递参数给内核。常见引导程序:
- x86:GRUB
- ARM:U-Boot(嵌入式设备主流选择)
- Windows:bootmgfw.efi(UEFI环境)
- 内核加载阶段。
这里存在一个”先有鸡还是先有蛋”的哲学问题:操作系统需要管理存储设备,但自身又存放在存储设备中。解决方案是:
- Bootloader直接加载包含基础驱动和文件系统模块的内核
- 通过initramfs(初始内存文件系统)建立临时根文件系统,最终挂载真正的根文件系统(rootfs)
基于上述原理,我们需要准备以下组件:
组件 | 作用 | 获取方式 |
---|---|---|
U-Boot | 二级引导程序 | 交叉编译或预编译二进制 |
Linux内核 | 系统核心 | 官方仓库或自定义编译 |
设备树(Device Tree) | 硬件描述文件 | 厂商SDK提供 |
initramfs | 临时根文件系统 | Arch Linux ARM官方镜像 |
rootfs | 根文件系统 | Arch Linux ARM官方镜像 |
实战部署流程
参考FriendlyElec Wiki配置交叉编译环境:
# 安装友善提供的交叉编译器
sudo bash -c \
"$(curl -fsSL http://112.124.9.243:3000/friendlyelec/build-env-on-ubuntu-bionic/raw/branch/cn/install.sh)"
#配置环境
export PATH=/opt/FriendlyARM/toolchain/11.3-aarch64/bin:$PATH
export GCC_COLORS=auto
首先编译UBoot。我推荐从上游U-boot项目编译,而不是wiki上面uboot-rockchip,更简单直接,项目开发比较活跃。
# 编译 ARM64 Rockchip SoC镜像
git clone --depth 1 https://github.com/TrustedFirmware-A/trusted-firmware-a.git
cd trusted-firmware-a
make realclean
make CROSS_COMPILE=aarch64-linux-gnu- PLAT=rk3328
cd ..
# 编译Uboot
git clone --depth 1 https://source.denx.de/u-boot/u-boot.git
cd u-boot
export BL31=../trusted-firmware-a/build/rk3328/release/bl31/bl31.elf
make evb-rk3328_defconfig
make CROSS_COMPILE=aarch64-linux-gnu-
编译好之后把它刷入SD卡。因为SoC ROM不像BIOS有充足的空间,所以它的连接方式比较简单粗暴,就是从存储设备的固定位置读取。
微星瑞芯片遵循它自己的分区标准。
阶段 | 名称 | 程序 | 文件 | 磁盘位置 |
---|---|---|---|---|
1 | 主引导 | ROM code | BootRom | |
2 | 二级引导 | U-Boot | boot-rockchip.bin | 0x40 |
TPL/SPL | ||||
3 | boot分区 | Linux内核 | boot.img | 0x8000 |
Initrd镜像 | initramfs-linux.img | |||
设备树二进制文件 | rk3328-nanopi-r2s.dtb | |||
boot.scr | ||||
4 | root文件系统 | 0x40000 |
请注意就像Bootloader不太够直接拉起系统,因此使用Initrd建立临时根文件系统一样,SoC ROM只能读取比较简单的程序,而U-boot相对于SoC显得像庞然大物,所以中间也有很多过度阶段。 这个过程被称为两步加载,甚至有三步加载(先拉起TPL/SPL,再由他们拉起更大的启动程序)。好消息是我们不用管这些,U-boot二进制程序里面包括第二步和第三步加载,直接把整个二进制文件刷到从64个扇区起的位置就可以了。
dd if=u-boot-rockchip.bin of=/dev/sdX seek=64 conv=notrunc
如果不行的话,就需要分别制作TPL/SPL,Uboot和Trust的镜像,然后分别刷到第64,16384,24576扇区了,参考Uboot文档。
然后分区、刷文件系统,然后根据ArchlinuxARM的指引下载和解压Rootfs文件。
# 为了简单,只分一个区
parted /dev/sdX makpart '' ext4 32768s -1s
# 选择ext4文件系统
mkfs.ext4 /dev/sdXp1
# 挂载分区
mount /dev/sdXp1 /mnt
# 下载和解压 Arch Rootfs
wget http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz
bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C /mnt
接下来,Archwiki上我们下载一个Boot.scr的脚本,放在/boot文件下面。这个文件是连接U-boot 和Linux操作系统的关键步骤。 我们编译的U-boot里面有个配置参数CONFIG_DISTRO_DEFAULTS,如果允许它,Uboot会扫描可启动的磁盘里面的boot.scr或者extlinux.conf文件,然后执行这些文件。 一个extlinux.conf 文件看起来是这样:
label Arch with uart devicetree overlay
kernel /arch/Image.gz
initrd /arch/initramfs-linux.img
fdt /dtbs/arch/board.dtb
fdtoverlays /dtbs/arch/overlay/uart0-gpio0-1.dtbo
append console=ttyS0,115200 console=tty1 rw root=UUID=fc0d0284-ca84-4194-bf8a-4b9da8d66908
这个配置文件告诉U-boot内核的位置,Initrd镜像的位置,和FDT文件的位置。FDT是关于设备的信息数据的二进制文件。然后是需要传递给内核的参数。
再看一下boot.scr脚本。需要注意的是boot.scr是一个二进制文件,我们不能直接打开,我们应该看对应的cmd文件。但是因为他们传递数据是基于文本,直接打开也能看出里面在干什么。 脚本里面内容比较多,我们不展示全文,只展示关键的部分。
setenv bootargs console=ttyS2,1500000 root=PARTUUID=${uuid}
load ${devtype} ${devnum}:${bootpart} ${kernel_addr_r} /boot/Image
load ${devtype} ${devnum}:${bootpart} ${fdt_addr_r} /boot/dtbs/${fdtfile};
load ${devtype} ${devnum}:${bootpart} ${ramdisk_addr_r} /boot/initramfs-linux.img;
booti ${kernel_addr_r} ${ramdisk_addr_r}:${filesize} ${fdt_addr_r};
可见这个文件里面就是一堆U-boot命令,而且作用和上面的extlinux.conf文件是一样的,即传递一些内核参数,加载内核,设备树二进制文件,以及Initrd镜像到内存的指定位置,最后启动内核和Initrd。
这些内存位置的变量则在源文件主板的Header文件中,比如rk3328_common.h里面的:
#define ENV_MEM_LAYOUT_SETTINGS \
"scriptaddr=0x00500000\0" \
"script_offset_f=0xffe000\0" \
"script_size_f=0x2000\0" \
"pxefile_addr_r=0x00600000\0" \
"fdt_addr_r=0x01e00000\0" \
"fdtoverlay_addr_r=0x01f00000\0" \
"kernel_addr_r=0x02080000\0" \
我们下载的这个boot.scr脚本没办法直接使用,反正我这里行不通,至少存在以下几个问题:
- Arch提供的initramfs镜像不能直接加载,需要编译成U-boot可以加载的形式。
- dtb文件也需要修改成合适的,反正Arch提供的dtb文件在我这里没有一个能用的,需要自己编译内核和dtbs。
不用担心,只要知道这个文件的作用,我们完全可以自己写这个脚本。 我的boot.cmd脚本差不都是这样:
load ${devtype} ${devnum}:${distro_bootpart} ${ramdisk_addr_r} ${prefix}uInitrd
load ${devtype} ${devnum}:${distro_bootpart} ${kernel_addr_r} ${prefix}Image
load ${devtype} ${devnum}:${distro_bootpart} ${fdt_addr_r} ${prefix}dtbs/rk3328-nanopi-r2-rev00.dtb
fdt addr ${fdt_addr_r}
fdt resize 65536
booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r}
写完之后,安装Uboot-tools包,然后用mkimage命令制作ramdisk和boot.scr:
# 输入路径跟脚本中的地址对应
mkimage -A arm -O linux -T ramdisk -C none -n "Initrd Image" -d /mnt/boot/initramfs-linux.img /mnt/boot/uInitrd;
mkimage -A arm -O linux -T script -C none -n "Boot Script" -d boot.cmd /mnt/boot/boot.scr
想了解这个命令可以上网查询,这里就不详细介绍了。 到了这一步我们有了U-boot作为启动加载程序,就基本完成了,然后插电看看是不是已经搞定了。
必坑指南
通过USB-TTL模块查看启动日志。我没有这个模块,如果指示灯不显示好了,我也不知道问题出在那个环节,浪费了很多时间。
现在想来最好从Armbian这个项目下载一个能用的系统, 测试Uboot和boot.scr或者extlinux.conf能不能工作,最后再刷入Arch的文件系统了。
不建议使用友善官方镜像,因为它们分区太细了,需要debug的环节就太多了。
最好能准备两个SD卡,一个用于测试,一个用于正式部署。
参考链接:
https://wiki.friendlyelec.com/wiki/index.php/NanoPi_R2S/zh
https://opensource.rock-chips.com/wiki_Boot_option
https://docs.u-boot.org/en/latest/board/rockchip/rockchip.html#rockchip-boards
https://archlinuxarm.org/platforms/armv8/rockchip/rock64
https://gist.github.com/larsch/a8f13faa2163984bb945d02efb897e6d