Communicating with tmp102 Sensor Over I2C Using PIC MCU and Assembly Part 2

Implementing the reading sequence on a PIC16F877A microcontroller using assembler

In the first part of this article I described how the I²C interface works and how it can be used to read a temperature sensor – if you haven’t read it you can find it here. In this second part I will write about how one can actually utilise the I²C interface on a PIC microcontroller.

In theory everything is very simple but in practice my experience shows quite a nightmare 🙂 I will describe exactly how I managed to get I²C communication running in assembler on the PIC16F877A microcontroller, but the same code should work across the whole 16F family of microcontrollers .

The PIC16F877A controller has a dedicated synchronous port which can be used for I²C and SPI communication. This means that we should only tell it what to send and the port will deal with the low level technicalities such as timing, checking the bus and so on. An alternative to using a dedicated “hardware” port is to code the conditions in software by turning on and off a general purpose IO pins. On some MCUs where such a port is not available this is the only option.

Configuring the MSSP Port

Before we start sending or receiving data on the bus we have to first configure the MSSP port of the microcontroller. This is done by setting the SSPEN bit of SSPCON1 register, setting the SSPM3:SSPM0 bits of the same registers to 1000 – which means master mode. We must set the baud generator by setting value into the SSPADD register. The value is determined by this formula:

clock (e.g. 100KHz) = Fosc / (4 * (SSPADD + ))

In my case I want 100KHz I²C clock, while running the microcontroller with 4MHz crystal. This gives value of SSPADD = 0x09
We must also set the SMP bit of SSPSTAT register to 1 to indicate – Slew rate control disabled.
Last but not least we must set the PORTC bits dedicated for the MSSP port (in my case these are pins 3  and 4) as inputs and set them to ones (I don’t know if setting them to ones is actually necessary but they must be inputs).

How this procedure looks like in assembly:

;setting up the i2c port
movlw         b'10000000'
banksel       SSPSTAT
movwf         SSPSTAT
movlw         b'00101000'
banksel       SSPCON
movwf         SSPCON
movlw         0x09
banksel       SSPADD
movwf         SSPADD
movlw         b'00011000'
banksel       TRISC
movwf         TRISC
banksel       PORTC
bsf                PORTC,3
bsf                PORTC,4

Checking for idle bus

We can check if the bus is idle by querying the SSPCON2 register whether any condition is taking place at the moment. The port has a bit for each condition: start, stop, etc.

We can check if a start condition is going on for example with this snippet:

;check for start condition on the bus
pagesel     SSPCON2
btfsc          SSPCON2, SEN
goto           $-1

It will loop until a start condition has finished. To check for all conditions and actually assuring an idle bus we can use the following piece of code:

; check for idle
banksel SSPCON2
goto $-1
goto $-3
btfsc SSPCON2, PEN
goto $-5
goto $-7
btfsc SSPCON2, SEN
goto $-9

Remember to use the pagesel directive in the beginning because the microcontroller may have more than 2K words of program memory which means that a goto statement may jump to a location outside the current page, which will lead to errors of course 🙂

Sending a Start Condition

Once the bus is known to be idle, we can send a start condition. This is done by setting the SEN bit of the SSPCON2 port. This is will make the MSSP port of the controller to drive the SDA line low while keeping the SCL high effectively reserving the I²C bus. Once the event has finished the bit is automatically cleared. We can use this to query for the end of the start condition:

;send start condition
banksel     SSPCON2
bsf             SSPCON2, SEN
btfsc          SSPCON2, SEN
goto $-1

Send Data

Once we have reserved the bus by sending a start condition we can send some data. This is done by feeding the byte we want to send into another register – SSPBUF. When data is put into the SSPBUF register it is sampled on the bus – bit by bit – by the MSSP port of the controller. Once all bits of the byte has been send the SSPIF flag/bit of the PIR1 register is set. Thus we can query this bit to get if the transmission has finished.

;send one byte
banksel        SSPBUF
movlw          b'10010000'
movwf          SSPBUF
banksel         PIR1
bcf                  PIR1, SSPIF
btfss               PIR1, SSPIF
goto                $-1
bcf                  PIR1,SSPIF

Wait for ACKnowledgement From the Slave

Checking for a acknowledgement is easy – the SSPCON2 register has the ACKSTAT bit which is cleared when ACK is received or set when NACK is received. Once we have sent all 8 bits we should query if the 9th bit is 0 – meaning ACK. In this case I assume the bit will be indeed an acknowledgement but in reality we can check for NACK as well and do some error handling, retransmission, etc.

;wait for ACK
banksel         SSPCON2
btfsc               SSPCON2, ACKSTAT
goto                $-1

Repeated Start or Restart

We said in the previous article that the master can continue the communication with the slave without releasing the bus. In this case if the master wants to send more bytes it should issue a restart condition. This is done by setting the RSEN bit of the SSPCON2 register. Once the repeated start has finished the bit is cleared by the microcontroller, so we can use this to check this happened. It is very important that a repeated start condition will only occur if the bus is idle, so we should assure that before. From the PIC16F877A datasheet:

Note: If RSEN is programmed while any other event is in progress, it will not take effect.

; check for idle
banksel         SSPCON2
btfsc               SSPCON2,ACKEN
goto                $-1
btfsc               SSPCON2,RCEN
goto                $-3
btfsc               SSPCON2,PEN
goto                $-5
btfsc               SSPCON2,RSEN
goto                $-7
btfsc               SSPCON2,SEN
goto                $-9
; repeated start condition
banksel            SSPCON2
bsf                     SSPCON2, RSEN
btfsc                 SSPCON2, RSEN
goto                  $-1

Enable Receiver Mode on the Master

Once we have send the address of the slave with the last bit set high indicating we want to read from it we must turn the master into receiving mode. This is done by setting the RCEN bit of the SSPCON2 register. Once the master is in receiving mode the bit is cleared. When the master is receiver the SSPBUF is used to hold incoming data as opposed to holding data to be send to the slave.

Note: What is important is that we cannot queue data into that port. If new byte comes while the old one has not been read from the SSPBUF a collision occurs.

Note: Once again we must ensure the bus is idle before we set RCEN bit on, otherwise it will not take effect.

Note: If we want to read a second byte we must again check for idle bus followed by another RCEN enablement.

; check for idle
; the code for idle check is the same as above, it will be not listed here
;enable receiver mode
bsf              SSPCON2, RCEN
btfsc           SSPCON2, RCEN
goto            $-1
; when set into receiving mode the master starts accepting bits and putting ;them into the SSPBUF, once a whole byte has arrived the SSPIF bit is set ;and the byte must be read from the register in order to be able to receive ;another one
banksel    PIR1
btfss          PIR1,SSPIF
goto           $-1
bcf              PIR1,SSPIF
banksel     SSPBUF
movfw       SSPBUF
banksel     byte1
movwf       byte1

Send Acknowledge From the Master

So far we were checking whether the slave acknowledged data sent by the master. Now the master receives data – thus it has to acknowledge it. This is done by setting ACKEN bit of the SSPCON2 register while the ACKDT bit is cleared. If we want to send NACK then the ACKDT bit should be set.

Note: It is important to first set the value of the ACKDT bit and the enable the ACKEN bit.

Note: Once the ACK bit is sent the ACKEN bit is cleared automatically.

;send acknowledgement
banksel         SSPCON2
bcf                  SSPCON2, ACKDT
bsf                  SSPCON2, ACKEN
btfsc              SSPCON2, ACKEN
goto               $-1
banksel        PIR1
bcf                 PIR1,SSPIF

Sending a Stop Condition

To send a stop condition we must set the PEN bit of the SSPCON2 register. Once the stop condition has finished the SSPIF bit of PIR1 register is set – this is how we can check if the stop has ended.

; send stop bit
banksel      PIR1
bcf               PIR1, SSPIF
banksel      SSPCON2
bsf               SSPCON2, PEN
banksel      PIR1
btfss            PIR1,SSPIF
goto             $-1


I have described how we can use the MSSP port of the PIC16F877A microcontroller to talk to all sort of electronic devices using the I²C interface. What I have described is the happy path – only one master and one slave on the bus. In reality we should include error checking and correction logic. The interface provides capabilities of connecting many masters to the same bus with additional logic of synchronising them with one another but this is out of scope for this article.


Leave a Reply