akira.keiのアイコン画像
akira.kei 2026年02月14日作成 (2026年02月14日更新) © MIT
セットアップや使用方法 セットアップや使用方法 閲覧数 26
akira.kei 2026年02月14日作成 (2026年02月14日更新) © MIT セットアップや使用方法 セットアップや使用方法 閲覧数 26

忘れられたUSB内蔵PIC16F1454/5をイマドキ使う(その6):USB-MIDI変換3

<前の記事 : 次の記事>

シリアル通信のリングバッファ

MIDIパーサの苦労話をする前に、シリアル通信のリングバッファの話をまとめておく。USBからは最大で1ms毎に64バイトのデータがやってくる。演奏データはもっと少ないが、システムエクスクルーシブやブレスコントロールなどは大量に送りつけられてくる。つまりUSB側は512kbps位ってことだ。一方のシリアル通信側は31.25kbpsと規格で決まっており16倍くらいの速度差がある。
Copilot調べでは伝統的なUSB-MIDI変換器のバッファサイズ、または典型的なシステムエクスクルーシブのサイズは256バイト程度らしい。PIC16F1454のメモリサイズは1024なので、送信待ちバッファを512バイトに設定した。ちなみに、リングバッファのサイズ自体は2のべき乗である必要は全くないが、カウンタークリアをAND演算でIF分岐なしで可能なのでこうしている。ただ、48MHz駆動だし保有メモリは少ないしメモリを余らせるのももったいないので、本当なら任意サイズにすべきかもしれない。

#define TXRING_BUFFER_SIZE 512 #define TXRING_BUFFER_MASK (TXRING_BUFFER_SIZE-1) typedef volatile struct { uint8_t buf[TXRING_BUFFER_SIZE]; uint8_t rp,wp; } txRingQue_t, *txRingQue_p;

一方で、受信側は1msの間に3バイトしかやってこないので受信バッファは小さくていい。8バイト位でもいいのだが、余ったメモリ量を割り当てたら64バイトになった。上にも書いたが、やはりバッファサイズは任意サイズにしておいた方が良かったかもしれない。

#define RXRING_BUFFER_SIZE 64 #define RXRING_BUFFER_MASK (RXRING_BUFFER_SIZE-1) typedef volatile struct { uint8_t buf[RXRING_BUFFER_SIZE]; uint8_t rp,wp; } rxRingQue_t, *rxRingQue_p;

諸々合わせてメモリ使用量はこうなっていて31バイト余っている。受信バッファを64から8に減らせば送信バッファはあと87バイト増やせるが、いま割り当てている512に比べたら微々たるものだ。

リングバッファ(キュー)への出し入れはsetとgetとし、以下のようになっている。キュー本体をvolatile structに指定してあるのでstatic inlineとすれば警告が出なくなるというCopilotの言い分に素直に従ったが、正直、よくわかっていない。また、カウンタのインクリメントは++演算子ではなく、以下のように1行で加算とANDを済ませるようCopilotから指示があった。割り込み対策らしいがあまり意味がないと思う。

static inline uint8_t getTxQue(txRingQue_p qp) { uint8_t c=qp->buf[qp->rp]; qp->rp=(qp->rp+1)&TXRING_BUFFER_MASK; return c; } static inline void setTxQue(txRingQue_p qp, uint8_t c) { qp->buf[qp->wp]=c; qp->wp=(qp->wp+1)&TXRING_BUFFER_MASK; }

ここでは速度優先でバッファが溢れた場合の処理はしていない。どうせデータは失われるのだから実装は不要、と割り切った。この他バッファが一杯か、バッファがEmptyか調べるルーチンを用意してある。しかし「->」演算子はわかりにくいので、ここはCopilotに書いてもらった。
USB側は送信待ちバッファに突っ込むか、受信待ちバッファから引っ張るか、しかインタフェースが無いので、putchとgetchだけ用意しておけば良い。

void putch(uint8_t c) { if(!isTxQueFull(&txque)) { setTxQue(&txque, c); PIE1bits.TXIE = 1; } } bool getch(uint8_t *c) { if(isRxQueEmpty(&rxque)) return false; else { *c = getRxQue(&rxque); return true; } }

Copilotの助言でgetch戻り値をboolにしたのは使い勝手が良かった。学習元が優秀だってことだな。

USB-MDIパケットをMIDIシリアルへ

usb_device_midi.hには4バイトのUSB-MIDIバケットが以下のように定められている(ちょっと省略してある)。

typedef union { uint8_t v[4]; union { struct { uint8_t CIN :4; uint8_t CN :4; uint8_t MIDI_0; uint8_t MIDI_1; uint8_t MIDI_2; }; }; } USB_AUDIO_MIDI_EVENT_PACKET, *P_USB_AUDIO_MIDI_EVENT_PACKET;

USBからシリアルへの変換は、要はCINに従ってシリアル側に流す数を決め、MIDI_0から2までのバイトを選んで送信すればいいだけだ。MIDIシリアルにはランニングスーテスという機能(同じステータスが続くと省略できる)もあるが、ここでは馬鹿正直に省略なしで送信している(これを実装するとループバックでデバッグする時にも有用だが、ちょっと面倒くさい)。

void usb_midi_to_uart(USB_AUDIO_MIDI_EVENT_PACKET p) { uint8_t cin=p.CIN; uint8_t b0=p.DATA_0; uint8_t b1=p.DATA_1; uint8_t b2=p.DATA_2; switch(cin) { case MIDI_CIN_2_uint8_t_MESSAGE: putch(b0); putch(b1); break; case MIDI_CIN_3_uint8_t_MESSAGE: putch(b0); putch(b1); putch(b2); break; case MIDI_CIN_SYSEX_START: putch(b0); putch(b1); putch(b2); break; case MIDI_CIN_SYSEX_ENDS_1: putch(b0); break; case MIDI_CIN_SYSEX_ENDS_2: putch(b0); putch(b1); break; case MIDI_CIN_SYSEX_ENDS_3: putch(b0); putch(b1); putch(b2); break; case MIDI_CIN_NOTE_OFF: case MIDI_CIN_NOTE_ON: case MIDI_CIN_POLY_KEY_PRESS: case MIDI_CIN_CONTROL_CHANGE: case MIDI_CIN_PITCH_BEND_CHANGE: putch(b0); putch(b1); putch(b2); break; case MIDI_CIN_PROGRAM_CHANGE: case MIDI_CIN_CHANNEL_PREASURE: putch(b0); putch(b1); break; case MIDI_CIN_SINGLE_uint8_t: putch(b0); break; default: break; } }

なお、「1バイト送信」「2バイト送信」「3バイト送信」という3通りしか処理がないので、こんなに長いcaseは不要ではあるんだが、ここでは最初に書いた時のまま(定義通りに)CINの昇順に並べてある。CINは4ビットで数は大きくないので全部テーブルに格納し、以下のようにしてもいい。

void usb_midi_to_uart(USB_AUDIO_MIDI_EVENT_PACKET p) { static const uint8_t cin_len[] = {/* 16要素、1から3 */ }; uint8_t n = cin_len[p.CIN]; if (n > 0) putch(p.DATA_0); // 1,2,3の時に送信 if (n > 1) putch(p.DATA_1); // 2,3の時に送信 if (n > 2) putch(p.DATA_2); // 3の時に送信 }

MIDIシリアルからUSBへの変換

ここはとにかく苦労した(それも楽しい)。まず送信バッファなしで苦労し、システムエクスクルーシブで苦労した。Copilotが嘘ばかり付くのだが、それはどうやらこちらの説明がまずかったせいだろう。勘違いとツールの不備を全てソースの誤りで説明しようとして破綻していた面がある。
特にPocket MIDIがエラー表示をしなかったり、巨大なシステムエクスクルーシブを送信できても受信できなかったりするのに気づくのが遅れたせいで、何のためにどこを修正したのか把握できなくなってしまった。こういうデバッグにはMIDI Monitorの方が有用だったが送信機能がないのでPocketMIDIと併用する必要がある。
P_USB_AUDIO_MIDI_EVENT_PACKET uart_midi_to_usb(uint8_t c)の処理内容は大体以下のようになる。パケットが作成できたらパケットへのポインタを返し、パケット作成中ならNULLを返すようになっている。

  1. 未定義のシステムコモンやリアルタイムを無視して即座にNULL(F4/F5/F9/FD)
  2. リアルタイム(1バイト)をパケットで即座に返す(F8/FA/FB/FC/FE/FF)
  3. システムエクスクルーシブの処理(F0/F7)
  4. システムコモンの処理(F1/F2/F3/F6)
  5. 残りのステータス処理(ノートオンなど)
  6. さらに残りのデータ処理

なお、ソースが長いのでここには載せない。システムエクスクルーシブ受信中にシステムコモンが来ても無視していいらしいし、リアルタイムは最優先で処理するので、この順番でいいはずだ。Copilotとも相談したしw

まとめ

USB-MIDI変換の記事はここまでにしておく。USBパケットを受け取って処理できるようになったので、シンセサイザーのCV変換やDCOを作るのに役立つはずだ。

akira.keiのアイコン画像
機械系エンジニアだが電子工作を趣味としている。週末はひとりバーベキュー。
ログインしてコメントを投稿する