I2C - Real Time Clock (RTC) DS1307 - Write/Read

The following article walks you through a hands on exercise with a microcontroller and a real time clock IC to better understand and appreciate how the I2C Serial Communication Protocol is implemented. 

The microcontroller to be used in this exercise is the Tiva TM4C123G Launchpad from Texas Instrument. You will write or read  the time set up in the real time clock IC DS1307. 

If you want to go straight to where the I2C Serial Protocol sequence is implemented feel free to jump to "I2C1_burstWrite() function" section. 

What you will need? 

   Discloser: “#ad”.

1. The microcontroller: Tiva TM4C123G Launchpad.

 2. The serial Real Time Clock (RTC) breakout board kit: DS1307

        Note: CR1220 12mm Diameter - 3V Lithium Coin Cell Battery is needed.  

 3. Female-to-Male Jumper Wire Dupont Cable x4.

 4. Breadboard

 5. Compiler: IAR Embedded Workbench

    Note: For compiler setup refer to this article: Setting Up your IAR Development Environment Tool 

    Make sure your project folder, where the main.c file would be located, contains:

  • This file:

    • startup_LM4F.s

  • An inc folder with the following two header files: 

    • system_TM4C123GH6PM.h

    • TM4C123GH6PM.h

  • And a src folder with the following source code file:

    • system_TM4C123GH6PM.c


How to connect it? 

WARNING: Below diagram shows TM4C123GXL as the microcontroller. Please note, this is as a reference. For this exercise we are using TM4C123GH6PM as the microcontroller.

To communicate with the RTC DS1307 using the TM4C123GH6PM make sure:

GPIO Port A pin 6 is connected to the Serial Clock (SCL) pin in the RTC DS1307

  • PA6 → (SCL)

GPIO Port A pin 7 is connected to the Serial Data (SDA) pin in the RTC DS1307. 

  • PA7 → (SDA)

GND is connected to the GND pin in the RTC DS1307

  • GND → (GND)

VBUS is connected to the 5V pin in the RTC DS1307. 

  • VBUS → (5V)





CODE


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/*
 *  I2C to DS1307 multi-byte burst write/read 
 *  
 *  This program communicate with the DS1307 Real-time Clock via I2C. 
 *  The seconds, minutes, and hours are written using burst write. 
 *  Set the time to 11:00:55
 *  
 *  The current time will be read as well using burst read. 
 *
 *  DS1307 parameters:
 *  fmax = 100 kHz
 *
 *  I2C1SCL PA6
 *  I2C1SDA PA7
 *
 */

/*-------------LIBRARIES-------------*/
#include <TM4C123GH6PM.h>               //Tiva C Series TM4C123G “Header File”. 
#include <stdio.h>

/*---------------MACRO---------------*/
#define SLAVE_ADDR 0x68                 /* 0110 1000 */

/*--------FUNCTION PROTOTYPES--------*/
void I2C1_init(void);                   /* I2C Module 1 Initialization Function */
char I2C1_burstWrite(int slaveAddr, char memAddr, int byteCount, char* data); 
char I2C1_read(int slaveAddr, char memAddr, int byteCount, char* data);

int main()
{
  char timeToSet[3] = {0x55, 0x00, 0x11};
  char timeReadBack[2];
  
  I2C1_init();
  
  /* use burst write to write seconds, minutes, and hours */
  I2C1_burstWrite(SLAVE_ADDR, 0, 3, &timeToSet[0]);
  
  for (;;)
  {
    /* use burst read to read time and date */
    I2C1_read(SLAVE_ADDR, 0, 3, timeReadBack);
    printf("Time: %x:%x:%x\n", timeReadBack[2], timeReadBack[1], timeReadBack[0]);	  
  }

  
}
 
 void I2C1_init(void)
 {
  /* ---------------------------- PART I ---------------------------- */
  /* --------- I2C & GPIOF Initialization and Configuration --------- */   
   SYSCTL->RCGCGPIO |= 0x01;           /* 1A. Enable clock to GPIOA for PA6(SCL), PA7(SDA) */
   SYSCTL->RCGCI2C |= 0x02;            /* 1B. Enable clock to I2C1 */
        
    /* Port 7, 6, for I2C1 */
    GPIOA->AFSEL |= 0xC0;               /* 3A. Enable Alternative Function: PA7, PA6 for I2C1 */
    GPIOA->PCTL &= ~0xFF000000;         /* 3B. assign pins to I2C */
    GPIOA->PCTL |= 0x33000000;          /* 3B. assign pins to I2C */
    GPIOA->ODR |= 0x80;                 /* 5. PA7 as Open Drain */
    GPIOA->DEN |= 0xC0;                 /* 6. Make PA7, PA6 as digital pins */
        
    I2C1->MCR = 0x10;                   /* 7. master mode */
    I2C1->MTPR = 7;                     /* 8. 100 kHz @ 16 Mhz */ 
  /* ------------------------ PART I (END) -------------------------- */    
 }
  
/* Wait until I2C master is not busy and return error code */
/* If there is no error, return 0 */
static int I2C_wait_till_done(void)
{
  while(I2C1->MCS & 1);                 /* wait until I2C master is not busy */
  return I2C1->MCS & 0xE;               /* return I2C error code */
}

/* Use burst write to write multiple bytes to consecutive locations */
/* burst write: S-(saddr+w)-ACK-maddr-ACK-data-ACK-data-ACK-...-data-ACK-P */
char I2C1_burstWrite(int slaveAddr, char memAddr, int byteCount, char* data)
{
  /* --------------------------- PART II -------------------------- */  
  char error;
  
  if (byteCount <=0)
    return -1;                          /* no write was performed */
  
  /* send slave address and starting address */
  I2C1->MSA = slaveAddr << 1;           /* 1. Store slave addr in I2CMSA  */
  I2C1->MDR = memAddr;                  /* 2. Store memory address in I2CMDR  */
  I2C1->MCS = 3;                        /* 3. Initiate transmission: S-(saddr+w)-ACK-maddr-ACK */
  
  error = I2C_wait_till_done();         /* 4. wait until write is complete/check for errors */
  if (error) return error;
  
  /* send data one byte at a time */
  while (byteCount > 1)                 /* 5. Store data in I2CMDR  */
  {
    I2C1->MDR = *data++;                /* &STEP2. Store memory address in I2CMDR  */
    I2C1->MCS = 1;                      /* &STEP3. Initiate transmission: S-(saddr+w)-ACK-maddr-ACK */
    error = I2C_wait_till_done();       /* &STEP4. wait until write is complete/check for errors */
    if (error) return error;
    byteCount--;
  }
  
                                        /* 6. send last byte and STOP */ 
  I2C1->MDR = *data++;                  /* &STEP2. Store memory address in I2CMDR  */
  I2C1->MCS =5;                         /* &STEP3. Initiate transmission: S-(saddr+w)-ACK-maddr-ACK */
  error = I2C_wait_till_done();         /* &STEP4. wait until write is complete/check for errors */
  
  while(I2C1->MCS & 0x40);              /* 8. wait until bus is not busy */
  
  if (error) return error;
  
  return 0;                             /* no error */
  /* ------------------------ PART II (END) ------------------------ */  
}


/* Read memory */
/* read: S-(saddr+w)-ACK-maddr-ACK-R-(saddr+r)-ACK-data-ACK-data-ACK-...-data-NACK-P */
char I2C1_read(int slaveAddr, char memAddr, int byteCount, char* data)
{
  /* --------------------------- PART III -------------------------- */  
  char error;
  
  if (byteCount <=0)
    return -1;                          /* no write was performed */
  
  /* send slave address and starting address */
  I2C1->MSA = slaveAddr << 1;           /* 1. Store slave addr in I2CMSA  */
  I2C1->MDR = memAddr;                  /* 2. Store memory address in I2CMDR  */
  I2C1->MCS = 3;                        /* 3. Initiate transmission: S-(saddr+w)-ACK-maddr-ACK */
  
  error = I2C_wait_till_done();         /* 4. wait until write is complete/check for errors */
  if (error) 
    return error;
 
  /* to change bus from write to read, send restart with slave addr */
  I2C1->MSA = (slaveAddr << 1) + 1;     /* 5. restart: -R-(saddr+r)-ACK*/
  
  if(byteCount == 1)                    /* if last byte, don't ack*/
    I2C1->MCS = 7;                      /* -data-NACK-P*/
  else                                  /* else ack */
    I2C1->MCS = 0xB;                    /* Continue transmission (START, RUN, ACK): -data-ACK- */
  
  error = I2C_wait_till_done();         /* &STEP4. wait until write is complete/check for errors */
  if (error)
    return error;
  
  *data++ = I2C1->MDR;                  /* Store the data received */
  
  if(--byteCount == 0)                  /* If single byte was read--> we are done */
  {
    while(I2C1->MCS & 0x40);            /* wait until bus is not busy */
    return 0;                           /* no error */
  }
  
  /* read the rest of the bytes */
  while (byteCount > 1)
  {
    I2C1->MCS = 9;                      /* 5. -data-ACK-  */
    error = I2C_wait_till_done();       /* 4. wait until write is complete/check for errors */
    if (error) return error;
    byteCount--;
    *data++ = I2C1->MDR;                /* store data received */
  }
  
 I2C1->MCS = 5;                         /* 6. -data-NACK-P */
  
 error = I2C_wait_till_done();          /* 7. wait until write is complete/check for errors */
  
 *data = I2C1->MDR;                     /* Store data received */
 
 while(I2C1->MCS & 0x40);               /* Wait until bus is not busy */
  
 return 0;                              /* no error */
 /* ------------------------ PART III (END) ------------------------ */  
}

How the above Code Works?

Libraries

You would need to include the TM4C123GH6PM.h header file, which includes a list of library functions, to interface with the TM4C123GH6PM Launchpad.


/*-------------LIBRARIES-------------*/

#include <TM4C123GH6PM.h>               /* Tiva C Series TM4C123G “Header File” */ 


The compiler would readed from the inc folder inside your project folder. Make sure you have setup this folder.

MACRO

In the I2C protocol, a transmission is initiated by a START condition followed by the address of the slave-receiver with the R/W bit low for write.  In this case, the address to be used to access the DS1307 RTC is 0x68 ( See page 13 of the datasheet ).


SLAVE_ADDR is now a fragment of code, sort of an abbreviation, that we have given this particular value for later use. 


/*---------------MACRO---------------*/

#define SLAVE_ADDR 0x68                 /* 0110 1000 */


FUNCTION PROTOTYPES

These are function declarations, it doesn’t contain a function body, It’s just telling the compiler that the function may be used later. Which in that case will perform a specific task.  



/*--------FUNCTION PROTOTYPES--------*/

void I2C1_init(void);                               /* I2C Module 1 Initialization Function */

char I2C1_burstWrite(int slaveAddr, char memAddr, int byteCount, char* data);

char I2C1_read(int slaveAddr, char memAddr, int byteCount, char* data);


We would go over these functions later on. 

int main()

The int main()function is the entry point of the program execution.


Here the time entries are stored in the following array: seconds, minutes, and hours. 


char timeToSet[3] = {0x55, 0x30, 0x19};


And the reading values will be stored in the timeReadBack array.


    char timeReadBack[2];


The initialization function for the Tiva TM4C123G is called: 


I2C1_init();


Because our main purpose is to understand how the I2C write protocol is executed, I will skip the I2C1_init()function explanation and go over the I2C1_burstWrite() & I2C1_read() function instead.

 I2C1_burstWrite() function

Like it was explained in this article I2C Serial Protocol, for the Master to send data to a slave the sequence begins with a START condition.  


STEP 1:

Before beginning you would first need to store the seven address bits (A6-A0), and the R/W bit low in the I2C Master Slave Address register for writing. 


Datasheet - I2C Master Slave Address (I2CMSA), offset 0x000, p.1019




#devine SLAVE_ADDR 0x68 /* 0110 1000 */

I2C1->MSA = SLAVE_ADDR << 1; /* 100 KHz @ 16MHz */


STEP 2:


Also, the memory address location where you want the data to be placed inside your DS1307 clock needs to be stored in the I2C Master Data register.


Datasheet - I2C Master Data (I2CMDR), offset 0x008, p.1025




I2C1->MDR = memAddr;


STEP 3:
Now that you have stored the seven address bits (A6-A0), plus the R/W bit low in the I2C Master Slave Address register & the memAddr in the I2C Master Data register you can begin part of the sequence by initiating the START condition & RUN condition using the Master Control/Status (MCS) register.

Datasheet - I2C Master Control/Status (I2CMCS), offset 0x004, p.1022




I2C1->MCS = 3; /* S-(saddr+w)-ACK-maddr-ACK */


STEP 4:
Wait until “write” is completed and check for errors by reading the I2CMCS register.

Datasheet - I2C Master Control/Status (I2CMCS), offset 0x004, p.1020




static int I2C_wait_till_done(void)

{

while(I2C1->MCS & 1); /* wait until I2C master is not busy */

return I2C1->MCS & 0xE; /* return I2C error code */

}


STEP 5:

Keep repeating STEP 2, 3 & 4 for the rest of the data bytes that you want to write; without reaching the last byte:


  • &STEP 2: Store the byte data in the I2CMDR register

I2C1->MDR = *data++;                /* &STEP2.. Store data in I2CMDR  */


  • &STEP 3: Initiate again the transmission, this time with a RUN condition only.

I2C1->MCS = 1;                      /* &STEP3. Initiate transmission: -data-ACK- */


  • &STEP 4: Wait until “write” is completed and check for errors by reading the I2CMCS register.

error = I2C_wait_till_done();       /*&STEP4. wait until write is complete/check for errors */

Quick Snapshot

 /* send data one byte at a time */

 while (byteCount > 1)                 /* 5. Store data in I2CMDR  */

  {

    I2C1->MDR = *data++;                /* &STEP2. Store memory address in I2CMDR  */

    I2C1->MCS = 1;                      /* &STEP3. Initiate transmission: S-(saddr+w)-ACK-maddr-ACK */

    error = I2C_wait_till_done();       /* &STEP4. wait until write is complete/check for errors */

    if (error) return error;

    byteCount--;

  }


STEP 6:


Once the while loop is over you are left with the last byte of data, repeat STEP 2, 3 (with a RUN & STOP condition) & STEP 4.


  • &STEP 2: Store the byte data in the I2CMDR register


I2C1->MDR = *data++;                /* &STEP2. Store data in I2CMDR  */


  • &STEP 3: Initiate again the transmission, this time with a RUN & STOP condition.


I2C1->MCS = 1;                      /* &STEP3. Initiate transmission: -data-ACK- */


  • &STEP 4: Wait until “write” is completed and check for errors by reading the I2CMCS register.


error = I2C_wait_till_done();       /* &STEP4. wait until write is complete/check for errors */

Quick Snapshot

 /* send data */

I2C1->MDR = *data++;                  /* &STEP2. Store memory address in I2CMDR  */

I2C1->MCS =5;                         /* &STEP3. Initiate transmission: S-(saddr+w)-ACK-maddr-ACK */

error = I2C_wait_till_done();         /* &STEP4. wait until write is complete/check for errors */

while(I2C1->MCS & 0x40);              /* STEP8. wait until bus is not busy */


error = I2C1->MCS & 0xE;

if (error) return error;


By now your have completed the I2C sequence for write:


  1. Master-transmitter: Sends a START condition.

  2. Master-transmitter: Send the I2C address of the slave-receiver with the R/W bit low for write.   

  3. Master-transmitter: Send the internal register number it wants to write to. 

  4. Master-transmitter: Sends data to slave-receiver (or bytes, it doesn't have to be just one byte). The master can continue to send data bytes to the slave and these will normally be placed in the following registers because the slave will automatically increment the internal register address after each byte. 

  5. Master-transmitter: Terminates the transfer with a STOP condition.


With the STOP condition in the Master Control/Status (MCS) register you have terminated the transfer of data. 


STEP 7:


Wait until the bus is not busy & return to the main function.


while(I2C1->MCS & 1); /* wait until bus is not busy */


At this point you have setup the DS1307 clock with the following time: --> 11:00:55


You have reached the end of the write function. To validate your work use the read function: I2C1_read().


I2C1_read() function


The read function will follow the same sequence as the write function with one difference only. After sending the internal register number it "suppose" to write to, you need to repeat another START condition, known as a restart, followed by the I2C address of the slave with the R/W bit high for read. Letting the sensor know that you are going to read from it.

  1. Master-receiver: Sends a START condition.

  2. Master-receiver: Send the I2C address of the slave-receiver with the R/W bit low for write.   

  3. Master-receiver: Send the internal register number it "suppose" to write to.

  4. Master-receiver: Send another start sequence (sometimes called a restart).

  5. Master-receiver: Sends the I2C address again - this time with the R/W bit high for read.

  6. Master-receiver: Read as many data bytes as you wish.

  7. Master-receiver: Terminates the transfer with a STOP condition.


That is implemented in line 139 of the code:


 /* to change bus from write to read, send restart with slave addr and R/W bit low*/

  I2C1->MSA = (slaveAddr << 1) + 1;     /* 5. restart: -R-(saddr+r)-ACK*/


After this it would keep reading from the slave and stored in the char timeReadBack[2] array following the same steps as before.


You should see the output time been displayed on your Terminal I/O of your IAR compiler:




Thank You for reading!!!

.

Please, leave your comment for any feedback or question.

Comments