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.