How should I best declare the array for DIGIT_FONT
?
Even though the elements are char
s, is it all right to input them as I would input integers, like you see here?
Am I using const char
correctly?
Is there something I could do to so that the array will not chew up RAM? I know it is only forty bytes, but still, I would like to try to conserve RAM.
#include "Wire.h"
#include <LiquidCrystal.h>
// Make sure your pin numbers match these!
// RS EN D4 D5 D6 D7
LiquidCrystal lcd( 7, 6, 5, 4, 3, 2);
const byte DST_SWITCH_PIN = 8;
const byte SPEAKER_PIN = 9;
const byte PLUS_BUTTON_PIN = 11;
const byte SET_BUTTON_PIN = 12;
// some useful constants (names of modes)
const byte SET_YEAR = 6;
const byte SET_MONTH = 5;
const byte SET_DATE = 4;
const byte SET_HOUR = 3;
const byte SET_MINUTE = 2;
const byte SET_SECOND = 1;
const byte KEEP_TIME = 0;
// another useful constant
const byte MINIMUM_YEAR = 22; // because I am writing this in 2022
// a font for digits
const char DIGIT_FONT[10][4] = {
{ 4, 2, 6, 2 },
{ 32, 2, 32, 2 },
{ 5, 2, 6, 1 },
{ 5, 2, 3, 2 },
{ 6, 2, 32, 2 },
{ 7, 1, 3, 2 },
{ 7, 1, 6, 2 },
{ 4, 2, 32, 2 },
{ 7, 2, 6, 2 },
{ 7, 2, 3, 2 }
};
// variables for the current date and time
byte yy=0, mo=1, dd=0, wd=6;
byte hh=0, mi=0, ss=0;
byte hhTwelve = 12;
byte wn=52;
// other helpful variables
bool gotTheTime = false;
bool timeIsGarbage = false;
byte old_ss = 99, halfSec = 198, old_halfSec = 198;
unsigned long microsNow = 0UL;
unsigned long microsAtLastSecond = 0UL;
bool dstOn = false;
bool plusPressed = false, old_plusPressed = false;
bool setPressed = false, old_setPressed = false;
byte clockMode = KEEP_TIME;
bool useBigDigits = false;
// a buffer for text to be displayed
char buf[20] = "";
void setup() {
pinMode(DST_SWITCH_PIN, INPUT_PULLUP);
pinMode(SPEAKER_PIN, OUTPUT);
pinMode(PLUS_BUTTON_PIN, INPUT_PULLUP);
pinMode(SET_BUTTON_PIN, INPUT_PULLUP);
Wire.begin();
lcd.begin(16, 2);
// BEGINNING of code for setting the date and time
// If you wish to set the date and time,
// uncomment the following:
/*
// code to precisely set the external real-time clock
Wire.beginTransmission(0x68); // address DS3231
Wire.write(0x00); // select register
// NOTE: before you run this code, you *must*
// change the following numbers to the correct time!
// (plus a few seconds to allow for compilation, etc.)
Wire.write(numberToBcd( 0)); // seconds
Wire.write(numberToBcd(21)); // minutes
Wire.write(numberToBcd( 1)); // hours (use 24-hour format)
Wire.write(numberToBcd( 6)); // day of week (I use Mon=1 .. Sun=7)
Wire.write(numberToBcd( 9)); // day of month
Wire.write(numberToBcd( 4)); // month
Wire.write(numberToBcd(22)); // year (use only two digits)
Wire.endTransmission();
*/
// END of code for setting the date and time
/*
// define special characters for single cell numerals 10 through 12
byte singleCellTen[] = { 18, 21, 21, 21, 21, 21, 18, 0 };
byte singleCellEleven[] = { 9, 27, 9, 9, 9, 9, 9, 0 };
byte singleCellTwelve[] = { 22, 21, 17, 18, 20, 20, 23, 0 };
lcd.createChar(10, singleCellTen);
lcd.createChar(11, singleCellEleven);
lcd.createChar(12, singleCellTwelve);
*/
// define special "box" characters for drawing large digits
byte boxDot[] = { 0, 0, 0, 0, 0, 0, 24, 24 };
byte boxLeftOnly[] = { 24, 24, 24, 24, 24, 24, 24, 24 };
byte boxBottomOnly[] = { 0, 0, 0, 0, 0, 0, 31, 31 };
byte boxTopAndLeft[] = { 31, 31, 24, 24, 24, 24, 24, 24 };
byte boxTopAndBottom[] = { 31, 31, 0, 0, 0, 0, 31, 31 };
byte boxLShape[] = { 24, 24, 24, 24, 24, 24, 31, 31 };
byte boxCShape[] = { 31, 31, 24, 24, 24, 24, 31, 31 };
lcd.createChar(1, boxDot);
lcd.createChar(2, boxLeftOnly);
lcd.createChar(3, boxBottomOnly);
lcd.createChar(4, boxTopAndLeft);
lcd.createChar(5, boxTopAndBottom);
lcd.createChar(6, boxLShape);
lcd.createChar(7, boxCShape);
// play a short tone (for testing the speaker)
tone(SPEAKER_PIN, 1000, 500);
// display a demo pattern (for testing the display)
lcd.setCursor(0, 0); // go to beginning of top line
lcd.print(F(" Display test "));
lcd.setCursor(0, 1); // go to beginning of bottom line
lcd.print(F("0123456789 (^_^)"));
for (int i = 5; i >= 1; i--) { // countdown from 5 to 1
lcd.setCursor(0, 0); // go to beginning of top line
lcd.print((char)('0' + i)); // print the digit
lcd.setCursor(15, 0); // go to end of top line
lcd.print((char)('0' + i)); // print the digit again
delay(998);
}
}
void loop() {
if (clockMode == KEEP_TIME) {
// normal timekeeping mode
// first, we (try to) read the time from the RTC
// send request to receive data starting at register 0
Wire.beginTransmission(0x68); // 0x68 is DS3231 device address
Wire.write((byte)0); // start at register 0
Wire.endTransmission();
Wire.requestFrom(0x68, 7); // request seven bytes
gotTheTime = false;
while(Wire.available())
{
ss = bcdToNumber(Wire.read()); // get seconds
mi = bcdToNumber(Wire.read()); // get minutes
hh = bcdToNumber(Wire.read()); // get hours
Wire.read(); // discard the day of the week (we will calculate it ourself)
dd = bcdToNumber(Wire.read()); // get day of month
mo = bcdToNumber(Wire.read()); // get month
yy = bcdToNumber(Wire.read()); // get year
gotTheTime = true;
}
microsNow = micros();
// detect garbage dates and times
if ((yy < MINIMUM_YEAR) || (yy > 99)) timeIsGarbage = true;
if ((mo < 1) || (mo > 12)) timeIsGarbage = true;
if ((dd < 1) || (dd > daysInMonth(yy,mo))) timeIsGarbage = true;
if (hh > 23) timeIsGarbage = true;
if (mi > 59) timeIsGarbage = true;
if (ss > 59) timeIsGarbage = true;
// read the Daylight Saving Time on/off switch
// NOTE: because we are using INPUT_PULLUP, LOW means on, and HIGH means off
dstOn = (digitalRead(DST_SWITCH_PIN) == LOW);
// adjust for Daylight Saving Time if applicable
if (dstOn) {
hh++;
if (hh >= 24) {
hh -= 24;
dd++;
if (dd > daysInMonth(yy, mo)) {
dd = 1;
mo++;
if (mo > 12) {
mo = 1;
yy++;
}
}
}
}
// try to figure out which half-second we are in
// (this is important to making the striking work properly)
if (ss != old_ss) microsAtLastSecond = microsNow;
halfSec = ss * 2;
if ((microsNow - microsAtLastSecond) >= 500000UL) halfSec++;
// calculate day of the week
wd = ymdToWeekday(yy, mo, dd);
// calculate week number
wn = ymdToWeekNumber(yy, mo, dd);
// convert hour to 12-hour format
hhTwelve = hh;
if (hhTwelve > 12) {
hhTwelve -= 12;
}
if (hhTwelve == 0) {
hhTwelve = 12;
}
if (gotTheTime && (!timeIsGarbage)) {
// only if we have successfully read the time
// (and it is not a garbage time)
// do we then attempt to indicate the time
if (halfSec != old_halfSec) { // do this only once every half-second
// see if it is time for the clock to strike
if (mi == 0) { // strike on the hour, i.e. when minutes are 0
if (halfSec < 26) {
// play the Westminster Chimes
switch (halfSec) {
case 0: tone(SPEAKER_PIN, 330, 420); break;
case 1: tone(SPEAKER_PIN, 415, 420); break;
case 2: tone(SPEAKER_PIN, 370, 420); break;
case 3: tone(SPEAKER_PIN, 247, 735); break;
case 6: tone(SPEAKER_PIN, 330, 420); break;
case 7: tone(SPEAKER_PIN, 370, 420); break;
case 8: tone(SPEAKER_PIN, 415, 420); break;
case 9: tone(SPEAKER_PIN, 330, 735); break;
case 12: tone(SPEAKER_PIN, 415, 420); break;
case 13: tone(SPEAKER_PIN, 330, 420); break;
case 14: tone(SPEAKER_PIN, 370, 420); break;
case 15: tone(SPEAKER_PIN, 247, 735); break;
case 18: tone(SPEAKER_PIN, 247, 420); break;
case 19: tone(SPEAKER_PIN, 370, 420); break;
case 20: tone(SPEAKER_PIN, 415, 420); break;
case 21: tone(SPEAKER_PIN, 330, 735); break;
default: break;
}
}
else if ((halfSec < (26 + 3 * hhTwelve)) && ((halfSec % 3) == 2)) {
// bong the hours
tone(SPEAKER_PIN, 415, 750);
}
}
}
if (useBigDigits) {
// update the display to show the current time with big digits
// build a string of characters for the top row of the display
buf[0] = ' ';
buf[1] = DIGIT_FONT[hh/10][0];
buf[2] = DIGIT_FONT[hh/10][1];
buf[3] = DIGIT_FONT[hh%10][0];
buf[4] = DIGIT_FONT[hh%10][1];
buf[5] = 1; // dot for colon
buf[6] = DIGIT_FONT[mi/10][0];
buf[7] = DIGIT_FONT[mi/10][1];
buf[8] = DIGIT_FONT[mi%10][0];
buf[9] = DIGIT_FONT[mi%10][1];
buf[10] = 1; // dot for colon
buf[11] = DIGIT_FONT[ss/10][0];
buf[12] = DIGIT_FONT[ss/10][1];
buf[13] = DIGIT_FONT[ss%10][0];
buf[14] = DIGIT_FONT[ss%10][1];
buf[15] = ' ';
lcd.setCursor(0, 0); // move to beginning of top row
lcd.print(buf); // print the characters to the display
// build a string of characters for the bottom row of the display
buf[0] = ' ';
buf[1] = DIGIT_FONT[hh/10][2];
buf[2] = DIGIT_FONT[hh/10][3];
buf[3] = DIGIT_FONT[hh%10][2];
buf[4] = DIGIT_FONT[hh%10][3];
buf[5] = 1; // dot for colon
buf[6] = DIGIT_FONT[mi/10][2];
buf[7] = DIGIT_FONT[mi/10][3];
buf[8] = DIGIT_FONT[mi%10][2];
buf[9] = DIGIT_FONT[mi%10][3];
buf[10] = 1; // dot for colon
buf[11] = DIGIT_FONT[ss/10][2];
buf[12] = DIGIT_FONT[ss/10][3];
buf[13] = DIGIT_FONT[ss%10][2];
buf[14] = DIGIT_FONT[ss%10][3];
buf[15] = ' ';
lcd.setCursor(0, 1); // move to beginning of bottom row
lcd.print(buf); // print the characters to the display
}
else {
// update the display to show the current date and time
// build a string of text containing the weekday and the full date
// (Hint: this code makes more sense if you read it vertically)
buf[0] = "BMTWTFSS"[wd];
buf[1] = "aouehrau"[wd];
buf[2] = "dneduitn"[wd];
buf[3] = ' ';
buf[4] = ' ';
buf[5] = ' ';
buf[6] = '0' + (mo/10);
buf[7] = '0' + (mo%10);
buf[8] = '/';
buf[9] = '0' + (dd/10);
buf[10] = '0' + (dd%10);
buf[11] = '/';
buf[12] = '2';
buf[13] = '0';
buf[14] = '0' + (yy/10);
buf[15] = '0' + (yy%10);
buf[16] = 0;
// suppress leading zero for month (character at position 6)
if (buf[6] == '0') buf[6] = ' ';
// display the weekday and full date on the top line
lcd.setCursor(0, 0); // move to beginning of top line
lcd.print(buf); // print the text to the display
// build a string of text containing the week number and the time
buf[0] = 'W';
buf[1] = 'k';
buf[2] = '0' + (wn/10);
buf[3] = '0' + (wn%10);
buf[4] = ' ';
buf[5] = ' ';
buf[6] = '0' + (hhTwelve/10);
buf[7] = '0' + (hhTwelve%10);
buf[8] = ':';
buf[9] = '0' + (mi/10);
buf[10] = '0' + (mi%10);
buf[11] = ':';
buf[12] = '0' + (ss/10);
buf[13] = '0' + (ss%10);
buf[14] = ((hh<12) ? 'a' : 'p');
buf[15] = 'm';
buf[16] = 0;
// suppress leading zero for hour (character at position 6)
if (buf[6] == '0') buf[6] = ' ';
// display the week number and the time on the bottom line
lcd.setCursor(0, 1); // move to beginning of bottom line
lcd.print(buf); // print the text to the display
}
}
else if (gotTheTime) {
// if we have read a garbage time from the RTC,
// then we will end up in here
// we request that the time be set
lcd.setCursor(0, 0); // go to beginning of top line
lcd.print(F(" Please set the "));
lcd.setCursor(0, 1); // go to beginning of bottom line
lcd.print(F(" date and time. "));
}
else {
// if we have *completely* failed to read *anything* from the RTC,
// then we will end up inside this "else"
// indicate failure to read the time
lcd.setCursor(0, 0); // go to beginning of top line
lcd.print(F("Error: "));
lcd.setCursor(0, 1); // go to beginning of bottom line
lcd.print(F("Can\'t read time "));
while(1) {
// do nothing, forever
}
}
while (micros() - microsNow < 10000UL) {
// do nothing for about 1/100 of a second
}
// remember these for the next time through loop()
old_ss = ss;
old_halfSec = halfSec;
}
else {
// time setting mode
microsNow = micros();
// show the screen for setting the time
// first, assemble the string to be displayed
switch (clockMode) {
case SET_YEAR:
// 01234567890123456
strcpy_P(buf, PSTR(" Set year: 20XX "));
buf[13] = '0' + (yy/10);
buf[14] = '0' + (yy%10);
break;
case SET_MONTH:
// 01234567890123456
strcpy_P(buf, PSTR(" Set month: XX "));
buf[13] = '0' + (mo/10);
buf[14] = '0' + (mo%10);
break;
case SET_DATE:
// 01234567890123456
strcpy_P(buf, PSTR(" Set date: XX "));
buf[13] = '0' + (dd/10);
buf[14] = '0' + (dd%10);
break;
case SET_HOUR:
// 01234567890123456
strcpy_P(buf, PSTR(" Set hour: XXXm "));
// Should I take care of this conversion here or elsewhere?
// I'll take care of it here, just to be safe.
hhTwelve = hh % 12;
if (hhTwelve == 0) hhTwelve = 12;
buf[11] = '0' + (hhTwelve/10);
buf[12] = '0' + (hhTwelve%10);
buf[13] = ((hh < 12) ? 'a' : 'p');
break;
case SET_MINUTE:
// 01234567890123456
strcpy_P(buf, PSTR(" Set minute: XX "));
buf[13] = '0' + (mi/10);
buf[14] = '0' + (mi%10);
break;
case SET_SECOND:
// 01234567890123456
strcpy_P(buf, PSTR(" Set second: XX "));
buf[13] = '0' + (ss/10);
buf[14] = '0' + (ss%10);
break;
default:
// 01234567890123456
strcpy_P(buf, PSTR(" Mode error! "));
}
lcd.setCursor(0, 0); // move to beginning of top line
lcd.print(buf); // print the text to the display
lcd.setCursor(0, 1); // go to beginning of bottom line
lcd.print(F(" ")); // print a full row of blanks
while (micros() - microsNow < 20000UL) {
// do nothing for about 1/50 of a second
}
}
// check the buttons
// NOTE: because we are using INPUT_PULLUP, LOW means the button is pressed
plusPressed = (digitalRead(PLUS_BUTTON_PIN) == LOW);
setPressed = (digitalRead(SET_BUTTON_PIN) == LOW);
if (plusPressed && !(old_plusPressed)) {
// the "plus" button was just pressed
switch (clockMode) {
case SET_YEAR:
yy = (yy + 1) % 100;
if (yy < MINIMUM_YEAR) yy = MINIMUM_YEAR;
break;
case SET_MONTH:
mo = (mo % 12) + 1;
break;
case SET_DATE:
dd = (dd % daysInMonth(yy, mo)) + 1;
break;
case SET_HOUR:
hh = (hh + 1) % 24;
break;
case SET_MINUTE:
mi = (mi + 1) % 60;
break;
case SET_SECOND:
ss = (ss + 5) % 60;
ss -= (ss % 5);
break;
default:
useBigDigits = !useBigDigits;
}
}
if (setPressed && !(old_setPressed)) {
// the "set" button was just pressed
// change to the new mode
if (clockMode == KEEP_TIME) {
clockMode = SET_YEAR;
}
else {
clockMode--;
}
// act according to the new mode
switch (clockMode) {
case SET_YEAR:
if (yy < MINIMUM_YEAR) yy = MINIMUM_YEAR;
if (yy > 99) yy = 99;
break;
case SET_MONTH:
if (mo < 1) mo = 1;
if (mo > 12) mo = 12;
break;
case SET_DATE:
if (dd < 1) dd = 1;
if (dd > daysInMonth(yy, mo)) dd = daysInMonth(yy, mo);
break;
case SET_HOUR:
if (hh > 23) hh = 23;
break;
case SET_MINUTE:
if (mi > 59) mi = 59;
break;
case SET_SECOND:
if (ss > 59) ss = 59;
break;
case KEEP_TIME:
// prepare to enter normal timekeeping mode
// read the Daylight Saving Time on/off switch
// NOTE: because we are using INPUT_PULLUP, LOW means on, and HIGH means off
dstOn = (digitalRead(DST_SWITCH_PIN) == LOW);
// because the RTC keeps "standard" (i.e. non-Daylight Saving) time,
// then, if we are in Daylight Saving Time,
// we will need to subtract 1 hour before we write to the RTC
if (dstOn) {
if (hh == 0) {
hh = 23;
dd--;
if (dd == 0) {
mo--;
if (mo == 0) {
mo = 12;
yy--;
}
dd = daysInMonth(yy, mo);
}
}
else {
hh--;
}
}
// calculate the day of the week (not that we really care, anyway)
wd = ymdToWeekday(yy, mo, dd);
// indicate that this is a valid time, not a garbage time
timeIsGarbage = false;
// write the date and time to the RTC
Wire.beginTransmission(0x68); // address DS3231
Wire.write(0x00); // select register
Wire.write(numberToBcd(ss)); // seconds
Wire.write(numberToBcd(mi)); // minutes
Wire.write(numberToBcd(hh)); // hours (use 24-hour format)
Wire.write(numberToBcd(wd)); // day of week (I use Mon=1 .. Sun=7)
Wire.write(numberToBcd(dd)); // day of month
Wire.write(numberToBcd(mo)); // month
Wire.write(numberToBcd(yy)); // year (use only two digits)
Wire.endTransmission();
// update these variables
microsNow = micros();
microsAtLastSecond = microsNow;
halfSec = ss * 2;
// change these variables to nonsense values
// this will force a display update next time through loop()
old_ss = 99;
old_halfSec = 198;
// maybe I don't need this delay, but I'm putting it in anyway
delay(50);
break;
default:
// should never happen
lcd.setCursor(0, 0); // move to beginning of top line
lcd.print(F("Error: bad mode!")); // display error message
while (1) {
// do nothing, forever
}
}
}
old_plusPressed = plusPressed;
old_setPressed = setPressed;
}
byte bcdToNumber(byte b) {
// convert BCD (binary-coded decimal) to an ordinary number
byte tens = (b >> 4) & 0xF;
byte ones = b & 0xF;
return (byte)((tens * 10) + ones);
}
byte numberToBcd(byte n) {
// convert a number to binary-coded decimal
byte tens = (n/10);
byte ones = (n%10);
return (byte)((tens << 4) + ones);
}
byte daysInMonth(byte y, byte m) {
// get the number of days in the given month
// y is for the year (0 to 99 for years 2000 through 2099)
// m is for the month (1 to 12)
// reject out-of-range input
if (y > 99) return 0;
if ((m < 1) || (m > 12)) return 0;
// Fourth, eleventh, ninth, and sixth,
// thirty days to each we fix.
if ((m==4)||(m==11)||(m==9)||(m==6)) return 30;
// Every other, thirty-one,
// except the second month alone,
if (m!=2) return 31;
// which hath twenty-eight, in fine,
// till leap-year give it twenty-nine.
if ((y%4)==0) return 29; // leap year
return 28; // not a leap year
}
byte ymdToWeekNumber (byte y, byte m, byte d) {
// get the week number for a given year, month, and day
// NOTE: This function uses two-digit years
// y is a number from 0 (for year 2000) to 99 (for year 2099)
// This function will not work for years outside of this range!
// reject out-of-range dates
if (y > 99) return 0;
if ((m < 1)||(m > 12)) return 0;
if ((d < 1)||(d > 31)) return 0;
// special case first two days of January 2000
if ((y == 0) && (m == 1) && (d <= 2)) return 52;
// (It is useful to know that Jan. 1, 2000 was a Saturday)
// compute adjustment for dates within the year
// If Jan. 1 falls on: Mo Tu We Th Fr Sa Su
// then the adjustment is: 6 7 8 9 3 4 5
byte adj = ((y + 1 + ((y+3)/4)) % 7) + 3;
// compute day of the year (in range 1-366)
int doy = d;
if (m > 1) doy += 31;
if (m > 2) {
if ((y%4)==0) doy += 29;
else doy += 28;
}
if (m > 3) doy += 31;
if (m > 4) doy += 30;
if (m > 5) doy += 31;
if (m > 6) doy += 30;
if (m > 7) doy += 31;
if (m > 8) doy += 31;
if (m > 9) doy += 30;
if (m > 10) doy += 31;
if (m > 11) doy += 30;
// compute week number
byte wknum = (adj + doy) / 7;
// check for boundary conditions
if (wknum < 1) {
// last week of the previous year
// go to previous year and re-compute adjustment
y--;
adj = ((y + 1 + ((y+3)/4)) % 7) + 3;
// check to see whether that year had 52 or 53 weeks
// all years beginning on Thursday have 53 weeks
if (adj==9) return 53;
// leap years beginning on Wednesday have 53 weeks
if ((adj==8) && ((y%4)==0)) return 53;
// other years have 52 weeks
return 52;
}
if (wknum > 52) {
// check to see whether week 53 exists in this year
// all years beginning on Thursday have 53 weeks
if (adj==9) return 53;
// leap years beginning on Wednesday have 53 weeks
if ((adj==8) && ((y%4)==0)) return 53;
// other years have 52 weeks
return 1;
}
return wknum;
}
byte ymdToWeekday(byte y, byte m, byte d) {
// get the day of the week for a given year, month, and day
// NOTE: This function uses two-digit years
// y is a number from 0 (for year 2000) to 99 (for year 2099)
// This function will not work for years outside of this range!
if (y > 99) return 0;
if (d < 1) return 0;
byte l = (((y%4)==0) ? 1 : 0);
byte n = y + (y/4);
switch (m) {
case 1: if (d > 31) return 0; n+=(1-l); break;
case 2: if (d>(28+l)) return 0; n+=(4-l); break;
case 3: if (d > 31) return 0; n+= 4; break;
case 4: if (d > 30) return 0; break;
case 5: if (d > 31) return 0; n+= 2; break;
case 6: if (d > 30) return 0; n+= 5; break;
case 7: if (d > 31) return 0; break;
case 8: if (d > 31) return 0; n+= 3; break;
case 9: if (d > 30) return 0; n+= 6; break;
case 10: if (d > 31) return 0; n+= 1; break;
case 11: if (d > 30) return 0; n+= 4; break;
case 12: if (d > 31) return 0; n+= 6; break;
default: return 0;
}
n += d;
n = ((n + 4) % 7) + 1;
return n; // 1 for Mon, 2 for Tue, ..., 7 for Sun
}