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