Arduino and the Maxim DS1306 Real Time Clock

I’m working on a project that needs a real-time clock, and so I thought I’d learn how to use one by interfacing it with an Arduino. Ultimately, it will interface with another AVR, but the Arduino is great for just prototyping the design, to make sure I really know how to work with the device.

I chose the Maxim DS1306 because it is an SPI compatible device, meaning I can just throw it on an SPI bus along with anything else. It’s also register based, so setting the time or reading the time is a fairly simple affair — you just read or write the appropriate register. The DS1306 has a sibling, the DS1305. Near as I can tell, the major differences are the existence of a 1Hz interrupt-drivable heartbeat on the DS1306.

The device also has a nice range of options for backup power, and keeping time even when primary power is cut off. I can use nothing and just loose the time, or I can hook up a 3V lithium battery to VBAT, or a 3V rechargeable, or a super capacitor. The last two options can be trickle-charged by the DS1306 while it is on primary power, so essentially, for most practical cases, the device will always keep the time, even when disconnected from primary power. It can even continue to generate an alarm (see below) while off primary power.

If you need a simple heartbeat, the DS1306 can provide an open-drain 1Hz signal that’s also suitable for driving an interrupt pin. It also provides a 32.768kHz signal that could be used similarly (the same frequency as the crystal used to drive the DS1306.)

The only real external components you’ll need for this are a 32.768 kHz crystal (pretty standard) and possibly a couple of pull-up resistors and some kind of battery (rechargeable or a simple lithium coin cell) or a super capacitor as back up power. Or, if you go without the backup power, just ground VCC2 and VBAT, as per the specification sheet.
Schematic

Easy-peasy. If you don’t plan on using INT0 for an alarm and/or don’t plan on using the 1Hz pulse you can leave those disconnected and you won’t need the pull-up resistors. (I used a 10K Ohm resistor.) The spec sheet for the DS1306 will tell you this, but you should tie VBAT to ground if you’re not using it. There’s a whole section on how to deal with VCC2 and VBAT depending on whether you actually have a backup supply, use a fixed battery, rechargeable or a super-capacitor, and how you charge the chargeable stuff. Read it carefully.

Programming
The device uses the SPI bs to communicate, and does so in SPI Mode 1, which means that the clock polarity is such that the leading edge is rising and the trailing edge is falling. (SPI has four modes, numbered 0-3, with all the combinations of clock polarity and clock phase. The device you’re using should specify either the mode or say something about the particular clock polarity or clock phase, and then you need to adjust the SPCR register appropriately. It’s simple, but a nuisance and probably the first thing you should check if something isn’t working correctly.) But, the clock phase is such that the leading edge is when the data is setup, and the trailing edge is when the data is sampled. The clock phase may seem “opposite” of convention, but it’s just another way of doing business. (In fact, if you look in the ATMega8 specifications — or any of the Atmel microcontrollers that support Serial Peripheral Interface – you’ll find that there are four SPI modes, with all the combinations of clock phase and clock polarity.) It’s just that the DS1306 expects the clock phase to sample on the trailing edge. How do I know this? The DS1306 specification document says so in its section on the Serial Peripheral Interface. (Woe is me for not reading the document thoroughly the first time I tried to use it – I didn’t even think that it might operate in a different SPI mode until a couple of hours of debugging finally lead me there..)

So, our set up of the ATMega8’s SPI interface has to be such that it’s ready to operate in the right mode. This little code idiom will do it:

  // SPI Enable (SPE) to one
  // Master/Slave Select (MSTR) to one turns the ATMega8 into the master
  // Clock Phase (CPHA) set to 1 means sample on the trailing edge
  // sample on trailing edge of clk for the DS1306
  SPCR = (1<<SPE)|(1<<MSTR)|(1<<CPHA);
  clr=SPSR;
  clr=SPDR;

Check the ATMega8 spec for more details about these registers.

(Parenthetically, I’ve pretty much got to the point where I have a bunch of devices on the SPI bus, and some of them require different modes. I’ve abstracted all of this functionality into read_register and write_register functions that accept a host of parameters, including what the clock phase and such all should be.)

What you do to program the DS1306 is basically write to and read from the registers. The registers hold the current time, the alarm settings, a number of control bits, and a small, 96 byte scratchpad of user RAM that you can read and write for your own purpose.

Each of the registers has it’s own read from and write to address, meaning each register has two addresses — one you use to read from the register, and one you use to write to the register. If you want to set the current day of the week, you’d write a value to the register at address 0x83. If you wanted to find out the current seconds, you’d read from the register at address 0x00.

Here’s the entire register table.

The control register manages enabling the two alarms, enabling the 1Hz heart beat output, and turning on or off the write protection, if you want to prevent any accidental writing of the registers. If you want to do a basic initialization of the device, the first thing you’d do is probably write 0x04, which would enabling writing registers, turn the heart beat on, and disable both alarms. Then you’d set the time by writing the the second, minute, hour and day of week registers, at which point the device would start keeping time. (Setting the right time initially requires getting the right time somehow – presumably through some sort of interface with the time keeping mothership or by setting it manually.)

BCD Time Data
All the time is stored in these registers in binary coded decimal (BCD), which is a data format appropriate for storing decimal numbers in a binary register. Decimal, of course, are numbers between 0-9. As is the case with time and date data, all the numbers there are decimal, too. So, storing this information as BCD makes sense — you don’t have to do any conversions from hexidecimal to a suitable display format. In other words, converting 0x0A to a number that represents seconds is avoided — the digit to be represented is what is stored in the register.

BCD stores each digit in half a byte (or a nibble, or 4 bits). Now, with 4 bits, you can represent any integer between 0-15 (or 0x00-0x0F in hexadecimal.) With BCD, the only “legal” integers are 0-9 (or 0x00-0x09 in hexadecimal). By convention, the other numbers are just ignored, or treated as illegal.

The DS1306 plays the same game. So, when you want to set the seconds to 45, you’d write 0x45 to the register at 0x80. If you wanted to set the hour to 8, you’d write 0x08 to the register at 0x82. Funny thing about hours to note is that you can either track in 24 hour time or the AM/PM variety. It’s up to you, but you need to set your register appropriate. It gets just a little bit more complicated with AM/PM time — read the spec for details on how to deal with that.

Alarms
The great thing about this RTC is that you can set a real-time alarm. Actually, the device supports two alarms in the form of the INT0 (active low) and INT1 pins. These pins are suitable for generating an signal on a microcontroller interrupt pin. (INT0 is active low, and open drain, so you’ll need to tie a 10K resistor or something from pin 5 to VCC1 to get a suitable signal to the microcontroller.)

Each alarm is set through four registers, one each for seconds, minutes, hours and day of week. The registers are used to match against the current time. In order to set up an alarm, you’ll need to set these registers to the time and day of the week you want the alarm to occur. Or, you can use the special mask bits, bit 7 of each register, to indicate a kind of catch-all. For instance, setting the mask bit in the minutes register indicates “all” minutes for the specified second, hour and day of week. Or, you could use the mask bit to have an alarm occur every minute, by setting the seconds register to 0x00, and setting the mask bit of all the other registers.

I mentioned the two alarms on the DS1306. Well, it turns out that INT1 is really only available when the device is powered by VCC2 (the backup supply) or VBAT (the battery supply). The other, the active-low interrupt 0 output INT0, is available when the device is powered by by VCC2, VBAT or VCC1 (the primary supply.) So, I guess you can have a special alarm that occurs when the device is running on the battery, or maybe this was an electrical design issue – INT1 isn’t open-drain, so it’s driven by VBAT or VCC2, which is something to keep in mind.

Here’s the Wiring/Arduino code to work with the DS1306 on the ATMega8, not the ATMega168 which is what the modern Arduinos use as their microcontroller. You’ll need to double-check which pins you’re hooked up to if you’re using another microcontroller.

Scroll down below to find the code for the modern Arduino’ss

This code sets the device up, and starts an alarm at 0 seconds, and then every 15 seconds thereafter. The alarm is the INT0 active low one on pin 5 of the DIP package. You’ll need to tie a pull-up resistor from that pin to VDD, or pin 9, and then tie a bit of hook-up wire from pin 5 to Arduino digital pin 3 (which is INT1 on the ATmega8).

You can also download the ATMega8 code here.

// ATMega8 Code
// ATMega8 Code
#define DATAOUT 11 //MOSI
#define DATAIN  12 //MISO
#define SPICLOCK  13 //sck
#define RTC_CHIPSELECT 7 // chip select (ss/ce) for RTC, active high
#define LED 10

byte clr;
char spi_transfer(volatile char data)
{
  /*
  Writing to the SPDR register begins an SPI transaction
   */
  SPDR = data;
  /*
  Loop right here until the transaction is complete. the SPIF bit is
   the SPI Interrupt Flag. When interrupts are enabled, and the
   SPIE bit is set enabling SPI interrupts, this bit will set when
   the transaction is finished.
   */
  while (!(SPSR & (1<<spif)))
  {
  };
  // received data appears in the SPDR register
  return SPDR;
}

void setup()
{
  char in_byte;
  clr = 0;
  in_byte = clr;
  Serial.begin(9600);
  // set direction of pins
  pinMode(LED, OUTPUT);
  pinMode(DATAOUT, OUTPUT);
  pinMode(DATAIN, INPUT);
  pinMode(SPICLOCK,OUTPUT);
  pinMode(RTC_CHIPSELECT,OUTPUT);
  digitalWrite(RTC_CHIPSELECT,LOW); //disable RTC


  // set up the RTC by enabling the oscillator, disabling the write protect in the control register,
  // enabling AIE0 and AIE1 and the 1HZ Output
  // 0x8F to 00000111 = 0x03
  // EOSC Active Low
  // WP Active High, so turn it off
  write_rtc_register(0x8F, 0x01|0x02|0x04);

  // little sanity checks
  in_byte = read_rtc_register(0x0F);
  Serial.print("CTRL REG [");
  Serial.print(in_byte, HEX);
  Serial.println("]");
  delay(10);

  in_byte = read_rtc_register(0x10);
  Serial.print("STATUS REG [");
  Serial.print(in_byte, BIN);
  Serial.println("]");

  // set up both alarms at 00 seconds?
  write_rtc_register(0x87,0x00);
  // mask all the other registers
  write_rtc_register(0x88,0x80);
  write_rtc_register(0x89,0x80);
  write_rtc_register(0x8A,0x80);

  write_rtc_register(0x8B,0x00);
  write_rtc_register(0x8C,0x80);
  write_rtc_register(0x8D,0x80);
  write_rtc_register(0x8E,0x80);

  in_byte = read_rtc_register(0x06);
  Serial.print("YEAR [");
  Serial.print(in_byte, HEX);
  Serial.println("]");

  in_byte = read_rtc_register(0x05);
  Serial.print("MONTH [");
  Serial.print(in_byte, HEX);
  Serial.println("]");

  // enable INT0, PORTD Bit 2 on the Atmega 8
  // we'll attach the 1HZ signal from the 1306, pin 7
  // or we can attach the active low interrupt output (pin 5 on the DS1306 DIP package)
  // to digital pin 3 on the Arduino (INT1 on the ATmega8)
  // I just picked INT1 arbitrarily - I think you could use any of the interrupts, so long as it
  // wasn't already being used by some other part of the Arduino.
  // cf. http://www.arduino.cc/en/Hacking/PinMapping?from=Main.PinMapping
  Serial.println(GICR, HEX);
  // enagle INT1 in the global interrupt control register
  GICR = (1<<int1);
  Serial.println(GICR, HEX);

  //Set the Interrupt Sense Control 1 Bit 1 and Bit 0
  //in the MCU control register
  //So that a falling edge of INT1 generates an interrupt request
  MCUCR = (1<<isc01);
  MCUCR = (0<<isc00);

  digitalWrite(LED, HIGH);

}

// can't really share variables unless they're declared
// "volatile", otherwise, they'll be set here, then popped
// back to their values before the interrupt handler was called
ISR(INT1_vect) {
  //signal that we have an interrupt
  //turn on the LED
  byte a;

  digitalWrite(LED, HIGH);
  // writing or reading from the DS1306 registers resets the alarm
  // so as to cause an alarm every 15 seconds.
  a = read_rtc_register(0x07);
  if(a == 0x00) {
    write_rtc_register(0x87,0x15);
  }
  if(a == 0x15) write_rtc_register(0x87,0x30);
  if(a == 0x30) write_rtc_register(0x87,0x45);
  if(a == 0x45) write_rtc_register(0x87,0x00);

  // every minute Ñ set bit 7, the mask bit, to 1
  write_rtc_register(0x88,0x80);
  // every hour
  write_rtc_register(0x89,0x80);
  // every day of the week
  write_rtc_register(0x8A,0x80);

}

void write_rtc_register(char register_name, byte data) {
  write_register(register_name, data, RTC_CHIPSELECT, HIGH, true, true);
}

char read_rtc_register(char register_name) {
  return read_register(register_name, RTC_CHIPSELECT, HIGH, false, true);
}

// reads a register
char read_register(char register_name, byte cs_pin, byte cs_active_level, boolean read_high, boolean cpha_trailing)
{
  char in_byte;
  if(cpha_trailing) {
    SPCR = (1<<spe)|(1<<mstr)|(1<<cpha)|(0<<spr1)|(0<<spr0);
  }
  else {
    SPCR = (1<<spe)|(1<<mstr)|(0<<cpha)|(0<<spr1)|(0<<spr0);
  }
  clr = SPCR;
  clr = SPDR;
  if(read_high) {
    // need to set bit 7 to indicate a read for the slave device
    register_name |= 128;
  }
  else {
    // if read low, means A7 bit should be cleared when reading for the slave device
    register_name &= 127;
  }
  // SS is active low
  digitalWrite(cs_pin, cs_active_level);
  // send the address of the register we want to read first
  spi_transfer(register_name);
  // send nothing, but here's when the device sends back the register's value as an 8 bit byte
  in_byte = spi_transfer(0);
  // deselect the device..
  if(cs_active_level == HIGH) {
    digitalWrite(cs_pin, LOW);
  }
  else {
    digitalWrite(cs_pin, HIGH);
  }
  return in_byte;
}


// write to a register
// write_high if true indicates set A7 bit to 1 during a write
void write_register(char register_name, byte data, byte cs_pin, byte cs_active_level, boolean write_high, boolean cpha_trailing)
{
  if(cpha_trailing) {
    SPCR = (1<<spe)|(1<<mstr)|(1<<cpha)|(0<<spr1)|(0<<spr0);
  }
  else {
    SPCR = (1<<spe)|(1<<mstr)|(0<<cpha)|(0<<spr1)|(0<<spr0);
  }
  clr=SPCR;
  clr=SPDR;
  // char in_byte;
  if(write_high) {
    // set A7 bit to 1 during a write for this device
    register_name |= 128;
  }
  else {
    // clear bit 7 to indicate we're doing a write for this device
    register_name &= 127;
  }
  // SS is active low
  digitalWrite(cs_pin, cs_active_level);
  // send the address of the register we want to write
  spi_transfer(register_name);
  // send the data we're writing
  spi_transfer(data);
  if(cs_active_level == HIGH) {
    digitalWrite(cs_pin, LOW);
  }
  else {
    digitalWrite(cs_pin, HIGH);
  }
  //return in_byte;
}

void loop()
{
  byte in_byte;
  // keep track of what our seconds alarm register is..
  // we use this in the ISR to make sure we alarm every 15 seconds
  in_byte = read_rtc_register(0x07);
  Serial.print("sec alarm is ");
  Serial.print(in_byte, HEX);

  in_byte = read_rtc_register(0x00);
  Serial.print(" SECS=");
  Serial.print(in_byte, HEX);

  in_byte = read_rtc_register(0x01);
  Serial.print(" MINS=");
  Serial.print(in_byte, HEX);

  in_byte = read_rtc_register(0x02);
  Serial.print(" HRS=");
  Serial.println(in_byte, HEX);
  digitalWrite(LED, LOW);
  delay(500);
}

Addendum: The example above was written in 2006 and was developed before Arduino started using the ATMega168 — it was built for the ATMega8. Between the two chips were a number of register changes and renames. They were moved around a bit in the hardware and so forth. There are often migration documents on the Atmel website or in the AVR Freaks site. (I ran into a similar problem migrating from an ATMega32 to an ATMega324, which you can read about here, fyi. That community is great for help, as are the specification sheets which you can compare and search for register names to help muddle through these simple, but annoying sorts of issues. Fortunately, it shouldn’t happen that often with the Arduino environment and its careful attendants.)

Here’s updated code for modern Arduino’s using the ATMega168. Double check your pins, too. Make sure you have the right Arduino pin for INT1, MOSI, MISO, SCK, the LED and the RTC_CHIPSELECT


// ATMega168 Code
// ATMega168 Code
#include
#include

#define DATAOUT 11 //MOSI
#define DATAIN  12 //MISO
#define SPICLOCK  13 //sck
#define RTC_CHIPSELECT 7 // chip select (ss/ce) for RTC, active high
#define LED 10

byte clr;
char spi_transfer(volatile char data)
{
  /*
  Writing to the SPDR register begins an SPI transaction
   */
  SPDR = data;
  /*
  Loop right here until the transaction is complete. the SPIF bit is
   the SPI Interrupt Flag. When interrupts are enabled, and the
   SPIE bit is set enabling SPI interrupts, this bit will set when
   the transaction is finished.
   */
  while (!(SPSR & (1<<spif)))
  {
  };
  // received data appears in the SPDR register
  return SPDR;
}

void setup()
{
  char in_byte;
  clr = 0;
  in_byte = clr;
  Serial.begin(9600);
  // set direction of pins
  pinMode(LED, OUTPUT);
  pinMode(DATAOUT, OUTPUT);
  pinMode(DATAIN, INPUT);
  pinMode(SPICLOCK,OUTPUT);
  pinMode(RTC_CHIPSELECT,OUTPUT);
  digitalWrite(RTC_CHIPSELECT,LOW); //disable RTC


  // set up the RTC by enabling the oscillator, disabling the write protect in the control register,
  // enabling AIE0 and AIE1 and the 1HZ Output
  // 0x8F to 00000111 = 0x03
  // EOSC Active Low
  // WP Active High, so turn it off
  write_rtc_register(0x8F, 0x01|0x02|0x04);

  // little sanity checks
  in_byte = read_rtc_register(0x0F);
  Serial.print("CTRL REG [");
  Serial.print(in_byte, HEX);
  Serial.println("]");
  delay(10);

  in_byte = read_rtc_register(0x10);
  Serial.print("STATUS REG [");
  Serial.print(in_byte, BIN);
  Serial.println("]");

  // set up both alarms at 00 seconds?
  write_rtc_register(0x87,0x00);
  // mask all the other registers
  write_rtc_register(0x88,0x80);
  write_rtc_register(0x89,0x80);
  write_rtc_register(0x8A,0x80);

  write_rtc_register(0x8B,0x00);
  write_rtc_register(0x8C,0x80);
  write_rtc_register(0x8D,0x80);
  write_rtc_register(0x8E,0x80);

  in_byte = read_rtc_register(0x06);
  Serial.print("YEAR [");
  Serial.print(in_byte, HEX);
  Serial.println("]");

  in_byte = read_rtc_register(0x05);
  Serial.print("MONTH [");
  Serial.print(in_byte, HEX);
  Serial.println("]");

  // enable INT0, PORTD Bit 2 on the Atmega 8
  // we'll attach the 1HZ signal from the 1306, pin 7
  // or we can attach the active low interrupt output (pin 5 on the DS1306 DIP package)
  // to digital pin 3 on the Arduino (INT1 on the ATmega8)
  // I just picked INT1 arbitrarily — I think you could use any of the interrupts, so long as it
  // wasn't already being used by some other part of the Arduino.
  // cf. http://www.arduino.cc/en/Hacking/PinMapping?from=Main.PinMapping
  // enable INT1 in the global interrupt control register

  // Atmega8 - uncomment these two lines
  // Atmega168 - comment these two lines
  //GICR = (1<<int1);
 //Serial.println(GICR, HEX);

  // Atmega168 - uncomment these four lines
  // Atmega8 - comment these four lines
  EIMSK = (1<<int1); // activate the external interrupt INT1 mask
  EICRA = (0<<isc11|1<<isc10); // set the interrupt sensing so that the falling edge of INT1 generates an interrupt request.
  SREG = (1<<7); // put a 1 in the 7th bit of the microcontroller's status register to globally turn on interrupts
  Serial.println(EIMSK, HEX); // print out the EIMSK register just so we can see for ourselves..

  digitalWrite(LED, HIGH);
  delay(1000);

}

// can't really share variables unless they're declared
// "volatile", otherwise, they'll be set here, then popped
// back to their values before the interrupt handler was called
ISR(INT1_vect) {
  //signal that we have an interrupt
  //turn on the LED
  byte a;

  digitalWrite(LED, HIGH);
  // writing or reading from the DS1306 registers resets the alarm
  // so as to cause an alarm every 15 seconds.
  a = read_rtc_register(0x07);
  if(a == 0x00) {
    write_rtc_register(0x87,0x15);
  }
  if(a == 0x15) write_rtc_register(0x87,0x30);
  if(a == 0x30) write_rtc_register(0x87,0x45);
  if(a == 0x45) write_rtc_register(0x87,0x00);

  // every minute — set bit 7, the mask bit, to 1
  write_rtc_register(0x88,0x80);
  // every hour
  write_rtc_register(0x89,0x80);
  // every day of the week
  write_rtc_register(0x8A,0x80);

}

void write_rtc_register(char register_name, byte data) {
  write_register(register_name, data, RTC_CHIPSELECT, HIGH, true, true);
}

char read_rtc_register(char register_name) {
  return read_register(register_name, RTC_CHIPSELECT, HIGH, false, true);
}

// reads a register
char read_register(char register_name, byte cs_pin, byte cs_active_level, boolean read_high, boolean cpha_trailing)
{
  char in_byte;
  if(cpha_trailing) {
    SPCR = (1<<spe)|(1<<mstr)|(1<<cpha)|(0<<spr1)|(0<<spr0);
  }
  else {
    SPCR = (1<<spe)|(1<<mstr)|(0<<cpha)|(0<<spr1)|(0<<spr0);
  }
  clr = SPCR;
  clr = SPDR;
  if(read_high) {
    // need to set bit 7 to indicate a read for the slave device
    register_name |= 128;
  }
  else {
    // if read low, means A7 bit should be cleared when reading for the slave device
    register_name &= 127;
  }
  // SS is active low
  digitalWrite(cs_pin, cs_active_level);
  // send the address of the register we want to read first
  spi_transfer(register_name);
  // send nothing, but here's when the device sends back the register's value as an 8 bit byte
  in_byte = spi_transfer(0);
  // deselect the device..
  if(cs_active_level == HIGH) {
    digitalWrite(cs_pin, LOW);
  }
  else {
    digitalWrite(cs_pin, HIGH);
  }
  return in_byte;
}


// write to a register
// write_high if true indicates set A7 bit to 1 during a write
void write_register(char register_name, byte data, byte cs_pin, byte cs_active_level, boolean write_high, boolean cpha_trailing)
{
  if(cpha_trailing) {
    SPCR = (1<<spe)|(1<<mstr)|(1<<cpha)|(0<<spr1)|(0<<spr0);
  }
  else {
    SPCR = (1<<spe)|(1<<mstr)|(0<<cpha)|(0<<spr1)|(0<<spr0);
  }
  clr=SPCR;
  clr=SPDR;
  // char in_byte;
  if(write_high) {
    // set A7 bit to 1 during a write for this device
    register_name |= 128;
  }
  else {
    // clear bit 7 to indicate we're doing a write for this device
    register_name &= 127;
  }
  // SS is active low
  digitalWrite(cs_pin, cs_active_level);
  // send the address of the register we want to write
  spi_transfer(register_name);
  // send the data we're writing
  spi_transfer(data);
  if(cs_active_level == HIGH) {
    digitalWrite(cs_pin, LOW);
  }
  else {
    digitalWrite(cs_pin, HIGH);
  }
  //return in_byte;
}

void loop()
{
  byte in_byte;
  // keep track of what our seconds alarm register is..
  // we use this in the ISR to make sure we alarm every 15 seconds
  in_byte = read_rtc_register(0x07);
  Serial.print("sec alarm is ");
  Serial.print(in_byte, HEX);

  in_byte = read_rtc_register(0x00);
  Serial.print(" SECS=");
  Serial.print(in_byte, HEX);

  in_byte = read_rtc_register(0x01);
  Serial.print(" MINS=");
  Serial.print(in_byte, HEX);

  in_byte = read_rtc_register(0x02);
  Serial.print(" HRS=");
  Serial.println(in_byte, HEX);
  digitalWrite(LED, LOW);
  delay(500);
}

References
An Application Note on the Maxim/Dallas Real-Time Clocks.
DS1306 Overview
Decent SPI Info Page
A project page I put up on interfacing the LIS3LV02DQ using SPI to an Arduino/Atmega8

Technorati Tags: ,

29 thoughts on “Arduino and the Maxim DS1306 Real Time Clock”

  1. Hey nice work. I bought an Arduino recently, and have been working with some SPI devices, so your page has been helpful.

    I have just two quick questions; out of interest, did you end up going with the capacitor or the battery, and how long can it go without the backup power?

    And also; how do you get your code coloured so nicely for the web?

    Thanks for your time!

    Sebastian Tomczak
    Adelaide, Australia

  2. Thanks — so, I have not tried using a super capacitor or such for battery back-up. I’m using small coin cell (12mm) batteries to hold the clock data. I’ve also started using the DS32C35 real-time clock for projects because it does not require an external crystal and has 8k bytes of onboard FRAM, which is useful for data collection applications.

    If you hook up a small cell to the battery input on these clocks it’ll pretty much run for ages — like, years. Of course, you probably won’t be able to actually read the data out normally. For that, you’ll need to have the clock properly powered.

    And I’m using the code2html to convert source code into slightly syntax highlighted html.

  3. Hi,

    Thanks for the entry. When looking for 32.768 kHz crystal, it shows lots of specs to choose from, especially the “load capacitance” and “frequency tolerance”. Are they critical for the clock chip.

    thanks,
    kursat.

  4. Depending on the load capacitance of the crystal, you’ll need to pick a proper value for the load capacitors you place in parallel with the crystal. I think I used about 22pF — I don’t recall the load capacitance of this particular crystal. You’ll probably do okay with load capacitors values in that area, maybe up to 33pF.

    The frequency tolerance is basically the number of pulses your crystal will loose/gain over time. I wouldn’t worry about it too much unless you’re trying to navigate to the moon or something. Typically, you’ll see values like +/-25ppm, which I think means that the frequency will vary from the specified value by +/-25×10^-6 Herz, which isn’t a whole lot to get concerned about, imho.

  5. BTW, have you tried this (or any other SPI device) with an Arduino Diecimila? Does it work? I’m curious if the LED on pin 13 causes any problems (by interfering with the SPI clock line).

  6. Hey David — I haven’t tried with the Diecimila — don’t have one yet. But, I’m pretty sure that LED on pin 13 has caused some problems sometimes with SPI devices.

  7. Hello,

    Nice tutorial. Thanks 🙂
    If I try to compile the code in Arduino I get this message:

    error: stray ” in program In function ‘void setup()’:
    In function ‘void __vector_2()’:
    In function ‘void loop()’:
    At global scope:

    But there isn’t a “” in the code….
    Can you tell me what’s wrong here?
    Thanks in advance and keep up the GOOD projects!!!!

  8. Atmoz,

    Yeah — I’m not sure. You’ll have to look for some stray syntax, which may actually appear in the code that Arduino generates rather than your source code. This is where you’ll have to help yourself a bit and carefully edit any stray whitespace in the code that might be hiding something, or look closely for that errant “”. Check the source code that Arduino generates first of all. If you look in the Arduino project folder for your application, there’s a folder called “applet” — in there are a bunch of source files with a .cc extension. Look in those in the functions indicated by the compilation error. Likely there’s something in your source code that caused this problem to appear in one of the .cc files. But, finding it in the .cc file will give you a clue as to what may have happened or what may be wrong in your Arduino .pdf file.

    Good luck!

  9. @ Diecimila users like David:

    Yes, it works with a Diecimila. However, for newbies like me I would like to point out that the schematic does not show the right wiring for the code example. It shows 3-wire communication instead of SPI as coded. So actually for that code to work pin 12 DATAIN from Arduino must connect to pin 13 SDO on the DS1306 and then pin 9 Sermode must be on Vcc for SPI (Gnd means 3-wire… took a while to figure that out, but the datasheet helped :-). BTW: you do not need to set the interrupt GICR register etc, because you can simply use the attachInterrupt() function to point to the interrupt routine.

  10. hallo..
    nice page! I’d like to ask you which kind of pull-up resistors are used in the progect? 10k will be fine?
    thanks a lot
    Ale

  11. 10k should be fine — I’ve used everything from 1k on up to whatever’s lying around. You can also enable the internal pull-ups of whatever Atmega microcontroller you’re using. Just check the specifications document for the correct register bits to set to enable them.

  12. hallo again!
    Due to my deep ignorance and inexperience, I really can’t understand how to wire it to an arduino diecimila..
    Should I connect only these wires?
    arduino pin12 to ds1306 pin13
    arduino pin9 to ds1306 pin16
    arduino gnd to ds1306 grd

    If so, how can I change then the code?
    #define DATAOUT 11
    #define DATAIN 12
    #define SPICLOCK 13
    #define RTC_CHIPSELECT 7

    thanks a lot for your patience
    Ale

  13. Ale,

    Basically you want the MOSI (Master Out, Slave In) to go to the SDO (Serial Data Out) of the DS1306, and the MISO (Master In, Slave Out) to go to the SDI (Serial Data In). You also want the RTC_CHIPSELECT to be connected to the CE line of the DS1306, and SPICLOCK on the Arduino to go to the SCLK of the DS1306. The Interrupt line INT_0 to INT0 (Pin 2) on the Atmega168 just for grins.

    Assuming you’re using the DIP version of the DS1306:

    SDO is Pin 13
    SDI is Pin 12
    SCLK is Pin 11
    CE is Pin 10
    INT_0 is Pin 5

    And assuming you’re using an Arduino with an Atmega168 (the newer ones, including the Diecimila) Then you want to connect:

    Arduino Pin 12 (MISO) to DS1306 Pin 13 (SDO)
    Arduino Pin 11 (MOSI) to DS1306 Pin 12 (SDI)
    Arduino Pin 13 (SCK) to DS1306 Pin 11 (SCLK)
    Arduino Pin 7 to DS1306 Pin 10 (CE)
    Arduino Pin 2 (INT0) to DS1306 Pin 5 (INT_0)
    Pin 7 was somewhat arbitrarily chosen — he can be any pin that can do a digital out..I just chose 7.

    The other pins MISO/MOSI/SCK are hardwired to the Atmega168 to the SPI (Three Wire) hardware within the microcontroller.

    Make sure the “#define”s in the beginning of the Arduino Sketch are consistent with the pin numbers on the Arduino that you’ve wired up and that should do it.

    Julian

  14. hi again..
    I’m not sure it’s working:
    here’s what you read in the serial output:

    CTRL REG [FFFFFFFF]
    STATUS REG [0]
    YEAR [0]
    MONTH [0]
    sec alarm is 0 SECS=8 MINS=0 HRS=4
    sec alarm is 0 SECS=0 MINS=0 HRS=4
    sec alarm is FF SECS=0 MINS=0 HRS=4
    sec alarm is FF SECS=0 MINS=80 HRS=4
    sec alarm is FF SECS=0 MINS=FF HRS=4
    sec alarm is 0 SECS=0 MINS=0 HRS=4

    The first reading of ‘secs’ varies every time, it’s not always 8..

    And if I try writing in some register (I’ve tried years,months,hours) nothing happens.

  15. It’s definitely not working, that’s for sure!

    You’re going to have to dig in there — it’s not hard, just requires some patience.

    Double and triple check the connections. The protocol is very simple between the Arduino and the DS1306, and it’s important that you understand it conceptually. You could even communicate to the DS1306 manually if you had to. Read up on the SPI/3-Wire Interface protocol — there are lots of descriptions of it and it’s conceptually simple, so it shouldn’t be a problem.

    Many times I’ll hook up a project like this and it won’t work. It can be frustrating — it should work, I understand how it should work. But, part of the process here is debugging a bit. Chances are — given my experiences — that it’s a simple error — a wire connected to the wrong thing, often enough.

    Some starting places — double check your Chip Enable connection. If Chip Enable (active LOW on the DS1306) is not correctly “activated” the DS1306 ignores all communication. Make sure MOSI is connected to SDI (Master output goes to Slave input), and MISO is connected to SDO (Master input goes to Slave output). Try slowing down the serial communication — this shouldn’t be a problem, but it may aid debugging. You could even write a small bit of code to communicate to the DS1306 “manually” — that is, bit by bit in the firmware, rather than using the Atmega’s internal hardware for serial communication.

    If you can get access to an oscilloscope or a logic analyzer, you can watch the signals flip — it can help to have a visualization of the communication to get a better grasp as to what’s happening.

    Finally, note that I did this on an Atmega8 and you’re doing it on an Atmega168. There should not be any differences to the code. The registers I believe are the same and have the same functionality. But, it’s always possible that between the Atmega8 and Atmega168 some registers were renamed or changed — I strongly doubt it, but a close look at the specifications sheet would be needed to double-check.

    Julian

  16. Re crystal characteristics: see the ds1306 (or in my case ds1305) specs. There’s also an appnote pointing out some of the concerns here … basically, the oscillator frequency will be wrong if you use the wrong capacitance. Dallas/Maxim says to use 6pf crystals, not 22pf/33pf …

    Also, both these RTCs do something unusual: they use active-HIGH “chip enable” not active-LOW “chip select”. Even if you’re used to SPI, so you reflexively get the CPOL and CPHA bits right the first time, that signal inversion can be a surprise (and make you lose much time).

    Another appnote, referenced on the Maxim page, can point you to a webpage with a calculator saying how long the supercap will keep things going. I used a 0.33 F supercap, and figure it’ll keep things going around a month. It got a basic charge in under 10 minutes.

  17. Thank you for sharing your project, looks like it saved allot of time for many folks.
    I tried this too but can’t get the code to compile.
    Trying to program Decimilia and this is what i’m getting…

    In function ‘void setup()’:
    error: ‘GICR’ was not declared in this scope

    I would greatly appreciate any advice.

    Thank you,
    Viktor

    1. It was developed back before the modern Arduino with the ATMega168. It was written for the ATMega8 and the registers have changed around a bit, names and so forth. There should be a migration document somewhere, but I couldn’t find one. A quick look in the ATMega8 data sheet shows that GICR is the General Interrupt Control Register. For the same functionality on the ATMega168, the corresponding register is called EICRA — external interrupt control register A. Atmel’s just moved things around a bit in there, so — sometimes there are migration documents that just spell it out; sometimes you have to hunt around a little. Spec sheets are your best friends! They’ll help you better understand what’s going on which will make it easier to figure new, weird things out in the future. Check out what the ATMega8 code does with the GICR and how the EICRA register on the ATMega168 gets you the same sort of functionality.

      I’ve updated the blog post to include an additional bit of code that should compile for modern Arduino’s.

      Julian

  18. Thanks great for this code! I’ve spent a few days struggling the SPI at Atmega16. The DS1306 never wanted to show me the correct time! Now I’m happy, the RTC works as expected even charging the supercap!

    Thanks a lot guys! You did a real useful job!

    1. It’s pretty easy. Look at the register map table above. A(M)/P(M) are indicated by the register at address 02H (0x02), bit 5. This is also how you would set AM/PM. Alternatively, you could stick to 24 hour time and infer based on the same bit.

      You can check whether a bit is set by using some bit masking and a conditional. You also need to be sure you put the chip in the right mode — 12-hour or 24-hour. As the spec sheet describes, bit 6 of the hours register (0x02) is defined as the 12- or 24-hour mode select bit.

      int reg = read_rtc_register(0x02);
      int mask = 1 << 5;
      if (mask & reg) {
      // it is PM
      } else {
      // it is AM
      }

    1. Check out the manual — in the table of RTC Registers, Bit 5 of the register at address 0x02 (Read) and 0x82 (Write) allows you to set AM/PM if you set Bit 6 to use a 12 hour clock. Otherwise, it’s irrelevant if you’re using a 24 hour clock.

      1. well i tried putting
        write_rtc_register(0x82, 1<<6|1<<5 );
        but then the hours say 60. I really don't understand what i need to do.

        1. That might be weird, or it might not. Hard to tell from the information you’ve got. You do know that the clock and calendar are meant to be read as binary coded data and not proper base 2 numbers? If by “60” you mean 60d, that could be 12 o’clock, but I’m not 100% convinced. 60d = 0b111100 which is only 6 bits of that register. Assuming that that’s what you have, then you might have 0b1100 -> 12 in the hours field. Still seems squirrely. Keep plugging away.

  19. Exelente amigo me fue de muchisisma ayuda le hice algunas mejoras, para tener acceso a registros completos y mostrar la información más detalla; sin embargo, no estoy trabajando con Arduino, este proyecto lo hago con un ATmega16 que tiene mucha similitud al que utilizas con arduino (ATmega8).

    Gracias por la información,

Comments are closed.