Skip to content
Find file
754 lines (574 sloc) 19.2 KB
// "Player" example sketch for Lilypad MP3 Player
// Mike Grusin, SparkFun Electronics
// This sketch turns the Lilypad MP3 Player into a basic MP3
// player that you can control with the optional rotary encoder.
// To use this sketch, an optional RGB rotary encoder must be soldered
// to the MP3 Player board. The board is designed to hold SparkFun
// part number COM-10892.
// This sketch requires the following libraries. These are included
// in the Lilypad MP3 Player firmware zip file and must be copied to
// a "libraries" folder in your Arduino sketch directory:
// Uses the SdFat library by William Greiman, which is supplied
// with this archive, or download from
// Uses the SFEMP3Shield library by Porter and Flaga, which is supplied
// with this archive, or download from
// Uses the PinChangeInt library by Lex Talionis, which is supplied
// with this archive, or download from
// Place your audio files in the root directory of the SD card.
// Your files MUST have one of the following extensions: MP3, WAV,
// MID (MIDI), MP4, WMA, AAC, FLA, OGG. Note that this is solely to
// prevent the VS1053 from locking up from being fed non-audio data
// (files without one of the above extensions are quietly skipped).
// You can rename any playable file to any of the above extensions,
// or add additional extensions to the isPlayable() function below.
// See the VS1053 datasheet for the audio file types it can play.
// The player has two modes, TRACK and VOLUME. In TRACK mode, turning
// the knob will move to the next or previous track. In VOLUME mode,
// turning the knob will increase or decrease the volume.
// You can tell what mode you're in by the color of the RGB LED in the
// knob of the rotary encoder. TRACK mode is red, VOLUME mode is green.
// To switch between modes, hold down the button on the rotary encoder
// until the color changes (more than one second).
// To start and stop playback, press the button on the rotary encoder
// *quickly* (less than one second). When the player is playing, it
// will stubbornly keep playing; starting new tracks when the previous
// one ends, and switching to new tracks if you turn the knob in TRACK
// mode. When the player is stopped, it will not start playing until
// you press the button *quickly*, but it will silently change tracks
// or adjust the volume if you turn the knob.
// This sketch can output serial debugging information if desired
// by changing the global variable "debugging" to true. Note that
// this will take away trigger inputs 4 and 5, which are shared
// with the TX and RX lines. You can keep these lines connected to
// trigger switches and use the serial port as long as the triggers
// are normally open (not grounded) and remain ungrounded while the
// serial port is in use.
// License:
// We use the "beerware" license for our firmware. You can do
// ANYTHING you want with this code. If you like it, and we meet
// someday, you can, but are under no obligation to, buy me a
// (root) beer in return.
// Have fun!
// -your friends at SparkFun
// Revision history:
// 1.0 initial release MDG 2013/1/31
// Required libraries:
#include <SPI.h>
#include <SdFat.h>
#include <SdFatUtil.h>
#include <SFEMP3Shield.h>
#include <PinChangeInt.h>
// Set debugging to true to get serial messages:
boolean debugging = true;
// Possible modes (first and last are there to make
// rotating through them easier):
#define FIRST_MODE 0
#define TRACK 0
#define VOLUME 1
#define LAST_MODE 1
// Initial mode for the rotary encoder. TRACK lets you
// select audio tracks, VOLUME lets you change the volume.
// In any mode, a quick press will start and stop playback.
// A longer press will switch to the next mode.
unsigned char rotary_mode = TRACK;
// Initial volume for the MP3 chip. 0 is the loudest, 255
// is the lowest.
unsigned char volume = 40;
// Start up *not* playing:
boolean playing = false;
// Set loop_all to true if you would like to automatically
// start playing the next file after the current one ends:
boolean loop_all = true;
// LilyPad MP3 pin definitions:
#define TRIG1 A0
#define ROT_LEDG A1
#define SHDN_GPIO1 A2
#define ROT_B A3
#define TRIG2_SDA A4
#define TRIG3_SCL A5
#define RIGHT A6
#define LEFT A7
#define TRIG5_RXI 0
#define TRIG4_TXO 1
#define MP3_DREQ 2
#define ROT_A 3
#define ROT_SW 4
#define ROT_LEDB 5
#define MP3_CS 6
#define MP3_DCS 7
#define MP3_RST 8
#define SD_CS 9
#define ROT_LEDR 10
#define MOSI 11
#define MISO 12
#define SCK 13
// RGB LED colors (for common anode LED, 0 is on, 1 is off)
#define OFF B111
#define RED B110
#define GREEN B101
#define YELLOW B100
#define BLUE B011
#define PURPLE B010
#define CYAN B001
#define WHITE B000
// Global variables and flags for interrupt request functions:
volatile int rotary_counter = 0; // Current "position" of rotary encoder (increments CW)
volatile boolean rotary_change = false; // Will turn true if rotary_counter has changed
volatile boolean rotary_direction; // Direction rotary encoder was turned (true = CW)
volatile boolean button_pressed = false; // Will turn true if the button has been pushed
volatile boolean button_released = false; // Will turn true if the button has been released (sets button_downtime)
volatile unsigned long button_downtime = 0L; // ms the button was pushed before release
char track[13];
// Library objects:
SdFat sd;
SdFile file;
SFEMP3Shield MP3player;
void setup()
byte result;
if (debugging)
Serial.println(F("Lilypad MP3 Player"));
// ('F' places constant strings in program flash to save RAM)
Serial.print(F("Free RAM = "));
Serial.println(FreeRam(), DEC);
// Set up I/O pins:
pinMode(TRIG1, INPUT);
digitalWrite(TRIG1, HIGH); // turn on weak pullup
pinMode(MP3_CS, OUTPUT);
pinMode(ROT_B, INPUT);
digitalWrite(ROT_B, HIGH); // turn on weak pullup
pinMode(TRIG2_SDA, INPUT);
digitalWrite(TRIG1, HIGH); // turn on weak pullup
pinMode(TRIG3_SCL, INPUT);
digitalWrite(TRIG1, HIGH); // turn on weak pullup
pinMode(TRIG5_RXI, INPUT);
digitalWrite(TRIG5_RXI, HIGH); // turn on weak pullup
pinMode(TRIG4_TXO, INPUT);
digitalWrite(TRIG4_TXO, HIGH); // turn on weak pullup
pinMode(ROT_A, INPUT);
digitalWrite(ROT_A, HIGH); // turn on weak pullup
pinMode(ROT_SW, INPUT);
// switch is common anode with external pulldown, do not turn on pullup
pinMode(MP3_DREQ, INPUT);
pinMode(MP3_DCS, OUTPUT);
pinMode(MP3_RST, OUTPUT);
pinMode(SD_CS, OUTPUT);
pinMode(MOSI, OUTPUT);
pinMode(MISO, INPUT);
pinMode(SCK, OUTPUT);
// Turn off amplifier chip / turn on MP3 mode:
digitalWrite(SHDN_GPIO1, LOW);
// Initialize the SD card:
if (debugging) Serial.println(F("Initializing SD card... "));
result = sd.begin(SD_SEL, SPI_HALF_SPEED);
if (result != 1)
if (debugging) Serial.println(F("error, halting"));
if (debugging) Serial.println(F("OK"));
//Initialize the MP3 chip:
if (debugging) Serial.println(F("Initializing MP3 chip... "));
result = MP3player.begin();
// Check result, 0 and 6 are OK:
if((result != 0) && (result != 6))
if (debugging)
Serial.print(F("error "));
if (debugging) Serial.println(F("OK"));
// Set up interrupts. We'll use the standard external interrupt
// pin for the rotary, but we'll use the pin change interrupt
// library for the button:
PCintPort::attachInterrupt(ROT_SW, &buttonIRQ, CHANGE);
// Get initial track:
sd.chdir("/",true); // Index beginning of root directory
if (debugging)
Serial.print(F("current track: "));
// Set initial volume (same for both left and right channels)
MP3player.setVolume(volume, volume);
// Initial mode for the rotary encoder
// Uncomment to get a directory listing of the SD card:
// | LS_DATE | LS_SIZE);
// Turn on amplifier chip:
digitalWrite(SHDN_GPIO1, HIGH);
void buttonIRQ()
// Button press interrupt request function (IRQ).
// This function is called *automatically* when the button
// changes state.
// Process rotary encoder button presses and releases, including
// debouncing (extra "presses" from noisy switch contacts).
// If button is pressed, the button_pressed flag is set to true.
// (Manually set this to false after handling the change.)
// If button is released, the button_released flag is set to true,
// and button_downtime will contain the duration of the button
// press in ms. (Set this to false after handling the change.)
// Raw information from PinChangeInt library:
// Serial.print("pin: ");
// Serial.print(PCintPort::arduinoPin);
// Serial.print(" state: ");
// Serial.println(PCintPort::pinState);
static boolean button_state = false;
static unsigned long start, end;
if ((PCintPort::pinState == HIGH) && (button_state == false))
// Button was up, but is currently being pressed down
// Discard button presses too close together (debounce)
start = millis();
if (start > (end + 10)) // 10ms debounce timer
button_state = true;
button_pressed = true;
else if ((PCintPort::pinState == LOW) && (button_state == true))
// Button was down, but has just been released
// Discard button releases too close together (debounce)
end = millis();
if (end > (start + 10)) // 10ms debounce timer
button_state = false;
button_released = true;
button_downtime = end - start;
void rotaryIRQ()
// Rotary encoder interrupt request function (IRQ).
// This function is called *automatically* when the
// rotary encoder changes state.
// Process input from the rotary encoder.
// The rotary "position" is held in rotary_counter, increasing
// for CW rotation (changes by one per detent).
// If the position changes, rotary_change will be set true.
// (You may manually set this to false after handling the change).
// This function will automatically run when rotary encoder input A
// transitions in either direction (low to high or high to low).
// By saving the state of the A and B pins through two interrupts,
// we'll determine the direction of rotation.
// Int rotary_counter will be updated with the new value, and boolean
// rotary_change will be true if there was a value change.
// Based on concepts from Oleg at circuits@home (
// Unlike Oleg's original code, this code uses only one interrupt and
// has only two transition states; it has less resolution but needs only
// one interrupt, is very smooth, and handles switchbounce well.
static unsigned char rotary_state = 0; // Current and previous encoder states
rotary_state <<= 2; // Remember previous state
rotary_state |= (digitalRead(ROT_A) | (digitalRead(ROT_B) << 1)); // Mask in current state
rotary_state &= 0x0F; // Zero upper nybble
if (rotary_state == 0x09) // From 10 to 01, increment counter. Also try 0x06 if unreliable.
rotary_direction = true;
rotary_change = true;
else if (rotary_state == 0x03) // From 00 to 11, decrement counter. Also try 0x0C if unreliable.
rotary_direction = false;
rotary_change = true;
void loop()
// "Static" variables are initalized once the first time
// the loop runs, but they keep their values through
// successive loops.
static boolean button_down = false;
static unsigned long int button_down_start, button_down_time;
// rotaryIRQ() sets the flag rotary_counter to true
// if the knob position has changed. We can use this flag
// to do something in the main loop() each time there's
// a change. We'll clear this flag when we're done, which
// lets us run this if() once for each change.
if (rotary_change)
switch (rotary_mode)
case TRACK:
// Before switching to a new audio file, we MUST
// stop playing before accessing the SD directory:
if (playing)
// Get the next file:
if (rotary_direction)
// If we were previously playing, let's start again!
if (playing) startPlaying();
if (debugging)
Serial.print(F("current track "));
case VOLUME:
// Change the volume. Easy.
rotary_change = false; // Clear flag
// The button IRQ also sets several flags to true, one for
// button_pressed, one for button_released. Like the rotary
// flag, we'll clear these when we're done handling them:
if (button_pressed)
if (debugging) Serial.println(F("button press"));
button_pressed = false; // Clear flag
// We'll set another flag saying the button is now down
// this is so we can keep track of how long the button
// is being held down. (We can't do this in interrupts,
// because the button state doesn't change while it's
// being held down):
button_down = true;
button_down_start = millis();
if (button_released)
if (debugging)
Serial.print(F("button release, downtime: "));
// For quick button presses, start or stop playback:
if (button_downtime < 1000)
playing = !playing;
if (playing)
button_released = false; // Clear flags
button_down = false;
// Now we can keep track of how long the button is being
// held down, and perform actions based on that time.
// This lets us implement the "hold down for one second
// to change mode" action:
if (button_down)
button_down_time = millis() - button_down_start;
if (button_down_time > 1000)
// Switch to next mode
// Loop around if necessary
if (rotary_mode > LAST_MODE) rotary_mode = FIRST_MODE;
// Set LED to mode color
if (debugging)
Serial.print(F("mode "));
// Reset counter so holding the button will continue
// switching through modes:
button_down_start = millis();
// Handle "last track ended" situations
// (should we play the next track?)
// Are we in "playing" mode, and has the
// current file ended?
if (playing && !MP3player.isPlaying())
getNextTrack(); // Set up for next track
// If loop_all is true, start the next track
if (loop_all)
playing = false;
void changeVolume(boolean direction)
// Increment or decrement the volume.
// This is handled internally in the VS1053 MP3 chip.
// Lower numbers are louder (0 is the loudest).
if (volume < 255 && direction == false)
volume += 2;
if (volume > 0 && direction == true)
volume -= 2;
MP3player.setVolume(volume, volume);
if (debugging)
Serial.print(F("volume "));
void getNextTrack()
// Get the next playable track (check extension to be
// sure it's an audio file)
while(isPlayable() != true);
void getPrevTrack()
// Get the previous playable track (check extension to be
// sure it's an audio file)
while(isPlayable() != true);
void getNextFile()
// Get the next file (which may be playable or not)
int result = (file.openNext(sd.vwd(), O_READ));
// If we're at the end of the directory,
// loop around to the beginning:
if (!result)
void getPrevFile()
// Get the previous file (which may be playable or not)
char test[13], prev[13];
// Getting the previous file is tricky, since you can
// only go forward when reading directories.
// To handle this, we'll save the name of the current
// file, then keep reading all the files until we loop
// back around to where we are. While doing this we're
// saving the last file we looked at, so when we get
// back to the current file, we'll return the previous
// one.
// Yeah, it's a pain.
while(strcasecmp(track,test) != 0);
void startPlaying()
int result;
if (debugging)
Serial.print(F("playing "));
result = MP3player.playMP3(track);
if (debugging)
Serial.print(F(" result "));
void stopPlaying()
if (debugging) Serial.println(F("stopping playback"));
boolean isPlayable()
// Check to see if a filename has a "playable" extension.
// This is to keep the VS1053 from locking up if it is sent
// unplayable data.
char *extension;
extension = strrchr(track,'.');
if (
(strcasecmp(extension,"MP3") == 0) ||
(strcasecmp(extension,"WAV") == 0) ||
(strcasecmp(extension,"MID") == 0) ||
(strcasecmp(extension,"MP4") == 0) ||
(strcasecmp(extension,"WMA") == 0) ||
(strcasecmp(extension,"FLA") == 0) ||
(strcasecmp(extension,"OGG") == 0) ||
(strcasecmp(extension,"AAC") == 0)
return true;
return false;
void LEDmode(unsigned char mode)
// Change the RGB LED to a specific color for each mode
// (See #defines at start of sketch for colors.)
switch (mode)
case TRACK:
case VOLUME:
void setLEDcolor(unsigned char color)
// Set the RGB LED in the (optional) rotary encoder
// to a specific color. See the color #defines at the
// start of this sketch.
digitalWrite(ROT_LEDR,color & B001);
digitalWrite(ROT_LEDG,color & B010);
digitalWrite(ROT_LEDB,color & B100);
void errorBlink(int blinks, byte color)
// This function will blink the RGB LED in the rotary encoder
// (optional) a given number of times and repeat forever.
// This is so you can see error codes without having to use
// the serial monitor window.
int x;
while(true) // Loop forever
for (x=0; x < blinks; x++) // Blink a given number of times
delay(1500); // Longer pause between blink-groups
Jump to Line
Something went wrong with that request. Please try again.