はじめに
「おうちハック」で定番のESP32。ESP-IDFをご利用の方も多いとは思いますが、秋月や千石、マルツで買ってきたセンサーを手っ取り早く使うにはArduinoのライブラリをそのまま使えるArduino IDEのほうが都合よかったりします。
そんなわけで私もESP32をArduinoで使っております、が、しかし、ArduinoのIDEは使いにくい...
また、esp32のArduinoボードにはバージョン1系統(2023秋時点で最新はv1.0.6)とバージョン2系統(同2.0.14)がございますが、微妙に2系では安定して動かないということもあります。
ということで、本記事では以下のことを目指します。
- CLIでESP32 Arduinoの開発を行う(ArduinoのGUIは使わない)
- LSPと
Makefile
を使う - 「ボード」のバージョン切り替えを可能にする
- OTAもmakeから可能とする
- 実行時例外発生時の遺言からの犯人探しをする(シリアル接続時・リモート時どちらも)
- macosまたはlinux環境
- Windows? VScode? PlatformIO? 知らない子ですね...
追記: PlatformIOではLSPが使えないと思っていたのですが、どうやらclangdを自分でコンパイルするとLSPできるようです。
ディレクトリ構成
path | 説明 |
---|---|
~/src/arduino/ | 開発環境のroot |
~/src/arduino/cli-config/ | arduino-cliのyamlの場所 |
~/src/arduino/core/esp32/x.y/ | ボードインストール先(x.yはバージョン) |
~/src/arduino/core/espota.py | espota.py |
~/src/arduino/core/decoder/ | 死因解明ツールの置き場 |
~/src/arduino/core/download/ | ダウンロードしたものを置く場所 |
インストールと初期設定
Arduino-CLIインストール
homebrewなどで適当にインストールします。
$ brew install arduino-cli
yamlの用意
arduino-cli config init
で雛形を作ります。 macosの場合、~/Library/Arduino15/arduino-cli.yaml
にできます。
残念ながら環境変数は展開してくれないようですのでバージョンに合わせて複数つくります。
$ arduino-cli config init
$ cp ~/Library/Arduino15/arduino-cli.yaml \
~/src/arduino/cli-config/arduino-cli-1.0.yaml
$ cp ~/Library/Arduino15/arduino-cli.yaml \
~/src/arduino/cli-config/arduino-cli-2.0.yaml
board_manager:
additional_urls:
- https://espressif.github.io/arduino-esp32/package_esp32_index.json
directories:
data: /home/sweethome/src/arduino/core/esp32/1.0/
user: /home/sweethome/src/arduino/
downloads: /home/sweethome/src/arduino/core/download/
board_manager:
additional_urls:
- https://espressif.github.io/arduino-esp32/package_esp32_index.json
directories:
data: /home/sweethome/src/arduino/core/esp32/2.0/
user: /home/sweethome/src/arduino/
downloads: /home/sweethome/src/arduino/core/download/
esp32のボードのURLはこちらに記載。
ボードのインストール
$ arduino-cli --config-file ~/src/arduino/cli-config/arduino-cli-1.0.yaml \
core install esp32:esp32@1.0.6
$ arduino-cli --config-file ~/src/arduino/cli-config/arduino-cli-2.0.yaml \
core install esp32:esp32@2.0.14
ccacheの追加
ccacheを追加してコンパイルを高速化します。ccacheなんて使うの、linux kernelの自前ビルドをしていた頃以来ですわ(老害発言)。
find ~/src/arduino/core/ -name platform.txt
して見つけ出し、recipe.c.o.pattern
とrecipe.cpp.o.pattern
の頭にccacheを追加します。
OTAの用意
http(s)やmqttではないotaをするには、esp32のSSIDのLANと開発マシンの間でいくつかtcpが通る必要があります。 以下では、開発マシン→esp32に18266/tcp, esp32→開発マシンに28266/tcpを通しています(家庭内でVLANを切る変態)。
スケッチを格納しているディレクトリの名前でesp32のホスト名を登録しておきます。 つまり、hoge/hoge.ino
を書き込んだesp32にはping hoge
ができるということです。
Makefileの中で使いやすいようにsymlinkしておきます。
$ cd ~/src/arduino/core/
$ ln -s $(find esp32/2.0 -name espota.py) .
私の場合、常時otaを待機させるのではなく特定のmqttを送った後にのみota出来るようにしてメモリの節約をしています。 (wifi, ble, webserver, otaを全部同時に使うのは結構きついです...) そのため、espota.pyは直接Makefileから呼ばずにmqttを含んだラッパーを作っています。
stack trace decoderのインストール
実行時クラッシュ時、シリアルに出てくるesp32の遺言を解析してくれる検視官を雇います。
$ mkdir ~/src/arduino/core/decoder/
$ cd !$
$ wget -nd https://github.com/littleyoda/EspStackTraceDecoder/releases/download/untagged-59a763238a6cedfe0362/EspStackTraceDecoder.jar
$ mkdir 1.0 2.0
$ cd 1.0
$ find ../../esp32/1.0/ -name \*addr2line
#適切なものを自分で選ぶ
$ ln -s ${みつけたもの} addr2line
$ cd -
$ cd 2.0
$ find ../../esp32/2.0/ -name \*addr2line
#適切なものを自分で選ぶ
$ ln -s ${みつけたもの} addr2line
esp-coredumpのインストール
$ git clone https://github.com/espressif/esp-coredump.git
$ cd esp-coredump
$ pip3 install .
coreダンプ救出コードはこちらが参考になります。
起動した時、前世で生成したcoredumpがフラッシュにあったら読み出してネットワーク経由で吐き出すようにしてあげます。
syslogにhexで出すのが良いでしょう。
xtensa-esp32-elf-gdb
は core/esp32/2.0/
の下にあるものを使えばokです。
上記の方法でdumpした場合、最初の20バイトはヘッダーのようですのでスキップするとELFになります。
$ esp-coredump info_corefile \
--core ${corefile} \
--core-format elf \
build-esp32/${name}.ino.elf
===============================================================
==================== ESP32 CORE DUMP START ====================
================== CURRENT THREAD REGISTERS ===================
pc 0x40094164 0x40094164 <tlsf_malloc+308>
lbeg 0x4008a9d4 1074309588
lend 0x4008a9de 1074309598
lcount 0x0 0
sar 0x1d 29
ps 0x60823 395299
threadptr <unavailable>
br <unavailable>
...
rsyslogdが動いているマシンからlogをscpしてきて、hexdumpをbinaryにして20バイトスキップして上記のコマンドを実行するラッパーesp32-coredecoder
を作っておきます。
これで、シリアルが繋がっていない時に死亡しても死因の解明ができるようになります。
arduino-language-serverのインストール
$ git clone https://github.com/arduino/arduino-language-server.git
$ cd arduino-language-server
$ go build
$ install arduino-language-server ~/bin/
言語サーバの起動は以下のようになりますので、お使いのエディッタやIDEの設定に入れておきます。
arduino-language-server \
-clangd /opt/homebrew/bin/clangd \
-cli /opt/homebrew/bin/arduino-cli \
-cli-config $HOME/src/arduino/cli-config/arduino-cli-2.0.yaml \
-fqbn esp32:esp32:esp32
emacsの場合、.dir_locals.el
を使うことで、host nativeのclangd
を阻害することなくArudino開発ができます。
なお、emacsの自動生成lock fileがプロジェクトのディレクトリに生成されるとclangdが落ちるので場所を変更するか無効化します。
Makefile
VERSION = 2.0
PARTITION = min_spiffs
PORT = /dev/cu.usbserial-0001
SPEED = 74880
ifneq ("$(wildcard env.mk)","")
include env.mk
endif
SKETCH = $(notdir $(CURDIR)).ino
NAME = $(notdir $(CURDIR))
TARGET_DIR = $(CURDIR)/build-esp32
FQBN = esp32:esp32:esp32
BOARDOPTIONS = PSRAM=disabled,PartitionScheme=$(PARTITION),CPUFreq=240,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=115200,DebugLevel=none
CLI_YAML = $(HOME)/src/arduino/cli-config/arduino-cli-$(VERSION).yaml
ESPOTA = python3 $(HOME)/src/arduino/core/espota.py --debug --port 18266 --host_port 28266 --auth=secret --progress
DECODER = $(HOME)/src/arduino/core/decoder/EspStackTraceDecoder.jar
ADDR2LINE = $(HOME)/src/arduino/core/decoder/$(VERSION)/addr2line
# syslogにでたcoreをデコードするラッパーを作っておく
COREDECODER = $(HOME)/bin/esp32-coredecoder
.PHONY: build
put: build ota
sput: build upload monitor
build:
@ mkdir -p $(TARGET_DIR)
arduino-cli compile \
--config-file $(CLI_YAML) \
--fqbn $(FQBN) \
--board-options $(BOARDOPTIONS) \
--build-path $(TARGET_DIR) \
$(CURDIR)
clean:
rm -rf $(TARGET_DIR)
ota:
$(ESPOTA) --ip $(NAME) --file $(TARGET_DIR)/$(SKETCH).elf
upload:
arduino-cli upload \
--config-file $(CLI_YAML) \
--fqbn $(FQBN) \
--board-options $(BOARDOPTIONS) \
--input-dir $(TARGET_DIR) \
--port $(PORT)
monitor:
arduino-cli monitor \
--config-file $(CLI_YAML) \
--fqbn $(FQBN) \
--port $(PORT) \
--config baudrate=$(SPEED)
decode: trace.txt
java -jar $(DECODER) $(ADDR2LINE) $(TARGET_DIR)/$(SKETCH).elf trace.txt
decodecore:
$COREDECODER --name $(NAME) --elf $(TARGET_DIR)/$(SKETCH).elf
開発
スケッチのあるディレクトリでその個体固有の設定を env.mk
に書きます。
VERSION = 1.0
PORT = /dev/cu.wusbserial-1234
あとはガリガリ書いてmake put
するだけ。
うまく行かないときでも前世のcoreがとれていれば、make decodecore
です。
「そんなcore産んだ覚えはありません」となったら、泣きながらttyを接続してmake monitor
して遺言をtrace.txt
に書き、make decode
すると犯人がわかります。
otaができない時も泣きながらシリアルを繋げてmake upload
します。
ところでinoは?
うちの*.ino
はほとんど空っぽで、似非シングルトンなc++のクラスの中で全部処理しています。
完全に中身空っぽ(0バイト、つまりrm hoge.ino; touch hoge.ino
)で、.cpp
に以下を入れておいても問題ありません。
#include "Hoge.h"
HogeBase *hoge = 0;
void setup(void)
{
hoge = new Hoge();
hoge->setup();
}
void loop(void)
{
hoge->loop();
}
多数のesp32で遊んでいるので、共通した処理(自分がよく使うセンサー、mqtt処理、influxdbやvictoria metricsへの書き込み、prometheusの/metrics
など)の入った基底クラスをつくっておいて、個体ごとに違うセンサーなどは継承したクラスの中で処理しています。
調子に乗ってなんでも入れるとフラッシュが足りなくなるのですが...
Arduinoのライブラリは使いたいがinoはキモいので嫌い、c++の機能はしっかり使いたい、でもLSPに助けてもらいたい、というわがまま。
おわりに
以上でemacsでc++-ts-mode
にeglotしてM-x compile
といった人も幸せになれます。
Macも次はARMからコイツに移行するのではないか、といわれているRISC-Vを搭載したESP32C3が混在するときも、同じような方法でMakefile
やenv.mk
, .dir_locals.el
に細工すればいけます。
Happy おうちHacking!
付録: rosettaless
M1が出てもう4年、ディアボロ天才少女のロゼッタさんもそろそろ引退です。
2つほどx86_64の残党を追放します。
esptool
$ brew install esptool
tools.esptool_py.path=/opt/homebrew/bin/
tools.esptool_py.cmd=esptool.py
ctags
$ git clone https://github.com/arduino/ctags.git
$ cd ctags
$ ./configure
$ vi general.h
%% 60行目付近のifをelseのみ有効にする
$ make
$ install ctags ~/src/arduino/core/esp32/2.0/packages/builtin/tools/ctags/5.8-arduino11/
そのうちrosetta廃止で強いメモリモデル互換モードを捨てて少しスリムになるのかもしれませんね。