Basics of Design • CC Blog • Projects • Research & Design Hub

Audio Player Using the RP2040 

FIGURE 1 The audio player reads PCM-coded .wav files from an SD card and plays them out to a pair of headphones connected to the jack on the top right. The connector at the top left is a SWD header for programming and debugging. The user interface is via a touchscreen LCD.
Written by Andrew Levido

The RP2040 is an interesting microcontroller (MCU) from Raspberry Pi. It appealed to me because it contains a pair of flexible programmable I/O peripherals (PIOs) and two ARM Cortex M0+ cores. I designed a simple audio player to explore the capabilities of this device. This article focuses on how I used the PIO to implement a variety of peripherals and exploited the dual-core architecture. Hopefully, it will inspire you to take a closer look at this unique device.


  • What are the capabilities of the RP2040?
  • How can I build an audio player using the RP2040?
  • How can you implement peripherals on the RP2040?

  • Raspberry Pi RP2040

  • Raspberry Pi | www.raspberrypi.com

I wanted to design a simple project to explore the capabilities of the RP2040 microcontroller (MCU) from Raspberry Pi. This is a low-cost MCU based on the ARM Cortex M0+ architecture. Of particular interest is that it has dual cores, and a highly flexible programmable IO (PIO) peripheral. The PIO can be programmed to perform a range of functions that would normally require dedicated hardware.

For this project, I wanted an application that was fairly simple, but which would exercise both cores of the MCU and the PIO fairly heavily. I chose to build a simple audio player with a touchscreen user interface (Figure 1). The device reads PCM-encoded audio files from an SD card and plays them out via an audio codec with an integrated headphone amplifier. Track selection and playback control is managed via a touchscreen GUI on a small QVGA TFT display.

FIGURE 1 The audio player reads PCM-coded .wav files from an SD card and plays them out to a pair of headphones connected to the jack on the top right. The connector at the top left is a SWD header for programming and debugging. The user interface is via a touchscreen LCD.
FIGURE 1
The audio player reads PCM-coded .wav files from an SD card and plays them out to a pair of headphones connected to the jack on the top right. The connector at the top left is a SWD header for programming and debugging. The user interface is via a touchscreen LCD.

The block diagram in Figure 2 shows the main circuit elements and the interfaces between them. The display uses an 8-bit parallel interface for pixel data and control, plus four GPIOs with two ADC channels to read the resistive touchscreen, and one PWM channel to control the backlight.

FIGURE 2
This diagram shows the major circuit blocks and key interfaces. I used the RP2040’s programmable I/O modules to implement a 4-bit SD card interface, an 8-bit parallel interface for the LCD pixel data, and an I2S interface to send the audio bitstream to the codec. The MCU’s dedicated I/O peripherals were used for the backlight PWM, the I2C control interface for the CODEC and for the USB interface.
FIGURE 2
This diagram shows the major circuit blocks and key interfaces. I used the RP2040’s programmable I/O modules to implement a 4-bit SD card interface, an 8-bit parallel interface for the LCD pixel data, and an I2S interface to send the audio bitstream to the codec. The MCU’s dedicated I/O peripherals were used for the backlight PWM, the I2C control interface for the CODEC and for the USB interface.

The audio codec/headphone amplifier is an SGTL5000 [1], which has an I2C interface for control as well as an I2S digital interface for the audio data stream. I chose to use the SD card in its native 4-bit mode, rather than in SPI mode—partly for speed, but mostly to see how capable the PIO peripheral really is.

Power is provided by a small single-cell LiPo battery via a buck-boost converter. The battery can be changed via the Micro-B USB connector. I included a battery charge LED, a power-on LED, as well as two programmable LEDs—one to indicate SD card activity and one for faults. Since we already have a USB connector for battery charging, it also made sense to make use of the RP2040’s USB serial capability. This includes a boot mode which allows firmware loading via USB. I also added a serial-wire debug port, because that’s how I prefer to program and debug my projects. The full schematic is shown in Figure 3.

FIGURE 3
This is the full schematic of the audio player. You will note that unlike most MCUs, the RP2040 requires an external Flash memory for code. I used a W25Q128. Other than that, there are just a handful of other chips—an SGTL5000 codec, an MCP7831 battery charger, a RT6150 buck-boost regulator and a 12MHz crystal oscillator. The MCP1700-1.8 regulator shown at the top right is not loaded on my prototype as the codec’s internal regulator worked fine.
FIGURE 3
This is the full schematic of the audio player. You will note that unlike most MCUs, the RP2040 requires an external Flash memory for code. I used a W25Q128. Other than that, there are just a handful of other chips—an SGTL5000 codec, an MCP7831 battery charger, a RT6150 buck-boost regulator and a 12MHz crystal oscillator. The MCP1700-1.8 regulator shown at the top right is not loaded on my prototype as the codec’s internal regulator worked fine.

— ADVERTISMENT—

—Advertise Here—

LCD Interface With PIO

The display I chose was a generic 57mm QVGA (320×240) TFT with an ST7789 [2] driver chip. This driver supports a 3-wire serial, or an 8- or 16-bit parallel interface. I wanted to use animations in the graphical UI, so using the faster parallel interface was necessary. I didn’t have enough pins to use a 16-bit interface, so 8 bits would have to do. In addition to the data lines, the interface requires a write strobe (WR) to latch the data, and a single address line to indicate whether the incoming byte is a command or pixel data (CMD).

The RP2040 contains two PIO modules. Each of these has four state machines that share 32 words of program memory. The user writes small assembler-like programs that control these state machines. You might think that 32 words is not much, but you can do an awful lot with just a few instructions, as you will see. The PIO section of the device datasheet contains all the information you need, as well as plenty of examples [3]. Even without reading this, you can get an idea of how the PIO works by looking at the program for the parallel interface in Listing 1.

LISTING 1
This listing shows the PIO code for the LCD parallel interface. The WR and address line are controlled via side-set, a feature that allows each instruction to change the state of a small number of pins in the same clock cycle it is executing. The whole thing takes just six instructions.


.program par_write.side_set 2start_wr:  pull    block           side 0b11     ; 1  jmp     !y data         side 0b11     ; 2  out     pins, 8         side 0b00     ; 3  jmp     start_wr        side 0b10     ; 4data:  out     pins, 8         side 0b01     ; 5  nop                     side 0b11     ; 6

The .program directive names this program, and the .side_set directive lets us know that each instruction can control the state of two pins in addition to doing whatever the instruction dictates. In our case, the two side-set pins are the address line and the write strobe.

Execution starts at the start_wr: label and wraps back to this point after executing the final nop instruction. The first instruction (pull) attempts to pull data from the transmit FIFO (TX FIFO) associated with this state machine and load it into the output shift register (OSR). If the FIFO is empty—which it will be until we push some data into it from our main C code—the state machine stalls on this instruction. The side set pins are both high while we are stalled.

When we want to write to the LCD, our C code must do two things. We have to tell the PIO state machine whether the byte we will output is a command or data by writing a one or a zero to the state machine’s y-register (each state machine has two scratch registers, x and y). Secondly, we initiate the write process by pushing a data byte into the transmit FIFO.

The PIO state machine that was stalled on the pull instruction will now load the data byte into the OSR. The next instruction at line 2 is a conditional jump based on the value of the y-register. If the y-register is non-zero (that is, we want to write a command), the jump is not taken. At this stage both side set pins are still held high.

— ADVERTISMENT—

—Advertise Here—

The third instruction outputs a byte from the shift register onto the eight pins we have configured to be the target of the out instruction. At the same time, we assert both the WR and the CMD side set pins. This is followed by an unconditional jump back to the start, ready for the next write. During this jump cycle, the WR pin is returned high to latch the data and address information into the LCD controller. Once we return to the first instruction the CMD pin is also returned high.

The top diagram in Figure 4 shows how this looks. An 8ns clock (full speed with a processor clock of 125MHz) is enough to meet the LCD timing requirements, which means a single write cycle, indicated by the shaded instructions, takes 64ns.

FIGURE 4
This diagram shows how the PIO is used to write data to the LCD via an 8-bit parallel interface. The instruction numbers correspond to those in the relevant code listing. The 4-cycle sequence required to write a command byte is highlighted in yellow. This write sequence takes just 64ns, demonstrating that the PIO can be used to write fairly fast interfaces.
FIGURE 4
This diagram shows how the PIO is used to write data to the LCD via an 8-bit parallel interface. The instruction numbers correspond to those in the relevant code listing. The 4-cycle sequence required to write a command byte is highlighted in yellow. This write sequence takes just 64ns, demonstrating that the PIO can be used to write fairly fast interfaces.

If the jmp in line 2 of the code is taken because we intend to write data rather than a command, the instructions at lines 5 and 6 are executed instead of those at lines 3 and 4. These instructions output the data and assert the WR pin as before, but leave the CMD pin high. The nop instruction at line 6 is required to make this path through the code exactly the same length as the alternative, since the wrap back to the start does not consume a cycle.

The lower part of Figure 4 shows that if we want to write data bytes sequentially, we can take advantage of the FIFO and load the next data while the state machine is transmitting the previous byte. We only need to write to the y-register once, before the first data byte, so we can push out one pixel (2 bytes) every 128ns. This means we can theoretically refresh the entire 320×240 pixel display in under 10ms. This is plenty fast enough—even with software overhead we should be able to update the display at 50fps for smooth on-screen animation.

— ADVERTISMENT—

—Advertise Here—

I used the LVGL graphics library to create the user interface. The RP2040 has 264KB of RAM—a fair bit for such a small MCU, but not enough for a full frame buffer at 16 bits per pixel. Given the need for some good-sized audio buffers, I used a partial frame buffer just one quarter of the screen size (38400 bytes) with no issues.

Audio CODEC Interface with PIO

The LCD interface was a good warm-up exercise for learning to use the PIO. Now on to something a little more challenging. The audio codec has two interfaces—an I2C bus for reading and writing the control registers, and a serial I2S bus for audio streaming. The RP2040 has inbuilt I2C peripherals, so I used one of those for the control interface. No need to reinvent that wheel. I did, however, use the PIO to implement the I2S interface with the codec as a master generating the bit clock and synchronization signal.

The codec’s audio interface can operate in a few different modes, but I chose the one shown in Figure 5. The bit clock (BCLK) runs at 32 times the audio sampling frequency, which can range from 8Ksps to 96Ksps. The left-right sync signal (LRC) is high for one cycle, indicating the start of the data frame, at which point the audio sample data is clocked out MSB first, starting with the left channel. The codec reads the data on the falling edge of the clock. One full data frame is shown highlighted in the diagram.

FIGURE 5
The codec supports a number of I2S interface formats. I chose this one in which the codec provides the bit clock (BCLK) and sync (LRC) signals. The PIO must detect the rising edge of the LRC signal and clock out the MSB of the left channel sample. Subsequent bits of the left sample, followed by the right sample, must be sent on the rising edge of each clock cycle. In this mode, the codec reads each sample on the falling edge of the clock.
FIGURE 5
The codec supports a number of I2S interface formats. I chose this one in which the codec provides the bit clock (BCLK) and sync (LRC) signals. The PIO must detect the rising edge of the LRC signal and clock out the MSB of the left channel sample. Subsequent bits of the left sample, followed by the right sample, must be sent on the rising edge of each clock cycle. In this mode, the codec reads each sample on the falling edge of the clock.

The challenge here is to synchronize the output of audio data with the incoming bit clock and LRC signal using the PIO. The PIO can run as fast as one cycle every 8ns, as we have already seen, and the minimum bit clock period we can expect with a 96Kbps sampling rate is 325ns (3.07MHz). This suggests that we can sample the clock and sync lines fast enough to detect edges.

Listing 2 shows the PIO code I used. Initially, the state machine waits for a rising edge on the LRC pin with the two wait instructions on lines 1 and 2. It then pulls 32 bits of data (a left and right sample) from the transmit FIFO into the OSR. This time, we use a non-blocking pull instruction that copies the x-register into the OSR if there is no data in the FIFO, rather than stalling. The x-register is set to zero, so silence is clocked out if no data is available.

LISTING 2
The PIO code for the codec’s I2S interface is shown here. The wait instruction pairs at lines 1&2 and 6&7 synchronize the data output to the incoming LRC and BCLK signals. A loop counter is used to clock out the correct number of bits (32 in total) before the code wraps back to the start for the next data frame.


.program cdc_dat  wait 0 pin 0            ; 1 wait for LRC rising edge  wait 1 pin 0            ; 2  pull noblock            ; 3 copies x to OSR if FIFO empty  out pins 1              ; 4  set y 30                ; 5loop:  wait 0 pin 1            ; 6 wait for clock rising edge  wait 1 pin 1            ; 7  out pins 1              ; 8  jmp y-- loop            ; 9  

As soon as the data is pulled, the MSB is shifted out via the out instruction on line 4. After that, the y-register, which we will use as a loop counter, is loaded with the value 30. The next two wait instructions (lines 6 and 7) look for a rising edge on the BCLK pin, after which the next bit of data is shifted out. Line 9 contains a conditional jmp instruction that tests the loop counter then decrements it. While the loop counter is non-zero, the jump is taken and subsequent bits are shifted out on each rising edge of the bit clock.

When the counter reaches zero, we will have clocked out a total of 32 bits—one during the LRC pulse and 31 thereafter. The C code uses two RAM buffers and two chained DMA channels to transfer audio data into the TX FIFO continuously during playback. One buffer is filled with PCM data samples read from the SD card while the other is playing out. The two DMA channels are chained to each other, allowing seamless playback.

SD Card Interface with PIO

So far, we have implemented two peripherals in the RP2040’s PIO modules. The LCD interface consumed one state machine and used six instructions, and the CODEC interface consumed one state machine and nine instructions. This is about half the capacity of one PIO module (and we have two of them).

Things get a bit more complicated with the SD card interface [4]. This has a clock line, one bidirectional command line, and four bidirectional data lines (which we will use in just one direction since we only need to read from the card).

In native SD card mode, the SD card receives commands and transmits responses on the command (CMD) line. Commands are always 48 bits long but may have no response, a 48-bit response, or a 136-bit response depending on the command (Figure 6).

FIGURE 6
In native SD mode commands are sent to the SD card over the CMD line, and responses from the card are received over the same line. Some commands do not have a response. The lower part of this figure shows how data is received from the card if a data read command is sent. Data arrives in 512-byte blocks on the data lines (4 in our case). Details of the command and data structures are shown in Figures 7 and 8 respectively.
FIGURE 6
In native SD mode commands are sent to the SD card over the CMD line, and responses from the card are received over the same line. Some commands do not have a response. The lower part of this figure shows how data is received from the card if a data read command is sent. Data arrives in 512-byte blocks on the data lines (4 in our case). Details of the command and data structures are shown in Figures 7 and 8 respectively.

The command line, therefore, must support bidirectional communications. Fortunately, the PIO can change the direction of I/O pins if necessary. Card data is transmitted one block (512 bytes) at a time on the data lines. SD cards support both single and multiblock reads, but I found the reliability better just using the former. The bit clock is provided by the host, initially at 400kHz, but it can be up to 25MHz once the card is initialized successfully.

The format of the command and response tokens is shown in Figure 7. Command tokens are always 48 bits long, starting with a zero-one bit combination, a 6-bit command, then a 32-bit argument, and finishing with a 7-bit CRC and a single stop bit.

FIGURE 7
This figure shows the structure of command and response tokens transmitted and received on the CMD line. Command tokens are always 48 bits long and include a command start zero-one sequence, a 6-bit command, and a 32-bit argument. A 7-bit CRC and a final stop bit complete the token. Response tokens can be 48 or 136 bits in length and vary in content depending on the command. All start with a zero-zero response start sequence, echo the command they are responding to, and end with a stop bit.
FIGURE 7
This figure shows the structure of command and response tokens transmitted and received on the CMD line. Command tokens are always 48 bits long and include a command start zero-one sequence, a 6-bit command, and a 32-bit argument. A 7-bit CRC and a final stop bit complete the token. Response tokens can be 48 or 136 bits in length and vary in content depending on the command. All start with a zero-zero response start sequence, echo the command they are responding to, and end with a stop bit.

There are four possible response token formats. Three are 48 bits long with 32-bits of content, and one is 136 bits long with 120 bits of content. Response tokens start with a zero-zero start sequence and finish with a stop bit. CRCs are used in some responses, but we will ignore them for the sake of simplicity.

Data transmission from the SD card in wide-bus mode is preceded by a start bit (zero), then clocked out most significant nibble first as shown in Figure 8. After 1024 nibbles (512 bytes) are sent, a 16-bit CRC and a stop (one) bit follows. Again, I chose to ignore the CRC on data transmitted from the SD card, but we do have to provide it for commands we send as the card will throw an error otherwise.

FIGURE 8
The SD card sends data over a 4-bit bus as shown here. Each data packet begins with a start bit, followed by 1024 nibbles of data corresponding to the 512-byte disk sector being read. The data is followed by a 16-bit CRC (calculated on each line) and a final stop bit. My code ignores the CRC on received data.
FIGURE 8
The SD card sends data over a 4-bit bus as shown here. Each data packet begins with a start bit, followed by 1024 nibbles of data corresponding to the 512-byte disk sector being read. The data is followed by a 16-bit CRC (calculated on each line) and a final stop bit. My code ignores the CRC on received data.

The PIO driver for the SD card uses three state machines—one for the clock, one for the command line, and one for the data lines. Listing 3 shows the clock state machine code at the top. The clock pin is toggled each cycle via side set while an internal interrupt flag is alternately set and cleared. This IRQ flag allows us to synchronize the other state machines with the serial clock.

LISTING 3
The SD card interface is the most complex by far. Three separate state machines are used—one to control the clock, one for the command and response, and one for the data lines. The clock state machine (top) alternately sets and clears an IRQ flag that the other two state machines use to synchronize with the clock signal.


.program clock.side_set 1  irq set sd_irq        side 1    ; 1  irq clear sd_irq      side 0    ; 2  .program cmd_rw.origin 0public start_rw:  set pins, 0b1           ; 1 set pin high  set pindirs, 0b1        ; 2 and set to outputpublic wait_tx:  out x, 8                ; 3 first byte is tx count -> x  out y, 8                ; 4 second byte is rx count -> y  wait 0 irq sd_irq       ; 5 sync on clockcmd_tx_loop:  out pins, 1             ; 6 send out bits  jmp x-- cmd_tx_loop     ; 7 until done   set pindirs, 0b0        ; 8 set pin to inputpublic wait_rx:  wait 0 pin, 0           ; 9 wait for a zerocmd_rx_loop:  in pins, 1              ; 10 read in each bit  jmp y-- cmd_rx_loop     ; 11 until done            .program dat_rstart_r:  out x, 32               ; 1 first word is nibble count -> x  wait 0 pin, 0           ; 2 wait for a zero  wait 0 irq sd_irq       ; 3 sync on clockdat_rx_loop:  in pins, 4              ; 4 read in each nibble  jmp x-- dat_rx_loop     ; 5 until done

The command state machine is quite complex, as shown in the middle of Listing 3. You will note a .origin directive at the beginning of this program. This forces the compiler to locate this code at the beginning of the PIO instruction memory. This will be important later. The state machine initially sets the command pin high and sets it to be an output (lines 1 and 2). It then stalls, waiting for command data to be written to the TX FIFO.

You will notice that there is no pull instruction before the out instruction (line 3) as we have had in previous examples. This is because the PIO has another cycle-saving trick up its sleeve. We can configure the TX FIFO to automatically pull data from the FIFO to the OSR once a certain amount of data has been transmitted. In this case, we configure the auto-pull to occur after one byte is transmitted. The out instruction will stall if the FIFO is empty, and auto-pull is enabled. There is a corresponding auto-push functionality for the RX FIFO which is how we separate the incoming bit stream into bytes as we read in the response (line 10).

The first byte of a transmit sequence is a count (n-1) of bits to transmit, and the second byte a count (n-1) of bits in the expected response. These counts are stored in the x and y scratch registers respectively (lines 4 and 5). The state machine then waits for the IRQ flag to be cleared, synchronizing the transmission with the clock, and then shifts out the first bit of the command. The count of remaining bits is tested, and if non-zero, the count is decremented and the jump is taken to shift out the next bit. If the count is zero, all 48 bits have been clocked out. The CMD pin is now switched to be an input, ready to read in the response (line 8).

The state machine now waits for the zero-bit representing the start of the response sequence. The state machine then reads in the number of bits dictated by the y-register, after which it wraps back to the beginning, setting the CMD pin back to be an output ready for the next command.

If the command requires no response, this state machine could be stuck waiting for a start-of-response bit that will never come. To avoid this, the C code terminates the state machine and resets it once execution reaches the wait_rx: point. It knows that this point has been reached by checking that the state machine program counter matches the offset of this label. The label is exported to the C program as an offset from the beginning of the program. By forcing the compiler to locate this state machine’s code at the bottom of the memory, we can safely compare the label offset with the program counter.

A third state machine reads the data pins. Its code is shown at the bottom of Listing 3. This works similarly to the previous state machines in that it blocks on the TX FIFO until a word is written to it to start the read sequence. The data written to the FIFO is the number of nibbles we are expecting to read (less one). This count is stored in the x-register.

The state machine then waits until the start bit (a zero) is detected on the D0 line (line 2). After synchronizing with the clock, we then read each nibble in a loop, decrementing the counter until all 1024 nibbles have been accounted for. The data is followed by 16 CRC nibbles and a stop nibble which are ignored.

The SD card driver uses three DMA channels. Two are used for the command state machine—one to transfer the 8 bytes comprising the transmit sequence from a send buffer to the TX FIFO—chained to a second one to read the 6 or 17 bytes comprising the response from the RX FIFO to a receive buffer. The third DMA channel is used to read the data block (usually a 512-byte disk sector) from the SD card to the file system buffer.

Two Cores are Better than One

The RP2040 contains two identical M0+ cores that operate independently but can communicate and synchronize via dedicated inter-core FIFOs and hardware spinlocks, respectively. The dual-core architecture is ideal for something like this audio player where there are two loosely coupled, asynchronous but time-critical processes happening at the same time. I used Core 0 for the audio processing and Core 1 for the graphical user interface and application logic. Figure 9 shows the overall architecture of the firmware.

FIGURE 9
This diagram shows the high-level firmware architecture of the audio player. We take advantage of the two independent cores by using core 0 to support the audio and SD card functions and core 1 for the application logic state machine and the GUI. The two separate threads communicate with each other via events posted to the inter-core FIFOs and through a small amount of shared memory protected from race conditions via hardware spinlocks.
FIGURE 9
This diagram shows the high-level firmware architecture of the audio player. We take advantage of the two independent cores by using core 0 to support the audio and SD card functions and core 1 for the application logic state machine and the GUI. The two separate threads communicate with each other via events posted to the inter-core FIFOs and through a small amount of shared memory protected from race conditions via hardware spinlocks.

The GUI is based on the LVGL library and comprises three screens. The first is a splash screen that only shows briefly on power-up. The main screen you can see in Figure 1 includes the playback controls and the title of the currently selected track. You can skip back and forth through any .wav tracks found in the root directory of the SD card.

There is also a tracklist screen that offers a dynamic scrollable list of available tracks. This features smooth scrolling and a bit of ballistic bounce—mostly to show off the display update rate and what you can do with a more or less dedicated M0+. All of the code for the UI was generated on my laptop by the SquareLine Studio application.

Core 1 runs the User Interface which includes the LVGL GUI and the finite state machine that controls the application’s logic. This core also handles the touchscreen and backlight.

As mentioned, Core 0 handles the audio system comprising the SD card and CODEC, including the file system, parsing of .wav files, and the data pump that transfers audio data from the SD card to the CODEC.

Communication between the two processes running on different cores is via messages posted to the inter-core FIFOs and a small amount of shared memory protected by a spinlock. The spinlock prevents one core reading from shared memory while the other is writing to it, forcing the reading core to “spin,” doing nothing until the write is complete. For obvious reasons, these spinlock-guarded critical sections are kept short.

Final Thoughts

This audio player is not meant to be a real product, and so it is simply a sandwich of two PCBs. The lower one (Figure 10) contains all the parts except for the display, while the upper one is just a mechanical support for the display. The battery is adhered to the main PCB with double-sided tape.

FIGURE 10
The main PCB contains all of the components, including the battery which is adhered to the board with double-sided tape. The display connector is at the bottom, above the battery connector, with the USB connector on the left and micro-SD card socket on the right. The headphone connector is at the top right and the SWD programming connector is on the top left. There are plenty of test points to allow debugging of the PIO interfaces.
FIGURE 10
The main PCB contains all of the components, including the battery which is adhered to the board with double-sided tape. The display connector is at the bottom, above the battery connector, with the USB connector on the left and micro-SD card socket on the right. The headphone connector is at the top right and the SWD programming connector is on the top left. There are plenty of test points to allow debugging of the PIO interfaces.

As an exercise in using the RP2040, this project has been a success. It plays .wav files nicely, and the GUI, though small in size, shows what can be achieved at a low cost. There are, however, a few limitations. I have made no attempt to reduce power consumption and prolong battery life. At present, the device only looks in the root directory for .wav files, and it only plays the selected file and then stops; there is no continuous play mode or track shuffling. Only 16-bit stereo PCM-coded files will play, although it does handle various sampling rates.

All these things could be addressed in the firmware with sufficient time and motivation. If I were to build the hardware again, I would use a slightly bigger display and battery, but otherwise I am pretty happy with the result.

The RP2040 is an impressive chip for the price—just $0.85 USD in single quantity at the time of writing. You do have to provide the Flash memory as well, so let’s say $3 USD all up. That’s still a lot of processor bang for the buck. The PIOs are extremely powerful, and frankly, a lot of fun to program and use. The C SDK is pretty good for such a new device and the toolchain was easy (as toolchains go) to set up with my preferred editor.

If I have a criticism, it is the lack of I/O. Thirty usable pins is just not enough for many applications. Given the power of the cores, I would like to see a version with 64 I/O pins and four PIO modules. Overall, though, I think this chip is well worth a look. Having two cores at your disposal is a real bonus that you don’t often see in this price range. 

RESOURCES
Raspberry Pi | www.raspberrypi.com

REFERENCES
[1] SGTL5000 | Ultra-Low-Power Audio Codec:
https://www.nxp.com/products/audio-and-radio/audio-converters/ultra-low-power-audio-codec:SGTL5000
[2] ST7786V Display Driver:
https://newhavendisplay.com/content/datasheets/ST7789V.pdf
[3] RP2040 Data Sheet:
https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
[4] Simplified Specifications | SD Association November 24, 2020. https://www.sdcard.org/downloads/pls/

Code and Supporting Files

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • MARCH 2024 #404 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
+ posts

Andrew Levido (andrew.levido@gmail.com) earned a bachelor’s degree in Electrical Engineering in Sydney, Australia, in 1986. He worked for many years in R&D for power electronics and telecommunication companies, before moving into management roles. Andrew has maintained a hands-on interest in electronics, particularly embedded systems, power electronics and analog design. Over the years, he has written articles for various electronics publications, and provides consulting services as time allows.

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2026 KCK Media Corp.

Audio Player Using the RP2040 

by Andrew Levido time to read: 20 min