仙石浩明の日記

2021年8月24日

M5Stack ATOM Lite を USBシリアル変換アダプタにしてみた 〜 Raspberry Pi Pico の UART コンソールを使う 〜

はじめてラズパイを買ったら意外に面白くてハマってしまった。 電子工作なんて 30年ぶりで、 半田ごてを握るには年を食いすぎている (手元がふるえる) のだけど、 ブレッドボードやら電子パーツやらをいろいろ買い揃えて、 オリンピックが終わってからの 2週間、寝る間も惜しんで楽しんでいる。

Raspberry Pi Zero WH が 1848円、 Raspberry Pi Pico が 550円、 M5Stack ATOM Lite が 1287円。 安いので次々と買ってしまった。 ラズベリーパイ (Raspberry Pi)、略してラズパイと呼ばれるようになって久しいが、 M5Stack をはじめとする ESP32 (や ESP8266) を使ったコントローラは、 何と呼ばれているのだろう?

Raspberry Pi Zero WH は普通の Linux マシンなので何でもありだが、 他はマイコン (マイクロコンピュータ) ならぬマイクロコントローラなので、 普通の OS を動かすことは難しく制約が多くて一筋縄にはいかない。

ググってみると、 開発環境として Arduino IDE を使い C (C++ ?) や Java などで開発している人が多いようだ。 30年前ならいざ知らず、 マイクロコントローラと言えども計算リソースが潤沢にある (30年前の汎用機並?) 昨今、 なぜコンパイラ言語 (しかも C や Java みたいなアセンブラと大差ない低級言語) を使うのか? インタプリタ言語なら対話的にコード片を実行して、 動作を確認しながらプログラミングできる (REPL, Read-Eval-Print Loop) ので、 開発効率が圧倒的に高い。

というわけで、 わたし的には MicroPython 一択 (ちなみに Python を使うのは今回がはじめて) なのであるが、 困ったことに Raspberry Pi Pico (以下 Pi Pico と略記) は、 開発環境である PC との通信手段が限られる。 USB コネクタが一つしかなく、 M5Stack ATOM Lite 等と違って Wi-Fi 機能もない。 つまり、 Pi Pico に USB 機器をつなぐ (Pi Pico が USB ホスト) 場合は、 USB で PC へつなぐ (PC が USB ホスト) ことができなくなるので、 PC と通信する手段が無くなってしまう。 プログラム実行中に PC と通信できなくては REPL にならない。

USB (Universal Serial Bus) がダメなら Universal じゃないシリアル通信を使えばいい、ということで Pi Pico にもシリアル通信のための UART (Universal Asynchronous Receiver/Transmitter, 調歩同期式汎用送受信機) が装備されている (ただし Pi Pico 用の MicroPython は UART では REPL できないので再ビルドの必要がある。 後述)。 PC 側でも UART 機能があれば通信できる。 というか USB や Wi-Fi が無かった時代は UART 通信 (RS-232C など) の方が一般的だった。

ところが、 いまどきの UART は 3.3V だという。 ±3~25V の信号線を使っていた RS-232 規格とは隔世の感がある。 ±25V な機器はさすがに捨ててしまったが、 いまでも手元にある USBシリアル変換アダプタは 0〜5V (TTL レベル) のものばかり。

5V を Pi Pico が扱える 3.3V まで下げるのは抵抗を使って分圧すればいいが、 その逆、 つまり Pi Pico から PC へ 5V の信号を伝えるのは少々やっかいである。 3.3V のままでも PC に H レベルと認識してもらえなくもないが、 マージンが狭くなるのは否めない。 もちろん 115200bps とかなら問題も起きないだろうが、 現代なら 1.5Mbps くらいは出したいところ。

もちろん素直に 3.3V 対応の USB to TTLシリアルアダプタを買えばいいのだが、 USBシリアル変換アダプタを既に (何個も) 持っているのに新たに買うのはモッタイナイ気がするし、 元々 200〜300円くらいしかしないパーツを、 本体と同じくらいの送料を払って買うのも業腹である (こんど秋葉原へ行ったときにでも買おうっと)。

Double Pico ! ATOM Lite as a Serial Converter to Pi Pico

要は 3.3V な UART があればいいわけで、 M5Stack ATOM Lite (以下 ATOM Lite と略記) を USB シリアルアダプタにしてしまえばいい!と思いついた。 つまり ATOM Lite も Pi Pico と同様 USB で REPL が使えるが、 ATOM Lite の REPL ではなく、 ATOM Lite (写真上) と UART シリアル (写真上の 3本のジャンパー線, うち黒は GND) でつないだ先の Pi Pico (写真下) の REPL を使おうという目論見。 ATOM Lite は PC と Pi Pico との通信を中継するだけ。 ATOM Lite もチップの名前は ESP32-PICO-D4 なのでダブルピコ!

PC (開発環境) ←──USB──→ ATOM Lite ←──UART──→ Pi Pico

MicroPython では flash メモリに boot.py を置いておくと起動時に実行してくれる。 ATOM Lite を常にシリアルアダプタとして使いたいわけではないので、 ATOM Lite のボタンを押しながら起動したときだけシリアルアダプタとして機能するようにしてみた。 シリアルアダプタとして動作中はボタン中央の LED が緑色に点灯する。 もう一度ボタンを押すと LED が消灯し、 通常の REPL モードになる。 boot.py に以下のプログラムを追記した:

import machine
import sys
import neopixel
import utime
import _thread

btn = machine.Pin(39, machine.Pin.IN)
if btn.value():
    sys.exit()

pxls = neopixel.NeoPixel(machine.Pin(27), 1)
pxls[0] = (0, 25, 0)
pxls.write()
start_time = utime.time()

uart = machine.UART(1, 115200, tx=21, rx=25)
done = False

def thread():
    global done
    while not done:
        c = sys.stdin.read(1)
        if c == "\n":
            uart.write("\r\n")
        else:
            uart.write(c)
    _thread.exit()

_thread.start_new_thread(thread,())

while not done:
    if btn.value() == 0:
        if utime.time() - start_time > 10:
            done = True
    if uart.any() > 0:
        sys.stdout.write(uart.read(1))
    else:
        utime.sleep_ms(1)

pxls[0] = (0, 0, 0)
pxls.write()

ATOM Lite の GPIO 21番ピンを UART TX として、 GPIO 25番ピンを UART RX として使い、 それぞれ Pi Pico の UART0 RX および UART0 TX につなぐ。 Pi Pico の VBUS と GND に 5V 電源を供給する (写真右下の赤と黒のジャンパー線) ことで USB コネクタを使わずに空けておける。

はじめての Python (文法すらろくに知らなかった) だけど、 ATOM Lite をシリアルアダプタとして使うことを思いついてから、 実際に使えるようになるまで半日もかかっていない。 まさに REPL さまさま。

MicroPython ESP32 版は poll クラスがサポートされていないようなので、 スレッドを用いた。 また stdin (標準入力) を生のまま (バイナリとして) 入力することができず、 0x00〜0x1F のコントロール文字を中継することができない。 例えば REPL ではプログラムの実行を止めたいときは Control-C を押すが、 Pi Pico で実行中のプログラムではなく、 ATOM Lite で実行中の boot.py が止まってしまう。 実用を考えるなら C で書き直すべきだが、 プロトタイプとしてならこれで充分。 (8月26日追記: C で書き直してみた)

ATOM Lite のボタン中央の LED には Neo Pixel LED が使われている。 単なる RGB LED (赤, 緑, 青の 3つの発光ダイオードが一つの LED パッケージに封入されている) とは異なり、 一つ一つの LED にコントロール用の IC が内蔵されていて、 1本の信号線で多数の LED (例えば長尺のテープに実装したり、 マトリックス状に配置してサイネージのピクセルとして使う) を自在にコントロールできる。

MicroPython では前掲したプログラムのように配列 (pxls[]) の各要素が一つの LED に対応していて、 RGB の値をそれぞれ書込むことで多数の LED の色・明るさを自由に設定できる。 ATOM Lite のように LED 一つだけだとあまり意味がない (pxls[0] の値を変えるだけ) が、 それでも Neo Pixel に触れてみるという目的には有用と思う。 実際、私も初めて使った。 ATOM Lite に 6軸 IMU (慣性計測装置) MPU6886 を追加した上位機種 ATOM Matrix には 25個の Neo Pixel が実装されているが、 Neo Pixel の本領を発揮するには数千〜数万個 (数百万個?) の LED が欲しいところ。

なお、ここまで書いておいてアレだが、 Pi Pico 用に提供される MicroPython のファームウェアUART シリアルで REPL できない。 ports/rp2/mpconfigport.h の設定を一箇所書き換え (MICROPY_HW_ENABLE_UART_REPL を 1 に変更) て、 ファームウェアをビルドし直す必要がある。

diff --git a/ports/rp2/mpconfigport.h b/ports/rp2/mpconfigport.h
index ccf11d4..2c59047 100644
--- a/ports/rp2/mpconfigport.h
+++ b/ports/rp2/mpconfigport.h
@@ -35,7 +35,7 @@
 
 // Board and hardware specific configuration
 #define MICROPY_HW_MCU_NAME                     "RP2040"
-#define MICROPY_HW_ENABLE_UART_REPL             (0) // useful if there is no USB
+#define MICROPY_HW_ENABLE_UART_REPL             (1) // useful if there is no USB
 #define MICROPY_HW_ENABLE_USBDEV                (1)
 
 // Memory allocation policies

この手の設定変更はソースを書き換えるのではなく、 環境設定ファイルの変更だけで済むようにしたほうがいいと思うのだけど...

ビルドには (当然ながら) クロスコンパイル環境が必要。 といっても既に Arduino IDE を利用して Pi Pico 用のプログラムを開発している PC なら Pi Pico のバイナリを生成できるクロスコンパイル環境がインストールされているハズで、 ~/.arduino15/packages/rp2040/tools/pqt-gcc 以下の gcc を PATH に含めるだけでよい。 ファームウェアの再ビルドというと紆余曲折がつきものだが、 あっさりビルドできてしまって拍子抜け。 ports/rp2/build-PICO/firmware.uf2 が得られるので、 Pi Pico の BOOTSELボタンを押しながら PC に接続したときに現れるストレージへ、 この firmware.uf2 ファイルをコピーするだけでファームウェアの書き換えが完了。

以下、Linux サーバ上でのビルド作業の記録 (一部省略):

senri:/usr/local/src $ git clone https://github.com/micropython/micropython.git
Cloning into 'micropython'...
   ・・・
senri:/usr/local/src $ cd micropython
senri:/usr/local/src/micropython $ git submodule update --init -- lib/pico-sdk
Submodule 'lib/pico-sdk' (https://github.com/raspberrypi/pico-sdk.git) registered for path 'lib/pico-sdk'
Cloning into '/usr/local/src/micropython/lib/pico-sdk'...
Submodule path 'lib/pico-sdk': checked out 'bfcbefafc5d2a210551a4d9d80b4303d4ae0adf7'
senri:/usr/local/src/micropython $ git submodule update --init -- lib/tinyusb
Submodule 'lib/tinyusb' (https://github.com/hathach/tinyusb) registered for path 'lib/tinyusb'
Cloning into '/usr/local/src/micropython/lib/tinyusb'...
Submodule path 'lib/tinyusb': checked out 'd49938d0f5052bce70e55c652b657c0a6a7e84fe'

senri:/usr/local/src/micropython $ export PATH=/home/sengoku/.arduino15/packages/rp2040/tools/pqt-gcc/1.3.1-a-7855b0c/bin:$PATH
senri:/usr/local/src/micropython $ make -C mpy-cross
   ・・・
LINK mpy-cross
   text	   data	    bss	    dec	    hex	filename
 269329	    776	    896	 271001	  42299	mpy-cross
make: Leaving directory '/usr/local/src/micropython/mpy-cross'
senri:/usr/local/src/micropython $ cd ports/rp2
senri:/usr/local/src/micropython/ports/rp2 $ make
[ -d build-PICO ] || cmake -S . -B build-PICO -DPICO_BUILD_DOCS=0 -DMICROPY_BOARD=PICO
PICO_SDK_PATH is /usr/local/src/micropython/lib/pico-sdk
Defaulting PICO_PLATFORM to rp2040 since not specified.
Defaulting PICO platform compiler to pico_arm_gcc since not specified.
PICO compiler is pico_arm_gcc
-- The C compiler identification is GNU 10.3.0
-- The CXX compiler identification is GNU 10.3.0
-- The ASM compiler identification is GNU
-- Found assembler: /home/sengoku/.arduino15/packages/rp2040/tools/pqt-gcc/1.3.1-a-7855b0c/bin/arm-none-eabi-gcc
PICO target board is pico.
Using board configuration from /usr/local/src/micropython/lib/pico-sdk/src/boards/include/boards/pico.h
-- Found Python3: /usr/bin/python3 (found version "3.9.6") found components: Interpreter 
TinyUSB available at /usr/local/src/micropython/lib/tinyusb/src/portable/raspberrypi/rp2040; adding USB support.
Found User C Module(s): 
-- Configuring done
-- Generating done
-- Build files have been written to: /usr/local/src/micropython/ports/rp2/build-PICO
make -s -C build-PICO
   ・・・
[ 99%] Linking CXX executable firmware.elf
   text	   data	    bss	    dec	    hex	filename
 280172	     88	 201652	 481912	  75a78	/usr/local/src/micropython/ports/rp2/build-PICO/firmware.elf
[100%] Built target firmware

git clone でソースプログラム一式をダウンロードして、 make -C mpy-cross で MicroPython のクロスコンパイラをビルドして、 make で ARM EABI (Embedded Application Binary Interface) 版 MicroPython をビルド。

8/26 追記:

前掲の Python プログラムを C で書き直してみた。 ESP-IDF は有難いことに UART select() をサポートしているのでスレッドを使わずに USB ⇔ UART 中継プログラムが書けるが、 UART がハードウェア フロー制御 (RTS CTS) を使えないと嬉しさ半減といったところ。

#include <stdio.h>
#include <sys/fcntl.h>
#include <sys/errno.h>
#include <sys/unistd.h>
#include <termios.h>
#include <sys/select.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_vfs_dev.h"
#include "driver/uart.h"

static const char* TAG = "usb_uart";

const int buf_max = 256;

void app_main(void)
{
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity    = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_APB,
    };

    ESP_ERROR_CHECK(uart_param_config(UART_NUM_0, &uart_config));
    ESP_ERROR_CHECK(uart_driver_install(UART_NUM_0, 2*1024, 0, 0, NULL, 0));

    ESP_ERROR_CHECK(uart_param_config(UART_NUM_1, &uart_config));
    ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, 21, 25, -1, -1));
    ESP_ERROR_CHECK(uart_driver_install(UART_NUM_1, 2*1024, 0, 0, NULL, 0));

    while (1) {
        int fd[2];
	int len[2];
	char buf[2][buf_max];
	fd_set rin, win, ein;
	fd_set rout, wout, eout;
	int i;

        if ((fd[0] = open("/dev/uart/0", O_RDWR)) == -1) {
            ESP_LOGE(TAG, "Cannot open UART 0");
            usleep(5000000);
            continue;
        }
        if ((fd[1] = open("/dev/uart/1", O_RDWR)) == -1) {
            ESP_LOGE(TAG, "Cannot open UART 1");
	    close(fd[0]);
            usleep(5000000);
            continue;
        }
	FD_ZERO(&rin);
	FD_ZERO(&win);
	FD_ZERO(&ein);
	for (i=0; i < 2; i++) {
	    struct termios termios;
	    esp_vfs_dev_uart_use_driver(i);
	    fcntl(fd[i], F_SETFL, O_NONBLOCK);
	    FD_SET(fd[i], &rin);
	    tcgetattr(fd[i], &termios);
	    termios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
				 | INLCR | IGNCR | ICRNL | IXON);
	    tcsetattr(fd[i], TCSANOW, &termios);
	}

        while (1) {
            int ret;
	    int i;
	    struct timeval tv = {
		.tv_sec = 1,
		.tv_usec = 0,
	    };
	    rout = rin;
	    wout = win;
	    eout = ein;
            ret = select(FD_SETSIZE, &rout, &wout, &eout, &tv);
	    if (ret < 0) break;
	    for (i=0; i < 2; i++) {
		if (FD_ISSET(fd[i], &rout)) {
		    len[i] = read(fd[i], buf[i], buf_max);
		    if (len[i] > 0) {
#ifdef USE_HW_FLOWCTRL
			FD_CLR(fd[i], &rin);
			FD_SET(fd[i], &win);
#else
			write(fd[1-i], buf[i], len[i]);
#endif
		    } else if (len[i] < 0) {
			ESP_LOGE(TAG, "UART %d read error", i);
		    }
		}
#ifdef USE_HW_FLOWCTRL
		if (FD_ISSET(fd[i], &wout)) {
		    int l = write(fd[i], buf[1-i], len[1-i]);
		    if (l == len[1-i]) {
			FD_CLR(fd[i], &win);
			FD_SET(fd[1-i], &rin);
			len[1-i] = 0;
		    } else if (l > 0) {
			memcpy(buf[1-i], buf[1-i]+l, l);
			len[1-i] -= l;
		    } else if (l < 0) {
			ESP_LOGE(TAG, "UART %d write error", i);
		    }
		}
#endif
	    }
	}
	ESP_LOGE(TAG, "Select failed: errno %d", errno);
	close(fd[0]);
	close(fd[1]);
	usleep(5000000);
    }
}

前掲の Python プログラムと同様、 GPIO 21番ピンを UART1 TX に、 GPIO 25番ピンを UART1 RX として使用する。 RTS CTS は使用しないが、 115200bps くらいなら取りこぼしも起きないようなので実用上の問題は無い?

ATOM Lite を USB シリアル変換アダプタ (もちろん C 版) として使って、 Thonny で Pi Pico にアクセスしてみた。 ふつうに開発できる。 PC で作成した Python プログラムをアップロードできるし、 プログラムの実行を contorl-C で止めることもできる。 ATOM Lite 経由で Pi Pico へアクセスしていることを忘れそう...

Thonny Pi Pico via ATOM Lite

MICROPY_HW_ENABLE_UART_REPL の値を 1 にしてビルドした MicroPython なので、 バージョンが v1.16-236-gb51e7e9-dirty などと末尾に「-dirty」が付いている。

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment