Thursday, 25 July 2013

I2C Communication - What, Where & How? - Interfacing with the Simple Labs RTC Module

I2C Communication


I2C is short form for 'Inter Integrated Circuit' I2C Communication is Communication Bus standard developed by Phillips for standardising Communication between Integrated Circuits. For Eg. In a circuit, there could be a number of ICs each offering specific functionality[RTC, Temperature Sensor, EEPROM, etc] and they can all communicate on a single I2C Bus and provide combined functionalities. Each device on the I2C Bus would have a unique address by which it can be addressed.

Here's an Interesting Introduction from NXP




I2C on  Induino R3 / Arduino

The I2C Bus uses 2 lines for Communication - SDA(Serial Data) & SCL (Serial Clock). On the Induino R3 / Arduino, these are available on  SDA (Analog Input 4) & SCL (Analog Input 5).  The Induino R3 Also has a I2C Interface to match the Simple Labs RTC Breakout Board.  The I2C bus can be accessed using the 'Wire' Library of Arduino. 

The Simple Labs' RTC Breakout Board
This is a simple breakout board based on the DS1307 RTC IC. The board comes with all the required circuitry for the DS1307 to be interfaced with any microcontroller. It has an on-board Battery holder as well that helps it retain the time settings even when not connected to a microcontroller. If you already don't have the board, You can buy it here - Buy Simple Labs' RTC Breakout Board


Plug in the board as shown below



Programming I2C devices - An Overview

I2C devices are often independent devices that provide us with specific functionality. Each of these devices will have an unique 8-bit address. The 8-bit address limits the number of possible devices on an I2C bus to 255 slaves and 1 Master. In our case, Our ATmega328 would be the master.

The Slave devices provide the the required data through registers. These registers can be accessed / manipulated by the master.
  1. Steps to access data from a device
  2. Create a transmission line with the address of a specific device (so that the device will know that the data is meant for it)  
  3. Write to the device the address of the register we want to access
  4. If we want to write data (set time), Write the Data to the device 1 Byte at a time. The register pointer will keep moving automatically by the number of bytes we write / read.
  5. Close the transmission line (you don't need it to read data)
  6. If we want to read data (get time), Initiate a request to the device, with the number of bytes of data you want to read. It would then read that number of bytes starting from the register location you wrote in step 3 and return it. The returned data is stored in the I2C buffer. 
  7. Read the data from the buffer and process it

Programming the DS1307

While the previous programming guideline is an outline,  the specifics of the data will depend upon the device so you will have to read the datasheet of the device to get a finer understanding. Here are 2 pages from the Datasheet of DS1307 that we need to read before being able to program it. Read it, Read it again, try to understand it before you proceed.

Page 1 of the Datasheet

Page 8 of the Datasheet

Page 8 of the datasheet lists the time keeper registers with their address and what they are used to store. These are the registers which we need to access to set / get time information from the DS1307. Data in these registers is stored in the BCD (Binary Coded Decimal) Format.  Note that different registers are of different size - excepting the years register all the other registers take a 7 bit value.

BCD Format

BCD is a way to represent Decimal Numbers in Binary Format, where each digit of the binary number is represented using a fixed number of bits.  Here's a Quick Example

Decimal Number :                                                                                                 91
Normal Binary Representation of 91 is  =>                                                   0101 1011
For BCD Representation, we split the decimal number into its digits :       9      |       1
and represent the digits in their binary form                                                  1001  |     0001  
So the Binary Representation in BCD would be                                           1001 0001
So to write the decimal number 91 into a register supporting BCD, We need to write the actual decimal value of the Binary Representation in BCD =>  145 (1001 0001)

Re read the above part on BCD again till you are clear ;)

Now if we want to write 91, we need to write 145, but how do we perform this conversion in our program?  Well its simple,

Say 'x' = 91 and 'y' is the value(145) we want to determine, then
 y = (x/10)*16 + (x%10)
 y = (91/10)*16 + (91%10)
 y =  (9*16) + 1
 y =  144 + 1 => 145

Now when we read the registers, we again will get data in the BCD format, we need to convert it to decimal before making use of it. Here's how we can convert the 145 back to 91.

say 'y' = 145 and 'x' is the value(91) we want to determine, then
x = (y/16) *10 + (y%16)
x = (145/16)*10 + (145%16)
x = (9*10) + 1
x = 91

The Hours Register
The data in the hours register of the DS1307 has to be handled differently from that of the other registers. The 6th bit of the hours register is used to determine is the DS1307 is keeping track of time in the 24hr mode or 12hr mode. If it is in the 12 hr mode, then the 5th bit of the hours register is used to keep track of 'AM / PM'. So any time we want to write / read values to/from this register, we need to evaluate these bits and then calculate the actual hour value

So Here our Programming Outline to Set the time (to be done during setup())
  • initialize transmission to the DS1307 (the device address of the DS1307 is 0x68)
  • write the register number to start writing from
  • convert each time parameter to BCD format and then write it to the DS1307
  • for the hour value, check the mode value, get the hour value converted to BCD, Modify the BCD to set the special parameter bits accordingly and finally write the modified BCD value to the DS1307
  • close the transmission line after writing all time parameters
Here's Programming Outline to Get the time (to be done anytime we need to check time)
  • initialize transmission to the DS1307 (the device address of the DS1307 is 0x68)
  • write the register number to start writing from
  • close the transmission line
  • Request 7 bytes of data from the DS1307
  • read each byte of data and then convert it from BCD to normal decimal format and store it in a variable
  • for the hour value, read the 1 byte of data, evaluate the special parameter bits and modify the BCD value accordingly and then Covnert the modified BCD value back to normal decimal and store it in a variable. Also while evaluating the Special Parameter bit, store the time mode in a variable for later use.


Here's a Program which sets and displays the time in the serial monitor

/*  Induino R3 User Guide - Program 11.0 - Interfacing with the Simple Labs DS1307 RTC Module */


#include <Wire.h> // I2C Library

#define myrtc 0x68 // I2C Address of DS1307

char *dow[]={
  " ","MON","TUE","WED","THU","FRI","SAT","SUN"}; // An Array to store the DAY text to match with the DAY parameter of the RTC

char *mode[]={
  "HR","AM","PM"}; // An Array to store the time mode 

int dd,mm,yy,day,hh,mins,ss,mde; // Variables to store the retrieved time value

void setup()
{
  Serial.begin(9600); // Initialise Serial Communication
  Wire.begin(); // Initialise Wire Communication - Join the I2C Bus
  delay(500);
  set_time(31,12,12,7,11,59,50,2); // Call the set_time function to set the intial time.
}

void loop()
{
  get_time();
  Serial.print(dd);
  Serial.print("/");
  Serial.print(mm);
  Serial.print("/");
  Serial.print(yy);
  Serial.print(" "); 
  Serial.print(dow[day]);
  Serial.print(" ");
  Serial.print(hh);
  Serial.print(":"); 
  Serial.print(mins);
  Serial.print(":");
  Serial.print(ss);
  Serial.print(" ");
  Serial.print(mode[mde]);  
  Serial.println();
  delay(1000);
}


// The set_time function takes parameters in the order of date, month, year, day of week, hours, minutes, seconds & mode
// the mode can have 3 possible values  (0=>24HR, 1=> AM, 2 => PM)

void set_time(int sdd, int smm, int syy, int sday, int shr, int smin, int ssec, int smode)
{
  Wire.beginTransmission(myrtc); // Initialise transmission to the myrtc I2C address
  Wire.write(0x00); // Write the value of the register to start with, 0 in this case represented in BCD format
  Wire.write(dec_to_bcd(ssec));  // convert the seconds value from decimal to bcd and write it to the seconds register
  // after the write operation the register pointer will be at the next register, so we do not have to set the value of the register again
  Wire.write(dec_to_bcd(smin));  // convert the minutes value from decimal to bcd and write it to the minutes register
  if(smode == 0) // Check if the mode is 24hrs mode
  {
    Wire.write(dec_to_bcd(shr));  // if 24 hours mode is on then convert the hours value from decimal to bcd and write it to the hours register
  }
  else // if the mode is 12 hr mode
  {
    // If 12 hour mode is selected then the 12 Hour mode bit (the 6th bit) has to be set to 1
    // convert the hour value to bcd first and then adding 64(2^6) to the converted hrs value will set the 6th bit HIGH

    shr = dec_to_bcd(shr)+64; 

    if(smode == 1) // check if it is AM
      Wire.write(shr); // if it is AM we can directly write the value of the above modified hours values to the hours register
    if(smode == 2) // check if it is PM
      Wire.write(shr+32); // If it is PM, then adding 32 (2^5) sets the 5th bit (the PM indication bit) HIGH, the calculated value is written to the hours register
  }
  Wire.write(dec_to_bcd(sday));  // convert the day value from decimal to bcd and write it to the day register
  Wire.write(dec_to_bcd(sdd));  // convert the date value from decimal to bcd and write it to the date register
  Wire.write(dec_to_bcd(smm));  // convert the month value from decimal to bcd and write it to the month register
  Wire.write(dec_to_bcd(syy));// convert the year value from decimal to bcd and write it to the year register
  Wire.endTransmission();  // end the transmission with the I2C device

}


// the get_time() function will retrieve the current time from the RTC and store it in the Global Variables declared

void get_time()
{
  Wire.beginTransmission(myrtc); // Initialise transmission to the myrtc I2C address
  Wire.write(0x00); // Write the value of the register to start with, 0 in this case represented in BCD format
  Wire.endTransmission(); // end the transmission with the I2C device
  Wire.requestFrom(myrtc, 7);  // Now ask the I2C device for 7 Bytes of Data // This corresponds to the values of the 7 registers starting with the 0th register 

  ss = bcd_to_dec(Wire.read()); // The first read will retrieve the value from the register address 0x00 or the seconds register, this is in the BCD format, convert this back to decimal
  mins = bcd_to_dec(Wire.read());// The second read will retrieve the value from the register address 0x01 or the minutes register, this is in the BCD format, convert this back to decimal
  hh = Wire.read();// The third read will retrieve the value from the hours register, this value needs to be processed for the 24/12 hr mode

  // Check of if the BCD hours value retrieved is greater than 35 (this indicates that the hours is in 12 hour mode
  // 35 is the maximum BCD value possible in the 24hr mode
  if(hh > 35) 
  {
    hh = hh - 64; // in the 12 Hours Mode the 12 hour mode bit (6th bit) is set to high, so we need to subtract 2^6 from our hours value
    if(hh > 32)// Now check if the hour value is greater than 32 (2^5 = 32) (this indicates that PM bit (5th bit) is high)
    {
      mde = 2; // Set the mde variable to indicate PM
      hh = hh-32; // subtract 32 from the hours value 
    }
    else // if the hour value is less than 32 it means that its in the AM mode
    {
      mde = 1; // Set the mde variable to indicate AM
    }   
  }
  else // if the 12 hour mode bit was not set, then the hour is in the 24 hour mode
  {
    mde = 0; // Set the mde variable to indicate 24 Hours
  }

  hh = bcd_to_dec(hh); // Convert the final hour value from BCD to decimal and store it back into the same variable
  day = bcd_to_dec(Wire.read());// The fourth read will retrieve the value from the register address 0x03 or the day register, this is in the BCD format, convert this back to decimal
  dd = bcd_to_dec(Wire.read());// The fifthread will retrieve the value from the register address 0x04 or the date register, this is in the BCD format, convert this back to decimal
  mm = bcd_to_dec(Wire.read());// The sixth read will retrieve the value from the register address 0x05 or the month register, this is in the BCD format, convert this back to decimal
  yy = bcd_to_dec(Wire.read());// The seventh read will retrieve the value from the register address 0x06 or the year register, this is in the BCD format, convert this back to decimal

}


// The dec_to_bcd() function converts a given decimal number to BCD format

int dec_to_bcd(int dec)
{
  return dec/10*16 + (dec%10); // convert and return the number from decimal to bcd format
}



// The dec_to_bcd() function converts a given BCD number to decimal format

int bcd_to_dec(int bcd)
{
  return bcd/16*10 + (bcd%16); // convert and return the number from bcd to decimal format
}

Here's another interesting Program that uses the button on pin 7 to enable / disable an Alarm that goes on a particular day at a particular time. This program uses the LCD to display the time.

/*  Induino R3 User Guide - Program 11.1 - Interfacing with the Simple Labs DS1307 RTC Module to Display time on the LCD using the LCD Shield */


#include <Wire.h> // I2C Library
#include <LiquidCrystal.h> // LiquidCrystal Library for LCD

#define myrtc 0x68 // I2C Address of DS1307

char *dow[]={
  " ","MON","TUE","WED","THU","FRI","SAT","SUN"}; // An Array to store the DAY text to match with the DAY parameter of the RTC

char *mode[]={
  "HR","AM","PM"}; // An Array to store the time mode 

char *month[]={
  "","JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"};

int dd,mm,yy,day,hh,mins,ss,mde; // Variables to store the retrieved time value

const int alr_day = 4, alr_hour=12, alr_hour_mode =1, alr_min=0;

boolean alarm_flag = 0; // stores the current state of the alarm 0-> Off / 1-> On
int match_cnt = 0; // This helps us ensure that alarm goes on only on the first instance of the time matching. 

LiquidCrystal lcd(8,9,10,11,12,13); // Create an object of LiquidCrystal class

void setup()
{
  Wire.begin(); // Initialise Wire Communication - Join the I2C Bus
  lcd.begin(16, 2);  // Initialise Communication with LCD
  delay(500);
  set_time(31,12,12,3,11,59,55,2); // Call the set_time function to set the intial time.
  pinMode(7,INPUT_PULLUP); // pinMode for the button pin
}

void loop()
{
  if(digitalRead(7)==0) // Check if the push button is being pressed and toggle the alarm state
  {
    alarm_flag=!alarm_flag;
  }

  get_time();// get the current time from the RTC

  lcd.clear(); // Clear the Display

  //Check if the time matches the alarm time and if the alarm_flag is on and that the first instance of the match has not yet occured
  if(alr_day==day && alr_hour == hh && alr_hour_mode==mde && alr_min==mins && alarm_flag == 1 && match_cnt==0)
  { 
    // set the match count to 1 as we have encountered a time match, this will ensure that within the set minute the alarm goes on only once
    // otherwise the alarm will be on for the whole minute
    match_cnt=1; 

    // A Simple LCD Blink to indicate ALARM
    for(int cnt=0;cnt<5;cnt++)
    {
      lcd.setCursor(5,0);
      lcd.print("ALARM!");
      delay(500);
      lcd.clear();
      delay(500);
    }
  }  
  else // Incase the Time does not match the alarm time then display time as usual
  {  
    lcd.print(dd);
    lcd.print("-");
    lcd.print(month[mm]);
    lcd.print("-");
    lcd.print(yy);
    lcd.setCursor(10,0); // Set the Cursor at a specific location to print the day of the week
    lcd.print(dow[day]);

    lcd.setCursor(0,1);
    lcd.print(hh);
    lcd.print(":");
    lcd.print(mins);
    lcd.print(":");
    lcd.print(ss);
    lcd.setCursor(11,1);
    lcd.print(mode[mde]);// Set the Cursor at a specific location to print the hour mode

    if(alarm_flag == 1) // check if the alarm flag is on and display alarm on indication
    {
      lcd.setCursor(14,1);
      lcd.print("AL");
    }
    delay(1000);
  }
  if(alr_day==day && alr_hour == hh && alr_hour_mode==mde && (alr_min+1)==mins) // check if the set minute has elapsed and reset the match_cnt
  {
    match_cnt=0;
  }
}


// The set_time function takes parameters in the order of date, month, year, day of week, hours, minutes, seconds & mode
// the mode can have 3 possible values  (0=>24HR, 1=> AM, 2 => PM)

void set_time(int sdd, int smm, int syy, int sday, int shr, int smin, int ssec, int smode)
{
  Wire.beginTransmission(myrtc); // Initialise transmission to the myrtc I2C address
  Wire.write(0x00); // Write the value of the register to start with, 0 in this case represented in BCD format
  Wire.write(dec_to_bcd(ssec));  // convert the seconds value from decimal to bcd and write it to the seconds register
  // after the write operation the register pointer will be at the next register, so we do not have to set the value of the register again
  Wire.write(dec_to_bcd(smin));  // convert the minutes value from decimal to bcd and write it to the minutes register
  if(smode == 0) // Check if the mode is 24hrs mode
  {
    Wire.write(dec_to_bcd(shr));  // if 24 hours mode is on then convert the hours value from decimal to bcd and write it to the hours register
  }
  else // if the mode is 12 hr mode
  {
    // If 12 hour mode is selected then the 12 Hour mode bit (the 6th bit) has to be set to 1
    // convert the hour value to bcd first and then adding 64(2^6) to the converted hrs value will set the 6th bit HIGH

    shr = dec_to_bcd(shr)+64; 

    if(smode == 1) // check if it is AM
      Wire.write(shr); // if it is AM we can directly write the value of the above modified hours values to the hours register
    if(smode == 2) // check if it is PM
      Wire.write(shr+32); // If it is PM, then adding 32 (2^5) sets the 5th bit (the PM indication bit) HIGH, the calculated value is written to the hours register
  }
  Wire.write(dec_to_bcd(sday));  // convert the day value from decimal to bcd and write it to the day register
  Wire.write(dec_to_bcd(sdd));  // convert the date value from decimal to bcd and write it to the date register
  Wire.write(dec_to_bcd(smm));  // convert the month value from decimal to bcd and write it to the month register
  Wire.write(dec_to_bcd(syy));// convert the year value from decimal to bcd and write it to the year register
  Wire.endTransmission();  // end the transmission with the I2C device

}


// the get_time() function will retrieve the current time from the RTC and store it in the Global Variables declared

void get_time()
{
  Wire.beginTransmission(myrtc); // Initialise transmission to the myrtc I2C address
  Wire.write(0x00); // Write the value of the register to start with, 0 in this case represented in BCD format
  Wire.endTransmission(); // end the transmission with the I2C device
  Wire.requestFrom(myrtc, 7);  // Now ask the I2C device for 7 Bytes of Data // This corresponds to the values of the 7 registers starting with the 0th register 

  ss = bcd_to_dec(Wire.read()); // The first read will retrieve the value from the register address 0x00 or the seconds register, this is in the BCD format, convert this back to decimal
  mins = bcd_to_dec(Wire.read());// The second read will retrieve the value from the register address 0x01 or the minutes register, this is in the BCD format, convert this back to decimal
  hh = Wire.read();// The third read will retrieve the value from the hours register, this value needs to be processed for the 24/12 hr mode

  // Check of if the BCD hours value retrieved is greater than 35 (this indicates that the hours is in 12 hour mode
  // 35 is the maximum BCD value possible in the 24hr mode
  if(hh > 35) 
  {
    hh = hh - 64; // in the 12 Hours Mode the 12 hour mode bit (6th bit) is set to high, so we need to subtract 2^6 from our hours value
    if(hh > 32)// Now check if the hour value is greater than 32 (2^5 = 32) (this indicates that PM bit (5th bit) is high)
    {
      mde = 2; // Set the mde variable to indicate PM
      hh = hh-32; // subtract 32 from the hours value 
    }
    else // if the hour value is less than 32 it means that its in the AM mode
    {
      mde = 1; // Set the mde variable to indicate AM
    }   
  }
  else // if the 12 hour mode bit was not set, then the hour is in the 24 hour mode
  {
    mde = 0; // Set the mde variable to indicate 24 Hours
  }

  hh = bcd_to_dec(hh); // Convert the final hour value from BCD to decimal and store it back into the same variable
  day = bcd_to_dec(Wire.read());// The fourth read will retrieve the value from the register address 0x03 or the day register, this is in the BCD format, convert this back to decimal
  dd = bcd_to_dec(Wire.read());// The fifthread will retrieve the value from the register address 0x04 or the date register, this is in the BCD format, convert this back to decimal
  mm = bcd_to_dec(Wire.read());// The sixth read will retrieve the value from the register address 0x05 or the month register, this is in the BCD format, convert this back to decimal
  yy = bcd_to_dec(Wire.read());// The seventh read will retrieve the value from the register address 0x06 or the year register, this is in the BCD format, convert this back to decimal

}


// The dec_to_bcd() function converts a given decimal number to BCD format

int dec_to_bcd(int dec)
{
  return dec/10*16 + (dec%10); // convert and return the number from decimal to bcd format
}



// The dec_to_bcd() function converts a given BCD number to decimal format

int bcd_to_dec(int bcd)
{
  return bcd/16*10 + (bcd%16); // convert and return the number from bcd to decimal format
}


Thats It For This Part! Enjoy... and feel free to drop us an email with questions you might have -> info@simplelabs.co.in

 Visit www.simplelabs.co.in for more interesting products

  Back to List of Contents

12 comments:

  1. Both program samples fail to compile.

    Sample 1
    sketch_jul28a.cpp: In function ‘void set_time(int, int, int, int, int, int, int, int)’:
    sketch_jul28a.cpp:61:18: error: call of overloaded ‘write(int)’ is ambiguous
    /usr/share/arduino/libraries/Wire/Wire.h:55:20: note: candidates are: virtual size_t TwoWire::write(uint8_t)
    /usr/share/arduino/hardware/arduino/cores/arduino/Print.h:49:12: note: size_t Print::write(const char*)
    sketch_jul28a.cpp: In function ‘void get_time()’:
    sketch_jul28a.cpp:95:18: error: call of overloaded ‘write(int)’ is ambiguous
    /usr/share/arduino/libraries/Wire/Wire.h:55:20: note: candidates are: virtual size_t TwoWire::write(uint8_t)
    /usr/share/arduino/hardware/arduino/cores/arduino/Print.h:49:12: note: size_t Print::write(const char*)

    Sample 2
    sketch_jul28a.cpp: In function ‘void setup()’:
    sketch_jul28a.cpp:41:13: error: ‘INPUT_PULLUP’ was not declared in this scope
    sketch_jul28a.cpp: In function ‘void set_time(int, int, int, int, int, int, int, int)’:
    sketch_jul28a.cpp:111:18: error: call of overloaded ‘write(int)’ is ambiguous
    /usr/share/arduino/libraries/Wire/Wire.h:55:20: note: candidates are: virtual size_t TwoWire::write(uint8_t)
    /usr/share/arduino/hardware/arduino/cores/arduino/Print.h:49:12: note: size_t Print::write(const char*)
    sketch_jul28a.cpp: In function ‘void get_time()’:
    sketch_jul28a.cpp:145:18: error: call of overloaded ‘write(int)’ is ambiguous
    /usr/share/arduino/libraries/Wire/Wire.h:55:20: note: candidates are: virtual size_t TwoWire::write(uint8_t)
    /usr/share/arduino/hardware/arduino/cores/arduino/Print.h:49:12: note: size_t Print::write(const char*)


    Please Help.

    ReplyDelete
    Replies
    1. Hi,

      Which version of Arduino IDE are you using? Looks like you are not using the latest version... just change the line
      Wire.write(0x00) to Wire.write(byte(0)); in all the locations and it will compile in a breeze
      :)

      Delete
    2. This comment has been removed by the author.

      Delete
    3. You Nailed the problem. Changing the 2 lines and installing Arduino from debian sid cleared up the mess. So, Ubuntu repositories do not contain the new Arduino.
      Thanks for Helping.

      Delete
  2. this program is not showing the exact date and time. i mean current date and time

    ReplyDelete
    Replies
    1. you need to set the current date and time for it to show.. this program will show the date that it uses.

      Delete
  3. bro, can you get me the procedure in short?

    - I've got the Induino R3 board and DS1307 RTC breakout Board
    - Connected the RTC in the steps mentioned above.

    - Can't figure out how to set the Date and TIme and Display it on the LCD.

    ReplyDelete
  4. Sir, When I switch off the induino the time and date resets during the next power on. Not taking from RTC as said.RTC with battery in ckt. Pls Help,

    ReplyDelete
    Replies
    1. Hi, First set time, then remove rtc, change program on the board to jus read time and then plug in rtc, if you still had the previous program to set time, it would keep setting the time every time you power on.

      Delete
  5. ..with LCD (induino kit) on project11_1..

    ReplyDelete
  6. Can you show how to interface the ds1307 rtc without the breakout board?

    ReplyDelete
    Replies
    1. http://quickstartkitforarduino.blogspot.com/2012/05/simple-labs-quick-start-kit-for-arduino_17.html

      Delete