#1.本丸攻略へ: Nerves/rpi + Tensorflow lite
前回、前々回と2回に渡って、Windows10上で Elixir+TensorFlow liteの簡単なアプリPlugMnistを作成した。ソースコードから Tensorflow liteライブラリをメイクするところから、Elixir/Erlangの拡張機能として Tensorflow lite interpreterを利用するところまで見てきた訳だが[*1]、実はそれらの修行(?)は全て「Nerves/rpiで AIしてみよう」と言う密かな目的を達成する為であった
[*1]前哨戦として、Ubuntu/WSL2にも PlugMnistをポーティングしてみたが、思いのほか作業がスムーズに進み、特筆すべき事柄がなかった。orzガックシ
さあ、経験値はそれなりに溜まった。今や時は満ちた、いざ本丸(Nerves/rpi)の攻略に着手しよう。
尚、本記事は多分に個人的な備忘録となっている。それ故に、PlugMnistのプログラムには全く触れず、専らポーティング作業にフォーカスしている。悪しからず。
PlugMnistの全ファイルセットは、https://github.com/shoz-f/plug_mnist_nerves で閲覧できる。
#2.作戦上の落とし穴
組み込みシステムの開発では、大概の場合ターゲット・ボードのリソースが貧弱なため、ターゲット上でのセルフ開発を行うことが難しい。そこでクロス開発環境の下で開発を行うことになるのだが、ソフト動作確認はターゲットで行わざるを得ずココが難関となることが多い。そんな訳で玄人は、ターゲットでの動作確認がスルッとPASSするように、できるだけ実績がある枯れたコードを利用したり、ホスト・コンピュータ上で可能な限りエミュレーションしたりと作戦を廻らすのだ。
このプロジェクトも、その例に漏れず、
- MinGW64/Windowsや Ubuntu/WSL2上で PlugMnistアプリを動作させ、事前に Bug出しを行う
- 可能な限り、巷でデファクトスタンダードで品質の安定したライブラリを使用する
- 他への依存関係が少ない C++テンプレート・ベースのライブラリなどを採用する
と言った作戦を展開して来た。しかしである。いざ、本丸の Nerves/rpiを攻略しようとしたところ、ポピュラーな筈のライブラリ libjpeg.*が、Nervesツールチェイン内に見つからないではないか。まさかの落とし穴である。
libjpegは PlugMnistアプリのビルドに不可欠なので、どうやって追加するかを急遽検討することになった。
作戦の立て直しだ。勘弁してよ~~
#3.Nerves開発環境の地形図
作戦を立て直すに当たって、まず「Nerves開発環境」の全体像を復習しておこう。「与えられているものは何か? 条件は何か」である。
Nervesのツールチェイン&ライブラリ群は、ホーム・ディレクトリの下の隠しディレクトリ **"~/.nerves/artifacts"**に置かれている。その中は、ターゲットの種類や Nervesのバージョンでネーミングされた下記のようなディレクトリで管理されている。
【ツールチェイン】
Raspberry Pi A+/B/B+/Zero/ZeroW用
・nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2
Raspberry Pi 2/3用ほか
・nerves_toolchain_arm_unknown_linux_gnueabihf-linux_x86_64-1.3.2
【Nervesシステム】
Raspberry Pi A+/B/B+用
・nerves_system_rpi-portable-1.12.2
・nerves_system_rpi-portable-1.12.1
:
Raspberry Pi 3用
・nerves_system_rpi3-portable-1.12.2
:
例えば、ターゲットを"MIX_TARGET=rpi"として Nervesを mix firmware
(ビルド)すると、
(※以下断らない限り最も非力な Raspberry Pi無印をターゲットとする)
C言語で記述されたモジュールは、コンパイラ・ドライバ
nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/bin/armv6-rpi-linux-gnueabi-gcc
でコンパイルされ、デフォルト動作では
nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/lib/gcc/armv6-rpi-linux-gnueabi/9.2.0
または nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/lib/gcc
または nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/armv6-rpi-linux-gnueabi/lib
または nerves_system_rpi-portable-1.12.2/staging/lib
または nerves_system_rpi-portable-1.12.2/staging/usr/lib
に置かれているライブラリとリンクされる
と言った処理が行われる。よぅ mix & elixir_make & gcc (+裏方のシェルスクリプト: nerves-env-helper.sh)、おめぇさん達、いい仕事するじゃねえか。
"MIX_TARGET=xxx"に応じて適切なファイルセットが選択される訳だが、その仕組みはとてもシンプルに出来ている。裏方の nerves-env-helper.shがターゲット情報を環境変数にセットし、それをツールチェインの各コマンドが見ているのだ(下記)。
何かしら自前の Makefileを書く場合には、どんな情報を受け取ることができるのか、nerves-env-helper.hの中身を確認ておいた方がよいだろう。
CROSSCOMPILE=~/.nerves/artifacts/nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/bin/armv6-rpi-linux-gnueabi
NERVES_SDK_SYSROOT=~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/staging
CC=$CROSSCOMPILE-gcc
CXX=$CROSSCOMPILE-g++
CFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -pipe -O2 -I $NERVES_SDK_SYSROOT/usr/include"
CXXFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -pipe -O2 -I $NERVES_SDK_SYSROOT/usr/include"
LDFLAGS="--sysroot=$NERVES_SDK_SYSROOT"
さて、先に進む前に ~/.nerves下のディレクトリ・ツリーをざっと見ておこう。
ほとんどのアイテムはクロス・コンパイルに係わるファイルだが、nerves_system_rpi-portable-1.12.2/images下のモノはそうではない。これらは、ターゲットの起動SDを作成する際に必要となるファイルで、簡単に言うと Linux OSそのものだ。それぞれのファイルの意味は次の通り。そうそう、rootfs.squashfsは後ほど再登場する予定である。
- zImage ‥‥‥‥‥‥‥ gz圧縮された Kernelイメージ
- rootfs.squashfs ‥‥‥ 圧縮された読み込み専用の rootファイルシステム
- bcm2708-rpi-*.dtb ‥‥ デバイスツリー
[~/.nerves下のディレクトリ・ツリー(抜粋)]
.nerves
├── artifacts
│ ├── nerves_system_rpi-portable-1.12.1
│ ├── nerves_system_rpi-portable-1.12.2
│ │ ├── images
│ │ │ ├── bcm2708-rpi-b.dtb
│ │ │ ├── bcm2708-rpi-zero-w.dtb
│ │ │ ├── rootfs.squashfs
│ │ │ └── zImage
│ │ ├── nerves-env.sh
│ │ ├── scripts
│ │ │ └── nerves-env-helper.sh
│ │ └── staging
│ │ ├── bin
│ │ ├── dev
│ │ ├── etc
│ │ ├── lib
│ │ │ ├── libc-2.30.so
│ │ │ ├── libc.so.6 -> libc-2.30.so
│ │ │ ├── libdl-2.30.so
│ │ │ ├── libdl.so.2 -> libdl-2.30.so
│ │ │ ├── libgcc_s.so
│ │ │ ├── libgcc_s.so.1
│ │ │ ├── libm-2.30.so
│ │ │ ├── libm.so.6 -> libm-2.30.so
│ │ │ ├── libpthread-2.30.so
│ │ │ └── libpthread.so.0 -> libpthread-2.30.so
│ │ ├── media
│ │ ├── mnt
│ │ ├── proc
│ │ ├── root
│ │ ├── run
│ │ ├── sbin
│ │ ├── sys
│ │ ├── usr
│ │ │ ├── bin
│ │ │ │ ├── erl -> ../lib/erlang/bin/erl
│ │ │ │ ├── erlc -> ../lib/erlang/bin/erlc
│ │ │ │ ├── escript -> ../lib/erlang/bin/escript
│ │ │ │ ├── raspistill
│ │ │ │ └── vcgencmd
│ │ │ ├── include
│ │ │ │ ├── EGL
│ │ │ │ │ ├── egl.h
│ │ │ │ │ ├── eglext.h
│ │ │ │ │ ├── eglext_android.h
│ │ │ │ │ ├── eglext_brcm.h
│ │ │ │ │ ├── eglext_nvidia.h
│ │ │ │ │ └── eglplatform.h
│ │ │ │ ├── GLES2
│ │ │ │ │ ├── gl2.h
│ │ │ │ │ ├── gl2ext.h
│ │ │ │ │ └── gl2platform.h
│ │ │ │ ├── assert.h
│ │ │ │ ├── byteswap.h
│ │ │ │ ├── errno.h
│ │ │ │ ├── limits.h
│ │ │ │ ├── math.h
│ │ │ │ ├── memory.h
│ │ │ │ ├── pigpio.h
│ │ │ │ ├── poll.h
│ │ │ │ ├── pthread.h
│ │ │ │ ├── regex.h
│ │ │ │ ├── stdint.h
│ │ │ │ ├── stdio.h
│ │ │ │ ├── stdlib.h
│ │ │ │ ├── string.h
│ │ │ │ ├── sys
│ │ │ │ │ ├── ioctl.h
│ │ │ │ │ ├── mman.h
│ │ │ │ │ ├── random.h
│ │ │ │ │ ├── select.h
│ │ │ │ │ ├── socket.h
│ │ │ │ │ ├── stat.h
│ │ │ │ │ └── types.h
│ │ │ │ ├── time.h
│ │ │ │ └── zlib.h
│ │ │ ├── lib
│ │ │ │ ├── libGLESv2.so
│ │ │ │ ├── libGLESv2_static.a
│ │ │ │ ├── libmmal.so
│ │ │ │ ├── libmmal_components.so
│ │ │ │ ├── libmmal_core.so
│ │ │ │ ├── libmmal_util.so
│ │ │ │ ├── libmmal_vc_client.so
│ │ │ │ ├── libpigpio.so -> libpigpio.so.1
│ │ │ │ └── libpigpio.so.1
│ │ │ ├── man
│ │ │ ├── sbin
│ │ │ ├── share
│ │ │ └── src
│ │ └── var
│ ├── nerves_system_rpi3-portable-1.12.1
│ ├── nerves_system_rpi3-portable-1.12.2
│ ├── nerves_toolchain_arm_unknown_linux_gnueabihf-linux_x86_64-1.3.2
│ └── nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2
│ ├── armv6-rpi-linux-gnueabi
│ │ ├── bin
│ │ ├── include
│ │ │ └── c++
│ │ │ └── 9.2.0
│ │ │ ├── algorithm
│ │ │ ├── array
│ │ │ ├── cerrno
│ │ │ ├── deque
│ │ │ ├── iostream
│ │ │ ├── map
│ │ │ ├── regex
│ │ │ ├── string
│ │ │ └── vector
│ │ ├── lib
│ │ │ ├── libatomic.a -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.a
│ │ │ ├── libatomic.so -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.so
│ │ │ ├── libatomic.so.1 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.so.1
│ │ │ ├── libatomic.so.1.2.0 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libatomic.so.1.2.0
│ │ │ ├── libgcc_s.so -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libgcc_s.so
│ │ │ ├── libgcc_s.so.1 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libgcc_s.so.1
│ │ │ ├── libstdc++.a -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.a
│ │ │ ├── libstdc++.so -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so
│ │ │ ├── libstdc++.so.6 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so.6
│ │ │ ├── libstdc++.so.6.0.27 -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so.6.0.27
│ │ │ └── libstdc++.so.6.0.27-gdb.py -> ../../armv6-rpi-linux-gnueabi/sysroot/lib/libstdc++.so.6.0.27-gdb.py
│ │ └── sysroot
│ ├── bin
│ │ ├── armv6-rpi-linux-gnueabi-addr2line
│ │ ├── armv6-rpi-linux-gnueabi-ar
│ │ ├── armv6-rpi-linux-gnueabi-as
│ │ ├── armv6-rpi-linux-gnueabi-c++
│ │ ├── armv6-rpi-linux-gnueabi-c++filt
│ │ ├── armv6-rpi-linux-gnueabi-cc -> armv6-rpi-linux-gnueabi-gcc
│ │ ├── armv6-rpi-linux-gnueabi-cpp
│ │ ├── armv6-rpi-linux-gnueabi-ct-ng.config
│ │ ├── armv6-rpi-linux-gnueabi-elfedit
│ │ ├── armv6-rpi-linux-gnueabi-g++
│ │ ├── armv6-rpi-linux-gnueabi-gcc
│ │ ├── armv6-rpi-linux-gnueabi-gcc-9.2.0
│ │ ├── armv6-rpi-linux-gnueabi-gcc-ar
│ │ ├── armv6-rpi-linux-gnueabi-gcc-nm
│ │ ├── armv6-rpi-linux-gnueabi-gcc-ranlib
│ │ ├── armv6-rpi-linux-gnueabi-gcov
│ │ ├── armv6-rpi-linux-gnueabi-gcov-dump
│ │ ├── armv6-rpi-linux-gnueabi-gcov-tool
│ │ ├── armv6-rpi-linux-gnueabi-gdb
│ │ ├── armv6-rpi-linux-gnueabi-gdb-add-index
│ │ ├── armv6-rpi-linux-gnueabi-gprof
│ │ ├── armv6-rpi-linux-gnueabi-ld
│ │ ├── armv6-rpi-linux-gnueabi-ld.bfd
│ │ ├── armv6-rpi-linux-gnueabi-ldd
│ │ ├── armv6-rpi-linux-gnueabi-nm
│ │ ├── armv6-rpi-linux-gnueabi-objcopy
│ │ ├── armv6-rpi-linux-gnueabi-objdump
│ │ ├── armv6-rpi-linux-gnueabi-populate
│ │ ├── armv6-rpi-linux-gnueabi-ranlib
│ │ ├── armv6-rpi-linux-gnueabi-readelf
│ │ ├── armv6-rpi-linux-gnueabi-size
│ │ ├── armv6-rpi-linux-gnueabi-strings
│ │ └── armv6-rpi-linux-gnueabi-strip
│ ├── include
│ ├── lib
│ │ ├── gcc
│ │ │ └── armv6-rpi-linux-gnueabi
│ │ │ └── 9.2.0
│ │ │ <<抜粋>>
│ │ │ ├── crtbegin.o
│ │ │ ├── include
│ │ │ │ ├── stdarg.h
│ │ │ │ └── stddef.h
│ │ │ ├── libgcc.a
│ │ │ └── libgcov.a
│ │ └── ldscripts
│ └── share
└── dl
ここまで、クロス開発のビルド・フェーズの視点に立って「Nerves開発環境」を復習してきた。だが、もう一つ押さえておくべき事柄がある。それは、ターゲット上の実行環境すなわち rootファイルシステムがどんな構成になっているのか、そしてそれをどうやって作成しているのかを知っておく必要がある。
と言うのも、C言語等で作成したプログラムは、その実行形式ファイルの他に共有ライブラリをいくつか必要とすることがあるからだ。その場合、プログラムが参照している共有ライブラリもターゲットにインストールしてやらないと、プログラムを実行することができないのだ。つまり、クロス開発では、ビルドに必要なファイル類を開発環境に追加するだけではダメなのだ。本プロジェクトも御多分に漏れず、Tensorflow liteを実装した tfl_interpが共有ライブラリlibjpeg.soをリンクしている。そう、他人事ではない。
ではまず、ターゲットrootファイルシステムの中身を覗いて、どこにどんな変更を加える必要があるのか見当をつけよう。覗く手段は、ざっと考えただけでも下記の4つの方法がある。たぶん、これらの他にも方法があるだろう。お薦めは、一番お手軽な 1.の手段だ。
- "mix firmware.unpack"で _build/rpi_dev/nerves/images/plug_mnist.fw をアンパックする
- "unzip"で plug_mnist.fw をアンパックしたのち、rootファイルシステムのイメージを "sudo mount -t squashfs -o loop data/rootfs.img ./xxx" で適当なマウントポイントにマウントする
- "unzip"で plug_mnist.fw をアンパックしたのち、rootファイルシステムのイメージを "unsquashfs data/rootfs.img" でアンパックする
- ターゲット・ボードを起動して sshでリモートログインし、"use Toolshed"でコマンド拡張したのちに "tree ほにゃらら"でディレクトリツリーを表示する
早速ターゲットrootファイルシステムを覗いてみる。
最初に、libjpeg.soが同梱されていないことを確認しておこう。findコマンドでごにょごにょすれば簡単に確認できる。確かに libjpeg.soは見つからなかった。次に、ディレクトリ・ツリーをざっと眺めてみたところ、共有ライブラリの置き場所はスタンダードな /libまたは /usr/lib のようだと分かった。ふむ、libjpeg.soを追加する場所は /usr/libで決まりだな。
おやっ、Elixirで記述したアプリは /srv/erlang/libの下に置かれるのか。privに置いた trf_interpなどのファイルはここに格納されるってことだな。
$ mix firmware
$ mix firmware.unpack
==> nerves
/usr/bin/make -C src all
make[1]: Entering directory '/home/shoz/plug_mnist_nerves/deps/nerves/src'
cc /home/shoz/plug_mnist_nerves/_build/rpi_dev/lib/nerves/obj/port.o -o /home/shoz/plug_mnist_nerves/_build/rpi_dev/lib/nerves/priv/port
make[1]: Leaving directory '/home/shoz/plug_mnist_nerves/deps/nerves/src'
if [ -f test/fixtures/port/Makefile ]; then /usr/bin/make -C test/fixtures/port; fi
==> plug_mnist
Nerves environment
MIX_TARGET: rpi
MIX_ENV: dev
Unpacking to plug_mnist-rpi...
Archive: /home/shoz/plug_mnist_nerves/_build/rpi_dev/nerves/images/plug_mnist.fw
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/meta.conf
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bootcode.bin
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/fixup.dat
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/start.elf
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/config.txt
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/cmdline.txt
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/zImage
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bcm2708-rpi-zero-w.dtb
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bcm2708-rpi-b.dtb
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/bcm2708-rpi-b-plus.dtb
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/w1-gpio-pullup.dtbo
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/pi3-miniuart-bt.dtbo
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/ramoops.dtbo
inflating: /home/shoz/plug_mnist_nerves/plug_mnist-rpi/data/rootfs.img
Parallel unsquashfs: Using 8 processors
2067 inodes (2290 blocks) to write
[=============================================================|] 2290/2290 100%
created 1898 files
created 249 directories
created 169 symlinks
created 0 devices
created 0 fifos
$ find plug_mnist-rpi/rootfs/ -name "libjpeg*"
$
$ ls plug_mnist-rpi/rootfs/srv/erlang/lib/
asn1-5.0.13 logger-1.10.4 plug_mnist-0.1.0 stdlib-3.13
compiler-7.6.2 mdns_lite-0.6.5 plug_static_index_html-1.0.0 system_registry-0.8.2
cowboy-2.8.0 mime-1.4.0 public_key-1.8 telemetry-0.4.2
cowlib-2.9.1 muontrap-0.6.0 ranch-1.7.1 toolshed-0.2.13
crypto-4.7 nerves_pack-0.4.0 ring_logger-0.8.1 uboot_env-0.3.0
dns-2.1.2 nerves_runtime-0.11.3 runtime_tools-1.15 vintage_net-0.9.1
eex-1.10.4 nerves_ssh-0.2.1 sasl-4.0 vintage_net_direct-0.9.0
elixir-1.10.4 nerves_time-0.4.2 shoehorn-0.6.0 vintage_net_ethernet-0.9.0
gen_state_machine-2.1.0 one_dhcpd-0.2.4 socket-0.3.13 vintage_net_wifi-0.9.0
iex-1.10.4 plug-1.10.4 ssh-4.10
jason-1.2.2 plug_cowboy-2.3.0 ssh_subsystem_fwup-0.5.1
kernel-7.0 plug_crypto-1.1.2 ssl-10.0
次に、ターゲットrootファイルシステムがどのようにして作成されるのかを調べてみよう。
これは、"mix firmware"で起動される Mixタスクを丹念に追いかけて行けば、その概略を知ることができる。キー・パーツは ~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/scriptsに置かれている "rel2fw.sh"とその下請け "merge-squashfs"の2つのシェルスクリプトだ。これらのシェルスクリプトが連携して、プロジェクト・ディレクト下の _build/rpi_dev/rel/plug_mnist/libおよび releasesと、ユーザー定義の rootfs_overlayと、そして*~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/image/rootfs.squashfs* を入力としてターゲットrootファイルシステムを作成している。処理フローの概略は以下の通り。
- _build/rpi_dev/rel/plug_mnist/libおよびreleasesからターゲットに必要なファイルだけをピックアップし、ワーク・ディレクトリに集める。この時、併せてターゲットのディレクトリ階層(/svr)をワーク・ディレクトリ内に構築する
- ユーザー定義の rootfs_overlayをワーク・ディレクトリにマージする。
- ~/.nerves/artifacts/nerves_system_rpi-portable-1.12.2/image/rootfs.squashfsをアンパックして、種rootファイルシステムとする
- 先のワーク・ディレクトリを種rootファイルシステムにマージする
- "mksquashfs"で種rootファイルシステムをパックしてターゲットrootファイルシステムとする
と言うことは、libjpeg.soをターゲットに追加するには、rootfs.squashfsに組み入れるか、あるいは rootfs_overlayに置くかだな。
#4. libjpegを調達する
一通り「Nerves開発環境」を復習できたので、そろそろ "libjpeg"の調達方法を考えようか。
まず、決めるべきことが 2つある。libjpeg.soを (A)rootfs.squashfsと (B)rootfs_overlayのどちらに追加しようか? そして libjpeg.soを(C)ソースコードからメイクしようか、それとも(D)バイナリ・パッケージを利用しようか?
前者の選択については、特別な理由が無い限り (B)rootfs_overlayを選ぶで良いと思う。
仮に (A)rootfs.squashfsを選ぶと、Nervesシステムのリコンフィグ(Buildroot)に戻ってrootfs.squashfsを作り直すか[*2]、あるいは手作業で rootfs.squashfsをアンパック⇒ファイルを追加⇒再びパックすることになりそうだ。そんなに手間をかけても、本家 Nerves Projectが、Nervesシステムのアップデートすれば、それに追従するために再び作業をやり直さざるを得ない。今回のプロジェクトのように、アプリ開発が目的の場合は敢えて茨の道を進むメリットは無いんじゃないかな。
後者の選択については、とりあえず(D)バイナリ・パッケージを選ぶだな。
少々バージョンが古かったりするが、Debianリポジトリには正式な arm向けバイナリ・パッケージがあれこれと用意されている。これを利用しない手はないだろう(枯れたコード)。(C)ソースコードからのメイクは、コンパイル・オプションを間違えて嵌ってしまうという嫌なパターンのリスクがある。私としては次善の策としたい。
[*2]libjpegの追加は Buildrootのコンフィグ・メニューに含まれている。今回はPASSするが、Nervesシステムのリコンフィグは意外と簡単なので、「ものは試しに」の精神で体験してみるのも一興だろう。参考:「備忘録: WSL2 Ubuntu上で Nervesをカスタマイズしてみる」
それでは、具体的な作業に入ろう。
libjpegのバイナリ・パッケージは、下記の Debianリポジトリから入手する:
- libjpeg62-turbo-dev (1:1.5.2-2 など) - 静的ライブラリ
- libjpeg62-turbo (1:1.5.2-2 など) - 共有ランタイムライブラリ
arm向けには arm64, armel, armhfの3種類のパッケージが用意されているが、Raspberry Piでは通常 armelか armhfを利用する。
Debian での名称 | アーキテクチャ | CPU | Raspberry Pi |
---|---|---|---|
armel | ARM | Armv6(32bit) | A, B, A+, B+, Zero, Zero W |
armhf | ハードウェア FPU がある ARM | Armv7-A(32bit) | 2, 3 ほか |
作業手順は次の通り: プロジェクト・ディレクトリ直下にパッケージ展開用の extraディレクトリを作成する。"wget"でパッケージをダウンロードし、"dpkg -x"で展開&インストールする。併せて、rootfs_overlayに共有ライブラリをコピー。
定型作業なのでシェルスクリプトにしてしまおう
下記のシェルスクリプト(抜粋)では、"Cimg"ライブラリと "nlohomann/json"ライブラリのダウンロード&インストールも行っている。こちらは、共有ライブラリを持たない C++テンプレート・ライブラリなので、rootfs.overlayへのコピーは不要だ。
proj_top=`pwd`/..
# setup deb packages
deb_repo=http://ftp.jp.debian.org/debian/pool/main
case "${MIX_TARGET}" in
"rpi"|"rpi0")
wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo-dev_1.5.2-2+b1_armel.deb
dpkg -x libjpeg62-turbo-dev_1.5.2-2+b1_armel.deb .
wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo_1.5.2-2+b1_armel.deb
dpkg -x libjpeg62-turbo_1.5.2-2+b1_armel.deb .
;;
"rpi2"|"rpi3")
wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo-dev_1.5.2-2+b1_armhf.deb
dpkg -x libjpeg62-turbo-dev_1.5.2-2+b1_armhf.deb .
wget -nc ${deb_repo}/libj/libjpeg-turbo/libjpeg62-turbo_1.5.2-2+b1_armhf.deb
dpkg -x libjpeg62-turbo_1.5.2-2+b1_armhf.deb .
;;
*) echo "Unknown target: ${MIX_TARGET}"
exit 1
;;
esac
wget -nc ${deb_repo}/c/cimg/cimg-dev_2.4.5+dfsg-1_all.deb
dpkg -x cimg-dev_2.4.5+dfsg-1_all.deb .
wget -nc ${deb_repo}/n/nlohmann-json3/nlohmann-json3-dev_3.5.0-0.1_all.deb
dpkg -x nlohmann-json3-dev_3.5.0-0.1_all.deb .
# copy shared lib to target rootfs_overlay
mkdir -p ${proj_top}/rootfs_overlay/usr/lib
case "${MIX_TARGET}" in
"rpi"|"rpi0")
cp -au usr/lib/arm-linux-gnueabi/libjpeg.so* ${proj_top}/rootfs_overlay/usr/lib
;;
"rpi2"|"rpi3")
cp -au usr/lib/arm-linux-gnueabihf/libjpeg.so* ${proj_top}/rootfs_overlay/usr/lib
;;
esac
以上で、想定外の事態「libjpegが Nervesツールチェイン内に見つからない」に対処することが出来た。次節からは予定路線に戻り、Nervesへの PlugMnistのポーティングを進めよう。
#5. Nerves/rpi用の Tensorflow liteをメイクする
いまのところ Nerves/rpi向けの Tensorflow liteバイナリ・パッケージを提供しているサイトはなさそうだ[*3]。 利用したければ、頑張って自前で makeするしかないだろう。
[*3]Nervesは、一部の熱いコミュニティを除き、知名度が低いので当たり前と言えば当たり前
おっと、そんなに構えなくとも大丈夫。CMakeのお手軽さには及ばないが、Tensorflow liteの Makeスクリプトは、いろいろなターゲットに簡単に対応できるように、とても巧く組織化されている。シェルスクリプトbuild_xxxx_lib.sh(ビルド環境を整えて makeコマンドを起動する)と、Makefileインクルードxxxx_makefile.inc(ターゲット設定ファイル)の2つを用意すれば、新たなターゲット向けのライブラリを makeすることができる。
tensorflow_src/tensorflow/lite/tools/make/build_xxxx_lib.sh
tensorflow_src/tensorflow/lite/tools/make/targets/xxxx_makefile.inc
おやっ、おあつらえ向きに本家Raspberry Pi用のbuild_rpi_lib.sh/rpi_makefile.incが Tensorflowファイルセットに同梱されているではないか。これらを元に Nerves用の build_nerves_lib.sh/nerves_makefile.incを作成しよう。ファイルの置き場所は、先の extraディレクトリの下で良いかな。
extra
├── make
│ ├── targets
│ │ └── nerves_makefile.inc
│ └── build_nerves_lib.sh
└── bash:setup_nerves_extra.sh
シェルスクリプトbuild_nerves_lib.shの改造は、nervesのツールチェインに PATHを通すことと、makeコマンドの起動引数に "TARGET=nerves"を渡す箇所だけ。あとはコピペ
set -x
set -e
case "${MIX_TARGET}" in
"rpi"|"rpi0")
PATH=~/.nerves/artifacts/nerves_toolchain_armv6_rpi_linux_gnueabi-linux_x86_64-1.3.2/bin:$PATH
;;
"rpi2"|"rpi3")
PATH=~/.nerves/artifacts/nerves_toolchain_arm_unknown_linux_gnueabihf-linux_x86_64-1.3.2/bin:$PATH
;;
*) echo "Unknown target: ${MIX_TARGET}"
exit 1
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TENSORFLOW_DIR="${SCRIPT_DIR}/../../../.."
FREE_MEM="$(free -m | awk '/^Mem/ {print $2}')"
# Use "-j 4" only memory is larger than 2GB
if [[ "FREE_MEM" -gt "2000" ]]; then
NO_JOB=4
else
NO_JOB=1
fi
make -j ${NO_JOB} TARGET=nerves -C "${TENSORFLOW_DIR}" -f tensorflow/lite/tools/make/Makefile $@
一方、Makefileインクルードの改造は、nervesツールチェイン・コマンドのプリフィックスの定義("TARGET_TOOLCHAIN_PREFIX := armv6-rpi-linux-gnueabi-"等)を書き換えるだけ。
# Settings for Raspberry Pi.
ifeq ($(TARGET),nerves)
# Default to the architecture used on the Pi Two/Three (ArmV7), but override this
# with TARGET_ARCH=armv6 to build for the Pi Zero or One.
# TARGET_ARCH := armv6
ifeq (, $(findstring "$(MIX_TARGET)","rpi" "rpi0" "rpi2" "rpi3"))
$(error "unknown target: $(MIX_TARGET)")
endif
ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi" "rpi0"))
TARGET_ARCH := armv6
TARGET_TOOLCHAIN_PREFIX := armv6-rpi-linux-gnueabi-
CXXFLAGS += \
-march=armv6 \
-mfpu=vfp \
-funsafe-math-optimizations \
-ftree-vectorize \
-fPIC
CFLAGS += \
-march=armv6 \
-mfpu=vfp \
-funsafe-math-optimizations \
-ftree-vectorize \
-fPIC
LDFLAGS := \
-Wl,--no-export-dynamic \
-Wl,--exclude-libs,ALL \
-Wl,--gc-sections \
-Wl,--as-needed
endif
ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi2" "rpi3"))
TARGET_ARCH := armv7
TARGET_TOOLCHAIN_PREFIX := arm-unknown-linux-gnueabihf-
CXXFLAGS += \
-march=armv7-a \
-mfpu=neon-vfpv4 \
-funsafe-math-optimizations \
-ftree-vectorize \
-fPIC
CFLAGS += \
-march=armv7-a \
-mfpu=neon-vfpv4 \
-funsafe-math-optimizations \
-ftree-vectorize \
-fPIC
LDFLAGS := \
-Wl,--no-export-dynamic \
-Wl,--exclude-libs,ALL \
-Wl,--gc-sections \
-Wl,--as-needed
endif
LIBS := \
-latomic \
-lstdc++ \
-lpthread \
-lm \
-ldl
endif
最後に、Tensorflow liteライブラリを作成する一連の作業を自動化するシェルスクリプトを作っておこう。
このスクリプトは、Tensorflowファイルセットを GitHubからダウンロードして、上で作成したNerves用 build_nerves_lib.sh/nerves_makefile.incを所定のディレクトリにコピーしたのち、ライブラリの作成を行う。
# setup tensorflow lite
git clone https://github.com/tensorflow/tensorflow.git tensorflow_src
pushd tensorflow_src
tfl_make=./tensorflow/lite/tools/make
if [ ! -e ${tfl_make}/downloads ]
then
${tfl_make}/download_dependencies.sh
fi
cp -a ../make/* ${tfl_make}
${tfl_make}/build_nerves_lib.sh
popd
#6. ひとまずの大団円
よお~し、PlugMnistポーティング完了まで、あと一息だ。
ここまで準備して来た内容を、"tfl_interp"の Makefileに反映しよう。主な変更点は、環境変数 MIX_TARGETをみて、C++コンパイラがヘッダーファイルやライブラリを extraディレクトリ以下で検索するように指定している点だ。
ifeq ($(MIX_APP_PATH),)
calling_from_make:
mix compile
endif
ifeq ($(CROSSCOMPILE),)
ifeq ($(shell uname -s),Linux)
DEPS_HOME ?= ./extra
LIB_EXT = -lpthread -ldl
TFL_GEN = linux_x86_64
else
DEPS_HOME ?= C:/msys64/home/work
LIB_EXT = -lmman
TFL_GEN = windows_x86_64
INC_EXT = -I$(DEPS_HOME)/CImg-2.9.2
endif
else
ifeq (, $(findstring "$(MIX_TARGET)","rpi" "rpi0" "rpi2" "rpi3"))
$(error "unknown target: $(MIX_TARGET)")
endif
ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi" "rpi0"))
DEPS_HOME ?= ./extra
TFL_GEN = nerves_armv6
INC_EXT = -I$(DEPS_HOME)/usr/include -I$(DEPS_HOME)/usr/include/arm-linux-gnueabi
LIB_EXT = -L$(DEPS_HOME)/usr/lib/arm-linux-gnueabi -lpthread -ldl -latomic
endif
ifeq ("$(MIX_TARGET)", $(findstring "$(MIX_TARGET)","rpi2" "rpi3"))
$(info "arm7")
DEPS_HOME ?= ./extra
TFL_GEN = nerves_armv7
INC_EXT = -I$(DEPS_HOME)/usr/include -I$(DEPS_HOME)/usr/include/arm-linux-gnueabihf
LIB_EXT = -L$(DEPS_HOME)/usr/lib/arm-linux-gnueabihf -lpthread -ldl -latomic
endif
endif
INCLUDE = -I./src \
-I$(DEPS_HOME)/tensorflow_src \
-I$(DEPS_HOME)/tensorflow_src/tensorflow/lite/tools/make/downloads/flatbuffers/include \
$(INC_EXT)
DEFINES = #-D__LITTLE_ENDIAN__ -DTFLITE_WITHOUT_XNNPACK
CXXFLAGS += -O3 -DNDEBUG -fPIC --std=c++11 -fext-numeric-literals $(INCLUDE) $(DEFINES)
LDFLAGS += $(LIB_EXT) -ljpeg
LIB_TFL = $(DEPS_HOME)/tensorflow_src/tensorflow/lite/tools/make/gen/$(TFL_GEN)/lib/libtensorflow-lite.a
PREFIX = $(MIX_APP_PATH)/priv
BUILD = $(MIX_APP_PATH)/obj
SRC=$(wildcard src/*.cc)
OBJ=$(SRC:src/%.cc=$(BUILD)/%.o)
all: $(BUILD) $(PREFIX) install
install: $(PREFIX)/tfl_interp
$(BUILD)/%.o: src/%.cc
$(CXX) -c $(CXXFLAGS) -o $@ $<
$(PREFIX)/tfl_interp: $(OBJ)
$(CXX) $^ $(LIB_TFL) $(LDFLAGS) -o $@
clean:
rm -f $(PREFIX)/tfl_interp $(OBJ)
$(PREFIX) $(BUILD):
mkdir -p $@
print-vars:
@$(foreach v,$(.VARIABLES),$(info $v=$($v)))
.PHONY: all clean calling_from_make install print-vars
クライマックスだ
PlugMnistのビルド手順は下記リストの通り。
残念ながら extraに追加したライブラリ類は、一度だけ "setup_nerves_extra.sh"を実行してメイクしておく必要がある。そうそう、その前に MIX_TARGETの設定を忘れずに!
extraのセットアップが終わっていれば、あとはいつも通りの "mix firmware.burn"
tfl_interpをメイクし、libjpeg.soと共に、_build\rpi_dev\nerves\images\plug_mnist.fw にパックしてくれる。
$ export MIX_TARGET=rpi
$ pushd extra
$ ./setup_nerves_extra.sh
$ popd
$ mix firmware.burn
Raspberry Piで PlugMnistを実行してみよう。事前の MinGW/Windows10, Ubuntu/WSL2上での結果と同じだね。オッケー👌
ひとまずの大団円。
#7.ふりかえり
なんだかんだで、無事に Nerves/rpi(無印)に PlugMnistをポーティングすることが出来た。本文では触れなかったが、Nerves/rpi3での動作もOKだ。ふぅ~。と言うことで、前回掲げた「展望リスト」のひとつ目をクリア(祝)
‥‥‥結局、Elixirのコードが一行も出てこない記事となってしまった
[展望リスト]
済み 1.今回作成した PlugMnistをそのまま Nerves RasPiにポーティングする
2.Nerves RasPiのカメラ・モジュールで撮影した画像で推論ができるように改造する
3.物体検出や面白そうなモデルをインストールして遊んでみる
4.liteではなく本家のTensorFlowを利用できるようにしてみる
#参考文献
[1] Window10で TensorFlow liteを使ってみる - 前編
[2] Window10で TensorFlow liteを使ってみる - 後編
[3] Can I put Debian on my Raspberry Pi?
[4] Debian:パッケージディレクトリを検索
[5] Nerves Proj Documentation: Customizing Your Own Nerves System
[6] Nerves Proj Documentation: Root Filesystem Overlays
[7] 「いかにして問題を解くか」G.Polya