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
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.
- 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.
- Set the resolution of the temperature sensor to 12-bit with an I2C write operation in the client’s Configuration register.
- 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
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); } }