M5Stack RCA Module に搭載されている I2S (PCM5102APWR) を、Rust で生成した PCM 波形で発音させたメモです。
波形の生成は libymfm.wasm として WebAssembly 向けに作成している Rust 製のシーケンサーとサウンドチップエミュレーションをそのまま esp-idf に持ってきて xtensa-esp32-espidf ビルドしています。また、libymfm.wasm は C++ でつくられた FM 音源エミュレータの ymfm もリンクしています。
ESP32 Xtensa の Rust は初挑戦でしたが、確認した範囲で問題なくすんなり動作しました。ESP32 Xtensa Rust ツールチェインの準備やビルドは大変な印象がありましたが、昨今は esp-rs の各プロジェクトを組み合わせることで簡単に設定できるようになっています。
ちなみに ESP32-S3 開発ボードでも軽く動かしてみましたが問題なさそうです。ESP-S3 では PSRAM 80MHz Octa 設定にしているのが効いたか、波形生成は 1.4 倍程度高速でした。(Rust モジュールの細かなメモリ配置については未調査で課題としています)
ということで、この記事には以下の内容が含まれます。
- I2S PCM5102APWR のイニシャライズ方法
- ESP32 Xtensa Rust のビルド
ソースコードは以下のリポジトリから見ることができます。詳しくはソースを見ていただくのが早いかもしれません。
https://github.com/h1romas4/m5stack-chipstream
This is a test to port C++’s ymfm and Rust’s vgmplay to ESP32(Xtensa).
Rust でつくられた VGM パーサーと SEGAPCM エミュレーションで I2S を発音させてるデモ:(残念ながら今回のビルドでは、ymfm の FM 音源エミュレーションは ESP32 で処理速度が間に合いませんでした)
I2S PCM5102APWR のイニシャライズ
M5Stack Core2 に接続した RCA Module の PCM5102A の i2s_config と pin_config は以下のように設定すると良いようです。
// i2s_driver_install
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = sample_rate,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = dma_buf_count,
.dma_buf_len = dma_buf_len,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = I2S_PIN_NO_CHANGE
};
ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL));
// i2s_set_pin
i2s_pin_config_t i2s_pin_config = {
.mck_io_num = GPIO_NUM_0,
.bck_io_num = GPIO_NUM_19,
.ws_io_num = GPIO_NUM_0,
.data_out_num = GPIO_NUM_2,
.data_in_num = I2S_PIN_NO_CHANGE
};
ESP_ERROR_CHECK(i2s_set_pin(I2S_NUM_1, &i2s_pin_config));
communication_format
は必ず I2S_COMM_FORMAT_STAND_I2S
を設定のこと。
うっかり I2S_COMM_FORMAT_STAND_MSB
を設定するとなんとなく発音するものの、送信する PCM の先頭 1bit が無視されるような動きになってややはまり。(-32768, 32767 のフルスイング矩形波が正しく発音しなくて気が付きました…)
bits_per_sample
を I2S_BITS_PER_SAMPLE_16BIT
、channel_format
を I2S_CHANNEL_FMT_RIGHT_LEFT
することで、int16_t
のステレオ PCM 形式になります。
DMA バッファは、波形生成側のプリレンダのバッファリングに合わせて dma_buf_len
を 1024 で dma_buf_count
を 32個設定して動作させています。一度に扱うサンプル数としては int16_t ステレオにて 256 です。
今回のオーディオ系の実装ですが、波形生成を ESP32 の core 0 で、I2S への PCM の送信を core 1 と FreeRTOS タスクを分割して実行しています。なお、i2s_write にはほとんど時間がかからないようですので、波形生成は core 1 に持ってきても影響ないかもしれません。
タスク間の通信には、esp-idf の FreeRTOS Additions になっている RingBuffer の Byte buffers を介して PCM データの送受信を行っています。
FreeRTOS Additions – Ring Buffers
Byte buffers do not store data as separate items. All data is stored as a sequence of bytes, and any number of bytes can be sent or retrieved each time. Use byte buffers when separate items do not need to be maintained (e.g. a byte stream).
ESP32 Xtensa Rust のビルド方法
Xtensa の Rust ツールチェインは espup
で導入するのが簡単でした。Windows の場合は WSL2 の Ubuntu 22.04 を使うと便利かもです。(sysroot は esp-idf 4.4.3 を使っています)
詳しくはリポジトリに GitHub Actions のビルドを入れていますの参考にしてください。
https://github.com/esp-rs/espup
Tool for installing and maintaining Espressif Rust ecosystem.
espup で Rust を導入すると ~/.rustup/toolchains/esp
に Xtensa の Rust が導入されます。また .espressif/tools/xtensa-esp32-elf-clang
に clang が入ります。
$ ls -laF ~/.rustup/toolchains/esp/bin/
合計 58120
drwxr-xr-x 2 hiromasa hiromasa 4096 2月 19 15:56 ./
drwxr-xr-x 7 hiromasa hiromasa 4096 2月 19 15:56 ../
-rwxr-xr-x 1 hiromasa hiromasa 21897920 2月 19 15:56 cargo*
-rwxr-xr-x 1 hiromasa hiromasa 954888 2月 19 15:56 cargo-clippy*
-rwxr-xr-x 1 hiromasa hiromasa 1796976 2月 19 15:56 cargo-fmt*
-rwxr-xr-x 1 hiromasa hiromasa 11781088 2月 19 15:56 clippy-driver*
-rwxr-xr-x 1 hiromasa hiromasa 759 2月 19 15:56 rust-gdb*
-rwxr-xr-x 1 hiromasa hiromasa 1933 2月 19 15:56 rust-gdbgui*
-rwxr-xr-x 1 hiromasa hiromasa 1072 2月 19 15:56 rust-lldb*
-rwxr-xr-x 1 hiromasa hiromasa 17264 2月 19 15:56 rustc*
-rwxr-xr-x 1 hiromasa hiromasa 11124040 2月 19 15:56 rustdoc*
-rwxr-xr-x 1 hiromasa hiromasa 11906968 2月 19 15:56 rustfmt*
$ ls -laF ~/.espressif/tools/xtensa-esp32-elf-clang/esp-15.0.0-20221201-x86_64-unknown-linux-gnu/esp-clang
合計 44
drwxr-xr-x 11 hiromasa hiromasa 4096 2月 19 15:57 ./
drwxr-xr-x 3 hiromasa hiromasa 4096 2月 19 15:57 ../
drwxr-xr-x 2 hiromasa hiromasa 4096 2月 19 15:57 bin/
drwxr-xr-x 4 hiromasa hiromasa 4096 2月 19 15:57 include/
drwxr-xr-x 8 hiromasa hiromasa 4096 2月 19 15:57 lib/
drwxr-xr-x 2 hiromasa hiromasa 4096 2月 19 15:57 libexec/
drwxr-xr-x 5 hiromasa hiromasa 4096 2月 19 15:57 riscv32-esp-elf/
drwxr-xr-x 9 hiromasa hiromasa 4096 2月 19 15:57 share/
drwxr-xr-x 5 hiromasa hiromasa 4096 2月 19 15:57 xtensa-esp32-elf/
drwxr-xr-x 5 hiromasa hiromasa 4096 2月 19 15:57 xtensa-esp32s2-elf/
drwxr-xr-x 5 hiromasa hiromasa 4096 2月 19 15:57 xtensa-esp32s3-elf/
ちなみに espup でツールチェインを入れると、esp-idf とは別のディレクトリ名に同バージョンの gcc が入るようです。(?)
さて、今回は Rust は波形生成をするライブラリの形で、C/C++ の Arduino Loop をメインとしたかったので、”esp-idf first” という構成としています。components
配下に Rust のプロジェクトをおいて、いつも通り idf.py build
すると Rust ごとビルドしてくれます。
cargo generate で以下のテンプレートを cmake
指定してビルドスクリプトやディレクトリストラクチャーをつくっています。(引数を cargo にすると Rust 中心の構成になります)
cargo generate https://github.com/esp-rs/esp-idf-template cmake
esp-idf first のビルドでは components
の下にいつも通りコンポーネントを配置し、CMakeLists.txt から external project として Rust を追加してビルドする動作するようにつくってくれます。
ビルドさえできてしまえば、あとは std 環境の Rust で、今回は外部ライブラリとしてバイナリーパーサ nom
や JSON シリアライズ serde
などを入れていますが、そのまま動作しました。
一点、Rust から返した *const i16
(16bit) の RAW ポインターを C 側から読むとメモリーの状態によって 1 byte (?) ズレるような動作がありました。いったん C からポインターを渡して Rust 側から書き込むことで修正していますが、Xtensa 特有なのか自分のミスなのかまだ詳しく原因を調査できていません。(Rust 構造体内の配列ポインターなのですがアライメント関連?)
というわけで、ESP32 で Rust std が呼べる。とても便利す。
Rust の特定モジュールを IRAM に載せたり、ヒープアロケータの SRAM/PSRAM コントロール(できる?)など未調査の部分もありますので、引き続きやってみたいと思います。
Nakitbahis, Nakitbahis Giriş, Nakitbahis Giriş