The 18F452 and the 18F4520 have a built-in I2C (Inter-Integrated Circuit) bus. I2C is a 2-wire synchronous serial bus. Some devices, such as sensors, communicate with microcontrollers on this 2-wire serial bus. Multiple devices, even different types of devices, can be attached to the same 2-wire I2C bus without causing collisions or errors. Each device contains a unique address allowing multiple devices attached to the same I2C bus. In our example, we will use a real-time clock (RTC) that communicates over I2C.
Before continuing, be sure to read through the I2C tutorial.
You will need the following:
According to the real time clock datasheet, the sensor contains 16 pins:
In order to get this to work, I assume you are using an external crystal of 10MHz to run the PIC. If not, some adjustments will need to be made which I will address in the document. Also, you must have the latest version of MPLAB installed and the C18 compiler installed.
According to the PIC18F4520 datasheet, you will notice that on the pinout, the I2C lines are located on pins 18 and 23 (CLK and DATA).
These pins happen to be located on PORT C. PORT C pins can be used for general input/output or can be configured for special uses such as I2C communication. Since I2C communication involves only 2 wires, these pins will serve as the I2C communication between the PIC and other I2C devices. The PIC will act as the “master” and all the other devices will act as the “slaves”.
First step is to “turn on” PORT C.
1: int main() 2: { 3: TRISC = 0x00; //turn on tri-state register and 4: //make all output pins 5: PORTC = 0x00; //make all output pins LOW 6: }
Now you are ready to configure the I2C bus on the PIC.
In MPLAB, it is critical that you include the “i2c.h” file in your code. Therefore, be sure to add it at the top of the main file.
1: #include <i2c.h> 2: int main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: }
You have now opened port C for use. Now, on to opening the I2C bus.
Now it’s time to open and configure the I2C port on the PIC. According to the C18 Libraries file, OpenI2C is the function to call. A few things are needed to be understood. The first thing is the difference between MASTER and SLAVE. Since the PIC will control all the devices, the PIC should be set as MASTER. Since bus speed isn’t a concern, slew control can be turned off using SLEW_OFF parameter.
1: #include <i2c.h> 2: int main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: 8: OpenI2C( MASTER, SLEW_OFF); 9: }
The second thing is to understand what the SSPADD register does inside the PIC. It contains the value which represents what baud rate (speed) communications will occur based on A) what crystal you are using and B) what speed you would like. For our example, we will assume a crystal speed of 10MHz and baud rate of 100KHz. See page 153 of 332 of the PIC datasheet for all values listed.
1: #include <i2c.h> 2: int main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: 8: OpenI2C( MASTER, SLEW_OFF); 9: SSPADD = 0x3F; 10: }
At this stage you have successfully initialized the I2C.
The DS3231 RTC is an IC that keeps the current time (hours, mins, secs), date, year, month, day, and week on a single chip. These are commonly used in watches, alarm clocks, cell phones, PDAs, etc. where a calendar and/or clock is needed. There are only 2 functions commonly used with this device: set values in memory and get values from memory. We will go through both functions using examples.
According to page 11 of 19 of the DS3231 datasheet, each memory register is listed. We will use the register addresses in the examples below.
First thing is to program the IC with the correct values for the calendar and the clocks. In our example, we will set the minutes. Later, we will set the day. Once these are set, all other registers can be set using the same procedures.
Page 16 of 19 of the DS3231 datasheet illustrates the timing and sequence of the I2C communication. We begin with the StartI2C() command.
1: #include <i2c.h> 2: void main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: 8: OpenI2C( MASTER, SLEW_OFF); 9: SSPADD = 0x3F; 10: 11: StartI2C(); // begin I2C communications 12: IdleI2C(); 13: WriteI2C(0xD0); // addresses the chip 14: IdleI2C(); 15: 16: }
Let’s stop here and understand what’s going on. First thing we do is begin the communication by using StartI2C().
WriteI2C() is our first attempt to address an IC located on the I2C bus. Since many devices can be attached to this bus, this address ‘awakens’ the correct device. The next data sequences will only be ‘looked at’ by this device on the bus.
What is the address of an I2C device? The address of any device is usually (but not always) made up of 2 parts. First part is a set of internal address bits. The second part is made up of hardwired address bits using the pins of the IC. However, the DS3231 is unique in that the entire address in internal to the chip. There are no external address pins. According to page 16 of 19 of the DS3231 datasheet, the 7-bit address is ‘1101000’.
So where’s the 8th bit? What is the 8th bit? Good question. After every address byte sent by the PIC using I2C, the 8th bit determines if the next byte is written or read by the PIC.
Therefore, in our example we are interested in accessing the MINUTES register.
1: #include <i2c.h> 2: void main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: 8: OpenI2C( MASTER, SLEW_OFF); 9: SSPADD = 0x3F; 10: 11: StartI2C(); // begin I2C communications 12: IdleI2C(); 13: WriteI2C( 0xD0 ); // addresses the chip 14: IdleI2C(); 15: WriteI2C( 0x01 ); // write register address for minutes 16: IdleI2C(); 17: 18: }
The least significant bit (8th bit) of 0xD0 is LOW. Therefore, the PIC is writing the next byte. What is the next byte? It is 0x01.
According to page 11 of 19 of the DS3231 datasheet, a table shows which registers are located in the IC. Each register has a hex value address associated with it. Notice that the MINUTES register is 01h (or 0x01). Therefore, to access the register, the PIC writes 0x01 as shown above.
Now that we have addressed the correct device and addressed the correct register, it’s time to write something into this MINUTES register.
According to page 11 of 19 of the DS3231 datasheet, a table shows all the bits of the MINUTES register. The MINUTES register is composed of 2 parts: The lower nibble (bit 0 through bit 3) and the upper nibble (bit 4 through bit 6). Both nibbles are used together to represent double-digit numbers. For example, the number “12” is composed of a ‘1’ and a ‘2’. Each digit is stored in a separate nibble. The ‘1’ is stored as a BCD number in the upper nibble while the ‘2’ is stored as a BCD number in the lower nibble.
For example, to store the number “12”, the MINUTES register should be set to ‘001 0010’ in binary. The ‘001’ is BCD for ‘1’ and 0010 is BCD for ‘2’. Since there are no double-digit minutes higher than 59, only 7 bits are needed to represent any double-digit from 00 to 59.
1: #include <i2c.h> 2: void main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: 8: OpenI2C( MASTER, SLEW_OFF); 9: SSPADD = 0x3F; 10: 11: StartI2C(); // begin I2C communications 12: IdleI2C(); 13: WriteI2C( 0xD0 ); // addresses the chip 14: IdleI2C(); 15: WriteI2C( 0x01 ); // access register address for minutes 16: IdleI2C(); 17: WriteI2C( 0b00010010 ); // write value into minutes register 18: IdleI2C(); 19: StopI2C(); // stop condition I2C on bus 20: 21: }
We have written the binary number 0010010 into the MINUTES register. Now, the RTC will keep track of the minutes and after 59min:59sec, the RTC will increment the HOURS register and roll-over to 00min:00secs.
Let’s continue on with the day of the week.
Since nothing changes with the I2C procedure and the IC address is the same, we can use the previous code as a guide in order to set the day. Below we use the StartI2C() command and address the chip using WriteI2C().
1: #include <i2c.h> 2: int main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: 8: OpenI2C( MASTER, SLEW_OFF); 9: SSPADD = 0x3F; 10: 11: StartI2C(); // begin I2C communications 12: IdleI2C(); 13: WriteI2C( 0xD0 ); // addresses the chip 14: IdleI2C(); 15: WriteI2C( 0x01 ); // access register address for minutes 16: IdleI2C(); 17: WriteI2C( 0b00010010 ); // write value into minutes register 18: IdleI2C(); 19: StopI2C(); // stop condition I2C on bus 20: 21: StartI2C(); // begin I2C communications 22: IdleI2C(); 23: WriteI2C( 0xD0 ); // addresses the chip 24: IdleI2C(); 25: 26: }
By looking at the table of registers in the datasheet, we can see the register for day of the week is 03h (0x03). The value of each day is represented with a binary number between 1 and 7. Each number represents a different day of the week. According to page 11 of 19, ‘1’ equals “Sunday”, ‘2’ equals “Monday”, etc. In our example we will set the day of the week to “Wednesday”. Let’s access the register for days of the week and store a ‘4’ in the register.
1: #include <i2c.h> 2: int main() 3: { 4: TRISC = 0x00; //turn on tri-state register and 5: //make all output pins 6: PORTC = 0x00; //make all output pins LOW 7: 8: OpenI2C( MASTER, SLEW_OFF); 9: SSPADD = 0x3F; 10: 11: StartI2C(); // begin I2C communications 12: IdleI2C(); 13: WriteI2C( 0xD0 ); // addresses the chip 14: IdleI2C(); 15: WriteI2C( 0x01 ); // access register address for minutes 16: IdleI2C(); 17: WriteI2C( 0b00010010 ); // write value into minutes register 18: IdleI2C(); 19: StopI2C(); // stop condition I2C on bus 20: 21: StartI2C(); // begin I2C communications 22: IdleI2C(); 23: WriteI2C( 0xD0 ); // addresses the chip 24: IdleI2C(); 25: WriteI2C( 0x03 ); // access register address for day of the week 26: IdleI2C(); 27: WriteI2C( 0x04 ); // write value into day register 28: IdleI2C(); 29: StopI2C(); // stop condition I2C on bus 30: }
Finally, we have the minutes set and the day of the week set. Repeat this procedure for any other register settings.
The second feature of the DS3231 is to get data from the RTC into PIC memory. Assuming I2C has been previously configured, all the correct files have been included, and the RTC has values stored, we can begin. In the following example, we are going to get the value for day of the week. At the top of the main function, add an object of type char to store the 8-bit value (which holds the BCD number).
3: char result;
The I2C read routine needs to be added. Therefore, we begin as usual using the StartI2C() command and address the chip using WriteI2C().
1: void main() 2: { 3: char result; 4: 5: // ... previous code here 6: 7: StartI2C(); // Start condition I2C on bus 8: IdleI2C(); 9: WriteI2C(0xD0); // addresses the chip 10: IdleI2C(); 11: }
At this point, we are interested in the day of the week. By looking at the table of registers in the datasheet, we can see the register for day of the week is 03h (0x03). Therefore we will write to this address next.
1: void main() 2: { 3: char result; 4: 5: // ... previous code here 6: 7: StartI2C(); // Start condition I2C on bus 8: IdleI2C(); 9: WriteI2C( 0xD0 ); // addresses the chip 10: IdleI2C(); 11: WriteI2C( 0x03 ); // write register address 12: IdleI2C(); 13: StopI2C(); // Stop condition I2C on bus 14: }
We have addressed the chip and the register. Now, we want to read data from this register. Begin with a StartI2C() and this time, change the LSB to a 1 in the chip address to initiate a read function.
1: void main() 2: { 3: char result; 4: 5: // ... previous code here 6: 7: StartI2C(); // Start condition I2C on bus 8: IdleI2C(); 9: WriteI2C( 0xD0 ); // addresses the chip 10: IdleI2C(); 11: WriteI2C( 0x03 ); // write register address 12: IdleI2C(); 13: StopI2C(); // Stop condition I2C on bus 14: 15: StartI2C(); // Start condition I2C on bus 16: IdleI2C(); 17: WriteI2C( 0xD1 ); // addresses the chip with a read bit 18: IdleI2C(); 19: }
The object ‘result’ will hold the 4-bit BCD number in it’s 8-bit memory. Use a ReadI2C() command followed by a NotAckI2C() and a StopI2C() commands. The read will move the contents of the RTC register 03h into the memory object ‘result’. Since the value from the RTC is a 4-bit BCD number, ‘result’ will contain 4 leading zeros. The not-acknowledge will signal the RTC that reading data has completed.
1: void main() 2: { 3: char result; 4: 5: // ... previous code here 6: 7: StartI2C(); // Start condition I2C on bus 8: IdleI2C(); 9: WriteI2C( 0xD0 ); // addresses the chip 10: IdleI2C(); 11: WriteI2C( 0x03 ); // write register address 12: IdleI2C(); 13: StopI2C(); // Stop condition I2C on bus 14: 15: StartI2C(); // Start condition I2C on bus 16: IdleI2C(); 17: WriteI2C( 0xD1 ); // addresses the chip with a read bit 18: IdleI2C(); 19: result = ReadI2C(); // read the value from the RTC and store in result 20: IdleI2C(); 21: NotAckI2C(); // Not Acknowledge condition. 22: IdleI2C(); 23: StopI2C(); // Stop condition I2C on bus 24: }
The RTC code using the I2C bus is complete.
1: #include <i2c.h> 2: void main() 3: { 4: char result; 5: 6: TRISC = 0x00; //turn on tri-state register and 7: //make all output pins 8: PORTC = 0x00; //make all output pins LOW 9: 10: OpenI2C( MASTER, SLEW_OFF); 11: SSPADD = 0x3F; 12: 13: StartI2C(); // begin I2C communications 14: IdleI2C(); 15: WriteI2C( 0xD0 ); // addresses the chip 16: IdleI2C(); 17: WriteI2C( 0x01 ); // access register address for minutes 18: IdleI2C(); 19: WriteI2C( 0b00010010 ); // write value into minutes register 20: IdleI2C(); 21: StopI2C(); // stop condition I2C on bus 22: 23: StartI2C(); // begin I2C communications 24: IdleI2C(); 25: WriteI2C( 0xD0 ); // addresses the chip 26: IdleI2C(); 27: WriteI2C( 0x03 ); // access register address for day of the week 28: IdleI2C(); 29: WriteI2C( 0x04 ); // write value into day register 30: IdleI2C(); 31: StopI2C(); // stop condition I2C on bus 32: 33: StartI2C(); // Start condition I2C on bus 34: IdleI2C(); 35: WriteI2C( 0xD0 ); // addresses the chip 36: IdleI2C(); 37: WriteI2C( 0x03 ); // write register address 38: IdleI2C(); 39: StopI2C(); // Stop condition I2C on bus 40: 41: StartI2C(); // Start condition I2C on bus 42: IdleI2C(); 43: WriteI2C( 0xD1 ); // addresses the chip with a read bit 44: IdleI2C(); 45: result = ReadI2C(); // read the value from the RTC and store in result 46: IdleI2C(); 47: NotAckI2C(); // Not Acknowledge condition. 48: IdleI2C(); 49: StopI2C(); // Stop condition I2C on bus 50: }