不久前,我们构建了一个名为 SolarCamPi 的项目,这是一个离网太阳能供电的WiFi摄像头。
SolarCamPi:https://kittenlabs.de/solarcampi/
在这个项目中,我们使用了Raspberry Pi Zero 2 W,它在启动时进入Linux系统,拍摄一张照片,建立WiFi连接,然后再次关闭(以节省电量)。这个过程每几分钟重复一次,以便持续向云服务发送最新图像
Raspberry Pi Zero 每开机一秒钟都会消耗宝贵的电力,这对于太阳能供电的设备来说是一种稀缺资源(至少在西欧的冬天是这样......)。用户空间的应用程序(服务器连接、图片上传等)已经尽可能进行了优化。电子设备的设置也特意设计为在休眠时尽可能少地消耗电力。
有两种方法可以进一步降低总能耗:
1.降低功耗/电流
2.减少运行时间
然而,在某些情况下,需要在两者之间找到平衡。例如,仅仅为了节省一些电流而禁用CPU涡轮加速并不是一个好主意,因为由此产生的额外时间将消耗比快速完成任务并关闭更多的能量。我们想要的是电流与时间图下的面积尽可能小。
硬件设置
在优化嵌入式启动过程时,能够在做出更改后迅速看到效果至关重要。在工作中频繁更换SD卡、摆弄读卡器和电源供应器既分散注意力又令人厌烦。
为了避免这种情况,存在一些有用的工具:
1.Nordic Power Profiler 套件 II
https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2?lang=zh-CN
2.USB-SD-Mux Fast
https://github.com/linux-automation/usbsdmux
3.USB-UART转换器
Power Profiler 套件
Power Profiler Kit II(现在称为PPK)可以为被测设备(DUT)供电,并随时间准确测量其功耗。您可以启用/禁用DUT,查看任何时间点的功耗,以及查看8个数字输入的状态!我们将其中一个数字输入连接到Raspberry Pi的GPIO引脚上。
这样,“我们的应用程序”的第一个动作(即终点线)将是切换GPIO引脚。然后,我们只需测量从开机到GPIO切换之间的时间。
USB-SD-多路复用器
USB-SD-Mux是硬件黑客们非常有用的工具,它是microSD卡和带有USB-C接口的DUT之间的转换器。计算机可以从DUT“窃取”microSD卡,重写其内容,然后将microSD卡插回DUT,而无需触摸设备。
这大大简化了测试更改的工作流程,避免了拔下卡片、将其插入microSD读卡器、刷新、将卡片插回DUT等繁琐步骤。它甚至可以使用板载GPIO来自动重置或供电DUT。
USB-UART转换器
几乎需要某种形式的UART接口。这些更改将在某个时刻破坏系统启动、WiFi连接等,而如果没有UART控制台,我们将无法看到发生了什么。标准的CP2102、FTDI等转换器都能很好地工作。
测量/测试设置
在干净的Debian 12(bookworm)arm64 Lite映像上,修改了/boot/firmware/cmdline.txt 文件以包含 init=/init.sh。这意味着内核将在用户空间的第一件事就是执行/init.sh 脚本(在运行systemd或任何其他内容之前)。
这样的 init.sh 脚本可能如下所示:
#!/bin/bash
gpioset 0 4=0
sleep 1
gpioset 0 4=1
sleep 1
gpioset 0 4=0
exec /sbin/init
这将切换GPIO4,然后用/sbin/init(即systemd)替换自己以恢复正常启动。
在Nordic的Power Profiler软件中,您可以看到Raspberry Pi在启动过程中的电流消耗(以5V计算)。大约12秒后,数字输入0变为低电平,表明我们的 init.sh 已执行。
在此过程中,总共使用了1.90库仑(库仑和安培秒是等价的)的电量。计算1.9As * 5.0V得出此启动过程的能耗为9.5Ws。
作为参考:一节AA碱性电池可以提供约13500Ws的能量。
降低电流
首先,我们来做简单的事情,尽可能降低工作电流。
禁用HDMI
我们可以完全禁用HDMI编码器。由于我们需要GPU来编码摄像头数据,因此无法禁用GPU。如果您的应用程序不需要摄像头/GPU支持,请尝试完全禁用GPU。
这可以将电流消耗从136.7mA降低到122.6mA(超过10%!)。
相关的config.txt参数:
# disable HDMI (saves power)
dtoverlay=vc4-kms-v3d,nohdmi
max_framebuffers=1
disable_fw_kms_setup=1
disable_overscan=1
# disable composite video output
enable_tvout=0
禁用活动LED
仅通过禁用活动LED,我们就可以节省2mA(从122.6mA降低到120.6mA)。
dtparam=act_led_trigger=none
dtparam=act_led_activelow=on
禁用摄像头LED
对摄像头LED重复相同的操作(如果存在)。这还将减少LED反射回图像的机会。
disable_camera_led=1
涡轮调整
如前所述,在浪费时间的同时节省电流可能并不理想。
在当前的更改下,Pi可以在使用1.62As的情况下启动。
force_turbo=0
initial_turbo=10
arm_boost=0
在没有强制涡轮模式的情况下,使用了1.58As:
出于某种未知原因,禁用涡轮/增强模式也会反转GPIO4的默认状态(因此我在 init.sh 中切换了极性)。
减少时间
电流降低了约13%,这很有帮助,但仍有很长的路要走。
Pi在出现Linux控制台上的第一行输出之前需要8秒钟(同时消耗约1As)。
幸运的是,有多种方法可以获取有关这8秒钟的更多信息。
调试启动
在Raspberry Pi家族的启动过程中,GPU首先初始化。
它与SD卡通信并查找bootcode.bin文件(Pi 4及更新版本使用EEPROM代替)。
我们可以修改此bootcode.bin以启用详细的UART日志记录:
sed -i -e "s/BOOT_UART=0/BOOT_UART=1/" /boot/firmware/bootcode.bin
UART日志记录:https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#bootcode-bin-uart-enable
首先备份原始的bootcode.bin,因为此过程可能是破坏性的。
使用启用的BOOT_UART重启后,我们会获得大量有用的信息:
Raspberry Pi Bootcode
Found SD card, config.txt = 1, start.elf = 1, recovery.elf = 0, timeout = 0
Read File: config.txt, 1322 (bytes)
Raspberry Pi Bootcode
Read File: config.txt, 1322
Read File: start.elf, 2981376 (bytes)
Read File: fixup.dat, 7303 (bytes)
MESS:00:00:01.295242:0: brfs: File read: /mfs/sd/config.txt
MESS:00:00:01.300131:0: brfs: File read: 1322 bytes
MESS:00:00:01.335680:0: HDMI0:EDID error reading EDID block 0 attempt 0
[..]
MESS:00:00:01.392537:0: HDMI0:EDID error reading EDID block 0 attempt 9
MESS:00:00:01.398632:0: HDMI0:EDID giving up on reading EDID block 0
MESS:00:00:01.406335:0: brfs: File read: /mfs/sd/config.txt
MESS:00:00:01.411272:0: gpioman: gpioman_get_pin_num: pin LEDS_PWR_OK not defined
MESS:00:00:01.918176:0: gpioman: gpioman_get_pin_num: pin LEDS_PWR_OK not defined
MESS:00:00:01.923999:0: *** Restart logging
MESS:00:00:01.927872:0: brfs: File read: 1322 bytes
MESS:00:00:01.933328:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 0
[..]
MESS:00:00:01.995436:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 9
MESS:00:00:02.002052:0: hdmi: HDMI0:EDID giving up on reading EDID block 0
MESS:00:00:02.007955:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 0
[..]
MESS:00:00:02.070610:0: hdmi: HDMI0:EDID error reading EDID block 0 attempt 9
MESS:00:00:02.077225:0: hdmi: HDMI0:EDID giving up on reading EDID block 0
MESS:00:00:02.082840:0: hdmi: HDMI:hdmi_get_state is deprecated, use hdmi_get_display_state instead
MESS:00:00:02.091586:0: HDMI0: hdmi_pixel_encoding: 162000000
MESS:00:00:02.799203:0: brfs: File read: /mfs/sd/initramfs8
MESS:00:00:02.803082:0: Loaded 'initramfs8' to 0x0 size 0xb0898e
MESS:00:00:02.821799:0: initramfs loaded to 0x1b4e7000 (size 0xb0898e)
MESS:00:00:02.836318:0: dtb_file 'bcm2710-rpi-zero-2-w.dtb'
MESS:00:00:02.840194:0: brfs: File read: 11569550 bytes
MESS:00:00:02.849171:0: brfs: File read: /mfs/sd/bcm2710-rpi-zero-2-w.dtb
MESS:00:00:02.854262:0: Loaded 'bcm2710-rpi-zero-2-w.dtb' to 0x100 size 0x8258
MESS:00:00:02.876038:0: brfs: File read: 33368 bytes
MESS:00:00:02.892755:0: brfs: File read: /mfs/sd/overlays/overlay_map.dtb
MESS:00:00:02.927145:0: brfs: File read: 5255 bytes
MESS:00:00:02.933541:0: brfs: File read: /mfs/sd/config.txt
MESS:00:00:02.937568:0: dtparam: audio=on
MESS:00:00:02.948005:0: brfs: File read: 1322 bytes
MESS:00:00:02.971952:0: brfs: File read: /mfs/sd/overlays/vc4-kms-v3d.dtbo
MESS:00:00:03.023016:0: Loaded overlay 'vc4-kms-v3d'
MESS:00:00:03.026278:0: dtparam: nohdmi=true
MESS:00:00:03.031105:0: dtparam: act_led_trigger=none
MESS:00:00:03.048180:0: dtparam: act_led_activelow=on
MESS:00:00:03.149316:0: brfs: File read: 2760 bytes
MESS:00:00:03.154502:0: brfs: File read: /mfs/sd/cmdline.txt
MESS:00:00:03.158504:0: Read command line from file 'cmdline.txt':
MESS:00:00:03.164369:0: 'console=serial0,115200 console=tty1 root=PARTUUID=26bbce6b-02 rootfstype=ext4 fsck.repair=yes rootwait cfg80211.ieee80211_regdom=DE init=/init.sh'
MESS:00:00:03.195926:0: gpioman: gpioman_get_pin_num: pin EMMC_ENABLE not defined
MESS:00:00:03.269361:0: brfs: File read: 146 bytes
MESS:00:00:03.812401:0: brfs: File read: /mfs/sd/kernel8.img
MESS:00:00:03.816343:0: Loaded 'kernel8.img' to 0x200000 size 0x8d8bd7
MESS:00:00:05.364579:0: Device tree loaded to 0x1b4de900 (size 0x8605)
MESS:00:00:05.370571:0: uart: Set PL011 baud rate to 103448.300000 Hz
MESS:00:00:05.377080:0: uart: Baud rate change done...
MESS:00:00:05.380495:0: uart: Baud rate[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034]
禁用HDMI探测
在启动过程中,引导加载程序会花费大量时间尝试自动检测可能连接的HDMI显示器的视频参数。然而,由于我们没有HDMI(而且它已经被禁用了),因此等待I2C响应以获取EDID(包括分辨率、帧率等信息)信息并不明智。
通过简单地硬编码一个EDID字符串,我们可以禁用任何探测:
# don't try to read HDMI eeprom
hdmi_blanking=2
hdmi_ignore_edid=0xa5000080
hdmi_ignore_cec_init=1
hdmi_ignore_cec=1
禁用HAT、PoE和LCD探测
启动过程还会尝试检测HAT上的I2C EEPROM,尝试检测需要风扇的PoE HAT以及其他一些内容。我们可以安全地禁用这些探测:
# all these options cause a wait for an I2C bus response, we don't need any of them, so let's disable them.
force_eeprom_read=0
disable_poe_fan=1
ignore_lcd=1
disable_touchscreen=1
disable_fw_kms_setup=1
禁用摄像头和显示器探测
探测连接的MIPI摄像头或显示器也会花费一些时间。我们知道连接了哪个摄像头(在这个案例中是HQ Camera,IMX477),因此我们可以硬编码这个信息:
# no autodetection for anything (will wait for I2C answers)
camera_auto_detect=0
display_auto_detect=0
# load HQ camera IMX477 sensor manually
dtoverlay=imx477
禁用 initramfs
上述更改将(自报告的)启动时间从5.38秒缩短到4.75秒。我们可以通过移除auto_initramfs=1来完全禁用initramfs,这取决于initramfs的大小,但可以将启动时间缩短到4.47秒。
经过测试,没有显著差异
尽管网上经常推荐将SD外设超频到100 MHz,但这在启动性能上并没有产生可测量的差异
# not recommended! data corruption risk!
dtoverlay=sdtweak,overclock_50=100
而且,在高速下操作SD外设还存在数据损坏的风险(在写入访问时),这在远程物联网设备中是非常不希望的。
内核加载
此时,加载内核是最慢的操作之一。
MESS:00:00:03.816343:0: Loaded 'kernel8.img' to 0x200000 size 0x8d8bd7
MESS:00:00:05.364579:0: Device tree loaded to 0x1b4de900 (size 0x8605)
加载9276375字节大约需要1.54秒,即大约6 MiB/s的传输速度
内核加载由GPU(使用其内部的VideoCoreIV处理器)完成,这可能是加载代码效率低下或使用了非常保守的设置。由于这是一个黑盒,我们无法直接操作寄存器或修改参数。
理论上,GPU处理器内核超频是可行的
# Overclock GPU VideoCore IV processor (not recommended!)
core_freq_min=500
core_freq=550
这确实减少了20%的内核加载时间。但是带来了未知的副作用(可靠性等。)
Buildroot/自定义内核
是时候将系统从Raspbian/Debian迁移到自定义构建的Buildroot发行版了(特别是为了获取自定义内核)。
使用 buildroot 2024.02.1,我们配置了一个非常精简的系统。
原生的 aarch64 工具链,仍然使用完整的 glibc 和 Raspberry Pi 用户区工具(如相机实用程序)。
内核已配置:
- 无声音支持
- 无大多数块设备和文件系统驱动(除了SD/MMC和ext4)
- 无RAID支持
- 无USB支持
- 无HID支持
- 无DVB支持
- 无视频和帧缓冲支持(HDMI已被禁用)
- 无高级网络功能(隧道、桥接、防火墙等)
- 未压缩(不使用Gzip)
- 模块未压缩(不使用Gzip)
测试表明,内核和模块均未压缩可以带来正的能量结果(即使GPU加载内核时花费了更多时间)。Gzip解压缩需要消耗大量能量(并且实际上涉及另一个重定位步骤)。
一个名为KASLR的安全功能也被禁用。
KASLR将内核在内存中的加载地址随机化,使得编写漏洞利用代码更加困难(因为内核的内存位置是未知的)。这要求内核在被GPU加载后重新定位。
在我们的用例中,网络攻击面非常有限,所以可以禁用KASLR(反正所有应用软件都以root身份运行)。投机性执行漏洞(如Spectre)的缓解也被禁用。
最终的内核大小为8.5兆字节(未压缩),4.1兆字节压缩为Gzip(这里没有使用,只是为了比较)。最初的Raspbian内核是25 MiB(未压缩),8.9 MiB压缩为Gzip
最终结果
现在,我们可以在不到3.5秒的时间内启动到Linux用户空间程序!
Linux内核占用时间约为400毫秒(从引脚0到引脚1的差值)。
总能耗为 0.364 As * 5.0 V = 1.82 Ws,与原始Debian相比,能耗降低了5倍(原始Debian直到用户空间需要9.5 Ws)。
降低输入电压
在发表这篇博文后,Graham Sutherland / Polynomial 指出,Pi Zero 中的调节器在5.0V输入下效率不是很高。
Graham Sutherland / Polynomial 指出:https://chaos.social/@gsuberland/113064738117907263
这可能不适用于所有情况,但在我们的测试场景和成品中,我们可以将输入电压降至4.0V。
在5.0V下运行:
好好注意这里正在进行的单元。通过切换到4.0V(因为电流更高),mC(毫库仑/毫安培秒)增加,但是总能量显著降低!
350.94 毫秒 * 5.0V = 1.754 瓦秒
在4.0V下运行:
390.77 毫安 * 4.0V = 1.563 瓦秒
我们可以更进一步:
在3.6V运行:
399.60 毫秒 * 3.6V = 1.438 瓦秒
我们刚刚又降低了20%的能耗,这仅仅是通过在更理想的工作点操作开关模式调节器实现的!这当然需要进一步测试稳定性/可靠性(因为这在技术上是不符合规格的),但这是一个非常令人印象深刻的结果。
链接
完整的config.txt配置文件:
https://github.com/Manawyrm/SolarCamPi-Buildroot/blob/v2/buildroot/board/raspberrypi0w/config.txt
精简的内核配置文件:
完整的Buildroot树:
https://github.com/Manawyrm/SolarCamPi-Buildroot/tree/v2
原文链接: https://kittenlabs.de/blog/2024/09/01/extreme-pi-boot-optimization/