树莓派快速入门

前言

实际上这又是一篇类似于 持续更新中的稀奇古怪技巧的文章,但内容主要是使用和测试树莓派时踩过的坑与解决方法。

文章的内容从树莓派系统的安装配置,到程序的编程开发都有涉及。但都不是什么保姆式的指引,毕竟树莓派的资料众多,可以在网上轻松搜索到。

背景

前段时间突发奇想,想要在房间里搭建一些物联网模块,自己动手实现一个包括温湿度监测、异常状态检测、稀奇古怪的音乐播放等功能。

购入一块树莓派Zero 2后,又发现了还有包括STM32/ESP32等等架构可以试玩,还有像CH32这种国产化的同型号替代可以测试,正是一入硬件深似海啊。

至于树莓派,主要是作为原型测试使用,毕竟相较于硬件性价比更高的国产开发板,树莓派的活跃社区和丰富文档可能是仅有的优势之一了。之后只要有条件,肯定会测试采用香橙派、幸狐这种国产板子的方案的。届时再在本站点上进行更新。

不过,也别太认真,这些方案更多是测试和整活为主,事实上要实现的内容已经有相当成熟的产品可以直接购买。就请把这一系列当作是一个大学生消遣娱乐所玩弄的东西好了。

树莓派Raspberry Pi OS防止Wifi自动休眠

问题

由于是嵌入式场景,给树莓派安装的是官方的Raspberry Pi OS Lite(也就是以前的Raspbian,无GUI版),并使用SSH通过局域网访问树莓派。

但在使用的头几天里,总是遇到一个问题:静置一段时间后,树莓派就从网络上消失了。路由器里也找不到树莓派的连接。

一开始以为,是系统存在自动休眠机制,但按网上的说法,无GUI版RPiOS Lite不存在X Window,按理说不会自动休眠。同时查看dmesg,也不存在休眠的记录。

后来查阅资料才知道,树莓派官方Raspberry Pi OS默认启用Wifi网卡自动休眠,以节约能耗。

但对于嵌入式与服务器这种,需要长期开机联网的场景来说,这简直是灾难性的。

解决方法

解决方法当然是关闭Wifi网卡的自动休眠。只需要一句命令:

1
2
3
4
iw dev wlan0 set power_save off

# OR use nmcli
nmcli connection modify <CONNECTION> wifi.powersave 2

首先创建一个systemd服务:

1
2
3
4
5
6
sudo systemctl --full --force edit wifi-powersave-off.service

# OR edit service file manually
sudo vim /etc/systemd/system/wifi-powersave-off.service
# Reloading daemon is needed after editing manually
sudo systemctl daemon-reload

填入以下内容:

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Turn off WiFi power save
After=sys-subsystem-net-devices-wlan0.device

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/iw dev wlan0 set power_save off

[Install]
WantedBy=multi-user.target

保存上述文件,启用该服务,并重启系统,网络就不会随意中断了。

1
2
sudo systemctl enable wifi-powersave-off
sudo reboot

设置chroot交叉编译环境

问题

不同的体系结构

树莓派Zero 2采用的是ARM64(aarch64, ARMv8)处理器架构,因此不能简单使用x86平台工具链为树莓派开发应用。

想要针对树莓派开发应用,一种最简单能想到的方法是,直接在树莓派上安装和设置编译工具链,在树莓派上进行编译和生成应用程序

这种方法最为直接且麻烦最少,但最大的瓶颈在于树莓派极其有限的硬件资源。对于一个四核A53 SoC来说,编译大型应用有点强人所难了。

因此,通常还希望寻找在普通x86电脑上编译ARM应用的方法,以充分利用不同平台上的硬件优势。

为了在x86 PC上编译ARM应用,一种方法是直接在x86系统中引入ARM的编译工具链,就可以生成对应的可执行文件。针对ARM的GNU工具链可以在ARM官网Home / Downloads / Arm GNU Toolchain Downloads找到。

使用上述工具,编写如Hello World等程序就是没有问题的了。这种方法同样具有简单易行的优点。

不同的依赖库体系结构

然而,事实上,我们编写的程序常常还需要链接到各种第三方库,如ncurses, openmp等等。而在我们x86主机上,这些库文件通常是x86版本的,不能被正常链接到ARM的应用。

既然如此,那是否可以将ARM的库文件放到x86系统里供编译器查找链接呢?答案当然是可以的。

例如在Debian/Ubuntu系统中,可以使用多架构功能,为系统添加不同的体系结构,之后就可以直接使用apt安装指定的库文件。如:

1
2
3
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install libc6:arm64

不同的库环境

然而,上述方法存在一些问题。首先,向系统中添加多体系结构软件包容易造成混乱,毕竟并非人人都是Debian系统运维专家。

更重要的是,通过上述方法获得的ARM库文件,其版本号直接由x86主机系统决定,这可能与树莓派Raspberry Pi OS的版本号不同,从而造成一些奇怪的问题。

这并非是什么稀奇事,因为Linux系统的诸多发行版,库文件之间常常存在兼容性差异。

就我个人经历而言,也遭遇了在x86 Ubuntu系统上编译好的程序,在Raspberry Pi OS Bookworm中,由于Debian Bookworm和Ubuntu的GLIBCXX库版本号有3个小版本的差距,从而导致找不到库文件,程序无法正确运行的情况。

虚拟化的选择

到了这里,鉴于直接使用x86主机系统进行编译时存在的诸多限制和挑战,一个答案呼之欲出:虚拟化。我们希望在虚拟化的环境中完成对树莓派应用的编译。

目前为止,能在Linux上虚拟出ARM系统的,似乎只有QEMU。但QEMU的完全虚拟化ARM虚拟机性能极其感人,不信可以自己试试看。

且慢,我们只是需要一套虚拟的环境,并不需要一个完全隔离的系统。因此,我们可以使用轻量级的chroot设置一个隔离的系统目录环境。

到此为止,我们就确定了接下来的目标:在x86系统中设置针对树莓派开发的ARM体系结构chroot环境

本文还使用了在Debian系统下的chroot管理小工具schroot。要是不喜欢这玩意,也没关系,它最大的作用就是简化chroot之前的挂载步骤和设置资源隔离(但实际上在我们当前场景中安全并不是很重要)。只需要自行在chroot前设置好/dev, /sys, /proc等挂载点,两者效果完全相同。

设置x86主机系统

在开始前首先需要设置一下x86主机上的主系统。

笔者使用的x86主机系统是Debian 12,其他发行版可能需要按需修改一下。经测试,加入一些简单的额外步骤后也可以用于WSL内的Debian/Ubuntu系统

安装schroot

首先需要安装schroot(会一同安装build-essential):

1
sudo apt install sbuild

安装debootstrap

如果希望创建一个Debian的chroot环境,那么肯定需要用到debootstrap

1
sudo apt install debootstrap

如果需要使用基于Ubuntu的chroot环境,可以使用Ubuntu Base。其他发行版需要自行寻找方法。

(仅WSL)开启Systemd

接下来使用的binfmt_misc模块需要systemd的支持,因此需要提前为WSL子系统开启systemd

首先编辑WSL子系统内的/etc/wsl.conf,填入以下内容:

1
2
[boot]
systemd=true

然后在子系统内安装systemd组件:

1
sudo apt install systemd systemd-sysv

完成后退出WSL,在Windows命令行中执行wsl --shutdown以彻底关闭WSL系统,再重新打开WSL即可。

配置ARM二进制文件转译支持

首先安装:

1
sudo apt install qemu-user-static binfmt-support

两个软件包中,qemu-user-static可以在允许用户直接执行另一个体系结构的可执行文件。

binfmt-support会为系统添加binfmt_misc模块,从而在直接调用ARM体系结构ELF二进制文件时,自动判断所属体系结构,并自动调用qemu-user-static翻译器。查看该模块的运行状态与已注册的文件类型可以前往/proc/sys/fs/binfmt_misc/查看。

完成安装后重启系统或WSL子系统。到此为止,x86系统就可以(高性能损失地)翻译执行ARM应用了。以Debian软件源中的hello示例软件包为例:

1
2
3
4
5
6
7
8
# Check package version and filename if failed or 404
curl -sSL http://deb.debian.org/debian/pool/main/h/hello/hello_2.10-3_arm64.deb | dpkg -x - hello

# Run hello world from ARM64 architecture.
./hello/usr/bin/hello

# (Optional) Equals to above line.
qemu-aarch64-static ./hello/usr/bin/hello

安装libc以支持动态链接

为了支持ARM虚拟环境中的动态链接,需要在主机上安装ARM体系结构的libc库。

1
2
3
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install libc6:arm64

至此,主系统上的前期配置就已经全部完成。下面将开始设置chroot环境。

创建ARM架构chroot环境

在接下来的文段中,会把chroot出来的虚拟化ARM环境,称为虚拟环境。

初始化虚拟环境文件目录(rootfs)

以创建Debian Bookworm ARM64的虚拟环境为例,需要运行以下命令:

1
sudo debootstrap --arch=arm64 --foreign bookworm /srv/chroot/bookworm-arm64-build https://deb.debian.org/debian/

着重解释:

  • --foreign参数会跳过debootstrap的第二阶段,即apt执行软件包配置代码的阶段。由于体系结构的不同,在当前阶段直接执行配置代码会出错,因此需要推迟到chroot进入系统之后执行(后文介绍步骤)。本参数不能省略!
  • /srv/chroot/bookworm-arm64-build是自行指定的chroot环境存放目录,爱放哪放哪。不过/srv/chrootschroot推荐的目录。
  • https://deb.debian.org/debian/是指定的软件源,国内可以使用如http://mirrors.163.com/debian/等镜像站替代。

执行上述命令后,/srv/chroot/bookworm-arm64-build就是新创建的ARM架构系统根目录(rootfs, /)。

接着还需要复制QEMU翻译器到chroot目录下:

1
2
3
4
sudo cp /usr/bin/qemu-aarch64-static /srv/chroot/bookworm-arm64-build/usr/bin

# (Alternative) This should work too.
sudo cp "$(which qemu-aarch64-static)" /srv/chroot/bookworm-arm64-build/usr/bin

挂载文件系统

进入虚拟环境前还需要给它挂载一些有用的虚拟系统。以下命令根据chroot - Debian Wiki编写:

1
for f in dev dev/pts sys proc run ; do sudo mount --bind /$f /srv/chroot/bookworm-arm64-build/$f ; done

(另一种方案)不过在chroot - ArchWiki上,介绍的挂载点略有差异,但似乎都能运行,暂时没遇到问题:

1
2
3
4
cd /srv/chroot/bookworm-arm64-build/
sudo mount -t proc /proc proc/
sudo mount --rbind /sys sys/
sudo mount --rbind /dev dev/

完成Debootstrap第二阶段

如果使用上上节中的debootstrap命令创建Debian系统,那么现在还需要执行debootstrap的第二阶段,以完成虚拟环境的配置。

1
sudo chroot /srv/chroot/bookworm-arm64-build /debootstrap/debootstrap --second-stage

经过漫长的等待后,配置过程总算结束了。

使用schroot管理chroot环境

schroot配置chroot环境

实际上,sbuild等工具可以帮助我们自动创建虚拟环境并自动配置。但是!相较于完全透明的完整工具,我还是更享受将这些过程掌握在手里的感觉。

schroot可以帮忙管理chroot环境,简化激活chroot环境所需要的命令。

如果希望使用schroot而非chroot,只需要编写配置文件即可。

/etc/schroot/chroot.d/下创建配置文件,如/etc/schroot/chroot.d/bookworm-arm64-build.conf

1
2
3
4
5
[bookworm-arm64-build]
type=directory
description=Debian Bookworm arm64 build
directory=/srv/chroot/bookworm-arm64-build
root-users=root

这样就足以直接开始使用:

1
2
3
4
5
# List all chroot(s)
schroot -l

# chroot to virtual environment.
schroot -c bookworm-arm64-build -d /

上述配置是最简单的配置,可以附加其他很多选项,参见:schroot(1)schroot.conf(5)

例如,希望使用一个更短的别名:

1
2
3
4
5
6
[bookworm-arm64-build]
type=directory
description=Debian Bookworm arm64 build
directory=/srv/chroot/bookworm-arm64-build
+aliases=d12arm64
root-users=root

之后就可以直接使用以下命令访问容器:

1
schroot -c d12arm64 -d /

schroot配置文件目录挂载

有时我们希望允许虚拟环境访问指定的某个目录,但在chroot环境下,软连接是无法访问的(超过了访问路径范围),而硬链接又只能链接单个文件。

因此,通常的做法是把主机上希望允许访问的目录mount --bind到虚拟环境中。例如:

1
sudo mount --bind /host/example /srv/chroot/bookworm-arm64-build/chroot/some/example

这种方法,在使用原始的chroot,或是当上一节schroot配置文件第一行指定环境类型为type=plain时是可行的。

需要首先介绍一下schroot的大致工作机制。schroot为了允许指定的非root用户访问虚拟环境,在用户要求访问虚拟环境时,会先将虚拟环境的rootfs,也就是/srv/chroot/bookworm-arm64-build/,使用mount挂载到/run/schroot/mount/下,再chroot到这一映射后的目录下,允许用户访问。

当配置文件type=plain时,schrootmountrootfs时使用的参数是--rbind,此时原先挂载到rootfs下的所有挂载点都能被正常跟随处理。

但在type=directory时,由于schroot改而使用--bind参数(有什么差别?参见mount(8) - Linux manual page)挂载rootfs,此时原先挂载到虚拟环境中的挂载点将不会被正常跟随,在打开的虚拟环境中,所见目标目录为空。

解决这一问题的办法一是,直接将配置文件中的type=directory改回type=plain

1
2
3
4
5
6
7
8
```diff
[bookworm-arm64-build]
-type=directory
+type=plain
description=Debian Bookworm arm64 build
directory=/srv/chroot/bookworm-arm64-build
aliases=d12arm64
root-users=root

然而plain类型的schroot环境需要手动挂载dev/sys/proc等路径,和chroot没有大差异。在不需要考虑安全等因素的场景中,选择使用schroot正是为了免去这些麻烦而准备的,现在直接改回去大有一夜回到解放前的感觉。

因此尝试以下第二种方法,也就是schroot.conf(5)所提到的,通过指定挂载虚拟环境时的fstab文件配置挂载点。该文件格式与/etc/fstab格式相同。

下面以挂载/host/example到虚拟环境中的/chroot/example为例。注意这里的/chroot/example虚拟环境中的路径,不是相对主机系统而言的。

首先在/etc/schroot/下创建一个fstab.d文件夹,再创建一个独立的fstab文件(实际上后缀名不作强制要求)。

1
2
sudo mkdir -p /etc/schroot/fstab.d
sudo vim /etc/schroot/fstab.d/bookworm-arm64-build.fstab

填入以下内容,其中有一部分内容可以直接照搬/etc/schroot/default/fstab

1
2
3
4
5
6
7
8
9
# Note that the mount point will be prefixed by the chroot path
# (CHROOT_PATH)
#
# <file system> <mount point> <type> <options> <dump> <pass>
/proc /proc none rw,bind 0 0
/sys /sys none rw,bind 0 0
/dev /dev none rw,bind 0 0
/dev/pts /dev/pts none rw,bind 0 0
/host/example /chroot/example none rw,bind 0 0

再修改/etc/schroot/chroot.d/bookworm-arm64-build.conf中的内容,添加一行setup.fstab,指向刚刚创建的fstab文件,该路径是相对于/etc/schroot/而言的。

1
2
3
4
5
6
7
[bookworm-arm64-build]
type=directory
description=Debian Bookworm arm64 build
directory=/srv/chroot/bookworm-arm64-build
aliases=d12arm64
root-users=root
+setup.fstab=fstab.d/bookworm-arm64-build.fstab

保存后,还需要在虚拟环境文件系统中创建挂载点:

1
sudo mkdir -p /srv/chroot/bookworm-arm64-build/chroot/example

重新进入schroot,就可以在/chroot/example下见到主机/host/example中的文件了。