Introduction: Faux Seven Segment Clock Display
I built a functional OLED clock in the style of the old seven segment displays. It also has a single button of operation used to adjust the time displayed. The clock runs from the internal timer of the CPU instead of using a real time clock or other time sync and I'll discuss some of the issues around that.
The clock system is built using the Grove Beginner kit for Arduino available from here, the kit has a fairly extensive wiki full of examples and support. I really like the way all the components are supplied on a fixed PCB allowing you to get straight into development without worrying about balancing all the bits on your desk/breadboard. When you're ready, the kit also comes with wires and connectors allowing you to break off the parts you want and extend them into your own design. It's allowed for some fast iteration of this project.
I won't cover getting started with Arduino and this board, it's assumed you can follow the wiki links for that, but essentially this board is treated as an Uno and programmed in the same way as many arduinos.
Attachments
Supplies
Step 1: Emulating the Seven Segments
A seven-segment display is a form of electronic display device for displaying decimal numerals. Typically found as seven LED or LCD segments, the segments are illuminated to show the digits 0-9 which makes them ideal for a clock display. The segments are usually labelled A-G with an optional decimal point, having just seven segments makes it very easy to store the information about a digit within a single byte of data.
There's a bit of chicken/egg when deciding what a digit should look like on the display so I opened up Inkscape and drew a few designs until I found something I was happy with. A lot of displays appear to have a slight slant on them but to simplify this design I'm going to stick with a basic straight sided digit. The first thing to do is describe how to draw each digit and segment within the code.
I wanted to keep some flexibility in the size and the segment length determines the size and location of each segment. These values can be calculated in the code but they never change during the operation of the program so I made liberal use of constant data types in the header file. By precalculating these values in the header file I can keep the actual code easier to read and more importantly faster to process.
// The location of each segment with respect to the centre of the digit and
// an indicator to say if they are vertical or horizontal
struct segPos {
uint8_t x;
uint8_t y;
bool vertical;
};
// Precalculate the segment relative positions to save processing them every time
const segPos segments[] = {{0, (SEGL*2)+4, false}, //a
{-SEGL-1, SEGL+2, true}, //b
{-SEGL-1, -SEGL-2, true}, //c
{0, -(SEGL*2)-4, false}, //d
{SEGL+1, -SEGL-2, true}, //e
{SEGL+1, SEGL+2, true}, //f
{0, 0, false}}; //g
The code defines a structure to hold the location of each of the seven segments relative to the centre of the digit and also records if the segment is horizontal or vertical.
// A simple structure to define each of the lines that make up a segment
struct segLin {
uint8_t start;
uint8_t length;
};
// Precalculate the lines in the segment to save processing them every time
const segLin segLengths[] = {{SEGL-2, (SEGL*2)-3},
{SEGL-1, (SEGL*2)-1},
{SEGL, (SEGL*2)+1},
{SEGL-1, (SEGL*2)-1},
{SEGL-2, (SEGL*2)-3}};
Another structure holds the calculated information about each of the five lines within a segment. The whole size of a digit and it's segments can be changed by adjusting the const value 'SEGL' which in this case is defined to be 9 pixels long.
Using these structures has the added advantage of making the code much cleaner and readable when it comes to drawing each digit. Here you can see that the code draws a vertical line based upon the lines starting position '.start' and it's length '.length'
u8g2.drawVLine(posX+i-2, posY-segLengths[i].start, segLengths[i].length);
Step 2: Drawing a Digit
Each digit is defined as a number of segments whose positions are calculated relative to the centre of the digit, this makes it very easy to describe where to draw each segment. You simply add the offset X and Y coordinates of a segment to the supplied X and Y position of the digit. If you want the digit to be vertical then you draw lines relative to the Y position instead of the X position. I have made use of the drawHLine and drawVLine functions that are supplied by the graphics library. These functions will be optimised for speed when compared to drawing the whole line out as sngle pixels.
// Draws a segment on the display at the desired coordinates
void drawSegment(uint8_t posX, uint8_t posY, bool vert) {
// Draws 5 lines on the display, centred around the coordinates
// The middle line is the longest, getting shorter further away from the middle
for(int8_t i=0; i<5; i+=1){
if(vert)
u8g2.drawVLine(posX+i-2, posY-segLengths[i].start, segLengths[i].length);
else
u8g2.drawHLine(posX-segLengths[i].start, posY+i-2, segLengths[i].length);
}
}
The function simply reads all fine lines of segment data defined in the header and draws a segment as five adjacent lines based upon their start points and lengths.
The information about which segments to illuminate is supplied as a single byte of data with the lowest bit representing segment A and the seventh bit representing segment G. The eighth bit of data is sometimes used to represent a decimal point on the digit but that's not implemented here. Keeping all seven bits of data allows us to illuminate any combination of segments and even though this project only uses digits 0-9 (10 values), the data table defines 6 more characters often used to display Hexadecimal characters and you could also implement other light patterns that use single segments such as a rotating circle of segments.
// An array of values to define which segments are lit to make the various characters
// This array has been extended to include the hexadecimal characters
const uint8_t litSegments [] = {B00111111, //0
B00000110, //1
B01011011, //2
B01001111, //3
B01100110, //4
B01101101, //5
B01111101, //6
B00000111, //7
B01111111, //8
B01101111, //9
B01110111, //A
B01111100, //b
B00111001, //C
B01011110, //d
B01111001, //E
B01110001, //F
};
The 'Lit' segments for each character are defined in a look up table in the header, this again simplifies the code for speed.
// Convert a digit to the corresponding lit segments on the display
void drawDigit(point pos, uint8_t digit) {
// Perform a simple check to ensure that the value of the digit isn't too high for the array
while(digit>9)
digit -= 10;
draw7Seg(pos, litSegments[digit]);
}
// Works out which of the seven segments need to be drawn
void draw7Seg(point pos, uint8_t lit) {
for(uint8_t i=0; i<7; i++) {
if(lit>>i & 1) {
drawSegment(pos.x+segments[i].x, pos.y+segments[i].y, segments[i].vertical);
}
}
}
There are two functions for drawing a digit. The first function draw7Seg can draw any combination of segments, it loops through the array of segment data and draws each segment individually
The second function drawDigitrestricts the incoming value between the numbers 0-9 and then converts that to the number that represents the illuminated segments before it calls the draw7Seg function.
Step 3: Implementing a Basic Clock
The clock itself is made up of four single digits, the location of these digits is defined in the header file so it's as simple as working out which values to draw in the four different positions. Clock usually have an indicator in the middle to separate the hours and minutes and this tends to blink in time with the seconds as they tick by. For simplicity I have drawn an additional vertical segment, right in the very middle of the display to serve this purpose.
My code makes us of Paul Stoffregen's excellent time library which made it incredibly simple to add some basic time functionality. The library holds a time variable which automatically increases it's own values, rolls seconds over to hours and also keeps track of the date. I was very pleased to discover that after a minute of faffing and working out how to add my own milliseconds the time displayed on my clock incremented itself. The library has lots of advanced features for syncing with a real time clock or an online time server but those are beyond the scope of this project/device.
With easy access to an updating time variable, drawing a clock was straight forward.
// In the normal mode of operation, draw the time with a seconds dash blinking between the hours and the mins
// Draw the minutes with a leading zero
drawDigit(digitPos[0], minute());
drawDigit(digitPos[1], minute()/10);
// Draw the hours with a leading zero
drawDigit(digitPos[2], hour());
drawDigit(digitPos[3], hour()/10);
// Draw the middle vertical dash if required
// (should be defined as a constant)
if(blink) drawSegment(65, 35, true);
Here I made use of another fabulous library FastLED.h by Daniel Garcia and Mark Kriegsman. Even though this project doesn't use addressable RGB LED's they have a clever macro called EVERY_N_MILLISECONDS, you provide a milliseconds value to the macro and the code within the brackets gets executed that often.
// Handy function from the FastLED library that gets called every 50ms
EVERY_N_MILLISECONDS (50) {
// Monitor the input state from the single button
doButton();
// Draw the appropriate thing to the display
UpdateScreen();
}
For basic operation as a clock the screen would only need to be updated every 500ms as the central character blinks on/off but a clock is more useful if you can adjust the time without having to reprogram the device.
Step 4: A Single Button for Multiple Inputs
The Grove Beginner Kit has several different input options but only a single push button and it's an interesting exercise implementing a simple button to function for multiple behaviours. In this case I want to be able to register the difference between a 'single press' and a 'press and hold'. This will allow me to use a single press to change the value displayed on the clock and the hold function to switch between the different modes on the clock.
Push buttons are notoriously noisy for electrical input signals. As you push it down you may experience several changes of electrical state before the button is fully pushed. The normal way to deal with this is to 'debounce' the switch. You check the state of the switch, wait a small amount of time and check the state a second time. If the two match then you know the switch is actually pressed rather than just electrical noise. The EVERY_N_MILLISECONDS macro proves incredibly useful for this too. The main loop in my code is called every 50ms, this allows me to check the state of the button every time and confirm the state, if the button is the same two times in a row then you know it has been pressed correctly. 50ms is fast enough to allow the system to feel responsive to button pushes.
// Checks the current state of the input button to decide if the button has
// been pressed once or if it is being held down
// Also handles debounce of the switch because it is called every 50ms
void doButton() {
// Declare some static variables to remember the states between loops
static uint8_t oldState = 0;
static uint8_t holdCount = 0;
// Read the current state of the button
uint8_t newState = digitalRead(btnPin);
// Start by checking what happened last time
if(oldState == LOW) {
if(newState == HIGH)
// The button has just been pushed down
holdCount = 0;
}
else {
if(newState == LOW) {
// The button has just been released
// Confirm that the button hasn't been used for a 'held' event
if(holdCount < 10)
// Adjust the value on the display based on the current mode
AdjustValue();
}
else {
// The button is being held down
if(holdCount < 10) {
// Count how long the button has been held down for
holdCount += 1;
// If the button is held down for 10 loops (half a second) then trigger an event
if(holdCount == 10)
// Change the mode the clock is running in
ChangeMode();
}
}
}
// Store the state of the button ready for the next loop
oldState = newState;
}
The key variable here is the oldState value, declared as a static at top of the function. This allows the function to know what happened last time the function was executed. The very last line of the function takes the new reading from the button and stores it ready for the next loop. By comparing these two values we can work out if the button is pushed correctly but also what kind of button push the system is experiencing.
If the button wasn't pushed last time but it is currently pushed then it is the start of a button push event. There was 50ms between the two states so the switch has been debounced. Start a timer to see how long the button has been pushed for.
If the button is held down for ten loops (half a second) then you can safely say the system is experiencing a 'button held' event type of button push. If the button is released within that half a second you can say it's a 'button press' event and act accordingly.
Step 5: Switching Between Modes
Being able to detect the different button push events means that the system can switch between a number of different modes. For a simple clock like this I decided that it should be able to adjust the minutes and the hours separately based upon single button pushes. In the worst case a user will have to press the button 59 times to set the minute correctly but that can still be done relatively quickly.
The system defines three states of operation
// Define the possible modes of operation and a variable to keep track of them
enum clockModes {Normal, AdjMin, AdjHour};
clockModes clockMode = clockModes::Normal;
Pressing and holding the button simply switches between these three states but the display needs to update appropriately to let the user know which state the clock is currently in. This is done by flashing either the minute or the hour value to let the user see which is being adjusted.
// Updates the image on the screen depending on the current mode of the clock
void UpdateScreen() {
// Some handy variable for keeping track of the system state
static uint8_t blinkCount = 0;
static bool blink = true;
// Update the count to record how many times this loop has been called
blinkCount += 1;
if(blinkCount > 10) {
blinkCount = 0;
// Update the blink variable every half a second (10x50ms)
blink = !blink;
}
// Start by clearing the whole display ready for the new image
u8g2.clearBuffer();
// In AdjMin operation the display will blink the value for the mins to indicate that's the value being changed
// The value will blink for 100ms every half a second
// Don't draw the minute value in the first two loops
if(!((clockMode == clockModes::AdjMin) && (blinkCount < 2))) {
// Draw the minutes with a leading zero
drawDigit(digitPos[0], minute());
drawDigit(digitPos[1], minute()/10);
}
// In AdjMin operation the display will blink the value for the hours to indicate that's the value being changed
// The value will blink for 100ms every half a second
// Don't draw the hour value in the first two loops
if(!((clockMode == clockModes::AdjHour) && (blinkCount < 2))) {
// Draw the hours with a leading zero
drawDigit(digitPos[2], hour());
drawDigit(digitPos[3], hour()/10);
}
// Draw the middle dash still blinking
if(blink) drawSegment(65, 35, true);
// Send the new image to the display
u8g2.sendBuffer();
}
Because the function loops every 50ms it keeps track of how many times the function has been called and every 10 loops it toggles the state of the central divider. The time library is constantly keeping up to date even while in the adjustments modes so this divider is left flashing the whole time. The minute and the hour values only flash when in the corresponding mode and they only go off for 100ms every half a second because the user needs to be able to see what value it has been adjusted to.
Step 6: Final Thoughts
As a simple clock this system actually works really well. The single button interface is very responsive and allows me to adjust the time as required. My controller even does a really good job of keeping accurate time but there are a few limitations to a system like this.
Lack of a real time clock
This is the main problem with my simple clock. There is no clock module to help this system keep truly accurate time. The clock will drift over time which can be easily rectified by adding a minute here and there on a button push. I had even planned to implement another setting which just added or subtracted a minute from a button push so you could quickly adjust if the clock ran fast or slow
Lack of memory
The biggest issue about the lack of RTC is that the system doesn't remember the time. People of a certain age will remember having to constantly reset the VCR ever time there was a power cut and we're very used to not having to do that anymore. I think it could be possible to store the time in the system EEPROM, if the value was updated every 5 mins then the clock would have some resilience over short power cuts and adjustments would be less of a chore.