La comunicación I2C es muy útil a la hora de comunicarse con dispositivos de un mismo circuito. Con esta entrada quiero dar un poco de luz a este protocolo que pese a su sencillez es un auténtico misterio a la hora de implementarlo.

Cabe destacar que, atendiendo a la definición del protocolo, la dirección del slave es de 7 bits (del bit 7 al 1). De esta manera el bit 0 se utiliza para decirle al slave si el master quiere leer (bit 0 = 1) o quiere escribir (bit 0 = 1). Es por eso que en el código se utilizan dos direcciones diferentes en función de si se quiere leer o escribir.

El master escribe continuamente “Saludos desde el Master” y después lee 23 bytes del Slave, que devuelve “Por el Slave todo bien”.
Para poder cambiar el número de bytes para leer o escribir desde el master hay que cambiar las variables n_read y n_write. (Si sabéis de algún método mejor, soy todo oídos). La implementación utiliza el propio hardware del PIC (en este caso el PIC16F877) aunque se podría haber hecho mediante software y haber utilizado cualquier pin del procesador. En este caso habría que añadir la opción FORCE_SW en la inicialización del I2C.
NOTA: Solo se puede implementar I2C por software para el master. Para el Slave debe de ser forzosamente hardware.
El código para el master es:


#include "16F877.h"
#device ADC=16
 
#FUSES NOWDT                    //No Watch Dog Timer
#FUSES NOBROWNOUT               //No brownout reset
#FUSES NOLVP                    //No low voltage prgming, B3(PIC16) or B5(PIC18) used for I/O
 
#use delay(crystal=14745600)
#use rs232(baud=115200,parity=N,xmit=PIN_C6,rcv=PIN_C7,bits=8,stream=PORT1)
#use i2c(Master,Fast,sda=PIN_C4,scl=PIN_C3)
 
#define SLAVE_WRITE_ADDR 0xA0
#define SLAVE_READ_ADDR 0xA1
 
void write_slave(int address, int *buffer, int n_read){
   int n_byte = 0;
   i2c_start();
   i2c_write(address);
   for(n_byte = 0;n_byte<n_read;n_byte++)
      i2c_write(buffer[n_byte]);
   i2c_stop();
}
 
void read_slave(int address, int *buffer, int n_read){
   int n_byte = 0;
   i2c_start();
   i2c_write(address);
   for(n_byte = 0;n_byte<n_read-1;n_byte++)
      buffer[n_byte] = i2c_read();
   buffer[n_byte] = i2c_read(0);
   i2c_stop();
}
 
void print_buffer(int *buffer, int length){
   int n_byte;
   for(n_byte = 0;n_byte<length;n_byte++)
         printf("%c",buffer[n_byte]);
    printf("\r\n");
}
 
void main(){
   int n_read=23,n_write = 23;
   int writeBuffer[] = {0x53,0x61,0x6c,0x75,0x64,0x6f,0x73,0x20,0x64,0x65,0x73,0x64,0x65,0x20,0x65,0x6c,0x20,0x4d,0x61,0x73,0x74,0x65,0x72}; //"Por el Slave todo bien"
   int readBuffer[23];
   printf("MASTER\r\n");
   while(TRUE){
      write_slave(SLAVE_WRITE_ADDR, writeBuffer, n_write);
      delay_ms(100);
      read_slave(SLAVE_READ_ADDR, readBuffer, n_read);
      print_buffer(readBuffer,n_read);
      delay_ms(100);
   }
}

El Slave lo único que hace es gestionar la llegada de paquetes I2C, tanto de escritura como de lectura. Para entender un poco más el código recomiendo leer la documentación del PIC C Compiler (CCS) (pulsando F1), donde explica con detalle las diferentes funciones que se utilizan.

Faltan añadir algunas mejores en el Slave para recibir un número flexible de bytes. Ahora solo imprime los datos por consola (RS232) cuando se envían el número exacto de bytes descritos en max_written.

El código del Slave es:

#include "16F877.h"
#fuses HS,NOWDT,NOPROTECT,PUT,BROWNOUT,NOLVP
#device ADC=10
#use delay(clock=14745600)
#use rs232(baud=115200, xmit=PIN_C6, rcv=PIN_C7,ERRORS)
#use i2c(SLAVE, SDA=PIN_C4, SCL=PIN_C3,FAST=100000, address=0xA0,FORCE_HW) 
#use spi(DI=PIN_A0, DO=PIN_A1, CLK=PIN_A2, ENABLE=PIN_A3, BITS=16)
  
 int max_read = 23,max_written = 23;
 int writeBuffer[] = {0x50,0x6F,0x72,0x20,0x65,0x6C,0x20,0x53,0x6C,0x61,0x76,0x65,0x20,0x74,0x6F,0x64,0x6F,0x20,0x62,0x69,0x65,0x6E};
 
#int_SSP 
void i2c_interrupt() {
   int state;
   //Get state 
   int readBuffer[23]; 
   state = i2c_isr_state(); 
   if(state==0) //Address match received with R/W bit clear, perform i2c_read( ) to read the I2C address. 
      i2c_read(); 
 
   else if (state==0x80) //Address match received with R/W bit set; perform i2c_read( ) to read the I2C address, and use i2c_write( ) to pre-load the transmit buffer for the next transaction (next I2C read performed by master will read this byte). 
      i2c_read(2); 
 
   if(state>=0x80){ //Master is waiting for data    
      i2c_write(writeBuffer[state - 0x81]); //Write appropriate byte, based on how many have already been written 
      if ((state-0x80)==max_written){
         //printf("\nFull data sent\r\n");
      }
   } 
   else if(state>0){ //Master has sent data; read. 
      readBuffer[state - 1] = i2c_read(); //LSB first and MSB secound 
      if (state==max_read){ 
         for(int i = 0;i<max_read;i++)
            printf("%c",readBuffer[i]); 
         printf("\r\n");
      } 
   }
} 
 
void main() {
   enable_interrupts(INT_SSP);
   enable_interrupts(GLOBAL);
   printf("SLAVE\r\n");
   while(1){
 
   } 
}

El código ha sido depurado y probado utilizando Proteus 8, así que os aseguro que el código funciona, aunque si vais a copiar y pegarlo, os recomiendo que os hagáis vuestra propia cabecera y solo copiéis las funciones.

Captura de pantalla (8)