Raspberry Pi Picoで32bit I2S DACを使う (PCM5102)

2021年3月1日月曜日

I2S DAC Raspberry Pi Pico

t f B! P L

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との接続は以下の通りです。

Pico Pin #Pin NamePCM5102 Board
31GP26BCK
32GP27LRCK
34GP28DIN
363V3(OUT)VIN
38GNDGND

またPCM5102ボードには設定ランドがありますが以下の通りにします。私が入手したボードの場合はSCKのブリッジ以外は初めからこの設定になっていました。

項目設定備考
SCKbridgeSCKは内部で生成する
H1L (FLT)L
H2L (DEMP)L
H3L (XSMT)H
H4L (FMT)LL: I2S Format, H: Left-justified

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信号を生成する
Audio API, I2S Audioに関しては32bitオーディオ信号を扱うためのAPIが存在していなかったため、とりあえずローカルに移動させて32bit対応しました。 最終的にはAudio APIでは8bit, 16bit, 32bitのMono/Stereo信号を扱い、これとは別途にI2S DACに対して16bit, 24bit, 32bit Stereoを指定できるようにして、この間の変換を自由に行えるようにできれば非常に使いやすくなりますが、とりあえずは16bit Stereoオーディオ信号から16bit Stereo I2Sへ、32bit Stereoオーディオ信号から32bit Stereo I2Sへの二つを対応しました。(my_pico_audio, my_pico_audio_i2sフォルダにて対応)

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) 続編

自己紹介

自分の写真
電子工作&プログラミング、オーディオ・音楽

注目の投稿

Raspberry Pi Pico Wで電波時計を合わせる (JJY標準電波エミュレータ)

Raspberry Pi Pico Wのアプリケーションとして 最少の周辺部品で電波時計むけJJYエミュレータ(時刻合わせ用)を製作しました。 ※2023年6月6日: ソースコード修正の内容を反映させました。 時刻合わせ風景 概要 電波時計は電波が届くところで使...

QooQ