There's been major progress on the Makerverse DAC over the past couple of weeks. We're exploring how to play audio with a Raspberry Pi Pico, and the journey has led us through the bare MicroPython source code! In order to get audio streaming off an SD card and through our handmade DAC we've had to reverse engineer parts of the MicroPython project, but we think you'll agree the results are worth it!


Welcome back to the factory! A couple of episodes ago, we were looking at the design for a 10 bit R2R DAC. This was just a theoretical idea at the time, but the prototypes have now arrived and been assembled. Brenton has already shot right through and is able to play music through this DAC.

A DAC (Digital Analog Converter) takes digital signals from the pins of a microcontroller and converts them into an analog voltage suitable for playing audio or creating other waveforms. GitHub user Liam Howe has been working on PiicoDev in his own spare time and has made an enhancement to the SSD 1306 OLED module, so that the OLED can now display circles.

Now, let's check out the DAC. Last time we looked at the DAC, it was just in KiCad. We had the circuit diagram and some theoretical behavior. This time, we've got a pair of modules that we're going to play with. Of the two modules that we've currently built, the two prototypes, one has been built with 100K resistors and the other with 1K resistors. The difference between these two is going to be made apparent by the fact that the Raspberry Pi Pico's output pins are not ideal sources. They've got some source impedance, and we're thinking that on the 1K version, that's going to introduce some errors.

The first demonstration we're going to look at with this DAC is just putting every single number we can into the DAC and looking at the output. We are generating a sawtooth waveform by creating a loop that counts from zero to 1023 and puts that number onto our DAC. On the oscilloscope, we can see a small discontinuity about halfway up due to the 1K resistors on the first prototype DAC introducing some error. To fix this, we did another prototype with 100K resistors, which works a lot better and produces a far more precise and better sounding piece of hardware.

We are also playing a sine wave through this DAC, which is being streamed off the SD card. On the oscilloscope, we can see the sine wave and the Fourier transform of the signal, which is the frequency spectrum of the output signal. There is a huge peak on the left hand side and then to the right of that, there are a bunch of smaller peaks. These are all the errors in the system coming out as extra frequencies, which are around 54 decibels below the main peak. This is close to what we'd expect from a 10-bit DAC like this, as the theoretical performance here is about 60 dB. There are also a few other errors due to the way the numbers are being handled in software, as I'm truncating 16-bit numbers.

We've been discussing the performance of the system, which is about where we expect it to be. Now, for the demo, we're going to play a song from the SD card through the DAC and speaker, and record it with the PC directly out of the DAC for comparison.

So how does this system work on a Raspberry Pi Pico written in MicroPython? We're using DMA (Direct Memory Access), a type of peripheral on the Raspberry Pi Pico that can read from an array in memory and write it somewhere else in memory. The DMA controller is reading from an array of audio data and dumping it into the PIO. The PIO is then pushing it out to the DAC at the sample rate of our audio file.

We're also using two buffers. The CPU is taking signed data from the wave file and turning it into unsigned data for the DAC. This data is being put into a buffer, which is just an array of numbers that represent the wave file to be played. At the same time, the second buffer is being emptied by the DMA controller, taking data from the buffer and putting it into the PIO driver on the Raspberry Pi Pico, and then dumping it onto the DAC.

These two processes are happening simultaneously, allowing us to play and record the audio file.

The process of playing a wave file on the Raspberry Pi Pico is quite complicated. We start by filling a buffer in the Pico's memory with data from the wave file by the CPU. This buffer is then played to our speaker by the Direct Memory Access Controller. When buffer two gets emptied, it can swap with buffer one so that this one starts to get filled up with new data and this one that was filled gets pushed to the DAC. If buffer two ends up emptying faster, this buffer gets empty before buffer one has actually been filled up, resulting in a condition called a buffer underrun, which causes a gap in the music.

Initially, I started by just filling up a single array in the Raspberry Pi Pico's memory and then playing that to the DAC one sample at a time. The limitation here is how much RAM you've got on the RP2040 chip, so you don't get much more than about one second of wave file data at a very slow sample rate. The step above that was then streaming data from the Raspberry Pi Pico's flash memory, which gives you somewhere between five and ten seconds of wave file data, limited by the size of the flash memory that comes with the Pico.

The bridging step was streaming off the Raspberry Pi Pico's memory through the DMA controller. One of the design features of MicroPython that made this pretty straightforward to go from the Raspberry Pi Pico's memory to an SD card is the fact that you can mount an SD card as if it is the internal memory. In Thonny, you can see this folder called SD. This is actually the SD card mounted as if it is the internal memory.

The files on the SD card mounted on this breadboard can be played with the same Python code as the internal memory. During the development process, we encountered an interesting hiccup while combining reading from the SD card with using the DMA controller. It was crashing and it took me a long time to work out why. It turns out that when you use the hardware SPI peripheral through MicroPython on the Raspberry Pi Pico, it uses a DMA controller to do the read and write transfer. This isn't documented in the documentation, but I found it in the source code for MicroPython.

Using a different DMA controller, this system started working. We started on open source and finished on it as well. If we couldn't dig into the MicroPython source code to find out the quirk with how DMA controllers are assigned by MicroPython, we would never have been able to get to this point where we can stream arbitrarily long audio files off an SD card. If this were a binary blob that was locked down and proprietary, that would never have happened.

If you have any questions about this content or if you just want to see something a little closer, let us know on the Core Electronics forums.

Until next time, thanks for watching!



Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.