I have recently decided to make a digital table clock. The design I chose
consists of three important parts:
- ESP32,
- 7-segment four digits display with no special driver,
and
- DS3231 real time clock.
The code is
on
github.
Since ESP32 has WiFi module, I used it for two purposes: to obtain the correct
time once a day (and synchronize the DS3231 to that correct time), and to obtain the temperature information
from my weather server, so I could get the outside temperature with a single button press.
I purchased the most simple 7-segment display which has 12 pins: 8 pins for
the digit segment selection and 4 pins for the digit selection. Those four pins are common cathodes of the
LEDs making each digit - that is why it has four common cathodes - for those four digits.
DS3231 is a good RTC with temperature compensation and optional battery
(CR2032) to work without external power source.
Here is the schematics:
Driving the 7-segment four digit display is quite interesting. You cannot
illuminate more than one digit in time. This means that you need to illuminate the first digit, then the
second, then the third, and the fourth, and then to do it all again, fast. Thanks to our vision persistence,
we get the impression that all four digits are illuminated.
If you look at the schematics, you will notice that the digit definition is
done using GPIO pins 32, 33, 25, 26, 27, 14, 12, and 2, which turn on/off segments a, b, c, d, e, f, g, and
dot. That would only set the corresponding LEDs input to high or low voltage, but would not illuminate them
(the circuit would not be closed until the common cathode is connected to the ground). To choose which digit
will be illuminated (to have the common cathode connected to the ground), I used GPIO pins 16, 17, 18, and
19. Logical one (3.3V) on those GPIO pins would turn on the corresponding transistor and it would connect
the common cathode of the connected digit to the ground.
// GPIO ports for
LEDs// Dot A B C D E F Gint LEDS[] = {2, 32, 33, 25,
26, 27, 14, 12};
// GPIO ports for digitsint DIGITS[] = {
18, 19, 17, 16 };Next I defined the matrix of LEDs for digit representation:
int
DIGIT_MATRIX[][7] = {// A B C D E F G {HIGH,
HIGH, HIGH, HIGH, HIGH, HIGH, LOW}, // 0{LOW, HIGH, HIGH, LOW, LOW, LOW, LOW}, //
1{HIGH, HIGH, LOW, HIGH, HIGH, LOW, HIGH}, // 2{HIGH, HIGH, HIGH, HIGH,
LOW, LOW, HIGH}, // 3{LOW, HIGH, HIGH, LOW, LOW, HIGH, HIGH}, //
4{HIGH, LOW, HIGH, HIGH, LOW, HIGH, HIGH}, // 5{HIGH, LOW, HIGH, HIGH,
HIGH, HIGH, HIGH}, // 6{HIGH, HIGH, HIGH, LOW, LOW, LOW, LOW}, //
7{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH}, // 8{HIGH, HIGH, HIGH, HIGH,
LOW, HIGH, HIGH}, // 9{LOW, LOW, LOW, LOW, LOW, LOW, HIGH}, // '-' index is
10{LOW, LOW, LOW, LOW, LOW, LOW, LOW} // BLANK index is
11};
To display a digit, you need to call two following functions:
void displayDigit(int value)
{
int j, k;
for (j = 1; j < 8; j++)
{
digitalWrite(LEDS[j], (DIGIT_MATRIX[value][j-1]));
}
}
The function above will set the corresponding LEDs for the given digit, but
will not illuminate anything, since we haven't selected which one of the four digits should be activated. To
do so, we need the following function:
void activateDigit(int digit)
{
switch(digit)
{
case 0:
digitalWrite(DIGITS[0], HIGH);
digitalWrite(DIGITS[1], LOW);
digitalWrite(DIGITS[2], LOW);
digitalWrite(DIGITS[3], LOW);
digitalWrite(LEDS[0], LOW);
break;
case 1:
digitalWrite(DIGITS[0], LOW);
digitalWrite(DIGITS[1], HIGH);
digitalWrite(DIGITS[2], LOW);
digitalWrite(DIGITS[3], LOW);
digitalWrite(LEDS[0], LOW);
break;
case 2:
digitalWrite(DIGITS[0], LOW);
digitalWrite(DIGITS[1], LOW);
digitalWrite(DIGITS[2], HIGH);
digitalWrite(DIGITS[3], LOW);
digitalWrite(LEDS[0], LOW);
break;
case 3:
digitalWrite(DIGITS[0], LOW);
digitalWrite(DIGITS[1], LOW);
digitalWrite(DIGITS[2], LOW);
digitalWrite(DIGITS[3], HIGH);
digitalWrite(LEDS[0], LOW);
break;
default:
digitalWrite(DIGITS[0], LOW);
digitalWrite(DIGITS[1], LOW);
digitalWrite(DIGITS[2], LOW);
digitalWrite(DIGITS[3], LOW);
digitalWrite(LEDS[0], LOW);
}
}
So, displaying time from the RTC module looks like this. First we read the
time from the RTC (once in two seconds):
void read_time()
{
DateTime now = rtc.now();
int hour = now.hour();
int minute = now.minute();
hour_1 = hour / 10;
hour_2 = hour % 10;
minute_1 = minute / 10;
minute_2 = minute % 10;
}
We break up the time into four digits: hour_1, hour_2,
minute_1, and minute_2. Then, in the main loop, we display that time by showing each digit
quickly (each digit is illuminated one millisecond):
void loop()
{
switch(counter)
{
case 0:
displayDigit(hour_1);
break;
case 1:
displayDigit(hour_2);
break;
case 2:
displayDigit(minute_1);
break;
case 3:
displayDigit(minute_2);
break;
}
activateDigit(counter);
counter++;
if (counter == 4)
{
counter = 0;
}
// duty cycle adjustment for the LED brightness
delayMicroseconds(500);
activateDigit(-1);
delayMicroseconds(500);
}
As you can see, the time is fetched from the RTC outside of the main loop. I
have done that because obtaining the time from the RTC inside the loop might screw up the LED illumination,
since the main loop would be blocked while reading the time from the RTC. To avoid that, I have used the
ESP32 task to do the RTC reading. In the setup function, I have created a task for obtaining time:
// create a task that will be executed in the Task1code() function,
// with priority 1 and executed on core 0
// this task will get time from RTC every two seconds
xTaskCreatePinnedToCore(
Task1code, /* Task function. */
"Task1", /* name of task. */
10000, /* Stack size of task */
NULL, /* parameter of the task */
1, /* priority of the task */
&Task1, /* Task handle */
1); /* pin task to core 1 */
Task1code() is a task function which is executed in parallel with
the loop() function. Task1 is a task handler variable, which holds a handler for this task:
TaskHandle_t Task1;
void Task1code( void * pvParameters ){
for(;;){
read_time();
delay(2000);
}
}
This task is executed as an infinite loop. It obtains the time from the
RTC and then sleeps for two seconds. Then it does all of it again.
This is the time displayed by the clock:
Getting the outside temperature from my weather server was an interesting
task. When I press the button, the clock would have to connect to the weather server, send the HTTP request
for the temperature info, receive the HTTP response, parse it and display the outside temperature on the
clock, instead of current time.
const char root_ca[]= {
// here comes the certificate in bytes...
};
http.begin("https://myserver/status", root_ca);
int httpCode = http.GET();
// httpCode will be negative on error
if(httpCode > 0) {
// got the response from the server
if(httpCode == HTTP_CODE_OK)
{
String payload = http.getString();
String t = parse_response(payload);
int temp = t.toInt();
if (t.startsWith("-"))
{
hour_2 = 10; // "-" sign
temp = -temp;
} else
{
hour_2 = 11; // BLANK character
}
hour_1 = 11;
minute_1 = temp / 10;
minute_2 = temp % 10;
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n",
http.errorToString(httpCode).c_str());
}
http.end();
The clock would display the temperature outside:
When I press the button again, the clock will display the internal
temperature, obtained from the RTC. RTC has the temperature sensor built in so that it can compensate the
time drift caused by the temperature change. That sensor info is exposed to the programmer:
float int_temp = rtc.getTemperature();
int temp = (int)int_temp;
temp = temp -2;
hour_1 = 11;
hour_2 = 11;
minute_1 = temp / 10;
minute_2 = temp % 10;
Here is the temperature inside:
How is this clock initialized when powered up? How is this clock
synchronized to the precise time? I used the WiFi connection to connect the clock to the time server:
http.begin("https://myserver/time", root_ca);
int httpCode = http.GET();
// httpCode will be negative on error
if(httpCode > 0) {
// got the response from the server
if(httpCode == HTTP_CODE_OK)
{
String payload = http.getString();
parse_time(payload);
rtc.adjust(DateTime(year, month, day, hour, minute, second));
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n",
http.errorToString(httpCode).c_str());
}
http.end();
The code above does not synchronize the clock to the exactly precise time
(because of the delay), but it is good enough for me.
Conclusion
This table clock build was very interesting. The main challenge was to make
all the digits show properly on the 7-segment display. When I mastered that, the rest was a breeze: getting
the temperature from the weather server and showing the internal room temperature.