ESP8266 上で Lua インタプリタが動くファームウェア、NodeMCU を使う

NodeMCU は、Node.js のように非同期でイベントドリブンなコードを書く事が出来る、ネットワークを利用する ESP8266 とは相性が良い開発環境(ファームウェア)。Lua インタプリタが動くので、マイコン上でプログラムを動的に実行できる。また NodeMCU 用のハードウェアも売っているが、ESP8266 シリーズならどれでも動く(もちろんESP-WROOM-02もね!)。NodeMCU という名前がわかりにくいので、名前で損してる感ある…。

また40個以上のモジュールがすでに本家に入っていたり、SSLにも対応していたり、コードが綺麗だったりと、なにやら良さそうなプロジェクトな予感がする。

firmware を取得する

以前は firmwaregithub 上で公開されていたようだが、今は三つの方法で取得する。

クラウド上でのビルド

で、必要なモジュールをチェックすると、ビルド終了時メールにて firmware の URL が送られてくる。

普通はこれを使えばOK。

Docker を使ったビルド

ESP8266 の xtensa のツールチェインをビルドするのは面倒くさい、という人に。

Linux 上でのビルド

ソースから普通にビルドする。Ubuntu 16.04 LTS環境で簡単にできた。

まずは esp nonos

コンパイルする。ドキュメント通りでおk。なお esp8266/Arduino の為に入れていた xtensa のバイナリツールチェインではうまく動かず、新たにコンパイルした。

sudo apt-get install make unrar autoconf automake libtool gcc g++ gperf \
    flex bison texinfo gawk ncurses-dev libexpat-dev python-dev python python-serial \
    sed git unzip bash help2man wget bzip2
sudo apt-get install libtool-bin

これでビルドに必要なパッケージが全部入るのであとは make してしばらく待つ。単体動作するtoolchain 群をコンパイルしてるので遅い。

Xtensa toolchain is built, to use it:

export PATH=$HOME/src/github.com/pfalcon/esp-open-sdk/xtensa-lx106-elf/bin:$PATH

Espressif ESP8266 SDK is installed, its libraries and headers are merged with the toolchain

と書かれてるように、xtensa-* にパスを必要に応じて通す。続いて

https://github.com/nodemcu/nodemcu-firmware

を make すると bin 以下に 0x00000.bin, 0x00001.bin ができあがるので、それを利用する。なお

app/.output/eagle/debug/image/eagle.app.v6.out

に bin ファイル分割前の ELF バイナリができあがってる。

[7]>_<X file app/.output/eagle/debug/image/eagle.app.v6.out
app/.output/eagle/debug/image/eagle.app.v6.out: ELF 32-bit LSB executable, Tensilica Xtensa, version 1 (SYSV), statically linked, not stripped
[7]>_<X xtensa-lx106-elf-size app/.output/eagle/debug/image/eagle.app.v6.out
   text    data     bss     dec     hex filename
 380472    2196   27512  410180   64244 app/.output/eagle/debug/image/eagle.app.v6.out

プログラムサイズは400KB弱(Arduinoは220KB)だけど、利用RAMが初期値で30KB(Arduinoは32KBほど)で、空きRAMが50KBもあるとは、大分コンパクトだなぁ。

なお、app/include/*config*.h を編集する(もしくは適当な環境変数をつけてコンパイルする)ことで、様々な値を変更することが出来る。

また必要な組み込みライブラリは app/include/user_modules.hコメントアウトを適宜編集することで組み込める。組み込むことによってもちろんプログラムサイズは増えるので注意。

firmware の書き込み

あたりに CUI のesptool.py を使った方法、GUI の NodeMCU Flasher を使った方法などなどがのってる。

ただ、前述の Linux 上に firmware のビルド環境を用意した場合

$ make flash
make -C ./app flash
make[1]: Entering directory '/home/yuichi/src/github.com/nodemcu/nodemcu-firmware/app'
../tools/esptool.py --port /dev/ttyUSB0 write_flash 0x00000 ../bin/0x00000.bin 0x10000 ../bin/0x10000.bin
esptool.py v1.2-dev
Connecting...
Running Cesanta flasher stub...
Flash params set to 0x0000
Writing 28672 @ 0x0... 28672 (100 %)
Wrote 28672 bytes at 0x0 in 2.5 seconds (91.8 kbit/s)...
Writing 356352 @ 0x10000... 356352 (100 %)
Wrote 356352 bytes at 0x10000 in 30.9 seconds (92.2 kbit/s)...
Leaving...
make[1]: Leaving directory '/home/yuichi/src/github.com/nodemcu/nodemcu-firmware/app'

で一発。なお esptool.py は --baud オプションをつけないと 115200 で転送するので、Makefile をちょっと修正して --baud 921600 をつけて転送するとあからさまに速い。この速度で転送できるかどうかはチップによるけど。

$ make flash
...
Writing 28672 @ 0x0... 28672 (100 %)
Wrote 28672 bytes at 0x0 in 0.5 seconds (449.4 kbit/s)...
Writing 356352 @ 0x10000... 356352 (100 %)
Wrote 356352 bytes at 0x10000 in 4.8 seconds (591.5 kbit/s)...
...

Hello World!

シリアルコンソールで繋いで Hello World! してみる。標準だとシリアルの baudrate は 115200 なのでそれにあわせる。

$ platformio device monitor -b 115200
--- Miniterm on /dev/ttyUSB0  115200,8,N,1 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
r␘␂␀l��r�␀�#␂�n�␄��␌␘�␌␜��␜p�<���␀�8␂��ǒ��␜p␌␘␌�nn�␂�;�nĒ��␌␛�␌b�$�r␘␂r␘␂p�n��܀␌␜���l␜�␌␜�␌␜�␌b�␄n��n��␎�l�ߌ�8␌␘��nn�␃����␀l`␛��␒�#�n�␄␏r␘␂␎␂nr���;␂��␌?��␜r␘␂p�n��܀␌␜�r�␜��␛␂␌␜�␌␜p␌␘p��<␂��l␜�␌␟p␌␘��nn�␃��␌␘r␘␂�␒�#�n�$��l`␛�`rn|��n�␄��␒�␜�`␛␄�~␂��␌␘�␒�#�n�$��␌␘��nn�␃��l`␛␄nn��܀␌␜�r�␜��␛␂␌␟��8p␌␘p��<␂��␌b�ľ~�n�␃���␜␀l`␛␄�␃␒n�␂��l`␛�`rn|��n�␄���␄�`␛与��;␂��␌?␒b�;�␂���␀�8␂�n��␒r���n�b␌␘�␌␜�l␟r␘␂����I8��␜�␛ ��

NodeMCU 1.5.4.1 build unspecified powered by Lua 5.1.4 on SDK 1.5.4.1(39cb9a32)
lua: cannot open init.lua

最初のスタートアップファイル、init.lua が無いと言われるけど、問題なく繋げる。文字化けしてるのは ESP8266 起動時のメッセージ(baudrate が違うから)だけど無視して問題ない。

> print("Hello World!")
Hello World!

おおお。マイコン上でインタプリタが動くと地味に感動するね。

インタプリタ接続時のヒープの空き容量は

> print(node.heap())
45056

で、44KBほど。それなりにプログラミングできそう。

コードをアップロードして実行

行実行ではなく、書いたソースコードを実行したい。

Java で書かれた IDE、ESPlorer や CUI の nodemcu-uploader を使っての方法が書かれている。

ESPlorer の場合

> file.remove("hello.lua");
> file.open("hello.lua","w+");
> w = file.writelinew([==[print("hello NodeMCU!")]==]);
> file.close();

とエディタ上の文字列をシリアルコンソール経由で書き込んで(!)ファイルとして保存する。その後

> dofile('hello.lua')
hello NodeMCU!

で実行してる。

ちなみに ESPlorer、左がエディタ、右がシリアルコンソールになっていて、すぐ実行結果を見たり、ボタン一つで heap の容量が見れたりと、結構良く出来ている。

nodemcu-uploader

nodemcu-uploader は recv() / send() コマンドの後に、ACK しつつ送信、という一般的なシリアルアップロードの方法でファイルを書き込む。pip でインストールできるので、すぐ使うことが出来る。

に利用方法が載ってるが、

nodemcu-uploader upload hello.lua --do

で転送してそのままファイルを実行したり、

nodemcu-uploader upload *.lua --compile

コンパイルして .lc ファイルにした後アップロードしたりできる。

Lチカする

さて、やっとLチカ…。NodeMCU は冒頭の通り、Node.js のようなイベントドリブンのアーキテクチャを採用しており、メインループを回すのではなく、イベントモデルで実行する。

LED_PIN = 2
gpio.mode(LED_PIN, gpio.OUTPUT)
ledState = gpio.HIGH
gpio.write(LED_PIN, ledState)

toggleLED = function()
  if ledState == gpio.HIGH then
    ledState = gpio.LOW
  else
    ledState = gpio.HIGH
  end
  gpio.write(LED_PIN, ledState)
end

tmr.alarm(0, 1000, tmr.ALARM_SEMI, toggleLED)

タイマー0に関数をセットし、1秒おきに実行することで Lチカを実現している。

もちろん、このLチカが動いているときに、hello world! を再度書き込んで実行しても、Lチカは止まらず実行されている。

なお、GPIO の番号が ESP8266 の pin と NodeMCU では違うので注意。例えば ESP8266 で IO4 のピンは NodeMCU では 2 となる。


今回はLチカまで。触ってみた感じ、やはり良く出来るてる感がある。あとはネットワーク周りと、実アプリケーションを作ったときに、はてして heap を食いつぶさずに作れるのかどうか。