SOPHEE Clock 1.0

CSULB IEEE

Designed by Adam Torrence with the assistance of Kevin Duncan, and Leo Che

Table of Contents

Abstract

Make a affordable 7 segment display clock using an ATtiny microcontroller, RTC module, and shift register to teach a class on the basics of soldering, digital logic, registers, passive filters, crystal oscillators, transistors, SPI communication, I2C communication, and parallel communication, arduino coding and other electronic basics.

Schematic

Starting in the bottom right is the ATTiny84 microcontroller which controls all the other blocks, with the provided code. To power the clock (no data) we have a 5V micro USB port shown above the microcontroller block. The Real Time Clock (RTC) circuit is communicating with the microcontroller via pins PA4 and PA6 which are I2C communication lines both with a 10k pull up resistor shown in the microcontroller block as recommended by the datasheet. The RTC pins X1 and X2 are reading the 32.7680KHZ 6.9pf crystal oscillator and the 2 load capacitors are ensuring it resonates at the desired frequency. These capacitors and crystals are carefully picked from the data sheet from the RTC as well as application notes AN-1519. I used the ECS-.327-6-13X crystal with 2 10pf capacitors as recommended by AN-1519. In addition the RTC also has a circuit for a 3.3V battery to continue counting the time for weeks without being plugged in (it wont display the time until it is plugged back in, this battery only supplies the RTC). Next the time needs to be displayed, this is done using serial communication from the microcontroller to the shift register (SN74HC595), then parallel communication from the shift register to the 7 segment display. The shift register is necessary to cut down on wires coming from the microcontroller, to control the display (8 LEDs per digit) with only 3 pins. The 4 digit 7 Segment displays can only display one digit at a time, so the way to display all 4 is to switch between digits very fast, so the human eye cannot see the switching. The Digit switching transistors are in charge of this and are controlled via the gate current provided by the microcontroller. The 7 segment display has a common cathode for each digit that the transistors pull to ground to complete the circuit once the gate current is supplied by the microcontroller. We chose 2n2222 npn transistors because we need something to be able to feed the required current to the display, the microcontroller (ATTiny in this case) is not recommended to provide the required current to run all 8 leds off and on hundreds of times a second so we use the transistors as switches thus lowering the current from microcontroller. The second reason we use NPN 2n2222’s is because the frequency is lower than 50k-hz, mosfets can have issues dissipating the gate voltage when run at lower frequencies thus staying on and bleeding the previous number into the next digit with a harmonic of one mosfet staying on the longest. There are two buttons SW3 and SW4 to set the time and brightness with pulldown resistors to ensure accurate reading of when the button is pressed and depressed. The microcontroller has a decoupling capacitor on the power and ground ensuring a stable DC voltage removing small high frequency ripples that can effect the function of the microcontroller. The schematic was done using Altium

PCB

As this was my first PCB I did not take much into account regarding the trace placements, widths or ground pours. I am aware this is not perfect but worked for my application. If I were to improve the design I would recommend taking more precaution with the placement of the communication lines, as well as follow the RTC datasheet regarding the crystal oscillator ground pour to ensure low capacitance and interference. I made sure to place the crystal oscillator and capacitors as close to the RTC as possible as recommended by the data sheet, however there are other improvements you can make reading the data sheet on the components before making the PCB is highly recommended to see what the manufacturer recommends. The 7 segment display 3D model was not completed and the RTC 3D model goes through the board. Red traces are top layer Blue is bottom, note the ground pour on the bottom layer. The PCB was done using Altium. 

Code

#include <TinyWireM.h>   
#include <tinySPI.h>
#define RTCADDR B1101111//page11 datasheet
#define RTCSEC 0x00
#define RTCMIN 0x01
#define RTCHOUR 0x02
#define RTCWKDAY 0x03
#define RTCDATE 0x04
#define RTCMTH 0x05
#define RTCYEAR 0x06
#define CONTROL 0x07
#define OSCTRIM 0x08
 
const int dataPin  = 8;  // 74HC595 pin 8 DS
const int latchPin = 10;  // 74HC595 pin 9 STCP
const int clockPin = 9;   // 74HC595 pin 10 SHCP
const int digit0   = 3;   // 7-Segment pin D4
const int digit1   = 2;   // 7-Segment pin D3
const int digit2   = 1;   // 7-Segment pin D2
const int digit3   = 0;   // 7-Segment pin D1
//#define lightSensor A5
#define selectBT 5 //Select - Set button at pin A0
#define changeBT  7  //Change  button at pin A1

Here we have the start of the code including 2 libraries, TinyWire and TinySPI. The ATtiny dose not have built in communication protocols so you need to have these libraries to make sense of the communication data being sent and received from the ATtiny, to use I2C and SPI. Next we have the constant variables for the registers of the RTC, so if we want to change the time, set alarm, read the time, or control the clock we need to go to these registers. The way the I2C works is that the first we need to access the register we want to read or write to, so once communication starts the ATTiny sends the register value for example RTCMIN 0x01 which is hexadecimal for 0000001 bits this 8 bit message will be sent to the RTC and the RTCMIN register will now be open for either reading or writing, refer to the datasheet of the RTC for more information on setting or reading bits https://ww1.microchip.com/downloads/aemDocuments/documents/MPD/ProductDocuments/DataSheets/MCP7940N-Battery-Backed-I2C-RTCC-with-SRAM-20005010J.pdf.

Again with a library you do not need to do this type of firmware where you are manually doing the bits, which is an advantage of common RTC like the maxim ones as there are many readily available libraries. Next we are setting the pins for the microcontroller 8,10,9 for SPI, 3,2,1,0 for 4 digits of 7 segment, 5,7 for buttons. 

/* ***************************************************
 *                Global Variables                   *
 *************************************************** */
int brightness = 100;                  // valid range of 0-100, 100=brightest//variables used here
byte rtcSeconds,rtcMinutes, rtcMinutesTens, rtcMinutesOnes, rtcHours,rtcHoursTens, rtcHoursOnes;
byte rtcWeekDay;
boolean rtc12hrMode, rtcPM, rtcOscRunning, rtcPowerFail, rtcVbatEn;
boolean mfpPinTriggered = false;

// Hex values reference which LED segments are turned on
// and may vary from circuit to circuit.  Note the mapping above.
byte table[]= 
    {   B00111111,  // = 0
        B00000110,  // = 1
        B01011011,  // = 2
        B01001111,  // = 3
        B01100110,  // = 4
        B01101101,  // = 5
        B01111101,  // = 6
        B00000111,  // = 7
        B01111111,  // = 8
        B01101111,  // = 9
        0x00   // blank
    };  //Hex shown
byte controlDigits[] = { digit0, digit1, digit2, digit3 };  // pins to turn off & on digits
byte displayDigits[] = { 0,0,0,0,0 }; // ie: { 1, 0, 7, 13, 0} == d701 (all values from table array)
byte selectdigit = 4;

The int brightness controls the time delay between how long we keep each digit on so we have the digit on for more or less time which in turn would control the brightness.

Here are more global variables that would hold the current seconds minute ones minute tens… ect. These are byte variables since one digit will never exceed one bye 0000 for example 9 is 1001. We want to take as little memory as possible from the microcontroller.

We also need to set the clock to be on a 12 or 24 hour mode and also need control variables like enabling the crystal, and battery these are boolean variables since they only need to be a 0 or a 1 on or off. The mfp or multi function pin is set to false (0) since we are not using an alarm clock which is its main purpose. The next lines of code are for the 7 segment displays serial representation of each number for example to make a 1 you need B and C turned on and you can see the bits go from right to left right most bit being A and 2nd to leftmost being G (the furthest to the left is the dp or the dot on the bottom right not shown in picture below) . They array of digits is so that the microcontroller can choose and turn on digits with a for loop later on.

void setup() { 
 

 

    rtcInit();//go initialize RTC

    //pinMode(lightSensor, INPUT);
    
    pinMode(selectBT, INPUT);
    pinMode(changeBT, INPUT);
  
    pinMode(digit0, OUTPUT);
    pinMode(digit1, OUTPUT);
    pinMode(digit2, OUTPUT);
    pinMode(digit3, OUTPUT);


    pinMode(latchPin,OUTPUT);
    pinMode(clockPin,OUTPUT);
    pinMode(dataPin,OUTPUT);
    for (int x=0; x<4; x++){
        pinMode(controlDigits[x],OUTPUT);
        digitalWrite(controlDigits[x],LOW);  // Turns off the digit  
    }
}

In the void setup we first go to the RTC initialization function  then set the pins as either outputs or inputs, then lastly make sure all the digits are turned off before we start the loop. Below is the initialization function.

void rtcInit() {// RTC Initialize
  //sets up I2C at 100kHz
   TinyWireM.begin();
  
   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(CONTROL);
   TinyWireM.send(B00000000);//clear out the entire control register
   TinyWireM.endTransmission();

   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(RTCWKDAY);
   TinyWireM.endTransmission();
   TinyWireM.requestFrom(RTCADDR, 1);
  delay(1);
  byte rtcWeekdayRegister =  TinyWireM.receive();
  rtcWeekdayRegister |= 0x08;//enable Battery backup
   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(RTCWKDAY);
   TinyWireM.send(rtcWeekdayRegister);
   TinyWireM.endTransmission();

   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(RTCHOUR);
   TinyWireM.endTransmission();
   TinyWireM.requestFrom(RTCADDR, 1);
  delay(1);
  byte rtc12hourRegister =  TinyWireM.receive();//read out Hours register
  rtc12hourRegister |= 0x40;// flip the 12 hour mode bit to ON
   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(RTCHOUR);
   TinyWireM.send(rtc12hourRegister);//write it back in... now the RTC is set to 12 hour mode
   TinyWireM.endTransmission();
  
   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(RTCSEC);
   TinyWireM.endTransmission();
   TinyWireM.requestFrom(RTCADDR, 1);
  delay(1);
  byte rtcSecondRegister =  TinyWireM.receive();//read out seconds
  rtcSecondRegister |= 0x80;// flip the start bit to ON
   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(RTCSEC);
   TinyWireM.send(rtcSecondRegister);//write it back in... now the RTC is running
   TinyWireM.endTransmission();
}


First the library uses the TinyWire.Begin starting the I2C communication clock line at 100khz, then it clears out the entire control register (again go to the RTC datasheet to see what the control register holds). Everytime we need to beginTransmission and send the RTCADDR this holds the address for the RTC so the RTC knows we are communicating to it and not another device, then once the address is sent it can then send the next address for the register is trying to access by sending that address value IE: TinyWireM.send(RTCSEC); this will then access the seconds address and we can now read or write to those 8 bits. Now to fully understand what is happening here you need to read the datasheet understand what bits do what then see we are ORing the bits with hex values we want to flip, so we need to do some logic math to see the outcome of what bits will then be written into the registers. For example: if the register reads 1011 and we want to flip the 3rd bit without changing the rest we would OR it with 0100 now when we OR those two values together we get 1111, but in the code we are just using hex values since they are shorter. Here is an example again of a bitwise operator OR

Link to page: https://realpython.com/python-bitwise-operators/#bitwise-or 

We continue to use the Bitwise operator to adjust and edit the registers the way we need them to be. Imma say it for the 100th time if you want to understand what bits are necessary to flip to get the desired outcome read the datasheet, but if future designs use libraries, you dont need to do all this firmware theres easy functions made to do this in the background if you have a library. But not every library will work with ATtiny, especially if it uses I2C since TinyWire I2C uses different functions to communicate IE: Receive vs Read. or something like that.

/* ***************************************************
 *                   Void Loop                       *
 *************************************************** */
void loop() {

    DisplaySegments();                                      // Caution: Avoid extra delays
    
    
    /* *************************************
     *         Control Brightness          *
     * *********************************** */
    //Serial.println("here");
//
    //brightness = analogRead(lightSensor);
//        Serial.println(brightness);
    //brightness = map(brightness,0,1023,0,100);
 
    delayMicroseconds(16383*((100-brightness)/10));         // largest value 16383
    
    /* *************************************
     *        Selects Display numbers          *
     * *********************************** */
     rtcGetTime();
   displayDigits[0] = rtcMinutes %10;
   displayDigits[1] = (rtcMinutes / 10) %10;
   displayDigits[2] = rtcHours %10;
   displayDigits[3] = (rtcHours/10) %10;

   if (digitalRead(selectBT)  == HIGH) {
   // Serial.println("Here");
    if (digitalRead(changeBT) == HIGH){
     // Serial.println("Here2");
      changetime();
    }
   }
  }

This is our full loop, it’s best to try to keep your loop as small as possible with as little delays as possible since the longer it takes to run your code the longer it takes for the digits to turn off and on again affecting your brightness. First off we have the display segments function to do just that we will go into more detail about what the function does soon. There is a commented out section there, I was trying to add a light sensor that would adjust the brightness if it was in a dark or light room, but I ran out of pins and it really wasn’t necessary since the display was never too bright in a dark room. The delaymicroseconds is there to control the brightness. Then the rtcGetTime(); is another function that dose just that… gets the time. Then it saves the value of each time digit into the display digits. Lastly if both the buttons are pressed at the same time it goes into time setting mode or the changetime function.

/* ***************************************************
 *                   Functions                       *
 *************************************************** */    
void DisplaySegments(){
    /* Display will send out all four digits
     * one at a time.  Elegoo kit only has 1 74HC595, so
     * the Arduino will control the digits
     *   displayDigits[4] = the right nibble controls output type
     *                      1 = raw, 0 = table array
     *                  upper (left) nibble ignored
     *                  starting with 0, the least-significant (rightmost) bit
     */
    
    for (int x=0; x<4; x++){
        for (int j=0; j<4; j++){
            digitalWrite(controlDigits[j],LOW);    // turn off digits
        }
        digitalWrite(latchPin,LOW);
        // table array value is sent to the shift register
        if (selectdigit !=4){
          if(selectdigit == x){

            shiftOut(dataPin,clockPin,MSBFIRST,table[displayDigits[x]]+ 128);
          }
          else{
          shiftOut(dataPin,clockPin,MSBFIRST,table[displayDigits[x]]);}
        }
        else
        {shiftOut(dataPin,clockPin,MSBFIRST,table[displayDigits[x]]);}
        digitalWrite(latchPin,HIGH);
        digitalWrite(controlDigits[x],HIGH);   // turn on one digit
        delay(1);                              // 1 or 2 is ok
    }
    for (int j=0; j<4; j++){
        digitalWrite(controlDigits[j],LOW);    // turn off digits
    }
}

Here is where we control the shift register and 4 digit 7 segment. First we turn off all the digits, then we set the latch pin to low ensuring the data is not being sent out to 4 digit seven segment, then the if function checks if we are in the time setting mode or not, if it is then add 128 of the first bit in the 8 bit message its sending, remember to represent the number 1 the data sent to the shift register is 00000110 but the leftmost bit is the dp or dot in the bottom right corner if the clock is in time setting mode and on that current digit it will display the dot on the bottom right to indicate that is the number you are changing. After the data is shifted out via serial it then triggers the Latch Pin High to send out the parallel data to 7 segments then turn on the digit the number is associated with. Then turn off the digit and repeat for every digit. 

void rtcGetTime() {
   TinyWireM.beginTransmission(RTCADDR);
   TinyWireM.send(RTCSEC);
   TinyWireM.endTransmission();
   TinyWireM.requestFrom(RTCADDR, 7);//pull out all timekeeping registers
  delay(1);//little delay

  //now read each byte in and clear off bits we don't need, hence the AND operations
  rtcSeconds =  TinyWireM.receive() & 0x7F;
  rtcMinutes =  TinyWireM.receive() & 0x7F;
  rtcHours =  TinyWireM.receive() & 0x7F;
  rtcWeekDay =  TinyWireM.receive() & 0x3F;

  //now format the data, combine lower and upper parts of byte to give decimal number
  rtcSeconds = (rtcSeconds >> 4) * 10 + (rtcSeconds & 0x0F);
  rtcMinutes = (rtcMinutes >> 4) * 10 + (rtcMinutes & 0x0F);

  if ((rtcHours >> 6) == 1)//check for 12hr mode
    rtc12hrMode = true;
  else rtc12hrMode = false;

  // 12hr check and formatting of Hours
  if (rtc12hrMode) { //12 hr mode so get PM/AM
    if ((rtcHours >> 5) & 0x01 == 1)
      rtcPM = true;
    else rtcPM = false;
    rtcHours = ((rtcHours >> 4) & 0x01) * 10 + (rtcHours & 0x0F);//only up to 12
  }
  else { //24hr mode
    rtcPM = false;
    rtcHours = ((rtcHours >> 4) & 0x03) * 10 + (rtcHours & 0x0F);//uses both Tens digits, '23'
  }

  //weekday register has some other bits in it, that are pulled out here
  if ((rtcWeekDay >> 5) & 0x01 == 1)
    rtcOscRunning = true;// good thing to check to make sure the RTC is running
  else rtcOscRunning = false;
  if ((rtcWeekDay >> 4) & 0x01 == 1)
    rtcPowerFail = true;// if the power fail bit is set, we can then go pull the timestamp for when it happened
  else rtcPowerFail = false;
  if ((rtcWeekDay >> 3) & 0x01 == 1)//check to make sure the battery backup is enabled
    rtcVbatEn = true;
  else rtcVbatEn = false;

  rtcWeekDay = rtcWeekDay & 0x07;//only the bottom 3 bits for the actual weekday value

  //more formatting bytes into decimal numbers
 
  //print everything out
}

This is the function to read the time remember we are reading the time with the registers bits, however we do not need every bit from the register so we do a bitwise AND operation to rid of the bits we do not need leaving just the values we need, then we need to separate the bits so we can get the tens and ones separate from one another then convert to decimal values. Next we have checks to make sure it is in 12 vs 24 hour mode so we can check pm or am. Lastly we have some checks for oscillation, and power loss (most of the code is already commented).

void changetime() {
  delay(1000);
  //variables used just for setting the time and alarms
  byte rtcNewHour, rtcNewMinute;
  selectdigit = 0;
  while ( selectdigit<4) {
      //Serial.println("Here: ");
      delay(10);
      while (digitalRead(selectBT) == HIGH){
        delay(10);
            if (digitalRead(selectBT) == LOW){
            switch(selectdigit){
                case 0: 
                    displayDigits[0] = displayDigits[0] +1;
                    if (displayDigits[0] >9){
                      displayDigits[0] = 0;
                    }
                    break;
                case 1:
                    displayDigits[1] = displayDigits[1] +1;
                    if (displayDigits[1] >5){
                      displayDigits[1] = 0;
                    }
                    break;
                case 2:
                    displayDigits[2] = displayDigits[2] +1;
                    if (displayDigits[2] >9){
                      displayDigits[2] = 0;
                    }
                    break;
                case 3:
                    displayDigits[3] = displayDigits[3] +1;
                    if (displayDigits[3] >1){
                      displayDigits[3] = 0;
                    }   
                    break;            
                default:
                break;    }
          }}
          delay(10);
      while (digitalRead(changeBT) == HIGH){
        delay(10);
        if(digitalRead(changeBT) == LOW){
        selectdigit = selectdigit +1;}
      } 

   DisplaySegments(); 
  }
selectdigit = 4;
      //first pull down the string, convert from decimal to binary, and combine the bytes
      rtcNewHour = (((displayDigits[3] << 4 )+64)& 0xF0) + (displayDigits[2] & 0x0F);
      rtcNewMinute = ((displayDigits[1] << 4 ) & 0xF0) + (displayDigits[0] & 0x0F);

      //let's go change the time:
      //first stop the clock:
       TinyWireM.beginTransmission(RTCADDR);
       TinyWireM.send(RTCSEC);
       TinyWireM.send(0x00);
       TinyWireM.endTransmission();

      rtcGetTime();//go grab the time again just to make sure the osc stopped

      if (rtcOscRunning == false) { //oscillator stopped, we're good
        //Serial.print("AHHHHHHHHHHH");
         TinyWireM.beginTransmission(RTCADDR);//set the time
         TinyWireM.send(RTCMIN);
         TinyWireM.send(rtcNewMinute);
         TinyWireM.send(rtcNewHour);
         TinyWireM.endTransmission();

         TinyWireM.beginTransmission(RTCADDR);//start back up
         TinyWireM.send(RTCSEC);
         TinyWireM.endTransmission();
         TinyWireM.requestFrom(RTCADDR, 1);
       // delay(1);
        byte rtcSecondRegister =  TinyWireM.receive();
        rtcSecondRegister |= 0x80;//start!
         TinyWireM.beginTransmission(RTCADDR);
         TinyWireM.send(RTCSEC);
         TinyWireM.send(rtcSecondRegister);
         TinyWireM.endTransmission();
      }
      else return;
    }

This last function is easily the worst written part of the code as its the last thing I did and i just desperately needed it to work. But first I have a while loop where it reads the reads the button presses and adds one to the current digit it is on, with one button incrementing the value and the other switching the digit, next we do the opposite when we read the data we are turing the decimal value into binary to send to the registers. Then we stop the oscillator and write to every register the new values we are doing to set the time.

Future improvements and troubleshooting

One of the main issues I ran into was the random addition or subtraction of a couple seconds from the RTC or other inaccuracies of counting, however once I switched to the correct crystal (6.9pf not 12pf) and capacitors (10pf and not 6.9pf) it was right on time. AN-1519 https://www.analog.com/en/design-notes/crystal-considerations-with-maxim-realtime-clocks-rtcs.html. Other improvements that can be made on the PCB is thicker 5V rail traces, and careful placement of communication lines. Another improvement can be the use of one analog pin to read multiple buttons, saving pins on the ATtiny so we can have pins for example alarm clocks or temperature reading. Another addition would be a buzzer, if we saved one pin by using the analog read and voltage dividers for multiple buttons instead of one button per pin we could use the other pin for a buzzer to play anything you want when the clock hits that time. Adding a tinted film tape over the display to hide the non lit segments.The option for battery powered would be nice adding a LIPO battery and charging circuit to run off the battery if not plugged in. Lastly adding port manipulation to pins to speed up the pins output possibly allowing for a brighter display.