見出し画像

M5Dialでモジュラーシンセ用のクロックモジュールを作る(はんだ付け、ブレッドボードの必要ナシ!)

最近、M5Dialというロータリーエンコーダーにディスプレイとボタンがついたデバイスを見つけたんです。
このくらいコンパクトにまとまっているの良いなー、拡張モジュールでいい感じのDACあれば嬉しいなーって思って調べてみると新しいDACモジュールが発売されていて、しかも出力電圧は5Vレンジ、10Vレンジを選択可能できて、なおかつ15bit分解能というかなり嬉しい仕様のものを見つけました。
早速これを使って簡単にモジュラーシンセ用のクロックモジュールを作ってみました。

いわゆる0hpモジュールでパネルなんかを持たず空中配線で他のモジュールと接続するタイプで半田付けの必要もなく、もちろんブレッドボードなんかも必要のない手軽なものです。自作で面倒なのが半田付けと電源を別に用意しなければならないところなんですよね。ちょこちょこ試したいのに大掛かりな準備が必要になってしまうのが何よりも手間(って思っているのは自分だけなのかもしれませんが)
ほぼほぼソフトウェアを書いていくだけなので、はんだ付けをしないと自作感がなくて好きじゃないという人には向かないかもしれませんが、その逆な人にこの組み合わせは結構おすすめです。

ただし、設計した通り信号が出ているのかを確認するのにオシロスコープくらいはあったほうがいいかもしれません。この辺はAndroidをお持ちでしたらラズピコを利用した格安オシロがあったり、電子楽器系でいろいろ使い回すならKORGのNTS-2が小さくて手軽なのでおすすめです。

部材

今回利用するものはスイッチサイエンスとAmazonで揃います。

M5Dialの仕様について

M5Dialはディスプレイとエンコーダーなどが一体型になった筐体にM5Stamp S3というコントローラが組み合わされたものです。
M5Stamp S3にはMCUとしてESP32-S3FN8が搭載されており、デュアルコア、クロック周波数は最高240MHz、RAMは512KB、搭載されているFlashメモリは8MBとなっており、USB接続が可能なほか2.4GHz帯Wi-FiやBLEも搭載されているなど高機能なデバイスです。

M5Dialの本体側には240px * 240pxの1.28インチTFTタッチスクリーンがついているほか、RFIDリーダー、RTC、ブザー、ボタンもついています。電源はUSBから供給できるほかに外部電源を利用することもできて、6Vから36Vの入力電圧に対応しています。12V時の動作電流は82.5mAということでモジュラーシンセ用の12V電源もそのまま利用できそうです。
M5Dialは本体にネジ山が切ってあって45mm系の穴にパカっと嵌め込んでナット(?)で固定できるようです。ユーロラックモジュールにするなら端に1cmくらいの余裕を設けると仮定するなら13hpでいけそう。あとは適当にジャックをパネルに固定すれば良いかと。

このM5DialはM5Stackという開発プラットフォーム群の製品のひとつで、さまざまなセンサーモジュールや出力モジュールを利用可能となっています。これらの拡張モジュールは専用のケーブルを利用して簡単に接続することができるようになっており、プログラムさえ組めばすぐに接続したモジュールの機能を試すことができるようになっています。

以前ラズピコで作業した時にもここのモジュールを使いましたが、その際にはラズピコのモジュールとこれらの拡張用モジュールを接続可能とする治具を介して使っていました。

開発環境

今回はVisual Studio Code + PlatformIO + Arduinoを使用します(と言いつつ途中からCLion使いましたができることはほぼ一緒)
他に基本のArduino StudioやグラフィカルにプログラミングができるUIFlowというものがあったり、ESP32 Arduinoフレームワークの礎となりつつあるESP-IDFという、より高度なプログラミング環境もあったりします。もちろんバイナリを作って書き込めればRustだろうと何だろうと大丈夫。ただ、M5Stackってかなり色々な機能が詰まっているのでおとなしくレールに乗っておいたほうが楽です。

さて、今回はM5Dialのロータリーエンコーダーとディスプレイ、あとボタンを使用します。ほか、DACとの通信にI2Cも利用します。
PlatformIOを利用することで面倒なMakefile、CMakeなどの記述が不要で、ビルドやデバッグ、モニタリングなどがクリックひとつで行えます。パッケージマネージャ機能も強力で、PlatformIOの画面からライブラリを選択してインストールするだけでスルスルとライブラリを利用できます。モダンですね。

Arduinoをベースにプロジェクトを作成するとsetup()やloop()が含まれたソースコードが生成されます。あとはここに初期化とループ処理のロジックを書くだけで良いという便利さ。ここにM5Dialライブラリさえ読み込めば、M5製品向けの統合ライブラリであるM5Unified、描画系ライブラリのM5GFXも読み込まれて大抵のことは行えるようになっています。

m5stack/M5Dial

からサンプルを見たり、その先のソースをちょっと追っていくだけである程度のものは作れるようになるんじゃないでしょうか。

プログラムが出来上がったらPlatformIOの「Upload and Monitor」をクリックするだけでビルドからM5Dialへの書き込み、プログラムのモニタまでやってくれます。
書き込みがうまくいかなかったらM5Dialの裏面にあるリセットボタンを押したり、もしくはM5Stampのほうのボタンを押しながらResetボタンを押したりすると書き込めるようになるんじゃないかと。

PlatformIOを使ってプログラミングするときにM5Dialからシリアルで文字列などを受け取って表示したい場合はplatformio.iniファイルのbuild_flagsの設定をそれ用に設定しておく必要があります。今回は

[env:m5stack-stamps3]
platform = espressif32
board = m5stack-stamps3
framework = arduino
build_flags =
	-DARDUINO_USB_MODE=1
	-DARDUINO_USB_CDC_ON_BOOT=1
lib_deps = 
	m5stack/M5Dial@^1.0.2
	m5stack/M5GFX@^0.1.16
	m5stack/M5Unified@^0.1.16
	dfrobot/DFRobot_GP8XXX@^1.0.1

こんな感じ。-DARDUINO_USB_CDC_ON_BOOT=1というのがないとコンピュータ側でデバイスのシリアル出力を受け取れません。

Arduino用のbuild_flagsの一覧はこちら。
arduino-esp32/platform.txt at master · espressif/arduino-esp32

ソースコード一式

こちらにまとめておきました。タスクごとにライブラリに分けてあります。EncoderHelperとかDisplayHelperなど安易にHelperなんて名前を使ったのはちょっと失敗(並べると可読性が悪い)ですがそのままにしておきます

euskace / M5Dial · GitLab

設計と実装

とりあえずシンプルなクロックだけですのでめちゃめちゃ単純です。Arduinoのフレームワークを利用しているのでsetup()やloop()の中に素直にロジックを押し込んでいっても良いのですが、せっかくFreeRTOSが使えるのでそちらのスケジューラやタスク管理機能を使っていきます。処理内容は

  • エンコーダーによるBPMアップデート処理

  • 現在のBPMを表示する処理

  • DACに電圧を出力させる処理

と、この程度です。
ファンクションジェネレーターなど高機能なものを作る際にはもっと複雑になりますが、ひとまずはこれだけ。

ちなみにChatGPTを利用するのであればFreeRTOS用コード生成の精度がかなり悪いので一度Arduino用に仕上げてからFreeRTOSの機能を使うように修正するのが良いかも。

M5Dialライブラリの概要

M5Dialライブラリのヘッダファイルがうまい具合にまとめられているので一読をお勧めします。実装ファイルも中身はシンプルです。

namespace m5 {
class M5_DIAL {
   private:
    /* data */

   public:
    void begin(bool enableEncoder = false, bool enableRFID = false);
    void begin(m5::M5Unified::config_t cfg, bool enableEncoder = false,
               bool enableRFID = false);

    M5GFX &Display         = M5.Display;
    M5GFX &Lcd             = Display;
    Touch_Class &Touch     = M5.Touch;
    Power_Class &Power     = M5.Power;
    RTC8563_Class &Rtc     = M5.Rtc;
    Speaker_Class &Speaker = M5.Speaker;
    Button_Class &BtnA     = M5.BtnA;

    /// for internal I2C device
    I2C_Class &In_I2C = m5::In_I2C;

    /// for external I2C device (Port.A)
    I2C_Class &Ex_I2C = m5::Ex_I2C;

    ENCODER Encoder = ENCODER(DIAL_ENCODER_PIN_A, DIAL_ENCODER_PIN_B);
    MFRC522 Rfid    = MFRC522(RC522_I2C_ADDRESS);
    void update(void);
};

}

FreeRTOSとは

RealTime OSのなかでもかなり軽量なもので、特に組み込みシステム向けに設計されています。

Beginner's guides to FreeRTOS

このうち今回使うのはxTaskCreatevTaskDelayといったタスク関連の機能くらいです。

FreeRTOS Documentation

こちらにFreeRTOSのドキュメントがふたつ、リファレンスマニュアルとハンズオン資料があります。どちらも大体400ページくらいなのでそれほど多い分量ではありません。中を見るとわかりますが、結構シンプルで理解しやすいです。

動画ですとDigiKeyのRTOS入門シリーズがオススメ!
Introduction to RTOS Part 1 - What is a Real-Time Operating System (RTOS)? | Digi-Key Electronics

ESP32-S3

ESP32シリーズのうち、デュアルコアでなかなか高性能なMCU。こういったIoT系のチップは、ADCやDACを多く積んだりで多くのノブや出力を扱えてシンセにピッタリなSTM32系とは違ってやはり入出力系が弱いんですが、I2Cでうまくカバーできる感じ(とはいえスピードにはやはり限りがあります)

esp32-s3_datasheet_en

エンコーダーによるBPMアップデート

さて、実際に機能を作っていきます。まずはBPMをエンコーダーの回転により調節できる機能を作ります。エンコーダーを早く回せばカウントアップも早く、ゆっくり回せばカウントアップはゆっくりとなるようにと思いましたが、元のままで十分良さそうだったのでそのままに。あとでバグ見つけたんですけど、BPMを0にするとリセットかかっちゃうのでBPMのmin-max値を指定しておくと良いですね。

FreeRTOSのxTaskCreateとvTaskDelayは以下のように使っています。タスクで登録した関数の中で無限ループを回して、その中でvTaskDelayを呼んでその間他のタスクをFreeRTOSに任せる、みたいな感じでしょうか。
Arduinoだとloop()の中で一通りの処理を行う必要があり、処理タイミングなどの見極めが面倒だったりしましたがFreeRTOSのタスク管理機能を使うことでそういったことから解放されるのが嬉しいです。

void EncoderHelper::startTask() {
    xTaskCreate(encoderTaskFunction, "EncoderTask", 2048, this, 1, nullptr);
}

[[noreturn]] void EncoderHelper::encoderTaskFunction(void* param) {
    auto* instance = static_cast<EncoderHelper*>(param);
    while (true) {
        instance->updateEncoder();
        vTaskDelay(5 / portTICK_PERIOD_MS);
    }
}

いちおうボタンの動作もつけてみました

if (M5Dial.BtnA.wasClicked()) {
    Serial.println("BtnA single clicked");
    M5Dial.Encoder.write(DEFAULT_BPM);
}
if (M5Dial.BtnA.wasDoubleClicked()) {
    Serial.println("BtnA double clicked");
    M5Dial.Encoder.write(DEFAULT_BPM);
}
if (M5Dial.BtnA.pressedFor(5000)) {
    Serial.println("Reset");
    M5Dial.Encoder.write(DEFAULT_BPM);
    M5Dial.BtnA.setRawState(0, false);
}

クリックに加え、ダブルクリックや長押しなんかにも対応していてなるほどなという感じ。

BPMの表示処理

M5.display.some_drawing_function()のようなものを愚直に書いていくとちらつきの問題にすぐにぶち当たってしまいます。
そこでCanvasというものを使って、一旦表示するものを全て準備してからエイっとディスプレイに転送できる機能を利用することになります。実はこのCanavsが曲者で2週間くらい悩んでました。
今回は今後のことも考えて背景キャンバスの上にもう一枚キャンバスを乗せていて、テキスト用のキャンバスの背景色を拍のタイミングで赤くして、その赤みを薄めていくような処理を入れつつ、その上にBPMの表示を載せました。

m5-docsのCanvasの説明

updateDisplay()の最後でpushSprite()を使って一気に出力することでちらつきを抑えられるようです。

display.startWrite();
backgroundCanvas.pushSprite(0,0);
display.endWrite();

DACにBPMを出力する処理

今回用意したのは2ch DACのUNIT-DAC2です。
UNIT-DAC2の製品ページ
datasheet: GP8413

これを利用するためにはPlatformIOのLibrariesからDFRobotで検索してDFRobot_GP8XXXというライブラリを追加しておきます。

モジュラーシンセのクロックの仕様なんですがひとまず5Vというのは良いとして、デューティサイクルというかdurationがどのくらいになるかまでは調べてもわからなかったんでひとまず1拍につき20msくらい電圧を出力することにしています。

DACとM5Dialとの通信にはI2Cを利用します。
DACのアドレスは製品ごとにデフォルトの値が決まっていて、このDACでは0x59が利用されます。
ピンアサインはM5Dialの場合、SDAが13、SCLが15です。
これらの情報はDACの製品ページに記載されています。

サンプルとして参考にしたのは以下のリポジトリ

M5Stack/examples/Unit/DAC2_GP8413/DAC2_GP8413.ino at master · m5stack/M5Stack
M5CoreS3/examples/Unit/DAC2_GP8413/DAC2_GP8413.ino at main · m5stack/M5CoreS3
DFRobot/DFRobot_GP8XXX

組み立てと動作確認

組み立てというほど大げさなものでもなく、コネクタを接続したり、ターミナルブロックでワイヤをかしめたりする程度です。そして出来上がったのがこちら。Pamelaに繋いで確認するとクロックを正しく受け取ってはいるようです。

次にKORGのNTS-2でも動作確認

気になっているところ

上でも書きましたがDACを直でジャックに接続していてバッファ&フィルタ回路がないところです。えぇぇぇぇ?と思った方もいるでしょうね。
手軽さを一番に考えて作成したのでそのような回路はつけていませんが、本来であればそういった回路を接続して出力信号を安定させるべきなのかなと。

今後の拡張

すぐにできる拡張として実はこのDACにはもう一つ出力がついているのでReset信号をつけてあげてもいいかなと

それと、今回はM5DialのPortAにひとつのDACを接続して実験しましたが、このPortAに接続できるハブというのがあってもっと多くの拡張モジュールを接続することができます。それを使ってADC経由で外部からのCVを読み取ったり、DACをもっと繋げてより多くの信号を出力できそうなのでそういうのもやってみたいなーって思っています。

ちなみに同一のDACを複数繋げる場合にDACデバイスのアドレスを変更して重複しないようにする必要があります。
変更はGP8413のA0、A1、A2ピンの短絡の組み合わせで行うものと思われます。なんとか以下のファイルにメモ書き的なものを見つけました。おそらくDACデバイスを開けるとアクセスできるんじゃないかと。

DFRobot_GP8XXX/examples/GP8413outputData/GP8413outputData.ino at master · DFRobot/DFRobot_GP8XXX

#include <DFRobot_GP8XXX.h>
/**************************
----------------------------
| A2 |  A1 | A0 | i2c_addr |
----------------------------
| 0  |  0  | 0  |   0x58   |
----------------------------
| 0  |  0  | 1  |   0x59   |
----------------------------
| 0  |  1  | 0  |   0x5A   |
----------------------------
| 0  |  1  | 1  |   0x5B   |
----------------------------
| 1  |  0  | 0  |   0x5C   |
----------------------------
| 1  |  0  | 1  |   0x5D   |
----------------------------
| 1  |  1  | 0  |   0x5E   |
----------------------------
| 1  |  1  | 1  |   0x5F   |
----------------------------
***************************/
DFRobot_GP8413 GP8413(/*deviceAddr=*/0x58);

あとESP32-S3はWi-Fiが利用できるのでAbleton Linkと接続してテンポ情報などを同期できるようにというのも面白そうです。正式なポートではないもののシーケンサやサンプラーなどを作っているTosro Electronicsの中の人が数年前にESP32でAbleton Linkを動かそうとした試みなんかもあったりします。

感想的なもの

ちょっとディスプレイのところでまごついたくらいでスルスルと書けたこともあって、手軽で良いなと思いました。特にFreeRTOSを利用することで機能の分離なども簡単になってコードを再利用しやすくできるのがいいですね。DACの数が足りない点も拡張モジュール用のハブを利用することでうまく解決できそうなところも素敵です