RaspberryPi Pico USB-DDC化 I2SとMSBファースト後詰め出力対応
RaspberryPi Picoには、PIOというステートマシンを使ったIOポート制御機能があり、マイコン側から見るとハードウェアとして機能するものが搭載されています。PIOは2個。その中にステートマシンが4個づつ入っています。
公式のusb_sound_cardというソフトでもI2S出力にこのPIOを使用しています。
そこで、PIOを改造して、I2SとMSBファースト後詰め(右詰め=Right-Justified)とを一緒に出してしまおうというのが今回の記事の内容です。ソースと、コンパイル済みデータも載せますので興味のある方は読んでみてください。
3線シリアルのフォーマットの再確認
PCM1795というDAC-ICのデータシートからの抜粋です。
MSBファースト後詰(Right-Justified)めは、このようにLRCKの切替りタイミングにLSBが合うようになっています。BCKが32fsの場合、16bitデータで埋まり空きデータがありません。
< 16bitデータ、BCK=32fsの場合 >
LRCKの直後がMSBになるところがポイントです。
これに対してI2Sフォーマットは以下のようになっています。
LRCKの極性が逆という部分はおいておいて、LRCKのあと1bitおいてからMSBが来るのがポイントです。
つまり、16bit固定という前提条件であれば、I2SとMSBファースト後詰め(右詰め=Right-Justified)とでLRCKの位置をズラすだけで同居できます。
図にするとこうなります。
それを実現したのが、一番上のオシロ波形です。I2S用のLRCK(ワードクロック)とMSBファースト後詰めのLRCKが別ポートに出力されています。
ソース改変
audio_i2s.pioというファイルがpioを制御するソースコードです。以下のようにしました。
.program audio_i2s
.side_set 3
; /---- LRCK(MSB fast)
; |/--- WCLK(i2s)
; ||/-- BCLK
bitloop1: ; |||
out pins, 1 side 0b010
jmp x-- bitloop1 side 0b011
out pins, 1 side 0b000
public entry_point:
set x, 14 side 0b001
bitloop0:
out pins, 1 side 0b100
jmp x-- bitloop0 side 0b101
out pins, 1 side 0b110
;public entry_point: 元のエントリポイントの位置
set x, 14 side 0b111
% c-sdk {
static inline void audio_i2s_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base) {
pio_sm_config sm_config = audio_i2s_program_get_default_config(offset);
sm_config_set_out_pins(&sm_config, data_pin, 1);
sm_config_set_sideset_pins(&sm_config, clock_pin_base);
sm_config_set_out_shift(&sm_config, false, true, 32);
pio_sm_init(pio, sm, offset, &sm_config);
uint pin_mask = (1u << data_pin) | (7u << clock_pin_base);
pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask);
pio_sm_set_pins(pio, sm, 0); // clear pins
pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_offset_entry_point));
}
%}
という感じで、pioソースを弄りました。side_setを2から3へと増やして別タイミングのLRCKを追加しているだけです。entry_pointの位置を動かしているのは後述します。最初は元の位置で検証しました。
audio_i2s.c というソースでもside_setを3に増やす対応が必要でした。
const audio_format_t *audio_i2s_setup(const audio_format_t *intended_audio_format,
const audio_i2s_config_t *config) {
uint func = GPIO_FUNC_PIOx;
gpio_set_function(config->data_pin, func);
gpio_set_function(config->clock_pin_base, func);
gpio_set_function(config->clock_pin_base + 1, func);
gpio_set_function(config->clock_pin_base + 2, func);
ソースコードの途中の抜粋です。 赤い行を追加しています。 IOの初期設定です。
これで無事にI2S信号にプラスして別タイミングのLRCKが出力されるようになりました。
アナログ出力波形の確認
ラズパイ公式のソースコードに不具合が残ったままでした。音が出たのでOKにしたのでしょうね。 LRのデータがズレています。
1kHz方形波。 黄色がLch。水色がRch。明らかにLchが遅れています。
X-Yリサージュ波形。位相が揃っていると斜め一直線になるハズですが・・・
webで検索したところ、I2SでLRタイミングずれに気が付いている人もいらっしゃいました。
Raspberry Pi PicoのPIOプログラミング(I2Sインターフェースの実装)
DMAでHalfWordスワップを起こすんだとか。。 よくある32bitマイコンではDMA転送時にバイトスワップとかハーフワードスワップさせるオプションがあったりするのですが、picoの公式資料には何も書いていません。32bitマイコンでDMA転送するときHalfWordスワップが標準とは考えにくいです。16bitバスで転送しているならありえますが。。。
バイトスワップさせる命令 channel_config_set_bswap()
というのがあったので、試したらデータが壊れました(笑) やりたいのはHalfWordのswapですからね。当然の結果。
※32bitマイコンなので、Word=32bit。HalfWord=16bit。
LR位相ズレの原因
一般のPCMデータはLchが先頭で送られることになっています。そのすぐ後ろのRchのデータと合わせて1サンプリング分のデータです。
以下の図は左側がデータの先頭です。1段目は通常のPCMデータの配列。
2段目のようにデータのLRスワップが発生すると、LRが逆になって再生されます。この時点では、ただ左右逆なだけです。
ああ、左右逆だぁっと単純にLRCK信号を反転させると、3段目のようにRchデータから先に出力され、1サンプリングのペアがズレてしまいLR位相ズレを引き起こします。
Lの信号はLchから。
Rの信号はRchから出てる。 よしよし、OK!
と判断を誤ったのがラズパイ公式のソースコードです。惜しい。
pioソースを見ても、エントリポイントがデータの最初の部分なのですが、ちょうどI2SのLRCKがHになった所に設定されていました。I2SのLRCK=HはRchを意味します。
Lchから始まるI2Sデータを正常に送るのであればLRCKがLになった所にエントリポイントを置かなくれはいけません。
LR位相ズレ対策
ソースコード上、どこでLRが入れ替わっているのか探したのですが、全く見つかりません。ホントにDMA転送時にHalfWordスワップが起きているのかもしれませんね。
数時間、悩んだあげく、
usb_sound_card.c でusbからパケットを受信している部分でLRを入れ換えることにしました。
static void _as_audio_packet(struct usb_endpoint *ep) 関数内の
for (int i = 0; i < audio_buffer->sample_count * 2; i++) {
out[i] = (int16_t) ((in[i] * vol_mul) >> 15u);
}
という部分を
for (int i = 0; i < audio_buffer->sample_count * 2; i++) {
out[i+1] = (int16_t) ((in[i] * vol_mul) >> 15u);
i++;
out[i-1] = (int16_t) ((in[i] * vol_mul) >> 15u);
}
という感じに16bitデータを入れこにします。 ここ、ボリュームを演算している部分です。
それと、pioのentry_pointの位置を移動してLchからデータが出力されるように変更しました。
位相ズレがなくなりました。
リサージュもばっちり一直線です。
良かった良かった。
R2R-DACの動作確認
というわけで、本題のMSBファースト後詰めR2R-DACでの動作です。
とりあえず実験なのでポストフィルタなしで素の出力を観測してみます。
さすがのNOS-DAC。
このガタガタ具合はまさにNOS-DACですね。
インパルスも完ぺきなまでに尖ってます。
R2R-DACというかマルチビットDACの特徴はデルタシグマ方式と違って、その瞬間、瞬間のデータをデコードすることで、前回のデータを一切引きずらない点にあると思います。
ただし、オシロで観測していると、5V電源の揺れをそのまま出力に出してしまうようですので、DAC周りの電源をしっかり設計してあげる必要があると感じました。
バイナリデータ&ソースコード
最後にXIAO RP2040のデータを置いておきます。
ダウンロード - usb_sound_card_kai.uf2
BOOTスイッチを押しながらUSBを接続するとフォルダが見えるので、そこに放り込んでください。そうすると勝手にリブートされてUSB-DACとして認識します。
改造したソースコードはこちら。
展開して、もとファイルを上書きしてからコンパイルしてください。入っているフォルダはそれぞれ別になってますのでお気を付けください。
ピンアサイン
上記バイナリをXIAO RP2040で使用したときのピンアサインは以下のようになっています。
使ったことないけど、Tiny2040でも同様に出力されると思います。
もちろん、すべてのIOがアサインされている公式のpico基板でも出力されます。GPIO 26~29です。
picoプログラムのコンパイル
ubuntuにてコンパイルする方法を備忘録的に軽く書いておきます。
・VirtualBOXにubuntuをインストール
この時点では20.04LTS日本語Remixを使いました。
・pico-SDKなどのインストール
公式サイトの手順をみてインストール。
2.1 Get the SDK and example
$ cd
$ mkdir pico
$ cd pico
$ git clone https://github.com/raspberrypi/pico-sdk.git --branch master
$ cd pico-sdk
$ git submodule update --init
$ cd ..
$ git clone https://github.com/raspberrypi/pico-extras.git --branch master
$ git clone https://github.com/raspberrypi/pico-playground.git --branch master
2.2. Install the Toolchain
$ sudo apt update
$ sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential
2.3. Updating the SDK
$ cd
$ cd pico/pico-sdk
$ git pull
$ git submodule update
3.1. Building cmake
$ cd
$ cd pico/pico-playground
$ mkdir build
$ cd build
$ export PICO_SDK_PATH=../../pico-sdk
$ cmake ..
3.2. Build
$ cd
$ cd pico/pico-playground/build/apps/usb_sound_card
$ make clean (一度目なら省略)
$ make
これで usb_sound_card.uf2 がコンパイルできます。
・pico実行ファイルを実機に転送
picoのBOOTスイッチを押しながらRESETすると見えるフォルダに.uf2ファイルを入れるとpicoは再起動します。
今後の展開
クロック設定している箇所とUSBのディスクリプタ設定をどうにかすれば、16bitのままでしたらpioそのままで96kHzや192kHzまで対応可能な可能性が高いです。この価格のマイコン基板でも結構遊べるUSB-DDCになりそうな気配を感じますね。
もし興味がありましたら、皆さんも遊んでみてはいかがでしょうか?
私よりも先にハイレゾ出力できるようになった方がいらっしゃったら、ぜひ、ソース公開などお願いします。
単なるDDC基板、もしくは秋月R2R-DACを乗せたUSB-DAC基板を作ってみるのも楽しそうです。
« RaspberryPi Pico USB-DDC化? I2S出力調査 | トップページ | Volumio/MoodAudioに秋月電子のI2CタイプOLEDを接続して曲名などを表示するPython3版 »
「PCオーディオ」カテゴリの記事
- RaspberryPi Pico USB-DDC化 I2SとMSBファースト後詰め出力対応(2022.03.21)
- RaspberryPi Pico USB-DDC化? I2S出力調査(2022.03.20)
- efuさんのWaveSpectraが公開停止していた(2022.03.09)
- ラズパイをUSB-DACとしてスマホに認識させる方法 gadget mode(USB Audio Class 2.0)(2020.10.16)
- PCに組み込むヘッドホンアンプ Sound RABBIT & 900万PVありがとう企画(2020.10.10)
「Raspberry Pi pico」カテゴリの記事
- RPiZeroRP2040 ラズパイZERO形状USB-DDC基板(2024.10.20)
- RPiZeroRP2040 USB-DDC基板試作(2024.09.01)
- ラズパイPico USB-DDC Feedbackを機能するように実装(3)(2024.08.24)
- ラズパイPico USB-DDC Feedbackを機能するように実装(2)(2024.08.23)
- ラズパイPico USB-DDC Feedbackを機能するように実装(1)(2024.08.22)
コメント
« RaspberryPi Pico USB-DDC化? I2S出力調査 | トップページ | Volumio/MoodAudioに秋月電子のI2CタイプOLEDを接続して曲名などを表示するPython3版 »
さすが、たかじんさん。現象の確認から対策までが速い。👍
プログラミングからは足を洗った(つもり?)ので、RaspberryPI系オーディオに手を出すことはありませんな。😄
投稿: 三毛にゃんジェロ | 2022年3月22日 (火) 14時37分
三毛にゃんジェロさん
picoは遊びですが、秋月のR2R-DACを鳴らしたいというのが主な目的です。。。
なにせマルチビットDACですからね。データシートを見ると15bit相当の分解能が出ていそうな雰囲気です。
ざっと見た感じではグリッジノイズも皆無なので、使いやすそうです。
投稿: たかじん | 2022年3月22日 (火) 23時06分