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

Monday, March 1, 2021

I2S DAC Raspberry Pi Pico

t f B! P L

I tested driving the I2S DAC PCM5102 in 32-bit mode with the Raspberry Pi Pico.

I2S 32-bit testing scene

Base Project (sine_wave)

In pico-playground, there is aaudio/sine_wave project, which I decided to use as a base and modify. This project performs the following operations.
  • Output can be selected from SPDIF, PWM, or I2S, with a sampling frequency of 44.1KHz for SPDIF and 24KHz for PWM/I2S output (this article focuses on I2S only)
  • Generates a mono 16-bit sine wave
  • Sends a 16-bit mono signal to the Audio API, which converts it into a 16-bit stereo signal buffer
  • Transfers the stereo signal buffer to PIO via DMA
  • PIO generates a 3-wire I2S format signal and outputs the 16-bit stereo signal to the DAC
  • Frequency and volume can be adjusted via serial terminal character input

Pin Configuration

Even with this project as-is, connecting a PCM5102 will drive I2S in 16-bit stereo mode and produce sound.
The connection to Raspberry Pi Pico is as follows.

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

The PCM5102 board has configuration pads that should be set as follows. On the board I obtained, all settings were already in this configuration except for the SCK bridge.

ItemSettingNotes
SCKbridgeSCK is generated internally
H1L (FLT)L
H2L (DEMP)L
H3L (XSMT)H
H4L (FMT)LL: I2S Format, H: Left-justified

The SCK solder bridge is on the front side of the PCM5102 board, and the HxL settings are on the back side.

32-bit Stereo Sample Project (rasp_pi_pico_sine_wave_i2s_32bit)

https://github.com/elehobica/pico_sine_wave_i2s_32b/tree/v0.1.0
I have published the project with 32-bit stereo support. The changes are as follows.
  • Changed the I2S sampling frequency to 44.1KHz
  • Modified the Audio API and I2S Audio API to support 32-bit stereo audio signals
  • Generates a 32-bit sine wave. Frequencies are controlled separately for left/right channel identification (Left: [ ], Right { })
  • PIO generates 32-bit stereo I2S format signals (16-bit stereo transmission mode is also retained)
  • Generates low-jitter BCK and LRCK signals by optimizing the PIO clock frequency
Since there was no existing API for handling 32-bit audio signals in the Audio API and I2S Audio, I moved them locally and added 32-bit support for the time being. Ultimately, if the Audio API could handle 8-bit, 16-bit, and 32-bit Mono/Stereo signals, and separately specify 16-bit, 24-bit, or 32-bit Stereo for the I2S DAC, allowing free conversion between them, it would be very user-friendly. For now, I implemented support for two conversions: 16-bit Stereo audio signal to 16-bit Stereo I2S, and 32-bit Stereo audio signal to 32-bit Stereo I2S. (Implemented in the my_pico_audio and my_pico_audio_i2s folders)

PIO I2S 32-bit Support

PIO, one of the key features of the Raspberry Pi Pico, allows various I/O specifications to be programmatically modified while maintaining a certain level of speed performance, making it advantageous for flexibly handling minor specification changes like this one. PIO programs are written in a proprietary assembly language called PIOASM. Since it is specialized for I/O control, the instruction set and specifications are relatively limited, but once you get used to it, it seems quite manageable. I achieved 32-bit support by modifying audio_i2s.pio in pico_audio_i2s.
Specifically, the BCK toggle count, which was hardcoded to 16 times per channel, was made variable by passing a value through the ISR register. PIO has X and Y scratch registers that can receive values from external sources in addition to being used within the program. However, for output-only programs like this I2S implementation, using the ISR (Input Shift Register) is preferable since it keeps the scratch registers free for other use.
The code for setting values to the ISR was referenced fromRP2040 Datasheet Section 3.6.8 PWM.

audio_i2s.pioExcerpt
.program audio_i2s
.side_set 2
 
; Inverted LRCK polarity to conform to I2S format (not Left-Justified)
; ISR is used as a register to set the bit count
 
                    ;        /--- LRCLK
                    ;        |/-- BCLK
bitloop1:           ;        ||
    out pins, 1       side 0b00
    jmp x-- bitloop1  side 0b01
    out pins, 1       side 0b10
    mov x, isr        side 0b11 ; Load bit shift count from 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 ; Load bit shift count from 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
 
    // Calculate and set the bit shift count to ISR from audio data bit count (*)
    // 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)); // Load all 32 bits of the input value into ISR (*)
    pio_sm_set_enabled(pio, sm, true);
 
    pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_offset_entry_point));
}

Generating Low-Jitter BCK and LRCK Signals

By default, the PIO frequency is generated by dividing down from the system clock frequency of 125.0MHz. However, since integer-only division from 125.0MHz has limitations, a fractional clock divider enables approximating the target frequency. For typical serial interfaces and other I/O control, this frequency matching is a very useful feature. However, when using PIO for I2S clock generation, it produces a signal with significant clock jitter, which is undesirable for audio applications. Therefore, it is preferable to output at a fixed frequency without using the fractional clock divider. (I have confirmed that the output clock contains jitter attributable to the fractional clock divider.)

With a sampling frequency of 44.1KHz, the BCK required for a 32-bit 2ch DAC is 44.1KHz x 32bit x 2 = 2.8224MHz, but since PIO needs to generate both BCK high and low periods, it requires double that: a 5.644MHz clock input. However, with a 125MHz system clock, it is impossible to obtain a frequency close to 44.1KHz using only integer division without the fractional clock divider. (125.0MHz / 22 = 5.6818MHz, equivalent to a sampling frequency of approximately 44.389KHz) 
If the clock is 96.0MHz, then 96.0MHz / 17 = 5.647MHz, yielding a sampling frequency equivalent to approximately 44.118KHz, which is relatively good. However, since lowering the sys_pll frequency would degrade overall performance, as a workaround I configured the usb_pll frequency to 96.0MHz and derived both the 48.0MHz frequency for USB and the 96.0MHz frequency for PIO from it.

sine_wave.cExcerpt
// Generate 96MHz with pll_usb for PIO use (48MHz supplied for USB)
    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.cExcerpt
#if 0 // PIO_CLK_DIV_FRAC (Using 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 // Not using fractional clock divider (this one is used)
    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

Logic Analyzer Waveform

Finally, here are the actual I2S signal waveforms captured with a logic analyzer.
I2S 32-bit Logic Analyzer Waveform

The sequel is available here
Using a 32-bit I2S DAC with Raspberry Pi Pico (PCM5102) - Sequel

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