MyRaspberryAndMe

Tinkering with Raspberry (and other things)

Tiny Word Clock with Attiny85

The “Hello World” of microcontroller projects undoubtedly is a clock. As this was my first try with an Attiny85 I decided to build a tiny version of my big living room word clock (which is 40 x 40 cm) and put it into an IKEA picture frame.
I have been using an 8×8 LED-matrix (WS218b, Adafruit Neopixel compatible) and the first challenge was to get the German words for the different times into this 64 positions. The source code is heavily based on Adafruits tutorials.I am describing this clock with a German layout. The source code is fairly simple, be welcome to adapt it to any language you see fit.

Displaying the (German) time in 8×8

To fit all possible time descriptions in an 8×8 matrix I had to make some compromises. So the German “Viertel nach”, which simply means “a quarter past”, is reduced to “Fünfzehn nach” (fifteen past). This only works in German as “Fünf” (5) and “Zehn” (10) combine seamlessly to “Fünfzehn” (15). The same goes for “Viertel vor” (a quarter to).
“Halb Vier” (3:30 = half past three) is one word, “Eins” (one) and “Sechs” (six) share the “s”, “Fünf” (five) is displayed vertically and “Sieben” (seven) spans two lines.
Take a look at the matrix:

 F Ü N F Z E H N
 V O R G N A C H
 H A L B V I E R
 E I N S E C H S
 S I E Z W Ö L F
 B E N D R E I Ü
 W Z E H N E U N
 A C H T M E L F

As I am writing this blog post I seem to find more and more examples of this layout. Apparently searching for “micro word clock” leads to different projects than searching for “tiny wordclock”. I started this project some time ago so I am not sure anymore where the idea for this layout came from. It is absolutely possible that I saw it on some project page in the past and just happened to remember it. In one clear sentence: this has been done before and I am pretty sure I did not “invent” it. Credits go to all the makers out there who did the same a long time before me.

Real Time Clock with DS3231

In the past I made some arduino clocks with the standard DS1307 circuit but I haven’t been too happy with it as this circuit is prone to thermal effects and not too accurate. So this time I decided to use a DS3231 module. These are fully compatible with the DS1307 so all libraries can be used without alterations. The chip compensates thermal effects and is much more accurate.

I got my modules from ebay (of course)… The modules are designed for rechargeable coin cells but most of the modules you can get ship with ordinary coin cells (or none at all). If you plan on using the module with an ordinary CR2032 coin cell you need to disable the loading circuit. This can be done by desoldering the diode and the resistor located in the red markings in the following picture:

ds3231_board

Do not connect power to the module if a non-rechargeable cell is mounted in the module. The coin cell can (and will) explode! Remove the loading circuit first.

Putting everything together

A clock is pretty useless if one can’t set the time. So I needed two switches for setting the minutes and the hours. And of course it would be nice to be able to select the color of the watch. Using RGB LEDs would be pretty useless if the color couldn’t be set.

Three buttons, two pins

This means that three push buttons are needed but after connecting the data line of the LED-matrix and the SDA and SCL lines, only two pins are left (I wanted to retain the reset pin). The solution is to combine a voltage divider with two push buttons and connect it to an anlogue port. Thus two buttons share one pin and by detecting the voltage the button pressed is identified. Take a look at the schematic:

The buttons for setting  the clock are connected to pin 2. R1 and R2 have identical values. If both push buttons are open, R2 simply serves as a pull-up resistor, leading to +5V at pin 2.
Pushing the HOUR_BTN pulls the pin to ground (0V @ pin 2). The voltage divider comes into effect when the MINUTE_BTN is pushed. The circuit reduces to VCC->R1->R2->GND. As R1 and R2 have the same value, half the VCC voltage drops at R2, the other half at R1. Pin 2 is connected between R1 and R2, thus with the MINUTE_BTN pressed there are roughly 2.5V at pin 2. Easy, yet cool.

Because of the simplicity of the circuit everything fit perfectly on a little piece of perfboard. I won’t post a PCB layout here, just position the Attiny and begin soldering.

Selecting colors

As the LEDs are able to display all RGB values it would have been cool to have three potentiometers to select the color. Apparently the Attiny does not have enough input pins, so I decided to have an array of preselected color values that are switched in sequence with a push button. The selected color value is saved in the EEPROM so that when switched on/off, the clock remembers the color.

Assembly

I wanted to fit everything into an IKEA RIBBA frame (10x15cm). As I do not own a 3D-printer, a laser-cutter or other fancy things, I went the cardboard way. It was meant to be a prototype but worked so well that I just left it that way. Here are some images that should give you an idea how everything fits together:

The “watch face” is simply printed on white paper. The print is mirrored so that it faces the “inside” of the picture frame. I have been using a scan of the actual LED-matrix and then created the layout in Inkscape.
When switched off the picture frame appears to be just white, the letters can only be seen when switched on.

The code

The code is completely comaptible with any Arduino, I developed it using the Arduino IDE and went this way to program the Attiny. Everything is straightforward so I did not add too many comments. The libraries used do have extensive documentation and as I mentioned earlier, the idea and basic parts of the code are inspired by Adafruit’s Wordclock tutorial.

/**
  *    Tiny Matrix Clock with Attiny/Arduino
  *    Copyright (C) 2017  Thomas Henkel (mypiandme@gmail.com)
  *
  *    This program is free software: you can redistribute it and/or modify
  *    it under the terms of the GNU General Public License as published by
  *    the Free Software Foundation, either version 3 of the License, or
  *    any later version.
  *
  *    This program is distributed in the hope that it will be useful,
  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  *    GNU General Public License for more details.
  *
  *    You should have received a copy of the GNU General Public License
  *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  *
 **/

#include <FastLED.h>
#include <TinyWireM.h>
#include <TinyRTClib.h>
#include <avr/pgmspace.h>
#include <EEPROM.h>

#define NUM_LEDS 64
#define DATA_PIN 1

RTC_DS1307 clock;

// declare variables to be reused.
uint8_t mins;
uint8_t lastminute;
bool changeFlag = false;

int analogPinSet = A3;
int analogPinColor = A2;

CRGB leds[NUM_LEDS];
uint8_t brightness = 16;

const PROGMEM uint32_t colors[] = { 0xffffff, 0x0000ff, 0x00ff00, 0xff0000, 0x00ffff, 0x7fffd4, 
                                    0x7fff00, 0x00008b, 0xff00ff, 0xffff00, 0x778899, 0xffa500 };

const uint8_t numColors = 12;
uint64_t mask;
uint8_t currentColorIndex = 0; // start with white
boolean randColor = false;
uint32_t currentColor;

#define MFUENF  mask |= 0xF000000000000000
#define MZEHN   mask |= 0x0F00000000000000
#define VOR     mask |= 0xE0000000000000
#define NACH    mask |= 0x0F000000000000
#define HALB    mask |= 0xF00000000000
#define VIER    mask |= 0x0F0000000000
#define EINS    mask |= 0xF000000000
#define SECHS   mask |= 0x1F00000000
#define SIEBEN  mask |= 0xE0E00000
#define ZWOELF  mask |= 0x1F000000
#define DREI    mask |= 0x1E0000
#define ZEHN    mask |= 0x7800
#define NEUN    mask |= 0x0F00
#define ACHT    mask |= 0xF0
#define ELF     mask |= 0x07
#define FUENF   mask |= 0x01010101
#define ZWEI    mask |= 0x18060000

void setup() {
  // put your setup code here, to run once:
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
  mask = 0;
  randomSeed(analogRead(A0));
  lastminute = 99;
  TinyWireM.begin();
  clock.begin();
  
  testDisplay();

  currentColorIndex = readCurrentColorIndex();
  if( currentColorIndex >= numColors )
    randColor = true;

  currentColor = pgm_read_dword(colors + currentColorIndex );
  testDisplay();
}

void loop() {
  checkSetButtons();
  checkColorButtons();

  DateTime now = clock.now();
  mins = now.minute();

  if ( changeFlag || (mins != lastminute) ) {
    lastminute = mins;
    changeFlag = false;
    calculateMask(now.hour(), mins );
    applyMaskToMatrix();
  }

/**  TESTING
  for (int h=0; h<24; h++) {
    for (int m=0; m<61; m++) { calculateMask(h, m); applyMaskToMatrix(); } } **/ } uint8_t readCurrentColorIndex() { uint8_t c = EEPROM.read(0); return c; delay(10); } void writeCurrentColorIndex() { EEPROM.write(0, currentColorIndex); delay(10); } void checkColorButtons() { int value = analogRead(analogPinColor); if (value > 1000) {
    return; // no button pressed;
  }
  if (value < 10) { if (randColor) { // if random colors are displayed stop random display // and set to first color currentColorIndex = 0; randColor = false; } else { // if not displaying random colors increase colorIndex currentColorIndex++; if( currentColorIndex == numColors ) { // colorIndex is at the end of array entries, so // start over with random colors randColor = true; currentColorIndex = 0; } } // having set the new color call testDisplay currentColor = pgm_read_dword(colors + currentColorIndex ); testDisplay(); changeFlag = true; // and save to RAM writeCurrentColorIndex(); } } void checkSetButtons() { int value = analogRead(analogPinSet); if (value > 1000) {
    return; // no button pressed
  }
  if (value<10) { // increase hour by one uint8_t hrs = clock.now().hour() +1; if (hrs >23) hrs = 0;
    clock.adjust(DateTime(clock.now().year(), clock.now().month(), clock.now().day(),
                hrs, clock.now().minute(), clock.now().second()));
    delay(300);
    changeFlag = true;
    return;
  }
  if(value>500 && value<530) { // minute button uint8_t mins = clock.now().minute() +1; if(mins>59) mins=0;
    clock.adjust(DateTime(clock.now().year(), clock.now().month(), clock.now().day(),
                clock.now().hour(),mins, 0));
    delay(300);
    changeFlag = true;
    return;
  }
}

void testDisplay() {
  for (byte i=0; i<64; i++) {
    if( !randColor ) {
       leds[i].setColorCode(currentColor);
    }
    if( randColor ) {
       leds[i].setRGB(random(256), random(256), random(256));
    }
  }
  FastLED.show(brightness);
  delay(700);
  //changeFlag = true;
}
void applyMaskToMatrix() {
  for (byte i = 0; i < 64; i++) { boolean light = bitRead(mask, 63-i); switch (light) { case 0: leds[i] = CRGB::Black; break; case 1: //leds[i] = CRGB::White; if( !randColor ) { leds[i].setColorCode(currentColor); } if( randColor ) { leds[i].setRGB(random(256), random(256), random(256)); } break; } } FastLED.show(brightness); delay(200); mask = 0; } void calculateMask(uint8_t hr, uint8_t mn) { if ((mn > 4) && (mn<10)) { MFUENF; NACH; } if ((mn>9) && (mn<15)) { MZEHN; NACH; } if ((mn>14) && (mn<20)) { MFUENF; MZEHN; NACH; } // from this point on hr gets increased by 1 // as we are displaying "xx vor halb" etc... if ((mn>19) && (mn<25)) { hr++; MZEHN; VOR; HALB; } if ((mn>24) && (mn<30)) { hr++; MFUENF; VOR; HALB; } if ((mn>29) && (mn<35)) { hr++; HALB; } if ((mn>34) && (mn<40)) { hr++; MFUENF; NACH; HALB; } if ((mn>39) && (mn<45)) { hr++; MZEHN; NACH; HALB; } if ((mn>44) && (mn<50)) { hr++; MFUENF; MZEHN; VOR; } if ((mn>49) && (mn<55)) { hr++; MZEHN; VOR; } if (mn>54) {
    hr++;
    MFUENF;
    VOR;
  }

  // now add the hour to display
  switch (hr) {
    case 1:
    case 13:
      EINS;
      break;
    case 2:
    case 14:
      ZWEI;
      break;
    case 3:
    case 15:
      DREI;
      break;
    case 4:
    case 16:
      VIER;
      break;
    case 5:
    case 17:
      FUENF;
      break;
    case 6:
    case 18:
      SECHS;
      break;
    case 7:
    case 19:
      SIEBEN;
      break;
    case 8:
    case 20:
      ACHT;
      break;
    case 9:
    case 21:
      NEUN;
      break;
    case 10:
    case 22:
      ZEHN;
      break;
    case 11:
    case 23:
      ELF;
      break;
    case 12:
    case 24:  // need this because of increase in minute calculation
    case 0:
      ZWOELF;
      break;
  }
}

Advertisements

Comments are closed.