16F876 & nRF24L01

Este proyecto está realizado exclusivamente en CCS C (PIC C).

Para facilitarnos la programación vamos a usar una librería llamada "lib_rf2gh4_10.h". Se encarga totalmente de la comunicación con el nRF24L01 a través del SPI por hardware del PIC. Esta librería pertenece a la empresa Bizintek Innova, S.L. y es utilizada en su proyecto del robot mOway. La librería es -Open Source- por lo que se puede distribuir y modificar bajo los términos de la licencia GNU. Es la mejor y más fácil de manejar que he probado. Personalmente llegué a ella gracias a una estupenda web "Aquí hay apuntes", donde se hace un ejercicio parecido a este, pero con el 16F877. También puedes consultar el foro de esa misma web haciendo clic aquí.

Más adelante verás un enlace de descarga de todo el proyecto, donde he modificado la librería para el PIC que estamos usando con el hardware propuesto: dos 16F876A, tal como ves en el esquema, aunque este ejercicio es totalmente compatible con cualquier PIC 16F87x (excepto 16F870 y 16F871), porque los puertos, los TRIS y la interrupción conservan las mismas direcciones.

Tenemos cinco patillas para controlar el SPI del nRF24L01, más una de interrupción que nos dirá si ha recibido algún dato y que procesaremos de forma inmediata. El emisor enviará datos y el receptor los procesará. Esto quiere decir que no hay comunicación bidireccional porque lo que nos interesa es ver cómo funcionan exclusivamente en modo emisor y modo receptor. A partir de este ejercicio luego podrás hacer comunicaciones bidireccionales (tipo half-duplex), modificando los programas según tus necesidades.

Como controlaremos el SPI del 16F876 por hardware con el nRF24L01, has de saber que hay tres patillas que no se puede modificar de lugar y son:

Se han de corresponder con el hardware del PIC que estés usando, en este caso es el 16F876.

Existen dos patillas del SPI que sí puedes modificar de lugar y son CE y CSN. Esas patillas del nRF24L01 las pongo en C2 y C1 respectivamente. Todo cambio que hagas en el hardware has de tenerlo presente en la librería, pero vamos a ir al grano porque eso se explicará más adelante en la segunda sección, esta es la primera.

Veamos el código del emisor:

#Include <16F876A.h>
#fuses NOWDT, XT, PUT, NOPROTECT, NODEBUG, NOBROWNOUT, NOLVP, NOCPD, NOWRT
#use delay(clock=4000000)
#include "lib_rf2gh4_10.h"
#byte porta=0x05
#byte portb=0x06
#int_ext                     // Esta rutina está para un futuro si haces comunicaciones bidireccionales.

void int_RB0()               // Si te da error esta línea, sustituir por: void int_ext_isr(void).
{                            // Se encargaría de la recepción de datos.
   int8 ret1;
   
   ret1 = RF_RECEIVE();
   if ( (ret1 == 0) || (ret1 == 1) )
   {
      do
      {  
         ret1 = RF_RECEIVE(); 
      }while ( (ret1 == 0) || (ret1 == 1) );
   }  
}

void main()
{  
   int8 ret2;
   RF_INT_EN();              // Habilitar interrupción RB0/INT.
   RF_CONFIG_SPI();          // Configurar módulo SPI del PIC.
   RF_CONFIG(0x40,0x01);     // Configurar módulo RF canal y dirección de recepción. Para recibir datos tiene la dirección 0x01.
   RF_ON();                  // Activar el módulo RF.
   
   delay_ms(5);              // Le damos un mínimo de 2.5 milisegundos para que se ponga en marcha.
   
   set_tris_a(0b111111);     // Todo el puerto A como entradas.
   
   while(true)               // Bucle infinito.
   {  
      RF_DATA[0]=porta;      // Cargamos el dato en RF_DATA[0].
      RF_DIR=0x08;           // Dirección del receptor.
      ret2=RF_SEND();        // Enviar datos.
   }
}

Partes importantes del programa emisor:

* La interrupción #int_ext en realidad no llega a usarse, sólo la he puesto en el programa para futuros proyectos en el que quieras hacer recepción de datos además de emitir. Si le llegara algún dato, tal y como está en el programa, lo único que haría sería vaciar el buffer de recepción, nada más; no afecta al programa principal. Pese a lo dicho, no elimines esta parte del programa principal. Luego, cuando todo te funcione bien, haz los cambios que quieras.

* RF_INT_EN(); Ese comando habilita la recepción de datos por interrupción de la RB0/INT, pero como sólo vamos a emitir, está puesta simbólicamente para futuros proyectos en el que sí haya recepción de datos. No elimines esta línea hasta que todo te vaya bien.

* RF_CONFIG_SPI(); Configura el SPI entre el PIC y el nRF24L01, definiendo los pines, TRIS e interrupción. Estas definiciones están en la librería "lib_rf2gh4_10.h".

* RF_CONFIG(0x40,0x01); Se refiere "0x40" al canal de comunicación; este siempre ha de ser el mismo para todos los PICs que se quieran comunicar entre sí. "0x01" es la dirección de recepción. Aunque en este caso, por ser exclusivamente emisor, no va a recibir datos, de todas formas hay que ponerle una dirección de recepción.

* RF_ON(); Activa el módulo RF. Después de este comando conviene que transcurra unos 2.5 milisegundos antes de hacer envíos o recepciones de datos.

* set_tris_a(0b111111); Todo el puerto A lo convertimos en entradas de datos. Sólo tiene 6 bits, pero para ver el funcionamiento de este ejercicio es más que suficiente.

* RF_DATA[0]=porta; Carga lo que hay en el puerto A en el buffer RF_DATA[0]. Hay 8 bytes: de RF_DATA[0] a RF_DATA[7]. Si quieres aprovechar todo el ancho de banda que da el nRF24L01 tendrías que usar los 8 bytes disponibles para enviar información. También lo puedes ver como la posibilidad de enviar 8 bytes de una vez.

* RF_DIR=0x08; Este comando le está diciendo a qué dirección ha de ir la información que quieras enviar. Piensa que puedes tener varios PICs de recepción (127 teóricos y 32 prácticos), pues irá al PIC que tenga configurado como recepción la dirección 0x08.

* ret2=RF_SEND(); Envía la información. "ret2" nos sirve para saber qué ha sucedido al intentar enviar. Nos dará '0' si se ha enviado y ha recibido ACK (confirmación de recepción); '1' si ha enviado pero no ha recibido el ACK; y '2' si no ha podido ser enviado (fallo del hardware del emisor). Se podría poner "RF_SEND();" a secas para enviar, pero ortodoxamente lo correcto es hacerlo tal como lo he puesto.

* La carga de datos en RF_DATA[] y la dirección de envío RF_DIR y RF_SEND(), han de estar dentro del programa juntos, como si fuese un bloque, porque RF_DATA y RF_DIR no contendrán valores constantes todo el tiempo, sino que puede variar, por ello has de cargarlos cada vez que vayas a hacer un envío de datos.

Veamos ahora el código del receptor:

#include <16F876A.h>
#FUSES NOWDT, XT, PUT, NOPROTECT, NODEBUG, NOBROWNOUT, NOLVP, NOCPD, NOWRT
#use delay(clock=4000000)
#include "lib_rf2gh4_10.h"
#byte porta=0x05           // Dirección del puerto A.
#int_ext                   // Interrupción del módulo RF.

void int_RB0()             // Si te da error esta línea, sustituir por: void int_ext_isr(void).
{
   int8 ret1;
   ret1 = RF_RECEIVE();
   if ( (ret1 == 0) || (ret1 == 1) )         // Tanto si hay recepción simple o múltiple, leer datos.
   {
      do
      {    
         porta=RF_DATA[0];                   // El puerto A contendrá el valor que le llegue del emisor, a través de RF_DATA[0].
         ret1 = RF_RECEIVE();                // "ret1" nos dirá si hay recepción simple, múltiple o no hay datos para leer.
      }while ( (ret1 == 0) || (ret1 == 1) ); // Mientras haya datos para leer, seguir leyendo.
   }  
}

void main()                  // Programa principal.
{
   set_tris_a(0b000000);     // Todo el puerto A como salida.
   porta=0;                  // Inicialmente lo ponemos a cero.
   
   RF_INT_EN();              // Habilitar interrupción RB0/INT.
   RF_CONFIG_SPI();          // Configurar módulos SPI del PIC.
   RF_CONFIG(0x40,0x08);     // Configurar módulo RF (canal y dirección).
   RF_ON();                  // Activar el módulo RF.
   
   while(true);              // Bucle infinito. 
       
}

Partes importantes del programa receptor:

* La interrupción #int_ext se encargará de recibir los datos.

* No debemos de confundir "ret2" (del emisor) con "ret1" (del receptor), porque en esas variables se guardará también valores "0, 1, 2...". Tienen significados distintos para emitir que para recibir. En el receptor, cuando vemos esto: ret1 = RF_RECEIVE(); "ret1" nos estará diciendo que, si el valor es '0' hay entrada simple de datos (una sola recepción, o dicho de otra manera, los bytes RF_DATA[0] a RF_DATA[7] están listos para ser leídos); si nos da '1' significa que hay más de una entrada (cada entrada es de 8 bytes) para ser leída, es decir, una segunda o tercera tanda, y no soporta más de tres niveles, el resto se perderían. Cuando "ret1" nos dé el valor '2', significa que no hay entradas para leer, o sea, no ha habido entrada nueva de datos. En el programa, tanto si hay entrada simple como múltiple, me quedo con lo último que ha llegado. El buffer (compuesto de 8 bytes por 3 niveles) queda vacío una vez que sale del "while ( (ret1 == 0) || (ret1 == 1) );". Hay que procurar que el emisor sea un poco más lento que el receptor para evitar saturar al receptor y eso puede suceder cuando ret1 nos devuelve el valor '1' (recepción múltiple).

* RF_INT_EN(); Este comando habilita la recepción de datos por interrupción de la RB0/INT. Una vez que llegan los datos los puedes tratar inmediatamente, en la misma interrupción, en el ejemplo propuesto lo hacemos de esta forma.

* RF_CONFIG_SPI(); Configura el SPI entre el PIC y el nRF24L01, definiendo los patillajes, TRIS e interrupción. Estas definiciones están en la librería "lib_rf2gh4_10.h" y las puedes modificar si tu hardware es diferente.

* RF_CONFIG(0x40,0x08); Se refiere "0x40" al canal de comunicación, este siempre ha de ser el mismo para todos los PICs que quieras comunicar entre sí. "0x08" es la dirección de recepción. Recuerda que en el emisor pusimos: RF_DIR=0x08; como dirección de envío, así que si el transceptor receptor está configurado con esa dirección podrá recibir esos datos, si no fuese esa dirección el transceptor entenderá que se refiere a otro y no a él, por tanto no atendería a esos datos.

* RF_ON(); Activa el módulo RF. Después de este comando conviene que transcurra unos 2.5 milisegundos antes de hacer envíos o recepciones de datos.

* set_tris_a(0b000000); Todo el puerto A lo convertimos en salidas de datos. Sólo tiene 6 bits, pero para ver el funcionamiento de este ejercicio es más que suficiente. Esas salidas son a través de LEDs. Recuerda que RA4 es de colector abierto, por tanto hemos de poner una resistencia a positivo para poder alimentar ese LED; en el esquema está detallado.

* do {...} while( (ret1 == 0) || (ret1 == 1) ); Mientras haya datos por leer eso estará haciendo. Hay que entender bien una cosa: el nRF24L01 envía y recibe 8 bytes de un vez (es un array de 8 bytes y nosotros sólo usamos el primero), esto significa que si hay más datos por leer, leerá otros 8 bytes y hay que cargar esos datos en variables o un array que hayamos declarado nosotros antes de hacer ret1 = RF_RECEIVE(); Esto sucede cuando hay entrada múltiple de datos y sólo soporta 3 niveles, si hubiese más de tres tandas, el resto se perderían. Es por ello que para un funcionamiento simple y sin problemas se recomienda que el emisor emita un poco más lento de lo que el receptor puede asimilar. En el ejemplo, lo que hace es quedarse con lo último que haya recibido.

* En la recepción de datos, si esos datos no son tratados inmediatamente los puedes perder. Es decir, que una vez que le lleguen datos al receptor has de cargarlos en una variable o matriz (según el caso) antes de que pase al programa principal. En el ejemplo que pongo de recepción no hace falta porque en la misma recepción (dentro de la interrupción) hago que ese dato salga por el puerto A.

Un detalle sobre el hardware.

En nuestro proyecto tenemos siempre a los PICs como maestros SPI y los transceptores como esclavos. Dado que los PICs que estamos usando funcionan con 5 voltios y los nRF24L01 funcionan con un rango de 3.3 voltios puede haber una descompensación de voltajes, en el sentido de: ¿cuándo considerar que hay un 1 lógico y cuándo un 0 en una entrada del PIC? Esto puede suceder en la única entrada SPI que tiene el PIC, que es la patilla MISO. Es por ello que se ha de poner una resistencia a positivo de 5 voltios en dicha patilla (tal como está en el esquema, al comienzo de esta página), para compensar la tensión y cuando le llegue un 1 lógico al PIC lo lea como real. Tener este simple gesto nos ahorrará muchos quebraderos de cabeza. Lo ideal sería utilizar conversores de nivel, son bidireccionales, es decir, no importa si es una entrada o salida de información, y a día de hoy están realmente baratos. Un esquema-ejemplo cualquiera sería la siguiente imagen:

Está claro que si adaptas los programas y librería a otros PICs que sí pueden funcionar a 3.3V o tolera esas tensiones (como por ejemplo Arduino) no te hará falta nada de esto y podrás conectarlo directamente, sin resistencia de polarización o conversores de tensión.

Un consejo: Muchas personas han tenido problemas para hacer funcionar otros proyectos parecidos y es que estos transceptores son bastante puñeteros. Mi consejo es realizar el proyecto tal como lo presento usando el esquema, los programas y librería que está en el zip de descarga. Una vez te funcione todo bien, puedes comenzar a realizar los cambios necesarios hasta convertirlo en tu propio proyecto. Si eres amante del paracetamol y el ibuprofeno entonces hazlo todo según tu criterio desde el comienzo.

Contiene código fuente, librería, HEX y esquema.

Si quieres ver un proyecto parecido con otra PIC distinta y profundizar en el uso de la librería haz clic aquí.