以 docker 安裝設定 Zephyr 編譯環境 for ESP32
因作 Bluetooth thermometer + esp32 hacking 之故,加上 pepe2k 也推薦這個,便決定試試 Zephyr 。後來克服過程中一些障礙,也順利試出來了。在此整理分享一二。
首次接觸的人,推薦先從官方提供的 Getting Started Guide 文件入手。依文件一一操作完成編譯並運作 sample demo 的流程,而對 Zephyr 的運作有了具體的感覺之後,再進入編寫應用程式的階段即可。Zephyr 附有許多結構良好的範例程式(見 zephyr/samples ),在撰寫應用程式時,有許多可以參考對照的地方,幫助很大。
在 Linux 的部份,Zephyr 文件是以 Ubuntu 為準。對於非 Ubuntu/Debian 的 Linux 用戶,若沒把握自行搞定中間差異的話,建議直接使用 docker
另開一個 Ubuntu container 來操作。
不過,開 container 雖然方便,但也會面臨幾個基本問題:
- Ubuntu container 內的
UID
和 Host OS 的UID
不一致 - 於 Ubuntu container 內能否存取 Host OS 的
/dev/ttyUSB0
並刷 firmware
所幸摸索後,都一一找到方法解決了。後面會逐一解說。這過程我於下方整理成一份 script ,提供一個直達車的捷徑,方便有需要的人更容易獲取使用。
Step-by-step hello world
需要預先準備的事項有:
- Linux 系統
docker
- 17 GB 的硬碟空間
- 預留 30~40 分鐘的時間,主要等 Zephyr git repos 跟 SDK tarball 的下載
- 目標硬體,例如: ESP32-C3
初入門就先從建立環境並編譯試跑 samples/hello_world
開始。
Step 1.
先建一個專案目錄,例如 ~/my_zephyr_test/
mkdir -p ~/my_zephyr_test/
Step 2.
於該目錄內,建立一 script 檔案 run_zephyr_docker
並 chmod +x
給予執行權限。script 檔案內容如下:
#!/usr/bin/env bash
function run_as_root() {
cd /home/ubuntu/zephyrproject || exit
# Guest OS' UID might be different with Host OS' UID
# Here we update the UID(default is 1000) to the Host OS' UID first before setup
username="ubuntu"
UID_OLD=$(id --user $username)
UID_NEW=$(stat -c %u .)
if [ -n "$UID_NEW" -a "$UID_OLD" != "$UID_NEW" ]; then
groupmod -g $UID_NEW $username
usermod -u $UID_NEW $username
find / \( -path "/proc" -o -path "/dev" \) -prune -group $UID_OLD -exec chgrp -v -h $UID_NEW {} \;
find / \( -path "/proc" -o -path "/dev" \) -prune -user $UID_OLD -exec chown -v -h $UID_NEW {} \;
fi
# Modify Guest OS' uucp GID and add default user to uucp group
# for permission of accessing /dev/ttyUSB0
UUCP_GID=$(getent group uucp | cut -d: -f3)
HOSTOS_UUCP_GID=$(stat -c %g /dev/ttyS0)
if [ -n "$UUCP_GID" -a "$UUCP_GID" != "$HOSTOS_UUCP_GID" ]; then
groupmod -g $HOSTOS_UUCP_GID uucp
fi
usermod -aG uucp $username
# Install and configure tzdata first to bypass the waiting prompt
apt update
echo "tzdata tzdata/Areas select Asia" | debconf-set-selections
echo "tzdata tzdata/Zones/Asia select Taipei" | debconf-set-selections
DEBIAN_FRONTEND=noninteractive apt install -y tzdata
# Preinstall required tools
apt install -y sudo picocom expect
echo "$username ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/$username
sudo -i -u $username run_zephyr_docker user
}
# Ref: https://docs.zephyrproject.org/latest/develop/getting_started/index.html#get-zephyr-and-install-python-dependencies
function get_zephyr_and_install_python_dependencies() {
sudo env DEBIAN_FRONTEND=noninteractive apt install -y python3-venv
python3 -m venv ~/zephyrproject/.venv
source ~/zephyrproject/.venv/bin/activate
sudo apt install -y --no-install-recommends git cmake ninja-build gperf \
ccache dfu-util device-tree-compiler wget \
python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
make gcc gcc-multilib g++-multilib libsdl2-dev libmagic1
pip install west
west init ~/zephyrproject
cd ~/zephyrproject
west update || west update || west update # retry 3 times
west zephyr-export
pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt
}
# Ref: https://docs.zephyrproject.org/latest/develop/getting_started/index.html#install-the-zephyr-sdk
function install_the_zephyr_sdk() {
(
cd ~/zephyrproject
wget --continue https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.8/zephyr-sdk-0.16.8_linux-x86_64.tar.xz
tar xf zephyr-sdk-0.16.8_linux-x86_64.tar.xz
cd zephyr-sdk-0.16.8
expect -c "spawn ./setup.sh; expect -re \".*Install host tools.*\"; send \"y\r\n\"; expect -re \".*Register Zephyr SDK CMake package.*\"; send \"y\r\n\"; interact;"
)
}
function install_esp32_dependencies() {
west blobs fetch hal_espressif || west blobs fetch hal_espressif || west blobs fetch hal_espressif
}
function setup_shortcut() {
cat >>~/.bash_aliases <<EOD
alias lls='ls --time-style=full-iso -l'
EOD
}
function run_as_user() {
get_zephyr_and_install_python_dependencies
install_the_zephyr_sdk
install_esp32_dependencies
setup_shortcut
printf "Entering shell...\n"
bash
}
case "$1" in
root)
run_as_root
;;
user)
run_as_user
;;
*)
[ -e /.dockerenv ] && {
echo "Already in docker, skip."
break
}
docker run -it --rm \
--privileged -v /dev:/dev \
-v $(pwd):/home/ubuntu/zephyrproject/ \
-v $(readlink -f $0):/usr/local/bin/run_zephyr_docker \
ubuntu:24.04 \
run_zephyr_docker root
;;
esac
備註:
docker
的選項說明:--rm
, 於終止後刪除該 container ,避免佔空間--priviledge -v /dev:/dev
, 用於 flash firmware 時存取 Host OS 的/dev/ttyUSB0
Step 3.
之後進到該目錄,並執行指令如下:
cd ~/my_zephyr_test/
./run_zephyr_docker
執行完成後,預計會得到畫面如下:
此時,Zephyr 的編譯環境已經安裝好了。
Step 4.
於 container 環境內,進一步下指令:
cd ~/zephyrproject/zephyr/
west build -p always -b esp32c3_devkitm samples/hello_world/
預期會得到結果如下:
而 build 出來的 image 檔案,預計會在 ~/zephyrproject/zephyr/build/zephyr/zephyr.bin
的位置。
Step 5.
之後再刷到目標硬體上,指令如下:
cd ~/zephyrproject/zephyr/build/
west flash -d . --skip-rebuild
預期結果如下:
之後再透過 picocom
工具觀看目標硬體的 console logs 即可,例如:
picocom -b 115200 /dev/ttyUSB0
參考 demo 如下:
至此,就完成整個流程了。
後續的應用程式開發,只要將 samples/hello_world
代換成要開發的目標程式即可。
Reinstall
若要重來一次的話,要留意前一次執行留下的檔案,會影響後一次的執行。因此需要先清理目錄如下:
(注意!若 zephyr/
有修改的部分,記得要先備份。)
rm -rf bootloader/ modules/ tools/ .venv/ .west/ zephyr-sdk-0.16.8/
rm -rf zephyr/
何不用 zephyr-build
docker image?
在弄完 script 之後,我才發現到 zephyr-build 的存在。
實際試用後,觀察如下:
- 主要文件為: zephyrproject-rtos/docker-image
- 官方文件網站 Zephyr Project Documentation 似乎沒有對應的文件
- 內建預裝需要的 Python 套件以及 SDK
- 沒有初始化 zephyr workplace , 要自行 init
- 有 VNC server
- 一樣有 Host OS 的 UID/GID 不一致問題,以及
/dev/ttyUSB0
存取問題 - 由 CI/CD 的 container image 延伸而來
- 其 container image 約佔 14GB 上下
因 zephyr-build
未在官方文件網站中提及,未來也可能會隨 CI/CD 而需求而變化。比較適合已經熟悉 Zephyr 用法的開發者,對於初學者可能還是建議以 Getting Started Guide 為主。
為方便一鍵執行使用,也比照整理了一份 script 如下。
於 ~/my_zephyr_test/
目錄裡建立一個 run_zephyr-build
的 script 檔案如下:
#!/usr/bin/env bash
TMPF=$(mktemp)
cat >$TMPF <<'EOD'
# Generate executable file to run as root
cat > /tmp/run_cmd_root <<EODD
#!/usr/bin/env bash
USERNAME=$(whoami)
UID_OLD=$(id -u)
UID_NEW=$(stat -c %u $(readlink -f $0))
usermod -u \$UID_NEW \$USERNAME
groupmod -g \$UID_NEW \$USERNAME
find / \( -path "/proc" -o -path "/dev" \) -prune -group \$UID_OLD -exec chgrp -v -h \$UID_NEW {} \;
find / \( -path "/proc" -o -path "/dev" \) -prune -user \$UID_OLD -exec chown -v -h \$UID_NEW {} \;
# Modify Guest OS' uucp GID and add default user to uucp group
# for permission of accessing /dev/ttyUSB0
GID_UUCP=$(stat -c %g /dev/ttyS0)
groupmod -g \$GID_UUCP uucp
usermod -aG uucp \$USERNAME
PWD=/workdir sudo --preserve-env=PWD -u \$USERNAME bash
EODD
chmod +x /tmp/run_cmd_root
sudo -u root /tmp/run_cmd_root
EOD
chmod a+rxx $TMPF
docker run -ti --rm -v $(pwd):/workdir \
--privileged -v /dev:/dev \
-v $TMPF:/usr/local/bin/run_cmd \
ghcr.io/zephyrproject-rtos/zephyr-build:main \
run_cmd
rm $TMPF
並於 ~/my_zephyr_test/
的目錄中執行即可。
問題解說
與 Host OS 的 UID 不同
在 Ubuntu container 裡,預設的使用者 ubuntu
的 UID 是 1000
。可能和 Host OS 的 Linux 的 UID 不一致,那掛載進去的檔案就會遇到讀寫權限的問題。
解決辦法是在 container 啟動之初, 就先變更 ubuntu
的 UID/GID 到和 Host OS 一致。具體指令如下:
UID_NEW=$(stat -c %u .)
groupmod -g $UID_NEW ubuntu
usermod -u $UID_NEW ubuntu
find / \( -path "/proc" -o -path "/dev" \) -prune -group 1000 -exec chgrp -v -h $UID_NEW {} \;
find / \( -path "/proc" -o -path "/dev" \) -prune -user 1000 -exec chown -v -h $UID_NEW {} \;
存取 /dev/ttyUSB0 刷 firmware
在 Ubuntu ccontainer 裡要順利用 west flash
刷 firmware。大致需要兩項設定。
其一, docker run
指令需要加上一些選項允許 container 存取 Host OS 的裝置。例如 --device=/dev/ttyUSB0
或 --privileged -v /dev:/dev
。
註: --privileged -v /dev:/dev
的用法比較不安全,但卻是成功率比較高的作法。因為此處是單人本機使用,所以故採用之。進一步的探討,可參考 Stack Overflow 上的討論串: Docker - a way to give access to a host USB or serial device?
其二, 需要將使用者 ubuntu
加到 uucp
的群組裡。具體指令如下:
usermod -aG uucp ubuntu
另外,要注意的是, Host OS 的 uucp
GID 跟 container 裡的 uucp
GID 可能不同。若不同時,會需要將 container 裡的 GID 修改和 Host OS 一致。
groupmod -g $HostOS_GID_UUCP uucp
apt install 的 timezone 設定 prompt
在 Ubuntu container 第一次執行 apt install
時, 程序會停下來等使用者輸入選擇時區,而阻礙了全自動進行。
解法之一是利用 debconf-set-selections
的機制,先餵指定時區供 apt
取用,參考指令如下:
echo "tzdata tzdata/Areas select Asia" | debconf-set-selections
echo "tzdata tzdata/Zones/Asia select Taipei" | debconf-set-selections
DEBIAN_FRONTEND=noninteractive apt install -y tzdata
安裝 SDK 時的 prompt
會遇到 prompt 而停下來等待用戶 yes/no 的輸入,而阻礙自動執行。
解法是用 expect
來自動應答,指令如下:
expect -c "spawn ./setup.sh; expect -re \".*Install host tools.*\"; send \"y\r\n\"; expect -re \".*Register Zephyr SDK CMake package.*\"; send \"y\r\n\"; interact;"