DUEがやって来て、最初から気になっていた内蔵DA変換器を使ってみたいと思います。アプリケーションはWAVファイルの再生です。
UNOやSTM32でも、自作したDA変換器や外付けのDA変換器モジュール(MCP4725)を使って、SDカードに保存したWAVファイルを再生してみた記事があります(↓)。
「#7-9 WAVファイルを再生する」
「#7-10 WAVファイルを再生する(回収編)」
DUEのMCUチップSAM3X8Eには、2チャンネルのDA変換器が搭載されているので、2チャンネル(ステレオ)のサウンド出力が可能となります。
SAM3X8Eのデータシートを確認して見ても、DA変換器の出力端子(DAC0,DAC1)に流すことのできる電流値規定が見つかりませんでした。さすがにある程度の電流負荷には耐えるのだろうけど、スペックが見つからなかったので、ボルテージフォロア(オペアンプ、LM358)で受けて出力しておくことにしました(下図)。
※SDカードI/Fについてはこちらをご参照ください。
SDカードI/FはSPI端子を直結、ボルテージフォロアの出力段にはコンデンサでDCカット後、ステレオミニジャックを接続してあります。イヤホンの入力は、VP-P=0.5〜1.0[V]程度の信号らしいので、DUEのDA変換器出力VP-P=3.3Vでは、ちょっと大きすぎます。ソフトウェア(デジタルボリューム)で振幅を絞って使おうと思います。
なお、LM358のスペックによると10mAくらいの電流駆動能力があるみたいですので、ミニジャックに32Ωのステレオイヤホンを接続した場合の出力は、最大で32Ω×(10mA)2=3.2mW程度です。LM358にはVcc=5Vの電源を供給して、サウンド出力が3.3Vフルスイングする様に作っておきます。
※LM358の出力VOH=Vcc−(1〜1.5[V])なので、Vcc=3.3Vだと、DA変換器出力が2Vそこそこまでしか上がりません。
DUE(SAM3X8E)のDA変換器は、12ビットで2チャンネルを搭載しています。DA変換クロックはfcpu/2となっていて、何らかのクロック同期型アルゴリズムが使われているようです(詳細はデータシートに記載なし)。変換時間はDA変換クロックの25クロックとなっているので、最速の出力レート=1/84MHz×2×25クロック≒595nsです。……サウンド出力に対しては、十分高速です。
WAVファイルの場合、テータサイズが8ビットの物と16ビットの物があり、16ビットWAVファイルに対しては12ビット出力と、少し足りません。オーディオ向けの高価なDA変換器であれば、もっと解像度の高い(〜16ビット)物もあるのかもしれませんが、Arduinoで遊ぶ程度であれば12ビットで十分でしょう。
※そもそも今回は、イヤホンの入力レベルに合わせるためデジタルボリュームで振幅を絞って使いますので、振幅方向の解像度は相当低下します。それを嫌う場合には、ボルテージフォロアの次段に反転増幅回路(増幅率<<1)を形成して、適切な信号レベルまで落として使ってください。
SAM3X8E搭載のDA変換器を使うに当たり、「#11-10 DUEでLogiscopeを動かす」でやった様に、DA変換器のレジスタ設定を確認しておこうと思います。
DUEにLogiscopeをコンパイル&書き込んで、WindowsでスクリプトLogiscope.olvを実行します。
※Logiscope.olvの実行には、拙作Olive+のインストールが必要です。ダウンロードコーナなどからダウンロードして、インストールを済ませてください。
実験サンプル:【 】内はLogiscopeからの送信データ
【dump4,400c8000,64】
long[ 0x400c8000 ]= 0x00000000
long[ 0x400c8004 ]= 0x00000000
: (省略)
OK: dump4,400c8000,64
まず、起動直後のDA変換器レジスタを読み出しておきます。一旦、テキストエディタやExcel等にコピー&ペーストしておいてください。
【ana,66,128】
OK: analogWrite(66, 128)
続いて、DA変換器のチャンネル0(DAC0)にアナログ値128LSBを出力するコマンドを打ちました。「#11-10 DUEでLogiscopeを動かす」で試してみた様に、DAC0端子はデジタル端子のD66に割り当たっているので、【ana】コマンドのピン番号には66を渡しています。
【dump4,400c8000,64】
long[ 0x400c8000 ]= 0x00000000
long[ 0x400c8004 ]= 0x10000800
: (省略)
OK: dump4,400c8000,64
先の【ana】コマンド実行で、LogiscopeによってanalogWrite()関数が実行されましたので、この時点でもう一度DA変換器レジスタ値を読み出すと、最初に読み出したレジスタ値と比較して、変化したレジスタの値を知ることが出来ます。設定が必要そうなレジスタは以下の通りでした。
レジスタ | アドレス | 初期値 | ⇒ | 設定後 | 解釈 |
DACC_MR | 0x400c8004 | 0x00000000 | 0x10000800 |
STARTUP=1024×(MCK/2), REFRESH=1024×8×(MCK/2), TRGEN=0(Free running mode) |
|
DACC_CHSR | 0x400c8018 | 0x00000000 | 0x00000001 | CH0=1 (enable) | |
DACC_CDR | 0x400c8020 | 0x00000000 | 0x00000000 | data register (FIFO entry) | |
DACC_IMR | 0x400c802c | 0x00000000 | 0x00000000 | no interrupts enabled | |
DAC_ACR | 0x400c8094 | 0x000001aa | 0x0000010a |
IBCTLDACCORE=0x1, IBCTLCH1=0x2, IBCTLCH0=0x2 |
DACC_ACRレジスタの設定値は意味不明なのですが、データシート記載の通りなので、analogWrite()関数が設定した値で良いでしょう。
DACC_MRレジスタには、他にもいくつか設定項目(ビット)が合って、今回の使い方では以下のビットを有効にしておこうと思います。
SAM3X8EのDA変換器は、何らかのクロック同期型アルゴリズムを使っており、DA変換器コアは1個しか搭載していないため、2チャンネル(DAC0,DAC1)の出力を同時に変化させることはできません。DA変換器には、データレジスタに4段分のFIFO(First In First Out)バッファが搭載されており、次々とデータを書き込んでおきさえすれば、各変換(DA変換クロックで25クロック時間を要する)後、次々とFIFOからデータを取り出して変換してくれます。
上に示した追加設定は、FIFOを使って2チャンネル(ステレオ)のDA変換を効率良く行うためのものとなります。
実際にDA変換器を使うためには、DA変換器レジスタだけでなく、クロック供給やポートの設定などが必要となります。そこで、一旦、analogWrite()関数にこれらの基本的設定をやらせておいて、追加設定だけを記述することにしました(下コード)。
// dac
analogWriteResolution (12);
analogWrite (ana_DACL, dac_HALF);
analogWrite (ana_DACR, dac_HALF);
REG_DACC_MR |= DACC_MR_TAG | DACC_MR_WORD;
DA変換器のデータレジスタDACC_CDRにデータを書き込むと、自動的にFIFOバッファにデータが積まれます。前節の様に、DACC_MR_WORD=1を設定した場合、DACC_CDRレジスタへの32ビットの書き込み値は、下位16ビットが先にFIFOに積まれ、後から上位16ビットが積まれます。FIFOなので、積まれた順番がそのまま変換される順番となります。
FIFOは4段搭載されているので、データを4個書き込むとバッファがいっぱいになってしまい、それ以上書き込むとデータが破壊される(corrupt)と記載されています。FIFOがデータを受け取り可能な状態になると、DACC_ISRレジスタのTXRDYフラグが1になるので、このフラグをポーリングして、データレジスタへの書き込みタイミングを計ります(↓イメージ)。
// polling TXRDY (FIFO ready to transfer new data)
while (!(REG_DACC_ISR&DACC_ISR_TXRDY))
;
// write data to 12 bit dac (integrated)
// data: [31:28]=chn#2, [27:16]=dat#2, [15:12]=chn#1, [11:0]=dat#1
REG_DACC_CDR=(0x01<<(16 + 12)) | (dat2<<16) | dat;
今回の回路では、DAC0を左チャンネル、DAC1を右チャンネルに設定してあるので、上のコードでは左チャンネルのデータdatをDAC0、右チャンネルのデータdat2をDAC1への変換データとして書き込んでいます。
WAVファイルの再生手順は以下の通りです。
以上、1)〜3)を再生終了するまで繰り返します。従って、出力レートはベストエフォートです。SDカードへのアクセス速度やMCUの処理速度などに応じて、その環境での最速出力レートで実行されます。
SDカードからWAVファイルを読み込んで、DA変換器へ出力するプログラムは、「#7-10 WAVファイルを再生する(回収編)」で作成したplay_wav.inoが有りますので、これをベースにDUE(SAM3X8E)用の場合分け(#ifdef)を追加したいと思います。
改修個所のポイントは前節までに解説済みですので、以下に完成したプログラムをリンクしておきます。
完成プログラム:play_wav.ino
いつもの様にオリジナルを差し替えてあるので、どこからダウンロードしても最新のコードとなります。
DUEターゲットでコンパイル&書き込むと、シリアルモニタからコマンドを受け付けるようになっています。コマンドの仕様についてはこちらをご参照ください。
このプログラム(play_wav.ino)は、MCUの処理速度に依存して最大パフォーマンスを出せるように設計してあるので、各MCUで聞こえ方(音質)が異なります。
UNOでの実験を読み返していただくと、16ビットデータやステレオデータの再生(ステレオ→モノラル変換して再生)にも対応してあったのですが、データレートが大きくなるWAVファイルフォーマットでは、まともな音として聞こえない状態でした。UNOを使った16ビットステレオデータの再生だと出力レートが952Hzであり、952/2=476Hz以上の周波数は再生音声に含まれませんので当然です。
UNO @16MHzに対して、DUE @84MHzですので、処理速度は格段に速くなっています。どの程度の音質で聞こえるか、実験で確かめてみましょう。
以下、実験中の様子です。
ステレオミニジャックにステレオイヤホンを繋ぐか、あるいはアンプを内蔵したステレオスピーカなどに接続して準備します。また、以下のサンプルデータをSDカードに書き込んでおいてください。
8ビットステレオ8kHzのWAVデータです。「わぶ」(左右チャンネル) → 「ひだり」(左チャンネルのみ) → 「みぎ」(右チャンネルのみ)と言う音声が聞こえるはずです。DUEの2チャンネルDA変換器から、ステレオデータを再生可能なことを確かめてください。
※WAVファイル(a12a_clr.wav)は、拙作sPeachを使って合成した音声を、同じくsPeachのミキシング機能で左・右にパンして生成しました。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pwav,a12a_clr.wav】
DBG: fmt=1, 2, 8000, 16000, 2, 8
OK: play_wav: 1293012[us]/71760[smpl]=18[us/smpl](55498[Hz])
まずは、ステレオイヤホンで聞いてみて、ステレオ音声が左右のイヤホンから聞こえましたか?
出力レートは、約55.5kHzでした。55.5/2[kHz]≒27.8kHzの音声まで再現可能ですので、可聴域20〜20k[Hz]に対して十分なパフォーマンスがあり、かつ、WAVファイルに記録されたサンプリングレート8kHzを超えていますので、もはや、このデータ(a12a_clr.wav)では再生音質の評価はできません。
各自、もっとサンプリングレートの高いWAVファイルを用意して評価して見てください。
非圧縮WAVファイルはファイルサイズが大きいため、本ページにはリンクしませんが、筆者はMP3等の音楽データを様々なフォーマットでWAVファイルに変換して、評価してみました(↓)。
WAVファイル | 出力レート | 判定 | |||
ビット | チャンネル | サンプリングレート | [μs/sample] | [kHz] | |
8 | 1 | 48kHz | 17 | 58.0 | ○ |
8 | 2 | 48kHz | 27 | 36.8 | △ |
16 | 1 | 48kHz | 28 | 35.6 | △ |
16 | 2 | 48kHz | 86 | 11.7 | × |
8 | 2 | 22.05kHz | 21 | 47.2 | ○ |
8 | 2 | 11.025kHz | 19 | 51.6 | ○ |
16 | 2 | 22.05kHz | 38 | 26.6 | × |
※長いWAVファイルを再生開始した場合、途中で停止したくなったら、シリアルモニタから空行で良いのでデータを送信してください。中断することができます。
以下雑感です。
前節の実験結果を受けて、追加の実験が必要だと判断しました。
何だか、出力レートの数字に対して音質が良くない。うまく表現できないのですが、ざらついているというか、クリア(綺麗)な音で聞こえません。それに、サンプリングレート8kHzのWAVファイルと48kHzのWAVファイルでは、出力レートの数値自体も低下しています。
STM32でLogiscopeを起動して、DUEのDAC0端子とSTM32のA0端子をジャンパ線で接続しておきます。両機のGND同士もジャンパ線で接続してください。
実験サンプル:【 】内はDUEシリアルモニタからの送信データ
【dvol,3】
OK: d-volume *3.00
↑波形の振幅が認識できるように、デジタルボリュームを少し上げておきます。出力が大きくなるので、イヤホン等には出力しないようにしてください。
実験サンプル:【 】内はDUEシリアルモニタからの送信データ
【pwav,test.wav】
少し長めのWAVファイルを再生したら、再生中に以下のコマンドをSTM32+Logiscopeから送信します。
実験サンプル:【 】内はLogiscopeからの送信データ
【osc,0,0,4,0】
こりゃダメだ。定期的に、出力信号が変化せず平坦になっている部分があります。平坦部分の時間は約1.7msで、約5.4ms毎に繰り返しているようです。データレートが低いWAVファイルを再生したところ、平坦部分の長さは約1.7msで変わりませんでしたが、頻度が減少しました(間隔が伸びた)。
このことから、平坦部分はSDカードからのデータ読み出し時間だと考えられます。SDカードにアクセスする、fin.read()メソッド等の処理でブロックされている間、DA変換器に新たなデータを書き込むことが出来ないため、上図の平坦な部分が出来てしまうのでしょう。
8ビットステレオ、サンプリングレート8kHzのWAVファイルと、同、サンプリングレート48kHzのWAVファイルでは、データレートが×6倍に上がったため、SDカードへのアクセス頻度が増加して出力レートが下がったのだと判りました。
「サンプリングレートが高い方が、高音質で聞こえるでしょ?」と、サンプリングレート48kHzでWAV変換してしまったことが、返ってデータレートを上げてしまい、SDカードへのアクセス頻度が上がったことで音質を低下させてしまったようです。
なるべくクリア(綺麗)な音でWAVファイルを再生したい場合、WAVファイルのデータレートはなるべく低くする方が良い様です。DUEの場合、処理速度の実力的には、8ビットステレオ11.025〜22.05[kHz]辺りが推奨です。
それで平坦部分の頻度は減少してだいぶ音質は向上しますが、平坦部分は消えていないので、この平坦部分で音質を低下させることに変わりはありません。平坦部分をなくすためには、別な機構で制御する必要がありますので、それはまた別なテーマとします。
現状の制御のままでも、上手くWAVファイルのサンプリングレートを調整しておくと、ステレオのWAVファイルをそこそこの音で再生することが出来ます。折角のステレオ音声なので、やっぱり音楽を再生したいですよね🎧?ノイズ対策は必要ですけど……。