PSI2C

SwordfishUser.PSI2C History

Show minor edits - Show changes to output

Added lines 1-641:
This module allows for the creation of multiple software I2C ports.
To use it you must first define the pins used for the SCL and SDA signals
by calling the the SetSDA() and SetSCL() routines. After that, calling
Initialize() will setup the pin hardware and init the pins to the idle state.

The SCL and SDA pins can be allocated to any port pins. The SDA line will require the use of a pullup resistor since it is used as a bidirectional open-drain pin.
The SCL output pin is actively driven in both the high and low states, so if the slave device supports clock stretching you will have to modify the SCL code to function in open-drain fashion.


!!! Sample Code (single port)
=code [=
include "PSI2C.bas"

// define I2C pins and init hdw
PSI2C.SetSDA(PORTB.0)
PSI2C.SetSCL(PORTB.1)
PSI2C.Initialize()

// You can now call Start(), Stop(), WriteByte(), ReadByte(), etc
PSI2C.Start()
PSI2C.WriteByte($55)
PSI2C.Stop()

=]

The number of ports and the I2C clock frequency are set by '#option' statements
'#option PSI2C_NUM_PORTS' and '#option PSI2C_CLOCK'. Note that because of the programmable nature of the pins
there is significant overhead, so the actual SCL clock frequency will be much lower than the setting.

If you wish to use multiple ports you must call SavePort(x) after the pins
are defined to save the port pin definitions, and call SelectPort(x) prior to
changing ports. Once you've selected a port all operations go to that port
until you select another. You can skip both these routines if you're only
defining a single port (but then you might be better off just using the SI2C.bas module)

!!! Sample Code (multiple ports)
=code [=
#option PSI2C_NUM_PORTS = 2    // use 2 I2C ports
#option PSI2C_CLOCK = 400 // 400KHz
include "PSI2C.bas"

PSI2C.SetSDA(PORTB.0)      // define pins and init port
PSI2C.SetSCL(PORTB.1)
PSI2C.Initialize()
PSI2C.SavePort(1)          // save current as Port 1

PSI2C.SetSDA(PORTC.3)      // define pins and init port
PSI2C.SetSCL(PORTC.4)
PSI2C.Initialize()
PSI2C.SavePort(2)          // save current as Port 2

// to use the ports:
PSI2C.SelectPort(1)        // send using port 1...
PSI2C.Start()
PSI2C.WriteByte($55)
PSI2C.Stop()

PSI2C.SelectPort(2)        // send using port 2...
PSI2C.Start()
PSI2C.WriteByte($55)
PSI2C.Stop()

=]

!!! PSI2C Module
=code [=
{
*****************************************************************************
*  Name    : PSI2C Library (Multi-port Pin Programmable Software I2C)      *
*  Author  : Jerry Messina (based on David John Barker's original SI2C)    *
*  Date    : 06/24/14                                                      *
*  Version : 1.1                                                            *
*  Notes  : 1.0 Initial release                                            *
*          : 1.1 change Initialize() and remove inline MakeSdaInput()      *
*****************************************************************************
}
module PSI2C

//
// Usage:
// This module allows for the creation of multiple software I2C ports.
// To use it you must first define the pins used for the SCL and SDA signals
// by calling the the SetSDA() and SetSCL() routines. After that, calling
// Initialize() will setup the pin hardware and init the pins to the idle state.
//
// If you wish to use multiple ports then you must call SavePort(x) after the pins
// are defined to save the port pin definitions, and call SelectPort(x) prior to
// changing ports. Once you've selected a port all operations go to that port
// until you select another. You can skip both these routines if you're only
// defining a single port.
//
// For example, to use a single port you would use:
//      PSI2C.SetSDA(PORTB.0)
//      PSI2C.SetSCL(PORTB.1)
//      PSI2C.Initialize()
// You can now call Start(), Stop(), WriteByte(), ReadByte(), etc like normal
//
// If you're using multiple ports you need a few addtl steps:
//      #option PSI2C_NUM_PORTS = 2    // use 2 I2C ports
//      include "PSI2C.bas"
//
//      PSI2C.SetSDA(PORTB.0)      // define and init port
//      PSI2C.SetSCL(PORTB.1)
//      PSI2C.Initialize()
//      PSI2C.SavePort(1)          // save current as Port 1
//
//      PSI2C.SetSDA(PORTC.3)      // define and init port
//      PSI2C.SetSCL(PORTC.4)
//      PSI2C.Initialize()
//      PSI2C.SavePort(2)          // save current as Port 2
//
// Then, to use the ports:
//      PSI2C.SelectPort(1)        // send using port 1...
//      PSI2C.Start()
//      PSI2C.WriteByte($55)
//      PSI2C.Stop()
//
//      PSI2C.SelectPort(2)        // send using port 2...
//      PSI2C.Start()
//      PSI2C.WriteByte($55)
//      PSI2C.Stop()
//
// Multiple ports rely on the SelectPort() routine to change the underlying
// data structures, and usage of the FSR0 and FSR1 registers to perform indirect
// operations. They should NOT be used in interrupt routines without a deep
// understanding of how they operate. Having said that, using ANY I2C routines
// inside an ISR is probably a bad idea in the first place
//

// max number of supported I2C ports
#option PSI2C_NUM_PORTS = 1
public const NUM_PORTS = PSI2C_NUM_PORTS
#if (PSI2C_NUM_PORTS > 1)
  const ARRAY_SIZE = NUM_PORTS + 1    // reserve addtl space for current port
#else
  const ARRAY_SIZE = 1
#endif

// select default SCL clock freq (100/400), in KHz
// note: since the ports are bit-banged using programmable pins you
// won't reach near these speeds because of all the overhead
// for example, with a system clock=64MHz the 400KHz setting ends up being
// about 240KHz, and with a system clock=8MHz it ends up < 100KHz
// at 64MHz and 400KHz, a start-write-stop transaction takes almost 50us
#option PSI2C_CLOCK = 100

#if (PSI2C_CLOCK = 100)
  const CLOCK_DELAY = 5        // usecs
#elseif (PSI2C_CLOCK = 400)
  const CLOCK_DELAY = 1        // usecs
#else
  #error "unsupported I2C clock selection"
#endif

// a pin structure...
public structure TPin
    PortAddr as word
    Pin      as byte
    PinMask  as byte
end structure 

// each I2C port requires two TPin structures... one for SCL and one for SDA
structure TI2C_Port
    Scl as TPin
    Sda as TPin
end structure

// array of I2C port assignments
// Ports(0) always holds the current port (the one currently in use)
// if you have NUM_PORTS=1 then this array only has a single Ports(0) element.
// otherwise, there are NUM_PORTS+1 elements and you have to swap the desired element
// into Ports(0) when you want to access it by using the SelectPort(x) routine
dim Ports(ARRAY_SIZE) as TI2C_Port

// index of currently active port (mainly for debugging)
dim Portno as byte

// simple alias names for the current Port data structures
dim
    FSCL as Ports(0).Scl,
    FSDA as Ports(0).Sda

// reg addr offsets from the PORT register
const
    LAT_OFFSET = 9,
    TRIS_OFFSET = 18
   
// SI2C constants...
public const
    I2C_ACKNOWLEDGE = 0,      // acknowledge data bit (acknowledge)
    I2C_NOT_ACKNOWLEDGE = 1    // acknowledge data bit (NOT acknowledge)

// This flag holds the status of WriteByte operations. If the slave receiver
// acknowledged the transfer (bit 9 NACK=0) then NotAcknowledged=false.
// If the receiver fails to ack the transfer (bit 9 NACK=1) this means the
// slave is busy (or isn't present) and NotAcknowledged=true. You should
// resend the byte
public dim
    NotAcknowledged as boolean


{
****************************************************************************
* SDA pin functions  (PRIVATE)                                            *
****************************************************************************
}
// The SDA line is used in 'open-drain' mode so it requires the use of a
// pullup resistor. Some PIC devices have provisions for using a pin in
// open-drain operation, but most don't. Here we simulate an open-drain by
// setting the pin LAT register to 0 and using the TRIS register to change
// the pin direction. To write a '0' set the TRIS register to output mode
// (which will actively assert the pin low), and to write a '1' set the TRIS
// register to input mode... the pin will go high due to the pullup.
//

// load FSR0 with PORT address (used to read PORT pin)
inline sub LoadSdaPortAddr()
    FSR0 = FSDA.PortAddr        // PORT address
end sub 

// load FSR0 with TRIS address (used to manipulate pin dir)
inline sub LoadSdaAddr()
    FSR0 = FSDA.PortAddr + TRIS_OFFSET      // TRIS address
end sub 

// set LATx bit low (for SetSdaLow routine)
inline sub ClearSdaLatch()
    FSR0 = FSDA.PortAddr + LAT_OFFSET      // LAT address
    INDF0 = INDF0 and FSDA.PinMask          // make sure LATx bit is low
end sub

// float pin/high state
sub MakeSdaInput()
    LoadSdaAddr()
    INDF0 = INDF0 or FSDA.Pin
end sub

// enables output pin... outputs current LAT contents
inline sub MakeSdaOutput()
    LoadSdaAddr()
    INDF0 = INDF0 and FSDA.PinMask
end sub

// SdaHigh()/SdaLow() assumes FSR0 contains TRIS reg addr, and the LAT reg=0
inline sub SdaHigh()
    INDF0 = INDF0 or FSDA.Pin      // set TRIS bit (input/float)
end sub

inline sub SdaLow()
    INDF0 = INDF0 and FSDA.PinMask  // clear TRIS bit (outputs LAT contents)
end sub

// read port pin (assumes MakeSdaInput() and LoadSdaPortAddr() called)
function SdaIn() as bit
    if ((INDF0 and FSDA.Pin) = 0) then
        result = 0
    else
        result = 1
    endif
end function

{
****************************************************************************
* Name    : SetSDA                                                        *
* Purpose : Sets the I2C SDA data pin structure                            *
****************************************************************************
}
public sub SetSDA(byref pDataPin as bit)
    FSDA.PortAddr = addressof(pDataPin)    // point address to port
    FSDA.Pin = bitof(pDataPin)
    FSDA.PinMask = not FSDA.Pin
end sub

{
****************************************************************************
* Name    : SetSDA/GetSDA                                                  *
* Purpose : access SDA TPin current data structure                        *
****************************************************************************
}
public sub SetSDA(byref pSDA as TPin)
    FSDA = pSDA
end sub

public function GetSDA() as FSDA
end function

{
****************************************************************************
* SCL pin functions  (PRIVATE)                                            *
****************************************************************************
}
// note: these routines drive the I2C SCL pin in push/pull mode (instead of
// open-collector style) so technically it's not 100% I2C compliant. This
// is normally only an issue if you have a slave device that supports clock
// stretching, in which case you'll have to change these routines to function
// like the SDA routines do. For most simple devices (like EEPROMS) the SCL
// pin is input-only and they don't use clock stretching, but you should
// verify this with the device datasheet.
//

// load FSR1 with the LAT reg addr
inline sub LoadSclAddr()
    FSR1 = FSCL.PortAddr + LAT_OFFSET    // load LAT address
end sub

// enables output pin... outputs current LAT contents
inline sub MakeSclOutput()
    FSR1 = FSCL.PortAddr + TRIS_OFFSET
    INDF1 = INDF1 and FSCL.PinMask
end sub

// SclHigh()/SclLow() assumes FSR1 contains LAT reg addr ie LoadSclAddr()
inline sub SclHigh()
    INDF1 = INDF1 or FSCL.Pin
end sub

inline sub SclLow()
    INDF1 = INDF1 and FSCL.PinMask
end sub

{
****************************************************************************
* Name    : SetSCL                                                        *
* Purpose : Sets the I2C SCL clock pin structure                          *
****************************************************************************
}
public sub SetSCL(byref pClockPin as bit)
    FSCL.PortAddr = addressof(pClockPin)    // point address to PORT
    FSCL.Pin = bitof(pClockPin)
    FSCL.PinMask = not FSCL.Pin
end sub

{
****************************************************************************
* Name    : SetSCL/GetSCL                                                  *
* Purpose : access SDA TPin current data structure                        *
****************************************************************************
}
public sub SetSCL(byref pSCL as TPin)
    FSCL = pSCL
end sub

public function GetSCL() as FSCL
end function

{
****************************************************************************
* Name    : SetPortAccess (PRIVATE)                                        *
* Purpose : load FSR0/FSR1 to access hardware                              *
****************************************************************************
}
sub SetPortAccess()
    LoadSclAddr()              // set FSR0 and FSR1 to access pins
    LoadSdaAddr()
end sub

{
****************************************************************************
* Name    : Delay (PRIVATE)                                                *
* Purpose : Delay a fixed number of microseconds                          *
****************************************************************************
}
inline sub Delay()
    delayus(CLOCK_DELAY)
end sub

{
****************************************************************************
* Name    : DataSetupDelay (PRIVATE)                                      *
* Purpose : Delay for SDA to settle prior to SCL                          *
****************************************************************************
}
// If you have a slow risetime on the SDA signal, you may need to provide
// a chance for the signal to slew before SCL transitions high. The I2C spec
// requires at least 250ns after the signal reaches the 3V level. This time
// is very hardware dependant (pullup resistor values, bus capacitance, etc)
// If your risetimes are fast enough, you can comment out this delay.
// If you're using internal pullups, this delay might need to be increased
//
inline sub DataSetupDelay()
    delayus(1)              // comment this out if not required
end sub

{
****************************************************************************
* Name    : ToggleClock (PRIVATE)                                          *
* Purpose : Clock the I2C bus SCL clock                                    *
****************************************************************************
}
sub ToggleClock()
    SclHigh()
    Delay()
    SclLow()
    Delay()
end sub

{
****************************************************************************
* Name    : ShiftOut (PRIVATE)                                            *
* Purpose : Shift out a byte value, MSB first                              *
****************************************************************************
}
sub ShiftOut(pData as byte)
    dim Index as byte

    Index = 8
    repeat
        if (pData.bits(7) = 0) then
            SdaLow()
        else
            SdaHigh()
        endif
        DataSetupDelay()
        ToggleClock()
        pData = pData << 1
        dec(Index)
    until (Index = 0)
end sub

{
****************************************************************************
* Name    : ShiftIn (PRIVATE)                                              *
* Purpose : Shift in a byte value, MSB first, sample before clock          *
****************************************************************************
}
function ShiftIn() as byte
    dim Index as byte

    Index = 8
    MakeSdaInput()
    LoadSdaPortAddr()
    Result = 0
    repeat
        Result = Result << 1
        if (SdaIn() = 1) then
            Result.bits(0) = 1
        endif
        ToggleClock()
        dec(Index)
    until (Index = 0)
    MakeSdaInput()
end function

{
****************************************************************************
* Name    : Initialize                                                    *
* Purpose : Initialize I2C bus. Assumes that SetSDA and SetSCL are called  *
****************************************************************************
}
public sub Initialize()
    // init SDA (input...float high)
    ClearSdaLatch()            // load output latch with '0'
    MakeSdaInput()              // make data pin an input (SDA requires a pullup)

    // init SCL (output high)
    LoadSclAddr()              // set clock LAT high...
    SclHigh()
    MakeSclOutput()            // now, make clock pin an output...
    LoadSclAddr()              // leave fsr pointing to the LAT reg
end sub

{
****************************************************************************
* Name    : Start                                                          *
* Purpose : Send an I2C bus start condition. A start condition is HIGH to  *
*        : LOW of SDA line when the clock is HIGH                        *
****************************************************************************
}
public sub Start()
    SetPortAccess()    // set FSR0 and FSR1 so we can access pins

    SdaHigh()          // make sure data and clock are high...
    Delay()
    SclHigh()
    Delay()

    SdaLow()            // data high to low while clock high
    Delay()
    SclLow()            // and set clock low
    Delay()
end sub

{
****************************************************************************
* Name    : Stop                                                          *
* Purpose : Send an I2C bus stop condition. A stop condition is LOW to    *
*        : HIGH of SDA when line when the clock is HIGH                  *
****************************************************************************
}
public sub Stop()
    SetPortAccess()    // set FSR0 and FSR1 so we can access pins

    SdaLow()            // make sure SDA is low...
    Delay()

    SclHigh()          // set clock high
    Delay()
    SdaHigh()          // data low to high while clock high
    Delay()
end sub

{
****************************************************************************
* Name    : Restart                                                        *
* Purpose : Send an I2C bus restart condition                              *
****************************************************************************
}
public inline sub Restart()
    Start()
end sub

{
****************************************************************************
* Name    : Acknowledge                                                    *
* Purpose : Initiate I2C acknowledge                                      *
*        : pAck = 1, NOT acknowledge                                      *
*        : pAck = 0, acknowledge                                          *
****************************************************************************
}
public sub Acknowledge(pAck as bit = I2C_ACKNOWLEDGE)
    SetPortAccess()        // set FSR0 and FSR1 so we can access pins
    if (pAck = 0) then
        SdaLow()
    else
        SdaHigh()
    endif       
    DataSetupDelay()
    ToggleClock()
end sub

{
****************************************************************************
* Name    : ReadByte (OVERLOAD)                                            *
* Purpose : Read a single byte from the I2C bus                            *
****************************************************************************
}
public function ReadByte() as byte
    SetPortAccess()        // set FSR0 and FSR1 so we can access pins
    Result = ShiftIn()
end function

{
****************************************************************************
* Name    : ReadByte (OVERLOAD)                                            *
* Purpose : Read a single byte from the I2C bus, with Acknowledge          *
****************************************************************************
}
public function ReadByte(pAck as bit) as byte
    Result = ReadByte()
    Acknowledge(pAck)
end function

{
****************************************************************************
* Name    : WriteByte                                                      *
* Purpose : Write a single byte to the I2C bus                            *
****************************************************************************
}
public sub WriteByte(pData as byte)
    SetPortAccess()        // set FSR0 and FSR1 so we can access pins
    ShiftOut(pData)

    // look for ack...
    MakeSdaInput()
    DataSetupDelay()
    LoadSdaPortAddr()
    SclHigh()
    Delay()
    if (SdaIn() = 0) then
        NotAcknowledged = false
    else       
        NotAcknowledged = true
    endif
    SclLow()
    MakeSdaInput()
end sub

{
****************************************************************************
* Name    : WaitForWrite                                                  *
* Purpose : Wait until write sequence has completed (or timeout)          *
****************************************************************************
}
public sub WaitForWrite(pControl as byte)
    dim Timeout as byte
    Timeout = $FF
         
    Start()
    WriteByte(pControl) 
    while NotAcknowledged and (Timeout > 0)
        Restart()
        WriteByte(pControl) 
        dec(Timeout)
    end while
    Stop()
end sub

{
****************************************************************************
* Name    : SavePort                                                      *
* Purpose : copy current port definitions to array                        *
****************************************************************************
}
public sub SavePort(pIndex as byte)
    if (pIndex < ARRAY_SIZE) then
        Ports(pIndex) = Ports(0)
    endif       
end sub

{
****************************************************************************
* Name    : SelectPort (1 to NUM_PORTS)                                    *
* Purpose : Load FSDA/FSCL (the current port) from array                  *
****************************************************************************
}
public sub SelectPort(pIndex as byte)
    if (pIndex < ARRAY_SIZE) then
        Ports(0) = Ports(pIndex)    // copy saved port to current
        Portno = pIndex            // set current portno
        SetPortAccess()            // set FSR0 and FSR1 to access pins
    endif       
end sub

{
****************************************************************************
* Name    : CurrentPort                                                    *
* Purpose : returns currently selected port number                        *
****************************************************************************
}
public function CurrentPort() as byte
    Result = Portno
end function
   
//
// module initialization code
//
Portno = 0

end module

=]