Fluentd Advent Calendar 24日目の記事です。
家にあるMIDIキーボードからMIDI信号をひろってFluentdにとばすという、誰得な工作をした。CPUやOSを使わず、MIDI信号のデコードからTCP接続、FluentdのMessagePackエンコードまで、すべてハードウェア実装なのだ。
まずはデモ動画をどうぞ:
MIDI keyboard + DE0 + Fluentd demo
MIDIキーボードを叩くと、Mac上のFluentdにMIDIメッセージが送られ、Fluentdのログとして表示されてるのがわかる。以下、このデモの中身を解説したい。
MIDI→DE0→WIZ→Fluentd
このデモの構成はこんな感じ:
以下、それぞれのコンポーネントの役割を見ていこう。
MIDI信号のデコード
MIDIキーボードから送られてくるMIDI信号のデコードは2年前に作ったMIDIデコーダーをそのまま使った。MIDIインタフェースを介してFPGAボードのTerasic DE0 のGPIOポートで信号を受信、Verilog HDLで書いたデコーダーでMIDIメッセージに変換する。鍵盤を押したり離したりするだびに、3バイトのMIDIメッセージがDE0上のバッファに入る動きだ。このあたり詳しくは上述のページを参照のこと。
この写真で手前にある赤い色のボードがMIDIインタフェースで、奥にあるDE0のGPIOにつながっている。
WIZnet W5300でTCP接続
問題は、このMIDIメッセージをどうやってTCP/IPでFluentdに送るかだ。RasPiを使えば簡単だろう...(というか、RasPiにFluentdを載せればよい)。しかし! ここはハードウェア実装でないと面白くない。
TCPスタックを実装した商用のIPコアを購入したり、OpenCoresにあるそれっぽいHDLをがんばっていじれば、TCPのハード実装も可能だが、どちらの方法もハードル高い。そこで手っ取り早く、WIZnet W5300を使うことにした。上の写真の右側に写ってるボードだ。
WIZについて詳しくは前に書いたこちらのエントリを見てほしい。これはハードウェアベースのTCP/IPプロセッサで、
- CPUもOSも使わずお手軽TCP/IP通信できる
- 1個2000円くらい。安い
- TCP、UDP、その他もろもろサポート。リスナもクライアントもOK
- 16/8bitバスでつないで使う
- 最大80Mbpsで動くけど、最大8本のソケットしか使えない
という製品。実用性はともかく、とりあえずTCPより上のレイヤをハードウェアで直接いじりたい! という人におすすめのボードと言えよう。
デコードしたMIDIメッセージをMessage Packにエンコードして、8bitバスを通じてWIZに渡すと、いい感じにMac上のFluentdに送ってくれる。このあたりの詳細は後述。
MacのFluentdで受ける
Mac上のFluentdは、ほとんどデフォルト設定のままだ。td-agent.conf
に
<source>
type forward
</source>
と書いておいて、ポート24224でmsgpack形式でイベントログを受信できるようにしておく。
SynthesijerでJavaからHDL生成
さて、キモとなるのは「MIDIメッセージをmsgpackでエンコードしてWIZ経由でTCP送信する」ところ。このロジックの記述には、みよし師匠作の高位合成ツールSynthesijerを使った。というか、師匠がすでにSynthesijerでWIZを動かすサンプルを作っていたので、それを俺がごにょごにょ改変しただけである。
高位合成とは、ハードウェア回路の実装に用いられるハードウェア記述言語(HDL)を、高級言語から生成する技術のこと。NECのCyberWorkBenchなどは高位合成ツールの代表例で、C言語のようなソフトウェア言語を用いてハードウェアのふるまいを記述するとHDLを生成してくれる。HDLをすべて手書きするハードウェア開発は、あまりに低レベル過ぎて実装も検証も大きな労力を必要とするが、高位合成を使えばコーディングもデバッグもかなりラクできる...らしい。
CyberWorkBenchのような商用の高位合成ツールは恐ろしく高く、個人にまったく手が届かない。一方で、Synthesijerはなんとオープンソース! AWESOME!しかもCじゃなくてJava(のサブセット)で書ける! というわけで、FPGAで遊びたい人は皆いますぐSynthesijerをダウンロードしよう。
Sjrの動き
Synthesijer(長いので以下sjr)を使うと、例えばWIZにデータを書き込むのもこんな感じで記述できる。
private void write_data(int addr, byte data) {
wiz830mj.address = addr;
wiz830mj.wdata = data;
wiz830mj.cs = true;
wiz830mj.we = true;
wait_cycles(3);
wiz830mj.we = false;
wiz830mj.cs = false;
}
これはつまり、
- WIZのバスに書き込み先アドレスとデータをセット
- WIZのcsとwe(書き込み信号)をtrueに
- 3クロックじっと待つ
- csとweと戻す
という動き。わかりやすい! HDLはすべてがパラレルな並行世界でソフトウェアエンジニアにはかなりとっつきにくいけど、sjrを使えば手続き脳な俺達でもJavaの文法でやりたいことを書き下せる。
まるでCPUがあるみたいだけど、CPUはない。Javaで書いた手続きを順次実行してくれるシーケンサーのデジタル回路をsjrが自動生成してくれるのである。ちなみに、今回のデモではDE0を50MHzクロックで動かしてるので、上記の各行は数10nsきざみで動く。OSが勝手に割り込みしたりしない。自分が書いたJavaコードがナノセカンド単位できっちり動いてくれるの快感だ。
sjrのJavaコードを書いたら、sjrのコンパイラを実行してHDL(Verilog HDLかVHDL)を生成する。例えば今回の例ではこんなふうなやたら長いVerilog HDLコードが生成される。あとは、このHDLを元にAltera Quartus等のFPGA開発ツール上で回路を合成、DE0に読み込ませて動かす。
SjrでWIZのコントロール
このsjrを使ってTCPクライアントを書くと、以下のような感じになる(コード全体はここ)。
// added by kaz
private void tcp_client(int wiz_port) {
// open TCP socket
int wiz_port_offset = wiz_port << 6;
write_data(Sn_MR0 + wiz_port_offset, (byte) 0x01); // use alignment
write_data(Sn_MR1 + wiz_port_offset, Sn_MR_TCP); // TCP mode
write_data(Sn_PORTR0 + wiz_port_offset, (byte) 0x13); // src port 5000
write_data(Sn_PORTR1 + wiz_port_offset, (byte) 0x88);
write_data(Sn_CR1 + wiz_port_offset, Sn_CR_OPEN); // open
while (read_data(Sn_SSR1 + wiz_port_offset) != Sn_SOCK_INIT)
;
// set dest IP and port
write_data(Sn_DIPR0 + wiz_port_offset, (byte) 192);
write_data(Sn_DIPR1 + wiz_port_offset, (byte) 168);
write_data(Sn_DIPR2 + wiz_port_offset, (byte) 1);
write_data(Sn_DIPR3 + wiz_port_offset, (byte) 110);
write_data(Sn_DPORTR0 + wiz_port_offset, (byte) 0x5e); // dest port
write_data(Sn_DPORTR1 + wiz_port_offset, (byte) 0xa0);
// connect
write_data(Sn_CR1 + wiz_port_offset, (byte) 0x04);
while (read_data(Sn_SSR1 + wiz_port_offset) != Sn_SOCK_ESTABLISHED)
;
なんとなく動きをイメージできるだろう。要するにWIZのレジスタを通じていろいろコマンドを送りながら状態遷移を管理するのだけど、まずはTCPソケットをオープン、ステータスがSOCK_INITになったらFluentdが動くMacのIPアドレスとポートを指定してCONNECTコマンドを送信、SOCK_ESTABLISHEDになるのを待つ、という流れ。ソフト開発のようなノリだ。
MIDIメッセージをmsgpackにエンコード
MacとTCP接続が確立したら、MIDIメッセージをmsgpack形式でエンコードしてWIZ経由で送る。送る内容は、バッファ上で以下のように書いて組み立てている。
private void buildMidiMsg() {
// send migi msg with msgpack ["midi", 0, {"msg": <<midi msg>>}]
buffer[0] = (byte) 0b10010011; // array with 3 elements
buffer[1] = (byte) 0b10100100; // str with 4 bytes
buffer[2] = 0x6d; // "midi"
buffer[3] = 0x69;
buffer[4] = 0x64;
buffer[5] = 0x69;
buffer[6] = (byte) 0; // int 0
buffer[7] = (byte) 0b10000001; // map with 1 pair
buffer[8] = (byte) 0b10100011; // str with 3 bytes
buffer[9] = 0x6d; // "msg"
buffer[10] = 0x73;
buffer[11] = 0x67;
buffer[12] = (byte) 0b10010011; // array with 3 elements
buffer[13] = (byte) 0xcc; // midi status
buffer[14] = (byte) (midi_msg >> 16);
buffer[15] = (byte) 0xcc; // midi data 1
buffer[16] = (byte) (midi_msg >> 8);
buffer[17] = (byte) 0xcc; // midi data 2
buffer[18] = (byte) midi_msg;
length = 19;
}
MIDIメッセージは1バイトの「MIDIステータス」と2バイトの「データ」の合計3バイトで構成されていて、上記コードではその3バイトをarrayに入れて、msg
というプロパティでカプセル化している。タイムスタンプはとりあえず0にしてある(システムクロックを実装する時間がなかった...)。
上記のメッセージがMacのポート24224に届くと、以下のようなイベントログが記録される。
2014-12-24T14:21:32+09:00 midi {"msg":[140,59,64]}
動いた動いた。yaay!! FluentdクライアントのFPGA実装という夢がかなって感無量である。
これは誰得か? このデモはクロック50MHzだし速さを追求してないので実用性は全然ない。でも、CPUもOSも使わずにそこそこ複雑なアプリロジックをハードウェア実装できる、しかも1万円くらいのFPGAボードとオープンソースの高位合成ツールを使ってお手軽に、って状況になりつつあるのは、じつはdisruptive innovationなんじゃないかと思う。
さらなる野望
しかし本当は、WIZのTCPリスナ機能を使ってKVSとかストリーム処理とかやってみたかった...のだが、なぜだか現状ではうまく動いてなくてあきらめた。年末休みを使ってもうちょっとがんばってみたい。あとMQTTもやってみたい!
今回書いたコードはGitHubに置いておいた。