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;
}