I2C Master Library for SDCC – Alpha

I’ve taken a break from assembly language programming to play with SDCC for PIC microcontrollers. As my first project, I decided to port an I2C Master implementation to C and evaluate the results. So far, I’ve come up with a working echo client and server and I feel as though I’m ready to share the very first iteration with anyone who would like to try it out. The SDCC implementation is master-only but can be used with existing slave firmware or devices that communicate using I2C. The relevant function calls that I use with my echo slave look like this:

i2cMasterStart(5);
result = i2cMasterWrite(0b10000000, (char*)&buffer, 4, 10);
if(result == _I2C_OK) {
   i2cMasterRepeatedStart(10);
   result = i2cMasterRead(0b10000000, (char*)&buffer, 4, 20);
} //write result ok
i2cMasterStop(5);

Please note the comments as they pertain to the “wait” parameters provided to each function. This value serves as a X10 instruction cycle modifier for each operation that could potentially block, such as ACK, waits for buffer full states during receives, etc. A larger value is usually not going to cause problems, but using too small a value may cause a timeout during a master read, for example, where conditions are ripe for such an occurance (clock stretching, etc).

The header and C are included below. Please leave any feedback or questions in the comments and please, be gentle 🙂

/*
 * File:   i2cMaster.h
 * Author: jcleland
 *
 *   This file is part of the PICLibI2C Project providing hardware I2C support
 *   Microchip PIC18 devices
 *
 *   Copyright (C) 2017  James A. Cleland
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#ifndef I2CMASTER_H
#define	I2CMASTER_H

#ifdef	__cplusplus
extern "C" {
#endif

#define _I2C_OK                     0
#define _I2C_RC_TIMEOUT             1
#define _I2C_RC_ACKTIMEOUT          2
#define _I2C_BUS_TIMEOUT            3
#define _I2C_RC_BFTIMEOUT           4
#define _I2C_START_TIMEOUT          5
#define _I2C_STOP_TIMEOUT           6
#define _I2C_IDLEWAIT_TIMEOUT       7
#define _I2C_ADDRACK_TIMEOUT        8
#define _I2C_TXIDLEWAIT_TIMEOUT     9
#define _I2C_TXDATAACK_TIMEOUT      10
#define _I2C_RXIDLEWAIT_TIMEOUT     11
#define _I2C_RXDATAACK_TIMEOUT      12

unsigned i2cMasterInit(unsigned long iFOSC);
unsigned i2cMasterStart(unsigned uWait);
unsigned i2cMasterStop(unsigned uWait);
unsigned i2cMasterRepeatedStart(unsigned uWait);
unsigned i2cMasterWrite(unsigned uiAddress, char* cBuffer, unsigned iCount, unsigned uWait);
unsigned i2cMasterRead(unsigned uiAddress, char* cBuffer, unsigned iCount, unsigned uWait);

#ifdef	__cplusplus
}
#endif

#endif	/* I2CMASTER_H */

 

/*
 * File:    i2cMaster.c
 * Author:    jcleland
 * Purpose:    This file contains the implementation of hardware master i2c for
 *            PIC18 devices.
 *
 *   This file is part of the PICLibI2C Project providing hardware I2C support
 *   Microchip PIC18 devices
 *
 *   Copyright (C) 2017  James A. Cleland
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

//Library includes
#include <pic18fregs.h>
#include <delay.h>

//Local includes
#include "i2cmaster.h"

//-----------------------------------------------------------------------------
//Declare global data

//.............................................................................
//Master status structure definition and global declaration (local to module))
typedef    struct {
        unsigned STOPPED            : 1;
        unsigned                    : 1;
        unsigned                    : 1;
        unsigned                    : 1;
        unsigned                    : 1;
        unsigned                    : 1;
        unsigned                    : 1;
        unsigned                    : 1;
} i2c_master_status_t;
i2c_master_status_t i2c_master_status = {0,0,0,0,0,0,0,0};

/*******************************************************************************
 * Function:        unsigned i2cIdleWait(unsigned)
 * Purpose:            Blocks until the bus is idle or until the specified timeout
 *                    expires. uWait is specified in tens of instruction cycles
 *                    and should be adjusted for oscillator frequency.
 *
 * Arguments:        unsigned uWait
 *                        X10 timeout multiplier for blocking operations
 *
 * Return:            _I2C_IDLEWAIT_TIMEOUT if the function times out waiting for
 *                    the bus to become idle, _I2C_OK if the the bus is idle.
 *
 */
unsigned i2cIdleWait(unsigned uWait) {
    //Declare locals
    unsigned mask;
    unsigned count;

    count = uWait;
    while(SSPSTATbits.R_W != 0) {
        delay10tcy(1);
        if(--count == 0) {
            i2cMasterStop(uWait);
            return _I2C_IDLEWAIT_TIMEOUT;
        };
    }

    mask = _SEN + _RSEN + _PEN + _RCEN + _ACKEN;
    count = uWait;
    while((mask & SSPCON2) != 0) {
        delay10tcy(1);
        if(--count == 0) {
            i2cMasterStop(uWait);
            return _I2C_IDLEWAIT_TIMEOUT;
        };
    }

    return _I2C_OK;
}

/*******************************************************************************
 * Function:        unsigned i2cAckWait(unsigned)
 * Purpose:            Waits for an ACK response from the slave device after
 *                    an address send or a write operation. For write operations,
 *                    this function will be called for each byte written as
 *                    the slave device is expected to drive the data line low
 *                    after receiving the byte of data.
 *
 * Arguments:        unsigned uWait
 *                        X10 timeout multiplier for blocking operations
 *
 * Return:            _I2C_RC_TIMEOUT if the function detects a NACK in response
 *                    to the byte that was sent, _I2C_OK if the slave device
 *                    ACKed the byte.
 *
 */
unsigned i2cAckWait(unsigned uWait) {
    unsigned count = uWait;
    while(SSPCON2bits.ACKSTAT == 1) {
        delay10tcy(1);
        if(--count == 0) {
            i2cMasterStop(uWait);
            delay10tcy(1);
            return _I2C_RC_TIMEOUT;
        };
    }
    return _I2C_OK;
}

/*******************************************************************************
 * Function:        unsigned i2cMasterInit(unsigned long)
 * Purpose:            Initializes the I2C peripheral device and sets the clock
 *                    speed to 400kbps. Note that some PIC18 devices such as the
 *                    PIC18F2620 don't calculate baud rate correctly using the
 *                    formula published in the datasheet and used below. Usually,
 *                    this doesn't cause problems with the operation of the bus
 *                    but the baud rate can be set manually AFTER calling this
 *                    function by setting SPADD to a value that is specified in
 *                    the device errata corresponding to the desired data rate.
 *
 * Arguments:        unsigned long iFOSC
 *                        The configured oscillator frequency in decimal format.
 *                        For example, if the device is configured to operate at
 *                        8MHz, then the value provided should be 8000000.
 *
 * Return:            This function always returns _I2C_OK
 *
 */
unsigned i2cMasterInit(unsigned long iFOSC) {
    //Setup tristate and port bits

    //Calculate clock value
    unsigned iClockValue = (((iFOSC/400000)/2)-1);

    SSPSTAT = 0;
    SSPCON1bits.SSPM3 = 1;        //I2C Master mode is b'xxxx1000'
    SSPCON1bits.SSPEN = 1;        //Enable transceiver
    SSPCON2bits.ACKSTAT = 1;
    SSPADD = iClockValue;

    return _I2C_OK;
}

/*******************************************************************************
 * Function:        unsigned i2cMasterStart(unsigned)
 * Purpose:            Signals a start condition on the bus after waiting for
 *                    the bus to become idle. Note that the uWait parameter
 *                    provided to this function is also passed to the bus idle
 *                    wait function, possibly resulting in delay longer than
 *                    the start condition timeout.
 *
 * Arguments:        unsigned uWait
 *                        X10 timeout multiplier for blocking operations
 *
 * Return:            _I2C_BUS_TIMEOUT if the bus doesn't become idle within the
 *                    allowed number of instruction cycles, _I2C_START_TIMEOUT if
 *                    the start condition doesn't complete within the timeout
 *                    period, and _I2C_OK if a start was successfully signaled.
 *
 */
unsigned i2cMasterStart(unsigned uWait) {
    //Declare locals
    unsigned count;

    //Wait for idle bus
    if(i2cIdleWait(uWait) != _I2C_OK) return _I2C_BUS_TIMEOUT;

    //Set start enable bit and wait for completion
    SSPCON2bits.SEN = 1;
    count = uWait;
    while(SSPCON2bits.SEN != 1) {
        delay10tcy(1);
        if(--count == 0) {
            i2cMasterStop(uWait);
            return _I2C_START_TIMEOUT;
        };
    };

    //Bus state is started
    i2c_master_status.STOPPED = 0;

    //For now, just return 0. This shouldn't fail
    return _I2C_OK;
}

/*******************************************************************************
 * Function:        unsigned i2cMasterStop(unsigned)
 * Purpose:            Signals a stop condition on the bus after waiting for
 *                    the bus to become idle. Note that the uWait parameter
 *                    provided to this function is also passed to the bus idle
 *                    wait function, possibly resulting in delay longer than
 *                    the stop condition timeout.
 *
 * Arguments:        unsigned uWait
 *                        X10 timeout multiplier for blocking operations
 *
 * Return:            _I2C_BUS_TIMEOUT if the bus doesn't become idle within the
 *                    allowed number of instruction cycles, _I2C_STOP_TIMEOUT if
 *                    the stop condition doesn't complete within the timeout
 *                    period, and _I2C_OK if a stop was successfully signaled.
 *
 */
unsigned i2cMasterStop(unsigned uWait) {
    //Declare locals
    unsigned count = 0;

    //Check to see if we are already stopped
    if(i2c_master_status.STOPPED == 0) {
        //Wait for idle bus
        if(i2cIdleWait(uWait) != _I2C_OK) return _I2C_BUS_TIMEOUT;

        //Set start enable bit and wait for completion
        SSPCON2bits.PEN = 1;
        count = uWait;
        while(SSPCON2bits.PEN != 1) {
            delay10tcy(1);
            if(--count == 0) {
                return _I2C_STOP_TIMEOUT;
            };
        }

        //Set stopped bit
        i2c_master_status.STOPPED = 1;
    }; //If already stopped

    //Return valid state
    return _I2C_OK;
}

/*******************************************************************************
 * Function:        unsigned i2cMasterRepeatedStart(unsigned)
 * Purpose:            Signals a repeated start condition on the bus after waiting
 *                    for the bus to become idle. Note that the uWait parameter
 *                    provided to this function is also passed to the bus idle
 *                    wait function, possibly resulting in delay longer than
 *                    the repeated start condition timeout.
 *
 * Arguments:        unsigned uWait
 *                        X10 timeout multiplier for blocking operations
 *
 * Return:            _I2C_BUS_TIMEOUT if the bus doesn't become idle within the
 *                    allowed number of instruction cycles,
 *                    _I2C_REPEATEDSTART_TIMEOUT if the repeated start condition
 *                    doesn't complete within the timeout    period, and _I2C_OK if
 *                    a repeated start was successfully signaled.
 *
 */
unsigned i2cMasterRepeatedStart(unsigned uWait) {
    //Declare locals
    unsigned count = 0;

    //Check to see if we are already stopped
    if(i2c_master_status.STOPPED == 0) {
        //Wait for idle bus
        if(i2cIdleWait(uWait) != _I2C_OK) return _I2C_BUS_TIMEOUT;

        //Set start enable bit and wait for completion
        SSPCON2bits.RSEN = 1;
        while(SSPCON2bits.RSEN != 1) {
            delay10tcy(1);
            if(--count == 0) {
                SSPCON2bits.RCEN = 0;
                return _I2C_REPEATEDSTART_TIMEOUT;
            };
        }
    }
    //For now, just return 0. This shouldn't fail
    return _I2C_OK;
}

/*******************************************************************************
 * Function:        unsigned i2cMasterWrite(unsigned, char*, unsigned, unsigned)
 * Purpose:            Writes an arbitrary number of bytes to the specified slave
 *                    device.
 *
 * Arguments:        unsigned uiAddress
 *                        The address of the slave device to which data will be
 *                        written.
 *                    char* cBuffer
 *                        A pointer to the buffer of data to be written to the
 *                        specified slave device.
 *                    unsigned iCount
 *                        The number of bytes to be written to the slave device
 *                        beginning with cBuffer.
 *                    unsigned uWait
 *                        X10 timeout multiplier for blocking operations
 *
 * Return:            _I2C_TXIDLEWAIT_TIMEOUT if the bus doesn't become idle
 *                    within the allowed number of instruction cycles,
 *                    _I2C_ADDRACK_TIMEOUT if the slave address is NACKed,
 *                    _I2C_TXDATAACK_TIMEOUT if any byte of data sent to the
 *                    slave device is NACKed, or _I2C_OK if all bytes are sent
 *                    successfully.
 *
 */
unsigned i2cMasterWrite(unsigned uiAddress, char* cBuffer, unsigned iCount,
    unsigned uWait)
{
    //Declare locals
    char index = 0;

    //Clear the LSb of the address for write
    uiAddress = (uiAddress & 0b11111110);

    //Wait for idle bus
    if(i2cIdleWait(uWait) != _I2C_OK) return _I2C_TXIDLEWAIT_TIMEOUT;

    //We want acknowledge
    SSPCON2 = 0;
    SSPCON2bits.ACKSTAT = 1;
    SSPBUF = uiAddress;

    //Wait for ACK
    if(i2cAckWait(uWait) != _I2C_OK) return _I2C_ADDRACK_TIMEOUT;

    //Loop through all data
    for(index = 0; index < iCount; index++) {
        //Wait for idle bus
        if(i2cIdleWait(uWait) != _I2C_OK) return _I2C_TXIDLEWAIT_TIMEOUT;

        //Clear the status register
        SSPCON2 = 0;
        SSPCON2bits.ACKSTAT = 1;
        SSPBUF = *(cBuffer+index);

        //Wait for ACK
        if(i2cAckWait(1) != _I2C_OK) return _I2C_TXDATAACK_TIMEOUT;
    };
    
    return _I2C_OK;
}

/*******************************************************************************
 * Function:        unsigned i2cMasterRead(unsigned, char*, unsigned, unsigned)
 * Purpose:            Writes an arbitrary number of bytes to the specified slave
 *                    device.
 *
 * Arguments:        unsigned uiAddress
 *                        The address of the slave device to which data will be
 *                        written.
 *                    char* cBuffer
 *                        A pointer to the buffer to contain the data read from
 *                        the slave device.
 *                    unsigned iCount
 *                        The number of bytes to be read from the slave device
 *                        and returned in the data pointed to by cBuffer
 *                    unsigned uWait
 *                        X10 timeout multiplier for blocking operations
 *
 * Return:            _I2C_RXIDLEWAIT_TIMEOUT
 *                        if the bus doesn't become idle within the allowed number
 *                        of instruction cycles
 *                    _I2C_ADDRACK_TIMEOUT
 *                        if the slave address is NACKed
 *                    _I2C_TXDATAACK_TIMEOUT
 *                        if any byte of data sent to the slave device is NACKed
 *                    _I2C_RC_TIMEOUT
 *                        Timeout waiting for the receiver to become active
 *                    _I2C_RC_BFTIMEOUT
 *                        Timeout waiting for a buffer full condition while
 *                        attempting to read a byte of data
 *                    _I2C_RXDATAACK_TIMEOUT
 *                        Timeout waiting for the ACK to complete
 *                    _I2C_OK
 *                        all bytes were sent successfully.
 *
 */
unsigned i2cMasterRead(unsigned uiAddress, char* cBuffer, unsigned iCount,
    unsigned uWait)
{
    //Declare locals
    char index = 0;
    unsigned count = 0;

    //Set the LSb of the address for write
    uiAddress = (uiAddress | 0b00000001);

    //Wait for idle bus
    if(i2cIdleWait(uWait) != _I2C_OK) return _I2C_RXIDLEWAIT_TIMEOUT;

    //We want acknowledge
    SSPCON2 = 0;
    SSPCON2bits.ACKSTAT = 1;
    SSPBUF = uiAddress;

    //Wait for ACK
    if(i2cAckWait(uWait) != _I2C_OK) return _I2C_ADDRACK_TIMEOUT;

    //Loop through all data
    for(index = 0; index < iCount; index++) {

        if(i2cIdleWait(uWait) != _I2C_OK) return _I2C_RXIDLEWAIT_TIMEOUT;

        //Enable receiver
        SSPCON2bits.RCEN = 1;
        count = uWait;
        while(SSPCON2bits.RCEN == 1){
            delay10tcy(1);
            if(--count == 0) {
                return _I2C_RC_TIMEOUT;
            };
        };

        //Wait for buffer full
        count = uWait;
        while(SSPSTATbits.BF == 0) {
            delay10tcy(1);
            if(--count == 0) {
                //Send stop condition
                return _I2C_RC_BFTIMEOUT;
            }
        }

        *(cBuffer+index) = SSPBUF;

        //Wait for buffer full
        count = uWait;
        while(SSPCON2bits.ACKEN == 1) {
            delay10tcy(1);
            if(--count == 0) {
                //Send stop condition
                return _I2C_RXDATAACK_TIMEOUT;
            }
        }

        if(index == iCount-1) {
            SSPCON2bits.ACKDT = 1;
            SSPBUF = 0;
            SSPCON2bits.RCEN = 0;
        }
        else {
            SSPCON2bits.ACKDT = 0;
        }
        SSPCON2bits.ACKEN = 1;


    }; //While bytes to read

    return _I2C_OK;
}

 

Leave a Reply

Your email address will not be published. Required fields are marked *

© Zillow, Inc., 2006-2016. Use is subject to Terms of Use
What's a Zestimate?