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?
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
Reference Documents:
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.
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.
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;
Datasheet - I2C Master Control/Status (I2CMCS), offset 0x004, p.1022
I2C1->MCS = 3; /* S-(saddr+w)-ACK-maddr-ACK */
STEP 4:
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:
&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:
Master-transmitter: Sends a START condition.
Master-transmitter: Send the I2C address of the slave-receiver with the R/W bit low for write.
Master-transmitter: Send the internal register number it wants to write to.
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.
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
Master-receiver: Sends a START condition.
Master-receiver: Send the I2C address of the slave-receiver with the R/W bit low for write.
Master-receiver: Send the internal register number it "suppose" to write to.
Master-receiver: Send another start sequence (sometimes called a restart).
Master-receiver: Sends the I2C address again - this time with the R/W bit high for read.
Master-receiver: Read as many data bytes as you wish.
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