Raspberry Pi Picoで32bit I2S DAC PCM5102を32bitモードで動作させてみました。
I2S 32bit対応 テスト風景 |
もとになるプロジェクト (sine_wave)
pico-playgroundにaudio/sine_waveというプロジェクトがあり、これをベースに変更を加えることにします。このプロジェクトでは以下の処理を行っています。- 出力はSPDIF, PWM, I2Sのいずれかが選択可能で、サンプリング周波数はSPDIF時44.1KHz、PWM, I2S出力時は24KHz (この記事では以下I2Sのみに注目)
- モノラル16bitのサイン波を生成
- Audio APIに対して16bitのモノラル信号を送り、16bitステレオ信号のバッファに変換
- ステレオ信号のバッファをDMAにてPIOに転送
- PIOにより3線 I2Sフォーマット信号を生成して16bit ステレオ信号をDACに出力
- シリアルからの文字入力で周波数と音量の調整が可能
ピン設定
このプロジェクトそのままの状態でもPCM5102を接続すると16bitステレオモードでI2Sが駆動されて音が出ます。Raspberry Pi Picoとの接続は以下の通りです。
またPCM5102ボードには設定ランドがありますが以下の通りにします。私が入手したボードの場合はSCKのブリッジ以外は初めからこの設定になっていました。
SCKのハンダブリッジはPCM5102ボードの表面、HxL設定は裏面で行います。
32bit Stereo対応サンプルプロジェクト (rasp_pi_pico_sine_wave_i2s_32bit)
https://github.com/elehobica/pico_sine_wave_i2s_32b/tree/v0.1.0に32bit Stereo対応したプロジェクトを置きました。変更点は以下の通りです。
- I2Sのサンプリング周波数を44.1KHzに変更
- Audio API, I2S Audio APIを修正し、32bitステレオオーディオ信号に対応
- 32bitのサイン波を生成。左右識別のため周波数を別々にコントロール (Left: [ ], Right { })
- PIOにより32bit ステレオのI2Sフォーマット信号を生成 (16bit ステレオ送出モードも残す)
- PIOクロック周波数の最適化によりジッターの少ないBCK信号、LRCK信号を生成する
PIOのI2S 32bit対応
Raspberry Pi Picoの目玉の機能の一つであるPIOは、多種多様な入出力の仕様をある程度の速度性能を確保しながらプログラマブルに変更できるので、今回のような仕様の小修正にも柔軟に対応できるのが利点です。PIOはPIOASMという独自のアセンブリ言語で記述しますが、IO制御に特化しているため命令数などの仕様は比較的限定されており、慣れてこれば思った通りに使いこなせそうです。pico_audio_i2s内のaudio_i2s.pioを修正することで32bit対応しました。具体的には、片チャネルあたり16回に決め打ちになっていたBCKのトグル数をISRレジスタを介して値を渡すことにより可変としました。PIOにはX, Yのスクラッチレジスタがありプログラム内で使用する以外に外部から値を与えることができるのですが、今回のI2Sのように出力オンリーでプログラムを組む場合はISR (Input Shift Register)を使うほうが、スクラッチレジスタを使わずに確保しておけるので良いようです。
値をISRに与える部分のコードはRP2040 Datasheetの3.6.8章 PWMの部分を参考にしました。
audio_i2s.pio抜粋
.program audio_i2s .side_set 2 ; I2Sフォーマット (Left-Justifiedではなく) に合わせるためにLRCKの極性を逆に修正 ; ビット数を設定するためのレジスタとしてISRを使用 ; /--- LRCLK ; |/-- BCLK bitloop1: ; || out pins, 1 side 0b00 jmp x-- bitloop1 side 0b01 out pins, 1 side 0b10 mov x, isr side 0b11 ; ISRからビットシフト数を取り込む bitloop0: out pins, 1 side 0b10 jmp x-- bitloop0 side 0b11 out pins, 1 side 0b00 public entry_point: mov x, isr side 0b01 ; ISRからビットシフト数を取り込む % c-sdk { static inline void audio_i2s_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint res_bits) { 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) | (3u << clock_pin_base); pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); pio_sm_set_pins(pio, sm, 0); // clear pins // オーディオデータのビット数からISRにビットシフト数を計算して設定 (※) // set resolution to ISR (use as config value) pio_sm_set_enabled(pio, sm, false); pio_sm_put_blocking(pio, sm, res_bits - 2); // res_bits should be 32, 16 or 8 (※) pio_sm_exec(pio, sm, pio_encode_pull(false, false)); pio_sm_exec(pio, sm, pio_encode_out(pio_isr, 32)); // inputに与えた値をISRに32bitすべて取り込む(※) pio_sm_set_enabled(pio, sm, true); pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_offset_entry_point)); }
ジッターの少ないBCK信号、LRCK信号の生成
デフォルトの設定では、PIOの周波数はシステムクロックの周波数である125.0MHzから分周して生成されます。しかし125.0MHzからの整数のみの分周では限界があるため、Fractional clock dividerによりターゲットの周波数に合わせこむことを可能としています。通常のシリアルI/Fやその他のIO制御の場合は、この周波数合わせこみは非常に役に立つ機能となるはずですが、一方でI2Sのクロック生成にPIOを使用した場合はオーディオ的には嫌われるクロックジッターを多く含む信号となるため、Fractional clock dividerを使用せずに固定の周波数で出力されることが望ましいです。(実際に出力クロックにFractional clock divider由来と考えられるジッターが含まれることを確認済みです。)サンプリング周波数を44.1KHzとした場合、32bit 2ch DACに必要とされるBCKは、44.1 KHz x 32bit x 2 = 2.8224 MHz となりますが、PIOではBCKのH, L周期を生成する必要があるため、さらに倍の5.644 MHzのクロック入力が必要です。しかしシステムクロック125MHzの状態でFractional clock dividerを使用せず整数の分周のみで44.1KHzに近い周波数を得ることは不可能です。(125.0MHz / 22 = 5.6818 MHz → サンプリング周波数 44.389 KHz 相当)
クロックが96.0MHzであれば、96.0MHz / 17 = 5.647MHz → サンプリング周波数 44.118 KHz 相当と比較的良好な周波数が得られますが、sys_pllの周波数を下げて全体のパフォーマンス低下を招きたくないため、苦肉の策としてusb_pllの周波数を96.0MHzと設定してここからUSB用の周波数48.0MHzと PIO用の周波数96.0MHzを得るように変更しました。
sine_wave.c抜粋
// pll_usbで96MHzを生成してPIO向けに使用 (USB向けに48MHz供給) stdio_init_all(); // Set PLL_USB 96MHz pll_init(pll_usb, 1, 1536 * MHZ, 4, 4); clock_configure(clk_usb, 0, CLOCKS_CLK_USB_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 96 * MHZ, 48 * MHZ); // Change clk_sys to be 96MHz. clock_configure(clk_sys, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX, CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_USB, 96 * MHZ, 96 * MHZ); // CLK peri is clocked from clk_sys so need to change clk_peri's freq clock_configure(clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, 96 * MHZ, 96 * MHZ); // Reinit uart now that clk_peri has changed stdio_init_all();
audio_i2s.c抜粋
#if 0 // PIO_CLK_DIV_FRAC (Fractional clock dividerを使用) float pio_freq = (float) system_clock_frequency * 256 / divider; // frac printf("System clock at %u Hz, I2S clock divider %d/256: PIO freq %7.4f Hz\n", (uint) system_clock_frequency, (uint) divider, pio_freq); pio_sm_set_clkdiv_int_frac(audio_pio, shared_state.pio_sm, divider >> 8u, divider & 0xffu); // This scheme includes clock Jitter #else // Fractional clock dividerを使用しない (こちらを使用) divider >>= 8u; float pio_freq = (float) system_clock_frequency / divider; // no frac printf("System clock at %u Hz, I2S clock divider %d: PIO freq %7.4f Hz\n", (uint) system_clock_frequency, (uint) divider, pio_freq); pio_sm_set_clkdiv(audio_pio, shared_state.pio_sm, divider); // No Jitter. but clock freq accuracy depends on PIO source clock freq #endif
ロジアナ波形
最後に実際にロジアナで取得したI2Sの信号波形を載せておきます。I2S 32bit対応 ロジアナ波形 |
続編はこちらから
Raspberry Pi Picoで32bit I2S DACを使う (PCM5102) 続編
0 件のコメント:
コメントを投稿