A good project to get the kids up in the morning: play a random song of their favourites list. This Arduino based alarm clock uses an MP3 shield with SD card, an OLED display, and a Real Time Clock module (RTC over I2C). The stereo output of the MP3 shield is amplified by 2 simple LM386 amplifiers, with 2x 3W 4Ω speakers.

IMG_20171114_110241

Above picture shows the prototype, in a box that i laser cut earlier for a different project (internet radio), but it really is an alarm clock 😉

The project is powered from a USB phone charger permanently plugged into a wall socket; we switch it on in the evening with the switch on the front panel, and off in the morning, after the alarm has gone off. I added a 1A fuse for safety.

Pressing the button starts a random song.

IMG_20171114_110314

The MP3 shield is based on the VS1053 chip, compatible with this Adafruit library.

The RTC uses I2C to connect to the Arduino (pins A4 and A5), and uses the RTClib library. My first version had a DS3231 but then i switched to DS1307 with no changes in the code. This picture shows a breadboard version, with the DS1307 RTC in the bottom left, and using an external amp with the mini-jack output of the MP3 shield.

IMG_20171115_174016 - Edited

I built the amplifier on top of the MP3 shield, as a DIY shield, on a piece of perf board with male headers that fit into MP3 shield. I needed to connect the OLED pins, and the I2C pins for the RTC, and the 2x LM386 amplifiers (below picture does not show the MOSFET, see further).

IMG_1886

IMG_1884

The MP3 shield music output is only available on a mini-jack plug, so i had to solder 3 wires to the pads on the bottom of the shield (L and R channels, GNDref). I would prefer to use a small stereo class D amplifier such as the PAM8403, but the problem is that the output of the MP3 shield is not referenced to the power supply GND, but to around 1.25V (if i remember correctly). This does not feed into the PAM8403 without filtering the DC component, which is a bit of a pain, so i decided to use the good old LM386 which has a differential input (pins 2 and 3). I was quite surprised with the good sound it produces, with minimal components: a large cap before the speaker, and a small cap+resistor to GND. The other large cap is for de-coupling.

The only issue was noise when not playing any song. So i added a P-channel MOSFET to power off the amplifiers while not in use, only switching on when a song is going to be played. I put a 220Ω resistor on the gate.

IMG_20171115_171906 - Edited

I used a USB power cable to connect to 5V and GND. In hindsight, i should have used the USB port on the Arduino, to be able to program it with the same cable, without having to take the Arduino out of the box. It would also be good to have the SD card easily accessible to install new songs.

The code defines an alarm time for weekdays, and for weekend days. It checks the RTC time in the loop, and if the time matches an alarm time, it chooses a random song and plays it, after switching on the amplifiers. It also plays a random song when the button is pressed.

Note 1: For reasons beyond my understanding, using the Serial prints interferes with the SD card file readings: when i put the debug boolean to ‘true’ the SD library failed to find any files on the SD card, or open any existing files.

Note 2: apologies for bad formatting below, WordPress does funny things to code


/*
  MP3 ALARM CLOCK

  Arduino UNO
  DS3231 RTC with super cap (old seeed item) -- or DS1307 with coin battery (telesky)
  MP3 VS1053 shield with SD card
  2x LM386 amp minimal config: small cap+10ohm resistor to GND on outputs, and 220uF caps before speakers 
  P-mosfet to switch amps on/off with 220ohm resistor on gate

  SD CARD: 8GB with standard FAT32 works fine
  (format FAT16 and limit to 2GB created problems with playing chunks of wrong files)
  FILE FORMAT: track0xx.mp3  from track000.mp3 to track099.mp3  (lower case .mp3 is fine)
  this sketch counts the files and choses a random track number from 0 to number_of_tracks-1
  
  Tom Tobback November 2017

*/
#include <avr/wdt.h>

#include    // Wire.h
#include "RTClib.h"
RTC_DS1307 rtc;                     // Seeedstudio RTC module with I2C: square dot=Vcc, SDA, SCL, n/c, GND
// or Telesky DS1307 P2 header

#include "U8glib.h"
U8GLIB_SSD1306_128X64 u8g(10, 5);		// OLED display: HW SPI CS = 10, A0/DC = 5 (Hardware Pins are  SCK/D0 = 13 and MOSI/D1 = 11) + RST to Arduino D4

// include SPI, MP3 and SD libraries
#include 
#include 
#include 

// These are the pins used for the music maker shield
#define SHIELD_RESET  8      // VS1053 reset pin (unused!)
#define SHIELD_CS     6      // VS1053 chip select pin (output)
#define SHIELD_DCS    7      // VS1053 Data/command select pin (output)

// These are common pins between breakout and shield
#define CARDCS 9     // SD Card chip select pin
// DREQ should be an Int pin, see http://arduino.cc/en/Reference/attachInterrupt
#define DREQ 2       // VS1053 Data request, ideally an Interrupt pin

#define BUTTON 3
// #define OLED_RST 4
#define MOSFET A2

const int volume = 50;  // lower is louder
int last_minute = 61;  // for checking if minute has changed, then display
boolean alarm_playing = false;
int track;              // number of track to play
int number_of_tracks = 0;  // to be checked at startup
boolean debug = false;      // true breaks SD card file reading..
////////////////////////////
const int weekday_alarm_hour = 6;
const int weekday_alarm_min = 0;
const int weekend_alarm_hour = 7;
const int weekend_alarm_min = 15;
////////////////////////////

Adafruit_VS1053_FilePlayer musicPlayer =
  Adafruit_VS1053_FilePlayer(SHIELD_RESET, SHIELD_CS, SHIELD_DCS, DREQ, CARDCS);

/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////

void setup () {

  wdt_enable(WDTO_4S);
  wdt_reset();

  randomSeed(analogRead(A0)); // for random playback

  pinMode(BUTTON, INPUT_PULLUP);   // for button
  pinMode(MOSFET, OUTPUT);         // amp switch
  digitalWrite(MOSFET, HIGH);   // switch off amp
  if (debug) {
    Serial.begin(9600);
    Serial.println();
    Serial.println("SETUP alarm clock");
  }

  ///////////////////////// OLED
  pinMode(4, OUTPUT);                            // RST pin for OLED display
  digitalWrite(4, LOW);
  delay(100);
  digitalWrite(4, HIGH);

  drawIntro();
  delay(2000);
  drawAlarmSetting();
  wdt_reset();

  ///////////////////  RTC ///////////////////////
  int rtn = I2C_ClearBus(); // clear the I2C bus first before calling Wire.begin()
  if (rtn != 0) {
    if (debug) Serial.println(F("I2C bus error. Could not clear"));
    if (rtn == 1) {
      if (debug) Serial.println(F("SCL clock line held low"));
    } else if (rtn == 2) {
      if (debug) Serial.println(F("SCL clock line held low by slave clock stretch"));
    } else if (rtn == 3) {
      if (debug) Serial.println(F("SDA data line held low"));
    }
  } else { // bus clear
    // re-enable Wire
    // now can start Wire Arduino master
    Wire.begin();
  }
  if (debug) Serial.println("RTC setup finished");

  rtc.begin();
  if (! rtc.isrunning()) {
    if (debug) Serial.println("RTC is NOT running!");
  }

  // This line sets the RTC with an explicit date & time, for example to set
  // rtc.adjust(DateTime(2015, 3, 24, 22, 03, 0));
  // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));         // this sets the RTC to computer time+date at upload

  wdt_reset();
  delay(100);

  ////////////////// SD and MP3 //////////////////////////////
  startMP3shield();
  wdt_reset();
  drawTracksFound();
  delay(1000);
}

/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////


void loop () {

  wdt_reset();

  DateTime now = rtc.now();

  int d = now.day();
  int m = now.month();
  int y = now.year();
  int h = now.hour();
  int mi = now.minute();
  int s = now.second();
  int wd = now.dayOfWeek();     // 1 = Mon

  if (debug) {
    Serial.print("time and weekday: ");
    Serial.print(y, DEC);
    Serial.print('/');
    Serial.print(m, DEC);
    Serial.print('/');
    Serial.print(d, DEC);
    Serial.print(' ');
    Serial.print(h, DEC);
    Serial.print(':');
    Serial.print(mi, DEC);
    Serial.print(':');
    Serial.print(s, DEC);
    Serial.print(' ');
    Serial.print(wd, DEC);
    if (musicPlayer.playingMusic) Serial.print(" playing");
    Serial.println();
  }

  if (!musicPlayer.playingMusic) digitalWrite(MOSFET, HIGH);   // switch OFF amp

  if (!alarm_playing) {
    checkAlarm(wd, h, mi);
  }
  else {
    if (!musicPlayer.playingMusic) {
      alarm_playing = false;
    }
  }

  wdt_reset();

  if (!digitalRead(BUTTON)) {
    if (debug) Serial.println("Button pressed");
    //    musicPlayer.stopPlaying();
    //    delay(1000);
    //    drawAlarmSetting();
    //    delay(1000);
    startRandomSong();
    delay(1000);
    last_minute = mi + 1;       // to make sure screen is refreshed
  }

  if (mi != last_minute) {                  // only refresh display when minute has changed
    u8g.firstPage();
    do {
      u8g.setFont(u8g_font_unifont);
      u8g.setPrintPos(30, 15);
      if (alarm_playing) {
        u8g.print("ALARM");
      }
      else {
        if (musicPlayer.playingMusic) {
          u8g.print("playing #");
          u8g.print(track);
        }
        else {
          u8g.print(d, DEC);
          u8g.print("-");
          u8g.print(m, DEC);
          u8g.print("-");
          u8g.print(y, DEC);
        }
      }

      u8g.setFont(u8g_font_fub25n);
      u8g.setPrintPos(20, 55);
      u8g.print(h, DEC);
      u8g.print(":");
      if (mi < 10) u8g.print("0"); u8g.print(mi, DEC); } while ( u8g.nextPage() ); last_minute = mi; digitalWrite(10, HIGH); } wdt_reset(); delay(200); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// /** This routine turns off the I2C bus and clears it on return SCA and SCL pins are tri-state inputs. You need to call Wire.begin() after this to re-enable I2C This routine does NOT use the Wire library at all. returns 0 if bus cleared 1 if SCL held low. 2 if SDA held low by slave clock stretch for > 2sec
           3 if SDA held low after 20 clocks.
*/
int I2C_ClearBus() {
  TWCR &= ~(_BV(TWEN)); //Disable the Atmel 2-Wire interface so we can control the SDA and SCL pins directly

  pinMode(SDA, INPUT_PULLUP); // Make SDA (data) and SCL (clock) pins Inputs with pullup.
  pinMode(SCL, INPUT_PULLUP);

  delay(2500);  // Wait 2.5 secs. This is strictly only necessary on the first power
  // up of the DS3231 module to allow it to initialize properly,
  // but is also assists in reliable programming of FioV3 boards as it gives the
  // IDE a chance to start uploaded the program
  // before existing sketch confuses the IDE by sending Serial data.

  boolean SCL_LOW = (digitalRead(SCL) == LOW); // Check is SCL is Low.
  if (SCL_LOW) { //If it is held low Arduno cannot become the I2C master.
    return 1; //I2C bus error. Could not clear SCL clock line held low
  }

  boolean SDA_LOW = (digitalRead(SDA) == LOW);  // vi. Check SDA input.
  int clockCount = 20; // > 2x9 clock

  while (SDA_LOW && (clockCount > 0)) { //  vii. If SDA is Low,
    clockCount--;
    // Note: I2C bus is open collector so do NOT drive SCL or SDA high.
    pinMode(SCL, INPUT); // release SCL pullup so that when made output it will be LOW
    pinMode(SCL, OUTPUT); // then clock SCL Low
    delayMicroseconds(10); //  for >5uS
    pinMode(SCL, INPUT); // release SCL LOW
    pinMode(SCL, INPUT_PULLUP); // turn on pullup resistors again
    // do not force high as slave may be holding it low for clock stretching.
    delayMicroseconds(10); //  for >5uS
    // The >5uS is so that even the slowest I2C devices are handled.
    SCL_LOW = (digitalRead(SCL) == LOW); // Check if SCL is Low.
    int counter = 20;
    while (SCL_LOW && (counter > 0)) {  //  loop waiting for SCL to become High only wait 2sec.
      counter--;
      delay(100);
      SCL_LOW = (digitalRead(SCL) == LOW);
    }
    if (SCL_LOW) { // still low after 2 sec error
      return 2; // I2C bus error. Could not clear. SCL clock line held low by slave clock stretch for >2sec
    }
    SDA_LOW = (digitalRead(SDA) == LOW); //   and check SDA input again and loop
  }
  if (SDA_LOW) { // still low
    return 3; // I2C bus error. Could not clear. SDA data line held low
  }

  // else pull SDA line low for Start or Repeated Start
  pinMode(SDA, INPUT); // remove pullup.
  pinMode(SDA, OUTPUT);  // and then make it LOW i.e. send an I2C Start or Repeated start control.
  // When there is only one I2C master a Start or Repeat Start has the same function as a Stop and clears the bus.
  /// A Repeat Start is a Start occurring after a Start with no intervening Stop.
  delayMicroseconds(10); // wait >5uS
  pinMode(SDA, INPUT); // remove output low
  pinMode(SDA, INPUT_PULLUP); // and make SDA high i.e. send I2C STOP control.
  delayMicroseconds(10); // x. wait >5uS
  pinMode(SDA, INPUT); // and reset pins as tri-state inputs which is the default state on reset
  pinMode(SCL, INPUT);
  return 0; // all ok
}


/// File listing helper
int printDirectory(File dir, int numTabs) {
  int n = 0;
  while (true) {

    File entry =  dir.openNextFile();
    if (! entry) {
      // no more files
      //Serial.println("**nomorefiles**");
      break;
    }
    for (uint8_t i = 0; i < numTabs; i++) {
      if (debug) Serial.print('\t');
    }
    if (debug) Serial.print(entry.name());
    if (entry.isDirectory()) {
      if (debug) Serial.println("/");
      printDirectory(entry, numTabs + 1);
    } else {
      // files have sizes, directories do not
      if (debug) {
        Serial.print("\t\t");
        Serial.println(entry.size(), DEC);
      }
      n++;
    }
    entry.close();
  }
  if (debug) {
    Serial.print("Number of tracks found: ");
    Serial.println(n);
  }
  return n;
}

////////////////////////////////////////////////////////////////////

void startMP3shield() {
  if (! musicPlayer.begin()) { // initialise the music player
    if (debug) Serial.println(F("Couldn't find VS1053, do you have the right pins defined?"));
    while (1);
  }
  if (debug) Serial.println(F("VS1053 found"));

  digitalWrite(MOSFET, LOW);   // switch ON amp
  delay(100);
  musicPlayer.sineTest(0x44, 10);    // Make a tone to indicate VS1053 is working
  delay(100);
  digitalWrite(MOSFET, HIGH);   // switch OFF amp

  if (!SD.begin(CARDCS)) {
    if (debug) Serial.println(F("SD failed, or not present"));
    while (1);  // don't do anything more
  }
  if (debug) Serial.println("SD OK!");

  // list and count files
  number_of_tracks = printDirectory(SD.open("/"), 0);

  // Set volume for left, right channels. lower numbers == louder volume!
  musicPlayer.setVolume(volume, volume);

  /***** Two interrupt options! *******/
  // This option uses timer0, this means timer1 & t2 are not required
  // (so you can use 'em for Servos, etc) BUT millis() can lose time
  // since we're hitchhiking on top of the millis() tracker
  //musicPlayer.useInterrupt(VS1053_FILEPLAYER_TIMER0_INT);

  // This option uses a pin interrupt. No timers required! But DREQ
  // must be on an interrupt pin. For Uno/Duemilanove/Diecimilla
  // that's Digital #2 or #3
  // See http://arduino.cc/en/Reference/attachInterrupt for other pins
  // *** This method is preferred
  if (! musicPlayer.useInterrupt(VS1053_FILEPLAYER_PIN_INT))
    if (debug) Serial.println(F("DREQ pin is not an interrupt pin"));

}

////////////////////////////////////////////////////////////////////

void drawIntro() {
  u8g.firstPage();
  do {
    u8g.setFont(u8g_font_unifont);
    u8g.drawStr(10, 20, "BuffaloLabs");
    u8g.drawStr(10, 50, "ALARM CLOCK");
  } while ( u8g.nextPage() );

}

////////////////////////////////////////////////////////////////////

void drawAlarmSetting() {
  u8g.firstPage();
  do {
    u8g.setFont(u8g_font_unifont);
    u8g.drawStr(10, 20, "Alarm set for:");
    u8g.setPrintPos(10, 40);
    u8g.print("weekday ");
    u8g.print(weekday_alarm_hour);
    u8g.print(":");
    if (weekday_alarm_min < 10) u8g.print("0");
    u8g.print(weekday_alarm_min);
    u8g.setPrintPos(10, 55);
    u8g.print("weekend ");
    u8g.print(weekend_alarm_hour);
    u8g.print(":");
    if (weekend_alarm_min < 10) u8g.print("0");
    u8g.print(weekend_alarm_min);
  } while ( u8g.nextPage() );

}

////////////////////////////////////////////////////////////////////

void drawTracksFound() {
  u8g.firstPage();
  do {
    u8g.setFont(u8g_font_unifont);
    u8g.drawStr(10, 20, "Tracks found:");
    u8g.setPrintPos(10, 50);
    u8g.print(number_of_tracks);
  } while ( u8g.nextPage() );

}

////////////////////////////////////////////////////////////////////

void drawFileNotFound(String f) {
  u8g.firstPage();
  do {
    u8g.setFont(u8g_font_unifont);
    u8g.drawStr(10, 20, "File not found:");
    u8g.setPrintPos(10, 50);
    u8g.print(f);
  } while ( u8g.nextPage() );

}

////////////////////////////////////////////////////////////////////

boolean startRandomSong () {

  musicPlayer.stopPlaying();
  if (debug) Serial.println("stopped music");

  char *filecc = "track0xx.mp3";
  while (1) {
    // track = random(0, number_of_tracks);  // 0 to number_of_tracks                     global var, to display
    track = millis() % number_of_tracks;   // random always returns same number, try different way
    if (debug) {
      Serial.print("Checking random track number: ");
      Serial.println(track);
    }
    if (track < 10) { filecc[6] = '0'; filecc[7] = 48 + track; // ascii value of number } else { filecc[6] = 48 + track / 10; filecc[7] = 48 + track % 10; } if (SD.exists(filecc)) { // if file found, exit loop if (debug) { Serial.print("Found file: "); Serial.println(filecc); } break; } else { drawFileNotFound(filecc); delay(1000); } } digitalWrite(MOSFET, LOW); // switch ON amp delay(100); if (! musicPlayer.startPlayingFile(filecc)) { if (debug) Serial.println("Could not open file"); return false; } else { if (debug) Serial.println("Started playing"); return true; } } //////////////////////////////////////////////////////////////////// void checkAlarm(int d, int h, int m) { if ((d > 0 && d < 6 && h == weekday_alarm_hour && m == weekday_alarm_min)  ||
      ((d == 6 || d == 0) && h == weekend_alarm_hour && m == weekend_alarm_min)) {
    if (debug) Serial.println("ALARM!");
    alarm_playing = startRandomSong();
  }
}

Leave a Reply

Your email address will not be published. Required fields are marked *