USB Audio2.0の自作!PICでI2Sハイレゾ出力に成功

USB Audio2.0の自作!PICでI2Sハイレゾ出力に成功

PIC32MZ を使って、ハイレゾ対応の USB Audio I/F を作ることができました。

TQFP-64pinのハンダ付けさえできれば、USB(USB Audio Class2.0)からハイレゾのI2S信号へ変換して、好きなDACへ流し込んでやることができます。

標準で動作するレートは、44.1kHz/48kHz 16/24/32bit ~ 176.4kHz/192kHz 16/24/32bit のPCM(I2S)。(DDSはPICでは無理なので非対応)

外付けのOSCなどを追加すれば、352.8kHz/384kHz 24/32bit でも動作します。

ちなみに、Harmony は使っていません。使わないで正解でした。

小型高性能なデスクトップ向けUSB Audio DACを作っている過程でできたものですが、同様の自作オーディオに挑戦される方の参考になるのではないかと思います。

※本作をベースに、高性能DACの BD34301EKV を使用したDACアンプの製作例
より詳しく⇒USBAudio2.0 DACの自作!BD34301EKV搭載

USB Audio自作への道のり

USB AudioからI2Sへ変換してDACへ流す系の自作では、シンプルというか、しっくり来る方法がないと思いませんか?しかもハイレゾ対応となると、なぜか見かけない。
USB Audio Class2.0 の仕様が出てから15年くらい経つのになんで?

例えば、自作で定番のUSB Audio I/Fを内蔵するチップとして、PCM2704/5/6/7がありますが、USBからI2Sを取り出せるものの、DAC内蔵というのが引っかかります。

もちろん簡単に作る時はいいのですが、他のDACを使いたい場合は無駄になってしまう。
そもそも、192kHzサンプリングといった真の?ハイレゾには対応していません。

シリコンラボに、CP2615というUSB Audio I2Sブリッジなるチップもありますが、これもハイレゾには対応していないですね。

他にないのかとようやく見つけたのが、中華のUSB Audio DACなどで使われている、VIA社のVT1736というチップなんですが、既に生産終了しており、そもそも販売している所がなくて個人では入手不可な品でした。

疲れたうさぎ今時、なんでないの?? ロイヤリティーの問題?

以前は、CPLDやFPGAを使った方法が雑誌などで紹介されていたようですが、なんだかそこまでしたくないし。。

他には、ハイレゾアンプの自作でもやった、USB Audioボード、例えば Combo384 などを使う方法もありますが、小型化したかったり、使いたいDACの制御シーケンスとマッチしないこともあります。

そこで、ふと、PIC32のペリフェラルで内蔵されているI2S(SPI)モジュールとUSBモジュールを使ってできないものかと調べていたところ、できそうな気がしてきたので試してみたのが本作です。

構成と概要

ブロック図

HiRes USB Audio2.0 ブロック図

PIC32が持つ USB2.0 と I2S(SPI) の機能を利用するのでこれだけです。

ただ、352.8kHz/384kHz サンプリング 24/32bit以上については、PIC32では想定されていないようで、ちょっとした工夫が必要です。>ハイレートのサポート方法

使えるPICの条件

PIC32にはいくつかのラインナップがあり、電子工作でよく使われているものといえば PIC32MX や PIC32MZ ではなかろうかと思います。

PIC32のラインナップ概略
ファミリ概要
PIC32MX 1/2/5PC32登場初期のシリーズ。エントリーモデル。
PIC32MX 6/7
PIC32MK
ミドルクラス。
PIC32MZ EF
PIC32MZ DA
ハイパフォーマンス。本作ではEFを採用。
DAは高機能だがUSB Audioでは使わない。

必要な機能は、USB2.0 と I2S ですが、PIC32ならどれも内蔵していると思います。

メモリ(RAM)は、意外にも50KByteもあれば余裕です。もっと少なくても大丈夫なんですが、余っても仕方ないので最大限使えば良いでしょう。

システムクロックは60MHzで問題なく動作します。これは、USB Audio機能だけを動かした場合のおおよその目安で、これより遅いとSPIモジュールでアンダーフローエラーが発生し始めます。※後述の、ハイレートをサポートする場合は100MHz。

結局のところ、最大クロックだけ注意すれば良いのではないかと思います。
ただし、PIC32MZ EF以外では試したことはなく、実際に動くかどうかはわかりません。

また、各ファミリで細かいスペックやレジスタの仕様が微妙に異なります。そのため、本作で公開しているソフトウェアがそのまま全てのPIC32で使えるわけではありません。

対応するUSB規格とUSB Audio Class

本作では、USB2.0(Full Speed)、USB Audio Class2.0 にのみ対応します。

古い規格である USB1.0(High/Low Speed)では、USB Audio Class1.0 での接続となり、その速度では最高でもサンプリング周波数96kHz 24/32bit までのオーディオしか再生することができません。

まあ、サンプリング周波数が96kHzなら一応ハイレゾと言えるのかもしれませんが、うれしくもなんともないですね。

USB Audio Class1.0と2.0とでは、通信プロトコルが少し異なるため、通信速度を変えるくらいのことで接続できるようなものではなく、互換性はありません。
なので、両方をサポートするなら、それぞれのソフト実装が必要ということになります。

USBホスト(PC)側とデバイス側でサポートする規格が異なる場合、低い方に合わせるのがセオリーだとは思いますが、今どきUSB1.0しかサポートしないPCなんて相手にするつもりはないので、USB2.0のみの対応としています。

I2Sフォーマットとクロック表

本作では、次のI2Sフォーマットを出力します。

16bit BCK=32Fs

I2Sフォーマット(16bit BCK=32Fs)の波形

16/24/34bit BCK=64Fs

I2Sフォーマット(16/24/34bit BCK=64Fs)の波形

データはMSB側から詰められ、32bitに満たない部分(BCK=64Fsの16/24bit時)は、0でパディングされます。

16bitデータの時に、32Fs/64Fsどちらのフォーマットで出力するかの設定をソフトに設けてあって、標準では64Fsになっています。ソフト設定を参照してください。
24/32bitの時は、常にBCK=64Fsです。

下表は、I2Sクロックの周波数の一覧表です。

クロック周波数一覧(BCK=32Fs)
Fs(kHz)MCLK(MHz)BCK(MHz)
44.122.5792 (512Fs)1.4112
4824.576 (512Fs)1.536
88.222.5792 (256Fs)2.8224
9624.576 (256Fs)3.072
176.422.5792 (128Fs)5.6448
19224.576 (128Fs)6.144
352.822.5792 (128Fs)11.2896
38424.576 (128Fs)12.288
クロック周波数一覧(BCK=64Fs)
Fs(kHz)MCLK(MHz)BCK(MHz)
44.122.5792 (512Fs)2.8224
4824.576 (512Fs)3.072
88.222.5792 (256Fs)5.6448
9624.576 (256Fs)6.144
176.422.5792 (128Fs)11.2896
19224.576 (128Fs)12.288
352.822.5792 (128Fs)22.5792
38424.576 (128Fs)24.576

352.8kHz/384kHz 24/32bitのサポート方法

352.8kHz/384kHz サンプリング 24/32bitでは、I2SのBCKは64Fs、つまり 22.5792MHz/24.576MHz となり、MCLKと同じ周波数になります。

しかし、PICのSPIモジュールのボーレートジェネレーターは、BCK生成において最低でも2倍の周波数のクロックが必要なため 45.1584MHz/49.152MHz が必要になるんですが、PIC内蔵のPLLと参照クロック回路ではその周波数が生成できません(近い周波数は生成できるが、誤差が大きすぎてまともに再生できない)。

そこで考えたのが、ボーレートジェネレーターの入力を外部クロックに設定して、外部から 45.1584MHz/49.152MHz を与えてやるという方法です。そして、それを1/2に分周したクロックをMCLKとしてDACへ供給します。

ただ、PICではボーレートジェネレーターに入力するクロックを分周して出力することができないため、外部にデバイダ(分周器)も必要になります。>外部OSCへ変更する方法

さらに、処理能力の都合から、システムクロックを100MHzにする必要があります。

ちなみに、現在視聴できる音源の市場状況を考えると、実用的には 176.4kHz/192kHz 24/32bit までで十分で、それ以上については特にこだわる必要はないと思います。
挑戦したい人向けですね。

低ジッタにしたい場合も外部OSCを使用する

先の高レートをサポートする場合もそうですが、それ以外にDACでの位相歪の低減を狙って、低ジッタのMCLK/BCKを生成したい場合にも外部OSCを使う必要があります。

というか正確には、低ジッタにするというよりも、高ジッタにしないための方法です。
なぜなら、PIC32のMCLKは、フラクショナル型分周器を使用して生成されるためです。

例えば、3MHzを2MHzへ分周する例では、
フラクショナル型分周によるクロック波形例

図のように、整数で割り切れない場合、つじつまが合うように所々クロックが抜かれる方式のため、周波数としては正しく分周されていても、クロックの位相が一定ではない部分が発生します。短い期間で見ると、ひどいジッタが発生していることになります。

これが音質や性能にどのように影響するかについては、使用するDACにもよるとは思いますが、一般にハイエンドなDACではMCLKを基に複雑な演算を行っていたり、音声出力タイミングに影響することが多いようなので、このようなクロックをMCLKとしては使いたくないですよね。(聴けないわけではありません、それでも普通には聴けます)

BCKもMCLKから分周して生成されるため、MCLKを必要としないDACでも同じです。

外部OSCへ変更する方法

外部OSCへ変更する必要があるのは、次の2つのケースです。

352.8kHz 24bit 以上の高レートをサポートしたい場合

→45.1584MHz/49.152MHz の外部OSC(それと1/2分周器)が必要。

低ジッタのMCLKを生成したい場合

→22.5792MHz/24.576MHz の外部OSCを使用する。(分周器は不要)

本作では、176.4kHz/192kHz 24/32bit までをサポートする例を標準として公開していますが、外部OSCへ変更することで 352.8kHz/384kHz 24/32bit でも動作することが確認できています。

また、本作をベースにしたDACアンプの製作でも、外部OSCを使用しています。
より詳しく⇒USBAudio2.0 DACの自作!BD34301EKV搭載

変更方法は次の通りです。

回路に外部OSCを追加する

目的の周波数のOSCを追加します。例えば次のようなものがあります。
●XLH73V024.576000I(24.576MHz)
●XLH736022.579200I(22.5792MHz)
●XLH735045.158400X(45.1584MHz)
●XLH736049.152000I(49.152MHz)
ルネサス XL Family データシート

高周波になると、出力形式がPECLやLVDSといった差動出力タイプが多くなってきますが、LVCMOSなどのCMOS系を使用するようにします。

また、必ずEnable制御ができるものが必要です。幸い、多くはEnableピンを備えており、大抵はpin1に割り当てられています。

回路にORゲート又はXORゲートを追加する

2つのOSCの出力を1つにまとめます。回路図を参照してください。
まとめるだけなので、マルチプレクサでも良いです。その場合、セレクト信号は外部OSCへのEnable信号を利用すると良いでしょう。

回路にデバイダを追加する

OSCの周波数が45.1584MHz又は49.152MHzの場合は、1/2デバイダ(分周器)を追加して、その出力をMCLKとします。回路図を参照してください。

例えば次のようなものがあります。
●542MLFT(クロックデバイダ)
ICS542 データシート

ソフトを外部OSC使用に切り替える

#defineにて設定を変更したソフトウェアを使用します。
ソフト設定を参照してください。

システムクロックを60MHz→99MHzへ変更する

これは、OSCの周波数が45.1584MHz又は49.152MHzの場合のみです。
外部OSCとは直接関係ないのですが、I2Sへの転送処理が間に合わなくてアンダーフローが出る事があるため、必要な対策になります。
sys.cにて、オシレータのConfigurationを変更します。

回路図

HiRes USB Audio2.0 回路図

HiRes USB Audio2.0 回路図

ハード的には最低限たったこれだけです。
点線で囲ってある部分は、先に説明した外部OSCを使う場合にだけ必要な部分です。

標準の場合(外部OSCを付けない場合)、MCLKはPICの内蔵PLLと参照クロック分周器を使って、12MHzから生成します。標準のシステムクロックは60MHzです。

なお、参照クロック分周器はわずかに分周誤差が出ます。これが嫌なら、システムクロック(SPLL)を192MHzにすれば、24.576MHzも22.5792MHzも誤差なしで生成できます。

HiRes USB Audio クロックダイアグラム

上の図で、青のラインは外部OSCを付ける場合のI2S用クロック経路になりす。

PIC32MZでは、USBを使用する場合、12MHz又は24MHzのクリスタルが必須です。
クリスタルに1Kの抵抗を挿入しているのは、エラッタにそのように指示されているためで、無いと不安定になる場合があるようです。
また、負荷抵抗に22pFと33pFを使っていますが、このように容量を変えることで起動が早くなるとのこと。ただ、必須ではなく同じ容量でも大丈夫です。

PICのT1CK(48pin)は、I2SのBCK(49pin)に接続します。後述しますが、これはBCKの周波数を測定してホスト側へフィードバックするために必要な接続です。

VUSBはコネクタ接続検知のためだけに使用し、電源としては使用しません。電源はセルフパワーで3.3Vを用意します。

UARTはPC側へログデータを送信するだけなので、必要なのは出力信号だけです(GND含め2本)。USBシリアルはバスパワーで動作するため、電源供給も不要ですね。

なお、今後の応用を考えて、PICのIOピンによるいくつかの入出力I/Fを設けています。

入出力インターフェース

これらのピンへの割当ては、ソフトのヘッダファイルで変更できるようにしてあります。

RE4(64pin):Enable入力
GNDへ接続すると動作を停止します。低消費電力状態へ遷移し、PC上ではUSBケーブルを抜いたのと同じ状態になります。
内部プルアップしているため、オープン又はVDDへの接続で動作状態になります。

RG6(4pin):出力機器電源操作出力
オーディオ再生が始まる前にHIになり、USBを抜いたりして再生されない状態になるとLOになります。これを使って、例えばアンプの電源を制御します。

RG7(5pin):アンミュート出力
ミュートが解除されるとHIを出力します。
PC上で音量ミュートしたり、サンプリング周波数の変更中などはLOになります。

RE1~3(61~63pin):サンプリング周波数情報出力
3bitで再生中のサンプリング周波数を表します。
0:44.1kHz, 1:48kHz, 2:88.2kHz, 3:96kHz, ・・・ 6:352.8kHz, 7:384kHz

RE6~7(2~3pin):量子化ビット数情報出力
2bitで再生中の量子化ビット数を表します。
1:16bit, 2:24bit, 3:32bit

RB14(29pin):外部OSC(22.5792MHz)制御出力
RB13(28pin):外部OSC(24.5760MHz)制御出力
外部OSCを使う場合に、そのEnable端子へ接続します。

製作手順

ブレッドボード上で組んで試すだけなので、製作というほどのものではないのですが、一応ご紹介しておきます。

用意するもの

主なものを記載します。他にも抵抗など、細かいものから電源まで色々必要です。

PIC32MZ

64pinのPIC32MZなら、どのデバイスでも公開している回路そのままで使えます。

64pinのPIC32MZ EFファミリ

本作では、以前秋月で売っていた「PIC32MZ2048EFH064-I/PT」を使っていますが、64pinの中で一番グレードの低い「PIC32MZ0512EFE064-I/PT」でも使えます。
ソフトは、プロジェクトの設定で、使うデバイスを変更してビルドするだけでOKです。

TQFP(64pin)変換基板

PICをブレッドボードで使うために必要。秋月の AE-QFP64PR5-DIP を使いました。
これにピンヘッダをハンダ付けして使います。

12MHzクリスタル

秋月で売っていたもの(HUSG-12.000-20)を使いました。

MicroUSBコネクタDIP化基板

秋月の AE-USB-MICRO-B-D を使いました。
要は、USBからブレッドボードへ接続できれば何でもOK。

USBシリアル

ログを見ないなら不要ですが、使うことをオススメします。
秋月の AE-FT234X を使いましたが、同様のものなら何でもOK。

音出し用DAC基板

ハイレゾのI2Sを入力できるDAC基板など。

PCM5102A搭載32bit 384kHz DAC完成基板32bit 384kHz DAC
PCM5102A搭載のDAC完成基板。DC4.2V~12Vを供給する必要がありますが、ヘッドホンジャックも付いているのでピンヘッダ接続ケーブルでつなげばすぐ聴けます。

製作例

PIC32MZへのピンヘッダのハンダ付けPICを変換基板にハンダ付けして、表側にはピンヘッダ(メス)もハンダ付けします。

こうしておけば、本作だけでなく他の実験などでも使えますね。

PIC32MZの変換基板へのハンダ付け(表側)こんな感じです。

PIC32MZの変換基板へのハンダ付け(裏側)裏側では、VDDピンの近くに0.1uFのパスコンをハンダ付けします。

また、回路図にはありませんが、念のため3216の10uFを2つ付けました。

MicroUSBのDIP化基板と、USBシリアルモジュールUSBシリアルモジュールと、MicroUSBのDIP化基板。

ブレッドボードに挿したPIC32MZ変換基板ブレッドボードに刺します。
PICKitと接続するためのピンヘッダ(5pin)も用意します。

ジャンパワイヤーで配線ジャンパワイヤーで配線します。

12MHzクリスタルの配線12MHzクリスタルの配線です。

今回使ったハイレゾDAC基板今回使ったハイレゾDAC基板です。

ステレオジャックが付いているので、ヘッドホンで簡単に聴けます。

DAC基板の入力端子DAC基板の入力端子部分にもメスのピンヘッダをハンダ付けしました。

ブレボとDAC基板の配線ブレボとDAC基板を配線します。

DAC基板の電源は、USBのVBUS(5V)を利用します。

PICKitを接続したところPICKitはこのように接続します。

USBシリアルへの接続はRxDとGNDの2本だけです。TxDは使いません。

通電して実験しているところ電源を入れて、ビルドしたファームウェアをPICへ書き込みます。
後は、USBケーブルをPCに接続するだけでUSB Audioとして認識され、PC上で何か再生すればDAC基板を通して音が聴こえます。

PICの消費電流は、再生時で66mA、待機時で32mAでした。

HiResUsbAudioのプロパティー表示
PC上でデバイスのプロパティーを表示させると、サポートしているレートがこのように表示されます。

切り替え速度は非常に早く、DACの切り替え処理が優れていることもあって、切り替え時のノイズはほとんどありません。

ソフトウェア

PICでUSBを扱うとき、まず考えるのがHarmonyの利用ですね。
私を含め、多くの電子工作家は、プログラミングできるほどUSBの仕様について詳しくないし、特に知りたいとも思ってないでしょう。

そんな時にHarmonyを使えば、ちょちょいと設定するだけでソースコードを生成してくれるので、開発が楽になるハズだと淡い期待を持つわけです。

HarmonyにはUSB Audio Class1.0のサンプルアプリが入っているので、それをベースにやっとの思いで作ってみたところ、無事に音出しすることができました。
ちなみにこのサンプルでは、確信犯なのかどうか分かりませんが、受信パケットサイズが固定のため、44.1kHzベースのオーディオ再生では時々データが欠落します。

そしてClass2.0に対応しようとした時です。あれ?Class2.0のサンプルはありません。とりあえず設定でClass1.0からClass2.0へ変更してソース生成してみます。

まあ、想像はしていましたがビルドは通りません。関数名や構造体などの名前が、Class1と全部異なっています。ここでようやく互換性がないことに気づきます。

さらに、Harmonyの生成するUSB Audio Class2.0には、基本的な所にバグがあって、そのままでは動きません。全くテストしていない事はないと思うんですが、なんで?

で、色々頑張った結果、気付けばUSB Audioについてある程度の知識が付き、Harmonyの化けの皮を剥がすところまで来ていました。

Harmony使うべからず

Harmonyは、色んなデバイスの共通プラットフォームとして使えるように設計されています。でも、それは作り手、つまりMicrochip側の都合でしかありません。

HarmonyでUSB Audioアプリのベースを生成すると、主にUSB関連を中心として、130以上のソースファイルがプロジェクトに追加されます。

えー!USBってそんなに複雑なんだ… 最初はそう思いましたが、違いました。
複雑なのはUSBだけではなくてHarmonyの構造です。

使おうとしているターゲットは、当然1種類のPIC32MZだけです。それなのに、様々なデバイスで使えるようにするためのラッパーなどのコードが大量にブチ込まれます。
つまり、自分にとって全く不要なものが、勝手に大量に入れられるんです。

なんとなくは分かっていましたが、少しくらいならいいんです。でも、実際の姿を目の当たりにして、もうHarmonyを使う気は失せました。

そして、スクラッチからUSB Audioを自作した結果、USB関連ファイルは全部で4つ、1100行程で済ませることができたんです。

仮にHarmonyでできてたとしても、メモリや実行速度など、リソースの面で厳しかったかもしれません。大変でしたが、完全自作で正解でした。

Harmonyは、簡単なプロジェクトや実験的なもので使う、あるいは、生成されたソースの一部を流用したり、参考にする程度に利用するのが良いのではと思います。

ソースとビルド

公開しているファイルは、MPLAB Xプロジェクトを含んでいます。

ソースファイル一式(c/hファイル in ZIP)

解凍して出てきたプロジェクトをパソコン上の適当な場所にコピーして、MPLAB X で開けばビルドできます。ビルドに必要な外部ライブラリなどはありません。

本作では、PIC32MZ2048EFH064 を使っていますが、別の品種を使う場合は、プロジェクトの設定でデバイスを変更してからビルドします。

なお、最適化レベルは「2」に設定してあります。ブレークポイントを当ててデバッグするときは、「0」に設定しないと変な所で止まったりします。

使用したIDEのバージョンは下記の通り。2022/4 頃の最新版です。

MPLAB X IDE:v6.00
XC32:v4.00

MPLAB- X IDE | Microchip Technology Inc.

書き込みやデバッグには PICkit4 を使いました。

Microchip MPLAB PICkit4インサーキットデバッガ/プログラマPICkit4
PICkit3より色々拡張されていますが、速度が早くなったのが一番のメリットです。PICkit3のもっさり感が解消されています。

ソフト設定

ソフト上での主な設定項目です。他にも細かな設定があります。

config.h

メインの設定ファイル(#define)で、各種設定をまとめてあります。
外部OSCを使う場合は、このファイルで切り替えます。(★印)

  • システムクロック周波数
  • キューのサイズ
  • ログ出力に関連する設定
  • 16bitデータ時のI2Sフォーマット選択
  • 外部OSCを使用するかどうか★
  • 高レート(352.8kHz/384kHz)をサポートするかどうか★
  • IOのピン割り当て
app.c

テストトーン再生に切り替える#defineがあります。
音がおかしい時の問題の切り分けに使います。

usb_desc.h

各種USBデスクリプタを記述しています。
ベンダーIDなどもここです。

USBデバイスの固有情報について

USBのプロトコルでやり取りされる情報の中には、製品IDやベンダーIDといった、デバイス固有の情報(文字列)も含まれています。

Windows10では、USB Audioを接続すると、製品IDが下のように表示されます。

本作のUSB製品IDが表示されているところ

製品IDは自由に付けて大丈夫ですが、ベンダーIDについては、USBの元締め USB Implementers Forumから、ユニークなIDを有償で割り当ててもらう必要があります。一般にUSB機器を製造・販売するメーカーが取得します。

Getting a Vendor ID | USB-IF

本作のベンダーIDは、ソース上で設定しており適当な文字列となっていますが、個人宅でしか使わないため自己責任で任意に設定したものです。
公開しているソースを利用する場合はこの点に注意して、変更するなどしてください。このIDも含め、公開ソースを使用した事により発生した問題の責任は一切持ちません。

ログ出力について

本作では、デバッグ用にログ出力機能を実装しており、PICのUARTピンにシリアル接続することでログを閲覧できます。

HiRes USBAudioのログ出力通信速度は115200bpsで、ストップビットなどの他の通信パラメータはデフォルトのままでOKです。

この画面は、Tera Termを使って表示した、起動直後から、オーディオ再生中に至るまでのログの一部です。

処理の概要

プログラムは主に、メインからループ呼び出しするTask処理と、割り込みハンドラ内での処理の2つで実行しています。

やっていることとしては、大まかに次の3つです。

  • PIC32のUSB制御
  • PIC32のSPI(I2S)制御
  • USB2.0/USB Audio Class2.0

PIC32のUSB制御

頼りにしたのは、次のマニュアルとネット情報(主に海外)でした。

セクション 51. ハイスピード USB On-The-Go (OTG)

USB関連レジスタの一覧が「表51-1」に、7ページに渡って記載されています。
これらのうち実際に使っているのは、最初のページにあるものがほとんどで、後はいくつかあったくらい、全部で20本もなかったと思います。

しかし、このマニュアルの記載には誤りや欠如している部分があり、ここに載っている情報だけでは無理です。ネットでググりまくって、主に海外のフォーラムなどから情報をいただきました。残念ながら日本ではほとんど見かけません。

設定してもしなくても動きが変わらないなど、未だに意味不明なレジスタがいくつもあるんですが、いくら調べてもでてきません。誰も知らないようで、もしかするとPIC32のUSB機能は、市場でさほど活用されていないのかもしれません。

話がずれましたが、エンドポイントの設定と割り込み処理が主な仕事になります。
エンドポイントとは通信チャンネルのようなもので、インターネットプロトコルでいうところのポートのようなものです。

PIC32では、8つのエンドポイントがハード的にサポートされていて、それぞれのレジスタを持っています。USB Audio Class2.0では、通常3つだけ使って残りは使いません。

使っているエンドポイント一覧
EndPoint方向転送タイプ備考
EndPoint 0ホスト ⇔ デバイスControl0は制御用に予約されている
EndPoint 1ホスト ⇐ デバイスIsochronousフィードバック用
EndPoint 2ホスト ⇒ デバイスIsochronousオーディオストリーム用

USBケーブルを挿すとエンドポイント0で標準デバイスリクエストを受信し、ホストからの基本的な要求に応えたら、オーディオクラス固有のネゴシエーションに入ります。

その後、アイソクロナスエンドポイントを介してオーディオストリームの転送が開始されるので、データを受け取ってはバッファに格納していく処理をひたすら繰り返します。

USBモジュールには専用のDMAが用意されており、実際にバッファに格納する処理はそのDMAを使って行います。そしてバッファのポインタをリング状のキューに格納します。

PCがスリープしたりUSBケーブルを抜くと待機状態に移行します。この時、システムクロックの周波数を60MHz→12MHzへ引き下げて、消費電力を抑えるようにしています。

なお、USB通信では、バイトオーダーはリトルエンディアンでやり取りします。

PIC32のSPI(I2S)制御

I2Sに関しては、さほど苦労はなかったです。少々古い(PIC32MX向けの)ですが、アプリケーションノートもあります。

セクション 23. シリアル ペリフェラル インターフェイス (SPI)
AN1422:PIC32 を使った高品位オーディオ アプリケーション

再生開始が決まったら、USB側で決定されたサンプリング周波数や量子化ビット数に合わせて、SPI関連レジスタやDMAを設定します。

USB処理で受信したデータがキューの半分まで溜まったところで再生を開始、後はDMAを使ってSPIモジュールへ転送する処理をひたすら繰り返すだけです。

なお、量子化ビット数16/24/32bitのうち、一番トリガ回数の多いのが24bitです。3Byteは2で割り切れず、DMAのセルサイズを1Byteにする必要があるためです。

ちなみに、Harmonyのサンプルアプリでは、24bit(3Byte)のシリアルデータを、なんとforループを使って1Byteづつコピーし32bit(4Byte)へ変換するという、素人仕事というか、コストの高い処理をしていました。

そんな事をしなくとも、SPIとDMAをうまく設定すれば、24bitデータをSPIのFIFOへ、ハードウェアを使って転送できます。

USB2.0/USB Audio Class2.0

次の2つが主な仕様書です。他にもありますが、ほとんど参照せずに済みました。

Universal Serial Bus Specification Revision 2.0
Universal Serial Bus Device Class Definition for Audio Devices Release 2.0

これを見る限り、USB Audio Class2.0は、USB2.0のベース仕様も合わせると膨大なボリュームになります。しかも英語のみ。

最初見たときには、コレ全部実装せなあかんのん?と気が遠くなりました。しかし実際には、細かい仕様のほとんどを使いません。

そもそも、ホスト側でサポートされていない、制限されている仕様はたくさんあります。
Windows10でサポートされているのは下の通り一部なんですが、これはWindowsに限った話ではないです。

USB Audio 2.0 のドライバー – Windows drivers | Microsoft Docs

本作では、この内容に基づいて最低限の実装だけを行っています。なので、MACに繋いでもうまくいかないかもしれません。

■デバイスリクエスト

本作で対応しているデバイスリクエスト一覧です。
これらはホストから送られてきて、デバイス側ではそれぞれに応じた処理を行います。

Standard Device Request
・SET_ADDRESS
・GET_DESCRIPTOR
・SET_CONFIGURATION
・GET_CONFIGURATION
・SET_INTERFACE
・GET_INTERFACE
・GET_STATUS

Class-Specific Requests(AudioControl Requests)
・Clock Source Control Request
・Clock Selector Control Request
・Feature Unit Control Request

標準デバイスリクエスト(Standard Requests)は、USB2.0で規定されています。
クラス固有のリクエスト(Class-Specific Requests)は、Audio Class2.0規定です。

■USB記述子(デスクリプタ)

本作で定義しているデスクリプタ一覧です。
これらは、最初の段階でホストから送られてくる GET_DESCRIPTOR リクエストを受けた時に、その応答としてホストへ送信します。

・Device Descriptor
・Configuration Descriptor
・InterfaceAssociation Descriptor
・Interface Descriptor
・ACInterfaceHeader Descriptor
・ACClockSource Descriptor
・ACClockSelector Descriptor
・ACInputTerminal Descriptor
・ACFeatureUnit Descriptor
・ACOutputTerminal Descriptor
・ASInterface Descriptor
・ASFormatType Descriptor
・DataEndpoint Descriptor
・ASIsochronousDataEndpoint Descriptor
・FeedbackEndpoint Descriptor
・String Descriptor

標準デスクリプタは、USB2.0で規定されています。
クラス固有の各ディスクリプタは、Audio Class2.0で規定されています。

■フィードバック制御について

オーディオデータは、アイソクロナス転送により、ホスト側から問答無用で次々と送られてきます。TCPやシリアル通信にあるようなフロー制御はありません。

ハードウェアやプログラムは、クロックを基に動作しているため、ホスト側とデバイス側で同じクロック源を使わない限り、発振器の周波数がスペック上は同じでも完璧に一致することはありえず、わずかながら動作スピードに差があることになります。

なので、そのまま転送を続けていると、いずれデバイス側のバッファでデータが溢れたり、逆に空になってしまうという状況が発生します。

これに対処するため、アイソクロナス転送では、ホストとデバイスが同期する方法として、次の3つが規定されています。

Asynchronous

フィードバック情報をホストへ常時送信し、ホスト側に転送速度を調整してもらう。
恐らく最も一般的で安定性の高い方法。

Synchronous

SOF(Start-of-Frame marker)を基準にして、デバイス側の処理全体(USBとI2S)のクロックソースを調整する。SOFにロックする形になる。

Adaptive

バッファリング量から速度を判定し、デバイス側のI2Sのクロックを操作して、再生速度だけを調整する。

本作では、Asynchronous を採用しています。HarmonyのサンプルアプリではAdaptiveでやっていますが、精度や安定性も悪く良いことは何一つないのでやめた方がいいです。

フィードバック情報は、フィードバック用に用意したエンドポイントを使って、そのエンドポイントディスクリプタで指定した間隔(bInterval)で送信します。

送る情報は、32bit 16.16形式の固定少数値で表したデータレート(サンプル数)で、USB2.0の「5.12.4.2 Feedback」で説明されていますが、少々難解で苦労しました。

ここで説明されているのは、要は、オーディオの基になるクロックやパケット送信間隔は全て2の乗数で表される値であることから、所定の期間カウントしたクロック数をシフト演算するだけで剰余誤差なく正確なレートを得ることができる、という事のようです。

なので、MCLKやそれを分周して得られるBCKのクロックをカウントすればいいわけで、本作ではそのためにTMR1の非同期モードカウンタを使用しています。

さらに、仕様書には1マイクロフレーム当たりのレートと書かれていますが、実際に試してみると「データエンドポイントで1回に受け取るデータ」で考えなければダメな事が分かりました。つまり、データエンドポイントの bInterval も考慮する必要があります。

具体的に示すと次のようになります。

例えば、192kHzサンプリング32bitのオーディオを受信するとします。この時、BCKは64Fs(32bit×L/R)で、12.288MHzになります。

USB2.0 High-Speed なので、1マイクロフレームは125us。
量子化ビット数32bitの場合、本作のデータエンドポイントの bInterval は3、フィードバックエンドポイントの bInterval は4です。

フィードバックは、2の(bInterval-1)乗、2の3乗ということは、125us×8=1ms間隔、実際にはSOFを8回受け取るごとに送信します。

TMR1でBCKをカウントし、この1msの間にカウントされた値が、1つのフィードバック情報の基になります。転送速度にズレがないとすれば、12.288MHz×1/1000秒で、12288カウントされるはずですね。

これをまず、64Fsなので64で割ってサンプル数を算出します。(×2の-6乗)
これはSOF8回分、つまり8マイクロフレーム分なので8で割ります。(×2の-3乗)
データの bInterval は3なので、2の(bInterval-1)乗をかけます。(×2の+2乗)
16.16形式の固定少数値へ変換するために、左へ16シフトします。(×2+16乗)

総じて、2の+9乗を掛ける(左へ9シフトする)だけで良いということになります。
左シフトだけなので桁落ちは発生せず、これが正確にレート算出できる根拠となります。
12288 ×(2の+9乗)=6291456、16進数で表すと0x00600000となり、この4バイトをフィードバック値として、1ms(8SOF)間隔で送信します。

この値が妥当な範囲から大きくズレていたり、送信間隔がズレていても、ホスト側からは無視されてしまいます。すると、次第にバッファ量が溢れたり空になっていきます。

うまくいくようになってからは、再生開始時のバッファリング量(全体の半分)を正確に保つようになりました。これが、メモリ量が少なくても大丈夫な理由です。
バッファが溢れて音が途切れたりということは全くありません。

フィードバックログの一部フィードバック情報のログ出力。
100個あるキューのうち、データが入っているのは50個を意味します。
49と51しか見たことがないです。

さらに本作では、何らかのトラブルなどでバッファ量が大きく増減してしまった場合も考慮し、その時のキューイング量に応じて微妙に調整するようにもしてあります。

なおWindows10では、フィードバックエンドポイントの bInterval は、最大4までしかサポートされていないため、遅くとも1ms間隔でフィードバックする必要があります。

ダウンロード・資料

製作に使用した全ファイルです。無断で二次配布することはご遠慮ください。ご紹介いただく場合は当記事へのリンクを張ってください。連絡は不要です。

全回路図(pdfファイル in ZIP)
ソースファイル(c/hファイル in ZIP)

ファームウェア開発環境:MPLAB-X IDE

関連資料

本作に関連する資料をまとめました。

USB仕様

Universal Serial Bus Specification Revision 2.0
Universal Serial Bus Device Class Definition for Audio Devices Release 2.0

PIC32MZ EF

PIC32MZ EF Family Datasheet
PIC32MZ EF Family Silicon Errata and Data Sheet Clarification

PIC32 マニュアル

セクション 12. I/O ポート
セクション 14. タイマ
セクション 21. UART
セクション 23. シリアル ペリフェラル インターフェイス (SPI)
セクション 31. DMA コントローラ
セクション 42. 拡張 PLL を備えたオシレータ
セクション 51. ハイスピード USB On-The-Go (OTG)

AN1422

PIC32 を使った高品位オーディオ アプリケーション

AN2582

Creating a USB Audio Device on a PIC32 MCU Using MPLAB Harmony