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 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
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
Conclusion
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.
Hi Sami
Many thanks for the helpful article. I tried many hours to connect a TMP117 to my PIC, unsuccessful. It drives me crazy. I deal since 30 years with PICs, starting with the old C types, but I had never such a bunch of problems. I tried it with the hints from Sprut. After a long trial and error period, it seems to me, understanding and handling the PICs Interface is more complicated than programming up from scratch with the TMP102/117 datasheet. But now it is working, thanks again.
In the section “checking for idle bus” you use pagesel instad of banksel. For beginners this ist a trap.
I use a more simpe “bus idle subroutine”
TESTIDLE:
BANKSEL SSPCON2
IDLELOOP: MOVLW B’00011111′ ; Bits ACKEN to SEN 0..4
ANDWF SSPCON2,W
SKPNZ
RETURN
GOTO IDLELOOP
Cheers
Bruno
PS:
Do you use the stethoscope in your Avatar to find defective ball bearings in old harddisks 🙂