This Instructable will be about designing a music player from using various building blocks. You will understand the communication between the microcontroller, memory, computer, LCD display, RTC, IR remote, and the music file decoder. I will try my best to to teach you in a way so that you can design your own projects using the skills you learn, without blindly following instructions.
I know most of you will simply glance at this first page and maybe skim through the rest. This Instructable has 18 steps and 5 appendices, with about 90 files and pictures. I sincerely hope you explore all my efforts.
Every step will be accompanied by a demonstration of that particular building block working. The source code will be provided. I will post the debug output, pictures, screenshots, USB device and packet analysis, and logic analyzer waveforms. NOTE: if the images look too compressed, don't worry, they are included inside my .ZIP files too.
To start off the project, set your goals. This will be a simple proof-of-concept music player. It will allow the user to load music as through USB as though it is a mass storage device, display the current song to the user, display the current time, set custom alarms for every day of the week, and allow the user to control it through a remote control. To accomplish these goals, you need:
* USB capable microcontroller
* LCD display
* Sound output
* IR receiver and remote control (any)
This is the obvious overview, however, we also need a RTC (real time clock) to keep track of time using a backup battery, just in case the power goes out.
Note that with my collection of supplies, budget, and skills, I've decided to use a VS1033D decoder IC from VLSI Solutions, which integrates music file decoding and digital-to-analog output. So the item "sound output" in the above list expands into "decoder" and "speaker"
I will be using the following components during this Instructable (this is not a full part list, not even close, but these are major):
AT90USB1286 microcontroller (on a Teensy++ http://www.pjrc.com/store/teensypp.html ), datasheet is here: http://www.atmel.com/dyn/products/product_card.asp?part_id=3874
VS1033D music decoder http://www.vlsi.fi/en/products/vs1033.html on a breakout board http://www.sparkfun.com/products/8792
16x2 character LCD display, ST7066/HD44780 compatible, using 3.3V instead of 5V
DS1307 real time clock
Note that the entire circuit will run off 3.3V, if you are buying a Teensy or Teensy++, please buy http://www.pjrc.com/store/mcp1825.html and follow the correct procedures to solder it and use it (it involves a jumper). Please also note that you must also run the Teensy at 8 MHz instead of 16 MHz because of the reduced voltage.
I also hope that once you are done, you'll be able to apply the skills you learn here with other microcontrollers and devices.
Step 1: Before You Begin
To get you started, I will make sure you know how to compile and upload a "hello world" program to the Teensy++. This code will show you how to output debug messages, which will be useful later.
Obviously you need an AT90USB1286 microcontroller for this, and since it's hard to solder by hand, I choose to buy a Teensy++. http://www.pjrc.com/store/teensypp.html
This example is based on "USB Serial" on PJRC
If you wish to learn more about communication with USB, please refer to my appendix "step" about USB.
Please refer to my appendix "step" about AVRs to figure out how to use makefiles and the GNU AVR toolchain.
Download the files attached. Run "make" to generate the .hex file. Upload the .hex file to the microcontroller. Open up a serial terminal to see the output. The baud rate shouldn't matter since this is a fake serial port.
Provided below is the USB analyzer dump of the device and a sample packet of data, for those of you who wish to learn more about USB.
Note: I personally REALLY like using RealTerm as a serial terminal http://realterm.sourceforge.net/ , I will be posting screenshots of the terminal output whenever I can. I will also post logic analyzer screenshots, .logicsession files (can be opened with the Saleae Logic software http://www.saleae.com/logic/ ), and exported files whenever I can.
Some people have asked me about how to use stdio.h and printf (and similar streaming and formatting functions) on AVR microcontrollers, the following links are in the code comments:
Also since this is "before you begin", go download Saleae Logic's software, if I ever attach .logicdata files, you need the software to view it. http://www.saleae.com/logic/ , it's in the downloads (version 1.1.14 is what I used) page, you can use it even if you do not own a Saleae logic analyzer. I will also try to include screenshots.
Step 2: Storage With MicroSD Card
SD and MMC cards are easy to use because they provide a SPI (serial peripheral interface) interface that can be used to read and write data to and from the card. Please read the following resources to understand SPI and the SD card:
AT90USB1286 Datasheet section 17 http://www.atmel.com/dyn/products/product_card.asp?part_id=3874
If you don't read the above three links, you will not know what I'm talking about next.
In short, the SPI bus is a bus where you place data onto the data lines (MISO and MOSI) one bit at a time, and the bit is sampled on the edge of a clock signal.
Our microcontroller has a dedicated SPI peripheral. By examining the above links I provided, we know the following facts:
Our microcontroller is the "master" and the SD card is the "slave"
The SD card uses SPI mode 0 (CPHA=0, CPOL=0), this means the clock signal starts low and the data input samples data when the clock transition to high
The maximum clock speed of the SPI bus
From the above information, we are able to initialize the dedicated SPI peripheral within the microcontroller. Refer to section 17 of the AT90USB1286 datasheet.
If you didn't work out the obvious electrical connections you will need, here's an explaination:
MOSI (Master Out Slave In)
The master refers to the device that generates the clock (the microcontroller), the SD card is the slave. Data on this pin travels from the microcontroller to the SD card. Also known as "DI".
Connect the DI pin on the SD card to the microcontroller's MOSI pin
MISO (Master In Slave Out)
Data on this pin travels from the SD card to the microcontroller. Also known as "DO".
Connect the DO pin on the SD card to the microcontroller's MISO pin
Chip select, the SD card pays attention to the data traveling on the SPI bus when this pin is low, and ignores the data on the bus when this pin is high. This is also known as "SS".
The CS pin on the SD card can be connected to any free pin on the microcontroller
SCK or SCLK or CLK
This is the serial clock pin,
Connect this pin on the SD card and microcontroller's SPI clock in (called SCK in the datasheet)
The next step will take you through a step-by-step that shows you the basics of communicating to a SD card. As preparation, if you do not already have a good MicroSD card holder, then take some male pin headers with 0.1" spacing, and solder it to a MicroSD card adapter, as shown in the pictures below. The steps to make this makeshift card holder is in my appendix.
Step 3: Communicating With SD Card Example
Warning, if you read the SD card specifications I linked you in the previous step, you will realize that SD cards run at 3.3V, and thus, using the Teensy++ at 5V may damage your SD card (however, this is unlikely, but we like to be safe and rule out reasons for failure).
Make the wiring connections like in the diagram provided.
The code is provided in the attachment below. You should study the source code while examining the SD card specifications I've linked to in the previous step. This way, you can make the mental connections between the commands I'm sending and what they are in the specifications, and then understand what my code expects to receive verses what the specification says what I should receive.
A logic analyzer session file is also provided for you to look at. It will show you the electrical signal waveforms during SPI communication between the microcontroller and the MicroSD card.
Step 4: FAT File System With MicroSD Cards
The FAT file system is quite complex and so we will be using FatFs from Elm-Chan.org to help us.
FatFs is completely written in C and is platform independent, while designed so that it can be configured so it's friendly with limited memory environments (such as microcontrollers). This makes it a perfect solution.
To integrate FatFs into your software project, simply have the files in place, compile "ff.c" (taken care of by the makefile), place the line
into where ever you need to use FatFs API
Also you need to provide a "diskio" module so FatFs can interface with the SD card. "diskio" will contain various methods that uses SPI to allow FatFs to read and write data to and from the SD card.
Note that the SD card should be formatted FAT16. Use whatever operating system you are using to do that. Try not to use FAT32 or SDHC cards (or cards bigger than 2GB) because they don't work with many DIY SD card solutions.
The next step in this instructable contains the demonstration source code for the Teensy++ that will read all the files on your SD card and display them through the serial terminal.
Step 5: Reading a File From SD Card Example
Create several text files and blank folders on the root of the card. Give the text files some content.
Insert the card into our makeshift MicroSD card holder.
Study the source code I've provided, it should read each file name and then output the contents of the file. Please refer to the FatFs API documentation while exploring the source code to understand how to iterate through files inside a directory, and how to open a file.
Compile the attached code and upload and watch the output inside the serial terminal. It should visit all the files in your SD card and then output them to the serial terminal.
Step 6: USB Mass Storage With MicroSD
Then, to understand USB mass storage
What I need you to understand is how the computer determines what kind of device is connected (so understand USB descriptors), and then understand that the computer will issue SCSI commands via USB to the microcontroller, and the microcontroller will execute those SCSI commands by communicating with the SD card. SCSI works directly on the raw memory of the SD card, without caring about the file system. It does not care about files, only bytes.
The AT90USB1286 is capable of full speed USB (not high speed!! keep this in mind as file transfer speeds will not be the best, plus our Teensy is only running at 8 MHz and the SPI clock is only at 4 MHz). To use its built-in USB, we have a few choices. Manually program an USB stack in C, or use the USB stack code provided by Atmel, or use LUFA (Lightweight USB Framework for AVRs).
We will use LUFA (version 101122 as of the time of me writing this), it's open source and it's design specifically for this particular family of AVR microcontrollers.
Download and explore LUFA's code, documentation, and examples.
We will utilize the the mass storage demonstration included in the LUFA distribution. However, that particular demo uses a dataflash IC instead of SD card. So here's an example by Elastic Sheep which uses SD cards:
BUT WAIT THERE'S MORE! I have modified the files from the above link to work with the Teensy++ and updated it to use LUFA version 101122. See attached example package. As usual, simple compile and upload the code. Inside the package I've also included an entire description of the USB device dumped from an USB traffic analyzer. If this worked, then you have effectively just built a SD card reader out of a Teensy++, congrats.
The files sd_raw.c and .h are there to communicate with the SD card directly. SDCardManager is there to allow SCSI to access the SD card in a way that SCSI can work without caring what kind of memory it is working with (abstraction, SCSI is higher level). MassStorage handles most USB mass storage device functionality, and uses SCSI according to the commands received from the computer.
Step 7: LCD Display Basics
This is the exact model I have used: http://www.sparkfun.com/products/9052
You can pick and choose the colour but make sure it's HD44780 compatible and runs at 3.3V
I liked "white on black" because it's readable at night but not disturbingly bright.
The significance of it being HD44780 compatible is because it's common. Everybody knows how to use it. Here are several tutorials by simply searching for "HD44780 tutorial"
Notice how all those tutorials were identical?
Now grab the datasheet for the display (this is from SparkFun's product page):
The data is sent via a parallel bus, the data is placed onto the data pins, and sampled when the "E" pin is toggled.
Take notice of the timing diagrams on page 7. If we do a quick calculation, the Teensy at 8 MHz means the time it takes for one assembly instruction is 125 nanoseconds. The LCD communication code has taken this into account (look for where I've put "_NOP();"). Each command also take a minimum time to execute as well, as you will see.
Take notice of the commands listed on page 10, it will help you understand the initialization code later when you read the source code. Also as I've said before, each command takes a minimum time to execute, and the amount of time is listed here. Also we will not be reading any data from the LCD, only writing, this means the "read/write" or "R/W" pin can be connected to ground, meaning "write only".
Look at the character table inside the datasheet, it's almost the same as ASCII with a few small exceptions. This makes it easy to display text strings.
Also note that we will be using 4 bit mode to save pins and wires. The description of how 4 bit mode works is not exactly on the datasheet but the command called "function set" is what is used to enable 4 bit mode. Notice that 4 bit mode uses bits DB7 to DB4, and "function set" is designed so that DB3 to DB0 are not required to enable 4 bit mode.
To understand more about 4 bit mode, try reading this first
Basically, you send the most significant 4 bits first, and then the least significant 4 bits second. Once the LCD has been set to 4 bit mode, it always expects 2 data transfers, whereas while in 8 bit mode, it only expects 1 data transfer, this is why setting 4 bit mode must be done early, although the commands are designed so that it doesn't have to be the first command.
The contrast is controlled by pin 3 on the LCD, I find using a 10 kilo-ohm trimmer potientiometer as a voltage divider input to pin 3 is the easiest method of controlling contrast.
The backlight LED will be controlled by our microcontroller. There should be a current limiting resistor in series so that the LED or our microcontroller GPIO doesn't burn out.
The next step will be an example demonstration, which will also show you the wiring connections.
Step 8: Using the LCD Display
First, make the connections following the wiring diagram I've provided below. Take notice of the pin numbers, which will also be defined in the code. You will need a 10 kilo-ohm trimmer potentiometer to control the contrast, and a 500 ohm resistor to limit current to the LED backlight.
Compile, upload, and run the code provided in the package. I have also included logic analyzer files to show you what exactly is going on the parallel bus of the LCD.
To summarize the initialization sequence from the file "lcd.c" (refer to commands listed in datasheet):
// setup the pins
// function set, enable 4 bit mode first
// entry mode set: left to right, no display shift
// display control: on, no cursor, no cursor blink
// clear the display
// start at line 1 position 0
Also notice how stdio.h is used for the two independent streams (the LCD stream and the serial debug stream). Read the code comments for more references and explanation.
This example will first print out "hello world", then you can input characters through your serial terminal (if "send keystrokes" is enabled).
I have also provided the waveforms for the signals on the LCD's parallel bus, so you can see the signals and the timing of the signals.
Step 9: Big Number Clock on LCD
We will use custom characters to create big number fonts on the LCD, making it look much better.
The LCD datasheet talks about two RAM sections, DDRAM which is for "display data", meaning the text on display, and CGRAM, which is the "character generator" data, containing the custom font.
So during initialization of the LCD, we load up the CGRAM with some custom characters. The commands we need are the ones needed to set the CGRAM address, and then write to the CGRAM.
So what I've done is create several "block-like" characters, which can be arranged into numbers that are two lines high.
Please see the attached demonstration, it should cycle through the possible numbers for you.
The idea is from http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1213319639
Step 10: Infrared Receiver Basics
Why is it being flashed? Because there are other sources of infrared light in a room, such as light bulbs or even sun light. That's why there's specially designed infrared receivers whose job is to detect change in infrared light at a particular frequency, to distinguish useful data from a remote control from the useless ambient light.
We need to find one of these, so do a search, and make sure it can be powered by 3.3V. The output should be "active low". Try to get one that is does not have an output which is "open collector" or "open drain". The frequency should be 38 KHz, and usually it should also work with anything between 36 KHz and 40 KHz but the range might be shorter as the error increases.
One such example is this one:
Go download its datasheet and read it, remember the pinout, take notice that there's a pull-up resistor shown in the block diagram that basically means it's not "open collector" or "open drain". There's a graph that shows the output signal, and we can conclude from the graph that the output is indeed active low.
For another tutorial, you can check out http://www.ladyada.net/learn/sensors/ir.html
I have wired up a small test setup with a random remote I have laying around. Below are output waveforms of a raw phototransistor along with the output of the TSOP34338. Open the logicdata files (or look at the uncompressed screenshots), notice that the waveform shows that the raw IR light is pulsing at about 37 KHz, and there's a slight delay in the receiver's response.
Also please take notice of how the remote repeats a pattern of codes if a button is held down. I have tested two remotes, one repeats the same pattern all the time. The other sends the command, and then repeats a different pattern to indicate when a button is held down. I will be using the first one since we might run into problems with the second type if we miss the initial pattern.
Step 11: Capturing IR Remote Control Data
First, go open the AT90USB1686 datasheet and read all about the timers. In particular, fully understand timer 1 and its input capture unit. I have an Appendix on timers, please read that.
We will run the timer continuously. The input capture unit will give us a timestamp of when the IR receiver changes its output. This way, we can measure the length of each "on" and "off" pulse.
Just so we are not confused, remember that our receiver is active low, meaning an "ON pulse" is when the output is low, and an "OFF pulse" is when the output is high. The ON and OFF refers to whether or not the IR light is being sent.
We know from the waveform I've captured and shown you in the previous step that one command is repeated over and over again while the button is held down on the remote. We know when this command is repeated because each command starts with the longest "ON pulse".
Knowing these facts, we can successfully capture and match a series of pulses against a known pattern using software. We can simply record the pulses into an array, and when we detect a really long "ON pulse", we know to start to record at the beginning of the array.
All the commands contain the same number of pulses, so we keep count of how many pulses to know when a command has ended. When the command ends, we try to match our recorded array to a known array to see which command was sent, using the process of elimination.
NOTE: This method is not full proof, but I've tried other methods such as doing a comparison in reverse order after every ON pulse (as opposed to after a certain number of ON pulses). The amount of data we need to compare and the slow microcontroller caused the comparison process to take too long, and the microcontroller "missed" some pulses.
So wire up the IR receiver's output to pin PORT-D-4, which is the input capture unit 1's input, and then connect the power and ground of the IR receiver. You do not need a diagram for this, at least I hope... But I've included one anyways.
Take the files I've provided, compile, upload, and run on the Teensy++. Then open the serial terminal and press a button on the remote control. You'll see the pulses listed on the screen. Save the results for each button, paste them into the "IR Code Visualizer" to see the waveform.
I'll show you how to use the results in the next step.
Pop quiz, what is the unit being used to capture and store the IR pulse width values we are using?
The Teensy++ has a 16 MHz crystal, but we are running it at 3.3V instead of 5V, so we are using "clock_prescale_set(clock_div_2);" so it's actually running at 8 MHz. The source code shows the timer 1 being configured to use a "divide by 8" prescaler, so the timer is running at 1 MHz. Thus, 1/1000000 seconds is 1 nanosecond, and that's what the values are captured and stored in.
-- Copied from my Appendix on timers
Step 12: Interpreting IR Remote Control Data
Download and open my source code. You need to fill in a bunch of arrays, and also configure a few definitions.
If you just copy a long list into the visualization tool, then it will be pretty obvious when a pattern repeats, which is why visualization is important.
Use the visualization tool I've provided in the previous step to generate an array for every command on your remote. Fill in "ircodes.h" with the arrays (I've filled them in with my own codes, which WILL NOT WORK FOR YOU, you need to replace them in a similar fashion). The size of the array is the "number of ON pulses" multiplied by 2 (since there's an OFF pulse for every ON pulse).
You need to determine the amount of error tolerance, to know this, just compare all of your data. If an ON pulse is about 600 nanoseconds on average, and the recorded values and 633, 579, 624, then an error tolerance of +/- 50 is probably enough. If the values are 492 and 613 and such and such, then try 100, this might take some trial and error.
The "start pulse threshold" should be shorter than your longest ON pulse, which signals the start of a command.
And remember to configure the number of ON pulses inside the code.
After you've filled in the arrays and configured the source code, compile and run it and watch your serial terminal, which should output the name of the command.
Great, now you can successfully read button commands from an IR remote control.
By the way, I realize there's better ways of doing this that is more robust (such as actually translating the pulse sequence into binary code first), but the method I outlined here is better for getting it to work with any remote without thinking too much.
Step 13: DS1307 Real Time Clock IC
Read the datasheet, it contains the commands for the chip and other details. You'll need to know this stuff when you look at my source code so you know what's going on.
What does it do? It tracks time, so your microcontroller doesn't have to. It'll even track time using a coin cell battery while its main power is off, so you don't get that classic 12:00 blinking that happens to old alarm clocks when the power goes out. Oh and it tracks the date too.
Note that there are much better alternatives to the DS1307 today with better features, however, I found this chip in my collection of odd parts, so I'm going to use it.
This chip requires the following things around it: a watch crystal, a battery backup (3V coin cell from the dollar store will last 9 years), a power supply, and a decoupling capacitor.
The microcontroller will communicate to the DS1307 via the I2C (inter-integrated circuit) bus. The I2C bus is also known as TWI (two wire interface, it only uses two wires). It is designed for communication between multiple integrated circuits. Please refer to Appendix D to learn more about I2C and TWI.
What we need to do with the DS1307 is simple, we just need to set the time, and read the time.
The DS1307 keeps its data in several registers. Refer to the datasheet, table 2, page 8. It shows the data being stored in binary coded decimal format, which our source code will encode and decode. We will write to these registers when we set the time, and read from the same registers to retrieve the time.
A good description of the I2C bus is given on page 10 of the datasheet. The AC electrical characteristics section of the datasheet says our I2C bus must run at 100 KHz or lower (this is configurable inside our source code), keep this in mind. The address of the DS1307 is 1101000, remember that too.
For example, we want to set the seconds, so the register is 0x00. We first send over I2C
this addresses the DS1307, and says we want to write
then we send
to indicate the register 0x00
then we send the number we want to set, coded in binary coded decimal format
Then, if we want to read back the seconds, the register is again, 0x00, we first send
notice that this is the address, but we are still specifying that we want to write
why specify "write" when we want to read? Because we have not told the DS1307 where we want to read from (setting the register address inside the DS1307). So we send
then we end the data transfer
now we are addressing the DS1307 and indicating we want to read
we then drive the clock to read back the data, which the DS1307 will place on the data line for us. We can then decode the data from binary coded decimal format.
It's important to understand that because we are using our AVR's built-in TWI module for I2C communication, it knows to automatically release the SDA line when we indicate "read" in the first byte we send. The hardware and software (we are using "twi.c" from "Wire" library of the Arduino). EVERYTHING is automated, even the acknowledgements.
For our purposes, we will be reading and writing all of the time settings in one go, for efficiency. The details of how this works is in the datasheet. You can also see my source code later. The idea is the same for single registers, but we repeatedly read/write without ending the transfer. You'll see how this works through the datasheet, my source code, and my logic analyzer waveforms.
Attached is a demonstration of a simple program that sets the time and then displays the time on your LCD. It will demonstrate using the TWI bus to perform single/multiple byte read/write operations (view the waveforms I've provided).
Add the DS1307 to your circuit according to my diagram first. A higher resolution picture is within the .ZIP package. The important point is that the DS1307 needs 5V, and we've modified the Teensy++ to run at 3.3V, the diagram shows you where to get 5V from the Teensy++.
You'll need the LCD still connected if you want to see the clock working. The serial terminal will still show you debug messages.
I've included the logic analyzer waveforms so you can clearly see what I2C looks like, the datasheet of the DS1307 also shows similar waveforms, study the waveforms together while also examining the data being exchanged (such as what command is sent, what was the reply), try matching the events to the points in the source code.
Step 14: Introduction to the VS1033D
VS1033D product page: http://www.vlsi.fi/en/products/vs1033.html
SparkFun breakout board: http://www.sparkfun.com/products/8792
The SparkFun breakout board already has the recommended external circuitry for the bare VS1033D chip, if you do not use this breakout board, you will need to implement the recommended external circuitry yourself, which you should figure out by reading the datasheet. But I will not show you how to do that, my wiring diagram will show you using the breakout board. If you take a look at SparkFun's VS1033D breakout board schematics, you can make comparisons between the recommended schematics inside the datasheet and the breakout board.
This chip has two SPI interfaces, one for commands and settings (clock speed, volume, etc) called SCI, the other for streaming audio data called SDI. Both SPI buses use SPI mode 0 (CPOL and CPHA both 0, same as our SD card, which is good), meaning the clock starts low and leaves low, and data is read on the rising edge. There's an active low chip select for both interfaces (CS for SCI and DCS for SDI, the DCS pin is also called BSYNC sometimes). With the 12.288 MHz crystal provided on the breakout board, your SPI clock speed can't be more than 2 MHz (so... we need to slow down the SPI clock before sending data to the VS1033D, and raise it back up to 4 MHz after, since the SD card uses 4 MHz SPI clock and we need the SD card to be fast).
(note, this is cut from http://www.frank-zhao.com/cache/mp3_decoder.htm which is my own website)
All commands, reads from / writes to VS1033D internal registers are done via the SCI interface.
You need to read the datasheet to understand the different commands and such, please do so.
The only data that is sent over the SDI interface is the contents of a song file. We don't need to send over the file name, the data inside a song already contains identifying information so the VS1033D always knows what file format we are sending, which is one less thing we need to worry about. Also, sending invalid data will cause the VS1033D to simply discard that data and ask for more data, this means even if we send over a bad file, the VS1033D is not going to freak out or freeze.
There's an internal decoder FIFO (first in first out) buffer inside the VS1033D. The pin "DREQ" is the data request pin, and if it is high, it means we are allowed to send over 32 bytes of song data.
The datasheet will talk about a VS1002 mode and a VS1001 compatibility mode. This is because the VS1002, and their chips after the VS1002 (including the VS1033) came after the VS1001, but can be configured to be compatible with the VS1001. This can be configured with the "MODE" register, but we are sticking with the newer mode.
Also the two SPI interfaces, SDI and SCI, can share the same chip select. This option can be enabled in the "MODE" register. We cannot use this feature because we have an additional chip select from the MicroSD card to take care of.
According to the shematics of the VS1033D breakout board from SparkFUn, the crystal used is 12.288 MHz. The "CLOCKF" register needs to be set to 0x9000 according to the datasheet. The datasheet has more details regarding this setting. In my own code, it's 0xF800 to support WMA and AAC decoding.
There are several efficient ways of determining how long a song is and the current position in the song we are playing. By efficient, I mean that we do not have to actually read the contents of the song. The datasheet says "SCI_DECODE_TIME" stores the time that has been played in seconds, simple enough. The datasheet also details "SCI_HDAT0" and "SCI_HDAT1", which will contain the current file-type and bit-rate, and if we know the bit-rate and total file size (which we do, FatFs tells us), then we can simply calculate the total size of a song.
Volume is controlled through the "SCI_VOL" register. The most signi?cant byte of the volume register controls the left channel volume, the low part controls the right channel volume. Usually, volume is a logarithmic value (a linear increase in a wave's amplitude does not equate to a linear rise in the volume produced), and we don't want to do logarithmic math on a small microcontroller, but the VS1033D takes care of this for us. The channel volume sets the attenuation from the maximum volume level in 0.5 dB steps. Since the change is in decibel steps, it means the VS1033D took care of the math for us. The maximum volume is 0x0000 and total silence is 0xFEFE, since "attenuate" means "to lower".
The general initialization sequence for the VS1033D is like this:
2. Set "MODE" register so that we are not using VS1001 compatibility mode and we are not sharing chip select between SDI and SCI
3. Set correct clock speed using "CLOCKF" register
4. Set the volume, this is optional, but we'd like to remember the user's previous settings
The wiring for this example demonstration is more complicated, and you need to also wire up the SD card. Compile-upload-run the example I've provided. It should play all the files on your SD card at max volume. Please read the source code to understand the API I've created and what is going on. The debug serial port will show simple messages as well.
I've also provided the logic analyzer files, which shows when I send what data where. Kind of important. You can match up the commands that I send to the VS1033D to the commands in the datasheet and find where I send them in my code.
I can't attach the raw logic analyzer waveform session file this time because it's too big. It's contained in the .ZIP package instead
This demo will simply play all the files on the root directory of the SD card at maximum volume.
Step 15: Building the Electronics
Read files from a MicroSD card
Turn the MicroSD card into a USB mass storage device
Print text and time onto a LCD display
Get the current time from a RTC
Read buttons from an IR remote control
Play sound using a VS1033D
I think we are ready to build an entire fully functioning music player.
If you have successfully followed all the previous steps and performed all the previous example demonstrations, you can already wire up the circuit.
You want to make your final circuit very reliable. You should put decoupling capacitors in several places. First, please read this
Pay attention to "Cosideration to Bus Floating and Hot Insertion"
You want a decoupling capacitor on the DS1307. Simply get a 0.1 uF capacitor between Vcc and GND of the DS1307. This capacitor should be physically close to the pins to be effective.
You want a decoupling capacitor on the IR receiver. Simply get a 0.1 uF capacitor between Vcc and GND of the IR receiver. This capacitor should be physically close to the pins to be effective.
If you are not using the Teensy++, then the microcontroller will need a decoupling capacitor too. Again, a 0.1 uF capacitor physically close to Vcc and GND should do it.
If you are not connecting the clock to the computer all the time, you might need to build a power supply. My power supply is made with a 7805 regulator first, which also feeds a low drop-out 3.3 volt regulator, plus a few capacitors. A DC barrel jack is used to accept 9V or 12V power from a wall AC/DC power adapter. I just threw this together with some parts I found. See diagram below. Make sure the capacitors are placed physically close to the components. The diagram DOES NOT INCLUDE decoupling capacitors for the ATmega644, the IR receiver, the DS1307, or the SD card. Those should be 0.1 uF capacitors placed physically close to their respective components.
Note that driving a speaker directly from the VS1033D is bad but acceptable if the speaker's wattage is low enough. The software configures the VS1033D to use "inverted output mode" which basically allows people use mono-speakers correctly.
My final design used an ATmega644 because I only have one Teensy++ and I need it for other projects. The ATmega644 does not have USB, oh well... The code I provide should work for both, depending on compilation options. The pin assignments are different and you can figure them out the new wiring by reading a file called "pins.h"
Using the ATmega644 means I had to resort to a ISP programmer to burn my code, instead of using a USB bootloader. Since ISP uses the SPI bus, I put pull-up resistors on the CS pin of the SD card, plus CS and DCS pin of the VS1033D. This is so that when I am programming the ATmega644, the other stuff on the SPI bus doesn't get accidentally "selected" because the AVR will have all other pins floating.
The pull-up resistor on the VS1033D reset pin is no-longer recommended, for the reason, see the next step about powering down the VS1033D. The pull-up resistor there is good for testing for various reasons (if you use an amplified speaker, you will hear noise when the reset pin is low), but when you need to leave this alarm clock on for months at a time, you don't want it there.
The .ZIP file below contains all the images in original resolution
Step 16: EMERGENCY: Fixing the Decoder
I changed the code to have power up and power down functions. The reset pin of the VS1033D is now used to power on and power off the VS1033D. See code package dated 20110209 or later.
I am a little paranoid, so I've also modified the SparkFun breakout board itself. There are two voltage regulators on board, each one has an "ENABLE" pin that is permanently wired to Vcc, thus always on. I cut the traces to these pins, and wired these pins to the reset pin. This allows the reset pin to totally power up/down the board. This saves even more power. Although I think this procedure is completely optional, I am doing it "just to be safe".
After thinking about it more, it may be because I placed a 10 kohm pull-down resistor on the DREQ pin, and the current through that resistor eventually caused the transistor driving the DREQ pin to fail. I'm not sure though. Don't worry though, I never told you to put that resistor there in this Instructable, it was only on my own prototype.
I've contacted VLSI who makes the VS1033D, it turns out, SparkFun forgot to connect the TEST pin to a pull-up resistor. This may be the problem, I will test out this theory soon.
Meanwhile read http://www.vsdsp-forum.com/phpbb/viewtopic.php?f=10&t=71
Although that link is about the VS1053D board, it also applies to the VS1033D board.
For clearer pictures, see "more_repairs.zip" attached.
I have some screenshots of the logic analyzer waveform showing you what happened when it the VS1033D fails. The actual file is 53 MB so I can't upload it, sorry.
EDIT: SparkFun is now aware of this problem and are working to fix it.
Step 17: Making an Enclosure
Cut a hole for your speaker
Cut a hole for your LCD
Drill holes for the power supply jack and the power switch, and mount them
Cut a slot for the MicroSD card adapter, and glue in the adapter
Drill a hole for the 3.5 mm jack, mount it and wire it (I have a special diagram for this). There is a special jack you need to buy that will automatically disable the speaker when a cable is plugged in.
Cut a hole for the IR light to reach your IR receiver, glue the receiver in place
Glue the speaker in place
Construct a circuit board with the LCD connected by female headers out front, position it correctly in the box so you know where you need screws, and then screw in the circuit board in place when you are ready.
I made my circuit board with a square piece of perf-board. You need to plan a bit so that nothing will hit the speaker on the side of the box, and that the LCD is centered. The wiring is done on the bottom using 30 gauge Kynar wire (Kynar is fairly heat resistant, it doesn't melt as fast as the coating on normal wire).
Notice how I used 3-pin servo cables for a lot of stuff, I had them on hand. I suggest you use some sort of polarized cable to avoid any reverse voltage mishaps. I also used a ribbon cable and a 3x2 header + IDC ribbon cable connector for the SD card.
Remember that components that are threaded are easier to mount (see how my power supply jack and my 3.5 mm jack are threaded?), my power switch is "panel mount" and "snap in", which is also good.
You can still use the Teensy++ for this instead of the ATmega644 I used, just find a good way to mount and wire a USB connector.
Attached is a ZIP file with better quality pictures
Step 18: Software Design
The code will play all the files on your SD card's root directory. The files will be sorted alphabetically. My source code has some extra features such as displaying volume when it changes and displaying the current file name, song length, and current time in the song. Please follow the source code to understand how each feature works.
The music player will not be able to play music while plugged into a computer as an USB mass storage device. This is because SCSI (remember I mentioned before?) works on blocks of raw data on the SD card directly but the FatFs module we used caches data into RAM, thus changing anything using USB will cause the data used by FatFs to be out-of-sync with the real data.
Although I wanted to use USB connect and disconnect events to automatically detect whether or not to allow FatFs access to the SD card, I can't because of the way that the Teensy++ is designed. There will always be a voltage at VBUS, which makes the events useless. So to enable USB access, you need to go into the menu of the clock to enable it.
There's one timer called the "one shot timer" which will fire an event only once after a certain amount of time. This is used to display the current time on the LCD after some other thing has been displayed on the LCD. So if you change the volume, it will show the volume, and after 3 seconds, it will go back to showing you the time.
Another timer is timer 0, which is programmed to fire an event every second, this keeps track of time internally so we don't have to query the DS1307 frequently. This also fires the alarm when the time matches.
I have a short appendix on timers, check it out.
All the LCD printing is done with string stored in flash memory, this is sort of slow in comparison with using RAM. I've structured the program flow to only update the LCD during periods of time when the decoder is busy. The strings are saved in flash because RAM memory is more precious, it's a good habit to always save non-changing strings in flash.
All settings are saved either to the DS1307 user RAM (which is battery backed), or the AVR's internal EEPROM (see appendix for more details). These settings are loaded and validated upon boot, and saved when the decoder is busy.
If you dig though the files, you may find test functions which I use to test various individual components of the system.
All the files that are purely written by me are in the top project folder. LUFA is stored under the LUFA folder, and "Lib" stores various components not written by me (there may be modifications).
Experienced C coders may find that I broke several rules. For the .C files that are included instead of compiled, my excuse is that they are separate to modularize without having to design APIs that abstract. For the other mistakes, my excuse is the limited amount of time I had to build, program, test, and document this project.
There's some mechanisms used in the code that allows the same code to be compiled for ATmega644 and ATmega644P chips, as well as the Teensy++, the pin assignments are different (and thus the circuit requires minor rewiring). The ported code has not been tested. Obviously there is no USB functionality in the ATmega644/ATmega644P version since that microcontroller does not have USB.
The fuses for the AT90USB1286 should not be changed if you are using the Teensy++.
The fuses for the ATmega644 should be: LFUSE: 0xE2, HFUSE: 0x9F , the importance of these settings is that the internal RC oscillator is used at 8 MHz (whereas the Teensy++ uses a 16 MHz external crystal which is divided by 2 internally).
For a neat way to calculate AVR fuses, try this: http://www.frank-zhao.com/fusecalc/
Attached are time-stamped complete source code.
Step 19: Appendix A: AVR Stuff
You better have WinAVR installed, or MHV AVR Tools, or an installation of AVR-GCC and other GNU AVR tools installed. Make sure that the path to the directory containing the binary executables of the GNU AVR tools is in your PATH environmental variable (the WinAVR installer does this for you). This means that you can execute AVR-GCC by simply typing "avr-gcc" in the console. Makefiles will not work unless you have installed the tools required correctly.
For example, if you placed MHV AVR Tools inside
in such a way that
exists and contains
and your PATH environment variable already contains
then change your PATH environment variable to
I think MHV AVR Tools is missing some components, like "make.exe", I hunted whatever executables I needed when I got errors saying they were missing.
Or you can just install WinAVR which comes with everything, but WinAVR is no longer maintained and won't support for new chips since it won't get updated.
To change your PATH environmental variable in Windows, go to Control Panel -> System -> Advanced -> Environmental Variables, and then look for "PATH" or "Path" or "path" inside System Variables.
Compiling with Makefiles
A makefile is a script that the tool "make" (GNU Automake) executes. The main idea here is that you can edit the makefile with some configuration settings for your project (such as what AVR processor you are using, what clock speed you are running it at, etc), and also tell it what files needs to be compiled in your project (do a search for "SRC =" inside the makefiles I provide).
The general script works like this, when you execute "make all" in the command line, the "all" section requires "build" to be done, and "build" generates the final .hex file you will upload.
But in order to generate this .hex file, build requires an .elf file, and to generate the .elf file, you need an .o file for each of your source code files. The makefile is designed with a set of rules so that if a file required is missing, the correct commands will be used to generate that file before going to the next step. If a file that is needed already exists, it will ignore that step.
It is important to note that since files are not automatically regenerated, if you change your code, you must execute "make clean", which is scripted to delete the old files. This ensures that your newer code is compiled if you perform a build again, not the old code.
For our purposes, I've included a makefile with every example demonstration and the final source code. Do not worry about editing it unless something is seriously wrong.
To use my makefiles, open up a console or command prompt, and navigate to where the makefile is (using the "CD" command, so "CD c:/projectfolder/"). Then execute "make". If the compilers give you any errors, or if you change any of the code, then first fix the errors, then execute "make clean", and then "make" again to rebuild.
To upload the code to the Teensy++, you should use the bootloader utility provided by PJRC.com, who makes Teensy and Teensy++.
But if you choose not to use the Teensy++ bootloader (for various reasons, like me, who use a custom bootloader), then please edit the makefile, and configure the "program" section with the correct settings (essentially the settings AVRDUDE needs), and then you may use the "make program" command to upload code to the AVR microcontroller via AVRDUDE (obviously via an AVR programmer or another bootloader).
More AVR Microcontroller Tutorials and Resources:
Step 20: Appendix B: Makeshift MicroSD Card Holder
Take the MicroSD-to-SD adapter, which should have 9 pads on it like a SD card should. Get some standard 0.1" pitch male header pins, and cut off a 9 pin long section. It should fit just perfectly against the pads. Solder this header to the pads.
Tip: first do one pad on one end, and keep the solder liquid while ensuring the header is aligned correctly, and then allow the solder to cool and solidify. This ensures that the header is straight. Then solder the other pads. Be careful as the plastic MicroSD-to-SD adapter can melt.
If you do not want to use my makeshift method, then purchase a real SD card holder or MicroSD card holder, the pinout should be provided by the manufacture. Many companies carry several breakout boards for these holders.
Step 21: Appendix C: the Shortest Explaination of USB Ever
USB busses have a device and host, the computer is usually the host, our music player is a device, more specifically, a mass storage device. It is important to note that the host always initiate communication, or the host checks the device frequently to see if it has anything to say.
There are pull-up resistors on D+ or D- depending on whether or not the device is USB 1.0, USB 1.1, or USB 2.0. The presense of these pull-up resistors is also how a computer knows when something has connected. In the AT90USB1286 (aka the microcontroller on the Teensy++), the pull-up resistors are built-in and configurable via software.
When a device connects to a host, the host tries to "enumerate" the device. If it fails to do so (device not responding, or responding with garbage), that's when Windows says "device not recognized".
The host and device talks over channels called "endpoints", endpoints are identified by a number. There are some endpoints that a reserved for special use, while others can be configured to operate in different modes (interrupt, bulk, etc).
The host will always first use the "control endpoint" (endpoint 0) first to request a description of the device, this "descriptor" will contain the device identifiers (vendor ID and product ID, etc), along with its device class, subclass, etc (HID like a mouse or keyboard? or maybe mass storage?). Then the configuration descriptor is requested, which also contains the number of endpoints available on the device. Each endpoint has its own descriptor as well. All of this data are sent as packets of data bytes representing a well known specified data structure.
LUFA (Lightweight USB Framework for AVRs) and other USB frameworks/stacks have "structs" and other methods to allow the programmer to change the content in the descriptors. You need to first understand each descriptor, and then check the documentation on LUFA to see how to change them.
The host makes the requests by sending "setup packets" to the "control endpoint". Setup packets have a defined structure making it easy for the device to understand what the host wants. LUFA (and similar frameworks) usually handles the default setup packets. A programmer can write drivers that sends custom setup packets, in which case the firmware must handle the setup packets manually, LUFA (and others) provides some methods to help with that.
In some of my examples, I have included a dump of the descriptors captured by my USB traffic analyzer. You can take a look and match it up with USB specifications to see what each portion represents.
Once all the descriptors have been retrieved from the device, the host can then understand the device and communicate with it. In some of my demonstrations, the USB device acts as a virtual serial port. When you call the function "usb_serial_putchar", the character to be sent is placed in a buffer, and sent out when the computer (host) does a periodic checkup (remember what I said about the host always initiating communication). When the AT90USB1286 on the Teensy++ becomes an USB mass storage device, when the computer sends over SCSI commands to read data blocks, the device replies back with the data over a bulk endpoint.
I have another Instructable which shows you how to build a USB keyboard that types out the code stored in RFID tags: https://www.instructables.com/id/USB-RFID-Reading-Keyboard/
Homework: Read USB in a Nutshell http://www.beyondlogic.org/usbnutshell/usb1.shtml which is pretty much a USB bible
Step 22: Appendix D: I2C / TWI Bus Basics
On a TWI bus, the two signal wires are SDA and SCL, basically data and clock. These signals are open drain (meaning its logic level is either high impedance or low, it cannot ever be high), but there must be a pull-up resistor on each of these signals (we are using the AVR's internal pull-up resistors). This is significant because any device on a TWI bus can drive the signals low at any time, so the signal can only become high when all the devices allow it to become high. This allows devices to detect when the bus is occupied ("arbitration using SDA") and also allow a slow device to dictate the speed of the clock, or even pause a transmission if the slower device is too busy (doing this is called "clock stretching". These facts makes the TWI bus good for communication between a bunch of chips using only two wires.
Every transaction is between a master (the one driving the clock signal) and a slave device. Every transaction starts with a "start condition" and ends with a "end condition". A start condition is when the bus master drives SDA low first, then driving SCL low second. An end condition is when the master releases the TWI bus by releasing SCL and then releasing SDA.
After the start condition, the master has to choose which device to talk to by sending a 7 bit address byte. The 8th (last being sent) bit indicates whether or not the master wishes to read (1) from or write (0) to the slave being addressed. If the master is writing, it will then send more data. If the master is reading, it will release the SDA line so the slave sends data (but the master is still driving the clock). When addressed
All bytes are sent MSB first (most significant bit first). Every byte is optionally ended by an acknowledgement/nacknowledgement. Check the device datasheet to see what the device will expect or will send back. Usually, to quote Wikipedia: "If the master wishes to write to the slave then it repeatedly sends a byte with the slave sending an ACK bit. (In this situation, the master is in master transmit mode and the slave is in slave receive mode.) If the master wishes to read from the slave then it repeatedly receives a byte from the slave, the master sending an ACK bit after every byte but the last one. (In this situation, the master is in master receive mode and the slave is in slave transmit mode.)"
More intricate details are usually specific to a particular device, and such information will come from its datasheet.
When I use I2C/TWI with AVR microcontrollers, I use the low level layer of the "Wire" library for Arduino. The Wire library is the C++ wrapper for the lower level "twi.c" and "twi.h" module, which I modify slighly and compile into my own code (since I don't usually use C++). It takes care of almost everything.
For some logic analyzer waveforms, please see the step about the DS1307 RTC
Step 23: Appendix E: Timers, and EEPROM, on AVRs
Electrically Erasable Programmable Read Only Memory. It's a type of non-volatile memory, meaning it stores info even if power is lost. Most microcontroller has some internally for you to use.
For AVR microcontrollers, just use avr/eeprom.h's functions to write to or read from the EEPROM. The functions are very very simple.
You just do stuff like
value = eeprom_read_byte(put address here);
This is too simple of a subject, and thus this appendix also contains...
Timers on AVRs
Timers keep track of time automatically in the background. This is useful for reading how long a pulse is (such as how we used timer input capture for the IR receiver), or outputting pulses repeatedly (such as PWM, not related to our project but commonly used for various thigns), or to repeat something, or to simply keep track of time. There are many many more uses...
On the AVR, simply set the timer prescaler which tells the timer how fast to run, and then start the timer module. You can optionally enable various interrupts, or the PWM module, or the input capture module. How this is done is all in the AVR's datasheet, they even have some example code in both assembly and C.
For example, if the prescaler is set to the system clock divided by 2, and the system clock is 8 MHz (in our case, it is), then the timer is running at 4 MHz, which means every 1/4000000 seconds, the timer counter will increase by 1. I wrote a neat utility that I use to calculate timer related values: http://www.frank-zhao.com/index.php?page=avrtimercalc
Pop quiz, what is the unit being used to capture and store the IR pulse width values we are using?
The Teensy++ has a 16 MHz crystal, but we are running it at 3.3V instead of 5V, so we are using "clock_prescale_set(clock_div_2);" so it's actually running at 8 MHz. The source code shows the timer 1 being configured to use a "divide by 8" prescaler, so the timer is running at 1 MHz. Thus, 1/1000000 seconds is 1 nanosecond, and that's what the values are captured and stored in.
There are several 8 bit timers and 16 bit timers in the AT90USB1286. 8 bit timers can store time counter values from 0 to 255, 16 bit timers are 0 to 65535. This is because the number of possible discrete numbers that a certain number of bits can represent is given by 2^n where n is the number of bits, 2 ^ 8 = 256, 2 ^ 16 = 65536.
If we enable an overflow interrupt routine on a timer, we can store an overflow counter, which basically extends the number of bits so we can keep track of time that cannot be stored within 8 bits.
Step 24: Final Thoughts
There is a bug in Atmel's distribution of their AVR Tools (which is another copy of the GNU AVR toolchain) and MHV AVR Tools that causes _delay_ms and _delay_us to make delays that are half as long as they should be. This messed me up a lot when examining my logic analyzer waveforms, making me wonder why the signals are so short. If you see delays in my code that are twice as long as they need to be, it's probably because I tried to fix this bug. Both of these packages contains this bug as of the time of this publishing, later versions may or may not. WinAVR does not have these bugs but it is no longer maintained and newer devices may not be supported.
SparkFun sort of forgot to mention that the VS1033D board has a jumper on it that enables the power LED. I spent a good few minutes trying to figure out why the LED wouldn't light up before realizing that there was a jumper that I had to solder across. It's not mentioned anywhere and the schematics does not use a jumper symbol to represent it.
The MicroSD card adapters I got are from DealExtreme for a couple of cents each. They are absolutely horrible, they are wider than normal and may get stuck inside your card reader, requiring a plier to pull it back out. Some of them do fit but have bad contacts on the external pads (the internals are fine). However, they are still perfect for converting into MicroSD card holders.
SparkFun also has FM radio modules, maybe I can try to use one some day. A clock radio sounds great if you want to wake up to the news.
I built the final product really fast at the last minute, I am very busy right now. Excuse me if some of my ramblings are repeated or don't make chronological sense.