以 docker 安裝設定 Zephyr 編譯環境 for ESP32

Posted by 每特17劃 on 2024-07-08

以 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_dockerchmod +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

執行完成後,預計會得到畫面如下:

Pasted image 20240711021410.png

此時,Zephyr 的編譯環境已經安裝好了。

Step 4.

於 container 環境內,進一步下指令:

cd ~/zephyrproject/zephyr/
west build -p always -b esp32c3_devkitm samples/hello_world/

預期會得到結果如下:

Pasted image 20240716004131.png

而 build 出來的 image 檔案,預計會在 ~/zephyrproject/zephyr/build/zephyr/zephyr.bin 的位置。

Step 5.

之後再刷到目標硬體上,指令如下:

cd ~/zephyrproject/zephyr/build/
west flash -d . --skip-rebuild

預期結果如下:

Pasted image 20240716004619.png

之後再透過 picocom 工具觀看目標硬體的 console logs 即可,例如:

picocom -b 115200 /dev/ttyUSB0

參考 demo 如下:

Pasted image 20240716005155.png

至此,就完成整個流程了。

後續的應用程式開發,只要將 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
  • 內建預裝需要的 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;"