dereenigne.org

reverse engineered

DS1307 I²C Clock

I wanted to learn how to interface with I²C devices using my Arduino, so I bought a DS1307 clock and crystal kit from Earthshine Electronics. I’ve no association to them, but I’d recommend having a look at their shop. Their prices are very reasonable, unlike most Arduino component retailers.

I²C is an addressable serial interface. That means that you can communicate with many I²C devices using the same 4 pins all the time. On the Arduino Duemilanove, the SDA and SCL lines are connected to analog in pin 4 and analog in pin 5 by default, but this can be changed by editing the Wire library. The I²C specification requires the SDA line to be pulled up when not in use, but all of the ATmega328 pins have internal pull up resistors, which saves us having to use an external one. The DS1307 datasheet shows the full pinout of the chip.

Below is a schematic of the circuit diagram. It looks slightly different to my actual circuit, but that was only because Fritzing didn’t have the components in the library, so I either replaced them with functionally identical components or else made them using a generic component. A Nokia 3310 LCD shield was added to display the time, but again, this doesn’t exist in Fritzing.

The DS1307 uses a 32.768kHz crystal connected between pins 1 and 2 to create the oscillations needed to drive the clock. The downside to this, of course, is that temperature variations will cause the clock to lose or gain seconds. In my experience however, this was negligible. Once set, the DS1307 will keep time provided that it is powered either by the Arduino, or the 3V button backup battery. I wrote the Arduino sketch to accept the date in the standard output format from the *nix date command. That meant that to sync the clock, I simply had to pipe the date command to the serial port.

sudo date > /dev/ttyUSB0
//Fri Sep  3 14:18:22 IST 2010

I tried to be as efficient as possible with the sketch size, and I use a hash table approach to convert the date output into a numeric byte. This approach won’t always work. But I was lucky in that there are no collisions in the function below for the input [Mon-Sun] and [Jan-Dec].

\[f(x,y,z)=x+y+z \\Where\:x,y,z\:corresponds\:to\:a\:3\:letter\:input.\]

I’m not quite sure why Dallas Semiconductor chose to add a 64 byte memory space rather than the minimum 8 byte memory space required for the clock operation. Economies of scale on the 64 byte parts perhaps, I’m not sure. This makes me a little less guilty about not utilising that that unused 56 bytes of RAM. I’m not quite sure if anybody has come up with any uses for such a small amount of memory, given that the ATmega328 has 512 bytes of EEPROM, but it might be of use to someone.

To make this easy to show to non-techy people, I decided to display the time on the Nokia 3310 LCD shield I had. I had to modify Andrew Lindsay’s Nokia 3110 LCD library a little bit to add the : and / characters to the font_big charset. I’ve passed these changes upstream so hopefully Andrew will incorporate them into the next release of his library. Once I had the library updated, a little bit of bit manipulation was required to convert the bytes to ASCII to be displayed.

#include <Wire.h> // include the I2C library
#include <nokia_3310_lcd.h> //include LCD library
#define DS1307_I2C_ADDRESS 0x68 // DS1307 I2C address

Nokia_3310_lcd lcd=Nokia_3310_lcd(); // instantiate new LCD instance

byte second, minute, hour, dayweek, day, month, year; // create bytes for storing values read from DS1307
// SS[0-60], MM[0-60], HH[0-24], D[1-7], DD[1-31], MM[1-12], YY[00-99]

char buffer[50]; // temporary buffer to store incoming serial data
 
void setup() {
  Wire.begin(); // initalise I2C interface
  lcd.init(); // initialise LCD
  lcd.clear(); // clear LCD
  lcd.writeString(0,0,"DS1307 I2C RTC",0);
  lcd.writeString(0,2,"dereenigne.org",0);
  delay(5000);
  lcd.clear();
  Serial.begin(9600); // initialise serial 
}

void loop() {
  if (Serial.available()) {     
    fillBuffer();
    setDateDS1307();
  }
   
  getDateDS1307(); // get the current time and store it
  writeLCD(); // output time to LCD
  //writeSerial(); // output time over serial
  delay(250);
} 
  
// convert bin to BCD
byte dec2bcd(byte val){
  return ((val/0xA*0x10)+(val%0xA));
}

// convert BCD to binary
byte bcd2dec(byte val){
  return ((val/0x10*0xA)+(val%0x10));
}

// convert two ASCII numbers from the input array to a single byte
byte digitToByte(int x,int y){
  return (byte) ((buffer[x]&0x0F)*10+(buffer[y]&0x0F));
}

// convert ASCII letters from the input array to a byte containing the day of the week
byte dayToByte(int x,int y, int z){
  int temp = buffer[x]+buffer[y]+buffer[z];
  switch(temp){
    case 0x12A: return (byte) 1; break; // Mon
    case 0x12E: return (byte) 2; break; // Tue
    case 0x120: return (byte) 3; break; // Wed
    case 0x131: return (byte) 4; break; // Thu
    case 0x121: return (byte) 5; break; // Fri
    case 0x128: return (byte) 6; break; // Sat
    case 0x136: return (byte) 7; break; // Sun
    default: return (byte) 0;
    }
}

// convert ASCII letters from the from the input array to a byte containing the month
byte monthToByte(int x,int y, int z){
  int temp = buffer[x]+buffer[y]+buffer[z];
  switch(temp){
    case 0x119: return (byte) 1; break; // Jan
    case 0x10D: return (byte) 2; break; // Feb
    case 0x120: return (byte) 3; break; // Mar
    case 0x123: return (byte) 4; break; // Apr
    case 0x127: return (byte) 5; break; // May
    case 0x12D: return (byte) 6; break; // Jun
    case 0x12B: return (byte) 7; break; // Jul
    case 0x11D: return (byte) 8; break; // Aug
    case 0x128: return (byte) 9; break; // Sep
    case 0x126: return (byte) 10; break; // Oct
    case 0xCF: return (byte) 11; break; // Nov
    case 0x10C: return (byte) 12; break; // Dec
    default: return (byte) 0;
    }
}

// handle RX'd serial data
void fillBuffer(){
  delay(500); // wait for rest of transmission
  int ptr =0;
  buffer[ptr++]=Serial.read();
  while(Serial.available() > 0) { // while there is serial data, read it
    buffer[ptr++]=Serial.read(); // store it in the buffer
    if(buffer[ptr]=='/n'){ // break on newline
      break;
    }
  }
} 

// set the DS1307 using the input piped from serial in Unix date format
// Fri Sep  3 14:18:22 IST 2010
void setDateDS1307(){
   second=digitToByte(17,18);
   minute=digitToByte(14,15);
   hour=digitToByte(11,12);
   dayweek=dayToByte(0,1,2);
   day=digitToByte(8,9);
   month=monthToByte(4,5,6);
   year=digitToByte(26,27);
   
   Wire.beginTransmission(DS1307_I2C_ADDRESS);
   Wire.send(0x00);
   Wire.send(dec2bcd(second));
   Wire.send(dec2bcd(minute));
   Wire.send(dec2bcd(hour));
   Wire.send(dec2bcd(dayweek));
   Wire.send(dec2bcd(day));
   Wire.send(dec2bcd(month));
   Wire.send(dec2bcd(year));
   Wire.endTransmission();
}

// get the date and time from DS1307
void getDateDS1307(){
  Wire.beginTransmission(DS1307_I2C_ADDRESS);
  Wire.send(0x00); // reset DS1307 register pointer
  Wire.endTransmission();

  Wire.requestFrom(DS1307_I2C_ADDRESS, 7);
  second=bcd2dec(Wire.receive() & 0x7f); // control bits
  minute=bcd2dec(Wire.receive());
  hour=bcd2dec(Wire.receive() & 0x3f); // control bits
  dayweek=bcd2dec(Wire.receive());
  day=bcd2dec(Wire.receive());
  month=bcd2dec(Wire.receive());
  year=bcd2dec(Wire.receive());
}

// write data to LCD. Convert byte to ASCII on write.
// take each byte, divide by 10 or mod by 10 and XOR result with 0x30 to convert to ASCII
void writeLCD(){
lcd.writeCharBig(0,0,(hour/0xA)^0x30,0);
lcd.writeCharBig(12,0,(hour%0xA)^0x30,0);
lcd.writeCharBig(24,0,':',0);
lcd.writeCharBig(28,0,(minute/0xA)^0x30,0);
lcd.writeCharBig(40,0,(minute%0xA)^0x30,0);
lcd.writeCharBig(52,0,':',0);
lcd.writeCharBig(56,0,(second/0xA)^0x30,0);
lcd.writeCharBig(68,0,(second%0xA)^0x30,0);
lcd.writeCharBig(0,10,(day/0xA)^0x30,0);
lcd.writeCharBig(12,10,(day%0xA)^0x30,0);
lcd.writeCharBig(24,10,'/',0);
lcd.writeCharBig(28,10,(month/0xA)^0x30,0);
lcd.writeCharBig(40,10,(month%0xA)^0x30,0);
lcd.writeCharBig(52,10,'/',0);
lcd.writeCharBig(56,10,(year/0xA)^0x30,0);
lcd.writeCharBig(68,10,(year%0xA)^0x30,0);
}

// write the date and time to serial if LCD is not available
void writeSerial(){
  Serial.print(hour, DEC);
  Serial.print(":");
  Serial.print(minute, DEC);
  Serial.print(":");
  Serial.print(second, DEC);
  Serial.print("  ");
  Serial.print(day, DEC);
  Serial.print("/");
  Serial.print(month, DEC);
  Serial.print("/");
  Serial.print(year, DEC);
  Serial.println();
}

My new hobby is pretending to make time stop by pulling out the crystal. I feel like the little boy in an old children’s tv show called Bernard’s Watch. Alas, the world doesn’t stop when I pull out the crystal.

DS1307 source code.


comments powered by Disqus