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);
}
}
}











