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.