ラベル UDP の投稿を表示しています。 すべての投稿を表示
ラベル UDP の投稿を表示しています。 すべての投稿を表示

2016年4月24日日曜日

WiFiワイヤレスオーディオ(失敗)

送信側

受信側。手前の補聴器1個15万円ナリ

■やりたいこと■

赤外線ヘッドフォンアダプタが壊れました。私は耳が悪いので、これがないとテレビが見られません。アニメ見られないのは死活問題ですw ただ、最近市販されているワイヤレスヘッドフォンは音量が低すぎてほぼ聞こえません。特にBluetoothものは出力が小さくて先日ヨドバシで試聴してみたのですが、全滅でした。なんせ私はiPhone+標準添付のイヤホンだと最大音量にしても音楽ほぼ聞こえないんです。やぁねぇ、難聴orz

ということで、ESP-WROOM-02を使ってライン入力した音声信号をWiFiで飛ばし、受信側もESP-WROOM-02を使って受けたデータをDACで出力しそれなりに増幅してヘッドフォンへ…ということを試してみました。ラズパイなら簡単なんですが首からラズパイぶら下げるわけにもいきませんしw

ESP内蔵のADCはあまりにも貧弱なのでMCP3204、DACはMCP4922を使いました。どっちも秋月で入手できます。アンプとしてはポタアン用オペアンプを試してみて、音量が出ないようなら適当なパワーアンプを使ってみようと思ってます。

…とスタートしたのですが。

■先に結論■

今のところ、意図したようには動いていません。ADC/DACからのやり取りとESP-NOWまたはUDPでの通信はそれぞれはちゃんと動いているのですが、一緒にするとデータが欠落してしまいます。

サインカーブを送信して受信側の信号をイヤホンで聞いていると、基本的には連続音がずーっと聞こえていて、数秒ごとにバタバタとノイズが入り、ときたまプチっと100mSecぐらい空白ができる…という感じです。HiFiはまったく期待していなかったのですが、ノイズはいかんです。

ADC/DACを扱うためのインターバルがESP-NOW/UDPの送受信処理によってブロックされているようです。FIFOバッファを持ったADC/DACでないと無理かなぁ。

当初の目標に関しては失敗なのですが、とりあえずADC/DACの接続、ESP-NOW/UDPでの伝送などの参考にしていただければ幸いです。

関係ないですが、インターフェース誌のMATLAB特集を読んでフィルタなどを生成できることを知りまして、「あれ、もしかしてMATLABで補聴器作れるんじゃね?」と思ってググってみたら、すでにやってるヒトが。世界は広い。ただ、帯域ごとのコンプレッションは含まれていないので、そこは遊べる余地があるなぁ。

■高速DA変換テスト■

苦手なSPIなので、メモリ上に作ったサイン波形を連続出力してみました。イヤホンを直結すると「ぷー」という200hzの単音が聞こえます。

■高速AD変換テスト■

標準SPIライブラリで読み込み処理を書きました。

昭和臭ただよう可変抵抗器

その簡易的な動作テストとしてボリューム(可変抵抗)をつないですべてのビットが出てくるかを確認します。



これはそのための文字列表示処理
  int a = readADC(kRightCh);
  char buf[32], buf2[32];
  itoa(a, buf, 2);
  sprintf(buf2, "%16s %5d", buf, a);
  Serial.println(buf2);


読み込み処理はMCPのタイミングチャート(page 21)の通りに書けばこうなります。
  digitalWrite(SS, LOW);
  SPI.transfer(0b00000110);
  uint16_t result = word(0x0f & SPI.transfer(0x00), SPI.transfer(0x00));  
  digitalWrite(SS, HIGH);
でも、これだとch選択が分かれてしまって、今ひとつ面倒くさいです。その前のページを見るとスタートビットさえあれば動くっぽいので
  digitalWrite(SS, LOW);
  SPI.transfer(0b00011000 | ch);
  uint16_t result = word(0x3f & SPI.transfer(0x00), SPI.transfer(0x00)) >> 2;  
  digitalWrite(SS, HIGH);
と書きました。ちなみに連続した24bitのパターンが崩れなければいいので、
  digitalWrite(SS, LOW);
  SPI.transfer(0b01100000 | ch << 2);
  uint16_t result = word(SPI.transfer(0x00), SPI.transfer(0x00)) >> 4;  
  digitalWrite(SS, HIGH);
と書いても動きます。計算量は同じなので、どちらでも。

■ESP-NOWについて■

一度に送れるパケット長は最大200バイトです。また、CRCをつけて調べてみたのですが、パケット単位の誤り検出が行われているようで、化けたデータは届きません。

別のESPから送信しっぱなしにしておいてデータを受信したらLEDが点灯する状態で走らせれば、本来なら常時点灯したままになるはずです。しかし、時々0.5-1Hzぐらいの周期でLEDが消えます。これはたぶん他のWiFiからの干渉なのだと思います。本来、WiFiはチャンネルを自動選択しますが、ESP-NOWは固定したままなので、これはどうしようもないですね。

UDPではその現象は起こりません。が、正弦波を流してイヤホンで聞いていると、前記の通り数秒ごとにバタバタとノイズが入ります。ノイズの入り方はESP-NOWでのLEDの点滅と同じような感じなので、共通した原因なのだろうと思います。

■動作テストと考察■

オーバーランやアンダーランの処理を真面目に書いてないですが、テストとして送信側では計算で作ったサインカーブをバッファにセットし、ADC読み込みをスキップした状態でパケットを送ります。受信側は受信したパケットを順次DACに送出して、オシロで波形を見ます。

きれいな正弦波が見えるのですが、ときどき直流になりますw 受信データが化けていないのはCRCで確認済(このソースには含まれていません)なので、WiFiの受信処理によってDACへ出力するためのインターバルが止まっているのが原因と思われます。ただ、送った波形が少なくとも数パケット分なんの障害もなく受信されていることの方が多いので、定常的な受信処理そのものでインターバルがブロックされるわけではなく、それ以外のオーバーヘッドによって止まっているのではないかと推測しています。

一般的に音声信号は微小時間単位で見ると同じパターンの繰り返しなので、パケットの受信にときどき失敗することがあってもDACへの出力が止まらなければ聴覚上それほど酷い状態にはならないのですが、ときどき止まるってのは冒頭に書いたようにわりと耳障りな状態になります。

■ソース■

githubに置いておきます。


■今後■

ESPでI2Sで動作させた事例を見つけたので、DACチップを入手できたら試してみます。

■メモ■

  • ADC/DACともに、CSはずっとLOWにしててもダメです。特にDACはラッチがついているから問題ないんじゃないかと思ったんですが、1データごとにCSを上げ下げしないと動いてくれません。
  • 前にも書いたかもしれませんが、Serialは結構CPUパワーを食いますしTimerの動きも邪魔します。動作の様子を見るためのSerialですが、高速処理にハサむとシュレディンガーの猫的な状態になるので気をつけましょう(経験者・談

2016年1月11日月曜日

ESP-WROOM-02でSPI接続のADCから取得したデータをUDPで送出


緑のマイク基板と赤いESP基板の間にあるのがMCP3002。赤い基板上で右側のLEDがうすぼんやりと点灯しているのは、SPIのDIN信号。

50kspsでサンプリングしたデータをUDPに激しく送信中。

■SPI接続のADC■

ADCもいろいろありますが、数十Ksps以上のシリアルはSPI接続のものが多いです。

今回、MCP3002という精度10bit、入力2chのADCを使います。これは5v電源なら200kspsまで動き(2.7vだと75ksps)、そのわりに秋月で180円という大変おトクなチップです。マイクをつなげばお手頃な音声入力などが可能です。

SPIはあまり使ったことがないので、こちらの記事(ESP8266 (ESP-WROOM-02) でセンサーを扱う)を参考にさせていただきました。ありがとうございます。

SPIドライバはこちらからダウンロードします(MetalPhreak/ESP8266_SPI_Driver)。通常のArduinoライブラリとは形式が異なるので、以下の作業でArduino IDEが認識できるようにします(もっといい方法をご存知の方、ご教示いただければ幸いです)。

  • ダウンロードしたらzipを解凍する
  • フォルダの名前をESP8266_SPI_Driverに変更する
  • フォルダの中のspi_register.h, spi.h, spi.cをESP_SPI_Driver直下に移動
  • Arduinoのlibrariesフォルダ直下へ移動
  • Arduino IDEを再起動

動作させてみたところ、1回のデータ取得に要する時間は11-12μ秒程度でした(関数を100回呼び出して計測)。

■ESP-WROOM-02からUDP■

1回のデータ送信に要する時間は60-70m秒程度でした(関数呼び出し)。

■ESP用のソース■

なるべく一定の速度でサンプリングしつつ、UDPで送信しなければならないのですが、両者のスピードが違いすぎるので何らかのマルチタスクっぽい仕組みが必要です。Arduinoではこういう場合Tickerを使うのですが、Tickerは最小単位がミリ秒なので、1秒間に1000データしか取れません。それではせっかく高速のSPIを使う意味がありません。

幸いなことにESPにはos_timer_arm_usというμ秒単位のインターバルが用意されていますので、これを使います。なお、これを使う場合には、setupの始めの方でsystem_timer_reinit()を使って初期化し直す必要がありますので、ご注意を。これを使わないとミリ秒でしか動いてくれません。

ということで、おおまかな構成としては

  • 初期化ルーチンでWifi接続してから、20μ秒ごとのインターバルを開始
  • インターバルで呼ばれたらSPIからADCのデータを取り込みバッファにしまう
  • バッファが一杯になったらフラグを立てて別のバッファに切り替える
  • loop()ではバッファ一杯になったかどうかのフラグをチェックし、いっぱいになっていたらUDPでデータを送信します。



■検証用のJavaコード■

UDPでちゃんとデータを送り出せているのかを検証するために受信側のコードをJavaで作りました。UDPを受信して、連番と最小値/最大値などを表示するだけです。グラフ化したり音声を出力する根性がなかったので値だけです。

こちらの記事を参考にさせていただきました。ありがとうございます。
http://k-hiura.cocolog-nifty.com/blog/2011/07/java-6e01.html


■ハマりどころ■

  • 自家製ESPモジュールからIO15が出てない。IO15のプルダウン抵抗近くのviaホールにハンダ付けして引っ張り出す。
  • SPIが動かない。これは当初参考にしていた記事の配線図が間違っていたため。私の2時間を返せ。ということで著者さんには間違ってますよ、とメールしておいた。まだ直ってないなぁ。
  • 接続直後にESPが暴走する。あれさっきまで動いてたのに…?? と思ったら、初歩的なC言語的間違い。30年ぶりぐらいにやらかした。
      char **buf; buf[0] = ptr1; buf[1] = ptr2; 
  • ESPはos_timer_arm_usってAPIでマイクロ秒単位のTickerを使えるはずなのに、ミリ秒単位でしか動いてくれない。改めてSDKマニュアルを読んだら、system_timer_reinit()で初期化しないとダメよと書いてあった。
  • Javaの符号なし整数にまたハマってしまった。いい加減覚えろよ>俺