自作MIDI音源「CureSynth」製作記事の一覧はこちら
回路図、ソースコードはこちら
前回は、自作MIDI音源「CureSynth」のソフトウェア概要について紹介しました。
今回は、STM32F7+HALライブラリを用い、MIDIメッセージを「USART割り込み」で受信する方法と、受信したMIDIメッセージを蓄積するためのリングバッファについて紹介します。
1.MIDI受信回路
本稿で対象とする回路は、以前作成したvs1053b用の回路を転用し、USART3(RX)に直結しています。以下、USART3をMIDI受信用として話を進めます。
2.USART割り込みによる受信
2.1.ペリフェラルの設定
USART3をMIDI受信用に設定します。STM32ではペリフェラルの設定がややこしいので、STM32CubeMXを使用すると便利だと思います。説明するのにも、設定画面を載せるのが手っ取り早いですね。たまにバグがあるので、自動生成されたソースコードはよく眺める必要はありますが…
STM32CubeMXで、USART3をAsynchronous(非同期)に設定します。フロー制御は行わないためオフとします。
ConfigurationタブのUSART3ボタンを押し、Parameter Settingタブの内容を設定します。通信速度を31.25kbps、8bitパリティなし、ストップビットを1(H)にします。MIDIはLSB firstなので、MSB FirstをDisableにしておきます。
MIDIのハードウェアレベルの仕様については、以下の記事がとても読みやすいので、お勧めします。
→MIDI のハードウェアについて(Y-Lab. Electronics, Elekenさん)
余談ですが、STM32のUSARTは、RX/TXの入れ替えや論理の入れ替えなどをハード側でやってくれるので、安心して設計ミスができますね!
さらに、NVIC Settingsで、USART3割り込みを有効にしておき、割り込みが発生できるようにします。
以上で設定は完了です。
2.2.実装
USART割り込みのサンプルです。
製作記事その3で示したとおり、MIDIメッセージはバイト単位なので、1バイト(8bit)受信するごとに割り込みが発生するようにします。
また、割り込み処理内でリングバッファ(FIFO)に蓄積し、処理の安定化を図ります。関数cureMidiBufferEnqueue()はバッファへ蓄積する関数です。後述します。
/*main.c*/ //グローバル変数 uint8_t midi_recieved_buf; //1バイト受信するとコールされる割り込み関数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART3) { //受信内容をリングバッファに蓄積する cureMidiBufferEnqueue(&midi_recieved_buf); //次の受信に備える HAL_UART_Receive_IT(&huart3, &midi_recieved_buf,1); } } int main(void) { //(中略) //MIDIメッセージの受信を開始。1バイト受信すると、割り込みを発生させる。 HAL_UART_Receive_IT(&huart3, &midi_recieved_buf, 1); //(中略) }
3.リングバッファ
3.1.概念
リングバッファ(循環バッファ)について、MIDIメッセージの受信に必要な概念に限定して説明します。汎用的な内容ついては、以下の記事が分かりやすいと思います。
→循環バッファ( ++C++; // 未確認飛行 C , 岩永 信之さん)
→キュー(お気楽C言語プログラミング超入門, 広井 誠さん)
製作記事その3で示したとおり、MIDIメッセージの受信はUSART割り込みで行い、MIDIメッセージの解析はメインループ内で行うため、MIDIメッセージ受信と解析のタイミングが同期するとは限りません。そこで受信したMIDIメッセージをバッファに蓄積しておき、解析するタイミングでバッファからデータを取り出します。そのためには、バッファにデータを蓄積した順に取り出す必要があります。
これを実現するのがリングバッファです。リングバッファは配列構造をリング状に(論理的に)接続したもので、イメージは次の通りです。リング状にすることで、データ領域の先頭・終端を任意の位置にすることができるため、使用済みの領域を再利用できます。エコですね。
バッファの蓄積(Enqueue)/取り出し(Dequeue)のイメージ図を下記に示します。この例では、(a)初期状態に対し、(b)[0x90, 0x3C, 0x64]の順に3バイト蓄積、(c)[0x90]の1バイト取りだし、(d)[0x91, 0x3D, 0x65]の順に3バイト蓄積、と操作しています。
ところで、図中のfront, rearは、要素の先頭や終端にアクセスするためのインデックス変数です。図では、(a)~(d)の各処理直後の値を表しており、次のように使います。
- front==rearのとき、バッファが空だと判断。(→上図(a))
- frontがrearの一つ前であるとき、バッファが満杯だと判断。
- 次に蓄積する位置は、frontとする。buffer[front]でアクセス。
- 次に取り出すべき位置は、rearとする。buffer[rear]でアクセス。
このとき、front, rearの値を次のように操作すれば、バッファが適切に動作します。
- バッファの蓄積(Enqueue)と同時にfrontを1増加
- バッファの取り出し(Dequeue)と同時にrearを1増加
- front, rearが要素数nを超えたら0にリセットし、要素数nで循環させる
3.2.実装
まず、バッファの構造体型を定義します。
/*curebuffer.h*/ //uint8_t用リングバッファ構造体 typedef struct{ uint16_t idx_front; uint16_t idx_rear; uint16_t length;//バッファ長 uint8_t *buffer;//バッファの先頭アドレス }RingBufferU8;
一般的に使えるように、バッファを配列としてではなくポインタ(*buffer)として確保しています。後述する初期化処理内で、領域を確保してから使います。
バッファ長はsizeof(buffer)で求まりますが、sizeofによる演算コスト増加を避けるため、初期化時にlengthに代入しておきます。
次に、このRingBufferU8構造体に対して、初期化処理・解放処理・データ蓄積処理・データ取り出し処理の関数を用意します。これらバッファ操作用関数は、curebuffer.h/cにまとめておきます。
/*curebuffer.c*/ //初期化処理 BUFFER_STATUS cureRingBufferU8Init(RingBufferU8 *rbuf, uint16_t buflen) { uint32_t i; cureRingBufferU8Free(rbuf); rbuf->buffer = (uint8_t *)malloc( buflen * sizeof(uint8_t) ); if(NULL == rbuf->buffer){ return BUFFER_FAILURE; } for(i=0; i<buflen; i++){ rbuf->buffer[i] = 0; } rbuf->length = buflen; return BUFFER_SUCCESS; } //解放処理 BUFFER_STATUS cureRingBufferU8Free(RingBufferU8 *rbuf) { if(NULL != rbuf->buffer){ free(rbuf->buffer); } rbuf->idx_front = rbuf->idx_rear = 0; rbuf->length = 0; return BUFFER_SUCCESS; } //データ蓄積処理 BUFFER_STATUS cureRingBufferU8Enqueue(RingBufferU8 *rbuf, uint8_t *inputc) { if( ((rbuf->idx_front +1)&(rbuf->length -1)) == rbuf->idx_rear ){//バッファが満杯 return BUFFER_FAILURE; }else{ rbuf->buffer[rbuf->idx_front]= *inputc; rbuf->idx_front++; rbuf->idx_front &= (rbuf->length -1); return BUFFER_SUCCESS; } } //データ取り出し処理 BUFFER_STATUS cureRingBufferU8Dequeue(RingBufferU8 *rbuf, uint8_t *ret) { if(rbuf->idx_front == rbuf->idx_rear){//バッファが空 return BUFFER_FAILURE; }else{ *ret = (rbuf->buffer[rbuf->idx_rear]); rbuf->idx_rear++; rbuf->idx_rear &= (rbuf->length -1); return BUFFER_SUCCESS; } }
ちなみに、ハイライトした行の演算(front, rearを要素数の範囲で循環させる演算)には、ビット演算を使い高速化しています。例えば下記の2つは同じ結果になりますが、1つ目の方が速いです。ただし要素数を2のべき乗としなければなりません。
//ビット演算(速い) idx_front &= (length -1); //条件分岐(遅い) if( idx_front >= length ){ idx_front -= length; } //ビット演算の性質上、要素数lengthには「2のべき乗」という制限あり。
バッファ操作用関数は、MIDI操作用関数群であるcuremidi.h/c内でwrapして使っています。下記のように、cureMidiInit()関数内でバッファ領域を初期化しておき、2.2節のようにcureMidiBufferEnqueue()関数を呼び出すことで、バッファにデータを蓄積できます。
/*curemidi.h, curemidi.c*/ //バッファ長さ #define MIDIBUFFER_LENGTH (1024) //バッファ用構造体 RingBufferU8 rxbuf; //初期化処理 FUNC_STATUS cureMidiInit() { //(中略) if( BUFFER_FAILURE == cureRingBufferU8Init(&rxbuf, MIDIBUFFER_LENGTH) ){ return FUNC_ERROR; } //(中略) return FUNC_SUCCESS; } //データ蓄積処理のラップ関数 BUFFER_STATUS cureMidiBufferEnqueue(uint8_t* inputc) { return cureRingBufferU8Enqueue(&rxbuf, inputc); } //※関数マクロにした方が高速化できると思いますが、読みやすさ優先で… //他のラップ関数は割愛
ちなみにBUFFER_STATUS, FUNC_STATUSは、それぞれバッファ用、一般関数用のエラーフラグであり、enumで定義しています。コーディングの見やすさのために用意しています。
typedef enum{ BUFFER_FAILURE,BUFFER_SUCCESS }BUFFER_STATUS; typedef enum{ FUNC_ERROR,FUNC_SUCCESS }FUNC_STATUS;
今回はここまで。
次回は、MIDIメッセージの解析処理について紹介します。
コメント