Reinventing the wheel: I2C implementation with DS1307 RTC

Hi all,

This is my first post over here, but I've been lurking around the forums for quite some time now. I'm new to electronics and recently started dabbling in it about 6 months ago. I've got a full time job, so this is pretty much a hobby for me. This post is for bragging as well as posting my findings over here for anyone who's interested. :slight_smile:

Recently, I saw a video of the DotKlok and was inspired to make the same. So I got myself the DS1307 and started reading the datasheet. Didn't really understand much, but saw that it uses the I2C bus protocol to transfer data. So, started reading up on the I2C protocol and I found it very interesting. That, and other resources on the net, helped me understand how the I2C works and how to use it with the DS1307.

So I decided to try and implement the I2C bus manually without using the arduino library Wire.h. It seemed simple enough, you just need two lines which are turned high or low to communicate like Morse code (I told you, I'm not an electronics engineer!). I followed the circuit diagram given here and set it up. But it's annoying that there's really not much to find on the web for this when you're really stuck.

The toughest part, for me, was figuring how to turn the data and clock lines high and low! I2C requires the Data Line (SDA) and Clock Line (SCL) to be in high when idle. Any communication happens by pulling the lines to low. In PIC C code, this I suppose is done by setting SDA=0 and SCL=0. (Refer: I2C tutorial) But how do we do that here? SDA and SCL should not be made active HIGH by the microcontroller since that would cause Logic Contention in the circuit. Logic Contention, as I understand it, happens when you've got 2 devices bringing the line to high at the same time. It shows up in Proteus without which I wouldn't have been able to find the problem.

So after a lot of reading on the Arduino website, Googling around and experimenting with the board, I finally realised that I shouldn't raise the lines to high, but should rather let them float to high using the pullup resistors in the circuit. But how do I do that without setting the pins to Output? The answer is by setting the pins to Input. When you set the pin to input, it uses a very minuscule amount of current to detect whether the line is high or low without affecting the circuit, effectively not letting current pass through. So the line goes to high. And how do you make the line go Low? By setting the pin as output! The reason is that when you put the pin to output, it defaults to a Low which is ground. If the pin is grounded, it pretty much brings the line down i.e. low. This was the EUREKA moment for me :).

I've attached the code in it's entirety and it's working well with the DS1307. Couldn't paste it as it was exceeding the character limit. I'll be happy to get some input/improvements about the code or any advice/warnings/pitfalls about the way I've set it up. I haven't referred to Wire.h, so don't know if this is how it's done there as well.

EDIT: Updated the file for one error in handling 12hr/24hr modes

i2c_ds1307.ino (7.8 KB)

Posting full code over here:

/*
TODO:
Implement time clock format for output -- DONE
Implement Clock line stretching -- DONE
*/

int DATAPIN = 8;
int CLOCKPIN = 9;
int WAITAMOUNT = 0;


/* DS1307 Addresses specified here */
byte SECONDS = 0x00; 
byte MINUTES = 0x01; 
byte HOURS   = 0x02;
byte DAY     = 0x03;
byte DATE    = 0x04;
byte MONTH   = 0x05;
byte YEAR    = 0x06;


/*
I used 2 LEDs to debug the setup. It's easier and more convenient
than using the Serial Monitor and necessary when testing using Proteus.
Of course, final testing was done using the Serial Monitor. Enable the flag to 
use LED Debugging
*/

int LEDPIN = 10; //Use for counting response value
int LEDPIN2 = 11; //Use for ack response and to signal progress

boolean enable_led_debug = false;

int BLINKTIME = 150;

byte ADDRESS_WRITE = B11010000;
byte ADDRESS_READ  = B11010001;

void setup(){
  
  pinMode(LEDPIN, OUTPUT);
  pinMode(LEDPIN2, OUTPUT);
  Serial.begin(9600);
  if(enable_led_debug) blink_led(LEDPIN, 4, BLINKTIME);
  i2c_init();

//Uncomment next 3 lines to set the time. Use the addresses for reference as given at the top
/*
  ds1307_init(); //Set the Clock Halt (CH) bit to 0
  byte current_time[7] = {0, 30, 0, 6, 15, 6, 12}; 
  ds1307_set_time(current_time);
*/
}

void loop(){
  //Show time every second. 
  byte time[7];
  ds1307_get_time(time);
  print_time(time);
  delay(1000);
}

void ds1307_init(){
  //Set Clock Halt bit to 0
  boolean response=false;
  i2c_start();
  i2c_send(ADDRESS_WRITE);
  i2c_send(0x00);
  i2c_send(B00000000); //Clock Halt is 8th Bit i.e. Bit 7. 0 enables clock, 1 disables it
  i2c_stop();  
}

void ds1307_set_time(byte time[]){
  encode_time(time);
  i2c_start();
  i2c_send(ADDRESS_WRITE);
  i2c_send(SECONDS); //Seconds address
  for(int i=0; i<7; i++){
    i2c_send(time[i]);
  }
  i2c_stop();

}

void ds1307_get_time(byte time[]){
  i2c_start();
  i2c_send(ADDRESS_WRITE);
  i2c_send(SECONDS);
  i2c_stop();
  
  i2c_start();
  i2c_send(ADDRESS_READ);
  for(int i=0; i<6; i++){
    time[i] = i2c_read(1);
  }
  time[6] = i2c_read(0); //Send NACK
  i2c_stop();
  decode_time(time);
}

/* 
Decode all parts of time and write it into the same array
*/
void decode_time(byte time[]){
  for(int i=0; i<7; i++){
    if(i==HOURS){ // Hours need special handling. We will return 24 hr format always, but need to see how it's stored in the DS1307
      if(time[i] & B01000000){ // 12 hr format
        if(time[i] & B00100000){ // Is PM?
          time[i] = bcd_decode(time[i] & B00011111)+12;
        } else { //Is AM!
          time[i] = bcd_decode(time[i] & B00011111);
        }
      } else { // 24 hr format
        time[i] = bcd_decode(time[i] & B00111111);
      }
    } else {  // All others are simple bcd
      time[i] = bcd_decode(time[i]);
    }
  }  
}

/*
Encode the time in the array to write to the DS1307
*/

void encode_time(byte time[]){
  for(int i=0; i<7; i++){
    time[i] = bcd_encode(time[i]); //We always put hours in 24 hr format. 
                                   //Better to handle it in the software
                                   
  }
}

/*
Print using the USB Serial input. 
*/
void print_time(byte time[]){
  Serial.print(time[HOURS]);
  Serial.print(":");
  Serial.print(time[MINUTES]);
  Serial.print(":");
  Serial.print(time[SECONDS]);
  Serial.print("   ");
  Serial.print(time[DATE]);
  Serial.print("/");
  Serial.print(time[MONTH]);
  Serial.print("/");
  Serial.println(time[YEAR]); 
}

/*
I2C handling starts here
*/

/*
Make both the data line and the clock line high
*/
void i2c_init(){
  setData(HIGH);
  setClock(HIGH);
}

/*
Signal the start of the communication
*/

void i2c_start(){
  setData(LOW);
  setClock(LOW);  
}

/*
Signal the end of the communication
*/

void i2c_stop(){
  setData(LOW);
  setClock(HIGH);
  setData(HIGH);
}

/*
Send the data to specified device
*/

boolean i2c_send(byte value){
  byte comp = B10000000;
  for(int i=0; i<8; i++){
    if(value & comp){
      setData(HIGH);
      setClock(HIGH);
      setClock(LOW);
    } else {
      setData(LOW);
      setClock(HIGH);
      setClock(LOW);
    }
    comp=comp>>1;
  }

  setData(HIGH);
  setClock(HIGH);

  boolean ack = digitalRead(DATAPIN);
  setClock(LOW);
  if(ack){
    if(enable_led_debug) blink_led(LEDPIN, 1, BLINKTIME);
    Serial.println("didn't receive ack");
  } else {
    if(enable_led_debug) blink_led(LEDPIN2, 1, BLINKTIME);
//    Serial.println("received ack"); //Uncomment for debugging
  }

  return !ack; //Response is active low. So ack will be false
                // if the IC responds. We want to send true if 
                // IC responds
}

/*
Read from the device. send_ack should be 0 to signal NACK to the slave
*/

byte i2c_read(boolean send_ack){
  setClock(LOW);
  setData(HIGH);
  byte read_value = B00000000;
  for(int i=0; i<8; i++){
    
    do{ //Required for clock stretching
      setClock(HIGH);
    } while (digitalRead(CLOCKPIN)==LOW);
    
    boolean inbit = digitalRead(DATAPIN);
    delay(WAITAMOUNT);
    byte temp = B10000000;
    temp=temp>>i;
    if(inbit){
      read_value=read_value|temp;
    } else {
    }
    setClock(LOW);
  }
  
  if(send_ack){
    setData(LOW);
  } else {
    setData(HIGH);
  }
  
  setClock(HIGH);
  setClock(LOW);
  
  //Serial.println(bcd_decode(read_value)); //Uncomment for debugging
  if(enable_led_debug) blink_led(LEDPIN, read_value, BLINKTIME);
  return read_value;
}

/*
Since the DS1307 gives data in Binary coded decimal (BCD), this function 
helps decode it into regular binary/integer values
*/

byte bcd_decode(byte val){
  byte high_comp = B11110000;
  byte low_comp = B00001111;
  
  byte high_num = (val&high_comp)>>4;
  byte low_num = val&low_comp;
  
  return (high_num*10+low_num);
}

/*
This takes a regular integer/binary value and converts it to BCD
*/

byte bcd_encode(byte val){
  
  byte high_num = (val/10)<<4;
  byte low_num = (val%10);
  
  return (high_num|low_num);
  
}

/*
Just a blink function, nothing much
*/

void blink_led(int pin, int num_times, int time_gap){
  for(int i=0; i<num_times; i++){
    digitalWrite(pin, HIGH);
    delay(time_gap);
    digitalWrite(pin, LOW);
    delay(time_gap);
  }
}

/*
The toughest part, for me at least! I2C requires the Data Line (SDA) and Clock
Line (SCL) to be in high when idle. Any communication happens by pulling the lines
to low. In PIC C code, this I suppose is done by setting SDA=0 and SCL=0.
(Refer: http://www.robot-electronics.co.uk/acatalog/I2C_Tutorial.html)
But how do we do that here? SDA and SCL should not be made active HIGH by the 
microcontroller since that would cause Logic Contention in the circuit. Logic 
Contention, as I understand it, happens when you've got 2 devices bringing the
line to high. It shows up in Proteus without which I wouldn't have been able to find it

So after a lot of reading on the Arduino website, Googling around and 
experimenting with the board, I finally realised that I shouldn't raise the 
lines to high, but should rather let them float to high using the pullup resistors
in the circuit. But how do I do that without setting the pins to Output? The answer
is by setting the pins to Input. When you set the pin to input, it uses a very 
miniscule amount of current to detect whether the line is high or low without affecting
the circuit, effectively not letting current pass through. So the line goes to high.
And how do you make the line go Low? By setting the pin as output! The reason is 
that when you put the pin to output, it defaults to a Low which is ground. If the
pin is grounded, it pretty much brings the line down i.e. low. This was the EUREKA
moment for me :).
*/

void setClock(boolean set_high){
  if(set_high){
    pinMode(CLOCKPIN, INPUT); //
    delay(WAITAMOUNT);
  } else {
    pinMode(CLOCKPIN, OUTPUT); //
    delay(WAITAMOUNT);
  }
}

void setData(boolean set_high){
  if(set_high){
    pinMode(DATAPIN, INPUT); //
    delay(WAITAMOUNT);
  } else {
    pinMode(DATAPIN, OUTPUT); //
    delay(WAITAMOUNT);
  }
}

Seems like a lot of work to do what the internal I2C hardware does for your already.

CrossRoads:
Seems like a lot of work to do what the internal I2C hardware does for your already.

That's for sure :). The whole idea was to learn how this stuff works.

So why not learn how the internal hardware works?

By internal hardware do you mean the arduino i2c library implementation or does the arduino implement the i2c bus protocol completely in the hardware. I was under the impression that the library does pretty much what I'm doing, through software implementation. I did wonder why it specified using Analog pins A4 and A5 when I could do it using any of the digital pins. There isn't really much documentation regarding this (Wire - Arduino Reference).

By internal hardware I mean the internal clock & shift register, SCL & SDA, that are located at A4 & A5.
You don't have to use the <Wire.h> if you don't want, but at least use the hardware that's there.
There's plenty of documentation in the Atmel datasheet, not all of it is repeated at arduino.cc

But an SCL and SDA are just simple lines that are high at +5V and low at GND. I'm not sure how these are different from any other pin that I choose to use. Do you mean to say that they have internal pullup resistors that help maintain the voltage level? I do know that there are ways to use the internal pullup resistors with all or some of the pins on the arduino.

Apologies if I have trouble understanding. I'm new to electronics and trying to understand the difference. :slight_smile:

Hi PlastyGrove

I too am just learning about various com. protocols. If I understand it correctly, they actually have part of the logic implemented in hardware. That takes most of the load away from your Atmegas processor and frees it up for other things.

Still, I'm pretty impressed with what you did. I am sure you now understand I2C a lot better than I do.

fkeel:
I too am just learning about various com. protocols. If I understand it correctly, they actually have part of the logic implemented in hardware. That takes most of the load away from your Atmegas processor and frees it up for other things.

Still, I'm pretty impressed with what you did. I am sure you now understand I2C a lot better than I do.

Thanks fkeel. One of the reasons for posting here was to show-off :).

The thing about I2C is that there's really not a whole lot of logic that you can embed. It's basically an address that gets sent and some data that gets transmitted or received. At the lowest level, it's turning the SDA and SCL on and off. Rest of it is just interpreting the data that gets transmitted and I don't see a whole lot of what can be done in hardware. The address, the data and everything depends on the device that you're communicating with. Perhaps converting Base 10 to BCD and vice versa? I'll try and go through the Atmega datasheet after I get home and see if there's something I'm missing. I definitely agree that if there's something that can be done with hardware, better go with that than software.

Okay, which is quicker:

Hardware: send out B01010101 for me.
(byte get written to a register, hardware takes care of sending it for you)
1 command, it is done, data is blasted out at 400 KHz.

Software: Do all the stuff you listed above, using AT LEAST one instruction per command:
Clock high.
data bit low
clock low
clock high
data bit high
repeat several more times
clock in acknowledge bit
etc.

Fast? Doubtful.

Hmm, okay I get it now. So the hardware on the microcontroller will allow me to do the same thing using lesser number of cycles, perhaps send out the entire address in a single cycle. Let me see which registers and instructions do that for the Atmega and how I can interact directly with the hardware using the arduino.

Thanks! :slight_smile:

EDIT: Came across this excellent reference on how the Wire.h actually works. Apparently, it blocks processing until the data transfer completes which the author says is a waste of time since the CPU could be doing something else while the transfer is happening. The post also gives a link to the complete Atmega328 datasheet (567 pages) which has several pages on the I2C. Why am I so excited to have found this?! :slight_smile:

plastygrove:
Apparently, it blocks processing until the data transfer completes which the author says is a waste of time since the CPU could be doing something else while the transfer is happening.

I'm sure I read somewhere that it's no longer blocking, but I may be wrong. Here's a good reference too: http://arduino.cc/playground/Code/ATMELTWI

plastygrove:
I was under the impression that the library does pretty much what I'm doing, through software implementation.

Not at all. The hardware is pretty sophisticated. Just as an example, you can go into "power down" sleep mode, and have an incoming I2C message, addressed to this processor, wake it up! Try doing that in software. :stuck_out_tongue:

Example code here: http://gammon.com.au/power (search for "Waking I2C slave from sleep").

Also the slave can be beavering away doing other stuff, and when an incoming I2C message, addressed to this processor, arrives, it generates an interrupt. You can't do that with a software implementation.

You could also send asynchronously, but the current library doesn't do that. I think this alternative library might:

I have quite a bit on I2C here: http://gammon.com.au/i2c

Well, this project was pretty much to see if I could manually communicate with a device using I2C manually without using any library or anything. It's kind of like picking up the phone and humming some sounds into it to see if you can make a call. I'm glad I could do it by reading datasheets since I'm a beginner. :smiley:

@Elijahg: Thanks for the link. I'll go through it. By no means do I intend to use the code I wrote in actual projects. It's more or less a POC.

@Nick Gammon: That's some really really good stuff on hardware interrupts and I2C. I'll make sure to go through them all. From the looks of it, there's a lot more I need to learn about the capabilities of the $10 Atmega328 :slight_smile:

$10? You're shopping in the wrong stores! $6 max if programmed, $3 & less for unprogrammed parts.
Yes indeed, the little chip can do quite a bit in hardware.

I finally got round to implementing CrossRoads' suggestion to use the Atmega328's hardware to do I2C, and boy is it hard! Took me 3 days to get it to work and I think it's reasonably okay now. Just goes to show how much effort the Arduino team has put in to simplify this. However, and here's the beauty of it, a complete EEPROM I2C implementation for the 24C16 takes about 926 bytes. This includes basic functions to write and read. On the other hand, the blink example in the Arduino takes 1084 bytes. Goes to show how much space can be saved if you directly work with the registers. Not that I would recommend it. I'm not sure if the effort is worth the savings, since there's a huge learning curve involved and you'll be spending about 90% of your time fixing issues. But I must admit, it's more fun than using the Arduino :slight_smile:

If anyone's interested, I've attached the code along with this post. A lot of it is thanks to this post on embedds.com

TestI2C.cpp (4 KB)