You are on page 1of 9

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out

PIC32 BASED AUDIO PLAYER WITH MICROSD STORAGE


SYED TAHMID MAHBUB
JANUARY 2015
http://tahmidmc.blogspot.com/2015/01/audio-player-using-pic32-microsd-card.html
INTRODUCTION
The PIC32 series of microcontrollers come with a powerful processor, lots of memory and a
very rich set of peripherals that make it ideal for a wide range of applications. One such application that
I wanted to have fun with is audio playback. Initially I made a simple audio player where I hard-coded
the audio in a header file and played it back. Obviously this had its drawbacks, specifically with
storage, and so I wanted something more flexible. That led me to exploring the use of a microSD card
in the project. I had an 8GB microSD card lying around that I could use for this project. So, I decided
to build a simple WAV player using that.
I had used the internal PWM module for audio playback but this time I wanted better audio
output and so decided to use the MCP4822 12-bit dual DAC that I had with me. I have previously
talked about it in my articles:
PIC32 SPI: Using the MCP4822 12-bit serial DAC
PIC32 DMA+SPI+DAC: Analog output synthesis with zero CPU overhead
The WAV player I've made supports PCM (pulse code modulation) format WAV files. I've tested
with 16-bit 44,100Hz audio files and the output sounds great. It works with 8-bit audio files as well but
getting that to work has (oddly) been particularly difficult. While I still haven't been able to figure out
what exactly I was doing wrong with the 8-bit part, I managed to find a solution to the problem and that
is what I have for my WAV player.
For now, the user interface is minimalistic and is done through the serial port. I didn't intend this
project to be a full-fledged music player project but just a learning experience and so did not enhance
the UI much further than necessary to select a song and play it back.

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out
PARTS
The circuit itself is very simple and consists of the following sections:

PIC32MX250F128B This is the heart of the project and is the central microcontroller

that interfaces with all the different sections. The PIC is operating off of the internal oscillator
(FRCPLL configuration) at 40MHz, with the peripheral bus clock also at 40MHz.

MicroSD/MDDFS This is the storage section of the project. The interface is done

using a PIC SPI module SPI1 specifically. A critical part in the project is Microchip's open-source
file-system library MDDFS. This is part of the Microchip Libraries for Applications (MLA). I have
found it very useful and fairly easy to use it comes with good documentation and demo code, but just
takes a little bit of effort to get going. I used v2013-06-15 (part of the legacy MLA).

Audio section The DAC I used is the MCP4822. It is a dual 12-bit DAC

communicating with the PIC using SPI SPI2 for the DAC. The SPI clock frequency I used was
20MHz. The two outputs of the DAC have capacitors for AC coupling and have current-limiting
resistors.

Serial port UART2 of the PIC is used for the serial port interface. Since my computer

does not have a physical serial port, I used a PL2303-based serial-to-USB converter (Adafruit product
954).
WAV AND AUDIO PLAYBACK
The audio format to be played back is WAV (PCM format). This is uncompressed audio. There
are two sections to the WAV file: the header and the actual data. The header is (usually) the first 44
bytes in the file. The rest of it is the actual audio data. Once the WAV file is opened (I used the
FSfopen

function provided as part of MDDFS library), the first byte will read 'R' (the ASCII

equivalent of R ie 82 in decimal); the next three will read 'I', 'F' and 'F'. Here you can find a table
describing the header file: http://www.topherlee.com/software/pcm-tut-wavformat.html
Multi-byte entries are little-endian. So, consider bytes 5-8 that give the file size. The actual file
size is given as [byte8:byte7:byte6:byte5] = (byte8) << 24 + (byte7) << 16 + (byte6) << 8 + (byte5).
In my code, after the WAV file is opened, the header is checked. If there is something wrong
with the header or in terms of compatibility (eg my code only runs if WAV format is PCM) the file
is obviously not played back. After that the parameters are read in bits per sample, number of
channels, data size, sample rate and block align and printed.

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out
Now on to the actual data. That depends on the number of channels and the bits per sample. The
first key thing to remember is that for 16-bit PCM files, the data is stored as 16-bit SIGNED samples,
whereas for 8-bit PCM files, the data is stored as 8-bit UNSIGNED samples.
The way the audio data is organized is illustrated below for 8-bit and 16-bit mono and stereo
audio files (each block is a byte; the leftmost block is the lowest-numbered byte):
8-bit MONO files (block align = 1):
SAMPLE 1

SAMPLE 2

SAMPLE 3

SAMPLE 4

...

8-bit STEREO files (block align = 2):


SAMP1 SAMP1 SAMP2 SAMP2 SAMP3 SAMP3 SAMP4 SAMP4 SAMP5 SAMP5
LEFT

RIGHT

LEFT

RIGHT

LEFT

RIGHT

LEFT

RIGHT

LEFT

...

RIGHT

16-bit MONO files (block align = 2):


SAMP1 SAMP1 SAMP2 SAMP2 SAMP3 SAMP3 SAMP4 SAMP4 SAMP5 SAMP5
LOW

HIGH

LOW

HIGH

LOW

HIGH

LOW

HIGH

LOW

...

HIGH

16-bit STEREO files (block align = 4):


SAMP1 SAMP1 SAMP1 SAMP1 SAMP2 SAMP2 SAMP2 SAMP2 SAMP3 SAMP3
LEFT

LEFT

LOW

HIGH

RIGHT RIGHT
LOW

HIGH

LEFT

LEFT

LOW

HIGH

RIGHT RIGHT
LOW

HIGH

LEFT

LEFT

LOW

HIGH

...

For the audio playback, I managed two cyclic buffers, each 128 points: LSTACK for the left
channel and RSTACK for the right channel. The audio file is read and then the samples are converted to
12-bit values to be stored in the buffers (along with channel information for the DAC). For mono files,
both channels play the same values. For 8-bit samples, the 12-bit data is obtained by bit-shifting left 4
times. For 16-bit samples, the 12-bit data is obtained using the 8 bits of the HIGH byte and 4 bits of the
LOW byte. For 16-bit samples, the SIGNED 12-bit values are then converted to UNSIGNED values
by centering about 2048 (because 2048 = 212/2) instead of 0.
Using the sample rate (obtained from the WAV header), the period register for timer 2 is
configured (PR2). The timer 2 interrupt is enabled and in the ISR, the required values from LSTACK
and RSTACK are output to the DAC. The cyclic buffers are used to ensure smooth audio playback.

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out
TOS is used as the Top Of Stack to indicate the position from where to retrieve the audio value to
push to the DAC. BOS is used as the Bottom Of Stack in the section where the audio data values are
read; BOS indicates the position onto which a new value is being pushed into the buffer. It was
determined, by testing, that 128-point stacks/buffers are enough to ensure smooth continuous audio
playback.
SERIAL, USER INTERFACE
The PIC32 UART module (UART2 since, by default, that's the one for printf) was used for the
serial port interface for the user interface. The used baud rate is 115,200. The scanf function does not
seem to work by default, so instead I just wrote simple functions to get user input when required.
I used the PL2303-based serial-to-USB converter from Adafruit (product 954). The white wire
on the converter (RX in) connects to the TX out from the PIC. The green wire (TX out) connects to the
RX in to the PIC. The black wire (VSS) connects to ground. The red wire (Vsupply) is left
unconnected.
Library functions from the PIC32 peripheral library (plib) are used to configure the module.
The user interface is minimalistic. Initially it prompts the user to enter the required character,
which I set as s (as in s for start). Once that is pressed, the microSD card is examined (in the root
directory) and all the WAV files present are displayed along with the corresponding file sizes. Then the
user is prompted to type the filename for the desired song. If the correct song is entered, playback
starts; otherwise the user is prompted again to enter the correct song. Once the song ends, the user is
shown the duration of playback with a message that it's done. At that point, the LED (which is initially
set high) starts rapidly blinking, and code execution is effectively complete. The reset button should be
pressed to repeat the process.
MDDFS
The Microchip Disk Drive File System library is simple to use and is part of the Microchip
Libraries for Applications (MLA) available for download for free on the Microchip website. Instead of
going into detail about the library, I'll just explain the specific parts I used in my project. Microchip
provides additional documentations if you're interested in learning more.
First, the required files. The only files from MDDFS that are needed for my project (and that I
have uploaded) are under the three folders MDD_includes, MDD_sources, PIC32. The required

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out
contents of the 3 folders are:
MDD_includes:

FSDefs.h,

FSIO.h,

MDD_sources:

FSIO.c ,

SD-SPI.c

PIC32:

Compiler.h,

FSconfig.h,

SD-SPI.h
HardwareProfile.h

Now on to the function explanations.

The first function I used is MDD_MediaDetect(). This is used to detect if the memory device
(microSD card in my case) is present in the microSD card connector. Several
holders/connectors provide a pin which can be tested to see if a card is inserted. However, mine
didn't have such a pin. So I went into the source file where this function was written and edited
the function so that it always returned 1. I kept it in place in case I was to use a connector that
had the card detect pin (in which case I can just go back to the source file and undo my
changes).

The next function I used is FSInit(). This initializes the file system. It returns FALSE (defined
as 0) if the initialization fails, and TRUE (defined as !FALSE) otherwise.

To search for all WAV files in the root directory (which is the default directory upon
initialization) I used two functions. The first one is FindFirst("*.WAV", attributes, &rec). The
function has three arguments the first one being the filename; the second is attributes and the
third is a pointer to a SearchRec structure (defined within the library). The function searches for
the first file with the specified filename and attributes, and returns 0 if a file is found; it returns
-1 otherwise. If a file is found, the resulting find results are stored in the SearchRec structure rec
which contains properties such as filename, filesize, attributes, timestamp, etc. In my project I
make use only of the filesize parameter to display the filesize of the found file. attributes is a
UINT8 type variable that can specify one of several attributes in my program I specified
ATTR_MASK, which says that the file can have any attribute. The way I have filename
specified, the function searches for the first WAV type file. If the filename was *.*, the
function would search for the first file (any filename, any filetype). If the filename was
SONG*.WAV, the function would search for the first WAV type file starting with the letters
SONG.

Once the first file is found, it is possible to search for other files using the FindNext(&rec)

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out
function. This function searches for other files with the same filename and attributes as used for
the FindFirst function. The resulting information is, as with FindFirst, stored in the SearchRec
structure rec. The function returns 0 if a file is found; -1 otherwise.

To open a file for reading, the function FSfopen(filename, r) is used. r can be replaced
with w to open the file for writing. Given a specified filename, the function returns NULL if
opening failed; otherwise it returns a pointer (type FSFILE defined in the library) used later in
my code for the FSfread function mentioned below.

To actually read data from the opened file, the function used was FSfread(buffer, sz, n,
pointer) is used. The parameter pointer is that returned by FSfopen mentioned above. The
parameter sz tells the function the size of a given item to read; the parameter n tells the function
how many of these items to read. The read values are stored in the provided buffer (the pointer
to which is passed to the function). The function returns the number of read items. I used sz = 1
so that one item was one byte and the number of returned items was equal to the number of
bytes read.

Once all file operations end, the file is closed with the function FSfclose(pointer), pointer being
the same as that mentioned above for FSfopen (returned by FSfopen).
I made some changes to the default provided files and the files I provide are inclusive of my

changes. Some important points are listed below:

In HardwareProfile.h, I added a #define RUN_AT_40MHZ along with corresponding


GetSystemClock(), GetPeripheralClock() and GetInstructionClock() macros. This is because I
am running my PIC at 40MHz, but there was not definition in this file for 40MHz.

From line 528 in HardwareProfile.h are the hardware-specific definitions that are of interest to
us, in relation to the use of MDDFS for the specific PIC32. Here I defined the use of SPI1 for
MDDFS and the specific SPI pins required. Note that even though I have pins and directions
defined for CD (Card Detect) and WE (Write Enable) I don't make use of these in my
program. I mentioned above regarding the change in code for CD. For WE, in the function
MDD_SDSPI_WriteProtectState in SD-SPI.c, I just had it return 0 to indicate that my card was
not write protected.

In the file SD-SPI.c, in the function MDD_SDSPI_InitIO, I defined the pins to be used for
SDO1 and SDI1. Note that SCK1 and SCK2 are not peripheral pin-selectable.

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out
USER FUNCTIONS
Several user functions were utilized in this project.

The functions initDAC and setupUART are self explanatory: initDAC sets up the SPI2 module
for use with the DAC; setupUART sets up UART2 for use for the serial port interface.

The function infiniteBlink is used at the end of program execution. All it does is toggle the
LED infinitely (as the name implies) with a small software delay so that the blinking is visible
by the naked eye.

The function setupAudioPWM essentially clears the timer 2 control register and sets the
corresponding interrupt priority.

The function setupSystemClock configures Timer 3 so that its period is 1ms. This is a system
clock of sorts used for higher level time-keeping (since its resolution is 1ms). The
corresponding timer interrupt and priority are set.

The function void getFilename(char * buffer) fills the character array (string) buffer with the
filename. This is called when the user is prompted to type in the filename of the song to play
back. The parameter passed is the pointer to the string.

The function void configureHardware(UINT32 sampleRate) sets up the period register for
timer 2 (PR2) so that the period is equal to the sample rate of the audio file. The sample rate is
read off the WAVE header and passed as the parameter to this function.

The function inline void writeDac(UINT16 dacVal) writes the required value, dacVal, to the
DAC digital input through the SPI2 interface. It is implemented as an inline function to
eliminate function call overhead since the function itself is fairly short. It lowers the slave select
(SS) line, checks for the transmit buffer to be empty, loads the buffer, waits for the completion
of transfer and then finally raises the slave select (SS) line.

The function UINT8 getWavHeader(FSFILE * pointer) checks the WAV header to ensure that
the corresponding audio file is supported. The parameter provided to the function [FSFILE *
pointer] is the pointer to the character array that contains the header information. The function
returns a zero if everything is ok; if not, it returns an error code indicative of what type of error
occurred (incorrect header or an unsupported WAV file type).

The function getParameters checks the read header file (a global character array) and retrieves
the bits per sample, number of channels, data size, sample rate and block align. The
function parameters to be passed are the pointers to the above parameters.

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out

The function zeroDacs writes to the DAC to set the output to 1.024V (half of the voltage swing
range of the used DAC), effectively the zero point of the output audio. While the absolute value
assigned is not significant due to the output AC coupling, half-range is used for consistency.

The ISRs (interrupt service routines) are the Timer 2 ISR and Timer 3 ISR. In the Timer 3 ISR,
all that is done is that a millisecond counter is incremented and the corresponding interrupt flag
is cleared. In the Timer 2 ISR, the audio playback happens. This is where the audio information
from the cyclic buffers is output to the DACs.

CONNECTORS/HEADERS
There were 4 headers/connectors used in this project: the microSD card connector/holder, the
3.5mm stereo audio connector, the ICSP (in circuit serial programming) header and the 3.3V serial-toUSB converter (http://www.adafruit.com/product/954).
Since the current requirement was very small, I used the PICKIT3 to power the microcontroller
with no external power supply. The test circuit was designed on a solderless breadboard/whiteboard and
ran without any issues.
NOTE AND IMPROVEMENTS
Dev environment: For development, I used MPLABX IDE v2.26, XC32 compiler v1.33 on
Ubuntu 14.04 LTS. The serial terminal I used was Gtkterm v0.99.7-rc1. In the MPLABX project
settings, optimization level was set to 1.
The project that I present is done as a hobby project, purely for educational purposes, as a proof
of concept. I recognize that the code may be improved for better efficiency and for smaller memory
footprint. However, this served all the educational goals I had hoped to meet and I thus present it, open
source, to you so that it may benefit you.
If you have suggestions or comments regarding the project and/or the documentation, feel free
to mention in the comments section. Feel free to make any changes to the project for your own needs;
all I ask is that I be given credit for my work. Let me know what you think!

SYED TAHMID MAHBUB PIC32 based audio player with microSD card and 12-bit audio out

DOWNLOADS

Entire project folder, including all source files, header files, and schematic:
https://drive.google.com/file/d/0B4SoPFPRNziHRGE0bVNZYV9uVjQ/view?usp=sharing

USEFUL LINKS

Microchip Libraries for Applications

AN1045: Implementing File I/O Functions Using Microchip's Memory Disk Drive File System
Library

You might also like