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

2021年7月3日土曜日

I2S DAC Raspberry Pi Pico

t f B! P L

以前Raspberry Pi Picoで32bit I2S DACを使う (PCM5102)で紹介した内容の続編です。
最新版のコードは以下にあります。
https://github.com/elehobica/pico_sine_wave_i2s_32b

I2S 32bit利用 アプリケーション作成風景

I2S DACからの音が途切れる

問題の波形

以前紹介した時点のコードのまま、少しずつ他の処理を追加して負荷を増やしていくと時々I2S DACからの音が途切れる現象が発生するようになりました。波形で見ると以下のような感じでBCKが断絶してその期間の分だけLRCKも間延びしている状態です。
音が途切れる部分のI2S波形
聴感上はちょうどレコードのスクラッチノイズのようなプチっという音が入ります。PCM5102の場合はI2Sへのデータ入力(DIN)を完全にゼロにした状態にすると、より顕著にわかりやすくなります。PCM5102では正常出力状態でデータがゼロの場合はZero Data DetectによりAnalog Muteがかかり、音が途切れる瞬間にミュートが外れるようなのでポコッというより大きな音がして容易に判別可能になります。

原因はPIOのFIFOアンダーフロー

この現象を解消するのにかなり苦労しました。DMA転送の割込の優先度を上げてみたり、LCD表示などのその他の処理のタイミングを変えてみたりしましたが、ほとんど効果はありませんでした。もちろん、LCD表示などのその他の処理自体をなくせば、この現象は収まるのですが、それでは意味がありません。ほかにもDMA割込とAudioデータの生成を別コアでおこなってみたりしましたが改善が見られませんでした。
PIOの仕様をもう一度見直してみると、TX, RX側に搭載されているFIFOは各4Word(32bit幅)ですが、片方向8Word(32bit幅)にも設定可となっていることに気づきました。元のコードではRX 4Word, TX 4Wordが割り当てられており、I2S I/FではTX方向のみ使用するので、TXに8Wordを割り当てるようにすると音の途切れは発生しなくなりました。具体的にはmy_pico_audio_i2s/audio_i2s.pioのPIO初期化コードに以下の1行を加えました。
    sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX);
また念のため、音の途切れ現象とFIFOレベルとの相関性を検証するために、PIOの対象ステートマシンに対応するFIFOのレベルを読み取るためのAPIを使ってmy_pico_audio_i2s/audio_i2s.cのaudio_start_dma_transfer()関数内でチェックするようにしたところ、音が途切れるときはFIFOのレベルが4未満になった場合とほぼ一致することを確認しました。
    uint tx_fifo_level = pio_sm_get_tx_fifo_level(audio_pio, shared_state.pio_sm);
    if (tx_fifo_level < 4) {
        printf("PIO TX FIFO too low: %d at %d ms\n", (int) tx_fifo_level, (int) _millis());
    }
ただしこのチェック用コードもDMA割込内に記述して毎回チェックするようにすると、それなりのオーバーヘッドになるようなので、相関性の確認などのデバッグ時のみ有効とするのが良いです。my_pico_audio_i2s/audio_i2s.cの#define WATCH_PIO_SM_TX_FIFO_LEVELのコメントアウトを外すとチェックが有効化されます。
しかし、よくよく考えてみればこのように一瞬たりとも途切れることが許容されないI/F信号に対して、FIFOが標準で4Word, 最大でも8Wordしか装備できないというのは非常に心もとない構成のように思えます。PIOは非常にフレキシブルで万能ではありますが、入出力FIFO段数の少なさは懸念点であり、せめて16 ~ 32Word程度は欲しいところではあります。

Audioデータの生成部

callback関数化

以前のコードでは、main関数内でAudioデータの生成を行っていましたが、Audioデータが必要なタイミングに合わせて生成処理を行うために、DMA割込(audio_i2s_dma_irq_handler)の中からi2s_callback_func()をコールするようにして、Audioデータ生成処理をi2s_callback_func内に実装するようにしました。
なお、i2s_callback_func()はaudio_i2s.c内ではweak属性で定義してあるので、通常は外部で再定義して使用するようにします。(sine_wave.cのi2c_callback_func()が使用される)
// irq handler for DMA
// i2s callback function to be defined at external
__attribute__((weak))
void i2s_callback_func()
{
    return;
}

void __isr __time_critical_func(audio_i2s_dma_irq_handler)()
{
    uint dma_channel = shared_state.dma_channel;
    if (dma_intsx & (1u << dma_channel)) {
        // DMA処理 省略

        i2s_callback_func();
    }
}

callback処理の別スレッド化

一方で上記のようにすると、割込ルーチン内での処理が増えるため、Audioデータ生成処理に時間がかかるような場合は望ましい方法とは言えません。したがってmy_pico_audio_i2s/audio_i2s.cの#define CORE1_PROCESS_I2S_CALLBACKをコメントアウトを解除することにより、i2s_callback_func()をCore1側で別スレッドで行うオプションを設けました。この場合、メインルーチン、および割込処理はCore0で実行され、i2s_callback_func()処理はCore1で実行されることになります。コア間の通信はInter-processor FIFOs (Mailboxes)を扱うAPIを介して行います。
enum FifoMessage {
    RESPONSE_CORE1_THREAD_STARTED = 0,
    RESPONSE_CORE1_THREAD_TERMINATED = 0,
    EVENT_I2S_DMA_TRANSFER_STARTED,
    NOTIFY_I2S_DISABLED
};

static const uint64_t FIFO_TIMEOUT = 10 * 1000; // us

// irq handler for DMA
void __isr __time_critical_func(audio_i2s_dma_irq_handler)()
{
    uint dma_channel = shared_state.dma_channel;
    if (dma_intsx & (1u << dma_channel)) {
        // DMA処理 省略

        bool flg = multicore_fifo_push_timeout_us(EVENT_I2S_DMA_TRANSFER_STARTED, FIFO_TIMEOUT);
        if (!flg) { printf("Core0 -> Core1 FIFO Full\n"); }
    }
}

void i2s_callback_loop()
{
    multicore_fifo_push_blocking(RESPONSE_CORE1_THREAD_STARTED);
    printf("i2s_callback_loop started (on core %d)\n", get_core_num());
    while (true) {
        uint32_t msg = multicore_fifo_pop_blocking();
        if (msg == EVENT_I2S_DMA_TRANSFER_STARTED) {
            i2s_callback_func();
        } else if (msg == NOTIFY_I2S_DISABLED) {
            break;
        } else {
            panic("Unexpected message from Core 0\n");
        }
        tight_loop_contents();
    }
    multicore_fifo_push_blocking(RESPONSE_CORE1_THREAD_TERMINATED);
    printf("i2s_callback_loop terminated (on core %d)\n", get_core_num());

    while (true) { tight_loop_contents(); } // infinite loop
    return;
}
ただし、i2s_callback_func()があまり負荷のない状況下では冒頭のI2Sの音が途切れる現象に対しては、Core1を使用することで逆に症状が悪化しました。RP2040のバス構成上、2つのコアからのバスアクセスが発生した場合に全体としてアクセス効率が低下することがあるのではないかと推測しています。したがってCORE1_PROCESS_I2S_CALLBACKはデフォルトでコメントアウト状態としてあります。

DC/DC電源ノイズ低減

少しI2S I/Fの趣旨からは外れますが、3.3V DC/DC電源ノイズ低減について書きます。PCM5102ボードの電源についてピン36の3V3(OUT)からの供給とした場合にDC/DCの残留リップルの影響を受けるようでAudio用途としてかなり厳しいレベルのノイズがPCM5102ボードのライン出力から出力されます。PCM5102ボードには3.3V LDOがそれぞれアナログ、デジタル用に1つずつ搭載されているので、3.3Vからの電源供給はそもそも正しい使用方法ではなく、ピン40のVBUSの5Vから取得すれば良好なノイズレベルとなります。
一方で通常のボードでは単純にLDOを搭載していることも多いなか、Raspberry Pi PicoボードはDC/DC搭載かつStep-up/Step-down両対応であり電流容量も最大800mAで充分であるため、バッテリー動作を考えた場合にこのDC/DCを活用できないのは非常に惜しくもあります。Raspberry Pi Picoの回路図(https://datasheets.raspberrypi.org/内のpico/RPi-Pico-R3-PUBLIC-20200119.zipにあります。)を眺めていたら、以下のような記述がありました。
DC/DCのモード

DC/DCの仕様書は以下から入手できます。
RT6150B-33GQW

DC/DCの変換効率は落ちても品質重視のPWMモードがあるようなので試してみたところ、3V3(OUT)からPCM5102に電源供給してもあまりノイズが気にならないレベルとすることが出来ました。コードの該当部分は以下の通りです。
    // DCDC PSM control
    // 0: PFM mode (best efficiency)
    // 1: PWM mode (improved ripple)
    gpio_init(PIN_DCDC_PSM_CTRL);
    gpio_set_dir(PIN_DCDC_PSM_CTRL, GPIO_OUT);
    gpio_put(PIN_DCDC_PSM_CTRL, 1); // PWM mode for less Audio noise
以上、最新版のコードは以下にあります。
https://github.com/elehobica/pico_sine_wave_i2s_32b

自己紹介

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

注目の投稿

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

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

QooQ