How the Code Works

If you want to make any changes to the design, you’ll need to understand a bit of how the code works. Certain things are dictated by the hardware, others are just programming shenanigans.

If you haven’t coded for the Wave Shield before, you’ll find easier-to-understand examples with the WaveHC library…the PiSpeakHC demo is pretty straightforward. The core parts of the talking clock code are very similar, no need to rehash that (or button debouncing) here.

The explanation needs to move around a bit, this isn’t entirely top-to-bottom.

Near the top of the clock code, before setup(), some pin numbers are #defined:

// Mouth LED needs PWM, limits pin options: must be a PWM-capable pin not
// in use by the Wave Shield nor interacting with Timer/Counter 1 (used by
// WaveHC).  Digital pin 6 is thus the only choice.
#define LEDMOUTH 6
// The trigger button and eye-blink LED can go on any remaining free pins.
#define LEDEYES  9
#define TRIGGER  A0

The mouth LED is “animated” using the Arduino’s analogWrite() function to vary the brightness. As explained on that function’s reference page, this is only available on certain pins (3, 5, 6, 9–11 on the Arduino Uno).

Meanwhile, the Wave Shield is using most of those pins for communicating with the DAC card and SD card (2–5, 10–13). It’s possible to rewire the shield and rewrite the code to use different pins, but that’s a bit of a nuisance and would break compatibility with all of the WaveHC example code! So there’s really no choice, the mouth LED needs to be on digital pin 6.

The analogWrite() reference mentions a problem with pin 6 though: it can’t display a 0% (off) duty cycle. As a workaround, later in the code, we simply set that pin to an INPUT when the mouth is not talking, and the LED will turn off:

      pinMode(LEDMOUTH, INPUT); // Disable mouth

(The pin is never explicitly set to OUTPUT in the code…analogWrite() already does that when it’s necessary.)


The WAV filenames are stored in a global set of tables before the setup() function. The PROGMEM directive is used so these strings are stored in flash memory instead of RAM (which is in very limited supply, especially in any Arduino code dealing with SD cards).

A quirk of PROGMEM makes it necessary first to declare all the strings, then follow with arrays containing references to these strings:

const char PROGMEM
  boot[] = "boot", annc[] = "annc", am[] = "am", pm[] = "pm",
  h01[] = "h01", h02[] = "h02", h03[] = "h03", h04[] = "h04",
  *hours[]  = { h12, h01, h02, h03, h04, h05, h06, h07, h08, h09, h10, h11 },
  *mins[]   = { m1, m2, m3, m4, m5, m6, m7, m8, m9 },
  *ampm[]   = { am, pm };

It’s explained a bit more on the Arduino PROGMEM reference page…and in more depth in this Adafruit guide…required reading for anyone dealing with RAM-hungry sketches!

PROGMEM strings can’t be accessed directly, one must use pgm_read_word() to access the pointer:

      playfile((char *)pgm_read_word(&hours[h])); // Say hour

No Weasels!

The very first thing in the setup() function is a trick we recently learned for avoiding the speaker “pop” at startup:

  mcpDacInit();               // Audio DAC is
  for(int i=0; i<2048; i++) { // ramped up to midpoint
    mcpDacSend(i);            // to avoid startup 'pop'

This shifts the digital-to-analog converter output from its default startup value (0) to the neutral speaker position where most WAVs start (2047) at a controlled rate.

Power Games

There are three GND pins and only one 5V pin on the Arduino. Sometimes it would be nice if there were just a couple extras…usually we use a small breadboard to provide more.

But, as long as the required power is very small (40 mA absolute max…ideally 20 mA or less) it’s totally legit to set an output pin as HIGH or LOW to provide an extra 5V or GND connection.

The DS1307 clock chip needs just a flea fart’s worth of power, so it’s no problem running the breakout board this way. The order of the pins on that board, coupled with the fact that Arduino pins A4 and A5 provide the I2C communication functions, means we can hang the board right off those pins and provide power through A2 and A3. The SQW pin isn’t used for this project, so it’s fine hanging off the edge.

Pins 7 and 8 provide grounds for the LEDs, and A1 is a ground for the pushbutton.

  // Sometimes having an extra GND pin near an LED or button is helpful.
  // Setting an output LOW is a hacky way to do this.  40 mA max.
  pinMode( 7, OUTPUT); digitalWrite( 7, LOW);
  pinMode( 8, OUTPUT); digitalWrite( 8, LOW);
  pinMode(A1, OUTPUT); digitalWrite(A1, LOW);

  // Along similar lines -- if using the DS1307 breakout board, it can
  // be plugged into A2-A5 (SQW pin overhangs the header, isn't used)
  // and 5V/GND can be provided through the first two pins.
  pinMode(A2, OUTPUT); digitalWrite(A2, LOW);
  pinMode(A3, OUTPUT); digitalWrite(A3, HIGH);

Having done this…between the Wave Shield, clock, button, LEDs and pins reserved for Serial use…we’ve now exhaused the Arduino’s entire complement of pins.

THEREFORE: if you want to make changes to the clock and need extra control pins (e.g. two separate LEDs for the eyes), then don’t this trick, or use less of it! The LEDs and trigger button can use any of the normal GND pins…the use of I/O pins for this was entirely a matter of proximity and convenience…you’ll simply need to run a wire to a different part of the board is all.

Walking while Chewing Gum

The loop() function is continually polling the button and plays sounds as necessary…the latter is handled by a separate function. Trying to use conventional program flow to keep the random eye blinks going while the code also manages these other tasks would make it really bloated and complex.

A timer interrupt provides a sort of multi-tasking, periodically calling a function to handle the eye blinks.

Timers are a hairy subject. There’s a very limited number of them (0, 1 and 2)…the first is already in use by the Arduino core library to provide PWM and the delay() and millis() functions…the second is used by WaveHC…leaving only Timer 2. Working with timers is not for the meek…it involves reading the ATmega328P datasheet and fiddling around with specific registers and bits:

  // A Timer/Counter 2 interrupt is used for eye-blink timing.
  // Timer0 already taken by delay(), millis(), etc., Timer1 by WaveHC.
  TCCR2A  = 0;                                 // Mode 0, OC2A/B off
  TCCR2B  = _BV(CS22) | _BV(CS21) | _BV(CS20); // 1024:1 prescale (~61 Hz)
  TIMSK2 |= _BV(TOIE2);                        // Enable overflow interrupt

The eye timing doesn’t require any super-specific frequency like 100.0 Hz. The way this one’s set up provides a 16,000,000 ÷ 1024 ÷ 256 = 61.035 Hz period, and for the sake of timing blinks it’s close enough to think of it as “60-ish Hz.”

An interrupt service routine (ISR)…in this case a Timer/Counter 2 overflow condition…then handles the periodic task:

ISR(TIMER2_OVF_vect) {

Dirty Pool

Finally, there’s the matter of modulating the mouth LED brightness in response to the audio being played. This uses a really dirty trick, nothing gentlemanly about it, and it would get you an “F” in a Computer Science class: we access one of the WaveHC library’s internal variables: playpos, a pointer to the value currently being output to the speaker.

The samples are presumed 16-bit. We look at just the high byte, it provides enough resolution for the animation, and track the minimum and maximum range over a very brief interval (however long it takes for 256 iterations of this loop to execute…which may actually be much quicker than 256 values from the WAV, that’s okay).

      // Sound level is determined through a nasty, grungy hack:
      // WaveHC library failed to make certain global variables static...
      // we can declare them extern and access them here.
      extern volatile int8_t *playpos; // Ooooh, dirty pool!
      int8_t s, lo=0, hi=0, counter=-1; // Current sample, amplitude range

      for(; wave.isplaying; ) {
        s = playpos[1];             // Audio sample high byte (-128 to +127)
        if(++counter == 0) {        // Mouth updates every 256 iterations
          int b = (hi - lo) * 4;    // Scale amplitudes for more brightness
          if(b > 255) b = 255;      // Cap at 255 (analogWrite limit)
          analogWrite(LEDMOUTH, b); // Update LED
          lo = hi = s;              // Reset amplitude range
        } else {
          if(s < lo)      lo = s;   // Track amplitude range
          else if(s > hi) hi = s;

Normally it’s good form for a C++ class to declare its internal variables as private, so they’re not accessible to outside code. This allows the developer of the class to make drastic internal architectural changes to the library without breaking outside code that relies on it…everything passes through an Officially Sanctioned Set of Methods and/or Variables That (ideally) Will Not Change Across Versions™.

So here we’re exploiting the fact that the WaveHC class variables are all public…we can get in there and peer at what the library’s doing. This is not without risk: if there’s any update to that library that either changes the code’s inner workings or simply declares those members private, our software breaks! This is why it’s bad form. If that should happen, we’d either have to require the use of an older version of the library, or make our own fork that provides a clean and proper interface for similar functionality.

Can I use NeoPixels instead of discrete LEDs?

No. The realtime requirements of the Wave Shield and NeoPixels don’t play nice together. But if you have two pins available, WS2801 or LPD8806 LED pixels are a possibility!

How about LED matrices?

Yes, but…

It’s a substantial addition, and we don’t have a ready-made recipe for this. The Animating Multiple LED Backpacks guide may offer some insights…it combines a Wave Shield and LED matrices for a talking face, but not reading the time. You’d need to devise a “mash up” of these two.

I’m trying to sleep but that blinking is driving me crazy. Can the eyes stay lit?

Sure…just comment out the line in the interrupt routine where the eyes turn off:

//      digitalWrite(9, LOW);
Last updated on 2015-05-04 at 04.27.56 PM Published on 2014-08-26 at 03.49.34 PM