AVRを使ってインターネットラジオを作りました.これを使えば,インターネット上でSHOUTcastやIcecastというプロトコルで配信されているmp3のストリーミング放送を聞くことができます.
音楽を聴く以外にも,英語の勉強のため外国のニュースなどを手軽に聞いてみたいと思い製作しました.
安価に入手した中古のLANカードを使い自作のTCP/IPプロトコルスタックで処理を行いました.
システムの構成は下記の図のようになっています:
システム全体の制御にはATmega328Pを使います.これは,32KBのフラッシュROMと2KBのRAMを持っており,セラロックを外付けして20MHzで動作させます.
イーサネットのインターフェースには,ノートPC用のLANカードである3ComのEtherLink-III (3C589D)を使いました.このカードの使い方の詳細については以前作成した「AVRで動かすWebサーバ」を参照して下さい.最近は,ENC28J60やW5100などの小型でSPI接続可能なイーサネットコントローラICが容易に入手できるので,それらのICを使うのもよいと思います.3C589Dは信号線が多く場所もとりますが消費電流は50mA(5V)であり,消費電流が100mA(3.3V)以上のENC28J60やW5100に対してその点だけは優れているようです.
インターネットラジオの音データはmp3等で圧縮されて送られてくるので,mp3デコード用ICのVS1011eを使用しました.
スタンドアローンで動作し手軽に使えるラジオにしたかったのでスピーカーも内蔵しました.直径16mmのとても小さなスピーカー(8Ω0.5W)をステレオで使います.
当初VS1011eにスピーカーを直接付けてもある程度音量が得られるのではないかと考えていましたが,全然音が鳴らなかったのでオーディオアンプを追加しました.オーディオアンプとしては,以前使ってみたPAM8403やポピュラーなNJM2073の使用を考えましたが,前者はフラットパッケージで実装が面倒であり後者は外付け部品が多いため,DIPパッケージで外付け部品も少ないHT82V739を使用しました.オーディオアンプのカップリングコンデンサにはフィルムコンデンサなどを使いたいところですが,今回はスペースが無かったのとスピーカーが超小型のもので音質は期待していないので,チップ型の積層セラミックコンデンサを使いました.
なお,このインターネットラジオでは現在のところバッファ用のメモリを外部に取り付けていません.バッファ無しでの動作に関する問題点については,後述の「通信遅延時間と再生可能ビットレートについて」を参照して下さい.
回路図,および配線図は次のとおりです:
回路図
配線図(mp3モジュール)
配線図(コントローラモジュール)
mp3モジュールとコントローラモジュールの2つの基板に分けて実装しました.mp3モジュールには,デコーダICのVS1011eと,5V→3Vのレベル変換用の74LV541が載っています.どちらもピッチ変換基板を使っています.コントローラモジュールは,AVRをはじめとしたそれ以外の部品が載っています.アンプICは別基板に分けたかったのですが,スペースの都合上この基板に載せています.
それぞれのモジュールはまず単体でテストして,その後結合してファームウェアを作りました.mp3基板の方は,パソコンにパラレルポートでつないで,テストモードの実行(サイン波の発生)とmp3ファイルの再生を行いテストしました.使用したプログラムをここに置いておきます(Linux上のgcc用です).また,コントローラモジュールの方は,TCP/IPプロトコルスタックuSockによる簡易httpサーバを動作させてテストしました.
今回はAVRにつなぐ周辺部品が多く(LANカード,mp3デコーダ,LCD,スイッチ),I/Oポートは全て使っています.
LANカードはCE以外の端子は共通のバスとして使えます.
VS1011eはインターフェースがSPIなので2つのCSピン(コマンド用のXCSとデータ用のXDCS)にそれぞれAVRのI/Oピンを割り当ててそれ以外はバスに接続できればよかったのですが,I/O数が足りなかったのでレベル変換用ICのEnable端子だけをI/Oに割り当てて,それ以外のピンはすべてバスにつなぎました.つまりレベル変換用ICは,5V→3Vのレベル変換だけではなく信号線を共通化する目的でも使っています.
スイッチは,チャンネルのアップ・ダウンと音量のアップ・ダウン用に4つ用意しました.プッシュスイッチではなく,中点OFFの両跳ね返り式トグルスイッチを使いました.プッシュスイッチ2個の変わりにトグルスイッチ1個ですむのでスペースは省略できましたが,スイッチのバネが強く少し操作性は悪くなりました.スイッチもバスに接続しています.スイッチの状態を読み取る場合,スイッチの共通端子をGNDに落とし,ダイオードを介してスイッチに接続したポートの内部プルアップ抵抗を有効にすることで,スイッチのON/OFFを調べます.
LCDは14セグメント8桁のものを使いました.これは日本橋のデジットで以前売られていたもので,こちらのページの解析結果が非常に参考になりました.この液晶は省スペースで必要な信号の数が3本と少なく,消費電流も5Vの電源の時に240μAと省電力である利点がありますが,ドットマトリックスの液晶を使った方が演奏中の曲の情報を表示することができて良かったかもしれません.14セグメントLCDでは一応アルファベットも表示できますがかなり読みにくいです.
14セグメントの表示パターンを用意するのは大変だったので,マウスでセグメントのON/OFFを操作して表示パターンを数値表現に変換する簡単なスクリプトをJavaScriptで書いて使用しました.
出来上がった基板と,それをケースに組み込んだものは下記のようになりました:
完成した基板1
ケースに組み込んだ基板
完成した写真は下のとおりです:
完成した本体(正面)
完成した本体(背面)
ケースはタカチのSW-125を使用しました.
CNCフライス盤でケースを加工する時にテーブルの可動範囲を考えていなかったため,左のスピーカーの穴開けに失敗してしまいました.
LANのRJ-45ジャックはエポキシ接着剤で,LANカードやプリント基板は両面テープで固定しました(mp3モジュールはフタに取り付けています).スピーカーはホットボンドで固定しています.
ファームウェアはC言語(avr-gcc)で開発しました.
このインターネットラジオは以下の3つのモードを持っていて,電源を入れるときのスイッチの状態でモードを決めます(モードを変えるには電源を入れなおします):
このインターネットラジオは,チャンネル設定モードではhttpサーバ,演奏モードではDNSクライアントとSHOUTcast/Icecastクライアントとして動作するためTCPやUDPのプロトコルを扱う必要がありますが,今回は自作のTCP/IPプロトコルスタックuSockを使用しました(というかこのインターネットラジオを作成するためにuSockを作りました).このインターネットラジオの製作には1ヶ月強かかりましたが,半分ほどの時間はプロトコルスタックの作成に費やしました.
ATmega328は2KBのRAMを持っているので,TCPの最大データ長(Maximum Segment Size; MSS)はEthernetにおける最大値の1,460バイト(Ethernetの最大フレーム長1,500バイトからIPパケットとTCPパケットの最小ヘッダ長40バイトを引いた数)として,その分のバッファ領域を確保しています.
また,使用したLANカードの3C589Dは6KBの受信用バッファを持っているので,TCPでのウィンドウサイズ(ACKを受け取らずに送信できる最大データ数)はMSSの整数倍で受信バッファよりも小さい(1,460*4=5,840)バイトとしています.
SHOUTcastやIcecastのプロトコルはhttpを元にしていてどちらも似ていますが,応答時のヘッダ情報が異なっています(SHOUTcastは"ICY 200 OK"で,Icecastは"HTTP/1.0 200 OK").また,SHOUTcastではUser-Agent名を"Mozilla"とするとmp3データが取得できませんでした.
演奏モードにおける基本的な処理内容は次のようになっています:
while (無限ループ) { mp3のデータをリモートホストから受信する; while (未再生のデータがある) { while (mp3デコーダがデータ受け入れ不可能) 待つ; mp3デコーダにデータを1バイト転送; } キー入力(チャンネル・ボリューム変更)を調べて処理する; }
このように割り込みは一切使わず,サーバからのmp3データの受信とmp3デコーダへの転送を順番に行いポーリングベースで処理をしています.
本当はリングバッファを用意しておいて,常にサーバからデータを受信してバッファを満たすようにしておきながら,タイマー割り込みで一定時間毎にmp3デコーダがBUSYでなければバッファのデータを転送する,という処理をするのが良いと思いますが,今回はバッファに使えるRAMが無いので単純なポーリングで処理しています.
ファームウェアを開発する際に,いきなり公開されているインターネットラジオサーバに接続するとプログラムの不具合により迷惑をかける恐れがあるので,自前でIcecastのサーバを用意してテストをしました.以下は,Linux (Debian lenny)でIcecastサーバを動かす手順です:
WinAVR-20100110で開発・テストを行いました.コンパイル後のファームウェアのサイズは約14KBです.
usock_conf.hの中のMACアドレスを使用するLANカードのものに書き換えてmakeを実行すればコンパイルできます.
注意点として,コンパイルの際にはavr-gccで"-fno-inline"オプションを指定しないと正常に動作しないという問題があります.
このファームウェアを使用する場合,AVRのヒューズビットを設定する必要があります.設定する箇所は,(1)外付けのセラロックをクロック元に選択,(2)EESAVEビットをクリアしてEEPROMを保護(これをしないとプログラムを書き込むたびにEEPROMの内容がクリアされてしまう),(3)4.3Vの低電圧検出リセットを有効化(これをしないとEEPROMの内容が破壊されることがある),の3つです.
このインターネットラジオを設計した時はストリーミング放送の受信(というかネットワークアプリケーション自体)を扱った経験が無かったため,通信の遅延のことは考慮せずMCU(AVR)の処理能力が不足しないかだけを心配していました.そのため,バッファ用のRAMを外付けすることを考えていませんでした.
自前のIcecastサーバでテストをしている段階では320kbpsのmp3放送を問題なく再生できたのでMCUの能力的には問題ないことが分かりましたが,いよいよインターネット上で公開されている他の放送を受信してみると問題が起きました.
正常に受信できる放送もありますが,放送によっては0.5秒ぐらいおきにプツプツと音が途切れる感じになりうまく再生できませんでした.そこで通信遅延時間の影響を考えてみました.
TCPでの通信速度は,以下のようにして決まります:
ウィンドウサイズは,ACK(確認応答)が来なくても送信側が送信することのできるデータサイズで受信バッファのサイズ以下の値となります.RTTは通信路をデータが往復するのに要する時間で,pingコマンドを使って調べることが出来ます.
つまりTCPでは送信されたデータに対してACKを返して確認をする仕組みになっているため,10BASE-Tの回線でも1000BASE-Tの回線でも最初のデータを送ってからそのACKが返るまでに送れるデータ量はウィンドウサイズしかないので,単位時間当たりに転送できるデータの量は上の式で決まります.RTTはホスト間の距離が物理的に離れている以上短くすることはできないので,通信速度を上げるには受信用のバッファを増やしてウィンドウサイズを大きくする必要があります.
今回作成したインターネットサーバでは,ウィンドウサイズはNICに備わっている受信バッファの容量により5,840バイトとしています.そのため128kbpsのmp3データを受信するには,(5,840*8/128,000)=365msよりもRTTが小さい必要があります(他のオーバーヘッドを無視した場合).
海外のインターネットラジオサーバのRTTは200ms~300ms台が多いようです.
しかしながら前述の問題が起きた放送のサーバは,RTTが約200msでした.
そこでtcpdumpを使ってパケットの転送状況を調べてみました(スイッチングハブでは異なるホスト間の通信を覗き見できませんが,昔ジャンク屋で買ったリピータハブがあったので役に立ちました).
その結果,SHOUTcastのある種のサーバでは,データを8,192バイト単位のブロックで区切って送信するようになっており,1つのブロックを送信するとそのブロックの最後のパケットに対するACKを受信するまで次のブロックを送信しない仕組みになっていることが分かりました.
具体的には,例えばMSSが1,414バイトの通信の場合,まず5つの1,414バイトのパケットを送信した後で1,122バイトのパケットを送り,その1,122バイトのパケットに対するACKが返るまではたとえウィンドウサイズに余裕があったとしても次のデータを送信しません.
128kbpsの放送を再生している場合,1,122バイトのパケットを受信バッファから読み込んでACKを返すと共にmp3デコーダへデータを転送し始めた場合,mp3デコーダへの転送は(1,122*8/128,000)=70msで終わりますが,次に再生すべきデータが到着する時間はサーバにACKが到着してからデータが送られるまでの時間,つまりRTTの時間(上記のサーバの場合約200ms)を要するため,mp3デコーダに送るべきデータが到着せず,音が途切れることになります.
この問題を防ぐには,受信したデータを蓄えておく(先読みしておく)バッファを用意しておき,最低でも(200-70)msの遅延が生じてもその間mp3デコーダにデータを送信し続ける必要があります(70msはブロック末尾のパケットサイズに依存するので,実際はRTT以上の遅延に対する対処が必要).注意点として,ACK送信前の受信データをバッファリングしても意味は無く,ACK送信後のデータをバッファリングする必要があるということです.TCPのウィンドウサイズを増加させてもこの問題は解決できません.
この問題に直面した後で既存のインターネットラジオを調べてみると,トライステート社のBB-Shoutでは2MBのRAMを,Microchip社のInternet Radio Demonstration Boardでは64KBのRAMをそれぞれバッファ用に外付けしていました.
ところで上記のようなことは起こらず問題なく聞ける放送局もありますが,この場合は使用しているサーバのソフトが,8,192バイト単位のブロックでACKを要求するような仕組みになっておらず,常にウィンドウサイズを上限としてデータを送信し続ける仕様になっていました.具体的には,"SHOUTcast Distributed Network Audio Server/win32 v1.9.5"というソフトを使っているサーバでは問題が起きて,"SHOUTcast Distributed Network Audio Server/Linux v1.9.8"というソフトのサーバでは問題なく受信できました.「Icecastサーバを動かす」で記述したLinux用のIcecastサーバにもこの問題はありませんでした.
問題をまとめると,今回作成したインターネットラジオは現状の回路では受信用バッファを外部に持っていないため,「放送局のサーバがウィンドウサイズに空きがある間はACKを待たずにデータを送信する仕様になっていて,RTTが(5,840*8/<ビットレート>)未満である」放送しか受信できないという制限があります.
問題なく受信できる放送局もあるのでとりあえずそのまま使っていますが,やはり受信できない放送局があるのは悲しいので,そのうちシリアルSRAMの23K256などを入手して改良したいと思います.23K256はSPI接続のRAMであり,mp3デコーダをつないでいる3ステートバッファ(74LV541)にまだ余りがあるのでそれを利用して接続できるのではないかと考えています.