I2C register-enabled slave

15 September 2018
This article details the use of register addresses and address/data holds on a PIC16F1828 microcontroller programmed as a stripped-down I2C slave. It was originally part of a work-in-progress firmware for graphical LCD display, but felt that the firmware code for the I2C transactions themselves warranted a stand-alone write-up, especially considering apparent pit-falls when using address holding within I2C reads. Past articles such as the 17-segment LCD display that made use of I2C slave functionality were based around the PIC16F88, which I have decided to retire in favour of microcontrollers within the PIC16F182x chipset family, the latter of which includes the PIC12F1822 used for the I2C-RS232 master. With this in mind I decided to also make the write-up self-contained, by covering setup issues as well.

Overview

For an I2C write transaction — a reception from the perspective of the I2C slave — any I2C register values are simply the first bytes within the transaction. However for an I2C read an initial 1 or 2 byte write needs to be performed, and then the transaction turned into a read using an I2C Restart. The firmware will also make use of three I2C features: Clock steteching, address hold, and data hold, which are detailed below:
Clock stretching
Clock stretching is w when an I2C slave holds SCL (the I2C clock line) low, which causes the I2C master to suspend clocking. This allows the I2C slave to defer the transaction until it is ready to service it.
Address hold
Address holding allows the firmware to decide whether a device address match should be acknowledged, or a NACK sent. If address holding is disabled, the MSSP hardware will automatically acknowledge the incoming address byte.
Data hold
When receiving data, the MSSP hardware automatically acknowledges the received byte before it is passed to the firmware. Data hold allows the firmware itself to decide whether the byte should be acknowledged or not.
The complete firmware is
available in Bitbucket, so the code snippets shown below will have simplifications and minor omissions for clarity. The firmware will be updated if the issues highlighted have future work-arounds.

I2C Setup

On Microchip PIC microcontrollers the pins used for I2C communications need to be tristated (i.e. configured as inputs) and set to binary mode. On the PIC16F1828 the I2C pins are RB4 and RB6, so the corresponding bits of TRISB need to be set high, and the corresponding bits of ANSELB cleared.

void pinSetup(void) { TRISB = 0b01010000; ANSELB=0; }

The next stage is to setup I2C itself, which in this case will be I2C slave mode with 7-bit addresses. Clock stretching and data-hold are both enabled, whereas interrupts on I2C Start & I2C Stop are not required. Address-hold is within conditional compilation because it is broken for read transactions.

void i2cSetupSlave(void) { SSP1CON1 = 0b00100110; // I2C slave, 7-bit address, no S/P interrupts SSP1CON2 = 0b1; // Enable clock stretching SSP1CON3 = 0b1; // Enable data-hold SSP1ADD = 0x02; // Slave address #ifdef I2C_ADDR_HOLD SSP1CON3 |= 0b10; // Enable address-hold #endif }

Processing loop

Although it is possible to service I2C transactions from within an interrupt handler, my preferred approach is to instead poll the PIR1 register for pending I2C transactions. If any are pending then bit 0x08 will be set — if this is the case the I2C handling code is executed:

void main(void) { pinSetup(); i2cSetupSlave(); while(1) { if( PIR1 & 0x08 ) { /* I2C interrupt */ i2cOverflow(); if( SSP1STAT & 0x04 ) { i2cSend(); continue; } while( SSP1STAT & 0b1 ) { /* Busy flag set */ i2cRecv(); } } } /* Non-I2C logic here */ }

The first thing to do is to handle any receive overflows. Afterwards if the transaction is a read the slave-to-master handling is called, otherwise the master-to-slave handling runs until there is no more pending data. All these handling routines are detailed in the following sections.

Receive overflow

If the SSPOV (receive overflow) bit of SSPxCON is set, it means that reception of a byte completed before the previous byte had been read by firmware. From what I have read if this happens the buffer needs to be drained and the SSPOV bit explicitly cleared. I suspect that in practice proper use of clock stretching should avoid the SSPOV bit ever getting set, so am not entirely sure whether the recovery procedure below is correct, let alone strictly necessary.

void i2cOverflow(void) { if(SSP1CON & 0x40) { i2cByte = SSP1BUF; SSP1CON1 &= ~0x40; PIR1 &= ~0x08; SSP1CON1 |= 0b10000; return; } }

I2C reads (transmission to master)

It is assumed that an I2C write has already been performed so that the register values have been put into i2cAddr[], but for robustness flags could be added which are invalidated when an I2C Stop occurs. If address hold is enabled the address byte received block is entered twice, the first time being the opportunity to decide whether to ack/nack the address byte, and the second being indication that it is time to send the first byte. The loop lap where the acknowledgement sequence is in progress, and when a Nack has been received from the master, are the two situation when no data is transmitted by the slave.

void i2cSend(void) { PIR1 &= ~0x08; if(! (SSP1STAT & 0x20)) { /* Address byte received */ i2cByte = SSP1BUF; #ifdef I2C_ADDR_HOLD if( SSP1CON3 & 0x80 ) { /* Acknowledge sequence */ SSP1CON2 &= ~0x20; SSP1CON1 |= 0x10; return; } #endif } else if( (SSP1CON & 0x40) ) { /* NACK from Master. Don't send any more data. */ SSP1CON1 |= 0x10; return; } i2cSlaveWrite(i2cAddr[0] + i2cAddr[1]); SSP1CON1 |= 0x10; // End clock stretch }

The final setting of the CKP bit of SSPxCON1 register following the write of the data byte releases the clock line, and hence ends clock stretching, so that the master is able to clock out the next byte. If clock stretching was absent, it is possible that the firmware might not read and handle a received byte before the reception of a subsequent byte has completed. The actual write, which for demonstration purposes uses the sum of the two register address bytes, is done by i2cSlaveWrite() below:

void i2cSlaveWrite(unsigned char bite) { SSP1CON &= ~0x80; SSP1BUF = bite; while(SSP1CON & 0x80) { SSP1CON &= ~0x80; SSP1BUF = bite; } }

The WCOL bit of SSP1CON indicates that a write to the I2C buffer was performed when the I2C hardware was not idle, which due to the logic should only happen if a previous transmission back to master is still in progress. In this scenario the bit is cleared and the write attempted again.

I2C writes (reception from master)

Writes are a bit more complex because they handle the register values, even if the ultimate intention is to read data from the I2C slave — the latter is handled by doing a write that is then flipped into a read using a I2C Restart. The read/write bit of SSP1STAT is used to reset a counter that is used to keep track of the received bytes, so that received bytes are all routed to the right place.

void i2cRecv(void) { PIR1 &= ~0x08; i2cByte = SSP1BUF; if(! (SSP1STAT & 0x20)) { /* Address byte received */ i2cAddrCnt = 0; // This should have no effect with AHEN disabled, // but it is required to recover when DHEN is used // to NACK incoming data. SSP1CON2 &= ~0x20; } else if(i2cAddrCnt < 2) { /* Data bytes that are really register addresses */ i2cAddr[i2cAddrCnt++] = i2cByte; if(i2cByte == 1) SSP1CON2 |= 0x20; else SSP1CON2 &= ~0x20; } else { /* Actual data bytes */ SSP1CON2 &= ~0x20; } SSP1CON1 |= 0x10; }

In the case of a read the Actual data bytes will never be reached as the I2C master should have turned the transaction into a read.

Broken address hold support

If address hold is enabled, the first byte clocked out to the master is the slave address bite-shifted one place (i.e. multiplied by two), rather than the first byte the firmware presents for transmission. This makes the feature functionally useless, because if it is enabled the I2C master has to contend with output that is not expected according to the I2C specifications. Much investigation has not revealed any mistake that would result in such non-standard output, so I can only conclude this is the result of a hardware bug — a drastic conclusion, but one dictated by circumstances.

In practice I am not interested in I2C transactions the hardware address matching & masking does not already exclude, but in the I2C read pipe-line the ability to Nack the device address is the only opportunity a I2C slave has of rejecting an incoming read transaction. The alternative is to serve garbage values in error conditions, which is far from elegant, and likely results in trouble elsewhere. I expect this to come back to sting me.