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

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

<前の記事 : 次の記事>

MLAのMIDI Exampleをよく見ると

前の記事でも書いた通り、MLAのMIDI ExampleはUSBコネクタを挿せばMIDIデバイスとして認識され、それだけでは何のデータも送受信しない。メインルーチンは以下のように構成されており、「USB以外を初期化」、「USBを初期化」、「USBの接続処理」した後に無限ループ内で「USBデバイスとしてのタスク」と「アプリケーションとしてのタスク」を順に実行し続けると言う、極めて普通でわかりやすい。

void main(void) { SYSTEM_Initialize(SYSTEM_STATE_USB_START); USBDeviceInit(); USBDeviceAttach(); while(1) { USBDeviceTasks(); APP_DeviceAudioMIDITasks(); } }

ちなみにUSBDeviceTasks()はポーリングなら無限ループ内で、割り込みなら割り込みルーチン内で呼ばれることになる。将来的に割り込みはシリアル通信で使いたいし、元々のサンプルもポーリングになっているので、USB向けの割り込みは使用しないことにする。
MIDIを取り扱うルーチンは無限ループ内のAPP_DeviceAudioMIDITasks()である。その構成は以下のとおり。

void APP_DeviceAudioMIDITasks() { if( (USBGetDeviceState() < CONFIGURED_STATE) || (USBIsDeviceSuspended() == true)) { return; } if(!USBHandleBusy(USBRxHandle)) { //INSERT MIDI PROCESSING CODE HERE //Get ready for next packet (this will overwrite the old data) USBRxHandle = USBRxOnePacket(USB_DEVICE_AUDIO_MIDI_ENDPOINT,(uint8_t*)&ReceivedDataBuffer,64); } if(BUTTON_IsPressed(BUTTON_DEVICE_AUDIO_MIDI) == true) { /* ここでスイッチの処理*/} }

デカデカと「//INSERT MIDI PROCESSING CODE HERE」とあるように受信ルーチンは自分で書けよwってことで、このサンプルでは受信しても何もしない。受信の実態はUSBRxOnePacket()で、送信はUSBTxOnePacket()である。スイッチはリセットスイッチを流用しており、リセット無効にしてからスイッチ入力端子としているが、押すと適当なノートオンパケットを送信し、離すとノートオフパケットを送信する。受信側は64バイトのバッファにデータが入り、送信時にはバッファは使っていない。

#pragma config

名前が気に入らないがsystem.c内にコンフィギュレーションビットの定義が書いてある。大切なのはsystem.h内で「#define USE_INTERNAL_OSC」を有効にすることだ。Microchipの評価ボードは外部発振らしいので、ここは最初は無効になっている。

#if defined (USE_INTERNAL_OSC) // CONFIG1 #pragma config FOSC = INTOSC #pragma config WDTE = OFF #pragma config PWRTE = OFF #pragma config MCLRE = OFF #pragma config CP = OFF #pragma config BOREN = ON #pragma config CLKOUTEN = OFF #pragma config IESO = OFF #pragma config FCMEN = OFF

この中で重要なのはFOSC=INTOSC、MCLRE=OFF(リセットボタンを入力スイッチとして使う)くらいか。USBに接続しっぱなしだからクロックは固定なのでIESOとかFCMENはOFFでいいし、電源関係の監視(PWRTE、BOREN)も不要だろう。

USARTを使う

USB-MIDI変換なので、「USBからのMIDIパケットをシリアルポートに順次流していく処理」と、「シリアルから来たバイト列をMIDIパケットに変換してUSB送信する処理」を考える。とにかくシリアルポートを初期化しないといけないが、最初からMIDI速度である31250にするとターミナル接続が面倒な場合があるので、Debug中は普通のボーレート(9600や115200)にできるようにしておいた。

void USART_init(unsigned long fosc, unsigned long baud) { TRISCbits.TRISC4=0; // TX TRISCbits.TRISC5=1; // RX BAUDCONbits.BRG16 = 1; RCSTAbits.SPEN = 1; RCSTAbits.CREN = 1; TXSTAbits.TXEN = 1; TXSTAbits.BRGH = 1; SPBRG = (uint16_t)((fosc/4UL/baud) -1UL); PIE1bits.RCIE=1;

バッファなしでも動く?

USBからのデータは受信バッファに入っているので、有効なパケットを1つずつ読み出し、バイト列にしてシリアルに流す、と言う部分はNoteOn/Off程度なら一見ちゃんと動いた。だがある程度まとまった8バイト程度のMIDIパケットや10バイト程度のシステムエクスクルーシブを送ってみると動作しない。結局、シリアルは送受信バッファ、USBは送信バッファを持つようにした。詰まりやすいシリアル送信バッファには1024しかないRAMの半分を当てた。

#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;

シリアル送信はputch(c)でバッファに積み、割り込みで実際に送信する構成である。割り込みルーチンはUSB用にsystem.c内に定義があるので、これを削除する必要がある。実際にはuart.hとuart.c内でシリアル関連の処理は完結させ、putchとgetchだけでMIDI関連とインタフェースする。

肝心の本体は

受信データがあればパケットを一つずつ取り出して「usb_midi_to_uart(c)」に送り、受信データがあれば「uart_midi_to_usb(c)」に送る、と言う単純な受け渡ししかやってないが、これらの中身が大変だった。

void APP_DeviceAudioMIDITasks() { if( (USBGetDeviceState() < CONFIGURED_STATE) || (USBIsDeviceSuspended() == true)) return; if(!USBHandleBusy(USBRxHandle)) { uint8_t length = USBHandleGetLength(USBRxHandle); uint8_t *p = RXdataBuffer; for(uint8_t i = 0; i < length; i += 4u) { USB_AUDIO_MIDI_EVENT_PACKET c; c.v[0] = p[i]; c.v[1] = p[i+1u]; c.v[2] = p[i+2u]; c.v[3] = p[i+3u]; usb_midi_to_uart(c); } USBRxHandle = USBRxOnePacket(USB_DEVICE_AUDIO_MIDI_ENDPOINT, (uint8_t *)&RXdataBuffer, 64); } uint8_t c; while(getch(&c)) { P_USB_AUDIO_MIDI_EVENT_PACKET p=uart_midi_to_usb(c); if(p!=NULL) set_usbTxQue(&usbtxq,*p); } usb_send_from_tx_queue(); }

残りは次の記事で。

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