M5Atomic Echo BaseをModdable SDKで動かす (格闘編)

# はじめに

M5Atomシリーズ(<A HREF="https://docs.m5stack.com/ja/core/AtomS3">M5AtomS3</A>)と<A HREF="https://docs.m5stack.com/ja/atom/Atomic%20Echo%20Base">M5Atomic Echo Base</A>を組み合わせて、小さい<A HREF="https://protopedia.net/prototype/2345">ｽﾀｯｸﾁｬﾝ</A>を動かすまでの過程を備忘メモに残します。

Moddable SDKでターゲットデバイスを追加する時の一例として参考にしていただければ幸いです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/60252f4d-efe7-4d89-a64d-7ffa52baacb7.jpeg" width="50%">


# ゴール

Node-RED MCU版(Moddable SDK版)の小さいｽﾀｯｸﾁｬﾝを動かすことが目的です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/9892a3bd-95fe-4087-9b3e-819292afa5b4.jpeg" width="50%">


# サンプルコードを動かす

## Arduinoのサンプルコード

Arduinoライブラリとサンプルコードが用意されています。

https://docs.m5stack.com/en/arduino/projects/atomic/atomic_echo_base

サンプルコードを動かす際に注意点がありました。

 * ESP32のArduinoライブラリは2.0.x系を使用する(ESP-IDF 4.4系を使用する)
   * 3.0.x系はESP-IDF 5.x系が使用されているため、ビルドでエラーが出る

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/ddee33e3-ef80-4c84-b8bf-2fdcce6820dc.png" width="25%">

 * M5Atomic-EchoBaseのライブラリ(1.0.0)の内容の一部(PI4IOESV6408チップのI2Cアドレス)が間違っている
   * GitHubの内容(最新)は修正されているが、リリースパッケージが更新されていない

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/b7c31e7b-b28e-46f4-ae9f-2363f5485c3f.png" width="25%"><BR>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/1b6431e1-1ff1-42f7-b0b0-647357036799.png" width="50%"><BR>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/4d396ece-4848-408b-8b97-7bbad981dbda.png">

https://github.com/m5stack/M5Atomic-EchoBase/commit/5e44a6759e5ea2c8f320b4fd2f6877961b1938fd


## UIFlow2のサンプルコード

UIFlow2(Micropython)のサンプルコードが用意されています。

https://uiflow-micropython.readthedocs.io/en/2.2.1/base/echo.html

実装はGitHubのソースコードを参考にしました。

https://github.com/m5stack/uiflow-micropython/blob/master/m5stack/libs/base/echo.py

https://github.com/m5stack/uiflow-micropython/blob/master/m5stack/libs/driver/es8311/


# Moddable SDKで実装する

過去にターゲットデバイスの追加や、液晶ディスプレイドライバの初期化処理のデバッグを行ったことがあったので、安直に考えていましたが間違いでした。
サンプルコードを参考に実装しましたが、デバイスから音が鳴りません。


## ロジアナで解析する

まずはI2Cの実装が合っているか、ロジアナ(激安USBロジアナ、sigrok、PulseView)を使用して確認しました。

(参考)
https://qiita.com/nanbuwks/items/9069165bfb66d8836583

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/f0b580eb-586d-4e50-b896-4279b5e58dd8.jpeg" width="50%">

ES8311とPI4IOE5V6408のコマンドシーケンスを追いかけます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/6b7d1415-802e-4903-974e-edba88841cb3.jpeg" width="50%">

I2Cのコマンドシーケンスは同じでも音が出ませんでした。
(たまに音が鳴っても、虫の音ほど微かに聞こえる程度)

次にI2Sの信号も確認しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/73c8a662-02c2-44b7-852c-1caacb4fc3fa.jpeg" width="50%">


## デバッグ作業

ES8311はモノラルオーディオデコーダーなのでR(右)の音声データの有無は関係ないと先入観を持っていましたが、最終的に気がついたのはI2Sの信号で、送信される音声データに差分(LRのデータと、Lのみのデータ)がありました。

![ok.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/8aa16115-a7b7-483f-aebb-76ccc2fa0f3d.jpeg)

また、後になって気づいたことですが、起動(初期化)後の数秒(1〜2秒)は音が出ない仕様のようです。

![init_muon.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/ddfb988e-c024-4b73-a63c-568db14c72d3.jpeg)


## 動いた

L(左)の音声データをR(右)にも送信することで音が鳴るようになりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/5cda259a-8074-40a2-a165-e4750582e42e.jpeg" width="50%">

https://youtu.be/7ZA_K5Ed_aE

ESP-IDFのソースコードでは、I2SのslotはI2S_STD_SLOT_LEFTが指定されていることを確認しました。
これをI2S_STD_SLOT_BOTHに変更すればLR(左右)のデータとして出力されそうです。
Moddable SDKではmanifest.jsonで設定できることを確認しました。

```json:manifest.json
"i2s": {
    "slot": "I2S_STD_SLOT_BOTH"
}
```


## コードの整理

起動(初期化)後の数秒(1〜2秒)は音が出ない仕様については、最初に1秒間の無音データ(16000サンプルデータ)を挿入することで問題を回避しました。

M5AtomS3 + M5Atomic Echo Baseのターゲットファイル(m5atom_s3_echo_base)を例に差分コードを示します。

```json:manifest.json
% diff manifest.json ../m5atom_s3/manifest.json 
19,21d18
< 		"setup/target": "./setup-target",
< 		"pins/audioout": "$(MODULES)/pins/i2s/*",
< 		"pins/smbus": "$(MODULES)/pins/smbus/smbus",
24,25d20
< 			"$(MODULES)/pins/audioin/*",
< 			"$(MODULES)/pins/audioin/esp32/*",
27c22,23
< 		]
---
> 		],
> 		"setup/target": "./setup-target"
30,31d25
< 		"pins/audioout",
< 		"audioin",
38,42c32
< 		"touch": "",
< 		"startupSound": "bflatmajor.maud",
< 		"es8311": {
< 			"volume": 160
< 		}
---
> 		"touch": ""
44,46d33
< 	"resources": {
< 		"*": "$(MODDABLE)/examples/assets/sounds/bflatmajor"
< 	},
111,135d97
< 		},
< 		"audioOut": {
< 			"bitsPerSample": 16,
< 			"numChannels": 1,
< 			"sampleRate": 11025,
< 			"volume_divider": 1,
< 			"i2s": {
< 				"num": 1,
< 				"slot": "I2S_STD_SLOT_BOTH",
< 				"bitsPerSample": 16,
< 				"bck_pin": 8,
< 				"lr_pin": 6,
< 				"dataout_pin": 5
< 			}
< 		},
< 		"audioIn": {
< 			"sampleRate": 11025,
< 			"bitsPerSample": 16,
< 			"i2s": {
< 				"num": 0,
< 				"bck_pin": 8,
< 				"lr_pin": 6,
< 				"datain": 7,
< 				"pdm": 1
< 			}
```

```javascript:setup-target.js
1c1
< //import Digital from "pins/digital";
---
> import Digital from "pins/digital";
9,11d8
< import AudioOut from "pins/audioout";
< import Resource from "Resource";
< import SMBus from "pins/smbus";
13,18d9
< const INTERNAL_I2C = Object.freeze({
< 	sda: 38,
< 	scl: 39,
< 	hz: 100000
< });
< 
41,64d31
<         // start-up sound
< 	if (config.startupSound) {
< 		const speaker = new AudioOut({streams: 1});
< 
<                 speaker.callback = function () {
< 			this.stop();
< 			this.close();
< 			Timer.set(this.done);
< 		};
< 		speaker.done = done;
< 		done = undefined;
< 
<                 new ES8311();
<                 new PI4IOE5V6408();
< 
< 		speaker.enqueue(0, AudioOut.Silence, 16000); // enqueue silence for no audio output (1 sec) of ES8311 I2S initial setup
<        		speaker.enqueue(0, AudioOut.Samples, new Resource(config.startupSound), 1);
< 		speaker.enqueue(0, AudioOut.Callback, 0);
< 		speaker.start();
< 
<         }
< 
< 
< 	// accelerometer and gyrometer
69c36
<         done?.();
---
>         done();
72,149d38
< class ES8311 {
< 	// initialize ES8311(0x18)
< 	// https://github.com/m5stack/uiflow-micropython/blob/master/m5stack/libs/driver/es8311/__init__.py
<         constructor(options) {
<                 const es = new SMBus({
<         		...INTERNAL_I2C,
<         		address: 0x18,
<         	});
<         	es.writeByte(0x00, 0x1F); // ES8311_RESET_REG00
<         	Timer.delay(20);
<         	es.writeByte(0x00, 0x00); // ES8311_RESET_REG00
<         	es.writeByte(0x00, 0x80); // ES8311_RESET_REG00
< 
<         	// clock config
<         	es.writeByte(0x01, 0xBF); // ES8311_CLK_MANAGER_REG01
<         	es.readByte(0x06); // ES8311_CLK_MANAGER_REG06
<         	es.writeByte(0x06, 0x03); // ES8311_CLK_MANAGER_REG06
<         	es.readByte(0x02); // ES8311_CLK_MANAGER_REG02
<         	es.writeByte(0x02, 0x10); // ES8311_CLK_MANAGER_REG02
<         	es.writeByte(0x03, 0x10); // ES8311_CLK_MANAGER_REG03
<         	es.writeByte(0x04, 0x10); // ES8311_CLK_MANAGER_REG04
<         	es.writeByte(0x05, 0x00); // ES8311_CLK_MANAGER_REG05
<         	es.readByte(0x06); // ES8311_CLK_MANAGER_REG06
<         	es.writeByte(0x06, 0x03); // ES8311_CLK_MANAGER_REG06
<         	es.readByte(0x07); // ES8311_CLK_MANAGER_REG06
<         	es.writeByte(0x07, 0x00); // ES8311_CLK_MANAGER_REG07
<         	es.writeByte(0x08, 0xFF); // ES8311_CLK_MANAGER_REG08
< 
<         	// format config
<         	es.readByte(0x00); // ES8311_RESET_REG00
<         	es.writeByte(0x00, 0x80); // ES8311_RESET_REG00
<         	es.writeByte(0x09, 0x10); // ES8311_SDPIN_REG09
<         	es.writeByte(0x0A, 0x10); // ES8311_SDPOUT_REG0A
< 
<                 //
<         	es.writeByte(0x0D, 0x01); // ES8311_SYSTEM_REG0D
<         	es.writeByte(0x0E, 0x02); // ES8311_SYSTEM_REG0E
<         	es.writeByte(0x12, 0x00); // ES8311_SYSTEM_REG12
<                 es.writeByte(0x13, 0x10); // ES8311_SYSTEM_REG13
<                 es.writeByte(0x1C, 0x6A); // ES8311_ADC_REG1C
<                 es.writeByte(0x37, 0x08); // ES8311_DAC_REG37
< 
<         	// set volume (0 - 256)
< 		let volume = config.es8311.volume ?? 128;
< 		if (volume < 0) volume = 0;
< 		if (volume > 255) volume = 255;
<         	es.writeByte(0x32, volume); // ES8311_DAC_REG32
< 
<         	// microphone
<                 es.writeByte(0x17, 0xFF); // ES8311_ADC_REG17
<                 es.writeByte(0x14, 0x1A); // ES8311_SYSTEM_REG14
< 
<         	es.close();
<         }
< }
< 
< class PI4IOE5V6408 {
< 	// initialize PI4IOE5V6408(0x43)
< 	// https://github.com/m5stack/uiflow-micropython/blob/master/m5stack/libs/base/echo.py
<         constructor(options) {
<                 const pi = new SMBus({
<         		...INTERNAL_I2C,
<                         address: 0x43,
<                 });
< 	        pi.readByte(0x00); // PI4IOE_REG_CTRL
<         	pi.writeByte(0x07, 0x00); // PI4IOE_REG_IO_PP
<         	pi.readByte(0x07);
<         	pi.writeByte(0x0D, 0xFF); // PI4IOE_REG_IO_PULLUP
<         	pi.writeByte(0x03, 0x6E); // PI4IOE_REG_IO_DIR
<         	pi.readByte(0x03);
<         	pi.writeByte(0x05, 0xFF); // PI4IOE_REG_IO_OUT
<         	pi.readByte(0x05);
< 
<         	pi.close();
<         }
< }
```


# プルリク

2025年5月11日、プルリク(Pull Request)を送りました。

https://github.com/Moddable-OpenSource/moddable/pull/1498

次回のリリース(5.7.0)にマージされると普通に利用できるようになります。

それまで待てないという方は
$MODDABLE/build/devices/esp32/targets
ディレクトリにターゲットファイルを配置(ディレクトリを作成)してください。


# 最後に

Node-RED MCUでも動くことを確認しました。

<A HREF="https://github.com/ralphwetzel/node-red-mcu-plugin">node-red-mcu-plugin</A>の修正(ターゲットデバイスの定義追加)が必要です。
プルリクがマージされるまでお待ちください！

```javascript:mcu_plugin.js
...
   let platform_identifiers = [
   (以下に行を追加)
     'esp32/m5atom_s3_echo_base',
```

![noderedmcu.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72479/0bbcd8e4-0c66-4285-8ef4-c01c88e09d68.jpeg)


# 追記 (2025年5月24日)

マイクも動くようになりました！

https://x.com/Zakkiea/status/1924119369824837961

https://github.com/Moddable-OpenSource/moddable/pull/1501