2.2 Bare Metal Code

The generalized code and functions to get the MSSP peripheral working is shown in this section. At the end there are presented multiple examples on how this code can be built upon to basic working applications.

Basic System Initialization

The first step will be to configure the microcontroller to disable the Watchdog Timer and to enable the Low-Voltage Programming (LVP).

#pragma config WDTE = OFF     /* WDT operating mode → WDT Disabled */ 
#pragma config LVP = ON       /* Low-voltage programming enabled, RE3 pin is MCLR */ 

Define the _XTAL_FREQ to the clock frequency and include the used libraries.

#define _XTAL_FREQ                  64000000UL
#include <pic18.h>
#include <xc.h>
#include <stdint.h>

The CLK_Initialize function selects the oscillator and the clock divider, and sets the nominal frequency:

static void CLK_Initialize(void)
{
    /* Set Oscillator Source: HFINTOSC and Set Clock Divider: 1 */
    OSCCON1bits.NOSC = 0x6;

    /* Set Nominal Freq: 64 MHz */
    OSCFRQbits.FRQ3 = 1;
}

I2C Pin Initialization

The PPS_Initialize function routes the SCL to pin RB1 and SDA to pin RB2:

static void PPS_Initialize(void)
{
    /* PPS setting for using RB1 as SCL */
    SSP1CLKPPS = 0x09;
    RB1PPS = 0x0F;

    /* PPS setting for using RB2 as SDA */
    SSP1DATPPS = 0x0A;
    RB2PPS = 0x10;
}

The PORT_Initialize function sets pins, RB1 and RB2, as digital pins with internal pull-up resistors and in open-drain mode:

static void PORT_Initialize(void)
{
    /* Set pins RB1 and RB2 as Digital */
    ANSELBbits.ANSELB1 = 0;
    ANSELBbits.ANSELB2 = 0;
    
    /* Set pull-up resistors for RB1 and RB2 */
    WPUBbits.WPUB1 = 1;
    WPUBbits.WPUB2 = 1;

    /* Set open-drain mode for RB1 and RB2 */
    ODCONBbits.ODCB1 = 1;
    ODCONBbits.ODCB2 = 1;
}

I2C Peripheral Initialization

The I2C1_Initialize function selects the I2C Host mode and sets the I2C clock frequency to 100 kHz:

static void I2C1_Initialize(void)
{
    /* I2C Host Mode: Clock = F_OSC / (4 * (SSP1ADD + 1)) */
    SSP1CON1bits.SSPM3 = 1;
    
    /* Set the baud rate divider to obtain the I2C clock at 100000 Hz*/
    SSP1ADD  = 0x9F;
}

The I2C1_interruptFlagPolling function waits for the SSP1IF flag to be triggered by the hardware and clears it:

static void I2C1_interruptFlagPolling(void)
{
    /* Polling Interrupt Flag */
    
    while (!PIR3bits.SSP1IF)
    {
        ;
    }

    /* Clear the Interrupt Flag */
    PIR3bits.SSP1IF = 0;
}

Open the I2C Bus for the Communication

The I2C1_open function, opens the I2C bus for read/write communication with a client on the bus. Resets the I2C interrupt flag SSP1IF, indicating the host is ready for the next data byte to send/receive. Enables the SSP1 module:

static void I2C1_open(void)
{
    /* Clear IRQ */
    PIR3bits.SSP1IF = 0;

    /* I2C Host Open */
    SSP1CON1bits.SSPEN = 1;
}

Closing the I2C Bus

The I2C1_close function disables the SSP1 module:

static void I2C1_close(void)
{
    /* Disable I2C1 */
    SSP1CON1bits.SSPEN = 0;
}

Start Condition

The I2C1_startCondition function sends the Start condition by setting the SEN bit and waits for the SSP1IF flag to be triggered:

static void I2C1_startCondition(void)
{
    /* Start Condition*/
    SSP1CON2bits.SEN = 1;
    I2C1_interruptFlagPolling();
}

Stop Condition

The I2C1_stopCondition function sends the Stop condition and waits for the SSP1IF flag to be triggered:

static void I2C1_stopCondition(void)
{
    /* Stop Condition */
    SSP1CON2bits.PEN = 1;
    I2C1_interruptFlagPolling();
}

Send Data to Client

The I2C1_sendData function loads in SSP1BUF the argument value and waits for the SSP1IF flag to be triggered:

static void I2C1_sendData(uint8_t byte)
{
    SSP1BUF  = byte;
    I2C1_interruptFlagPolling();
}

Set Host to Receive

The I2C1_setReceiveMode function sets the receive enable bit in SSP1CON2, which starts the clock signal so that the client can transmit data:

static void I2C1_setReceiveMode(void)
{
    /* Start receiving mode */
    SSP1CON2bits.RCEN = 1;
}

Read Data From Client

The I2C1_readData function loads the value from the SSP1BUF into and variable and waits for the SSP1IF flag to be triggered:

static uint8_t I2C1_readData(void)
{
    I2C1_interruptFlagPolling();
    uint8_t data = SSP1BUF;
    return data;
}

Poll ACK Received From Client

The I2C1_getAckstatBit function returns the ACKSTAT bit from the SSP1CON2 register. This is used to see if the Client responded with an ACK or NACK:

static uint8_t I2C1_getAckstatBit(void)
{
    /* Return ACKSTAT bit */
    return SSP1CON2bits.ACKSTAT;
}

Send ACK to Client

The I2C1_sendAcknowledge function sets the acknowledge bit and sends the ACK to the client device:

static void I2C1_sendAcknowledge(void)
{
    /* Send ACK bit to client */
    SSP1CON2bits.ACKDT = 0;
    SSP1CON2bits.ACKEN = 1;
    I2C1_interruptFlagPolling();
}

Send NACK to Client

The I2C1_sendNotAcknowledge function clears the acknowledge bit and sends the NACK to the client device:

static void I2C1_sendNotAcknowledge(void)
{
    /* Send NACK bit to client */
    SSP1CON2bits.ACKDT = 1;
    SSP1CON2bits.ACKEN = 1;
    I2C1_interruptFlagPolling();
}

Bare Metal Example Codes

The following code examples are build upon the above code.

I2C Use Cases

Use Case Description

GPIO 8-bit I/O Expander

Setting up the I2C Client GPIO 8-bit I/O expander (MCP23008) as output and blinking its LEDs by writing 1 byte of data repeatedly.

10-bit DAC

Setting up the I2C Client 10-bit DAC (TC1321) and toggling its output between 0v and 2.5v by writing 2 bytes of data at a time.

9-12 bit Temperature Sensor

Setting up the I2C Client temperature sensor (MCP9800) resolution to 12-bits, then repeatedly read out its value.

EEPROM

Setting up the I2C Client EEPROM (24LC02B), storing a test set of data onto it, and repeatedly reading it back to the user. Using EUSART to display the data to the user.

GPIO 8-Bit I/O Expander

In this use case, the microcontroller is configured in I2C Host mode using the MSSP1 instance of the MSSP peripheral, and communicates with the client MCP23008, an 8-bit I/O expander that can be controlled through the I2C interface.

The extended pins are set as digital output with an I2C write operation in the client's I/O Direction (IODIR) register.

After the pins are set, the program will repeatedly:

  • Set pins to digital low, with an I2C write operation in the GPIO register.
  • Set pins to digital high, with an I2C write operation in the GPIO register.

The I2C1_write1ByteRegister function executes all the steps to write one byte to the client:

static void I2C1_write1ByteRegister(uint8_t address, uint8_t reg, uint8_t data)
{
    /* Shift the 7-bit address and add a 0 bit to indicate a write operation */
    uint8_t writeAddress = (address << 1) & ~I2C_RW_BIT;
    
    I2C1_open();
    I2C1_start();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        return ;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        return ;
    }
    
    I2C1_sendData(data);
    if (I2C1_getAckstatBit())
    {
        return ;
    }
    
    I2C1_stop();
    I2C1_close();
}

The main function has multiple responsibilities:

  • Initializes the clock frequency, peripheral pin select, ports and I2C peripheral.
  • Sets the client’s I/O Direction (IODIR) register to ‘0’, the value for digital output pins.
  • Continuously sets the client’s General Purpose I/O PORT register to digital low and digital high using I2C write operations.
#define I2C_CLIENT_ADDR                 0x20
#define MCP23008_REG_ADDR_IODIR         0x00
#define MCP23008_REG_ADDR_GPIO          0x09
#define PINS_DIGITAL_OUTPUT             0x00
#define PINS_DIGITAL_LOW                0x00
#define PINS_DIGITAL_HIGH               0xFF

void main(void)
{
    CLK_Initialize();
    PPS_Initialize();
    PORT_Initialize();
    I2C1_Initialize();
    
    /* Set the extended pins as digital output */
    I2C1_write1ByteRegister(I2C_CLIENT_ADDR, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT);
    
    while (1)
    {
        /* Set the extended pins to digital low */
        I2C1_write1ByteRegister(I2C_CLIENT_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_LOW);
        __delay_ms(500);
        /* Set the extended pins to digital high */
        I2C1_write1ByteRegister(I2C_CLIENT_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_HIGH);
        __delay_ms(500);
	}
}

10-Bit DAC

In this use case, the microcontroller is configured in I2C Host mode using the MSSP1 instance of the MSSP peripheral, and communicates with the client TC1321, a 10-bit DAC that can be controlled through the I2C interface.

The program will repeatedly:
  • Write data to the DAC which toggles between min(0V) and max(2.5V) output. The output can be measured with a multimeter.

The I2C1_writeNBytes function executes all the steps to write N bytes to the client:

static uint8_t I2C1_writeNBytes(uint8_t address, uint8_t reg, uint8_t* data, size_t len)
{
    /* Shift the 7 bit address and add a 0 bit to indicate write operation */
    uint8_t writeAddress = ((address << 1) & (~I2C_RW_BIT));
    uint8_t dataSentSuccessful = 1;
    
    I2C1_open();
    I2C1_startCondition();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        /* Handle error */
        dataSentSuccessful = 0;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        /* Handle error */
        dataSentSuccessful = 0;
    }

    for(uint8_t i = 0; i < len; i++)
    {
        I2C1_sendData(*data++);
        if (I2C1_getAckstatBit())
        {
            /* Handle error */
            dataSentSuccessful = 0;
        } 
    }
    
    I2C1_stopCondition();
    I2C1_close();
    return dataSentSuccessful;
}

The main function has multiple responsibilities:

  • Initializes the clock frequency, peripheral pin select, ports and I2C peripheral.
  • Continuously write data to the client device using I2C write operations. After the data is written, the bit values are toggled before writing them to the client again.
int main(void)
{   
    /* Initialize the device */
    CLK_Initialize();
    PPS_Initialize();
    PORT_Initialize();
    I2C1_Initialize();

    uint8_t data[DATALENGTH];
    data[0] = DATA_HIGH;
    data[1] = DATA_HIGH;
    while(1)
    {   
        
        /* Write to DATA REGISTER in TC1321 */
        if (I2C1_writeNBytes(I2C_CLIENT_ADDR, TC1321_REG_ADDR, data, DATALENGTH))
        {
            /* Overwrite data with its inverse*/
            data[0] = ~data[0];
            data[1] = ~data[1];
            /*Delay 1 second*/
            __delay_ms(1000);
        }
        else
        {    
            /* Handle error */
        }
    }
}

9 to 12-Bit Temperature Sensor

In this use case, the microcontroller is configured in I2C Host mode using the MSSP1 instance of the MSSP peripheral, and communicates with the client MCP9800, a 9 to 12-bit temperature sensor that can be controlled through the I2C interface.

The program will:
  • Set the resolution of the temperature sensor to 12-bit with an I2C write operation in the client’s Configuration register.
After the resolution is set, the program will repeatedly:
  • Read the temperature, with an I2C read operation in the temperature registers.

Write 1 Byte to Client Device Register

The I2C1_write1ByteRegister function executes all the steps to write one byte to the client:

static void I2C1_write1ByteRegister(uint8_t address, uint8_t reg, uint8_t data)
{
    /* Shift the 7 bit address and add a 0 bit to indicate write operation */
    uint8_t writeAddress = (address << 1) & ~I2C_RW_BIT;
    dataOperationSuccessful = 1;
    
    I2C1_open();
    I2C1_startCondition();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_sendData(data);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_stopCondition();
    I2C1_close();
}

Read 2 Bytes From Client Device Register

The I2C1_read2ByteRegister function executes all the steps to read two bytes from the client:

static uint16_t I2C1_read2ByteRegister(uint8_t address, uint8_t reg)
{
    /* Shift the 7-bit address and add a 0 bit to indicate a write operation */
    uint8_t writeAddress = (address << 1) & ~I2C_RW_BIT;
    uint8_t readAddress = (address << 1) | I2C_RW_BIT;
    uint8_t dataRead[2];
    dataOperationSuccessful = 1;
    
    I2C1_open();
    I2C1_startCondition();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }

    I2C1_startCondition();
    
    I2C1_sendData(readAddress);

    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    I2C1_setRecieveMode();
    
    dataRead[0] = I2C1_readData();
    /* Send ACK bit to receive byte of data */
    I2C1_sendAcknowledge();
    
    I2C1_setRecieveMode();
    dataRead[1] = I2C1_readData();
    /* Send NACK bit to stop receiving mode */
    I2C1_sendNotAcknowledge();
    
    I2C1_stopCondition();
    I2C1_close();
    
    return (uint16_t)((dataRead[0] << 8) | (dataRead[1]));
}

The main function has multiple responsibilities:

  • Initializes the clock frequency, peripheral pin select, ports and I2C peripheral.
  • Sets the temperature sensors resolution to 12-bit.
  • Continuously reads back the temperature and coverts it into degrees Celsius.

The I2C1_operationSuccessful function checks the status of the dataOperationSuccessful variable:

static uint8_t I2C1_operationSuccessful(void)
{
    return dataOperationSuccessful;
}
#define I2C_CLIENT_ADDR                 0x49
#define MCP9800_REG_ADDR_CONFIG         0x01
#define MCP9800_REG_ADDR_TEMPERATURE    0x00
#define CONFIG_DATA_12BIT_RESOLUTION    0x60
#define I2C_RW_BIT                      0x01

uint8_t dataOperationSuccessful;
void main(void)
{
    CLK_Initialize();
    PPS_Initialize();
    PORT_Initialize();
    I2C1_Initialize();

    uint16_t 	rawTempValue;
    float       tempCelcius;

    /* Set the resolution to 12-bits */
    I2C1_write1ByteRegister(I2C_CLIENT_ADDR, MCP9800_REG_ADDR_CONFIG, CONFIG_DATA_12BIT_RESOLUTION);
    if (!I2C1_operationSuccessful())
    {
        /* Handle Error */
    }
    
    while (1)
    {
        /* Read out the 12-bit raw temperature value */
        rawTempValue = I2C1_read2ByteRegister(I2C_CLIENT_ADDR, MCP9800_REG_ADDR_TEMPERATURE);
        /* Convert the 16-bit value into 12-bit value */
        rawTempValue = (rawTempValue >> 4);
        
        if (!I2C1_operationSuccessful())
        {
            /* Handle Error */
        }
        
        /* Convert the raw temperature data to degrees Celsius */
        /* Tc = rawTemp * 2^-4 */
        tempCelcius = (float) rawTempValue / 16.0;
        __delay_ms(500);
	}
}

EEPROM

Write One Byte to Client Device Register

The I2C1_write1ByteRegister function executes all the steps to write one byte to the client:

static void I2C1_write1ByteRegister(uint8_t address, uint8_t reg, uint8_t data)
{
    /* Shift the 7 bit address and add a 0 bit to indicate write operation */
    uint8_t writeAddress = (address << 1) & ~I2C_RW_BIT;
    dataOperationSuccessful = 1;
    
    I2C1_open();
    I2C1_startCondition();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_sendData(data);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_stopCondition();
    I2C1_close();
}

Write N Bytes to Client Device

The I2C1_writeNBytes function executes all the steps to write N bytes to the client device:
void I2C1_writeNBytes(uint8_t address, uint8_t reg, uint8_t* data, uint8_t length)
{
    /* Shift the 7-bit address and add a 0 bit to indicate a write operation */
    uint8_t writeAddress = (address << 1) & ~I2C_RW_BIT;
    dataOperationSuccessful = 1;
    I2C1_open();
    
    /* Write the address we want to read to the device */
    I2C1_startCondition();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    uint8_t i = 0;
    while (i < length)
    {
        I2C1_sendData(*data++);
        if (I2C1_getAckstatBit())
        {
            /* Error occurred */
            dataOperationSuccessful = 0;
        }
        i++;
    }

    I2C1_stopCondition();
    I2C1_close(); 
}

Read N Bytes to Client Device

The I2C1_readNBytes function executes all the steps to read N bytes from the client device:

void I2C1_readNBytes(uint8_t address, uint8_t reg, uint8_t* data, uint8_t length)
{
    /* Shift the 7-bit address and add a 0 bit to indicate a write operation */
    uint8_t writeAddress = (address << 1) & ~I2C_RW_BIT;
    uint8_t readAddress = (address << 1) | I2C_RW_BIT;
    
    I2C1_open();
    
    /* Write the address we want to read to the device */
    I2C1_startCondition();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
 
    /* Start reading data*/
    I2C1_startCondition();
    
    I2C1_sendData(readAddress);

    if (I2C1_getAckstatBit())
    {
        /* Error occurred */
        dataOperationSuccessful = 0;
    }
    
    uint8_t i = 0;
    while (i < (length - 1))
    {
        I2C1_setRecieveMode();
        
        *data++ = I2C1_readData();
        I2C1_sendAcknowledge();
        i++;
    }
    
    I2C1_setRecieveMode();
    *data++ = I2C1_readData();

    /* Send NACK bit to stop receiving mode */
    I2C1_sendNotAcknowledge();
    
    I2C1_stopCondition();
    I2C1_close();  
}

Write N Bytes to I2C EEPROM

The I2C1_writeNBytes_EEPROM function executes all the steps to write N bytes to the EEPROM device and handles issues like page buffering:

The EEPROM pagesize can be found in the EEPROM datasheet found in the reference list.

static uint8_t I2C1_writeNBytes_EEPROM(uint8_t address, uint8_t memoryAddress, uint8_t* data, uint8_t length, uint8_t EEPROMPagesize)
{
    uint8_t pageCounter = memoryAddress/EEPROMPagesize;
    uint8_t pageEnd = pageCounter + length / EEPROMPagesize;
    uint8_t dataPerIteration = MIN(EEPROMPagesize - (memoryAddress%EEPROMPagesize), length);
    uint8_t dataBuffer[8];  /* PAGESIZE */
    
    while (pageCounter <= pageEnd)
    {
	/* Loading the desired data onto the buffer */
	for (uint8_t i = 0; i < dataPerIteration; i++)
	{
		dataBuffer[i] = *data++;
	}
        /* Writing the memory address and data to EEPROM */
	I2C1_writeNBytes(address, memoryAddress, dataBuffer, dataPerIteration);
	
	/* Updating variables for next iteration */
    	length -= dataPerIteration;
    	memoryAddress += dataPerIteration;
        dataPerIteration = MIN(EEPROMPagesize, length);
	pageCounter++;
	/* page write time for the EEPROM is about 20ms */
	__delay_ms(20);
    }
    return memoryAddress;

The MIN function returns the minimum of x and y:

static uint8_t MIN(uint8_t x, uint8_t y)
{
    if (x < y)
    {
        return x;
    }
    return y;
}

The I2C1_operationSuccessful function checks the status of the dataOperationSuccessful variable:

static uint8_t I2C1_operationSuccessful(void)
{
    return dataOperationSuccessful;
}

The main function has multiple responsibilities:

  • Initializes the clock frequency, peripheral pin select, ports and I2C peripheral.
  • Makes an arbitrary test set.
  • Writes the test set onto the EEPROM.
  • Continuously reads back the data saved on the EEPROM.
#define I2C_CLIENT_ADDR                 0x50
#define I2C_RW_BIT                      0x01
#define PAGESIZE                        8
#define TESTSIZE                        12

uint8_t dataOperationSuccessful;

void main(void)
{
    CLK_Initialize();
    PPS_Initialize();
    PORT_Initialize();
    I2C1_Initialize();

    
    uint8_t 	dataWrite[TESTSIZE];
    uint8_t 	dataRead[TESTSIZE];
    uint8_t 	EEPROM_write_address = 0x00;
    uint8_t     EEPROM_readback_address = 0x00;
    
    /* Make the text set */
    for(uint8_t i = 0; i < TESTSIZE; i++){
        dataWrite[i] = i;
    }
    
    EEPROM_write_address = I2C1_writeNBytes_EEPROM(I2C_CLIENT_ADDR, EEPROM_write_address, dataWrite, TESTSIZE, PAGESIZE);
    if (!I2C1_operationSuccessful())
    {
        /* Handle Error */
    }
    
    while (1)
    {
        I2C1_readNBytes(I2C_CLIENT_ADDR, EEPROM_readback_address, dataRead, TESTSIZE);
        if (!I2C1_operationSuccessful())
        {
            /* Handle Error */
        }
        __delay_ms(5000);
	}
}