I2C en PIC utilizando CCS como master y slave

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)

Entradas relacionadas

12 comentarios en «I2C en PIC utilizando CCS como master y slave»

  1. Saludos, excelente trabajo, me preguntaba ¿Es posible cambiar de master a esclavo o de esclavo a master en tiempo de ejecución? suponiendo que durante el programa el dispositivo debe funcionar un tiempo como esclavo y otro como master, gracias por compartir!

    1. Hola Javier,
      Gracias por tu comentario. Me alegro que pueda serte de ayuda.

      En cuanto al cambio dinámico entre máster y slave, nunca lo he probado y desconozco si es posible. Todo es cuestión de ir al datasheet del micro en cuestión y ver si una vez el programa está corriendo te deja cambiar los valores del registro que habilitan las funciones hardware de máster o de slave.

      De todas maneras, y aunque fuese posible, la sincronización de ambos microcontroladores te va a complicar mucho el código. Por lo que yo te recomiendo que decidas cuál de los dos va a ser el máster y que crees una rutina en el máster para monitorizar el estado del slave. De esta manera podrás saber cuándo el slave tiene datos que enviar al slave y no será necesario cambiar los roles, que es algo que no es nada habitual en un protocolo como I2C.

      Si de verdad necesitas una comunicación half-duplex te recomendaría que buscases otro protocolo de comunicación. Aunque en el entorno embebido SPI, I2C y Serial son los que se utiliza en el 99% de los casos y suelen ser suficientes para la mayor parte de aplicaciones.

  2. Hola Javier,

    Una pregunta:

    cuando defines writeBuffer, que código estas usando dentro del mismo, ya que no se como puedes escribir «Por el Slave todo bien» con ese código.

    Y por otro lado, donde está el mensaje «Saludos desde el Master» en el código que no lo puedo ver?

    Gracias, un saludo,

    Pablo

    Perdona mi ignorancia.

    1. Hola Pablo,

      En C para definir un char y asignarlo se hace de la siguiente manera:

      char c = 'A';
      

      También se puede hacer del siguiente modo:

      char c = 0x41;
      

      o también:

      char c = 65;
      

      y esto es porque un char en C no es más que un número. Existe un estándar de codificación llamado ASCII en el que cada letra se codifica como un número (ya sea en formato decimal o hexadecimal) y es el que utiliza C para imprimir un carácter. Puedes ver una tabla de equivalencias en este enlace.

      Cuando definí el vector writeBuffer, por ejemplo en el slave, hice lo siguiente:

      int writeBuffer[] = {0x50,0x6F,0x72,0x20,0x65,0x6C,0x20,0x53,0x6C,0x61,0x76,0x65,0x20,0x74,0x6F,0x64,0x6F,0x20,0x62,0x69,0x65,0x6E};
      

      En este vector cada número hexadecimal corresponde a un carácter como puedes comprobar:
      0x50 = P
      0x6F = o
      ox72 = r
      0x20 =
      0x65 = e

      Espero que haya aclarado tu duda.

      Saludos,
      R

  3. Hola,
    Lo primero es agradecer por tu gran trabajo, ha sido de mucha utilidad.

    Por otra parte he encontrado un detalle en tu código y lo he modificado un poco para obtener exactamente el resultado esperado.

    El problema ¿?
    Al aplicar tu codigo directamente sobre mis uControladores pic16f886 (haciendo previamente el reemplazo de los operadores «&lt»,»&gt» por respectivamente), he notado una discrepancia en los datos impresos por el Master via UART. La diferencia es que el terminal (Realterm) musetra que el primer Byte es un «ETB» y los siguientes entregan el mensaje esperado. Lo anterior descrito puede ser visto en la siguiente imagen.
    http://imageshack.com/a/img921/7784/dCfFFU.png

    Pues esperaba que algo asi pasara ya que el mensaje que envia el Slave en respuesta a la solicitud del Maestro contiene 22 Bytes y no 23 como se definió en ambas partes.
    ……………………………………………………………………………………………………………………………………………….

    La solución 🙂
    Para solucionar este Byte indeseado hice modificaciones leves que se muestran a continuación.

    MASTER
    – «int n_read=23;» se cambia por «int n_read=22;»
    ……………………………………………………………………………………………………………………………………………….
    SLAVE
    – if(state >= 0x80){ //Master is waiting for data
    i2c_write(writeBuffer[state – 0x81]);
    if ((state-0x80) == max_written){
    //printf(«\nFull data sent\r\n»);
    }
    }

    La rutina anterior la he reemplazado por la siguiente. Dado que se sabe el largo exacto del arreglo que contiene la frase a enviar me pareció mejor usar un ciclo «FOR».

    – int e;
    for(e=0;e<22;e++){
    i2c_write(writeBuffer[e]);
    delay_us(2); // Este delay le da tiempo a que acabe y se estabilice la instrucción
    //"i2c_write"
    }
    ……………………………………………………………………………………………………………………………………………….

    Con las modificaciones anteriormente mencionadas el resultado es el siguiente.
    http://imagizer.imageshack.us/a/img922/843/HCH7os.png

    Espero ver mas de tu trabajo pronto, muchas gracias 🙂
    Saludos.

    Nota:
    Hardware utilizado
    – 2xPIC16F886
    – FT232
    – Pickit3
    Software utilizado
    – MPLAB X IDE v3.51
    – CCS compiler v5.015
    – Realterm v2.0.0.70

    Si es necesario puedo compartir tambien los codigos fuente.

  4. Olá, já algum tempo estou tentando elaborar uma saída de pulsos do contador up/down. A saída deve ser por I2C e a cada mudança de contagem, deve ser transmitido os pulsos binários referente a contagem. Exemplo: Parei a contagem em «1250», então o ultimo trem de pulsos pelo barramento I2C foi «10011100010», se o numero do display for «8955» os pulsos em binário foi de «10001011111011» e assim por diante. Possuo o contador com display de 7 segmentos, mas preciso montar essa parte do código I2C que faz a saída da contagem em binário. O projeto é compilado por CCS C Compiler.

  5. Hello, for some time I am trying to elaborate an output of counter pulses up / down. The output must be by I2C and with each count change, the binary pulses referring to the count must be transmitted. Example: I stopped the count at «1250», then the last pulse train on the I2C bus was «10011100010», if the display number was «8955» the torque pulses was «10001011111011» and so on. I have the counter with 7-segment display, but I need to mount that part of the I2C code that outputs the count in binary. The project is compiled by CCS C Compiler.

  6. buenas tardes,
    me gustaria saber como podria leer por ejemplo, el puerto adc en una vez convertido a float( es decir ya la tension, si fuese temperatura) en el slave desde el master.

    gracias

  7. Muchas gracias por su código Ing. Rubén Sánchez, me sirvió como referencia para terminar el mío y solucionar el problema.

Responder a Euclides Mazzucatto Cancelar la respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.