Pixel Clock

electronics
Published

July 31, 2019

A large (~20” × 30”) wall clock I designed and built. A 3D-printed array of ~1” cubes lines up with cheap APA102 RGB LED strips. An Arduino controls the lighting and reads from an RTC. An acrylic case was laser cut from Pololu — transparent back and sides, frosted front to diffuse the light.

There’s also a microphone + spectrum analyzer chip (MSGEQ7) that reads ambient sound to make the lights react to nearby music. The spectrum visualizer is still a work in progress — the first chip didn’t give great response.

Hardware

  • Display: 21×13 grid of APA102 LEDs (273 total)
  • MCU: Arduino
  • RTC: DS3231 (time kept via __TIME__ compile constant, updated by RTC)
  • Audio: MSGEQ7 spectrum analyzer, microphone
  • Case: Laser-cut acrylic from Pololu

Firmware

Show firmware
//this version will dynamically determine min and max for each channel and implement a tasker. also smooth out the bars a little
//other screen ideas: solid background that changes with daytime and a rainbow clock layover
// make into alarm clock. buttons would help set time and programmable sunrise time. also a switch to toggle alarm function on/off
//add 3 rotary encoders. 1) screen selection, 2) brightness, and 3) alarm/time setting
#include <AudioAnalyzer.h>
#include <FastLED.h>
//task schedule intialization stuff
//#define _TASK_MICRO_RES //uncomment for microseconds
#include <TaskScheduler.h>

//Tasks
void adjustFreqMinMax();
void getFreqs();
void drawScreen();
void keepTime();
Task adjustFreqMinMax_task(50, TASK_FOREVER, &adjustFreqMinMax);
Task getFreqs_task(30, TASK_FOREVER, &getFreqs);
Task drawScreen_task(15, TASK_FOREVER, &drawScreen);
Task keepTime_task(1000, TASK_FOREVER, &keepTime);
Scheduler runner;

Analyzer Audio = Analyzer(A3,A4,A5);//Strobe pin ->3  RST pin ->4 Analog Pin ->5

#define DATA_PIN    4
#define CLOCK_PIN   3
#define BRIGHTNESS  25
#define LED_TYPE    APA102
#define COLOR_ORDER BGR
const uint8_t kMatrixWidth = 21;
const uint8_t kMatrixHeight = 13;
const bool    kMatrixSerpentineLayout = true;
#define NUM_LEDS (kMatrixWidth * kMatrixHeight)
CRGB leds[NUM_LEDS];

uint16_t freqMinVals[7] = {1023,1023,1023,1023,1023,1023,1023};
uint16_t freqMaxVals[7] = {1023,1023,1023,1023,1023,1023,1023};
int FreqVal[7];
uint16_t castFreqVal[7];
uint8_t currentBarVals[7]; //actual value of bar graph based on current measurement
uint8_t displayedBarVals[7]; //bar value that is shown on screen. this will be smoothed vs currentBarVals
uint8_t screenIndex = 0; //this will control which screen is displayed at any time.

//__TIME__ is a pointer and format is 12:34:56
uint8_t fullHour = atoi(__TIME__); //save this for scaling colors over 24h period
uint8_t hour = atoi(__TIME__)%12; //us time
uint8_t minute = atoi(__TIME__+3);
uint8_t second = atoi(__TIME__+6);

const uint8_t numberIndices[10][13][2] = { //this contains the pixels for each digit 0-9. each row is padded with the first pixel in case you accidentally iterate through the whole loop rather than the length defined in numberIndicesLength
  {{0,0},{1,0},{2,0},{0,1},{2,1},{0,2},{2,2},{0,3},{2,3},{0,4},{1,4},{2,4},{0,0}},
  {{2,0},{2,1},{2,2},{2,3},{2,4},{2,0},{2,0},{2,0},{2,0},{2,0},{2,0},{2,0},{2,0}},
  {{0,0},{1,0},{2,0},{2,1},{0,2},{1,2},{2,2},{0,3},{0,4},{1,4},{2,4},{0,0},{0,0}},
  {{0,0},{1,0},{2,0},{2,1},{0,2},{1,2},{2,2},{2,3},{0,4},{1,4},{2,4},{0,0},{0,0}},
  {{0,0},{2,0},{0,1},{2,1},{0,2},{1,2},{2,2},{2,3},{2,4},{0,0},{0,0},{0,0},{0,0}},
  {{0,0},{1,0},{2,0},{0,1},{0,2},{1,2},{2,2},{2,3},{0,4},{1,4},{2,4},{0,0},{0,0}},
  {{0,0},{1,0},{2,0},{0,1},{0,2},{1,2},{2,2},{0,3},{2,3},{0,4},{1,4},{2,4},{0,0}},
  {{0,0},{1,0},{2,0},{2,1},{2,2},{2,3},{2,4},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}},
  {{0,0},{1,0},{2,0},{0,1},{2,1},{0,2},{1,2},{2,2},{0,3},{2,3},{0,4},{1,4},{2,4}},
  {{0,0},{1,0},{2,0},{0,1},{2,1},{0,2},{1,2},{2,2},{2,3},{2,4},{0,0},{0,0},{0,0}}
};
const uint8_t numberIndicesLength[10] = {12,5,11,11,9,11,12,7,13,10};

void setup()
{
  Serial.begin(9600);   //Init the baudrate
  Audio.Init();//Init module
  FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN, COLOR_ORDER, DATA_RATE_MHZ(1)>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip );
  FastLED.setBrightness(  BRIGHTNESS );

  runner.init();
  runner.addTask(adjustFreqMinMax_task);
  adjustFreqMinMax_task.enable();
  runner.addTask(getFreqs_task);
  getFreqs_task.enable();
  runner.addTask(drawScreen_task);
  drawScreen_task.enable();
  runner.addTask(keepTime_task);
  keepTime_task.enable();
}

void loop()
{
  runner.execute();
}

void rainbowScreenWithClock()
{
  uint8_t currentTime = millis() >> 4;
  paintRainbowBackground(currentTime); //this paints the background

  currentTime = millis() >> 3;
  paintTime(hour, minute, 100, 1, currentTime, 0, 0);  //this paints the numbers, the 1 value means use rainbow
}

uint8_t determineTimeWidth(uint8_t hour, uint8_t minute){
  uint8_t digit1 = 0;
  uint8_t digit2 = 3;
  uint8_t digit3 = 3;
  uint8_t digit4 = 3;
  if(hour/10==1){digit1 = 2;}
  if(hour%10==1){digit2 = 1;}
  if(minute/10==1){digit3 = 1;}
  if(minute%10==1){digit4 = 1;}
  return digit1 + digit2 + 3 + digit3 + 1 + digit4;
}

void complementEqualizer()
{
  uint8_t currentTime = millis() >> 5;
  for (uint8_t x = 0; x < kMatrixWidth; x++)
  {
    for (uint8_t y = 0; y < kMatrixHeight; y++)
    {
      leds[ XY( x, y) ] = CHSV(currentTime%255,255,255);
    }
  }

  for(uint8_t x = 0;x<kMatrixWidth;x++)
  {
    for(uint8_t y = 0;y<kMatrixHeight;y++)
    {
      if(y<=kMatrixHeight-displayedBarVals[x/3])
      {
        // background only
      }
      else
      {
        leds[ XY( x, y) ] = CHSV((currentTime+128)%255, 255, 255);
      }
    }
  }
  paintTime(hour, minute, 100, 1, currentTime, (kMatrixWidth-determineTimeWidth(hour, minute))/2-1, 3);
}

void keepTime()
{
  second++;
  if(second>=60)
  {
    second = 0;
    minute++;
  }
  if(minute>=60)
  {
    minute = 0;
    hour++;
    hour = hour%12;
  }
}

void blankScreen()
{
  for(uint16_t z=0;z<NUM_LEDS;z++)
  {
    leds[z] = CHSV(255,0,0);
  }
}

void drawScreen()
{
  if(screenIndex==0) complementEqualizer();
  if(screenIndex==1) rainbowScreenWithClock();
  if(screenIndex==2) blankScreen();
  FastLED.show();
}

void getFreqs(void)
{
  Audio.ReadFreq(FreqVal);
  for(uint8_t iter=0;iter<7;iter++)
  {
    castFreqVal[iter] = (uint16_t)FreqVal[iter];
    if(castFreqVal[iter]<freqMinVals[iter]) freqMinVals[iter] = castFreqVal[iter];
    if(castFreqVal[iter]>freqMaxVals[iter]) freqMaxVals[iter] = castFreqVal[iter];
    currentBarVals[iter] = (uint8_t)map(castFreqVal[iter],freqMinVals[iter],freqMaxVals[iter],0,kMatrixHeight);
    if(currentBarVals[iter]>displayedBarVals[iter]) displayedBarVals[iter]++;
    else if(currentBarVals[iter]<displayedBarVals[iter]) displayedBarVals[iter]--;
  }
}

void adjustFreqMinMax(void) //every few seconds decrease the max value and increase the min value
{
  for(uint8_t x = 0;x<7;x++)
  {
    if(freqMinVals[x]<1023) freqMinVals[x]++;
    if(freqMaxVals[x]>1) freqMaxVals[x]--;
  }
}

uint16_t XY( uint8_t x, uint8_t y)
{
  uint16_t i;

  if( kMatrixSerpentineLayout == false) {
    i = (y * kMatrixWidth) + x;
  }

  if( kMatrixSerpentineLayout == true) {
    if( y & 0x01) {
      uint8_t reverseX = (kMatrixWidth - 1) - x;
      i = (y * kMatrixWidth) + reverseX;
    } else {
      i = (y * kMatrixWidth) + x;
    }
  }

  return i;
}

void paintRainbowBackground(uint8_t timeVal)
{
  for (uint8_t x = 0; x < kMatrixWidth; x++)
  {
    for (uint8_t y = 0; y < kMatrixHeight; y++)
    {
      uint16_t p = (timeVal - x * 10 - y * 5 - x * y)%255;
      leds[ XY( x, y) ] = CHSV((uint32_t)p, 255, 255);
    }
  }
}

void paintTime(uint8_t hour, uint8_t minute, uint8_t color, boolean rainbow, uint8_t rainbowTime, uint8_t xOffset, uint8_t yOffset)
{
  uint8_t timeWidth = determineTimeWidth(hour, minute);
  uint8_t currentColumn = (kMatrixWidth - timeWidth)/2 + xOffset;
  uint16_t p = 0;
  uint8_t tempX = 0;
  uint8_t tempY = 0;
  uint8_t timeRowOffset = (kMatrixHeight - 5)/2 - yOffset;
  if(hour/10==1)
  {
    for(uint8_t z = 0;z<numberIndicesLength[1];z++)
    {
      tempX = currentColumn + numberIndices[1][z][0] - 2;
      tempY = timeRowOffset + numberIndices[1][z][1];
      if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
      else p = color;
      leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
    }
    currentColumn = currentColumn + 2;
  }
  if(hour%10==1)
  {
    for(uint8_t z = 0;z<numberIndicesLength[1];z++)
    {
      tempX = currentColumn + numberIndices[1][z][0] - 2;
      tempY = timeRowOffset + numberIndices[1][z][1];
      if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
      else p = color;
      leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
    }
    currentColumn = currentColumn + 2;
  }
  else
  {
    for(uint8_t z = 0;z<numberIndicesLength[hour%10];z++)
    {
      tempX = currentColumn + numberIndices[hour%10][z][0];
      tempY = timeRowOffset + numberIndices[hour%10][z][1];
      if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
      else p = color;
      leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
    }
    currentColumn = currentColumn + 4;
  }
  tempX = currentColumn;
  tempY = timeRowOffset+1;
  if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
  else p = color;
  leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
  tempY = timeRowOffset+3;
  if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
  else p = color;
  leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
  currentColumn = currentColumn + 2;
  if(minute/10==1)
  {
    for(uint8_t z = 0;z<numberIndicesLength[1];z++)
    {
      tempX = currentColumn + numberIndices[1][z][0] - 2;
      tempY = timeRowOffset + numberIndices[1][z][1];
      if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
      else p = color;
      leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
    }
    currentColumn = currentColumn + 2;
  }
  else
  {
    for(uint8_t z = 0;z<numberIndicesLength[minute/10];z++)
    {
      tempX = currentColumn + numberIndices[minute/10][z][0];
      tempY = timeRowOffset + numberIndices[minute/10][z][1];
      if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
      else p = color;
      leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
    }
    currentColumn = currentColumn + 4;
  }
  if(minute%10==1)
  {
    for(uint8_t z = 0;z<numberIndicesLength[1];z++)
    {
      tempX = currentColumn + numberIndices[1][z][0] - 2;
      tempY = timeRowOffset + numberIndices[1][z][1];
      if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
      else p = color;
      leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
    }
    currentColumn = currentColumn + 2;
  }
  else
  {
    for(uint8_t z = 0;z<numberIndicesLength[minute%10];z++)
    {
      tempX = currentColumn + numberIndices[minute%10][z][0];
      tempY = timeRowOffset + numberIndices[minute%10][z][1];
      if(rainbow){p = (rainbowTime + tempX * 10 + tempY * 5 - tempX * tempY)%255;}
      else p = color;
      leds[ XY( tempX, tempY) ] = CHSV((uint32_t)p, 255, 255);
    }
  }
}