DS3231 calendar clock with hourly chime

I made a clock for my parents, and I decided to post the code here.

Required hardware:

  • DS3231 real-time clock
  • LCD 16x2 display (one that does not use I2C)
  • A speaker or buzzer for the chimes
  • EDIT: And of course an Arduino.
  • EDIT: You will also need a few resistors to hook everything up properly.

Optional hardware:

  • A toggle switch (to turn Daylight Saving Time on and off)

What it does:

  • Displays the date (month/day/year) and time (12 hour format)
  • Calculates and displays the day of the week and week number
  • Chimes every hour, on the hour (Westminster Chimes)
  • When the Daylight Saving Time switch is turned on, it adds 1 hour to the time to be displayed and chimed

What it doesn't do:

  • Allow you to set the time manually (you have to re-upload the sketch for that)

The code:

#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;

// 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;
byte old_ss = 99, halfSec = 198, old_halfSec = 198;
unsigned long microsNow = 0UL;
unsigned long microsAtLastSecond = 0UL;
bool dstOn = false, old_dstOn = false;

// a buffer for text to be displayed
char buf[20] = "";

void setup() {
  pinMode(DST_SWITCH_PIN, INPUT_PULLUP);
  pinMode(SPEAKER_PIN, OUTPUT);
  
  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);
  */
  
  // 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("  Display test  ");
  lcd.setCursor(0, 1); // go to beginning of bottom line
  lcd.print("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() {
  // 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();
  
  // 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) {
    // only if we have successfully read the 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 ((ss != old_ss) || (dstOn != old_dstOn)) { // only once every second
      // 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;
      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;
      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 we have failed to read the time,
    // 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("Error:          ");
    lcd.setCursor(0, 1); // go to beginning of bottom line
    lcd.print("Can\'t read time ");
  }
  
  while (micros() - microsNow < 10000) {
    // do nothing for about 1/100 of a second
  }
  
  // remember these for the next time through loop()
  old_ss = ss;
  old_halfSec = halfSec;
  old_dstOn = dstOn;
}

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
}

I hope that someone finds this useful.

1 Like

Thanks for sharing. Hope your parents like it.

No Arduino required?

Using an i²c backpack would be an interesting enhancement. Then the Arduino could be an attiny45/85 or a DigiSpark.

What country are your parents living in? I hear daylight savings time will become permanent in the US next year, so the switch would not be needed.

Oops, I forgot to mention that an Arduino (or the functional equivalent thereof) is needed. I guess I just figured that that went without saying, this being an Arduino forum.

Let's assume you are right and year-round DST becomes a thing. In that case, all I will need to do is turn the DST switch on (or ground the relevant pin) and forget about it. The switch is really just an insurance policy in case the legislators later change their minds and decide that year-round DST is a bad idea.

I also forgot to mention that you also need a few resistors in order to get the other hardware (display, real-time clock, and buzzer) working properly. Really, I should make a more detailed list explaining exactly how I have everything hooked up. I think I will do that, once I pick up my multimeter (for reading resistor values), which I don't have at home with me. (Resistor color bands can be hard to read.)

1 Like

That's one of the problems here, beginners always forget to mention what type of Arduino they are having a problem with!

You might want to mention what type you used, and whether that's the type you would recommend to others for the same purpose, or what alternatives might be equally good, perhaps even a smarter choice.

You should definitely post a schematic. If you want to post a Fritzing-style "cartoon" wiring diagram also, because you think they might be more beginner-friendly, then by all means do so, but do post a proper schematic too. If you use Fritzing, it can make both types of diagram, using it's "breadboard view" and "schematic view" modes. Fritzing will help you do that by carying over the components and connections from one type of diagram to the other, and will spot any continuity errors you make between the two.

What for? There would be too many lines going every which way. I figure it would be less confusing simply to list what is hooked up to what and how.
What exactly is the purpose of a schematic?

It's an Uno. Does it matter?
I'm just using the Uno, together with a solderless breadboard, for testing and debugging. I figure that once I'm satisfied, I'll move to a Nano Every and solder everything up.

Fine. Good luck with your next project.

Here is how I have my hardware hooked up:

The external RTC (e.g. DS3231) is connected to the Arduino thus:

  • RTC SDA goes to A4 of Arduino
  • RTC SDA goes through a 5100-ohm resistor to +5V
  • RTC SCL goes to A5 of Arduino
  • RTC SCL goes through a 5100-ohm resistor to +5V
  • RTC VCC goes to +5V
  • RTC GND goes to ground

The LCD display to Arduino is connected to the Arduino thus:

  • LCD VSS goes to ground
  • LCD VDD goes to +5V
  • LCD V0 goes through a 9000-ohm resistor to +5V
  • LCD V0 goes through a 1000-ohm resistor to ground
    • These numbers (9000 and 1000) were what worked for me. Feel free to try different numbers, but I read that they need to add up to 10000 ohms.
  • LCD RW goes to ground
  • LCD LED+ (or A) goes through a 220-ohm resistor to +5V
  • LCD LED- (or K) goes to ground
  • LCD RS goes to Arduino pin 7
  • LCD EN (or E) goes to Arduino pin 6
  • LCD D4 goes to Arduino pin 5
  • LCD D5 goes to Arduino pin 4
  • LCD D6 goes to Arduino pin 3
  • LCD D7 goes to Arduino pin 2
    • For LCD pins RS, EN, D4, D5, D6, and D7, you can (I believe) choose any six free Arduino pins. Just make sure to specify which six pins in the code.

The buzzer is connected thus:

  • Buzzer (-) to ground
  • Buzzer (+) to Arduino pin 9 through a 150-ohm resistor

The Daylight Saving Time switch, (once I get a hardware switch) will be connected thus:

  • one contact of DST switch to ground
  • the other contact of DST switch to Arduino pin 8

Note: If you don't have, for example, a 9000-ohm resistor, just use two (or more) resistors that add up to the resistance you need. Just be sure to connect them in series, so that the math works right. (If you connect them in parallel, then the math won't work, and you'll end up with much less resistance than you were aiming for.)

I made a new version, which uses two buttons to set the time. The time-setting procedure is as usual for a digital clock: one button changes which number (year, month, and so forth down to seconds) is selected, and the other button changes the value of the currently selected number. The buttons are connected as INPUT_PULLUPs, which means that one contact of each button goes to an Arduino pin, and the other contact goes to ground. For one of these buttons, I am using Arduino pin 11, and for the other, I am using Arduino pin 12.

#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

// 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;
byte old_ss = 99, halfSec = 198, old_halfSec = 198;
unsigned long microsNow = 0UL;
unsigned long microsAtLastSecond = 0UL;
bool dstOn = false, old_dstOn = false;
bool plusPressed = false, old_plusPressed = false;
bool setPressed = false, old_setPressed = false;
byte clockMode = KEEP_TIME;

// 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);
  */
  
  // 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();
    
    // 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) {
      // only if we have successfully read the 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 ((ss != old_ss) || (dstOn != old_dstOn)) { // only once every second
        // 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;
        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;
        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 we have failed to read the time,
      // 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;
    old_dstOn = dstOn;
  }
  
  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(buf, " Set year: 20XX ");
        buf[13] = '0' + (yy/10);
        buf[14] = '0' + (yy%10);
        break;
      case SET_MONTH:
        //           01234567890123456
        strcpy(buf, " Set month:  XX ");
        buf[13] = '0' + (mo/10);
        buf[14] = '0' + (mo%10);
        break;
      case SET_DATE:
        //           01234567890123456
        strcpy(buf, " Set date:   XX ");
        buf[13] = '0' + (dd/10);
        buf[14] = '0' + (dd%10);
        break;
      case SET_HOUR:
        //           01234567890123456
        strcpy(buf, " 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(buf, " Set minute: XX ");
        buf[13] = '0' + (mi/10);
        buf[14] = '0' + (mi%10);
        break;
      case SET_SECOND:
        //           01234567890123456
        strcpy(buf, " Set second: XX ");
        buf[13] = '0' + (ss/10);
        buf[14] = '0' + (ss%10);
        break;
      default:
        //           01234567890123456
        strcpy(buf, " 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:
        // do nothing
        ;
    }
  }
  
  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);
      
        // 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
}

I have updated my code to insert some sanity checks on the date and time. If the date and time read from the RTC fail these sanity checks, then the clock will ask you to set it.

#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

// 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, old_dstOn = false;
bool plusPressed = false, old_plusPressed = false;
bool setPressed = false, old_setPressed = false;
byte clockMode = KEEP_TIME;

// 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);
  */
  
  // 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 ((ss != old_ss) || (dstOn != old_dstOn)) { // only once every second
        // 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;
    old_dstOn = dstOn;
  }
  
  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:
        // do nothing
        ;
    }
  }
  
  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
}

I have updated my code yet again to include another display mode. This other display mode shows the date and time with big digits, but without the year, week number, or seconds. When in normal timekeeping mode, you can press the "plus" button to toggle between display modes. The boolean useBigDigits keeps track of which display mode you are in.

#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 and date with big digits
        
        // build a string of characters for the top row of the display
        buf[0]  = ((hhTwelve < 10) ? 32 : DIGIT_FONT[hhTwelve/10][1]);
        buf[1]  = DIGIT_FONT[hhTwelve%10][0];
        buf[2]  = DIGIT_FONT[hhTwelve%10][1];
        buf[3]  = (((halfSec % 2) == 0) ? 1 : 32);
        buf[4]  = DIGIT_FONT[mi/10][0];
        buf[5]  = DIGIT_FONT[mi/10][1];
        buf[6]  = DIGIT_FONT[mi%10][0];
        buf[7]  = DIGIT_FONT[mi%10][1];
        buf[8]  = 32;
        buf[9]  = "BMTWTFSS"[wd];
        buf[10] = "aouehrau"[wd];
        buf[11] = "dneduitn"[wd];
        buf[12] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][0]);
        buf[13] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][1]);
        buf[14] = DIGIT_FONT[dd%10][0];
        buf[15] = DIGIT_FONT[dd%10][1];
        buf[16] = 0;
        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]  = ((hhTwelve < 10) ? 32 : DIGIT_FONT[hhTwelve/10][3]);
        buf[1]  = DIGIT_FONT[hhTwelve%10][2];
        buf[2]  = DIGIT_FONT[hhTwelve%10][3];
        buf[3]  = (((halfSec % 2) == 0) ? 1 : 32);
        buf[4]  = DIGIT_FONT[mi/10][2];
        buf[5]  = DIGIT_FONT[mi/10][3];
        buf[6]  = DIGIT_FONT[mi%10][2];
        buf[7]  = DIGIT_FONT[mi%10][3];
        buf[8]  = 32;
        buf[9]  = "BJFMAMJJASOND"[mo];
        buf[10] = "aaeapauuuecoe"[mo];
        buf[11] = "dnbrrynlgptvc"[mo];
        buf[12] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][2]);
        buf[13] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][3]);
        buf[14] = DIGIT_FONT[dd%10][2];
        buf[15] = DIGIT_FONT[dd%10][3];
        buf[16] = 0;
        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;
        
        // turn big digits off
        useBigDigits = false;
        
        // 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
}

Now it has support for 24-hour mode. The switch to select 24-hour mode needs to be a toggle switch, and I have it connected to pin A0.

#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;
const byte USE_24_HOUR_SWITCH_PIN = A0;

// 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 hhShow = 0;
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;
bool use24Hour = 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);
  pinMode(USE_24_HOUR_SWITCH_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
    // NOTE: Striking is always 12-hour format, even if 24-hour switch is on
    hhTwelve = hh;
    if (hhTwelve > 12) {
      hhTwelve -= 12;
    }
    if (hhTwelve == 0) {
      hhTwelve = 12;
    }
    
    // read the 24-hour format on/off switch
    // NOTE: because we are using INPUT_PULLUP, LOW means on, and HIGH means off
    use24Hour = (digitalRead(USE_24_HOUR_SWITCH_PIN) == LOW);
    
    // get the hour number to show on the display (either 12- or 24-hour format)
    if (use24Hour) {
      hhShow = hh;
    }
    else {
      hhShow = hhTwelve;
    }
    
    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 ((hh >= 9) && (hh <= 22)) {  // strike only from 9 a.m. to 10 p.m.
            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 and date with big digits
        
        // build a string of characters for the top row of the display
        if (hhShow < 20) {
          buf[0]  = ((hhShow < 10) ? 32 : 2);
          buf[1]  = DIGIT_FONT[hhShow%10][0];
          buf[2]  = DIGIT_FONT[hhShow%10][1];
          buf[3]  = (((halfSec % 2) == 0) ? 1 : 32);
          buf[4]  = DIGIT_FONT[mi/10][0];
          buf[5]  = DIGIT_FONT[mi/10][1];
          buf[6]  = DIGIT_FONT[mi%10][0];
          buf[7]  = DIGIT_FONT[mi%10][1];
          buf[8]  = 32;
        }
        else {
          buf[0]  = DIGIT_FONT[hhShow/10][0];
          buf[1]  = DIGIT_FONT[hhShow/10][1];
          buf[2]  = DIGIT_FONT[hhShow%10][0];
          buf[3]  = DIGIT_FONT[hhShow%10][1];
          buf[4]  = (((halfSec % 2) == 0) ? 1 : 32);
          buf[5]  = DIGIT_FONT[mi/10][0];
          buf[6]  = DIGIT_FONT[mi/10][1];
          buf[7]  = DIGIT_FONT[mi%10][0];
          buf[8]  = DIGIT_FONT[mi%10][1];
        }
        buf[9]  = "BMTWTFSS"[wd];
        buf[10] = "aouehrau"[wd];
        buf[11] = "dneduitn"[wd];
        buf[12] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][0]);
        buf[13] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][1]);
        buf[14] = DIGIT_FONT[dd%10][0];
        buf[15] = DIGIT_FONT[dd%10][1];
        buf[16] = 0;
        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
        if (hhShow < 20) {
          buf[0]  = ((hhShow < 10) ? 32 : 2);
          buf[1]  = DIGIT_FONT[hhShow%10][2];
          buf[2]  = DIGIT_FONT[hhShow%10][3];
          buf[3]  = (((halfSec % 2) == 0) ? 1 : 32);
          buf[4]  = DIGIT_FONT[mi/10][2];
          buf[5]  = DIGIT_FONT[mi/10][3];
          buf[6]  = DIGIT_FONT[mi%10][2];
          buf[7]  = DIGIT_FONT[mi%10][3];
          buf[8]  = 32;
        }
        else {
          buf[0]  = DIGIT_FONT[hhShow/10][2];
          buf[1]  = DIGIT_FONT[hhShow/10][3];
          buf[2]  = DIGIT_FONT[hhShow%10][2];
          buf[3]  = DIGIT_FONT[hhShow%10][3];
          buf[4]  = (((halfSec % 2) == 0) ? 1 : 32);
          buf[5]  = DIGIT_FONT[mi/10][2];
          buf[6]  = DIGIT_FONT[mi/10][3];
          buf[7]  = DIGIT_FONT[mi%10][2];
          buf[8]  = DIGIT_FONT[mi%10][3];
        }
        buf[9]  = "BJFMAMJJASOND"[mo];
        buf[10] = "aaeapauuuecoe"[mo];
        buf[11] = "dnbrrynlgptvc"[mo];
        buf[12] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][2]);
        buf[13] = ((dd<10) ? 32 : DIGIT_FONT[dd/10][3]);
        buf[14] = DIGIT_FONT[dd%10][2];
        buf[15] = DIGIT_FONT[dd%10][3];
        buf[16] = 0;
        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' + (hhShow/10);
        buf[7]  = '0' + (hhShow%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);
        if (use24Hour) {
          buf[14] = ' ';
          buf[15] = ' ';
        }
        else {
          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:
        // find out whether or not to use 24-hour format
        use24Hour = (digitalRead(USE_24_HOUR_SWITCH_PIN) == LOW);
        // assemble the string using the appropriate format
        //                  01234567890123456
        strcpy_P(buf, PSTR(" Set hour: XXXX "));
        if (use24Hour) {
          buf[11] = ' ';
          buf[12] = ' ';
          buf[13] = '0' + (hh/10);
          buf[14] = '0' + (hh%10);
        }
        else {
          // 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');
          buf[14] = 'm';
        }
        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;
        
        // turn big digits off
        useBigDigits = false;
        
        // 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
}