3.3 Bare Metal Code
The necessary code and functions to implement the presented example are analyzed in this section.
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 */
The CLK_Initialize
function selects the HFINTOSC oscillator and the clock
divider, and sets the nominal frequency to 64 MHz:
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; }
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:
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; }
The I2C1_Initialize
function selects the I2C Host mode and the baud
rate divider:
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 INTERRUPT_Initialize
function enables the global and peripheral
interrupts:
static void INTERRUPT_Initialize(void) { /* Enable the Global Interrupts */ INTCONbits.GIE = 1; /* Enable the Peripheral Interrupts */ INTCONbits.PEIE = 1; }
The following functions are part of the I2C driver. Their implementation can be found below, at the GitHub link.
static uint8_t I2C1_open(void); static void I2C1_close(void); static void I2C1_startCondition(void); static void I2C1_stopCondition(void); static uint8_t I2C1_getAckstatBit(void); static void I2C1_sendNotAcknowledge(void); static void I2C1_setReceiveMode(void); static void I2C1_write1ByteRegister(uint8_t address, uint8_t reg, uint8_t data); static uint8_t I2C1_read1ByteRegister(uint8_t address, uint8_t reg);
The following functions are associated with an I2C transmission state. Their implementation can be found below, at the GitHub link.
static void I2C_stateWriteStartComplete(void); static void I2C_stateWriteAddressSent(void); static void I2C_stateWriteRegisterSent(void); static void I2C_stateWriteDataSent(void); static void I2C_stateReadStart(void); static void I2C_stateReadStartComplete(void); static void I2C_stateReadAddressSent(void); static void I2C_stateReadReceiveEnable(void); static void I2C_stateReadDataComplete(void); static void I2C_stateStopComplete(void);
The MSSP1_interruptHandler
function is called every time the SSP1IF flag is
triggered. This handler must execute different operations, depending on the current state, which
are stored in I2C1_status.state
.
The I2C_stateFuncs
vector contains all the function pointers associated with
all the I2C transmission states.
The MSSP1_interruptHandler
function calls the function for the current state,
where the state is updated, after which the SSP1IF flag is cleared.
static void MSSP1_interruptHandler(void) { /* Call the function associated with the current state */ I2C_stateFuncs[I2C1_status.state](); /* Clear the Interrupt Flag */ PIR3bits.SSP1IF = 0; } void __interrupt() INTERRUPT_InterruptManager (void) { if(INTCONbits.PEIE == 1) { if(PIE3bits.SSP1IE == 1 && PIR3bits.SSP1IF == 1) { MSSP1_interruptHandler(); } } }
The main
function has multiple responsibilities:
- Initializes the clock frequency, Peripheral Pin Select, ports and I2C peripheral.
- Enables the peripheral and global interrupts.
- Sets the client’s I/O Direction (IODIR)
register to ‘
0
’, the value for digital output pins. - Continuously sets the extended port to the
value
data
with a write operation in the client’s General Purpose I/O PORT Register; reads the value from the same register and inverts the read value.
#define I2C_CLIENT_ADDRESS 0x20 #define MCP23008_REG_ADDR_IODIR 0x00 #define MCP23008_REG_ADDR_GPIO 0x09 #define MCP23008_DATA 0x0F #define PINS_DIGITAL_OUTPUT 0x00 void main(void) { CLK_Initialize(); PPS_Initialize(); PORT_Initialize(); I2C1_Initialize(); INTERRUPT_Initialize(); /* Set the initial state to Idle */ I2C1_status.state = I2C_IDLE; /* Set data to use in the I2C operations */ uint8_t data = MCP23008_DATA; /* Set the extended pins as digital output */ I2C1_write(I2C_CLIENT_ADDRESS, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT); while (1) { /* Write data to the GPIO port */ I2C1_write(I2C_CLIENT_ADDRESS, MCP23008_REG_ADDR_GPIO, data); /* Read data from the GPIO port */ data = I2C1_read(I2C_CLIENT_ADDRESS, MCP23008_REG_ADDR_GPIO); /* Overwrite data with the inverted data read */ data = ~data; __delay_ms(500); } }