Clock Synchronization Via I2C (TWI)

When I was doing the clock/stop watch project last month, I mentioned that I intended to add I2C (TWI) communication functionality so that I could get time from this clock and use it as timer signal to control other electronics.

The reason I mentioned back then for going this route was that I did not have enough I/O pins since bq3287 used most of the pins on ATmega328p. Of couse, I could’ve just used a serial real-time clock like DS1307 and thus free up the pins, but I thought it would be a cool idea to experiment with bidirectional I2C communications. A key benefit of using I2C is that only three wires are required between the two MCUs, which makes it possible to use standard two-conductor shielded wire commonly used in stereo earphones to serve as the communication link.

For those who are impatient, the full source code can be downloaded toward the end of this article.

My main design goal was to achieve bi-directional communication between two ATmega328’s using I2C (TWI). Namely I wanted to be able to:

  • transmit date/time information from the 7seg LED clock to the other MCU where time can be displayed on an LCD.
  • set date/time from the remote MCU and sync the date/time settings back to the main 7seg LED clock.

At a high level, bidirectional communication using the Arduino Wire library can be achieved by assigning unique addresses to the devices that participate in the communication:

//device 1
Wire.begin(ADDRESS_1);
Wire.beginTransmission(ADDRESS_2);
...
Wire.endTransmission();

//device 2
Wire.begin(ADDRESS_2);
Wire.beginTransmission(ADDRESS_1);
...
Wire.endTransmission();

In order to be able to send the time and date information between the two ATmega328p’s, I defined the structure RTCMessage as follows:

struct RTCMessage {
    byte sender;
    byte msgType;
    byte year;
    byte month;
    byte day;
    byte hour;
    byte minute;
    byte secondInd;
    byte dayOfWeek;
};

On the 7seg clock side (where the RTC chip bq3287 is located), the Wire library is initialized to be a host on address 5 and listens on address 4. The event handler function receiveEvent processes the incoming date/time data and adjust the RTC settings accordingly. In the main loop, the current date/time info is sent to the other ATmega328p (address 4, ADDR_TWI_REMOTE) constantly so that the two clocks can keep in sync.

const int ADDR_TWI_LOCAL = 5;
const int ADDR_TWI_REMOTE = 4;

 //... ... code omitted here ... ...

void setup() {

    //... ... code omitted here ... ...

    Wire.begin(ADDR_TWI_LOCAL);
    Wire.onReceive(receiveEvent);
}

void receiveEvent(int howMany) {
    RTCMessage msg;

    msg.sender = Wire.receive();
    msg.msgType = Wire.receive();
    msg.year = Wire.receive();
    msg.month = Wire.receive();
    msg.day = Wire.receive();
    msg.hour = Wire.receive();
    msg.minute = Wire.receive();
    msg.secondInd = Wire.receive();
    msg.dayOfWeek = Wire.receive();

    adjust(ADDR_CUR_YEAR, msg.year);
    adjust(ADDR_CUR_MON, msg.month);
    adjust(ADDR_CUR_DAY, msg.day);
    adjust(ADDR_CUR_HOUR, msg.hour);
    adjust(ADDR_CUR_MIN, msg.minute);
    adjust(ADDR_CUR_DOW, msg.dayOfWeek);
}

void loop() {

    //... ... code omitted here ... ...

    RTCMessage message;

    message.day = curDay;
    message.dayOfWeek = curDOW;
    message.hour = curHour;
    message.minute = curMin;
    message.month = curMonth;
    message.year = curYear;
    message.msgType = MsgGetTime;
    message.sender = ADDR_TWI_LOCAL;
    message.secondInd = secondIndLEDOn ? 1 : 0;

    Wire.beginTransmission(ADDR_TWI_REMOTE);

    Wire.send(message.sender);
    Wire.send(message.msgType);
    Wire.send(message.year);
    Wire.send(message.month);
    Wire.send(message.day);
    Wire.send(message.hour);
    Wire.send(message.minute);
    Wire.send(message.secondInd);
    Wire.send(message.dayOfWeek);

    Wire.endTransmission();
}

The code on the other ATmega328p is slightly more complex but the idea is essentially the same. We first set up the master and slave address for TWI communication. Note that the master/slave addresses we set up here are the opposite compared to those we did on the RTC clock side.

Here is the code snippet relevant to the TWI communication on the LCD display side:

const int ADDR_TWI_LOCAL = 4;
const int ADDR_TWI_REMOTE = 5;

 //... ... code omitted here ... ...

void setup() {

    //... ... code omitted here ... ...

    Wire.begin(ADDR_TWI_LOCAL);
    Wire.onReceive(receiveEvent);

}

void receiveEvent(int howMany) {
    intProc = true;

    message.sender = Wire.receive();
    message.msgType = Wire.receive();
    message.year = Wire.receive();
    message.month = Wire.receive();
    message.day = Wire.receive();
    message.hour = Wire.receive();
    message.minute = Wire.receive();
    message.secondInd = Wire.receive();
    message.dayOfWeek = Wire.receive();

    if (currentMode == MODE_TIME_SETUP || currentMode == MODE_NORMAL_CANCEL) {
        displayTime();
    } else if (currentMode >= MODE_ALARM_SETUP) {
        displayAlarm(currentMode - MODE_ALARM_SETUP);
    }

    intProc = false;
}

void saveTimeSettings() {
    Wire.beginTransmission(ADDR_TWI_REMOTE);
    Wire.send(messageCopy.sender);
    Wire.send(messageCopy.msgType);
    Wire.send(messageCopy.year);
    Wire.send(messageCopy.month);
    Wire.send(messageCopy.day);
    Wire.send(messageCopy.hour);
    Wire.send(messageCopy.minute);
    Wire.send(messageCopy.secondInd);
    Wire.send(messageCopy.dayOfWeek);
    Wire.endTransmission();

    message = messageCopy;
}

void loop() {
    //process button pressed events
    for (int i = 0; i < NUM_OF_BUTTONS; i++) {
        if (buttonPressed(i)) {
            if (intProc) return;
            cli();

            lcd.setCursor(15, 1);
            lcd.print(" ");

            if (i == BTN_MODE_SELECT) {
                currentMode++;
                renderCurrentMode();
            } else if (i == BTN_CURSOR_CTRL) {
                if (currentMode == MODE_TIME_SETUP) {
                    renderTimeSetup();
                } else if (currentMode >= MODE_ALARM_SETUP) {
                    renderAlarmSetup();
                }
            } else if (i == BTN_UP || i == BTN_DOWN) {
                if (currentMode == MODE_TIME_SETUP) {
                    adjustTimeSetup(i);
                } else if (currentMode >= MODE_ALARM_SETUP) {
                    adjustAlarmSetup(i);
                }
            } else if (i == BTN_ENTER) {
                if (currentMode == MODE_TIME_SETUP) {
                    sei();
                    saveTimeSettings();
                    cli();
                    lcd.setCursor(15, 1);
                    lcd.write(SAVED_SYM);
                } else if (currentMode >= MODE_ALARM_SETUP) {
                    saveAlarmSettings(currentMode - MODE_ALARM_SETUP);
                    lcd.setCursor(15, 1);
                    lcd.write(SAVED_SYM);
                } else if (currentMode == MODE_NORMAL_CANCEL) {
                    backLight = !backLight;

                    if (backLight) digitalWrite(PIN_BACKLIGHT, HIGH);
                    else digitalWrite(PIN_BACKLIGHT, LOW);
                }
            }
            sei();
        }


        if (buttonRleased(i)) {
            //the button is released, no action is necessary.
        }
    }

    checkAlarms();
}

The LCD side has five buttons, which are used to set/navigate time and alarm settings (you can download the full source code below). The information displayed on the LCD changes depending on the modes these buttons are in. This aside, the general idea is that whenever the date/time information is transmitted from the remote 7seg RTC clock, it is displayed on the LCD via displayTime if the current mode is set to display time.

When the date/time settings are changed on the LCD side, it is saved back to the RTC clock via saveTimeSettings in the main loop. Note that the interrupts are disabled for most of the loop via cli except for syncing the date/time settings. This is necessary to ensure that the button selections are fully processed before any interrupt handling occurs. If this is not done, the output on the LCD would be garbled as the TWI interrupt might happen during a screen update.

The following picture shows how the LCD (in the front) and the 7seg clock are in sync with the I2C primitives shown above.

Clock Synchronization Via I2C

Clock Synchronization Via I2C

The alarm settings are stored in the EEPROM of the ATmega328p on the LCD side which are compared against the current time stamp in checkAlarms to determine whether the alarm conditions are met.

The full source code for this project can be downloaded here (note, I used NetBeans IDE to create these projects. Please refer to this post to see how it is done):

The 7seg Digital Clock: DigitalClock.tar
The LCD Controller: LCD.tar

Be Sociable, Share!

One Comment

  1. Eduardo castellani says:

    Hello, thanks for the tutorial.

    Dimmer want a shower, LCD that shows the current temperature of the water, and the consumption of the electric network in dollars, because my son will know how much this spending his bath, and I can put a code that after by example, 10 minute shower, the dimmer switch off the hot water. kkk

    Could you help me with the code in C or C ++

    Thank U

    Eduardo castellani – From Brazil

Leave a Reply