0%

OrangePi-3B 折腾笔记—— 构建文件系统镜像

OrangePi-3B 系列笔记:

温馨提示:

  1. OrangePi-3B 硬件版本 V1.1.1 的板子出现过有线网卡“无故”失灵的故障, 所以在进行操作前请确认开发板工作良好(修复这个问题需要更换 RK3566 芯片).
    https://www.bilibili.com/read/cv33224126
  2. 香橙派目前已经推出了硬件版本为 V2.1 的 OrangePi-3B, 有线网卡供应商更换为 RTL. 本系列文章没有针对 V2.1 的板子做过适配, 请根据实际情况操作(同时强烈谴责此种不负责任的行为, 如考虑购买此型号板子商用, 建议换个型号避坑).
  3. 本系列文章最后更新时间: 2024年11月02日.

编译环境

  • 主板: OrangePi-3b_V1.1.1
  • 芯片: RK3566
  • 环境: Debian:12_x86_64(Docker)

构建方式

一般嵌入式系统构建根文件系统会使用 buildroot 这个项目,构建的根文件系统足够小。但 buildroot 构建时严重依赖外网, 导致编译过程很慢。

为了使用有包管理器的操作系统,可以基于 DebianFedora 来构建根文件系统。它们都提供了专门的工具来支持这个操作:

  • debootstrap:
    debootstrapdebian/ubuntu 下的一个工具,用来构建一套基本的操作系统(根文件系统)。生成的目录符合 Linux 文件系统标准 (FHS),即包含了 /boot、/etc、/bin、/usr 等等目录,它比发行版本的 Linux 体积小很多,可以说是 “基本的操作系统”。
  • febootstrap:
    febootstrapfedora/centos 下的一个工具,功能类似 debootstrap

前几年从 CentOS 7 全面切换到 Debian 操作系统,为了方便这里也选择基于 Debian 来构建根文件系统。

构建可以分为两部分,分别是构建 rootfs 文件系统和构建 initramfs 文件系统。前者是 Linux 发行版,后者给 Kernel 作为 ramfs 使用。

个人理解 initramfs 的主要作用是方便在挂载真正的根分区时执行一些用户空间的操作,比如磁盘检查等操作,还是很有必要存在的。

为了能够通过 U-Boot 的引导配置文件启动系统,这里没有使用之前构建 U-Boot 的分区方案,而是只保留一个分区作为根分区。

U-Boot 加载 extlinux.conf 引导配置文件默认从存储设备的第一个分区加载。

构建

克隆代码

cd ~/projects/
git clone https://github.com/armbian/firmware.git -b master armbian-firmware
  1. 驱动 UWE5622 需要对应的固件,同样也是用 Armbian 仓库中现成的二进制文件;

启动容器

docker run -ti --rm --privileged -v ~/projects/:/projects/ build-linux bash
  1. 关于镜像 build-linux 的内容请参考之前的笔记,其中已经安装构建所需要的软件包。

以下是相关软件包的介绍:

  • debootstrap
    用于构建 Linux 发行版的程序
  • binfmt-support
    用于开启内异构程序模拟器运行的支持
  • qemu-user-static
    用于运行异构程序的模拟器,安装过程会自动开启异构程序的支持;也可以手动开启(比如使用容器时需要手动开启):
    # 开启
    sudo update-binfmts --package=qemu-user-static --enable
    # 检查
    sudo ls -lh /proc/sys/fs/binfmt_misc/
    # 关闭
    sudo update-binfmts --disable
    

    注意: update-binfmts 命令是内核控制指令,行为会影响宿主机,所以容器环境一般只需要开启一次;

  • u-boot-tools
    用于制作 U-Boot 镜像文件的工具集(只是记录,实际得使用虚拟环境中的 u-boot-tools)。
  • initramfs-tools
    用于创建 initramfs 镜像文件的工具集(只是记录,实际得使用虚拟环境中的 initramfs-tools)。
  • android-sdk-libsparse-utils
    用于创建 AndroidSparse 镜像文件的工具集。
  • e2fsprogs
    Ext2/3/4 文件系统工具(Ext2Filesystems Utilities),它包含了诸如创建、修复、配置、调试 ext 文件系统等的标准工具。
  • file/fdisk/gdisk/parted
    一些文件和文件系统的辅助工具。

构建基础系统

cd /projects/buildfs/

# 下载基础系统和包
sudo debootstrap --verbose \
    --foreign \
    --arch=arm64 \
    bookworm \
    rootfs \
    https://mirrors.tuna.tsinghua.edu.cn/debian/

# 使用 chroot 切换到文件系统进行第二阶段部署(部署基础系统和包)
sudo chroot rootfs /debootstrap/debootstrap --second-stage --verbose

# 清理临时文件
sudo rm -rf rootfs/var/cache/*
sudo rm -rf rootfs/var/log/*
sudo rm -rf rootfs/var/lib/apt/lists/*
  1. 用于下载基础系统和包的 debootstrapx64 架构程序;
  2. 用于第二阶段部署的 debootstraparm64 架构的程序;
  3. 使用 chroot 命令切换到 rootfs 文件系统,搭配 qemu 来模拟出 arm64 的运行环境执行 debootstrap 完成第二阶段部署;当然也可以用真正的 arm64 环境来执行第二阶段部署;
  4. 根据网上流传的教程,执行第二阶段部署前都需要挂载宿主机 /proc//sys//dev/ 和拷贝 qemu-*-static 可执行文件到目标目录结构,但是实际测试发现好像也不需要(可能是由于我是在 docker 容器的原因);

部署工具 debootstrap 的参数含义如下:

  • –verbose
    显示构建细节。
  • –foreign
    在与主机架构不相同时需要指定此参数,仅做初始化的下载。如果已经部署了模拟环境,可以不设置这个参数使得一并完成第二阶段部署(代替 –second-stage 参数)。
  • –variant=X
    使用引导脚本的变体X(目前支持的变体: buildd, fakechroot, minbase)。
  • –arch
    指定制作的文件系统的架构。
  • –include=A,B,C
    将指定的名称添加到基包列表中。
  • –exclude=A,B,C
    从列表中删除指定的包。
  • –second-stage
    第二阶段部署。一般制作异构文件系统时,宿主机无法执行目标架构的可执行文件。所有先下载包,然后切换到目标架构运行环境执行部署。
  • bookworm
    bookwormDebian 12 的发行版本名称。
  • rootfs
    存放文件系统的文件夹,能够自动创建。
  • https://mirrors.tuna.tsinghua.edu.cn/debian/
    清华 Debian 镜像服务器地址。

自定义系统

现在已经通过 debootstrap 初始化了 Debian 基础系统。接下来安装常用软件和自定义系统配置:

cd /projects/buildfs/

# 挂载本地设备
sudo mount -t proc -o nosuid,noexec,nodev proc rootfs/proc
sudo mount -t sysfs -o nosuid,noexec,nodev sysfs rootfs/sys
sudo mount -t devtmpfs -o mode=0755,nosuid devtmpfs rootfs/dev
sudo mount -t devpts -o gid=5,mode=620 devpts rootfs/dev/pts

# 进入虚拟 arm64 环境
sudo chroot rootfs /bin/bash

# 安装软件包
apt update
apt install -y sudo ssh locales rsyslog \
            net-tools nfs-common wget curl ntp vim bash-completion command-not-found \
            network-manager rfkill wireless-tools bluetooth bluez-tools \
            u-boot-tools initramfs-tools \
            file fdisk gdisk parted \
            lm-sensors
# 构建 command-not-found 数据库
apt update

# 设置时区
dpkg-reconfigure tzdata

# 设置语言
dpkg-reconfigure locales

# 设置主机名
HOSTNAME=OrangePi-3b
echo $HOSTNAME > /etc/hostname
sed -i '/localhost/s/$/ '"$HOSTNAME"'/g' /etc/hosts

# 添加用户
useradd --create-home --groups users,sudo --shell /bin/bash user
echo 'user:1111' | chpasswd

# 设置别名
cat > /etc/profile.d/aliases.sh <<\EOF
alias ll='ls -lh -F --color=auto --time-style=long-iso'
alias la='ls -lhA -F --color=auto --time-style=long-iso'
alias lt='ls -lht -F --color=auto --time-style=long-iso'
alias lat='ls -lhAt -F --color=auto --time-style=long-iso'
EOF

# 退出虚拟 arm64 环境
apt clean
exit

# 卸载本地设备
sudo umount rootfs/proc rootfs/sys rootfs/dev/pts rootfs/dev

# 清理临时文件
sudo rm -rf rootfs/var/cache/*
sudo rm -rf rootfs/var/log/*
sudo rm -rf rootfs/var/lib/apt/lists/*
sudo rm -rf rootfs/root/.bash_history
  1. 使用 chroot 命令搭配 qemu 的功能切换到 rootfs 文件系统,然后可以执行一些手动的配置;

拷贝 Kernel & Firmware 到文件系统

cd /projects/buildfs/

# 自动获取内核版本
utsrelease=$(cat ../kernel/out/include/generated/utsrelease.h)
kernel_version=$(echo ${utsrelease:21:-1})
echo ${kernel_version}

# 拷贝 kernel 产物(config/kernel/dtb)
sudo rm -rf rootfs/boot/*
sudo cp -a ../kernel/out/.config rootfs/boot/config-${kernel_version}
sudo cp -a ../kernel/out/arch/arm64/boot/Image.gz rootfs/boot/Image.gz
sudo mkdir -p rootfs/boot/dts/rockchip/overlay/
sudo cp -a ../kernel/out/arch/arm64/boot/dts/rockchip/rk3566*.dtb rootfs/boot/dts/rockchip/
sudo cp -a ../kernel/out/arch/arm64/boot/dts/rockchip/rk35*.dtbo rootfs/boot/dts/rockchip/overlay/
sudo chown -R root:root rootfs/boot/

# 拷贝 kernel 产物(modules)
sudo rm -rf rootfs/lib/modules/
sudo cp -a ../kernel/out/lib/modules/ rootfs/lib/
sudo chown -R root:root rootfs/lib/modules/

# 注册驱动开机加载(UWE5622)
sudo cat > uwe5622.conf <<\EOF
sprdbt_tty
sprdwl_ng
EOF
sudo chown root:root uwe5622.conf
sudo mv uwe5622.conf rootfs/etc/modules-load.d/

# 拷贝固件(Rockchip)
sudo cp -rf ../armbian-firmware/rockchip/ rootfs/lib/firmware/
# 拷贝固件(UWE5622)
sudo cp -rf ../armbian-firmware/uwe5622/ rootfs/lib/firmware/
sudo cp -rf ../armbian-firmware/bt_configure_*.ini rootfs/lib/firmware/
sudo ln -s uwe5622/wcnmodem.bin rootfs/lib/firmware/wcnmodem.bin
sudo ln -s uwe5622/wifi_2355b001_1ant.ini rootfs/lib/firmware/wifi_2355b001_1ant.ini

# 拷贝定制软件包(UWE5622)
sudo cp -rf ../armbian-build/packages/bsp/rk3399/hciattach_opi rootfs/usr/bin/hciattach_opi
sudo chmod 755 rootfs/usr/bin/hciattach_opi
sudo cp -rf ../armbian-build/packages/bsp/rk3399/sprd-bluetooth.service rootfs/lib/systemd/system/
sudo ln -s /lib/systemd/system/bluetooth.service rootfs/etc/systemd/system/multi-user.target.wants/sprd-bluetooth.service
  1. 注意其中 ${kernel_version}Kernel 构建的版本号,Kernelmodules 需要一致;(可通过在 Kernel 目录执行 make O=out kernelrelease 查看)
  2. 缺少 modules 可能会导致一些设备无法正常驱动,比如网卡;

创建 U-Boot 引导文件

U-Boot 支持 boot.scrextlinux.conf 两种启动方式的引导文件,两种任选其一即可

方式一:使用 boot.scr 作为启动脚本

cd /projects/buildfs/

# 生成脚本配置
sudo cat > boot.env <<\EOF
earlycon=on
loglevel=7
fdtfile-overlay=
fdtfile=rockchip/rk3566-orangepi-3b-custom.dtb
EOF
sudo chown root:root boot.env
sudo mv boot.env rootfs/boot/

# 生成启动脚本
sudo cat > boot.cmd <<\EOF
setenv load_addr_r "0x6000000"
setenv kernel_addr_r "0x4000000"
setenv ramdisk_addr_r "0x2000000"
setenv fdt_addr_r "0x1000000"

# default values
setenv earlycon "off"
setenv console "both"
setenv bootlogo "false"
setenv prefix "/boot/"
setenv fdtfile "rockchip/rk3566-orangepi-3b.dtb"
setenv rootdev "/dev/mmcblk0p1"
setenv rootfstype "ext4"
setenv rootfsmode "rw"
setenv loglevel "7"

echo "Boot script loaded from ${devtype} ${devnum}"

if test -e ${devtype} ${devnum} ${prefix}boot.env; then
 	load ${devtype} ${devnum} ${load_addr_r} ${prefix}boot.env
 	env import -t ${load_addr_r} ${filesize}
fi

if test "${earlycon}" = "on"; then setenv consoleargs "${consoleargs} earlycon"; fi
if test "${console}" = "display" || test "${console}" = "both"; then setenv consoleargs "console=tty1"; fi
if test "${console}" = "serial" || test "${console}" = "both"; then setenv consoleargs "${consoleargs} console=ttyS2,1500000n8"; fi
if test "${bootlogo}" = "true"; then
 	setenv consoleargs "${consoleargs} splash plymouth.ignore-serial-consoles"
else
 	setenv consoleargs "${consoleargs} splash=verbose"
fi

setenv bootargs "${consoleargs} consoleblank=0 loglevel=${loglevel} root=${rootdev} rootfstype=${rootfstype} rootwait ${rootfsmode} ${extraargs}"

load ${devtype} ${devnum} ${kernel_addr_r} ${prefix}Image.gz
load ${devtype} ${devnum} ${ramdisk_addr_r} ${prefix}initramfs.img
load ${devtype} ${devnum} ${fdt_addr_r} ${prefix}dts/${fdtfile}

fdt addr ${fdt_addr_r}
fdt resize 65536
for file in ${fdtfile-overlay}; do
	if load ${devtype} ${devnum} ${load_addr} ${prefix}dts/${file}; then
		echo "Applying DT overlay ${file}"
		fdt apply ${load_addr} || setenv overlay_error "true"
	fi
done
if test "${overlay_error}" = "true"; then
	echo "Error applying DT overlays, restoring original DT"
	load ${devtype} ${devnum} ${fdt_addr_r} ${prefix}dts/${fdtfile}
fi

booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r}
EOF
sudo chown root:root boot.cmd
sudo mv boot.cmd rootfs/boot/
sudo mkimage -C none -A arm -T script -d rootfs/boot/boot.cmd rootfs/boot/boot.scr
  1. 这个启动脚本是仿照的 Armbian 的启动脚本;

方式二:使用 extlinux.conf 作为启动配置

cd /projects/buildfs/

# 生成启动配置
sudo cat > extlinux.conf <<\EOF
# u-boot/boot/pxe_utils.c
# static const struct token keywords[]
menu title ORANGEPI-3B

default Debian
timeout 10

label Debian
    kernel /boot/Image.gz
    initrd /boot/initramfs.img
    devicetree /boot/dts/rockchip/rk3566-orangepi-3b-custom.dtb
    # devicetree-overlay
    append earlycon console=tty1 console=ttyS2,1500000n8 consoleblank=0 loglevel=7 root=/dev/mmcblk0p1 rootfstype=ext4 rootwait rw
EOF
sudo chown root:root extlinux.conf
sudo mkdir -p rootfs/boot/extlinux/
sudo mv extlinux.conf rootfs/boot/extlinux/
  1. timeout 单位是 0.1 秒;

制作 initramfs 镜像

使用 Debian 系操作系统的 mkinitramfs 命令来实现:

cd /projects/buildfs/

# 进入虚拟 arm64 环境
sudo chroot rootfs /bin/bash

# 自动获取内核版本
config_name=$(ls /boot/config-*)
kernel_version=$(echo ${config_name:13})
echo ${kernel_version}

# 新增自定义程序配置
cat > /etc/initramfs-tools/hooks/fsck <<\EOF
#!/bin/sh
. /usr/share/initramfs-tools/scripts/functions
. /usr/share/initramfs-tools/hook-functions
copy_exec /sbin/e2fsck
copy_exec /sbin/fsck
copy_exec /sbin/fsck.cramfs
copy_exec /sbin/fsck.ext2
copy_exec /sbin/fsck.ext3
copy_exec /sbin/fsck.ext4
copy_exec /sbin/logsave
EOF
chmod 755 /etc/initramfs-tools/hooks/*
chown root:root /etc/initramfs-tools/hooks/*

# 新增自定义模块配置
cat > /etc/initramfs-tools/modules <<\EOF
# List of modules that you want to include in your initramfs.
# They will be loaded at boot time in the order below.
#
# Syntax:  module_name [args ...]
#
# You must run update-initramfs(8) to effect this change.
#
# Examples:
#
# raid1
# sd_mod
blocklayoutdriver
EOF

# 打包 initramfs 文件系统
mkinitramfs -c gzip -o /boot/initramfs.gz ${kernel_version}
# 构建镜像文件
mkimage -A arm64 -O linux -T ramdisk -C none -a 0 -e 0 -n initramfs -d /boot/initramfs.gz /boot/initramfs.img

# 退出虚拟 arm64 环境
exit

# 清理临时文件
sudo rm -rf rootfs/boot/initramfs.gz
sudo rm -rf rootfs/root/.bash_history
  1. 同样使用 chroot 命令搭配 qemu 的功能来切换到 initramfs 文件系统目录结构,然后使用 initramfs-tools 的工具来制作 initramfs 文件系统镜像;
  2. 注意其中 ${kernel_version}Kernel 构建的版本号,需要和 Kernelmodules 对应;(可通过在 Kernel 目录执行 make O=out kernelrelease 查看)

制作 rootfs 镜像文件

cd /projects/buildfs/

# 创建 rootfs 文件系统镜像
dd if=/dev/zero of=rootfs.img bs=1M count=0 seek=2048
sudo mkfs.ext4 -F rootfs.img

# 临时挂载镜像文件并填充内容
mkdir -p image
sudo mount rootfs.img image
sudo cp -a rootfs/* image/
sudo umount image
rm -rf image

# 修改镜像文件大小(可选,优化之后分区也随之变小)
sudo e2fsck -fy rootfs.img
sudo resize2fs -f rootfs.img 800M
# 缩小镜像至最小(可选,优化之后分区也随之变小)
sudo e2fsck -fy rootfs.img
sudo resize2fs -M rootfs.img

主线 U-Boot 内置的 fastboot 支持刷入 AndroidSparse 镜像文件,这种镜像文件的尺寸会更小很多。可以使用 img2simg 将上面制作的 rootfs.img 转换成 Sparse 镜像:

img2simg rootfs.img rootfs-sparse.img
  1. AndroidSparse 镜像文件不能使用 RKDevTool 直接刷入;也不能直接挂载,需要使用 simg2img 还原之后才能挂载;

rootfs.img 刷入开发板后,可能文件系统的大小和分区表是不一样的,可以在进入系统之后使用 resize2fs 命令对齐分区到分区表的大小:

sudo resize2fs /dev/mmcblk0p1

构建产物

  • buildfs/rootfs/boot/initramfs.img
    initramfs 文件系统镜像,可通过 U-Boot 提供给 Kernel 使用;
  • buildfs/rootfs.img
    rootfs 文件系统镜像,烧录到磁盘 rootfs 分区使用;
  • buildfs/rootfs-sparse.img
    rootfs 文件系统镜像的 Sparse 版本,使用 U-Bootfastboot 模块刷入磁盘 rootfs 分区使用;

调试

如果频繁变动 RootFS,可以通过 Kernel 挂载 NFS 来快速验证。

使用 boot.scr 作为启动脚本

编辑 /boot/boot.env 文件添加相关配置即可:

earlycon=on
loglevel=7
fdtfile-overlay=
fdtfile=rockchip/rk3566-orangepi-3b-custom.dtb
rootdev=/dev/nfs
extraargs=nfsroot=192.168.8.13:/home/liux/projects/buildfs/rootfs/,vers=4 ip=dhcp
rootfstype=ext4

使用 extlinux.conf 作为启动配置

编辑 /boot/extlinux/extlinux.conf 文件修改相关配置即可:

# u-boot/boot/pxe_utils.c
# static const struct token keywords[]
menu title ORANGEPI-3B

default Debian
timeout 10

label Debian
    kernel /boot/Image.gz
    initrd /boot/initramfs.img
    devicetree /boot/dts/rockchip/rk3566-orangepi-3b-custom.dtb
    # devicetree-overlay
    append earlycon console=tty1 console=ttyS2,1500000n8 consoleblank=0 loglevel=7 root=/dev/nfs nfsroot=192.168.8.13:/home/liux/projects/buildfs/rootfs/,vers=4 ip=dhcp rootfstype=ext4 rootwait rw

问题记录

  • Kernel 启动 rootfs 过程中串口加载超时:
    [    **] Job dev-ttyS2.device/start running (30s / 1min 30s)
    [ TIME ] Timed out waiting for device dev-ttyS2.device - /dev/ttyS2.
    [DEPEND] Dependency failed for seri…rvice - Serial Getty on ttyS2.
    
    解决办法:
    rootfs中预置 udev 软件包