Using the I2C Bus
Many many thanks to Ken Pergola for helping me learn the I2C bus.
What is the I2C bus and why would you want to use it? I2C, or IIC, pronounced "I squared C", stands for Inter-Integrated Circuit. It's a useful way of communicating between integrated circuits, for example between your PIC18F452 and another PIC, like the one on an SRF08 Ultrasonic Rangefinder, or an EEPROM, which would allow you to store lots of data, like maybe webpages if you're looking to implement a PIC webserver.
Basics
I won't get too into the physics of I2C because that's covered in depth in many other places on the web much more accurately than I could hope to do. The I2C bus has a master (or multiple masters if you're a fancy weirdo, but we won't get into that here) and a bunch of slaves. This network can be managed on a single two-wire bus. The bus uses two lines, a clock, SCL, and a data line, SDA, which are both shared by the master and its cohort of slaves. The I2C bus operates at anywhere from 1kHz to 400kHz. 400kHz is not supported by all devices, and 1kHz is unnecessarily slow, so we'll go with 100kHz, the highest commonly supported speed.
There are, for our purposes at least, seven basic operations a master can use to communicate with its slaves: start, restart, stop, write, receive, acknowledge and not acknowledge. The details of all of these operations are managed by the PIC's hardware. The PIC's registers for controlling this functionality are SSPSTAT, SSPCON1 and SSPCON2.
Initialization
First we need to set up the PIC's MSSP module to work as an I2C master. The MSSP module has a few other functions, including being an I2C slave. First we need to set the SDA and SCL pins to input, even though the MSSP module will handle the pins details later. This is simple, just like setting an LED blinking pin's direction:
bsf TRISC, SCL ; I2C SCL pin is input
bsf PORTC, SCL ; (will be controlled by SSP)
bsf TRISC, SDA ; I2C SDA pin is input
bsf PORTC, SDA ; (will be controlled by SSP)
Then we need to setup the baud rate. Just like with the UART baud rate, there is a register that controls whether the baud rate is high or low, with the MSSP this is called the "slew control". I have no idea why. Let's disable the slew control since we're shooting for 100kHz, if we wanted 400kHz we'd enable it.
bsf SSPSTAT, SMP ; slew rate control disabled
To get the value to set the baudrate to 100kHz we'll use a formula that is provided in the datasheet.
#define FOSC d'4000000' #define I2CClock d'100000' #define ClockValue (((FOSC/I2CClock)/4) -1)
FOSC is the speed in Hz of your PIC's clock. The I2CClock is the desired clock speed of the I2C bus. ClockValue will end up holding the value you need to give to the MSSP baudrate generator. These define statements go at the top of the file with the header and configuration flags.
Now we'll set the MSSP to I2C master mode and enable the MSSP.
bsf SSPCON1, SSPM3 ; I2C master mode in hardware
bcf SSPCON1, SSPM2 ;
bcf SSPCON1, SSPM1 ;
bcf SSPCON1, SSPM0 ;
bsf SSPCON1, SSPEN ; enable MSSP module
Finally, we give the ClockValue we calculated earlier to the MSSP baudrate generator:
mov ClockValue, SSPADD ; set baudrate
This "mov" instruction is a macro I defined for convenience, instead of
movlw LiteralValue
movwf FileRegister
I'll leave the details of that to you. Put all of that into a subroutine. I'll show the whole initialization routine in a second. First:
Waiting for the MSSP to be idle
If you don't wait for the MSSP to be idle before trying to execute another operation you end up with write collisions. Before executing any operation I check for the MSSP module to be idle. It's not entirely necessary, but at worst it only wastes a few clock cycles.
The datasheet specifies that the idle condition of the MSSP can be detected by Oring the SSPSTAT, R_W bit with the five operation bits of SSPCON2. That didn't make too much sense to me, so I figured it out for myself.
First, SSPSTAT, R_W will be set if the MSSP is transmitting data. So this needs to be clear before we do anything. The five least significant bits of SSPCON2 indicate whether the MSSP is executing any of the actions it can do. These bits are ACKEN, RCEN, PEN, RSEN, and SEN. If you read on I'll explain what each of them does. For the time being, know that they all need to be clear for the MSSP to be idle.
Checking whether the R_W bit is clear is easy. We just loop using a btfsc. The five bits of SSPCON2 can be checked in one pass. Since they're the five least significant bits, if a bitwise AND between the SSPCON2 register and the literal mask 00011111 equals zero, we know all five are clear. Another way of writing 00011111 is 0x1F.
;; --- IDLE -------------------------------
;; Loop until MSSP is idle
I2C_Idle
i2cidle_loop1
btfsc SSPSTAT, R_W ; transmitting?
bra i2cidle_loop1
i2cidle_loop2
movf SSPCON2, W
andlw 0x1F ; mask ACKEN, RCEN, PEN, RSEN, SEN
btfss STATUS, Z
bra i2cidle_loop2
return
First we loop until R_W is clear. then we copy SSPCON2 to W. Then we apply the mask 0x1F. If the product of that mask is zero (0x00), the Z bit of the STATUS register will be clear. If it's not clear we loop until it is, then we move on. Let's pop that subroutine into the initialization routine so that we pause to make sure the MSSP is idle before doing anything else. Then the initialization looks like this:
;; --- INIT -------------------------------
;; Initialize the PIC I2C
I2C_Init
bsf TRISC, SCL ; I2C SCL pin is input
bsf PORTC, SCL ; (will be controlled by SSP)
bsf TRISC, SDA ; I2C SDA pin is input
bsf PORTC, SDA ; (will be controlled by SSP)
bsf SSPSTAT, SMP ; slew rate control disabled
bsf SSPCON1, SSPM3 ; I2C master mode in hardware
bcf SSPCON1, SSPM2 ;
bcf SSPCON1, SSPM1 ;
bcf SSPCON1, SSPM0 ;
bsf SSPCON1, SSPEN ; enable MSSP module
mov ClockValue, SSPADD ; set baudrate
call I2C_Idle
return
Call that from the initialization block of the main program.
Start
First thing first, we need to know how to assert a start condition on the I2C bus. If you want to know what that looks like in terms of voltage, check the datasheet, wikipedia, google, or Phillips (who created I2C) for the I2C specification. The start condition wakes up all slaves on the bus and gets them listening for further instructions. All we do to set the start condition is set the SEN bit of SSPCON2 and wait for it to clear, meaning that the operation is complete.
;; --- START ------------------------------
;; Send an I2C start sequence
I2C_Start
call I2C_Idle
bsf SSPCON2, SEN ; start sequenceI2C_Start_loop
btfsc SSPCON2, SEN
bra I2C_Start_loop
return
Simple.
Restart
Restart conditions are similar to start conditions. They're used in reading from the slave. It works just like setting a start condition, except you use SSPCON2, RSEN
;; --- RESTART ---------------------------
;; Repeat an I2C start sequence
I2C_Restart
call I2C_Idle
bsf SSPCON2, RSEN ; repeated start sequence
i2crestart btfsc SSPCON2, RSEN
bra i2crestart
return
Stop
Where start wakes up all the slaves, stop finishes all operations and sends the slave away. Again, it's implemented just like start and restart except it uses SSPCON2, PEN.
;; --- STOP -------------------------------
;; Send an I2C stop sequence
I2C_Stop
call I2C_Idle
bsf SSPCON2, PEN ; stop bit
i2cstop btfsc SSPCON2, PEN
bra i2cstop
return
Write
Okay. Now things get trickier. The first difference here is that we're using an interrupt to check whether the operation is complete, since when a write operation completes the MSSP sets the SSPIF flag of PIR1. I guess this could be used so that you start the write operation, move off to some other code that, perhaps, washes your dishes or takes a smoking break, and then when the write is complete the interrupt grabs the program's attention. We're not doing that here.
To write a byte to the I2C bus, you just need to move the byte to the SSPBUF register. Then the hardware takes care of the rest, triggering the SSPIF interrupt when it's done. Finally we'll return a value that denotes whether the slave acknowledged our writing. If the slave didn't acknowledge we'll need to try something else. We'll deal with what that something else is in the next tutorial.
;; --- WRITE ------------------------------
;; Write the WREG to the I2C bus
;; INPUT: W: byte to write
I2C_WriteW
call I2C_Idle ; wait until MSSP is idle
bcf PIR1, SSPIF ; clear interrupt flag
movwf SSPBUF ; load byte and send
I2C_WriteW_send_loop
btfss PIR1, SSPIF ; send complete? bra I2C_WriteW_send_loop
btfss SSPCON2, ACKSTAT
retlw d'0'
retlw d'1' ; ackstat is high if ack NOT received
return
When the write process is completed, the PIC sets SSPIF and moves the acknowledgment from the slave to ACKSTAT. We then return ACKSTAT to the WREG, where we can check it and respond appropriately in program code.
Receive
Receiving works like writing except we're not returning an acknowledge status, since the master sends the acknowledge in this case. This also uses an interrupt flag.
;; --- RECEIVE ----------------------------
;; Receive a byte over I2C
I2C_Receive
call I2C_Idle
bcf PIR1, SSPIF ; clear interrupt flag
bsf SSPCON2, RCEN ; enable reception
i2creceive1 btfss PIR1, SSPIF ; byte received
bra i2creceive1
return
Acknowledge
After we've received a byte from a slave we need to acknowledge the transmission if we want to read more bytes. If we're done reading we can Not Acknowledge. Acknowledging works just like starting, restarting, or stopping, except is uses ACKEN. Also, we need to set the ACKDT bit to tell whether this is an acknowledge or a not acknowledge (ack or nack).
;; --- ACKNOWLEDGE ------------------------
;; Send an acknowledgement
I2C_Acknowledge
bcf SSPCON2, ACKDT ; acknowledge
bsf SSPCON2, ACKEN ; send acknowledge
I2C_Acknowledge_send_loop
btfsc SSPCON2, ACKEN ; sent?
bra I2C_Acknowledge_send_loop
return
Not Acknowledge
This works just like acknowledgment, but with a set ACKDT bit.
;; --- NACKNOWLEDGE -----------------------
;; Send a negative acknowledgement
I2C_NAcknowledge
bsf SSPCON2, ACKDT ; not acknowledge
bsf SSPCON2, ACKEN ; send negative acknowledge
I2C_NAcknowledge_send_loop
btfsc SSPCON2, ACKEN ; sent?
bra I2C_NAcknowledge_send_loop
return
Those are our seven I2C operations. Of course, they're totally useless to us without a slave to communicate with, and a program to use them. Check the next tutorial for that. Put these subroutines into an include file called I2C.inc.