ArduTips

LA RECEPCIÓN "DIRECTA"

Lo primero que cabe destacar de la recepción es que, a diferencia de la transmisión, no podemos predecir cuándo llegará algo al Serial. Por lo tanto, si sencillamente leemos un byte del puerto tenemos dos posibilidades:

  • Que haya un dato en el buffer, con lo que la lectura será instantánea.
  • Que no haya tal dato, con lo que leeremos un byte falso.
    Así que la primera conclusión que podemos sacar es que Serial.available() es imprescindible, tanto si no queremos leer un dato erróneo como si no queremos que nuestro programa permanezca detenido innecesariamente y sólo entre en la rutina de recepción cuando haya llegado algo. Sin embargo, a menudo vemos que sólo se tiene en cuenta parcialmente:
    -Primera opción… La mala:
if (Serial.available()) { 
  for (int a=0; a<30;a++) {
    miBuffer[a]=Serial.read();
  }
}

De esta forma vamos a tener muy probablemente errores de lectura. ¿Por qué? Efectivamente, entraremos en el for sólo cuando se detecta que hay al menos un byte en el buffer; pero luego intentamos leer 30 bytes del tirón sin volver a verificar si hay datos en el buffer. Entonces, esas 30 lecturas se habrán realizado en el tiempo en que por serial apenas habrán llegado uno o dos bytes. El resultado es que leeremos el primer byte correctamente, y todas, o casi todas las lecturas posteriores devolverán dato falso.

-Segunda opción… La rígida:

if (Serial.available()) { 
  for (int a=0; a<30;a++) {
    while(¡Serial.available()); // Mientras no haya dato en el buffer permanecerá bloqueado en este comando
      miBuffer[a]=Serial.read();
   }
}

Si las cosas van bien, en cuanto entremos en el for, iremos pasando los 30 datos a medida que llegan y no tendremos lecturas falsas. El problema es que si por cualquier factor se interrumpe la recepción una vez iniciada, el programa permanecerá indefinidamente parado esperando el resto de la trama.

-Tercera opción… La optimista:

if (Serial.available()) {
for (int a=0; a<30;a++) {
           miBuffer[a]=Serial.read();
           delay(1); // Una pequeña pausa* para dar tiempo a que llegue el siguiente byte
     }
}

*Habría que calcular la pausa necesaria dependiendo de la velocidad de transmisión.
Una vez entrados en el bucle, las 30 lecturas se realizarán en aproximadamente 30 milisegundos. Lo que no podremos dirimir es si alguna de las lecturas fue falsa. Deberemos, bien confiar en que nuestro canal de datos no tenga fallos, o implementar algún sistema de control de errores.

-Cuarta opción… La buena:
¿Por qué no combinar las dos anteriores, sustituyendo el delay, combinando millis + Serial.available de tal forma que si transcurren los milisegundos establecidos sin que haya llegado un dato salimos del bucle sin leer más datos? Podríamos implementarlo, pero resulta que hay varias funciones de lectura múltiple que ya implementan esa característica: readBytes, readBytesUntil, parseInt y parseFloat. La primera sería la adecuada a nuestro caso:

if (Serial.available()) {
  int x = Serial.readBytes(miBuffer,30);
  if (x==30){
    // Leidos los 30 datos
  } else {
    // Sólo llegaron sin interrupción x bytes (¿error?)
  }
}

readBytes volcará en myBuffer en este caso hasta 30 bytes, salvo que transcurra un timeout sin recibir el siguiente byte, que dejará de leer. Como además de volcar datos, devolverá un int con el número de datos leídos, podremos utilizar dicho valor para comprobar si se recibió toda la trama de forma continua.
El único pero es que si salta el timeout nuestro readBytes habrá tenido ocioso un segundo al procesador lo que, en ocasiones será asumible, y en otras no. Si está establecido que el otro dispositivo, una vez que inicia el envío de una trama lo va a hacer de forma continua, sabemos que entre byte y byte no van a transcurrir más de dos milisegundos (a 9600bps), por lo que podríamos reducir drásticamente el tiempo de timeout.
Si vamos a utilizar parseInt o parseFloat para recibir uno o varios valores, y no queremos sufrir un segundo de espera tras leer el último valor, podemos reducir el timeout, o poner carácter separador también tras el último valor de la trama, de forma que dicho valor se devuelva inmediatamente al recibir el separador, en lugar de por timeout al dejar de recibir datos.
Si vamos a recibir una trama de texto de tamaño variable con algún tipo de terminador, readBytesUntil será nuestro aliado.

LECTURA "DIFERIDA"

Hasta ahora, las lecturas que hemos realizado tienen la característica de que, si bien no se entra en rutina de recepción hasta que llega algún carácter, una vez entrados en dicha rutina, ésta acaparará la atención del procesador durante el tiempo que tardan en llegar el resto de los bytes. A menudo esto no supondrá ningún problema para el devenir del programa; pero en alguna ocasión echaremos de menos los 30 milisegundos que se comieron nuestros 30 delay(1).
Recordemos que disponemos de un buffer de 64 bytes, por lo que no es necesario leer un byte nada más llegar del puerto, si hay otras cosas más urgentes que atender. De hecho suele ser mucho más efectivo atender a Serial cuando sepamos que tenemos todo lo que necesitamos en el buffer, ya que el volcado de datos se realizará sin esperas. Por ejemplo, si estamos esperando la trama de 30 bytes anterior, lo podríamos hacer así de sencillo y rápido:

if (Serial.available()>=30) {
   Serial.readBytes(miBuffer,30);
   // resto de acciones tras recibir la trama
}

Esto por sí sólo, dependiendo de las circunstancias podría ser demasiado "optimista", ya que cuando detectamos que hay 30 bytes en el buffer, no sabemos a priori si corresponden a una trama correcta o a dos tramas incompletas. Deberíamos implementar algún método adicional, como algún byte marcador de inicio y/o final de trama. Entonces haremos la recepción de la trama en varias fases, mediante una máquina de estados:

// Nuestra trama tendrá la forma <trama de 30 bytes>
void setup()
{
  Serial.begin(9600);
}

void loop()
{
  static byte estado=0;
  const char INICIO_TRAMA = '<';
  const char FIN_TRAMA = '>';
  const int LONGITUD_TRAMA = 30;
  char myBuffer[30];
  switch (estado) {
      // Estado 0: esperando byte de inicio de trama.
      // Mientras no llegue dicho byte, descartamos datos y permanecemos en este estado.
      case 0:
        if ( Serial.available() ) {
          if (Serial.read() == INICIO_TRAMA) {
            estado = 1;
          }
        }
        break;
        // Estado 1: esperando los 30 bytes de datos
      case 1:
        if ( Serial.available() >= 30) {
          Serial.readBytes(myBuffer,30);
          estado = 2;
        }
        break;
      // Estado 2: carácter de fin de trama
      case 2:
        if ( Serial.available() ) {
          if ( Serial.read() == FIN_TRAMA) {
            // Enhorabuena. La trama se leyó correctamente.
            // Realizamos las acciones correspondientes.
          } else {
            // Mal asunto. No ha llegado el carácter que debería.
          }
          // Reiniciamos la máquina en cualquier lugar.
          estado = 0;
        }
        break;
  }
}

noter:
LA TRANSMISIÓN

. . . Podemos prever si vamos a desbordar el buffer (y por tanto a detener el flujo del programa), si Serial.availableForWrite() es menor que el número de bytes que vamos a dejar en la mesa.

Hago esta observacion, ¿ a vosotros os funciona el Serial.availableForWrite() ? porque a mí me dá el siguiente error cuando intento compilar un programa con ella:

sketch_feb20a.ino: In function 'void setup()':
sketch_feb20a:20: error: 'class HardwareSerial' has no member named 'availableForWrite'

Y lo curioso es que tras investigar un poco parece ser que no está implementada en las librerias estandar :astonished: :astonished:

Merece la pena seguir investigando,...

A mí en Stino + Arduino 1.5.0, sí me compila perfectamente, tanto para placa UNO como MEGA. ¿Qué IDE/placa estás utilizando?

Arduino 1.0.6, placa UNO.

Pero lo curioso es que me metí en Reference y en la descripcion de la funcion no avisa de que sea válida a partir de una determinada version y no antes, de ahí mi asombro al ver que no está en las librerias de mi IDE.

Edito:
Tras descargar la version IDE 1.8.1 y analizar la libreria Serial he comprobado que podemos definir el tamaño de los bufer de Transmision y de Recepcion de forma independiente, a valores mayores de 256 bytes, lo que da un margen cómodo para aquellos programas que hagan un uso intensivo de las comunicaciones serie.

La unica condicion es hacer en nuestro programa la obligada declaracion:

 #define SERIAL_TX_BUFFER_SIZE 128   // Por ejemplo
#define SERIAL_RX_BUFFER_SIZE 256   // Por ejemplo

Aunque este es un detalle que casi todos sabiamos, lo que de verdad me ha sorprendido es el tamaño que podemos asignar a estos bufers (para sistemas con mas SRAM es una gozada).

En muchas circunstancias nos encontramos con situaciones en las que representaremos mediante un número un elemento de una lista “imaginaria”. Entonces, si somos un poco ordenados (lamentablemente no es el caso del que suscribe) seguramente acompañaremos la declaración de la variable en cuestión con una serie de comentarios del estilo:

// estado: 0 ->apagado, 1->aumentando, 2->encendido, 3->reduciendo.

Luego, establecemos sentencias condicionales del tipo:

if (estado==0 && temperatura<lowLevel) estado=1;

Por supuesto que podemos comentarlo para aclararlo, pero ¿no sería maravilloso y menos propenso a errores poder escribir el comando así?

If (estado==apagado && temperatura<lowLevel) estado=aumentando;

Esto es, precisamente, lo que podemos hacer con una enumeración:

enum tipoEstado {apagado, aumentando, máximo, reduciendo} estado=apagado;

Hemos de tener en cuenta que aunque este tipo de dato internamente está utilizando un entero, no admite ser tratado como tal, por lo que no podemos utilizarlo de formas como

estado=2;
estado++;

Índice: