I2C slave for the K22

General discussion relating to the library modules supplied with the compiler

Moderators: David Barker, Jerry Messina

Post Reply
Jerry Messina
Swordfish Developer
Posts: 1469
Joined: Fri Jan 30, 2009 6:27 pm
Location: US

I2C slave for the K22

Post by Jerry Messina » Mon Apr 20, 2015 5:29 pm

Here's the shell for an I2C slave module for the K22 series.

It's from some development code I wrote a while back so it's a little rough around the edges, but it can be used to
test basic functionality. It doesn't do much of anything... just collect up bytes sent.

The code should be used with an I2C master that supports clock stretching, so it may not work well with some bit-banged masters.
Also, I've had mixed results porting it to other chips. It seems that there are a number of different hardware MSSP module implementations
and they each have their own quirks and errata.

i2cslave.bas:

Code: Select all

//
//------------------------------------------------------------------------------
//  Name    : i2cslave.bas
//  Author  : Jerry Messina
//  Date    : 5/1/2012
//  Version : 1.0.0
//  Notes   :
//
//  MSSP I2C slave for the 18F26K22
//
//  ver 1.0.0
//    - initial
//------------------------------------------------------------------------------
//
module i2cslave

// i2c_handler state tracking (for debugging)
#option ENA_I2C_STATE_TRACKING = false

// select MSSP1 or MSSP2
#option _I2C_SLAVE_MSSP = 1

// i2c slave address and mask bits
// note: in address mask mode, addr 0 is reserved (no ACK) unless GCEN is set
// in 7-bit address masking mode (the default configuration with MSSPMSK = 1)
// clearing a bit in the SSPMSK causes the address bit to be masked, while setting
// SSPMSK bit requires a match in that position.
#option I2C_SA = $80       // slave address
#option I2C_SAM = $80      // slave addr mask

const
    I2C_SLAVE_ADDR      = I2C_SA,
    I2C_SLAVE_ADDR_MASK = I2C_SAM
    'I2C_SLAVE_ADDR      = %10100000,       // $A0
    'I2C_SLAVE_ADDR_MASK = %11110010       // $A0, $A4, $A8, $AC
    'I2C_SLAVE_ADDR      = %10100000,       // $00
    'I2C_SLAVE_ADDR_MASK = %00000000       // $A0-AF

//
//----------------------------------------------------------------
// MSSP definitions
//----------------------------------------------------------------
//

// device-specific register check
#if not(_device in (18F25K22, 18F26K22))
  #warning "check device MSSP register assignments"
#endif

#if (_I2C_SLAVE_MSSP = 1)
// MSSP1 pins
dim
    SCL     as PORTC.3,            // MSSP1 I2C scl
    SDA     as PORTC.4             // MSSP1 I2C sda

// MSSP1 control registers
public dim
    SSPCON1      as SSP1CON1,
    SSPCON2      as SSP1CON2,
    SSPSTAT      as SSP1STAT,
    SSPADD       as SSP1ADD,         // addr and mask reg share same reg addr
    SSPMSK       as SSP1MSK,
    SSPBUF       as SSP1BUF

// MSSP1 interrupt flag bits
public dim
    SSPIF as PIR1.bits(3),
    SSPIE as PIE1.bits(3),
    SSPIP as IPR1.bits(3),
    BCLIF as PIR2.bits(3),
    BCLIE as PIE2.bits(3),
    BCLIP as IPR2.bits(3)

#elseif (_I2C_SLAVE_MSSP = 2)
// MSSP2 pins
dim
    SCL     as PORTB.1,            // MSSP2 I2C scl
    SDA     as PORTB.2             // MSSP2 I2C sda

// MSSP2 control registers
public dim
    SSPCON1      as SSP2CON1,
    SSPCON2      as SSP2CON2,
    SSPSTAT      as SSP2STAT,
    SSPADD       as SSP2ADD,         // addr and mask reg share same reg addr
    SSPMSK       as SSP2MSK,
    SSPBUF       as SSP2BUF

// MSSP2 interrupt flag bits
public dim
    SSPIF as PIR3.bits(7),
    SSPIE as PIE3.bits(7),
    SSPIP as IPR3.bits(7),
    BCLIF as PIR3.bits(6),
    BCLIE as PIE3.bits(6),
    BCLIP as IPR3.bits(6)
#else
  #error "invalid _I2C_SLAVE_MSSP selection"
#endif

// MSSPx register bits
// SSPxCON1 bits
const
    SSPM0 = 0,
    SSPM1 = 1,
    SSPM2 = 2,
    SSPM3 = 3,
    CKP = 4,
    SSPEN = 5,
    SSPOV = 6,
    WCOL = 7

// SSPxCON2 bits
const
    SEN = 0,
    RSEN = 1,
    PEN = 2,
    RCEN = 3,
    ACKEN = 4,
    ACKDT = 5,
    ACKSTAT = 6,
    GCEN = 7

// SSPxSTAT bits
const
    BF = 0,
    UA = 1,
    R_W = 2,
    S = 3,
    P = 4,
    D_A = 5,
    CKE = 6,
    SMP = 7

// the RD/WR# bit (lsb of the address byte)
const RD_WRN = 0        // bit(0)

//
//----------------------------------------------------------------
// I2C state definitions (from microchip AN734)
//----------------------------------------------------------------
//
// State 1: MWR_ADDR - I2C write operation, last byte was an address byte
//          SSPSTAT bits: S = 1, D_A = 0, R_W = 0, BF = 1
//
// State 2: MWR_DATA - I2C write operation, last byte was a data byte
//          SSPSTAT bits: S = 1, D_A = 1, R_W = 0, BF = 1
//
// State 3: MRD_ADDR - I2C read operation, last byte was an address byte
//          SSPSTAT bits: S = 1, D_A = 0, R_W = 1, BF = 1 (see note 3)
//
// State 4: MRD_DATA - I2C read operation, last byte was a data byte
//          SSPSTAT bits: S = 1, D_A = 1, R_W = 1, BF = 0, CKP = 0
//
// State 5: M_NACK - Slave I2C logic reset by NACK from master
//          SSPSTAT bits: S = 1, D_A = 1, (R/W = 1), BF = 0, CKP = 1 (see note 5)
//
// note 3: In PIC16 and older PIC18 devices, the BF flag is not set.
// In newer PIC18 devices, the BF flag is set and needs to be read and cleared
// for State 3
//
// note 5: In PIC16 and older PIC18 devices, the R/W flag is expected
// to be cleared. In newer PIC18 devices, R/W remains set. Instead of testing
// this bit, the state machine tests the CKP bit, expecting it to be set
//
// note: AN734 assumes that these states are qualified with S=1 (START bit), but the NACK state
// can be an issue. It is not necessarily possible to catch the NAK state in an interrupt handler.
// SCL will not be held low because ACK=1, and the stop bit can occur just a few microseconds after
// the falling edge of bit 9's clock (which is what sets SSPIF), causing P to be set before you get
// a chance to see SSPIF. This condition can be seen by testing for SSPCON1.CKP==1 and ignoring S and P
// In general, probably don't really care about this state... just look for the STOP.
//
// addtl note: if using General Call (GC), then the state of R_W is not modified, and so it
// reflects whatever the PREVIOUS packet was! For whatever reason, this is "by design", perhaps
// since GC must always be a write. This means that the state machine would have to deal with
// this as a special case since R_W becomes a don't care.
//

// SSPSTAT I2C states mask... START, D/A, R/W, BF
public const I2C_STATE_MASK as byte = ( (1<<S) + (1<<D_A) + (1<<R_W) + (1<<BF) )

public const
    MWR_ADDR as byte = (1<<S) + (1<<BF),                 // Master Write Address  D/A=0, R/W=0, BF=1, (S=1)
    MWR_DATA as byte = (1<<S) + (1<<D_A) + (1<<BF),      // Master Write Data     D/A=1, R/W=0, BF=1, (S=1)
    MRD_ADDR as byte = (1<<S) + (1<<R_W) + (1<<BF),      // Master Read Address   D/A=0, R/W=1, BF=1, (S=1)
    MRD_DATA as byte = (1<<S) + (1<<D_A) + (1<<R_W),     // Master Read Data      D/A=1, R/W=1, BF=0, (S=1)
    M_NACK   as byte = (1<<D_A) + (1<<R_W)              // Master NACK last data byte (SSPCON1.CKP = 1)

//
// variables
//
public dim
    ssp_stat as byte,
    addr as byte,
    rxc as byte,
    rxb(64) as byte,
    rxix as byte,
    txc as byte,
    txix as byte

#if (ENA_I2C_STATE_TRACKING)
public dim
    stbuf(64) as byte,
    stix as byte
#endif

// pic18 nop
inline sub nop()
    asm
        NOP
    end asm
end sub
            
//
//------------------------------------------------------------------------------
// slave write byte
//------------------------------------------------------------------------------
//
public sub write_byte(b as byte)
    // send the data, making sure a write collision did not occur
    // if clock stretching, the clock will be released by the i2c_handler
    // code (CKP = 1)
    repeat
        SSPCON1.bits(WCOL) = 0
        SSPBUF = b
    until (SSPCON1.bits(WCOL) = 0)
end sub

//
//------------------------------------------------------------------------------
// slave read byte
//------------------------------------------------------------------------------
//
public function read_byte() as byte
    // check that overflow has not occured. since we're clock-stretching, this should
    // never happen but if it does we must clear the bit so reception can continue
    if (SSPCON1.bits(SSPOV) = 1) then
        SSPCON1.bits(SSPOV) = 0
    endif
    result = SSPBUF
end function

//
//------------------------------------------------------------------------------
// mssp I2C state handler
// example shell handler (over-ride with your own)
//------------------------------------------------------------------------------
//
public sub handler()
    dim state as byte

    if (SSPIF = 1) then
        SSPIF = 0                           // clear intr request
        ssp_stat = SSPSTAT                  // read status reg

      #if (ENA_I2C_STATE_TRACKING)
        stbuf(stix) = ssp_stat              // record state transitions
        inc(stix)
      #endif

        if (ssp_stat.bits(S) = 1) then      // most slave states require START to be set
            state = ssp_stat and I2C_STATE_MASK
            select (state)
                case MWR_ADDR               // I2C write operation, last byte was an address byte
                    addr = read_byte()          // get the addr byte (clears BF)
                    rxix = 0                    // reset data count
                case MWR_DATA               // I2C write operation, last byte was a data byte
                    rxc = read_byte()           // get the data byte (clears BF)
                    rxb(rxix) = rxc
                    rxix = rxix + 1
                case MRD_ADDR               // I2C read operation, last byte was an address byte
                    addr = read_byte()          // get the addr byte (clears BF)
                    write_byte(addr)            // send back the matched addr as the first byte
                    txix = 0
                case MRD_DATA               // I2C read operation, last byte was a data byte
                    if (txix >= rxix) then
                        txc = 0
                    else
                        txc = rxb(txix)
                        txix = txix + 1
                    endif
                    write_byte(txc)
                case M_NACK                 // Slave I2C logic reset by NACK from master
                    nop()                       // not currently implemented
                else                        // unknown state
                    nop()
            end select
        endif

        if (ssp_stat.bits(P) = 1) then      // STOP bit...
            addr = 0                        // slave idle
        endif

        SSPCON1.bits(CKP) = 1               // release SCL clock stretching
    endif
end sub

//
//------------------------------------------------------------------------------
// set address mask register
//------------------------------------------------------------------------------
//
public sub set_addrmask(mask as byte)
    // in 7-bit address masking mode the SSPMSK register is not directly addressable,
    // and must be accessed via the SSPCON1/SSPADD registers
    SSPCON1 = %1001                 // select SSPMSK access mode
    SSPMSK = mask                   // write the addr mask value
end sub

//
//------------------------------------------------------------------------------
// module initialization
//------------------------------------------------------------------------------
//
public sub init_buf()
    clear(rxb)
    rxix = 0
end sub

public sub init()
    // make sure pins are inputs
    input(SCL)
    input(SDA)

    // use the default 7-bit address masking mode (config MSSPMSK=1)
    set_addrmask(I2C_SLAVE_ADDR_MASK)

    'SSPCON1 = $0E               // I2C slave mode: 7-bit address with start and stop
    SSPCON1 = $06               // I2C slave mode: 7-bit address
    SSPCON2 = $00
    SSPCON2.bits(SEN) = 1       // enable clock stretching. disable general call

    SSPADD = I2C_SLAVE_ADDR     // 7 bits I2C slave

    SSPSTAT.bits(7) = 1         // Slew Rate Control: disabled for standard speed mode (100kHz and 1MHz)
    SSPSTAT.bits(6) = 1         // SMBus Select bit: Enable input logic thresholds to SMbus specification
    SSPCON1.bits(CKP) = 1       // release clock
    SSPCON1.bits(SSPEN) = 1     // enable MSSP

    init_buf()
    
  #if (ENA_I2C_STATE_TRACKING)
    clear(stbuf)
    stix = 0
  #endif

    // mssp polled mode
    BCLIE = 0
    SSPIE = 0
    BCLIF = 0
    SSPIF = 0
end sub

end module
example usage:

Code: Select all

device = 18F26K22

include "i2cslave.bas"

i2cslave.init()

while (true)
    i2cslave.handler()
end while        

User avatar
Coccoliso
Posts: 152
Joined: Mon Feb 17, 2014 10:34 am

Re: I2C slave for the K22

Post by Coccoliso » Mon Apr 20, 2015 5:50 pm

Hi Jerry,
now I have to go find the breadboard... :D

Jerry Messina
Swordfish Developer
Posts: 1469
Joined: Fri Jan 30, 2009 6:27 pm
Location: US

Re: I2C slave for the K22

Post by Jerry Messina » Tue Apr 21, 2015 11:38 am

Something you said on the firewing forum about having problems with some PIC18's got me thinking back.

I tried doing an I2C slave with a J series device (26J53 if I remember) and had a heck of a time getting it to work.
When I changed between write and read modes sometimes the R/W bit of the status reg and the lsb of the address byte
didn't match. Even though I was using clock stretching I found that I had to slow the master down by adding delays
in between all of the different transfer states.

I don't know if this was due to some errata or what the problem was, but in the end I could never get it reliable
enough and had to switch to a different interface scheme.

Post Reply