Blog Entry




Interfacing the Microchip PIC18F Microcontroller Master Synchronous Serial Port (MSSP) to various I2C Devices

December 11, 2011 by , under Microcontroller.




The Inter-Integrated Circuit or I2C (read as I square C) bus has been introduced in 1980 by Philips, and has become a de-facto world standard for data exchange between Microcontroller and various devices such as temperature sensor, ADC (analog to digital converter), DAC (digital to analog converter), I/O expander, EEPROM, and many more. With more than thousand different IC devices have been manufactured with an I2C-bus interface, making the understanding of the working principle of this I2C bus is an essential knowledge that has to be acquired by anyone who want to involve in the embedded world professionally or just as hobbyist.

In this project we will learn of how to use the powerful 8-Bit Microchip PIC18F14K22 microcontroller Master Synchronous Serial Port (MSSP) in the I2C master mode to control various I2C devices simultaneously

The I2C Bus Protocol

The I2C bus use master and slave communication principle which mean the slave will response to any master request and only one master or one slave could use the I2C bus at the time (half-duplex communication). Therefore the master and slave have to be connected known as “Wired OR” connection using the pull-up resistors for both the I2C serial data (SDA) and serial clock (SCL) as shown on this following picture.

Unlike the SPI (Serial Peripheral Interface) slave devices, the I2C devices don’t have the chip select (CS) pin where the SPI master could simply drive the CS pin to logic “0” in order to communicate with the target SPI slave device, instead in I2C protocol the I2C master will transmit the I2C slave device unique address in order to communicate with it.

When the I2C bus is idle both of the SDA and SCL line will be logic “1”. When the I2C master want to start communicate first it will send the START signal by putting the SDA line to logic “0” then it start to send the 7-bit I2C slave address followed by 1-bit I2C bus transfer direction command (WRITE logic “0” or READ logic “1”). The 7-bits I2C slave address consists of the upper 4-bits (ID3, ID2, ID1, and ID0) whose are the device specific ID (identification) and encoded within each of the I2C slave device.

The next 3-bits address is called a configurable I2C slave device address as it depend on the address pins (A2, A1, and A0) on the I2C slave device logical input. For example if we connect the A2, A1, and A0 pins to the ground (GND) or logic “0”, then these 3-bits become “000” or if connect these pins to VCC, then these 3-bits become “111”. For example the complete I2C slave address for writing to the Microchip 24LC0B EEPROM whose configurable address A2, A1, and A0 pins connected to logic “0” is “10100000”.

After the I2C master send the I2C slave address, then the I2C slave address will response by pulling the SDA line to low (logical “0”) and this is known as the acknowledge (ACK) to the I2C master. Next the I2C master will continue send the data or terminated the communication by changing the SDA line from logic “0” to logic “1” or known as the STOP signal to all the I2C slave devices and the I2C bus is back to the idle state status which both of SDA and SCL line is not being driven by both of I2C master and slave devices. In idle state the pull-up resistors will pull-up the SDA and SCL lines to become logic “1”.

The I2C master read operation could be done by supplying the read transfer command (LSB) with logic “1”, for example the complete I2C slave address for reading from the Microchip 24LC02B EEPROM whose configurable address A2, A1, and A0 pins connected to logic “0” is “10100001”. Next the I2C master will send the not acknowledge (NACK) signal (logic “1”) to the I2C slave before sending the STOP signal, this NACK signal usually will tell the I2C slave device to stop transmitting any data to the I2C master.

Sometimes before the I2C master read operation could be accomplish (indirect read), we have to do the I2C master write operation first which is used to set the I2C slave internal register, then we send the RESTART signal condition (i.e. STOP and START signal) followed by the I2C master read operation command.

The I2C Project

As I mention before in this project we learn to control various I2C slave devices simultaneously using the Microchip PIC18F14K22 microcontroller, now let’s list down all the necessary hardware, software, and some supported documents in order to accomplish this project objective:

  • Microchip PICkitTM Serial I2C Demo Board, this board has Microchip 2K I2C 24LC0B EEPROM, MCP9801 Microchip 2-Wire High-Accuracy Temperature Sensor, MCP3221 Microchip Low Power 12-Bit A/D Converter With I2C Interface, TC1321 Microchip 10-Bit Digital-to-Analog Converter with Two-Wire Interface, and MCP23008
  • Microchip 8-Bit I/O Expander with Serial Interface
  • Additional SMD pull-up 10K (2) resistors for the PICKit Serial I2C Demo Board, you need to solder these two SMD resistors on the board.
  • PCA8574 Philips/NXP 8-bit I/O expander for I2C-bus
  • One Hitachi HD44780U or compatible 2×16 LCD with blue backlight
  • SMD Resistors 1K (1) and 4.7K (1)
  • One 10K Trimpot
  • One SMD LED (1)
  • One SMD N-Channel MOSFET MMBF170 or equivalent
  • One half size breadboard for the I2C LCD
  • JazMate 5 and 3.3 volt LM2587 switching power supply from ermicro
  • PICJazz 20-PIN learning board with Microchip PIC18F14K22 microcontroller from ermicro
  • Microchip PICKit2 programmer (used in this project), you could also use the Microchip PICKit3 programmer.
  • Microchip MPLAB IDE version 8.63 and Microchip HI-TECH C PRO for the PIC18 MCU Family (Lite) V9.63PL3
  • Reference Document: Microchip PIC18F14K22 datasheet, Microchip 24LC02B datasheet, Microchip MCP9801 datasheet, Microchip MCP3221 datasheet, Microchip TC1321 datasheet, Microchip MCP23008 datasheet, and Philips/NXP PCA8574 datasheet.

 

The following is the complete C source code for this project:

/* ***************************************************************************
**  File Name    : i2cmaster.c
**  Version      : 1.0
**  Description  : I2C Master With Microchip PIC18F14K22 Microcontroller
**  Author       : RWB
**  Target       : PICJazz 20PIN Board: PIC18F14K22
**                 Microchip PICKit Serial I2C Demo Board
**                 Philips PCA8574 I2C I/O Expander and 2x16 LCD
**  Compiler     : HI-TECH C PRO for the PIC18 MCU Family (Lite)  V9.63PL3
**  IDE          : Microchip MPLAB IDE v8.63
**  Programmer   : Microchip PICkit 2 - Operating System Version 2.32.0
**  Last Updated : 16 Nov 2011
** ***************************************************************************/
#include <pic18.h>

/*
** PIC18F14K22 Configuration Bit
** Alternative Microchip MPLAB IDE Menu Configure -> Configuration Bits
**
** CONFIG1H: CONFIGURATION REGISTER 1 HIGH
** FCMEN = OFF  - Fail-Safe Clock Monitor disabled
** PCLKEN = ON  - Primary Clock Enable
** PLLEN = OFF  - PLL is under software control
** FOSC = IRC   - Internal RC Oscillator
** CONFIG2L: CONFIGURATION REGISTER 2 LOW
** PWRTEN = OFF - Power Up Timer Enabled
** BOREN = OFF  - Brown-out Reset disabled in hardware and software
** CONFIG2H: CONFIGURATION REGISTER 2 HIGH
** WDTEN = OFF  - WDT is controlled by SWDTEN bit of the WDTCON register
** CONFIG3H: CONFIGURATION REGISTER 3 HIGH
** MCLRE = ON   - MCLR pin enabled, RE3 input pin disabled
** HFOFST = ON  - Clocking the CPU without waiting for the oscillator to stabilize
** CONFIG4L: CONFIGURATION REGISTER 4 LOW
** XINST = ON   - Instruction set extension and Indexed Addressing mode enabled
** STVREN = ON  - Stack full/underflow will cause Reset
** LVP = ON     - Single-Supply ICSP Enable
** CONFIG5H, CONFIG6L, CONFIG6H, CONFIG7L, CONFIG7H - All Not Protected
*/
__CONFIG(1, 0x2800);
__CONFIG(2, 0x0001);
__CONFIG(3, 0x8800);
__CONFIG(4, 0x0065);
__CONFIG(5, 0xFFFF);
__CONFIG(6, 0xFFFF);
__CONFIG(7, 0xFFFF);

// I2C Bus Control Definition
#define I2C_DATA_ACK 0
#define I2C_DATA_NOACK 1
#define I2C_WRITE_CMD 0
#define I2C_READ_CMD 1

#define I2C_START_CMD 0
#define I2C_REP_START_CMD 1
#define I2C_REQ_ACK 0
#define I2C_REQ_NOACK 0

// Microchip MCP23008 8-bit I/O Expander
#define MCP23008_ADDR 0x40   // MCP23008 Device Identifier
#define IODIR 0x00           // MCP23008 I/O Direction Register
#define GPIO  0x09           // MCP23008 General Purpose I/O Register
#define OLAT  0x0A           // MCP23008 Output Latch Register

// Microchip 24LC02B 2KB I2C EEPROM
#define M24LC02B_ADDR  0xA0  // 24LC02B Device Identifier

// Microchip MCP9801 I2C Temperature Sensor
#define MCP9801_ADDR  0x92  // MCP9801 Device Identifier
#define TEMP_REGISTER 0x00  // MCP9801 Ambient Temperature Register
#define CONF_REGISTER 0x01  // MCP9801 Configuration Register
#define TEMP_SAMPLE 5

// Microchip MCP3221 I2C ADC
#define MCP3221_ADDR  0x9A  // MCP3221 Device Identifier

// Microchip TC1321 I2C DAC
#define TC1321_ADDR  0x90   // TC1321 Device Identifier
#define DATA_REGISTER 0x00  // TC1321 Data Register Select Command
#define CONF_REGISTER 0x01  // TC1321 Config Register Select Command

// Philips PCA8574 8-bit I/O Expander
#define PCA8574_ADDR 0x4E   // PCA8574 Device Identifier 

// PCA8574 I2C LCD Port Definition
// P7,P6,P5,P4 = Data, P3=Backlight (BL), P2=E, P1=RW, P0=RS
#define LCD_BL 0b00001000
#define LCD_EN 0b00000100
#define LCD_RW 0b00000010
#define LCD_RS 0b00000001

// LCD Command
#define LCD_HOME 0x02
#define LCD_NEXT_LINE 0xC0
#define LCD_CLEAR 0x01
#define LCD_1CYCLE 0
#define LCD_2CYCLE 1

// Using Internal Clock of 16 Mhz
#define FOSC 16000000UL
#define MAX_DATA 32           

// Used for Displaying the Numeric Value
char sdigit[]={'0','0','0','0','0','\0'};

// Simple Delay Function, you might adjust the value for different clock speed
#define	delay_us(x) {unsigned char _dcnt; \
		    _dcnt = (x)/(24000000UL/FOSC)|1; \
		    while(--_dcnt != 0) continue; \
                    }

void delay_ms(unsigned int cnt)
{
  unsigned char i;

  if (cnt == 0) return;
  do {
    i = 5;
    do {
      delay_us(164);
    } while(--i);
  } while(--cnt);
}

// Start PIC18F14K22 I2C Function
void i2c_init(void) {
  // Initial PIC18F14K22 I2C bus Ports: RB4 - SDA and RB6 - SCL, Set as Input
  TRISBbits.TRISB4 = 1;
  TRISBbits.TRISB6 = 1;  

  // Initial the PIC18F14K22 MSSP Peripheral I2C Master Mode
  // I2C Master Clock Speed: 16000000 / ((4 * (SSPADD + 1)) = 16000000 / (4 * (39 + 1))
  SSPSTAT = 0x80;      // Slew Rate is disable for 100 kHz mode
  SSPCON1 = 0x28;      // Enable SDA and SCL, I2C Master mode, clock = FOSC/(4 * (SSPADD + 1))
  SSPCON2 = 0x00;      // Reset MSSP Control Register
  SSPADD = 39;         // Standard I2C Clock speed: 100 kHz  

  PIR1bits.SSPIF=0;    // Clear MSSP Interrupt Flag
}

void i2c_idle(void)
{
  // Wait I2C Bus and Status Idle (i.e. ACKEN, RCEN, PEN, RSEN, SEN)
  while (( SSPCON2 & 0x1F ) || ( SSPSTATbits.R_nW));
}

void i2c_start(unsigned char stype)
{
  i2c_idle();                     // Ensure the I2C module is idle
  if (stype == I2C_START_CMD) {
    SSPCON2bits.SEN = 1;          // Start I2C Transmission
    while(SSPCON2bits.SEN);
  } else {
    SSPCON2bits.RSEN = 1;         // ReStart I2C Transmission
    while(SSPCON2bits.RSEN);
  }
}

void i2c_stop(void)
{
  // Stop I2C Transmission
  SSPCON2bits.PEN = 1;
  while(SSPCON2bits.PEN);
}

unsigned char i2c_slave_ack(void)
{
  // Return: 1 = Acknowledge was not received from slave
  //         0 = Acknowledge was received from slave
  return(SSPCON2bits.ACKSTAT);
}

void i2c_write(unsigned char data)
{
  // Send the Data to I2C Bus
  SSPBUF = data;
  if (SSPCON1bits.WCOL)         // Check for write collision
    return;  

  while(SSPSTATbits.BF);        // Wait until write cycle is complete
  i2c_idle();                   // Ensure the I2C module is idle
}

void i2c_master_ack(unsigned char ack_type)
{
  SSPCON2bits.ACKDT = ack_type;   // 1 = Not Acknowledge, 0 = Acknowledge
  SSPCON2bits.ACKEN = 1;          // Enable Acknowledge
  while (SSPCON2bits.ACKEN == 1);
}

unsigned char i2c_read(void)
{
  // Ensure the I2C module is idle
  i2c_idle();                         

  // Enable Receive Mode
  SSPCON2bits.RCEN = 1;           // Enable master for 1 byte reception
  while(!SSPSTATbits.BF);         // Wait until buffer is full
  return(SSPBUF);
}

unsigned char Read_24LC02B(unsigned int mem_addr)
{
  unsigned char data;

  // Start the I2C Transmission
  i2c_start(I2C_START_CMD);

  // Write 24LC02B Control Byte - Write
  i2c_write(M24LC02B_ADDR|I2C_WRITE_CMD);  

  // Sending the 24LC02B 8-Bit Memory Address Pointer
  i2c_write(mem_addr & 0x00FF);     

  // ReStart the I2C Transmission
  i2c_start(I2C_REP_START_CMD);

  // Write 24LC02B Control Byte - Read
  i2c_write(M24LC02B_ADDR|I2C_READ_CMD); 

  // Read Data from 24LC02B EEPROM
  data=i2c_read(); 

  // Master send No Acknowledge Required to the Slave
  i2c_master_ack(I2C_DATA_NOACK);  

  // Stop the I2C Transmission
  i2c_stop();  

  return(data);
}

void Write_24LC02B(unsigned int mem_addr,unsigned char data)
{
  // Start the I2C Write Transmission
  i2c_start(I2C_START_CMD);

  // Write I2C OP Code
  i2c_write(M24LC02B_ADDR|I2C_WRITE_CMD);     

  // Sending the 24LC02B 8-bit Memory Address Pointer
  i2c_write(mem_addr & 0x00FF);       

  // Write data to 24LC02B EEPROM
  i2c_write(data);   

  // Stop I2C Transmission
  i2c_stop();  

  // Put some delay 5ms here
  delay_ms(5);
}

void Write_MCP23008(unsigned char reg_addr,unsigned char data)
{
  // Start the I2C Write Transmission
  i2c_start(I2C_START_CMD);

  // Write I2C OP Code
  i2c_write(MCP23008_ADDR|I2C_WRITE_CMD); 

  // Sending the Register Address
  i2c_write(reg_addr);  

  // Write data to MCP23008 Register
  i2c_write(data);   

  // Stop I2C Transmission
  i2c_stop();
}

unsigned int Read_MCP3221(void)
{
  unsigned int adcdata;
  unsigned char hidata,lodata;

  // Start the I2C Write Transmission
  i2c_start(I2C_START_CMD);    

  // Read MCP3221 I2C ADC Control Byte - Read
  i2c_write(MCP3221_ADDR|I2C_READ_CMD);     

  // Get the High Byte of MCP3221 A/D Conversion
  hidata=i2c_read();

  // Send Acknowledge to the Slave
  i2c_master_ack(I2C_DATA_ACK);  

  // Get the Low Byte of MCP3221 A/D Conversion
  lodata=i2c_read();   

  // Send No Acknowledge to the Slave
  i2c_master_ack(I2C_DATA_NOACK);  

  // Stop I2C Transmission
  i2c_stop();  

  // Return 12-bit ADC Data
  adcdata = lodata;
  adcdata += ((int)hidata) << 8;      

  return(adcdata);
} 

void Write_MCP9801(unsigned char reg_addr,unsigned char data) {
  // Start the I2C Write Transmission
  i2c_start(I2C_START_CMD);   

  // Write MCP9801 I2C OP Code
  i2c_write(MCP9801_ADDR|I2C_WRITE_CMD);    

  // Sending the Register Address
  i2c_write(reg_addr);

  // Write data to MCP9801 Register
  i2c_write(data);        

  // Stop I2C Transmission
  i2c_stop();
} 

unsigned char Read_MCP9801(unsigned char *dval) {
  unsigned char hidata,lodata;
  char decval[]={0,25,50,75};     

  // Start the I2C Write Transmission
  i2c_start(I2C_START_CMD);        

  // Read MCP9801 I2C Temp Sensor Control Byte - Read
  i2c_write(MCP9801_ADDR|I2C_READ_CMD);        

  // Get the High Byte of MCP9801 I2C Temp Sensor
  hidata=i2c_read();   

  // Send Acknowledge to the Slave
  i2c_master_ack(I2C_DATA_ACK);     

  // Get the Low Byte of MCP9801 I2C Temp Sensor
  lodata=i2c_read();      

  // Send No Acknowledge to the Slave
  i2c_master_ack(I2C_DATA_NOACK);     

  // Stop I2C Transmission
  i2c_stop();     

  // Return 10-bit Temp Sensor Data
  *dval=decval[lodata >> 6];           // Convert lower data to decimal.

  return(hidata);
}

void Write_PCA8574(unsigned char data)
{
  // Start the I2C Write Transmission
  i2c_start(I2C_START_CMD);

  // Write PCA8574 I2C OP Code
  i2c_write(PCA8574_ADDR|I2C_WRITE_CMD);   

  // Write data to PCA8574 Register
  i2c_write(data);     

  // Send No Acknowledge to the Slave
  i2c_master_ack(I2C_DATA_NOACK);  

  // Stop I2C Transmission
  i2c_stop();
}

/*
** PCA8574 I2C LCD Routine
** LCD Data PCA8574: P7,P6,P5,P4
** LCD Control: P3: Back Light, P2: E-Enable, P1:RW, P0: RS
*/
void LCD_putcmd(unsigned char data,unsigned char cmdtype)
{
  unsigned char lcddata;  

  // Put the Upper 4 bits data
  lcddata = (data & 0xF0)|LCD_BL;
  Write_PCA8574(lcddata | LCD_EN);
  delay_us(2);      // Delay 2us for 16 MHz Internal Clock   

  // Write Enable Pulse E: Hi -> Lo
  Write_PCA8574(lcddata & ~LCD_EN);
  delay_us(1);      // Delay 1us for 16 MHz Internal Clock   

  // cmdtype = 0; One cycle write, cmdtype = 1; Two cycle writes
  if (cmdtype) {
    // Put the Lower 4 bits data
    lcddata = ((data << 4) & 0xF0)|LCD_BL;
    Write_PCA8574(lcddata | LCD_EN);
    delay_us(2);    // Delay 2us for 16 MHz Internal Clock    

    // Write Enable Pulse E: Hi -> Lo
    Write_PCA8574(lcddata & ~LCD_EN);
    delay_us(1);    // Delay 1us for 16 MHz Internal Clock
  }
}

void LCD_putch(unsigned char data)
{
  unsigned char lcddata;

  // Put the Upper 4 bits data
  lcddata = (data & 0xF0)|LCD_BL|LCD_RS;
  Write_PCA8574(lcddata | LCD_EN);
  delay_us(2);      // Delay 2us for 16 MHz Internal Clock     

  // Write Enable Pulse E: Hi -> Lo
  Write_PCA8574(lcddata & ~LCD_EN);
  delay_us(1);      // Delay 1us for 16 MHz Internal Clock   

  // Put the Lower 4 bit data
  lcddata = ((data << 4) & 0xF0)|LCD_BL|LCD_RS;
  Write_PCA8574(lcddata | LCD_EN);
  delay_us(2);      // Delay 2us for 16 MHz Internal Clock    

  // Write Enable Pulse E: Hi -> Lo
  Write_PCA8574(lcddata & ~LCD_EN);
  delay_us(1);      // Delay 1us for 16 MHz Internal Clock
}

void LCD_init(void)
{
  // Wait for more than 15 ms after VCC rises to 4.5 V
  delay_ms(30);

  // Send Command 0x30
  LCD_putcmd(0x30,LCD_1CYCLE);

  // Wait for more than 4.1 ms
  delay_ms(8);

  // Send Command 0x30
  LCD_putcmd(0x30,LCD_1CYCLE);

  // Wait for more than 100 us
  delay_ms(1);           

  // Send Command 0x30
  LCD_putcmd(0x30,LCD_1CYCLE);

  // Function set: Set interface to be 4 bits long (only 1 cycle write).
  LCD_putcmd(0x20,LCD_1CYCLE);  

  // Function set: DL=0;Interface is 4 bits, N=1; 2 Lines, F=0; 5x8 dots font)
  LCD_putcmd(0x28,LCD_2CYCLE);

  // Display Off: D=0; Display off, C=0; Cursor Off, B=0; Blinking Off
  LCD_putcmd(0x08,LCD_2CYCLE);

  // Display Clear
  LCD_putcmd(LCD_CLEAR,LCD_2CYCLE);

  // Entry Mode Set: I/D=1; Increament, S=0; No shift
  LCD_putcmd(0x06,LCD_2CYCLE);

  // Display On, Cursor Off
  LCD_putcmd(0x0C,LCD_2CYCLE);
}

void LCD_puts(const char *s)
{
  while(*s != 0) {      // While not Null
    if (*s == '\n')
      LCD_putcmd(LCD_NEXT_LINE,LCD_2CYCLE);  // Goto Second Line
    else
      LCD_putch(*s);
    s++;
  }
}

// Implementing integer value from 0 to 65530
char *num2str(unsigned int number,unsigned char start_digit)
{
   unsigned char digit;

   if (number > 65530) number = 0;    

   digit = '0';                       // Start with ASCII '0'
   while(number >= 10000)             // Keep Looping for larger than 10000
   {
     digit++;                         // Increase ASCII character
     number -= 10000;                 // Subtract number with 10000
   }

   sdigit[0]='0';                     // Default first Digit to '0'
   if (digit != '0') sdigit[0]=digit; // Put the first digit

   digit = '0';                       // Start with ASCII '0'
   while(number >= 1000)              // Keep Looping for larger than 1000
   {
     digit++;                         // Increase ASCII character
     number -= 1000;                  // Subtract number with 1000
   }

   sdigit[1]='0';                     // Default Second Digit to '0'
   if (digit != '0') sdigit[1]=digit; // Put the Second digit

   digit = '0';                       // Start with ASCII '0'
   while(number >= 100)               // Keep Looping for larger than 100
   {
     digit++;                         // Increase ASCII character
     number -= 100;                   // Subtract number with 100
   }

   sdigit[2]='0';                     // Default Second Digit to '0'
   if (digit != '0') sdigit[2]=digit; // Put the Second digit

   digit = '0';                       // Start with ASCII '0'
   while(number >= 10)                // Keep Looping for larger than 10
   {
     digit++;                         // Increase ASCII character
     number -= 10;                    // Subtract number with 10
   }

   sdigit[3]='0';                     // Default Second Digit to '0'
   if (digit != '0') sdigit[3]=digit; // Put the Second digit

   sdigit[4]='0' + number;
   return(sdigit + start_digit);
}

void Write_TC1321(unsigned int data)
{
  unsigned int dac_out;

  // Start the I2C Write Transmission
  i2c_start(I2C_START_CMD);

  // Write TC1321 I2C OP Code
  i2c_write(TC1321_ADDR|I2C_WRITE_CMD);   

  // Select TC1321 Data Register
  i2c_write(DATA_REGISTER);       

  // Write 10-bit Data to the TC1321 Data Register
  // Format xxxx xxxx xx00 0000 

  // Write High Byte Data
  dac_out=(data << 6) & 0xFF00;   i2c_write(dac_out >> 8);       

  // Write Low Byte Data
  dac_out=(data << 6) & 0x00C0;
  i2c_write(dac_out);       

  // Stop I2C Transmission
  i2c_stop();
}

void main(void)
{
  unsigned char buffer[MAX_DATA]= {0b00000001,0b00000011,0b00000110,0b00001100,0b00011001,
  		  	           0b00110011,0b01100110,0b11001100,0b10011000,0b00110000,
			           0b01100000,0b11000000,0b10000000,0b00000000,0b00000000,
			           0b00000000,0b10000000,0b11000000,0b01100000,0b00110000,
			           0b10011000,0b11001100,0b01100110,0b00110011,0b00011001,
			           0b00001100,0b00000110,0b00000011,0b00000001,0b00000000,
			           0b00000000,0b00000000};		

  unsigned int addr_ptr,delay_value,adc_value;
  unsigned char eeprom_data,temp_value,temp,dvalue,sign,tsample,disp_stat,press_count;
  unsigned char LED[3] = {0x01,0x02,0x04};	 

  OSCCON=0x70;         // Select 16 MHz internal clock 

  TRISA = 0x03;        // Input for RA0 and RA3
  TRISB = 0x00;        // Input for PORTB
  TRISC = 0x00;        // Set All on PORTC as Output
  PORTC = 0x00;        // Reset PORTC
  ANSEL = 0x00;        // Set AN0 - Analog Input and PORT AN1 to AN7 as Digital I/O
  ANSELH = 0x00;       // Set PORT AN8 to AN11 as Digital I/O    

  // Initial the PIC18F14K22 I2C Master
  i2c_init(); 

  // Init the PIC18F14K22 ADC Peripheral
  ADCON0=0b00000001;   // ADC port Channel 0 (AN0), Enable ADC
  ADCON1=0b00000000;   // Use Internal Voltage Reference (Vdd and Vss)
  ADCON2=0b10101011;   // Right justify result, 12 TAD, Select the FRC for 16 MHz 

  // Initial LCD using 4 bits data interface
  LCD_init();
  LCD_puts("PICJazz 18F14K22\n");    

  // Initial and Write the 24LC02B 2K I2C EEPROM
  for(addr_ptr=0;addr_ptr < MAX_DATA;addr_ptr++) {
   Write_24LC02B(addr_ptr,buffer[addr_ptr]);
  }            

  // Initial the MCP23008 8-bit I2C I/O Expander
  Write_MCP23008(IODIR,0b00000000);
  Write_MCP23008(GPIO,0x00);    // Reset all the Output Port         

  // Initial the MCP9801 10-bit I2C Digital Temperature Sensor
  Write_MCP9801(CONF_REGISTER,0b00100000); // Used 10-Bit Mode
  Write_MCP9801(TEMP_REGISTER,0b00000000); // Select Temperature Register    

  // Reset the TC1321 DAC Voltage Output
  Write_TC1321(0);         

  addr_ptr=0;                   // EEPROM Address Pointer
  tsample=TEMP_SAMPLE;          // Temperature Sample
  press_count=0;                // Debounce Button Pressed Count
  disp_stat=0;                  // Default Display Temperature
  PORTC=LED[disp_stat];         // Display Status Monitor        

  for(;;) {                     // Loop Forever
    if (PORTAbits.RA1 == 0) {   // Read Switch
      if (++press_count > 4) {  // Read 5 Times for Simple Debounce
        press_count=0;          // Reset Press Count Variable
        if (++disp_stat > 2)
           disp_stat=0;

        PORTC=LED[disp_stat];   // Display Status on PORTC

        LCD_putcmd(LCD_CLEAR,LCD_2CYCLE);  // LCD Clear
        LCD_putcmd(LCD_HOME,LCD_2CYCLE);   // LCD Home
        LCD_puts("PICJazz 18F14K22\n");
      }
    }              

    // Read the PIC18K14K22 10-Bit AN0 ADC Input
    ADCON0bits.GO_nDONE=1;
    while (ADCON0bits.GO_nDONE) continue;  // Wait conversion done
    adc_value=ADRESL;                      // Get the 8 bit LSB result
    adc_value += (ADRESH << 8);            // Get the 2 bit MSB result         

    // Write to the TC1321 10-Bit DAC
    Write_TC1321(adc_value);      

    // Read 24LC0B I2C EEPROM Data
    eeprom_data=Read_24LC02B(addr_ptr);                 

    // Write to MCP23008 I2C I/O
    Write_MCP23008(GPIO,eeprom_data);        

    // Increase the Serial EEPROM Index Pointer
    if (++addr_ptr >= MAX_DATA) addr_ptr=0;                    

    // Read the MCP3221 I2C 12-Bit ADC
    delay_value=Read_MCP3221();            

    // Read the MCP9801 I2C Digital Temp Sensor
    if (++tsample > TEMP_SAMPLE) {
      temp_value=Read_MCP9801(&dvalue);
      temp=temp_value & 0x7F;
      sign=temp_value & 0x80;         

      // Reset Temperature Sample
      tsample=0;
    }        

    // Display to I2C PCA8574 I/O LCD
    LCD_putcmd(LCD_HOME,LCD_2CYCLE);       // LCD Home
    LCD_putcmd(LCD_NEXT_LINE,LCD_2CYCLE);  // Goto Second Line

    switch(disp_stat) {
      case 0:                              // Display Temperature
        LCD_puts("Temp:"); 

        // Put negative sign
        if (sign == 0x80) {
          LCD_putch('-');
          temp = ~(temp & 0x7F);
        } else {
          LCD_putch(' ');
        }

        // Now put the Temperature value;
        LCD_puts(num2str(temp,2));        // Display Temperature
        LCD_putch('.');
        LCD_puts(num2str(dvalue,3));      // Display the decimal part
        LCD_putch(0xDF); LCD_putch('C');  // Put Degree and Centigrade sign
        break;
      case 1:                             // Display 10-Bit PIC18K14K22 ADC Value
        LCD_puts("10B ADC: ");
        LCD_puts(num2str(adc_value,1));   // 10-Bit ADC Value for DAC
        break;
      case 2:                             // Display 12-Bit MCP3221 ADC Value
        LCD_puts("12B ADC: ");
        LCD_puts(num2str(delay_value,1)); // 12-Bit ADC Value for Delay
        break;
    } 

    // Used 12-bit ADC for Loops Delay
    delay_ms(delay_value);
  }
}

/* EOF: i2cmaster.c */

The Microchip PIC18F14K22 MSSP Module

The Microchip PIC18F14K22 microcontroller I2C peripheral actually is the part of Master Synchronous Serial Port (MSSP) peripheral inside the PIC18F14K22 microcontroller. Each module could be operated in one of the two modes: Serial Peripheral Interface (SPI) or Inter-Integrated Circuit (I2C). The I2C module could support both I2C master and I2C slave modes. For the purpose of this tutorial we will only focusing on the I2C Master mode.

To initialize the I2C peripheral inside the PIC18F14K22 microcontroller we need to enable the SDA and SLC pins for the I2C master operation using the Synchronous Serial Port Enable bit (SSPEN) and set the I2C master clock frequency using the MSSP Synchronous Serial Port Mode Select bits SSPM<3:0> in control register 1 (SSPCON1). Next we set the Baud Rate Register (SSPADD) so the I2C master will generate the required I2C frequency clock at 100 kHz. These following are the complete C code for initializing the PIC18F14K22 microcontroller MSSP in I2C master mode.

// Initial PIC18F14K22 I2C bus Ports: RB4 - SDA and RB6 - SCL, Set as Input
TRISBbits.TRISB4 = 1;
TRISBbits.TRISB6 = 1;
// Initial the PIC18F14K22 MSSP Peripheral I2C Master Mode
// I2C Master Clock Speed: 16000000 / ((4 * (SSPADD + 1)) = 16000000 / (4 * (39 + 1))
SSPSTAT = 0x80; // Slew Rate is disable for 100 kHz mode
SSPCON1 = 0x28; // Enable SDA and SCL, I2C Master mode, clock = FOSC/(4 * (SSPADD + 1))
SSPCON2 = 0x00; // Reset MSSP Control Register
SSPADD = 39; // Standard I2C Clock speed: 100 kHz
PIR1bits.SSPIF=0; // Clear MSSP Interrupt Flag

Transmitting and receiving I2C data to and from the I2C bus is done in the SSPBUF register, therefore by placing the data on this register we instruct the MSSP module to start the I2C master data transmission. The I2C write is implemented in i2c_write() function as shown on these following C codes:

// Send the Data to I2C Bus
SSPBUF = data;
if (SSPCON1bits.WCOL) // Check for write collision
  return;
while(SSPSTATbits.BF); // Wait until write cycle is complete
i2c_idle(); // Ensure the I2C module is idle

The Buffer Full bit (BF) in MSSP status register (SSPSTAT) is used to check the I2C master buffer. When the I2C master has shift all the 8-bit data through the MSSP shift register (SSPSR) then the BF bit will be cleared (logic “0”). Next we use ACKEN (Acknowledge Sequence Enable), RCEN (Receive Enable), PEN (Stop Condition Enable), RSEN (Repeated Start Condition), SEN (Start Condition Enable) bits in MSSP control register 2 (SSPCON2) , and read/write information bit (R/W) in SSPSTAT register to check the I2C bus idle condition. This I2C bus idle checker is implemented in i2c_idle() function.

The I2C master reading could be done by setting the RCEN bit in SSPCON2 register and wait until the BF bit in SSPSTAT register is being clear, next the 8-bit data could be retrieve from the SSPBUF register. The I2C read is implemented in i2c_read() function as shown on these following C codes:

// Ensure the I2C module is idle
i2c_idle();
// Enable Receive Mode
SSPCON2bits.RCEN = 1; // Enable master for 1 byte reception
while(!SSPSTATbits.BF); // Wait until buffer is full
return(SSPBUF);

The I2C master START, STOP, and slave NACK signal are implemented in i2c_start(), i2c_stop(), and i2c_slave_ack() functions. Now as you understand the principle of how the PIC18F14K22 microcontroler MSSP in I2C master work, next I used this following block diagram template to perform the PIC18F14K22 MSSP I2C master write and read operation to various I2C slave devices used in this project. For the complete information about the MSSP in I2C mode please refers to the Microchip PIC18F14K22 microcontroller datasheet.

The Microchip 2KBit I2C Serial EEPROM

The 24LC02B is a 2 Kbit Electrically Erasable PROM (EEPROM). The device is organized as one block of 256 x 8-bit memory with a I2C interface

Because the Microchip 24LC02B I2C EEPROM only has 2Kbit (256 byte) memory, therefore we only need 8-bit address pointer to write or read data to or from the EEPROM. For the I2C EEPROM that has memory more than 256 byte such as Microchip 24AA128 EEPROM (128 Kbit) you have to use the 16-bit address pointer this mean first you need to send the upper 8-bits of the EEPROM address then next the lower 8-bits of the EEPROM address.

In this project I used the 24LC02B I2C EEPROM to store the 8-bits LED data pattern and next retrieve the data to be displayed by the Microchip MCP23008 I2C 8-bits I/O expansion. The complete 24LC02B write and read (I2C indirect data read) operation is implemented in Write_24LC02B() and Read_24LC02B() functions. For more information on how to operate the Microchip 24LC02B please refers to the 24LC02B datasheet.

The Microchip MCP9801 I2C Temperature Sensor

The Microchip MCP9801 is the I2C temperature sensors which convert temperatures between -55°C and +125°C to a digital word. It provide an accuracy of ±1°C (max.) from -10°C to +85°C

The Microchip MCP9801 could be configured to output 9-bits, 10-bits, 11-bits, or 12-bits digital precision temperature reading, in this project I used the 10-bits precision mode which could measure the temperature from +0.25 °C to 125 °C. Setting the MCP9801 could be done by choosing the correct register through the MCP9801 register pointer. These following C codes show how to configure the MCP9801 temperature sensor to use the 10-bits precision and set the MCP9801 register pointer to point to the temperature register for the temperature data reading:

// Initial the MCP9801 10-bit I2C Digital Temperature Sensor
Write_MCP9801(CONF_REGISTER,0b00100000); // Used 10-Bit Mode
Write_MCP9801(TEMP_REGISTER,0b00000000); // Select Temperature Register

The MCP9801 also capable to rise an alert on ALERT pin (pin 3 is an open collector), If the ambient temperature exceeds the limit preconfigured maximum temperature on the temperature limit-set register. The complete MCP9801 write and read operation is implemented in Write_MCP9801() and Read_MCP9801() functions. For more information on how to operate the Microchip MCP29801 please refers to the MCP29801 datasheet.

The Microchip MCP3221 I2C Analog to Digital Converter

The Microchip MCP3221 is a successive approximation A/D converter with 12-bit resolution, unlike the other I2C device, the MCP3221 doesn’t have the A2, A1, and A0 pins, instead this address is hard coded inside the chip (A2=1, A1=0, and A0=1). Therefore you could only connect one MCP3221 temperature sensor on the same I2C bus, according to the datasheet if you need different address setting you need to contact the Microchip Technology Inc.

Because the MCP3221 has 12-bits ADC output, therefore we need to read it twice. First the MCP3221 will send the upper 8-bits (high byte) data and next the lower 8-bits data (low bytes).

I used the MCP3221 ADC reading to give a delay inside the infinite loop; this will give an adjustable speed of the LED chasing effect display (attached on MCP23008 8-bit I/O expansion) according to the ADC value returned by MCP3221. The complete MCP3221 read operation is implemented in Read_MCP3221() function. For more information on how to operate the Microchip MCP3221 please refers to the MCP3221 datasheet.

The Microchip TC1321 I2C Digital to Analog (DAC) Converter

The Microchip TC1321 is a serially accessible 10-bits voltage output digital-to-analog converter (DAC). The DAC produces an output voltage that ranges from ground to an externally supplied reference voltage. The TC1321 is ideal to be used in the Programmable Voltage Sources and the Digital Controlled Amplifiers/Attenuators. The TC1321 also doesn’t have the A2, A1, and A0 pins, instead this address is hard coded inside the chip (A2=0, A1=0, and A0=0). Therefore you could only connect one TC1321 DAC on the same I2C bus.

The 10-bits TC1321 I2C DAC circuit above used the Microchip MCP1525 2.5 volt voltage reference for the DAC circuitry to convert the 10-bits digital input to the corresponding analog output according to this following formula:

Vout = Vref (DATA/1024)

Therefore using the voltage reference of 2.5 volt, the DAC voltage output (Vout) will swing from 0 volt to maximum of 2.5 volt according to the 10-bits data input. In this project the 10-bits data input is provided by the PIC18F14K22 internal 10-bits ADC peripheral on channel 0 (AN0) which is connected to 10K user trimpot and the TC1321 DAC output voltage (Vout)  is simply connected to the DVM (Digital Volt Meter). The complete TC1321 write operation is implemented in Write_TC1321() function. For more information on how to operate the Microchip TC1321 please refers to the TC1321 datasheet.

The Microchip MCP23008 I2C 8-Bit I/O Expander

The Microchip MCP23008 I2C 8-bits I/O Expander has the 3-bit configurable address (000 to 111) which provides up to 7 devices that could be attached simultaneously on the same I2C bus. This will give you a total of 56 ports. MCP23008 also come with 11 internal register to control its operation.

Before we could use the MCP23008 I/O ports first we have to configure the I/O port as the output port or input port using the MCP23008 I/O direction register (IODIR). To write or read the MCP23008 I/O port we have to use the general purpose I/O register (GPIO).

In this project I used all the MCP23008 I/O ports expander to display the LED pattern retrieved from the 24LC02B EEPROM. These following C codes show of how to initialize the MCP23008 I/O ports as an output ports.

// Initial the MCP23008 8-bit I2C I/O Expander
Write_MCP23008(IODIR,0b00000000);
Write_MCP23008(GPIO,0x00); // Reset all the Output Port

The complete MCP23008 write operation is implemented in Write_MCP23008() function. For more information on how to operate the Microchip MCP23008 please refers to the MCP23008 datasheet.

The Philips PCA8574 I2C 8-bits I/O Expander

Comparing to the Microchip MCP23008, the Philips PCA8574 is more simple to operate all you need is to directly write or read to or from the PCA857A I/O register ports. Similar to the MCP23008 I/O expander, the PCA8574 I/O expander has 3-bit configurable address (000 to 111) which provides up to 7 devices that could be attached simultaneously on the same I2C bus. In this project the Philips PCA8574 8-bits I/O expander is used to control the Hitachi HD44780U (or its compatible) 2×16 LCD. The complete I2C 2×16 LCD schematic is shown on this following picture.

In this project I used the 4-bit modes of Hitachi 44780U 2×16 LCD driver to send the data from the PCA8574 I/O expander. You could read the complete explanation of how to drive the Hitachi 44780U 2×16 LCD on this following article AVR LCD Thermometer Using ADC and PWM Project.

The complete PCA8574 write operation is implemented in Write_PCA8574() function. For more information on how to operate the Philips PCA8574 please refers to the PCA8574 datasheet.

Inside the Infinite Loop

The program begins with the port initialization and continues with the PIC18F14K22 MSSP and the PIC18F14K22 ADC peripheral configuration. The internal ADC peripheral is used to provide the Microchip TC1321 I2C DAC with the required 10-bit digital input where the ADC value is taken from the 10K user trimpot connected to the AN0 port. The following is the C code to initialize the PIC18F14K22 ADC peripheral:

// Init the PIC18F14K22 ADC Peripheral
ADCON0=0b00000001; // ADC port Channel 0 (AN0), Enable ADC
ADCON1=0b00000000; // Use Internal Voltage Reference (Vdd and Vss)
ADCON2=0b10101011; // Right justify result, 12 TAD, Select the FRC for 16 MHz

We use the “right justify” ADC result setting to get the 10-bit result and store it on the adc_value variable inside the infinite loop as shown on this following C code:

// Read the PIC18K14K22 10-Bit AN0 ADC Input
ADCON0bits.GO_nDONE=1;
while (ADCON0bits.GO_nDONE) continue; // Wait conversion done
adc_value=ADRESL; // Get the 8 bit LSB result
adc_value += (ADRESH << 8); // Get the 2 bit MSB result

The ADC result (adc_value) is used as the input to Microchip TC1321 I2C DAC as shown in this following C code:

// Write to the TC1321 10-Bit DAC
Write_TC1321(adc_value);

You could read more information about using the Microchip PIC ADC peripheral on these following articles PIC Analog to Digital Converter C Programming and PIC18 Microcontroller Analog to Digital Converter with Microchip C18 Compiler

After initializing the PIC18F14K22 ADC peripheral, the program continue initializing the PCA8574 I2C 2×16 LCD, Microchip 24LC02B 2KB I2C EEPROM, Microchip MCP23008 8-bit I2C I/O Expander, Microchip MCP9801 I2C Temperature Sensor, and Microchip TC1321 I2C DAC consecutively. Next the program enters the infinite loop and performs reading and writing to all of these I2C devices.

Now you could watch and enjoy the entire project presented here on this following video, where you could watch how we use the Microchip PIC18F14K22 MSSP I2C master to control various I2C slave device simultaneously.

The Final Though

The I2C nowadays is become a popular choice of the embedded system interface because of its main advantages of using 2 wire bus interface. On this tutorial you’ve learned to utilize the Microchip PIC18F14K22 microcontroller MSSP in I2C master mode to control various I2C devices simultaneously. Hopefully what you’ve learned here could extend your knowledge of how to use various I2C slave devices out there in your next embedded system project.

Bookmarks and Share


bookmark bookmark bookmark bookmark bookmark bookmark bookmark bookmark bookmark bookmark