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 digital temperature sensor that communicates over I2C.
The following is geared for the 18F4520. This tutorial will also work with the 18F452 and the 18F4525. Before continuing, be sure to read through the first tutorial in this series, PIC Programming Basics
You will need the following:
According to the digital temperature sensor datasheet, the sensor contains 8 pins:
In order to get this to work, I assume you are using an external crystal of 10MHz. 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.
int main() { TRISC = 0x00; //turn on tri-state register and //make all output pins PORTC = 0x00; //make all output pins LOW }
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.
#include <i2c.h> int main() { TRISC = 0x00; //turn on tri-state register and //make all output pins PORTC = 0x00; //make all output pins LOW }
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.
#include <i2c.h> int main() { TRISC = 0x00; //turn on tri-state register and //make all output pins PORTC = 0x00; //make all output pins LOW OpenI2C( MASTER, SLEW_OFF); }
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.
#include <i2c.h> int main() { TRISC = 0x00; //turn on tri-state register and //make all output pins PORTC = 0x00; //make all output pins LOW OpenI2C( MASTER, SLEW_OFF); SSPADD = 0x3F; }
At this stage you have successfully initialized the I2C.
The MAX6633 temperature sensor communicates using I2C as a “slave”. As you can see, there are 2 pins for this, pins 1 and 2. Pin 1 is the DATA line. Pin 2 is the CLOCK line.
According to the I2C specifications, both lines must be pulled HIGH using a resistor. In this example, a 1K resistor is used for both lines. I’ve used 2.2K and worked fine.
On page 6 of 16 of the MAX6633 datasheet, a table shows the timing pattern used in order to communicate with the device. We will use the functions located in the C18 libraries file which is apart of the ‘i2c.h’ library.
On page 7 of 16 of the MAX6633 datasheet, they show both registers inside the IC. CONFIGURATION and TEMPERATURE.
We will set the CONFIGURATION register first.
Let’s add the code to set the configuration register. We will begin an I2C sequence.
#include <i2c.h> int main() { TRISC = 0x00; // turn on tri-state register and // make all output pins PORTC = 0x00; // make all output pins LOW</code> OpenI2C( MASTER, SLEW_OFF); SSPADD = 0x3F; StartI2C(); // begin I2C communication IdleI2C(); WriteI2C( 0x80 ); // sends address to the // device IdleI2C(); }
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. So for example, on page 12 of 16 of the MAX6633 datasheet, you can see a table of available addresses to choose from. Notice all the addresses start with 100. This means the internal address bits for all MAX6633 devices are 100. The rest of the 7-bit address is determined by what address pins are wired to either ground (GND) or power (VCC).
So for our example, we are using 0x80. This gives us a 7-bit binary number consisting of ‘100’ internal and A0, A1, A2, A3 pins external all tied to ground.
Another example would be 0x82. In this case, the internal address bits would be ‘100’, the external A3, A2, A1 pins be connected to ground and A0 pin be connected to power.
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 CONFIGURATION register.
#include <i2c.h> int main() { TRISC = 0x00; // turn on tri-state register and // make all output pins PORTC = 0x00; // make all output pins LOW OpenI2C( MASTER, SLEW_OFF); SSPADD = 0x3F;</code> StartI2C(); // begin I2C communication IdleI2C(); WriteI2C( 0x80 ); // sends address to the // device IdleI2C(); WriteI2C( 0x01 ); // sends a control byte to // the device IdleI2C(); }
The least significant bit (8th bit) of 0x80 is LOW. Therefore, the PIC is writing the next byte. What is the next byte? It is 0x01.
According to page 13 of 16 of the MAX6633 datasheet, a table shows which registers are located in the IC. Each register has a hex value associated with it. Notice that the configuration 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 CONFIGURATION register.
According to page 13 of 16 of the MAX6633 datasheet, a table shows all the bits of the CONFIGURATION register and explains each one. Every bit of this register will change (configure) the behavior of this device in some way. For our example, we will use 0x20 as the byte to store in this register. You can set this differently later.
#include <i2c.h> int main() { TRISC = 0x00; // turn on tri-state register and // make all output pins PORTC = 0x00; // make all output pins LOW OpenI2C( MASTER, SLEW_OFF); SSPADD = 0x3F; StartI2C(); // begin I2C communication IdleI2C(); WriteI2C( 0x80 ); // sends address to the // device IdleI2C(); WriteI2C( 0x01 ); // sends a control byte to // the device IdleI2C(); WriteI2C( 0x20 ); // sends configuration byte – // continuous conversion, 9 // bit res IdleI2C(); StopI2C(); }
Now we are done with the configuration. Last thing to do is run StopI2C() and we’re finished. This code only has to be run once per device. Therefore, more than one temp sensor? Then must repeat this for all MAX6633 devices.
BTW, it is also possible to read this register at any time. For now, we’ll move on.
First thing we need to do is have a place to put the temperature bytes in our code. I created 2 variables.
char temperatureHI = 0; char temperatureLO = 0;
These ‘chars’ will contain 8-bit values when together, will represent a 12-bit temperature value. Since a single 8-bit register won’t hold 12-bits, we need 2 of these variable to hold both parts.
If you examine page 13 of 16 of the MAX6633 datasheet, a table shows where the temperature value will be stored (D5 through D14). Without getting into a lengthy discussion on temperature accuracy and bit shifting, we will read 2 bytes (all 16bits) into the PIC however only analyze the upper portion (temperatureHI). Why? Well, for this example, it’s all the accuracy you will need without building a lot of complex bit shifting code. Notice D15 is the sign bit allowing for signed values.
Let’s begin with StartI2C().
(I’ve removed our previous code from the following examples below. Be sure to continue this code after the configuration code)
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; ... // (previous configuration code here) StartI2C(); IdleI2C(); }
Next, we want to address the device again. Notice the 8th bit of 0x80 is LOW.
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; ... // (previous configuration code here) StartI2C(); IdleI2C(); WriteI2C( 0x80 ); IdleI2C(); }
According to page 13 of 16 of the MAX6633 datasheet, the TEMPERATURE register is located at address 00h (0x00). So, let’s access it.
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; ... // (previous configuration code here) StartI2C(); IdleI2C(); WriteI2C( 0x80 ); IdleI2C(); WriteI2C( 0x00 ); IdleI2C(); }
Next, initiate a restart.
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; ... // (previous configuration code here) StartI2C(); IdleI2C(); WriteI2C( 0x80 ); IdleI2C(); WriteI2C( 0x00 ); IdleI2C(); RestartI2C(); // Initiate a RESTART command IdleI2C(); }
Now, we’re going to read the temperature of the sensor. Notice the 8th bit of the address is now HIGH.
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; ... // (previous configuration code here) StartI2C(); IdleI2C(); WriteI2C( 0x80 ); IdleI2C(); WriteI2C( 0x00 ); IdleI2C(); RestartI2C(); // Initiate a RESTART command IdleI2C(); WriteI2C( 0x81 ); // address device w/read IdleI2C(); }
At this stage, the PIC will begin to pulse the clock line as usual, however, the MAX6633 will take over the data line and begin sending data to the PIC’s I2C receive register. When done, the value will be stored in temperatureHI. When done, it’s the PIC’s turn to acknowledge (ACK) the device, letting the device know it has received the data. This ACK tells the device the PIC is prepared to receive a second byte.
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; ... // (previous configuration code here) StartI2C(); IdleI2C(); WriteI2C( 0x80 ); IdleI2C(); WriteI2C( 0x00 ); IdleI2C(); RestartI2C(); // Initiate a RESTART command IdleI2C(); WriteI2C( 0x81 ); // address device w/read IdleI2C(); temperatureHI = ReadI2C(); // Returns the MSB byte // and stores it in // 'temperatureHI' IdleI2C(); AckI2C(); // Send back Acknowledge IdleI2C(); }
Since the MAX6633 device is made to send ALL 16 bits, we must run another ReadI2C command again. What I do is store the second set of data into another variable called temperatureLO although I’m not going to use it. I only care about the value in temperatureHI.
When done, the PIC sends a Not-Acknowledge (NotAck) to tell the device to stop sending data. Finish with a Stop command.
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; ... // (previous configuration code here) StartI2C(); IdleI2C(); WriteI2C( 0x80 ); IdleI2C(); WriteI2C( 0x00 ); IdleI2C(); RestartI2C(); // Initiate a RESTART command IdleI2C(); WriteI2C( 0x81 ); // address device w/read IdleI2C(); temperatureHI = ReadI2C(); // Returns the MSB byte // and stores it in // 'temperatureHI' IdleI2C(); AckI2C(); // Send back Acknowledge IdleI2C(); temperatureLO = ReadI2C(); // returns the LSB of // the temperature IdleI2C1); NotAckI2C(); IdleI2C(); StopI2C(); }
Finally, you have configured the device and have received a temperature (with +/- sign) into a variable called ‘temperatureHI’.
#include <i2c.h> int main() { char temperatureHI = 0; char temperatureLO = 0; TRISC = 0x00; // turn on tri-state register and // make all output pins PORTC = 0x00; // make all output pins LOW OpenI2C( MASTER, SLEW_OFF); SSPADD = 0x3F; StartI2C(); // begin I2C communication IdleI2C(); WriteI2C( 0x80 ); // sends address to the // device IdleI2C(); WriteI2C( 0x01 ); // sends a control byte to // the device IdleI2C(); WriteI2C( 0x20 ); // sends configuration byte – // continuous conversion, 9 // bit res IdleI2C(); StopI2C(); StartI2C(); IdleI2C(); WriteI2C( 0x80 ); IdleI2C(); WriteI2C( 0x00 ); IdleI2C(); RestartI2C(); // Initiate a RESTART command IdleI2C(); WriteI2C( 0x81 ); // address device w/read IdleI2C(); temperatureHI = ReadI2C(); // Returns the MSB byte // and stores it in // 'temperatureHI' IdleI2C(); AckI2C(); // Send back Acknowledge IdleI2C(); temperatureLO = ReadI2C(); // returns the LSB of // the temperature IdleI2C1); NotAckI2C(); IdleI2C(); StopI2C(); }