Raspberry Pi Pico (RP2040) I2S WAV Sound Player

From Squirrel's Lair


  • Cargo:


  • Categories:
  • Default form


The Raspberry Pi Pico is a very capable device but it doesn't have an I2S bus. That's where its PIO capabilities come in handy. There don't seem to be as much information out there for PIO in MicroPython but it wasn't hard to adapt it from the C/C++ examples.

Learned a lot doing this - got a basic grasp of the PIO and its instruction set and how sideset works.

Not much to it

This program will load a small WAV file (<50k) from the Pico's flash into memory, then play it through a PIO block that's acting as an I2S interface, to a generic MAX98357A mono output board.

Requirements:

  • MicroPython (tested with v1.17 and 1.18)
  • Raspberry Pi Pico (or other RP2040 board) with enough free flash space
  • Three available GPIO pins (the program below uses GP15-17)
  • MAX98357A board
  • Speaker **DO NOT USE HEADPHONES, SEE PROGRAM COMMENTS FOR WARNING**
# SL-pico-play-i2s.py
#
# Written by the Squirrel's Lair on 2022-02-01
#
# Uses Raspberry Pi Pico (RP2040) and a generic MAX98357A mono output
# board to play short WAV files saved on the Pico's internal flash.
#
# Currently limited to amount of available heap space but should be
# able to use other core to help load larger files.
#
# Tested with signed 16-bit PCM mono files @ 16kHz.
# Works with higher sample rates but heap space becomes a problem.
#
# Adapted from the Raspberry Pi Pico pico-extras C/C++ library:
# https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/pico_audio_i2s/audio_i2s.pio
#
# Additional information from the Raspberry Pi Pico C/C++ SDK:
# https://datasheets.raspberrypi.com/pico/raspberry-pi-pico-c-sdk.pdf
#
# and the Raspberry Pi Pi Python SDK:
# https://datasheets.raspberrypi.com/pico/raspberry-pi-pico-python-sdk.pdf
#
# and the RP2040 Datasheet:
# https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
#
# and the Maxim MAX98357 datasheet:
# https://datasheets.maximintegrated.com/en/ds/MAX98357A-MAX98357B.pdf
#
# and WAV file documentation from FILEFORMAT:
# https://docs.fileformat.com/audio/wav/
#
# and Wikipedia:
# https://en.wikipedia.org/wiki/WAV
#
# MAX98357 supports sample rates of:
# 8, 16, 32, 44.1, 48, 88.2, and 96kHz
# 8kHz has additional timing requirements, so avoiding it for now
#
# MAX98357A DOES NOT SUPPORT LJM
#
# Also, BCLK must be 32, 48, or 64x the rate of LRCLK
# BCLK minimum is 0.2432MHz, max is 25.804MHz
#
# The frequency for BCLK is calculated by:
# sample rate * bits/channel * number of channels
#
# So, starting with, let's try:
# Mono (L/2 + R/2), 16-bit, 16kHz
# For that setup, BCLK must run at 16kHz*16*2 = 512kHz, so need
# PIO clock set to 1.024MHz
#
# WAV files:
# -if RIFF (most of them), they're little-endian. Who knew?
# -if RIFX, they're big-endian
#
# *****WARNING*****
# MAX98357 boards (like many others) can put out a surprising amount of
# sound - more than enough to damage your hearing if you're not careful.
# Sudden, unexpected, and VERY LOUD sounds can come out of them when
# you're working with software that's not robust or fully tested (like
# this software, for example).

import time
from machine import Pin
import machine
import rp2


@rp2.asm_pio(autopull=True, pull_thresh=32, out_shiftdir=rp2.PIO.SHIFT_LEFT, out_init=(rp2.PIO.OUT_LOW), sideset_init=(rp2.PIO.OUT_HIGH, rp2.PIO.OUT_LOW))


def i2s_interface():
    '''
    # LRCLK FREQUENCY: 16kHz
    # BCLK FREQUENCY: 512kHz
    # SYS FREQUENCY: 1024kHz
    # DATA is on GP15 (pins)
    # LRCLK is on GP16 (sideset)
    # BCLK is on GP17 (sideset)
    '''
    set(x, 14)        .side(0b11) # sideset bits are LRCLK and BCLK

    label("loop_left")
    out(pins, 1)                      .side(0b10)
    jmp(x_dec, "loop_left")    .side(0b11)
    out(pins, 1)                      .side(0b00)
    
    set(x, 14)                          .side(0b01)

    label("loop_right")
    out(pins, 1)                      .side(0b00)
    jmp(x_dec, "loop_right")  .side(0b01)
    out(pins, 1)                      .side(0b10)

# State machine instantiation
sm = rp2.StateMachine(0, i2s_interface, freq=1024000, sideset_base=Pin(16), out_base=Pin(15))

sm.active(1) # Turn on the state machine

filename = "FILENAME.wav" # File to read from the internal flash
file = open(filename, "rb")
wav_data = file.read()


while True:
    for i in range(0, len(wav_data), 2): # Go through 2 bytes at a time
        word_out = (wav_data[i+1]<<8) | (wav_data[i]) # Change to big-endian
        sm.put(word_out) # Send the data to the state machine for output

    time.sleep(2)