/*-------------------------------------------------------------------------
The Roman Numerial Clock
Concept: Radarmus (https://www.thingiverse.com/thing:4771222)
2021-03-02 V1 John Bradnam (jbrad2089@gmail.com)
- LEDs are an array of 4 columns | \ / _ (anodes) x 14 digits (cathodes)
Cathodes are connected to a STP16CPS05 16 Channel shift register with constant current outputs
Anodes are connected to 4 P-Channel MOSFETs (Active LOW)
- Created code base
*/
//#define DEBUG
#include <SPI.h>// SPI Library used to clock data out to the shift registers
#include <TimeLib.h>
#include <DS1302RTC.h>
#define DATA_PIN 11 // used by SPI, must be pin 11
#define CLOCK_PIN 13 // used by SPI, must be 13
#define BLANK_PIN 8 // same, can use any pin except 12 you want for this, just make sure you pull up via a 1k to 5V
#define BLANK_PORT PORTB //Port for BLANK pin
#define BLANK_BIT 0 //Pin number of BLANK pin
#define LATCH_PIN 10 //can use any pin except 12 you want to latch the shift registers
#define LATCH_PORT PORTB //Port for LATCH pin
#define LATCH_BIT 2 //Pin number of LATCH pin
#define ANODE_A A0 // A anode
#define ANODE_B A1 // B anode
#define ANODE_C A2 // C anode
#define ANODE_D A3 // D anode
#define RTC_CE 7 // RTC CE pin
#define RTC_IO 4 // RTC IO pin
#define RTC_CLK 3 // RTC SCLK pin
#define SW_SET 2 // SET switch input
#define SW_UP 5 // UP switch input
#define SW_DOWN 6 // DOWN switch input
#define ROWS 4 // Number of rows of LEDs
#define LEDS_PER_ROW 16 // Number of leds on each row
#define BYTES_PER_ROW 2 // Number of bytes required to hold one bit per LED in each row
//Bit buffer for matrix
byte ledStates[ROWS][BYTES_PER_ROW]; //Store state of each LED (either off or on)
byte ledNext[ROWS][BYTES_PER_ROW]; //Double buffer for fast updates
int activeRow = 0; //this increments through the anode levels
//The digits are not wired to the corresponding pins on the STP16CPS05
//This is to simplify routing on the PCB. This table maps the logical channels
//to the physical pins.
uint8_t digitMap[] = {0,1,7,6,5,4,3,15,14,8,9,10,11,2,12,13};
//Bit order is abcd, digits are left to right
// | \ /
// a b c
// | \ /
// ----d----
const uint16_t unitsFont[] PROGMEM =
{
0b0000000000000000, //0
0b1000000000000000, //1
0b1000100000000000, //2
0b1000100010000000, //3
0b1000101000000000, //4
0b1010000000000000, //5
0b1010100000000000, //6
0b1010100010000000, //7
0b1010100010001000, //8
0b1000011000000000 //9
};
const uint16_t tensFont[] PROGMEM =
{
0b0000000000000000, //0
0b0110000000000000, //10
0b0110011000000000, //20
0b0110011001100000, //30
0b0110100100000000, //40
0b1001000000000000, //50
0b1001011000000000, //60
0b1001011001100000, //70
0b1001011001100110, //80
0b0110100100000000 //90 (no C)
};
//Secondary menus
enum ClockEnum { CLK, CLK_H, CLK_M };
ClockEnum clockMode = CLK;
//OK I know the DS1302 is a pretty crappy RTC but I have a lot of them to use up :-)
DS1302RTC rtc(RTC_CE, RTC_IO, RTC_CLK);
#define FLASH_TIME 100 //Time in mS to flash digit being set
#define STEP_TIME 350 //Time in mS for auto increment or decrement of time
int lastMinutes = -1; //Used to detect change in minute to update display
int nowH = 0; //Current Hour
int nowM = 0; //Current Minute
int setH = 0; //Hour being set
int setM = 0; //Minute being set
long flashTimeout = 0; //Flash timeout when setting clock or alarm
bool flashOn = false; //Used to flash display when setting clock or alarm
long stepTimeout = 0; //Set time speed for auto increment or decrement of time
//---------------------- General initialisation ----------------------------
void setup()
{
SPI.setBitOrder(MSBFIRST);//Most Significant Bit First
SPI.setDataMode(SPI_MODE0);// Mode 0 Rising edge of data, keep clock low
SPI.setClockDivider(SPI_CLOCK_DIV2);//Run the data in at 16MHz/2 - 8MHz
#ifdef DEBUG
Serial.begin(115200);// if you need it?
#endif
noInterrupts();// kill interrupts until everybody is set up
clearDisplay(); //Clear the primary buffer
refresh(); //Transfer to display buffer
activeRow = 0;
//We use Timer 1 to refresh the display
TCCR1A = B00000000; //Register A all 0's since we're not toggling any pins
TCCR1B = B00001011; //bit 3 set to place in CTC mode, will call an interrupt on a counter match
//bits 0 and 1 are set to divide the clock by 64, so 16MHz/64=250kHz
TIMSK1 = B00000010; //bit 1 set to call the interrupt on an OCR1A match
OCR1A = 150; // you can play with this, but I set it to 150, which means:
// our clock runs at 250kHz, which is 1/250kHz = 4us
// with OCR1A set to 150, this means the interrupt will be called every (150+1)x4us 0.6mS,
// which gives a refresh rate (all 4 anodes) of 417 times per second
//finally set up the Outputs
pinMode(LATCH_PIN, OUTPUT);//Latch
pinMode(DATA_PIN, OUTPUT);//MOSI DATA
pinMode(CLOCK_PIN, OUTPUT);//SPI Clock
//Setup anode pins
pinMode(ANODE_A, OUTPUT);
pinMode(ANODE_B, OUTPUT);
pinMode(ANODE_C, OUTPUT);
pinMode(ANODE_D, OUTPUT);
//Setup switches
pinMode(SW_SET, INPUT_PULLUP);
pinMode(SW_UP, INPUT_PULLUP);
pinMode(SW_DOWN, INPUT_PULLUP);
//Setup RTC pins
//Check if RTC has a valid time/date, if not set it to 00:00:00 01/01/2018.
//This will run only at first time or if the coin battery is low.
//setSyncProvider() causes the Time library to synchronize with the
//external RTC by calling RTC.get() every five minutes by default.
setSyncProvider(rtc.get);
if (timeStatus() != timeSet)
{
#ifdef DEBUG
Serial.println("Setting default time");
#endif
//Set RTC
tmElements_t tm;
tm.Year = CalendarYrToTm(2020);
tm.Month = 06;
tm.Day = 26;
tm.Hour = 7;
tm.Minute = 52;
tm.Second = 0;
time_t t = makeTime(tm);
//use the time_t value to ensure correct weekday is set
if (rtc.set(t) == 0)
{ // Success
setTime(t);
}
else
{
#ifdef DEBUG
Serial.println("RTC set failed!");
#endif
}
}
clearDisplay();
refresh();
delay(100);
//pinMode(BLANK_PIN, OUTPUT);//Output Enable important to do this last, so LEDs do not flash on boot up
SPI.begin();//start up the SPI library
interrupts();//let the show begin, this lets the multiplexing start
delay(500);
clockMode = CLK;
}
//--------- TIMER1 interrupt routine to update the display ------------
ISR(TIMER1_COMPA_vect)
{
BLANK_PORT |= 1 << BLANK_BIT; //The first thing we do is turn all of the LEDs OFF, by writing a 1 to the blank pin
//Turn on all columns
for (int shift_out = 0; shift_out < BYTES_PER_ROW; shift_out++)
{
SPI.transfer(ledStates[activeRow][shift_out]);
}
//Enable row that we just outputed the column data for
digitalWrite(ANODE_A, (activeRow == 0) ? LOW : HIGH);
digitalWrite(ANODE_B, (activeRow == 1) ? LOW : HIGH);
digitalWrite(ANODE_C, (activeRow == 2) ? LOW : HIGH);
digitalWrite(ANODE_D, (activeRow == 3) ? LOW : HIGH);
LATCH_PORT |= 1 << LATCH_BIT;//Latch pin HIGH
LATCH_PORT &= ~(1 << LATCH_BIT);//Latch pin LOW
BLANK_PORT &= ~(1 << BLANK_BIT);//Blank pin LOW to turn on the LEDs with the new data
activeRow = (activeRow + 1) % ROWS; //increment the active row
pinMode(BLANK_PIN, OUTPUT);
}
//---------------------- Main program loop ----------------------------
void loop()
{
if (clockMode == CLK)
{
//DateTime now = rtc.now();
time_t t = now();
nowH = hour(t);
nowM = minute(t);
if (nowM != lastMinutes)
{
lastMinutes = nowM;
showTime(nowH, nowM);
#ifdef DEBUG
Serial.println("Current time: " + String(nowH) + ":" + String(nowM));
#endif
}
}
readBtns(); //Read buttons
}
//--------------------------------------------------
//Read buttons state
// Handles setting of time
void readBtns()
{
if (digitalRead(SW_SET) == LOW)
{
delay(10);
if (digitalRead(SW_SET) == LOW)
{
clockMode = (clockMode == CLK_M) ? CLK : (ClockEnum)((int)clockMode + 1);
switch (clockMode)
{
case CLK_H:
setH = nowH;
setM = nowM;
flashTimeout = millis() + FLASH_TIME;
flashOn = false;
break;
case CLK_M:
flashTimeout = millis() + FLASH_TIME;
flashOn = false;
break;
case CLK:
#ifdef DEBUG
Serial.println("Set time: " + String(setH) + ":" + String(setM));
#endif
//Set RTC
tmElements_t tm;
tm.Year = CalendarYrToTm(2020);
tm.Month = 1;
tm.Day = 1;
tm.Hour = setH;
tm.Minute = setM;
tm.Second = 0;
time_t t = makeTime(tm);
//use the time_t value to ensure correct weekday is set
if (rtc.set(t) == 0)
{ // Success
setTime(t);
}
else
{
#ifdef DEBUG
Serial.println("RTC set failed!");
#endif
}
//force update
lastMinutes = -1;
break;
}
//Wait until button is released
while (digitalRead(SW_SET) == LOW)
{
delay(10);
}
showTime(setH, setM);
}
}
if (clockMode != CLK)
{
if (millis() > flashTimeout)
{
flashTimeout = millis() + FLASH_TIME;
flashOn = !flashOn;
showTime(setH, setM, (clockMode == CLK_H && flashOn), (clockMode == CLK_M && flashOn));
}
if (millis() > stepTimeout)
{
if (digitalRead(SW_UP) == LOW)
{
switch (clockMode)
{
case CLK_H:
setH = (setH + 1) % 24;
break;
case CLK_M:
setM = (setM + 1) % 60;
}
showTime(setH, setM, (clockMode == CLK_H && flashOn), (clockMode == CLK_M && flashOn));
stepTimeout = millis() + STEP_TIME;
}
else if (digitalRead(SW_DOWN) == LOW)
{
switch (clockMode)
{
case CLK_H:
setH = (setH + 23) % 24;
break;
case CLK_M:
setM = (setM + 59) % 60;
}
showTime(setH, setM, (clockMode == CLK_H && flashOn), (clockMode == CLK_M && flashOn));
stepTimeout = millis() + STEP_TIME;
}
}
}
}
//--------------------------------------------------
//show the time
//h = hours (0..11)
//m = minutes (0..59)
void showTime(int h, int m)
{
showTime(h, m, true, true);
}
//show the time
//h = hours (0..23)
//m = minutes (0..59)
//he = hours enable (true/false)
//me = minutes enable (true/false)
void showTime(int h, int m, bool he, bool me)
{
#ifdef HOUR12
if (h >= 12)
{
h = h - 12;
}
if (h == 0)
{
h = 12;
}
#endif
displayRomanNumber((he) ? h : 0, true, false);
displayRomanNumber((me) ? m : 0, false, false);
refresh();
}
//--------------------------------------------------
//Display a number in roman numerals
// number - (0 to 59) - note 0 is blank
// top - true to display top number, false to display bottom number
// centered - true to center number on display
void displayRomanNumber(int number, bool top, bool centered)
{
uint8_t shift;
uint32_t mask;
//Get the bits for the tens portion of the number
uint16_t ft = pgm_read_word_near(&tensFont[min(number / 10, 9)]);
uint16_t fu = pgm_read_word_near(&unitsFont[number % 10]);
uint8_t ct = 0;
mask = 0b1111000000000000;
while (ct < 4 && (ft & mask) != 0)
{
ct++;
mask = mask >> 4;
}
//Count ones
uint8_t cu = 0;
mask = 0b1111000000000000;
while (cu < 4 && (fu & mask) != 0)
{
cu++;
mask = mask >> 4;
}
uint8_t c = ct + cu;
uint8_t r = (top) ? 0 : 7;
uint8_t o = (centered) ? (7 - c) >> 1 : 0; //Calculate offset for centering
for (int i = 0; i < 7; i++)
{
if (i < o)
{
setDigitInArray(0,r + i);
}
else if (i < (o + ct))
{
if (i == o)
{
shift = 12;
}
setDigitInArray((ft >> shift) & 0x0F, r + i);
shift = shift - 4;
}
else if (i < (o + ct + cu))
{
if (i == (o + ct))
{
shift = 12;
}
setDigitInArray((fu >> shift) & 0x0F, r + i);
shift = shift - 4;
}
else
{
setDigitInArray(0,r + i);
}
}
}
//--------------------------------------------------
//Sets the bit in the 6 byte array that corresponds to the physical column and row
// s = 4 bit segment (3 - Seg A, 2 - Seg B, 1, Seg C, 0 - Sed D)
// c = column (0 to 13 - representing 14 digits - top row 0 to 6, bottom row 7 to 13)
void setDigitInArray(uint8_t s, int c)
{
uint8_t cx = digitMap[c];
int by = cx >> 3;
uint8_t bi = cx - (by << 3);
uint8_t mask = 0b00001000;
for (int r = 0; r < ROWS; r++)
{
if (s & mask)
{
ledNext[r][1 - by] |= (1 << bi);
}
else
{
ledNext[r][1 - by] &= ~(1 << bi);
}
mask = mask >> 1;
}
}
//--------------------------------------------------
//Transfers the working buffer to the display buffer
void refresh()
{
memcpy(ledStates, ledNext, ROWS * BYTES_PER_ROW);
}
//--------------------------------------------------
//Clears the working buffer
void clearDisplay()
{
//Clear out ledStates array
for (int r = 0; r < ROWS; r++)
{
for (int c = 0; c < BYTES_PER_ROW; c++)
{
ledNext[r][c] = 0;
}
}
}
Comments