Back To Basics: The Ring Buffer

If you’ve been writing software for very long, you’ve likely encountered the humble ring buffer. Ring buffers are everywhere . They appear in code for audio engines, network stacks, kernel drivers, graphics engines, etc. They’re one of the most important data structures for getting data from point A to point B in a timely and thread-safe way. Today, we’re going to show the ring buffer some love. We’ll talk about how the ring buffer works, why you might use it, and we’ll even implement one in C++.

What Is a Ring Buffer?

You may have heard of something called a circular buffer, or maybe even a cyclic queue. Both of those are just other names for the ring buffer. You can think of a ring buffer as a specialized type of queue. Just as with any old, vanilla queue, a producer produces some data, shoves it into the data structure, and a consumer comes along and consumes it. This all happens in first-in-first-out (FIFO) order. But what sets the ring buffer apart is the way it manages its data and the limitations it imposes.

A ring buffer has a fixed capacity. It can’t grow and it can’t shrink. If you create a ring buffer that can store 10 items, 10 items will forever be its max capacity. Because the capacity is fixed, a producer has two options once it fills the ring buffer – the choice of which is largely driven by how the ring buffer is designed and the application’s needs. The producer can either wait until a slot is free in the ring buffer so that it can add more data. Or it can just stomp over data that hasn’t been consumed yet. Both approaches are valid in certain contexts.

The consumer’s role is to consume data.  If there’s no data available in the ring buffer, the consumer has to wait or go do something else. The side-effect of a consumer reading data is that it frees up slots within the ring buffer for the producer to use.

Ideally, the producer’s producing is always just slightly ahead of the consumer’s consuming, resulting in a nice game of “catch me if you can” with virtually no waiting by either party.

The canonical diagram of a ring buffer looks like so.

In this diagram, we have a buffer with 32 slots. The producer has filled 15 of them, indicated by blue. The consumer is behind the producer, reading data from the slots, freeing them as it does so. A free slot is indicated by orange.

Keep in mind that these sorts of diagrams are meant to demonstrate a concept and not so much depict an implementation. It’s certainly possible to implement a ring buffer that’s physically circular, as in the image above, using something like a linked list of separate smaller buffers. Quite often, however, you’ll see a ring buffer implemented using one big memory allocation. In this case, separate state is used to track used capacity as well as read and write indices.

Why Use a Ring Buffer?

You’ll most commonly see ring buffers used in circumstances that involve high volumes of data that need to be produced and consumed at high speeds. Real-time audio/video, network drivers, etc. are all contexts where you’ll find ring buffers being used. In these situations, performance of the data structure is key.

Performance takes a beating if you’re having to allocate and deallocate memory all the time. Memory allocation is slow. So ring buffers avoid it by allocating a chunk of memory one time and reusing it over and over. Memory fragmentation is also avoided.

Anything that involves locking (e.g.,  mutexes, and even memory allocation) is problematic when it comes to performance.  You simply can’t predict how long a lock will be held. As we’ll see in our example implementation, ring buffers can be both thread-safe and lock-free.

Speed Is My Jam. When Would I Not Want to Use a Ring Buffer?

Ring buffers are ideally suited for the single producer/single consumer scenario. If there are more than one of either, things get complicated and locking will certainly need to be introduced either directly within the ring buffer or as a wrapper.

If you need your data structure to grow to an arbitrary size, then ring buffers are off the table.

If you need to explicitly control the lifetime of data within the ring buffer (e.g., if you need to hold pointers/references to contained data), then ring buffers are obviously the wrong choice.

If your data needs to be sorted or have a priority associated with it, ring buffers are the wrong choice here as well. Ring buffers are ALWAYS first-in, first-out.

The Gotcha

Because the ring buffer size is fixed, you sometimes have to experiment to find the optimum size to keep the producer ahead of the consumer at a rate that neither ever has to wait to do their job. Take the world of professional audio software as an example. Here, you’ll sometimes find the ring buffer capacity explicitly configurable within the UI as a mechanism for the user to tradeoff between audio latency and glitchy audio for a given hardware configuration.

Screenshot taken from Cakewalk BandLab

When is it ok for a producer to stomp over unconsumed data?

I mentioned earlier that in some scenarios it’s ok for a producer to stomp over unread data. When would this ever be ok? Consider a real-time audio or video streaming application such as a radio streamer or video conferencing app, or perhaps the broadcasting of player states in an online game. For sundry reasons, network hiccups occur. We’ve all experienced them. But regardless of the why, it’s always important to charge forward and have our applications processing the latest and greatest data.

Implementing an Audio Ring Buffer

The time has come. We’re going to implement a simple ring buffer in C++. We’ll make it data type agnostic and lock-free. Let’s start things off with a class skeleton, a constructor that allocates the buffer, and some state for tracking how much of the buffer is free.

template<typename DataType>
class RingBuffer
{
public:
    RingBuffer(int capacity) :
        m_capacity(capacity),
        m_freeItems(capacity),
    {
        static_assert(ATOMIC_INT_LOCK_FREE == 2);
        m_spBuffer = std::make_unique<DataType[]>(m_capacity);
        memset(m_spBuffer.get(), 0, sizeof(DataType) * m_capacity);
    }
    ...
private:
    ...
    std::unique_ptr<DataType[]> m_spBuffer;
    const int m_capacity;
    std::atomic<int> m_freeItems;
};

This is a data type agnostic implementation. RingBuffer is a class template that allows clients to parameterize it with whatever data type they wish (so long as it’s memcpy and memcmp compatible).

The constructor allows the client to specify the size of the ring buffer. This is stored in m_capacity.

We also have a std::atomic<int> that’s used to track how many free slots are available for writing. Having this atomic is key to making this class thread-safe, as both producers and consumers will be indirectly checking this value during the reading and writing of ring buffer data.

You’ll also note that the constructor has a static_assert for ensuring that std::atomic<int> is lock free. This is to avoid any locking that could impact performance.

And just for fun, we initialize the contents of m_spBuffer to zero to put us in a known initial state. This isn’t really all that important. But it might benefit unit tests (which I did write for this, incidentally).

Next up, let’s add the state and functions needed for writing data into the buffer.

    int m_writeIndex; // Initialized to zero in constructor. Not shown above.
 
    int getWriteableItemCount() const
    {
        return m_freeItems.load();
    }
 
    int write(DataType * pData, int numItems)
    {
        int writeableItemCount = getWriteableItemCount();
        if (writeableItemCount == 0)
            return 0;
 
        int totalItemsToWrite = std::min(writeableItemCount, numItems);
 
        // Note that writeableItemCount will do nothing to help us 
        // determine if we're on the edge of the buffer and need to wrap around.
        // That's up to us to determine here.
        int itemsLeftToWrite = totalItemsToWrite;
 
        // Note that we're treating m_capacity like an index here for 
        // one-past-the-end.
        if ((m_writeIndex + itemsLeftToWrite) &gt;= m_capacity)
        {
            // We'd exceed the extent of the buffer here if wrote totalItemsToWrite samples, 
            // so let's do a partial write.
            int itemsAvailableUntilEnd = m_capacity - m_writeIndex;
            memcpy(m_spBuffer.get() + m_writeIndex, pData, itemsAvailableUntilEnd);
 
            // Bookkeeping
            m_writeIndex = 0;
            itemsLeftToWrite -= itemsAvailableUntilEnd;
            pData += itemsAvailableUntilEnd;
        }
 
        if (itemsLeftToWrite &gt; 0)
        {
            memcpy(m_spBuffer.get() + m_writeIndex, pData, itemsLeftToWrite * sizeof(DataType));
 
            // Bookkeeping
            m_writeIndex += itemsLeftToWrite;
            itemsLeftToWrite = 0;
        }
 
        m_freeItems -= totalItemsToWrite;
        return totalItemsToWrite;
    }

There’s one new piece of state here and two new functions.

The new state, m_writeIndex, is used to track where the producer is writing next. This value is only ever used by the producer, so we don’t need to make it atomic.

The function getWriteableCount() merely returns the number of free slots available for writing in the ring buffer. It’s for the benefit of both the producer and the write() function itself.

The write() function attempts to write the specified number of items into the ring buffer and returns the actual number of items written. There are some things worth noting here.

We first check to see if we CAN actually write anything. If we can’t, we return immediately.

Next, we decide how MUCH we can actually write and store it in a local variable called totalItemsToWrite. This value may be less than what the producer requested if they requested more than we have space for. After that, we check to see if we might be trying to write more towards the end of the buffer than we have space for. If so, we write what we can and then loop back around to the beginning to write what’s left. Anything that’s left goes wherever the current write position is located.

Before leaving the function, we update the relevant member variables.

Now let’s look at the state and functions for reading data from the buffer.

    int m_readIndex; // Initialized to zero in constructor. Not shown above.
 
    int getReadableItemCount() const
    {
        return m_capacity - m_freeItems.load();
    }
 
    int read(DataType * pData, int numItems)
    {
        int readableItemCount = getReadableItemCount();
        if (readableItemCount == 0)
            return 0;
 
        int totalItemsToRead = std::min(readableItemCount, numItems);
 
        // Note that readableItemCount will do nothing to help us 
        // determine if we're on the edge of the buffer and need to wrap around.
        // That's up to us to determine here.
        int itemsLeftToRead = totalItemsToRead;
 
        if ((m_readIndex + itemsLeftToRead) &gt;= m_capacity)
        {
            // We'd exceed the extent of the buffer here if read totalItemsToRead items, 
            // so let's do a partial read.
            int itemsAvailableUntilEnd = m_capacity - m_readIndex;
            memcpy(pData, m_spBuffer.get() + m_readIndex, itemsAvailableUntilEnd);
 
            // Bookkeeping
            m_readIndex = 0;
            itemsLeftToRead -= itemsAvailableUntilEnd;
            pData += itemsAvailableUntilEnd;
        }
 
        if (itemsLeftToRead &gt; 0)
        {
            memcpy(pData, m_spBuffer.get() + m_readIndex, itemsLeftToRead * sizeof(DataType));
 
            // Bookkeeping
            m_readIndex += itemsLeftToRead;
            itemsLeftToRead = 0;
        }
 
        m_freeItems += totalItemsToRead;
        return totalItemsToRead;
    }

Much like the writing side of things, we have a new piece of state for tracking the consumer’s read position. We also have a complementary getReadItemCount() function for returning the number of items available for reading. And then there’s the read() function.

If you compare the write() and the read() function implementations here, you’ll see they’re almost exactly the same. There are only two big differences – the direction the data goes in and out of m_spBuffer and the way the member variables are updated (m_freeItems being incremented vs. decremented, and m_readIndex being used vs. m_writeIndex). Apart from that, they’re pretty much the same.

The checks against m_freeItems ensure that the consumer will never overtake the producer. And, in this implementation, the producer can never overwrite data. So accessing m_spBuffer from two different threads is safe because the producer and consumer are never accessing the same slots at the same time.

The only other shared state between the producer and the consumer is m_freeItems and that’s atomic.

The complete RingBuffer implementation is as follows.

/**
 * A simple ring buffer class. This is thread-safe so long as only a
 * single producer and a single consumer are clients.
 */
template<typename DataType>
class RingBuffer
{
public:
    /**
     * Constructor.
     *
     * @param capacity The total number of items to accommodate in the RingBuffer.
     */
    RingBuffer(int capacity) :
        m_capacity(capacity),
        m_freeItems(capacity),
        m_readIndex(0),
        m_writeIndex(0)
    {
        // Lock-free would be important for scenarios that can't use locking, such
        // as real-time audio. If you don't have real-time concerns, then this can 
        // possibly be removed.
        static_assert(ATOMIC_INT_LOCK_FREE == 2);
        m_spBuffer = std::make_unique<DataType[]>(m_capacity);
        memset(m_spBuffer.get(), 0, sizeof(DataType) * m_capacity);
    }
    /**
     * @return The number of items that can be read by the consumer.
     */
    int getReadableItemCount() const
    {
        return m_capacity - m_freeItems.load();
    }
    /**
     * @return The number of items that can be written by the producer.
     */
    int getWriteableItemCount() const
    {
        return m_freeItems.load();
    }
    /**
     * Attempts to read the specified number of items.
     *
     * @return The number of items read.
     */
    int read(DataType * pData, int numItems)
    {
        int readableItemCount = getReadableItemCount();
        if (readableItemCount == 0)
            return 0;
 
        int totalItemsToRead = std::min(readableItemCount, numItems);
        // Note that readableItemCount will do nothing to help us 
        // determine if we're on the edge of the buffer and need to wrap around.
        // That's up to us to determine here.
        int itemsLeftToRead = totalItemsToRead;
        // Note that we're treating m_capacity like an index here for 
        // one-past-the-end.
        if ((m_readIndex + itemsLeftToRead) >= m_capacity)
        {
            // We'd exceed the extent of the buffer here if we read totalItemsToRead 
            // items, so let's do a partial read instead.
            int itemsAvailableUntilEnd = m_capacity - m_readIndex;
            memcpy(pData, m_spBuffer.get() + m_readIndex, itemsAvailableUntilEnd);
            // Bookkeeping
            m_readIndex = 0;
            itemsLeftToRead -= itemsAvailableUntilEnd;
            pData += itemsAvailableUntilEnd;
        }
        if (itemsLeftToRead > 0)
        {
            memcpy(pData, m_spBuffer.get() + m_readIndex, itemsLeftToRead * sizeof(DataType));
            // Bookkeeping
            m_readIndex += itemsLeftToRead;
            itemsLeftToRead = 0;
        }
        m_freeItems += totalItemsToRead;
        return totalItemsToRead;
    }
    /**
     * Attempts to write the specified number of items. This is only
     * guaranteed to write what we have space for. The amount of available
     * space can be determined by invoking getWriteableItemCount(). 
     * 
     * @return The number of items actually written.
     */
    int write(DataType * pData, int numItems)
    {
        int writeableItemCount = getWriteableItemCount();
        if (writeableItemCount == 0)
            return 0;
        int totalItemsToWrite = std::min(writeableItemCount, numItems);
        // Note that writeableItemCount will do nothing to help us 
        // determine if we're on the edge of the buffer and need to wrap around.
        // That's up to us to determine here.
        int itemsLeftToWrite = totalItemsToWrite;
        // Note that we're treating m_capacity like an index here for 
        // one-past-the-end.
        if ((m_writeIndex + itemsLeftToWrite) >= m_capacity)
        {
            // We'd exceed the extent of the buffer here if we wrote totalItemsToWrite
            // samples, so let's do a partial write instead.
            int itemsAvailableUntilEnd = m_capacity - m_writeIndex;
            memcpy(m_spBuffer.get() + m_writeIndex, pData, itemsAvailableUntilEnd);
            // Bookkeeping
            m_writeIndex = 0;
            itemsLeftToWrite -= itemsAvailableUntilEnd;
            pData += itemsAvailableUntilEnd;
        }
        if (itemsLeftToWrite > 0)
        {
            memcpy(m_spBuffer.get() + m_writeIndex, pData, itemsLeftToWrite * sizeof(DataType));
            // Bookkeeping
            m_writeIndex += itemsLeftToWrite;
            itemsLeftToWrite = 0;
        }
        m_freeItems -= totalItemsToWrite;
        return totalItemsToWrite;
    }
private:
    std::unique_ptr<DataType[]> m_spBuffer; //! The data.
    int m_writeIndex; //!< Where the producer is writing to next.
    int m_readIndex; //!< Where the consumer is reading from next.
 
    const int m_capacity; //!< Total number of frames managed by the ring buffer.
    std::atomic<int> m_freeItems; //!< Number of frames that are available to be written into.
};

Hopefully this article has helped you to appreciate the humble ring buffer a little bit more. If ring buffers were new to you before this article, I hope this was a helpful introduction and that you’ll be able to recognize them in the wild when you see them (they’re not always given helpful names like RingBuffer. 🙂 ). The implementation shown here is one of the simplest ones, and just one of many possible ways to do it.

If you find a bug in my code or have any questions or comments, please let me know. If you’ve seen any bizarre ring buffer implementations, tell me about that too. Weird code is always fun to see.

Until next time.

Decibels and dB SPL

(Note: This is a slightly modified excerpt of Chapter 1 from a book I’ve been working on entitled “Digital Audio for C++ Programmers.”)

The decibel is perhaps one of the most confusing and misunderstood topics in audio. It has a confusing formula that appears to change based on the context. It’s also used in a variety of applications beyond audio. In fact, much of the documentation you’ll find is actually related electronics and telecommunications. And to muddy things even more, by itself the plain ole decibel doesn’t even really convey much meaning. It merely relates one power value to another. So, if after reading this article, you still find decibels confusing, don’t fret. You’re in good company.

The decibel originated with telephony in the early 1900’s. It was used as a way to describe the power efficiency of phone and telegraph transmission systems. It was formally defined as 1/10th of something called a bel. Interestingly, the bel was rarely used. The decibel got all the glory. The decibel has since found its way into all sorts of other domains, such as optics, electronics, digital imaging, and, most importantly to me, audio.

There are two benefits to using the decibel. The first is that switching to a logarithmic scale converts an awkward range of values (e.g., 0.0002 Pascals – 20 Pascals) to something much easier to reason about (e.g., 0 dB SPL – 120 dB SPL) . The other benefit applies to audio – a logarithmic scale is much closer to how the human ear actually hears. With a linear scale, like Pascals, doubling the value doesn’t usually feel like a doubling of loudness. With decibels, we actually get a scale that’s much closer to how to perceive loudness.

The decibel, in the generic sense, is not strictly a physical unit. When we think of physical units, we typically think about things like Amperes (number of moving electrons over time), Pascals (pressure), meters (distance), Celsius (temperature), etc. These are absolute units that correspond to physical things. The decibel isn’t like that. It’s a relative unit. It provides a relation of two things, which are themselves physical units. And it does this on a logarithmic scale.

The general formula for the decibel is as follows.

The decibel, abbreviated dB, is the logarithmic ratio between two power values. One of these two values is a reference value. The other is a measured value.

You may notice the phrase “power value” in that formula. In physics, this means the amount of energy transferred over time. The unit for power is usually the watt. However, there are plenty of units used that aren’t power values (such as Pascals in acoustic audio). So we have to convert those units into something related to power. This typically just means squaring the measured and reference values. The decibel formula ends up looking like so.

With logarithms, we can pull that exponent out and turn it into a multiplication.

This can be simplified even further like so.

And this is the formula you’ll most likely encounter when applying the decibel to measured and reference units which aren’t power-based (like Pascals in acoustic audio). It’s just a derivation of the original formula with the measured and reference values tweaked.

Standard Reference Values

A lot of domains, such as electronics and audio, have standardized reference values for the various things being measured. When we talk about these standardized flavors of the decibel, we add suffixes to the dB abbreviation. Examples of this are dBV (voltage based), dBm (radio power), dBZ (radar power), etc. The one we’re most concerned with in the field of acoustic audio is dB SPL.

dB SPL

dB SPL is the most common flavor of decibel for indicating the loudness of acoustic audio. SPL stands for sound pressure level. The reference value used in calculating dB SPL is the threshold of human hearing – 0.000020 Pa. We plug this into the decibel formula along with a measured value, also in Pascals, to come up with a dB SPL value.

A measurement of 0 dB SPL is considered the threshold of human hearing. That is, it’s the quietest sound that the human ear is capable of hearing. On the upper end of the scale, somewhere between 130 dB SPL and 140 dB SPL, is what’s referred to as the threshold of pain. When the volume of sound approaches this level, it can result in physical discomfort and some amount of hearing loss is almost certain.

The following two tables shows some common sounds and their approximate sound pressure measurements. The first table shows measurements in Pascals. The second table shows them in dB SPL. Compare them and you’ll see that dB SPL is much less awkward to use.

Sound SourceDistance from EarPascals
Jet Engine1 meter632
Threshold of PainAt ear20 – 200
Yelling Human Voice1 inch110
Instantaneous Hearing Loss Can OccurAt ear20
Jet Engine100 meters6.32 – 200
Chainsaw1 meter6.32
Traffic on a Busy Road10 meters0.2 – 0.63
Hearing Loss from Prolonged ExposureAt ear0.36
Typical Passenger Car10 meters0.02 – 0.2
Television (typical volume)1 meter0.02
Normal Conversation1 meter0.002 – 0.02
Calm RoomAmbient0.0002 – 0.0006
Leaf rustlingAmbient0.00006
Threshold of HearingAt ear0.00002
Sound Pressure Measured in Pascals, “Sound pressure” Wikipedia: The Free Encyclopedia. Wikimedia Foundation, Inc, 22 July 2004, https://en.wikipedia.org/w/index.php?title=Sound_pressure&oldid=1112496481. Accessed 29 Nov. 2022.

Sound SourceDistance from EardB SPL
Jet Engine1 meter150
Threshold of PainAt ear130-140
Yelling Human Voice1 inch135
Instantaneous Hearing Loss Can OccurAt ear120
Jet Engine100 meters110-140
Chainsaw1 meter110
Traffic on a Busy Road10 meters80-90
Hearing Loss from Prolonged ExposureAt ear85
Typical Passenger Car10 meters60-80
Television (typical volume)1 meter60
Normal Conversation1 meter40-60
Calm RoomAmbient20-30
Leaf rustlingAmbient10
Threshold of HearingAt ear0
Sound Pressure Measured in dB SPL, “Sound pressure” Wikipedia: The Free Encyclopedia. Wikimedia Foundation, Inc, 22 July 2004, https://en.wikipedia.org/w/index.php?title=Sound_pressure&oldid=1112496481. Accessed 29 Nov. 2022.

There are instruments available that measure sound pressure levels and report dB SPL. One such instrument is shown below. This happens to be my personal db SPL meter.

These devices are fun to take to concerts or demolition derbys if for no other reason than giving you the intellectual authority to complain about permanantly damaged hearing.

Conclusion

Hopefully, this article has helped demystify the decibel. Mathematically, they’re not something to be feared. It’s usually the logarithms that scare folks away. And if you’ve long since forgotten how logarithms work, go brush up on them and come back to this article a second time. It will make a lot more sense.

If you found this content useful, or if something could have been explained better, please leave me a comment below.

Until next time.

Digital Audio with the DFPlayer

My oldest daughter and I recently built one of Mr. Baddeley’s Baby R2s. These units are small, almost cartoonish, radio-controlled R2D2s that are extremely easy to build. And fun! But something missing from the design (at least at the time of this writing) is the ability for these little guys to produce sounds. And what’s an R2D2 with his signature beeps and boops?

For my life-size R2, I incorporated an MP3 Trigger from Sparkfun and paired that with an Arduino. But I couldn’t use that here because the MP3 Trigger is too large. The Baby R2s just can’t accommodate it. So I went in search of something else. And that’s when I came across the DFPlayer from DFRobot.

In this article (the first of three), we’ll be exploring the DFPlayer and beginning our journey into ultimately using an RC radio to trigger audio. If that’s not something that interests you, no worries. This article is focused entirely on the DFPlayer.

DFPlayer

DFRobot’s DFPlayer is a tiny (~ 21mm x 21mm) module that’s capable of playing MP3, WMV, and WAV audio data. It features a micro-SD slot for your audio files. It can be connected directly to a small speaker (< 3 W) or an amplifier. And it can be controlled a few different ways, including via a serial connection which works well for my particular needs.

Perhaps the biggest selling point for the DFPlayer is its price – $6 as of this writing. Compare that to the SparkFun MP3 Trigger, which comes in at around $50. The DFPlayer is practically a guilt-free impulse buy. In fact, I picked up a 3 pack from Amazon for around $10.

One of the downsides to this module is that there are various models that exhibit different tolerances to electrical noise, which means you might struggle with an audible hiss. Killzone_kid posted a great writeup on the Arduino forums that examines some of the models and makes some recommendations on possible ways to mitigate the hiss. Some of the flavors also apparently have compatibility issues with the officially supported DFPlayer Arduino library, which we’ll look at in a bit.

Here’s the pin diagram for the DFPlayer.

Left-Side Pins

There’s a Vcc pin and a ground pin as you might expect. These pins are used to power up the device. The source voltage must be between 3.2V and 5V.

The Serial RX and TX pins are used to provide a serial interface for controlling the device. It defaults to 9600 baud, 1 data bit, no check bits, no flow control. The serial protocol is detailed in the datasheet. But there’s also an official support library for Arduino called DFRobotDFPlayerMini that implements the serial protocol and provides a high-level interface for controlling the DFPlayer.

The Amp Out pins are for connecting to an audio amplifier or headphones.

The Spkr pins are for very small speakers – less than 3 watts. This is actually what I’ll be using for my project. I’m connecting the DFPlayer to a small speaker that I harvested from one of my kids’ annoying…er, I mean, broken toys.

 

Right-Side Pins

On the right-hand side of the pin diagram, you’ll see pairs of pins for I/O and ADKey. These are two other mechanisms for controlling the DFPlayer. We’ll use the I/O pins to test the DFPlayer shortly. But we won’t be using the ADKey pins at all. I won’t be discussing them further. If you want to learn more about them, I advise you to check out the DFPlayer Wiki.

The USB pins allow the DFPlayer to work as a USB device. I haven’t been able to find very much information about this. It’s not discussed much in the DFPlayer manual. Apparently, it provides a mechanism for updating the contents of the SD card from your PC. This could be handy for projects where the DFPlayer ends up hidden away inside of an enclosure that can accommodate a USB connector on the outside. For my project, I don’t need it so I won’t be exploring it. However, if anyone knows where I can find more information about this, please let me know. It could come in handy on another project.

The pin labeled “Playing Status”, referred to as the “Busy” pin, is normally high when the device is idle and goes low when the device is playing audio. The device already has a small LED that lights up when it’s playing files. But if that’s not good enough, you can connect an LED to this pin, or connect it to a microcontroller for more sophisticated behaviors.

Adding Media Files

The DFPlayer manual describes the naming of folders and files on the SD card. With regards to files, it specifies names using 3 digit numbers, such as 001.mp3, 002.wav, etc. Folders can be named similarly. I didn’t actually create any folders on my SD card, and it worked just fine. My file layout looks like so.

Testing the DFPlayer

Before doing anything else, I like to do a quick smoke test. This simply involves powering up devices straight out of the box (if feasible) and seeing if any magic blue smoke appears. I also like to note if any LEDs light up, as there’s often a power LED that will indicate the device is at least turning on. In this case, nothing happened. After a bit of reading, I learned that the device’s single LED only lights up when it’s actually playing media. So at this point, I wasn’t sure if it was even powering up. My benchtop power supply said the DFPlayer was pulling a small amount of current, so something was happening.

Next, I wanted to see if I could get some sound out of the device. I plugged in my micro SD card and connected my speaker. The I/O pins (9 and 11) were the key to this. Grounding I/O pin 1 for a quick duration will cause the DFPlayer to play the “next” track. Grounding it for a long duration lowers the audio level. Grounding I/O pin 2 for a quick duration will cause the DFPlayer to play the “previous” track. Grounding it for a long duration raises the audio level.

I grounded I/O pin 2 for a couple of seconds to get the audio level all the way up and then quickly grounded I/O pin 1 with a quick tap. The device’s LED lit up and I immediately heard some beeps and boops. The device was working. Success!

Now I knew any issues I might have trying to drive it over a serial connection would be limited to my code and/or the serial interface itself.

Connecting the Arduino

For this project, I used a Nano clone from Lavfin. These come with the pins pre-soldered. When I originally bought this, you could get a pack of 3 from Amazon for around $14. The price has since gone up to $28 as of this writing (presumably, because of supply chain issues).

For testing, I’m used a Nano expansion board. This provides convenient screw terminals.

 

I connected the DFPlayer to the Arduino using the serial pins of the DFPlayer and digital pins 10 and 11 of the Arduino. The DFPlayer’s RX pin connects to the Arduino’s digital pin 11. The DFPlayer TX pin connects to the Arduino’s digital pin 10.

Why didn’t I use the Arduino’s serial pins? I could have. But the process of writing new code to the Arduino makes use of the UART. So I’d have to disconnect and reconnect the DFPlayer every time I wanted to update the software.

I don’t recommend connecting the DFPlayer Vcc and ground pins to the Arduino’s 5v and ground pins unless you REALLY need to leverage the Arduino’s voltage regulator. This is how I’ve seen it wired in the online examples. It’s convenient, sure. But the Arduino has current supply limitations. Having the DFPlayer and the Arduino connected independently to the power source is the better option.

This is how my DFPlayer and Nano are wired together.

The source code for my Arduino/DFPlayer test is as follows.

#include "Arduino.h"
#include "DFRobotDFPlayerMini.h"
#include "SoftwareSerial.h"
 
static SoftwareSerial g_serial(10, 11);
static DFRobotDFPlayerMini g_dfPlayer;
 
/**
 * Called when the Arduino starts up.
 */
void setup()
{
    // We'll use the built-in LED to indicate a communications
    // problem with the DFPlayer.
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW);
 
    // Let's give the DFPlayer some time to startup.
    delay(2000);
 
    g_serial.begin(9600);
 
    if (!g_dfPlayer.begin(g_serial))
    {
        // There's a problem talking to the DFPlayer. Let's turn on the LED
        // and halt.
        digitalWrite(LED_BUILTIN, HIGH);
        while(true)
        {
            delay(0);
        }
    }
 
    // Valid values for volume go from 0-30.
    g_dfPlayer.volume(20);
    // Plays the first file found on the filesystem.
    g_dfPlayer.play(1);
}
 
// Called over and over as long as the Arduino is powered up.
void loop()
{
    static unsigned long timeLastSoundPlayed = millis();
 
    // We're going to iterate through the sounds using DFRobotDFPlayerMini's next() function,
    // playing a new sound every five seconds.
    if ((millis() - timeLastSoundPlayed) &gt; 5000)
    {
        g_dfPlayer.next();
        timeLastSoundPlayed = millis();
    }
 
    // Consumes any data that might be waiting for us from the DFPlayer.
    // We don't do anything with it. We could check it and report an error via the
    // LED. But we can't really dig ourselves out of a bad spot, so I opted to
    // just ignore it.
    if (g_dfPlayer.available())
        g_dfPlayer.read();
}

To build this sketch, the DFRobotDFPlayerMini library must be installed. This can be downloaded from GitHub –
https://github.com/DFRobot/DFRobotDFPlayerMini. Installing it is as simple as extracting it to the Arduino libraries directory (e.g., C:\Program Files (x86)\Arduino\libraries).

Line #5 creates an instance of the SoftwareSerial class. I call it g_serial. This is used to emulate a UART over digital pins. I reserved the hardware UART so I could update the software on the Arduino while being connected to DFPlayer. If you’d rather use the hardware UART and the Serial global variable, that’ll work fine too. You just have to disconnect the DFPlayer every time you need to update the code. The two arguments to the SoftwareSerial constructor are the pin numbers for RX and TX, respectively.

Line #2 of the source above includes the DFRobotDFPlayerMini header file, which brings the various DFRobotDFPlayerMini types into scope. I then declare the static global instance of DFRobotDFPlayerMini called  g_dfPlayer. This will be the object we use to interact with the DFPlayer.

The first thing the setup() function does is configure the Arduino’s on-board LED. Since I’m not actually using the Serial Monitor in the IDE, I wanted to light this up if there were problems initiating communications with the DFPlayer.

I then delay execution for 2 seconds to give the DFPlayer time to startup. This is important to note because I didn’t see this happen in any of the sample sketches that cames with the DFRobotDFPlayerMini library. Without the delay, things just wouldn’t work for me. I don’t know if it’s because of my particular flavor of DFPlayer and/or Arduino. But 2 seconds seems to be the delay I need to get the devices to talk to one another.

I call begin() on g_serial with an argument of 9600 baud since this is the baud rate supported by DFPlayer. I then attempt to start communication with the DFPlayer by calling g_dfPlayer’s begin() function, passing it the serial object I want it to use. If an error occurs, I light up the LED and effectively halt. If no error occurs, I crank the volume up to 20 (out of 30 max) and play the first file on the DFPlayer’s file system. If all things are well, I should hear a sound.

In the loop() function, we do two things – 1) check to see if it’s been 5 seconds since we last played a sound and, if so, play one and 2) eat up any data sent to us by the DFPlayer. I should point out that I don’t actually know if we need to consume data if we’re not doing anything with it. But without diving too deep into the DFRobotDFPlayerMini code, I’m erring on the side of caution and hoping to keep some buffer somewhere from filling up.

Once this code was compiled and flashed to the Arduino, I had to restart both the Arduino and the DFPlayer. After a couple of seconds, I started hearing more beeps and boops.

Wrapping Up

This code is a good starting point. I encourage you to explore the sample code that accompanies the DFRobotDFPlayerMini library. There are some good nuggets there.

In the next article, I’ll be focusing on interfacing a radio controller with the Arduino. And then a third article will follow that which will tie everything together so that we can trigger our audio with a radio.

Until next time…