[Перевод] Экстремальная оптимизация скорости загрузки Raspberry Pi
Некоторое время назад был создан проект SolarCamPi — автономная камера на солнечных батареях с Wi‑Fi.
В этом проекте Raspberry Pi Zero 2 W загружается в Linux, делает снимок, подключается к Wi‑Fi и затем выключается (для экономии энергии). Цикл повторяется каждые несколько минут, чтобы постоянно отправлять актуальные изображения в облачный сервис.
Каждая секунда работы Pi Zero расходует ценную электроэнергию — ресурс, который постоянно находится в дефиците у устройств на солнечных батареях (по крайней мере, зимой в Западной Европе). Пользовательский софт (подключение к серверу, загрузка изображений и т. д.) уже был оптимизирован по максимуму. Электроника также была специально разработана для минимального потребления энергии в спящем режиме.
Существует два возможных способа дальнейшего снижения общего энергопотребления:
Уменьшить потребление энергии / тока.
Сократить время работы.
Однако в некоторых ситуациях необходимо найти баланс между этими двумя способами. Например, отключение буста CPU ради уменьшения электропотребления может привести к увеличению времени работы, что нивелирует влияние на энергопотребление.
Подготовка оборудования
Короткий цикл внесения и проверки изменений критически важен при оптимизации процессов загрузки встроенных систем. Замена SD‑карт, работа с карт‑ридерами и источниками питания во время работы отвлекает и раздражает.
Для облегчения этого процесса есть несколько полезных инструментов:
Power Profiler Kit
Power Profiler Kit II (PPK) может питать тестируемое устройство (DUT) и точно измерять его энергопотребление. Вы можете включать и выключать DUT, отслеживать потребление энергии в любой момент, а также видеть статус 8 цифровых входов. Мы подключим один из цифровых входов к GPIO‑пину на Raspberry Pi.
Таким образом, первым действием нашего приложения (то есть финишной чертой) будет переключение GPIO‑пина. Нам останется лишь измерить время между включением питания и переключением GPIO.
USB-SD-Mux
USB‑SD‑Mux — это очень полезный инструмент для хардварщиков. Он представляет собой переходник между microSD‑картой и тестируемым устройством (DUT) с интерфейсом USB‑C. Компьютер может «забрать» microSD‑карту у DUT, переписать её содержимое, а затем снова подключить карту обратно к DUT, не прикасаясь к устройству.
Это значительно упрощает и ускоряет процесс тестирования изменений, исключая необходимость извлекать карту, вставлять её в карт‑ридер, прошивать, а затем снова подключать карту к DUT и т. д. Кроме того, его можно использовать для автоматизации сброса или включения питания DUT с помощью встроенных GPIO.
Адаптер USB-UART
Также нам потребуется какой‑то вариант интерфейса UART. Вносимые нами изменения в какой то момент могут нарушить загрузку системы, подключение WiFi и т. д., и без консоли UART мы будем действовать вслепую. Стандартные адаптеры, такие как CP2102, FTDI и т. п., отлично подойдут для этой задачи.
Подготовка тестов
Чистый образ Debian 12 Lite для arm64, единственное изменение — в файл /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’s Power Profiler показано потребление тока Raspberry Pi (на 5 В) во время загрузки. Примерно через 12 секунд напряжение на цифровом входе 0 меняется на низкое, что свидетельствует о завершении работы скрипта init.sh
.
В общей сложности было потрачено 1,90 кулона (кулон и ампер‑секунда эквивалентны). Расчёт 1,9 А·с * 5,0 В даёт 9,5 Вт·с энергии, использованной в процессе загрузки.
Для справки: одна щелочная АА батарейка может предоставить около 13 500 Вт·с энергии.
Уменьшение потребления тока
Давайте займёмся самой простой частью и попробуем как можно сильнее уменьшить потребление тока.
Отключение HDMI
Мы можем полностью отключить HDMI‑encoder. Отключить GPU невозможно, так как он используется для декодирования данных с камеры. Если для работы вашего ПО GPU не требуется, то вы можете попробовать его отключить. Это снизит потребление тока с 136,7 мА до 122,6 мА (более чем на 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
Отключение индикатора активности
Просто отключив индикатор активности, мы можем сэкономить 2 мА (с 122,6 мА до 120,6 мА).
dtparam=act_led_trigger=none
dtparam=act_led_activelow=on
Отключение индикатора камеры
Повторим то же самое для индикатора камеры (если он присутствует). Это также уменьшит вероятность того, что свет от индикатора будет отражаться в изображении.
disable_camera_led=1
Настройка буста CPU
Как уже упоминалось ранее, экономия энергии из‑за отключения буста CPU может быть нивелирована возможным увеличением времени запуска.
С описанными выше изменениями и включенным бустом CPU Pi загружается, потребляя 1,62 А·с.
force_turbo=0
initial_turbo=10
arm_boost=0
Если буст CPU отключить, то потребление уменьшается до 1.58А·с:
По неизвестной причине отключение буста CPU также изменяет начальное состояние GPIO4 (поэтому я изменил полярность в init.sh
).
Снижение времени загрузки
Снижение потребления тока на ~13% — это, конечно, хорошо, но до идеала ещё далеко.
Pi требуется 8 секунд (при потреблении около 1 А), прежде чем на консоли появится первая строка вывода Linux. К счастью, есть несколько способов получить больше информации об этих 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
Сделайте резервную копию оригинального файла 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
Процесс загрузки также попытается обнаружить EEPROM на HAT‑устройствах, определить PoE (который требует вентилятора) и так далее. Мы можем спокойно отключить эти функции:
# 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 секунды. Мы можем полностью отключить initramfs
, удалив параметр auto_initramfs=1
.
Экономия зависит от размера initramfs
, в нашем случае это сократило время до 4,47 секунды.
Протестировано, но не влияет на скорость загрузки
В интернете часто рекомендуют разогнать SD‑периферию до 100 МГц, но в нашем случае это не дало какого‑либо прироста в скорости загрузки.
# not recommended! data corruption risk!
dtoverlay=sdtweak,overclock_50=100
Работа с SD‑периферией на таких высоких скоростях также несёт риск повреждения данных (при операциях записи), что крайне нежелательно для удалённых IoT‑устройств.
Загрузка ядра
На данном этапе одной из самых медленных операций является загрузка ядра:
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)
Загрузка 9 276 375 байт занимает около 1,54 секунды, что соответствует скорости передачи около 6 МиБ/с. Эта загрузка выполняется GPU (!) с помощью встроенного проприетарного процессора VideoCore IV. Возможно, код загрузчика просто неэффективен и медлителен либо использует очень консервативные настройки. К сожалению, мы не знаем, как он устроен, и не можем изменять его параметры путём взаимодействия с регистрами или каким‑либо другим способом.
Я пока не нашёл хорошего способа оптимизировать загрузку, поэтому потребуется уменьшить само ядро.
Теоретически можно разогнать процессорное ядро 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 всё равно отключен)
Без расширенных сетевых функций (туннели, мосты, файрволы и т. д.)
Без сжатия
Модули не сжаты
В тестах оказалось, что использование несжатого ядра и модулей приводит к положительному эффекту с точки зрения энергопотребления (даже если больше времени тратится на загрузку ядра GPU). Декомпрессия Gzip требует много энергии (и включает ещё один шаг перемещения).
Также была отключена функция безопасности под названием KASLR. KASLR случайным образом изменяет адрес загрузки ядра в памяти, что усложняет написание эксплойт‑кода (поскольку местоположение ядра в памяти неизвестно). Это требует переноса ядра после его загрузки GPU.
В нашем случае поверхность атаки сети очень ограничена, поэтому KASLR можно отключить (все приложения всё равно работают с правами root). Защита от уязвимостей спекулятивного выполнения, таких как Spectre, также отключена.
Получившееся ядро имеет размер 8,5 МиБ (несжатое), 4,1 МиБ после сжатия Gzip (которое здесь не используется, приведено просто для сравнения).
Оригинальное ядро Raspbian было 25 МиБ (несжатое), 8,9 МиБ после сжатия Gzip.
Итоговый результат
Теперь мы можем загрузиться в программу пользовательского пространства Linux менее чем за 3,5 секунды! Примерно 400 мс тратится в ядре Linux (разница между пином 0 и пином 1).
Общее потребление энергии составляет 0,364 Ас * 5,0 В = 1,82 Вт·с. Мы уменьшили потребление энергии в 5 раз (по сравнению со стандартным Debian, где оно составляло 9,5 Вт·с до пользовательского пространства).
Уменьшение входного напряжения
После публикации этой статьи Грэм Сазерленд / Polynomial отметил, что регуляторы питания на Pi Zero не очень эффективны при входном напряжении в 5,0 В. Это не всегда будет подходящим решением, но в нашем тестовом сценарии и также в готовом продукте мы можем просто снизить входное напряжение до 4,0 В.
При 5,0 В:
Обратите внимание на используемые единицы измерения. МиллиКулоны (mC) увеличиваются при переходе на 4,0 В (из‑за большего тока), но потребление энергии значительно уменьшается!
350,94 мАс * 5,0 В = 1,754 Вт·с
При 4,0 В:
390,77 мАс * 4,0 В = 1,563 Вт·с
Попробуем снизить напряжение ещё больше:
При 3,6 В:
399,60 мАс * 3,6 В = 1,438 Вт·с
Мы только что снизили потребление энергии ещё на 20%, просто настроив импульсные стабилизаторы напряжения! Конечно, это требует дальнейшего тестирования на стабильность и надёжность (так как это технически вне спецификаций), но результат очень впечатляющий.