Using a 32-bit I2S DAC with Raspberry Pi Pico (PCM5102) - Sequel

Saturday, July 3, 2021

I2S DAC Raspberry Pi Pico

t f B! P L

Previously, in Using a 32-bit I2S DAC with Raspberry Pi Pico (PCM5102), I introduced the basics. This is the sequel.
The latest code is available at:
https://github.com/elehobica/pico_sine_wave_i2s_32b

I2S 32-bit application development scene

Audio Dropouts from I2S DAC

Problematic Waveform

At the time of the previous article, the code was used as-is. As I gradually added other processing to increase the load, audio dropouts from the I2S DAC began to occur intermittently. Looking at the waveform, BCK is interrupted and LRCK is stretched for the corresponding period, as shown below.
I2S waveform at the audio dropout point
Audibly, it sounds like a small click similar to record scratch noise. With the PCM5102, it becomes more noticeable when the I2S data input (DIN) is set completely to zero. When the PCM5102 is in normal output mode with zero data, Analog Mute is engaged via Zero Data Detect, and when the audio dropout occurs, the mute is released, producing a noticeable pop sound that makes it easy to identify.

Cause: PIO FIFO Underflow

I struggled considerably to resolve this issue. I tried raising the DMA transfer interrupt priority and changing the timing of other processing such as LCD display, but these had almost no effect. Of course, removing other processing like LCD display would eliminate the issue, but that defeats the purpose. I also tried running the DMA interrupt and audio data generation on separate cores, but saw no improvement.
Upon reviewing the PIO specifications again, I noticed that while the TX and RX FIFOs each have 4 words (32-bit width), they can also be configured as a unidirectional 8-word (32-bit width) FIFO. The original code allocated RX 4 words and TX 4 words, and since I2S only uses the TX direction, configuring 8 words for TX eliminated the audio dropouts. Specifically, I added the following line to the PIO initialization code in my_pico_audio_i2s/audio_i2s.pio.
    sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX);
As a further verification, I used the API to read the FIFO level of the target PIO state machine and added a check in the audio_start_dma_transfer() function in my_pico_audio_i2s/audio_i2s.c. This confirmed that audio dropouts correlated almost exactly with cases where the FIFO level dropped below 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());
    }
However, placing this check code inside the DMA interrupt to run every time introduces noticeable overhead, so it should only be enabled during debugging. Uncommenting #define WATCH_PIO_SM_TX_FIFO_LEVEL in my_pico_audio_i2s/audio_i2s.c enables this check.
However, thinking about it more carefully, for an interface signal that cannot tolerate even momentary interruptions, having only 4 words standard and 8 words maximum FIFO capacity seems like a very fragile configuration. While PIO is extremely flexible and versatile, the small number of I/O FIFO stages is a concern, and at least 16 to 32 words would be desirable.

Audio Data Generation

Callback Function Implementation

In the previous code, audio data was generated inside the main function. To perform generation at the timing when audio data is needed, I changed it so that i2s_callback_func() is called from within the DMA interrupt (audio_i2s_dma_irq_handler), and the audio data generation is implemented inside i2s_callback_func.
Note that i2s_callback_func() is defined with the weak attribute in audio_i2s.c, so it is normally redefined externally. (The i2s_callback_func() in sine_wave.c is used)
// 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 processing omitted
 
        i2s_callback_func();
    }
}

Offloading Callback Processing to a Separate Thread

On the other hand, this approach increases processing inside the interrupt routine, making it unsuitable when audio data generation takes a long time. Therefore, I added an option to run i2s_callback_func() on Core1 in a separate thread by uncommenting #define CORE1_PROCESS_I2S_CALLBACK in my_pico_audio_i2s/audio_i2s.c. In this case, the main routine and interrupt processing run on Core0, while i2s_callback_func() runs on Core1. Inter-core communication is performed through APIs that handle Inter-processor FIFOs (Mailboxes).
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 processing omitted
 
        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;
}
However, under low-load conditions for i2s_callback_func(), using Core1 actually worsened the audio dropout symptoms. I suspect that when bus accesses occur from both cores due to the RP2040's bus architecture, overall access efficiency may decrease. Therefore, CORE1_PROCESS_I2S_CALLBACK is commented out by default.

DC/DC Power Supply Noise Reduction

This is slightly off-topic from I2S, but I will discuss 3.3V DC/DC power supply noise reduction. When powering the PCM5102 board from pin 36 3V3(OUT), it appears to be affected by residual DC/DC ripple, resulting in a fairly severe level of noise on the PCM5102 board's line output for audio purposes. Since the PCM5102 board has separate 3.3V LDOs for analog and digital, supplying power from 3.3V is not the correct approach, and using 5V from pin 40 VBUS provides good noise levels.
On the other hand, while many typical boards simply use an LDO, the Raspberry Pi Pico board features a DC/DC converter that supports both step-up and step-down with a sufficient current capacity of up to 800mA. Not being able to utilize this DC/DC for battery operation would be quite unfortunate. Looking at the Raspberry Pi Pico schematic (https://datasheets.raspberrypi.org/, available in pico/RPi-Pico-R3-PUBLIC-20200119.zip), I found the following description.
DC/DC Modes

The DC/DC datasheet can be obtained from:
RT6150B-33GQW

There appears to be a PWM mode that prioritizes quality over conversion efficiency. When I tried it, the noise level when powering the PCM5102 from 3V3(OUT) was reduced to an acceptable level. The relevant code is as follows.
    // 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
The latest code is available at:
https://github.com/elehobica/pico_sine_wave_i2s_32b

About Me

My photo
Electronics, programming & audio

Featured Post

Synchronizing Radio-Controlled Clocks with Raspberry Pi Pico W (JJY Standard Radio Wave Emulator)

As a Raspberry Pi Pico W application, I built a JJY emulator for radio-controlled clocks (for time synchronization) with minimal peripheral...

QooQ