Disclaimer: Just like many Google products … this design is always in Beta! :)
Deliverables:
documentation of the design and development process
your project code, and
a video of the final working player in use.
Project Description:
Design an MP3 player of your own interpretation.
Point of View:
The problem:
Imagine a runner warming up. She wants to listen to music that slowly builds up in tempo or energy that will help ease her mind and focus onto a difficult workout during her warmups. During the middle portion of her workout, she would love to keep a steady energy or tempo. And finally, during her cooldown, she would enjoy changing to a more mellow set of songs to keep her in tune with winding down her workout. This type of scenario would work for many other types of exercise including cycling and weight training. The problem, however, is that most athletes don't enjoy carrying a music player in their hand to change music while they workout. The more serious athletes would rather strap their player onto their upper arm, onto their bike, onto their waist belt, or even onto their clothes if the player is small enough. And they don't want to have to strain their neck or bend over completely to view the lcd screen if they want to change to a different set of songs.
The Solution:
A 2-piece MP3 player with an accelerometer + wireless controller in the remote unit; in addition to an accelerometer controller solution on the main unit.
Once again, imagine a runner warming up. She has pre-recorded a handful of playlists to her mp3 player for different training occasions. As she walks into her warm-up, without looking at any part of the mp3 two-piece unit she is wearing on her upper-arm(main unit) and on her wrist (watch unit), she taps on the remote unit and, in a few seconds, she hears the playlist she wants to hear on her warmup.
Similarly, picture a cyclist, also using a 2-piece unit for his workout. Placing his remote unit within reach of his right thumb (while still gripping the bike handlebars), he taps the remote unit a few times and finds the particular playlist he wants to listen to.
And again, picture an athlete at the gym taking a few seconds of rest in between sets of lifting. She doesn't want to spend her short rest period changing the songs that are currently in play on her player. She is using a single-unit mp3 player on her upper left arm. To change songs quickly, she taps on the main player until she finds the playlist that fits the mood she wants to reinforce during the rest of her workout. On another occasion, she decides that although she really loves the current playlist, she has heard it in this same order too many times already. And so she sends a series of taps on her main unit and in a couple of seconds, her player starts playing the same playlist but in a different order.
As is evident in these scenarios, we take advantage of the accelerometer in either the main unit or the remote unit to command the player to perform certain specific functions which will include, change playlist, shuffle, forward one song, repeat song, and pause.
Results:
The Design:
Hardware Design
Primary Components:
-
Teensy 2.0
-
Nokia 5110 GLCD
-
3-axis accelerometer board
-
MP3 decoder board
-
Rotary encoder
-
SD card adapter for microSD card
-
Amber Wireless
-
Blackberry trackball board
Minor components:
Hardware connections:
Teensy 2.0:
Nokia 5110 GLCD:
-
Graphical LCD
|
Teensy
|
8 - LED
|
3.3V
|
7 - SCLK
|
Pin 1 (SCLK)
|
6 - DN(MOSI)
|
Pin 2 (MOSI)
|
5 - D/C
|
Pin 4
|
4 - RST
|
Pin 8
|
3 - SCE
|
Pin 9
|
2 - GND
|
GND
|
1 - Vcc
|
Pin 10
|
Accelerometer board:
-
Accelerometer
|
Teensy
|
Vcc
|
3.3V
|
GND
|
GND
|
X0
|
Pin 2 (MOSI)
|
Y0
|
Pin 4
|
Z0
|
N/C
|
MP3 decoder board:
-
Decoder
|
Teensy
|
CS
|
Pin 21
|
SCLK
|
Pin 1 (SCLK)
|
SI
|
Pin 2 (MOSI)
|
SO
|
Pin 3 (MISO)
|
VCC
|
3.3V
|
GND
|
GND
|
BSYNC (DCS)
|
Pin 20
|
DREQ
|
Pin 19
|
RST
|
Pin 18
|
Audio Jack to MP3 board:
-
Decoder
|
Audio Jack
|
LEFT
|
Pin 2 (TIP)
|
GBUF
|
Pin 1 (GND on jack NOT Teensy GND)
|
RIGHT
|
Pin 3 (RING)
|
Blackberry Trackball board:
-
Decoder
|
Teensy
|
CS
|
Pin 21
|
SCLK
|
Pin 1 (SCLK)
|
SI
|
Pin 2 (MOSI)
|
SO
|
Pin 3 (MISO)
|
VCC
|
3.3V
|
GND
|
GND
|
BSYNC (DCS)
|
Pin 20
|
DREQ
|
Pin 19
|
RST
|
Pin 18
|
Amber Wireless Transceiver board:
-
Ez430_Pin
|
Description
|
Teensy_pin
|
1
|
GND
|
GND
|
2
|
VCC
|
Vcc
|
3
|
P2.0/A0
|
N/C
|
4
|
P2.1/A1
|
N/C
|
5
|
P2.2/A2
|
N/C
|
6
|
P2.3/A3
|
N/C
|
7
|
P2.4/A4
|
N/C
|
8
|
P4.3/A12
|
N/C
|
9
|
P4.4/A13
|
N/C
|
10
|
P4.5/A14
|
N/C
|
11
|
P4.6/A15
|
N/C
|
12
|
GND
|
GND
|
13
|
P2.6
|
N/C
|
14
|
P2.7
|
N/C
|
15
|
P3.2/B0_MISO/SCL
|
Pin3 (MISO)
|
16
|
P3.3/B0_SCLK
|
Pin1 (SCLK)
|
17
|
P3.0/B0_STE/A5
|
Pin 24
|
18
|
P3.1/B0_MOSI
|
Pin2 (MOSI)
|
SD Card adapter:
-
microSD card
|
Teensy
|
MISO
|
Pin 3 (MISO)
|
GND
|
GND
|
SCK
|
Pin 1 (SCLK)
|
3.3V+
|
3.3V
|
GND
|
GND
|
MOSI
|
Pin 2 (MOSI)
|
CS
|
Pin 12
|
Software:
State diagrams
The code:
/*
* example sketch to play audio file(s) in a directory using the mp3
* library for playback and the arduino fatfs wrapper to read files.
* based on frank zhao's player at http://frank.circleofcurrent.com/
*
* (c) 2010 david sirkin sirkin@stanford.edu
*
* Heavily reworked by Matthew Seal (mattseal@stanford.edu)
* Modified for use with the Teensy 2.0 by Akil Srinivasan (akils@stanford.edu)
*
*/
// Use 40*13 = 520 bytes of EEPROM
#define MAX_FILE_COUNT 40
#define MAX_NAME_LENGTH 13
#include <mp3.h>
#include <mp3conf.h>
// include the SD library:
#include <SD.h>
#include <EEPROM.h>
// set up variables using the SD utility library functions:
Sd2Card card;
SdVolume volume;
SdFile root;
#define DEBUG // Comment this line to remove debugging features
#define mp3_cs 21 // 'command chip select' to cs pin
#define sd_cs 12 // 'chip select' for SD card
#define dcs 20 // 'data chip select' to bsync pin
#define rst 18 // 'reset' to decoder's reset pin
#define dreq 19 // 'data request line' to dreq pin
#define read_buffer 512 // size of the microsd read buffer
#define mp3_vol 175 // output volume range is 0 to 254 (0 = minimum, 254 = max)
// these DEFINES used by the spi functions
#define CS 21 // command CS, connect to CS
#define DCS 20 // (Pin A0) data CS, connect to BSYNC
#define RESET 18 // soft reset, connect to RESET
#define SCLK 1 // clock, connect to SCK
#define MOSI 2 // MOSI, connect to SI
#define MISO 3 // MISO, not used but part of built-in SPI
#define SS 0 //SS pin on Teensy
#define READ 0x3
#define WRITE 0x2
#define SCI_MODE 0x0
#define SCI_CLOCKF 0x3
#define SCI_VOL 0xB
#define DEFAULT_MODE 0x0820 // sets SDINEW mode and SM_TESTS
#define DEFAULT_CLOCKF 0x0000 // set to no internal clock multiplier
#define DEFAULT_VOL 0x8181 // a middle-range initial volume level
// Creating volume function using Rotary Encoder
#define encoder0PinA 8
#define encoder0PinB 7
// declare global variables
volatile unsigned int encoder0Pos = 0;
unsigned char myVolume = 128;
unsigned char spi_transfer(volatile unsigned char data) {
SPDR = data; // start the transmission
while (!(SPSR & (1 << SPIF))); // wait for end of transmission
return SPDR; // return the received byte
}
void spi_init() {
pinMode(CS, OUTPUT);
pinMode(DCS, OUTPUT);
pinMode(RESET, OUTPUT);
pinMode(SCLK, OUTPUT);
pinMode(MOSI, OUTPUT);
pinMode(MISO, INPUT);
pinMode(SS, OUTPUT); //SS Pin on Teensy must be set to output
digitalWrite(SS, HIGH); // disable any SPI device using hardware SS pin
digitalWrite(CS, HIGH);
digitalWrite(RESET, HIGH);
delay(100);
digitalWrite(RESET, LOW);
delay(100);
digitalWrite(RESET, HIGH);
// set interrupt disabled, spi enabled, msb 1st, master, clk low when
// idle, sample on leading edge of clk and system clock/8 (fastest).
// the VS1053 datasheet says that the max SPI clock frequency is 1.755MHz,
// the data order is MSB, and the data is valid on the rising edge of
// the clock, so the default settings of the SPI shortcut (at 500kHz,
// MSB, rising edge) are applicable. SPR0 set to 1 and SPI2X set to 1 means 8/8 = 1 MHz
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
delay(10);
SPSR = (1 << SPI2X);
delay(10);
}
void write_sci(unsigned int address, unsigned int value) {
digitalWrite(CS, LOW); // select chip for start of transfer
spi_transfer(WRITE); // send a 'write' cmd
spi_transfer(address); // address to write to
if (value > 0xFF) {
// spi_transfer(value / 0x0100); // data to write
// spi_transfer(value % 0x0100); // data to write
spi_transfer((value >> 8) & 0xFF); // data to write
spi_transfer(value & 0xFF); // data to write
} else {
spi_transfer(value); // data to write
}
#ifdef DEBUG
Serial.print("Wrote ");
Serial.print(value, HEX);
Serial.print(" to address ");
Serial.println(address, HEX);
delay(100);
#endif
digitalWrite(CS, HIGH); //release chip, signal end of transfer
}
void write_sci(unsigned int address, unsigned int value1, unsigned int value2) {
digitalWrite(CS, LOW); // select chip for start of transfer
spi_transfer(WRITE); // send a 'write' cmd
spi_transfer(address); // address to write to
spi_transfer(value1); // data to write
spi_transfer(value2); // data to write
#ifdef DEBUG
Serial.print("Wrote ");
Serial.print(value1, HEX);
Serial.print(value2, HEX);
Serial.print(" to address ");
Serial.println(address, HEX);
delay(100);
#endif
digitalWrite(CS, HIGH); //release chip, signal end of transfer
}
unsigned int read_sci(unsigned int address) {
unsigned int data;
digitalWrite(CS, LOW); // select chip for start of transfer
spi_transfer(READ); // send a 'read' cmd
spi_transfer(address); // address to read from
data = spi_transfer(0xFF); // get the data bytes
data = data << 8;
data |= (spi_transfer(0xFF) & 0xFF);
digitalWrite(CS, HIGH); //release chip, signal end of transfer
return data;
}
/*
* Adopted from the ladyada.net tutorial on Ethernet protocol
* @http://www.ladyada.net/learn/arduino/ethfiles.html
*
* Lists all files from a directory:
* ListFiles(SdFile *dir, char *doubleBuf, int maxCount)
*
* dir is the directory to list
* doubleBuf is the buffer to store the results in -- this should be fileNames to start
* maxCount is the size of the buffer.
*
* returns the number of file names written into the doubleBuf array
*/
int ListFiles(SdFile *dir) {
int count = 0;
dir_t p;
dir->rewind();
while (dir->readDir(&p) > 0 && count < MAX_FILE_COUNT) {
// done if past last used entry
if (p.name[0] == DIR_NAME_FREE) break;
// skip deleted entry and entries for . and ..
if (p.name[0] == DIR_NAME_DELETED || p.name[0] == '.') continue;
/* Uncomment to allow subdirectories to be listed (with a '/' after the name) */
// only list subdirectories and files
//if (!DIR_IS_FILE_OR_SUBDIR(&p)) continue;
/* Uncomment to only allow files to be listed */
// only list files
if (!DIR_IS_FILE(&p)) continue;
// print file name into string
unsigned char pos = 0;
for (unsigned char i = 0; i < 11; i++) {
if (p.name[i] != ' ') {
EEPROM.write((count*MAX_NAME_LENGTH)+pos, p.name[i]);
pos++;
}
}
// append slash if file is a directory
if (DIR_IS_SUBDIR(&p)) {
EEPROM.write((count*MAX_NAME_LENGTH)+pos, '/');
pos++;
}
// add the end string character
EEPROM.write((count*MAX_NAME_LENGTH)+pos, '\0');
count++;
}
#ifdef DEBUG
Serial.print("Stored ");
Serial.print(count+1);
Serial.print(" files, using ");
Serial.print((count+1)*MAX_NAME_LENGTH, DEC);
Serial.println(" bytes in EEPROM.");
#endif
return count+1;
}
/*
* read in buffer 'bytes' of 'read_buffer' size from the file opened
* in the while loop below. This function assumes that file has already
* been opened and does NOT close the file. This means you need to do this
* outside of the function.
*/
void mp3_play (SdFile *file) {
unsigned char bytes[read_buffer]; // buffer to send to the decoder
unsigned int bytes_to_read; // number of bytes to read from sd card
// reset the file to be at the beginning of data
file->seekSet(0);
// Try to read 'read_buffer' length of bytes and send it to the decoder.
// If less than 'read_buffer' bytes are available, stop last send.
do {
bytes_to_read = file->read(bytes, read_buffer);
Mp3.play(bytes, bytes_to_read);
//Serial.println('.');
}
while (bytes_to_read == read_buffer);
#ifdef DEBUG
Serial.println("Played song...");
#endif
}
/*
* play back an mp3 or wav file (only!) in the root directory. first
* check that it's a file (and not a directory); next, check that it
* has a proper extension; finally, play only if it opens cleanly.
*/
void dir_play (SdFile *dir) {
int numFiles = ListFiles(dir);
for (int i = 0; i < numFiles; i++) {
char fn[MAX_NAME_LENGTH+2];
int fnln = MAX_NAME_LENGTH;
// get file name and name length
for (int j = 0; j < MAX_NAME_LENGTH; j++) {
fn[j] = EEPROM.read((i*MAX_NAME_LENGTH)+j);
// end of name
if (fn[j] == '\0') {
fnln = j;
j = MAX_NAME_LENGTH;
break;
}
// directory => nullify entry name
else if (fn[j] == '/') {
fn[0] = '\0';
fnln = 0;
j = MAX_NAME_LENGTH;
break;
}
}
if (fnln > 4) {
fn[fnln+1] = '\0';
fn[fnln] = fn[fnln-1];
fn[fnln-1] = fn[fnln-2];
fn[fnln-2] = fn[fnln-3];
fn[fnln-3] = '.';
fnln++;
#ifdef DEBUG
Serial.print("Opening ");
Serial.println(fn);
#endif
}
SdFile dataFile;
// ensure we can open the file
if (dataFile.open(dir, fn, O_RDONLY) > 0) {
// ensure it's not a directory
if (!dataFile.isDir() || fn == 0) {
//get filenames in directory fn
//get length of the filename fnln
#ifdef DEBUG
Serial.print(fn);
Serial.println(": File is valid.");
#endif
if ((fn[fnln-3] == 'M' && fn[fnln-2] == 'P' && fn[fnln-1] == '3') ||
(fn[fnln-3] == 'W' && fn[fnln-2] == 'A' && fn[fnln-1] == 'V')) {
#ifdef DEBUG
Serial.print("Playing ");
Serial.println(fn);
#endif
//buffer data for playing
mp3_play(&dataFile);
}
#ifdef DEBUG
else {
Serial.print("Skipping ");
Serial.println(fn);
}
#endif
}
#ifdef DEBUG
else {
Serial.print("File is directory ");
Serial.println(fn);
}
#endif
}
else {
Serial.print("File is not valid ");
Serial.println(fn);
}
}
}
void doEncoder() {
/* If pinA and pinB are both high or both low, it is spinning
* forward. If they're different, it's going backward.
*/
if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {
write_sci(SCI_VOL, 0x6060);
encoder0Pos++;
myVolume++;
Mp3.volume(myVolume);
} else {
encoder0Pos--;
myVolume--;
Mp3.volume(myVolume);
}
Serial.println (encoder0Pos, DEC);
Serial.println (myVolume, DEC);
}
/*
* initialize the processor speed, setup the fatfs sd card (or, mms)
* filesystem, setup mp3 playback and register the pins used (device
* specific configuration)
*/
void setup() {
Serial.begin(9600); // initialize the serial terminal
spi_init();
pinMode(encoder0PinA, INPUT);
digitalWrite(encoder0PinA, HIGH); // turn on pullup resistor
pinMode(encoder0PinB, INPUT);
digitalWrite(encoder0PinB, HIGH); // turn on pullup resistor
attachInterrupt(3, doEncoder, CHANGE); // encoder pin on interrupt 3 - pin 8
pinMode(SS_PIN, OUTPUT); // change this to 53 on a mega
digitalWrite(SS_PIN, HIGH);
myVolume = mp3_vol;
// see if the card is present and can be initialized:
if (!SD.begin(sd_cs)) {
Serial.println("Card failed, or not present");
// don't do anything more:
return;
}
// we'll use the initialization code from the utility libraries
// since we're just testing if the card is working!
if (!card.init(SPI_HALF_SPEED, sd_cs)) {
Serial.println("Initialization failed. Things to check:");
Serial.println("* is a card is inserted?");
Serial.println("* Is your wiring correct?");
Serial.println("* did you change the SD chipSelect pin to match your setup?");
return;
}
// Now we will try to open the 'volume'/'partition' - it should be FAT16 or FAT32
if (!volume.init(card)) {
Serial.println("Could not find FAT16/FAT32 partition.\nMake sure you've formatted the card");
return;
}
if (!root.openRoot(&volume)) {
Serial.println("Failed to open root");
// don't do anything more:
return;
}
Mp3.begin(mp3_cs,dcs,rst,dreq); // decoder cs, dcs, rst, dreq pin from your setup
Mp3.volume(mp3_vol); // default volume level is silent (note: 0 = minimum, 254 = max)
#ifdef DEBUG
Serial.println("Card initialized.");
Serial.println("\nFiles found on the card (name, date and size in bytes): ");
// list all files in the card with date and size
root.ls(LS_R | LS_DATE | LS_SIZE);
Serial.println("Checking free memory...");
Serial.print("There are ");
Serial.print(get_free_memory(), DEC);
Serial.println(" bytes of free memory.");
Serial.print("~");
Serial.print(read_buffer, DEC);
Serial.println(" free bytes required for mp3 playing.");
#endif
// List all files to in the root directory and play each
// one at a time. Maximum of MAX_FILE_COUNT files will be read
dir_play(&root);
root.close();
}
// Do nothing for now
void loop() {
while (1);
}
Prototyping Process:
Some notes and diagrams on paper:
My initial attempts at physically trying to miniaturize the footprint. I chose a couple of small perfboard pieces and commenced to place the parts on the board.
The next set of three pictures shows a second iteration of working out the optimal part placement for a compact footprint.
The next board is the final configuration that actually got implemented. Notice that the trackball was moved from the bottom third of the player to the middle third. When physically placing the parts on the board and getting a feel for their placement, it became evident that the trackball and that same boards control buttons would be in the wrong spot and the player would feel awkward to hold and clumsy to control.
Here is an indication that the wirewrapping would work out just fine. I was nervous to use it as I was afraid it would take too long and spotting errors might be difficult. It was neither. It went fairly quickly and there were no uncaught errors during the whole process.
Finally, after the housing I tried to put together for the presentation on August 14th was an absolute failure. Shortly after the presentation, I had an idea that pieces from an Erector set would be great for putting a frame around my player. And so I tried it out the next day and I was right. The boards, as poorly mechanically designed as they are, are now secure in the Erector frame and the whole thing feels strong. It would be fairly easy to open it up for fixes. Here are a couple pictures of the new housing:
Appendix:
Comments (1)
Akil Srinivasan said
at 8:32 pm on Aug 16, 2011
We like:
- Great motivation/story that describes the 'use scenario' of your player.
- Your 'results' section is a very useful hit list of tips/tricks for new students.
- Detailed parts list, connection pinouts .. again very useful for someone trying to replicate/take inspiration from your project.
- Small and clean layout .. well done! Although sometimes going larger for initial prototype would make your life a lot easier.
- Your design notes and sketches. We can see you spent some time planning!
- Great, creative thinking with the Erector set package at the end!
We wish:
- We also had a verplank diagram from when you were in the mapping/brainstorming stage.
Overall, good project and excellent final report!
Akil, David, and Ben
You don't have permission to comment on this page.