En esta entrada vamos a ver cómo enviar y recibir mensajes por puerto serie realizando la comunicación directamente con bytes.
En las últimas entradas hemos visto métodos para recibir números y cadenas de texto en un microprocesador como Arduino. Incluso hemos visto cómo recibir arrays separados por comas u otro separador (aunque normalmente no sea una buena idea).
Sin embargo, ya habíamos avanzado que a niveles avanzados lo normal es trabajar directamente con envíos de bytes. Y es así, los chicos/as grandes mandan bytes.
¿Es más complejo realizar la comunicación con bytes? No, para nada. Incluso es más sencillo que el tratamiento directo como texto. Pero eso sí, tenemos que entender que es lo que estamos haciendo.
En general, a la hora de establecer una comunicación en la que nosotros controlamos tanto el receptor como el emisor lo primero que vamos a hacer es definir una trama, es decir, una secuencia específica de bytes en un orden que transmiten un mensaje con una estructura determinada.
En esta entrada vamos a ver cómo enviar y recibir secuencias de bytes, y cómo generar y tratar estas secuencias de bytes partir de nuestras variables (int, float, etc).
En próximas entradas veremos cómo enviar un array de números (sin necesidad de emplear un array separado por comas), y en la siguiente el envío de agrupaciones arbitrarias de variables mediante una estructura.
Enviar una serie de bytes
Enviar una secuencia de bytes es sencillo ya que la librería Serial proporciona la función write(byte*, int) que realiza justamente esta función.
const byte data[] = {0, 50, 100, 150, 200, 250};
const size_t dataLength = sizeof(data) / sizeof(data[0]);
void setup()
{
Serial.begin(9600);
Serial.write(data, dataLength);
}
void loop()
{
}
Recibir una serie de bytes
Recibir una secuencia no es mucho más complicado, ya que también disponemos de la función read(byte*, int).
Sin embargo integrarlo en nuestro código es algo más complejo ya que, a diferencia del emisor que es un agente activo (sabe el momento en que quiere realizar el envío), el receptor es un sujeto pasivo (debe comprobar si existe un mensaje disponible a la vez que sigue ejecutando su bucle de control, con mayor o menor grado de asincronía).
Por ejemplo, el siguiente código emplea una función TryGetSerialData() que llamamos desde el bucle principal. Si el número de bytes disponible es mayor que el número de bytes esperado, se realiza la lectura y devuelve true. En caso contrario, devuelve false.
El bucle de control emplear el valor devuelto para realizar la acción oportuna cuando se recibe un paquete de datos, en el ejemplo representado por la función OkAction().
const int NUM_BYTES = 3;
byte data[NUM_BYTES];
bool TryGetSerialData(const byte* data, uint8_t dataLength)
{
if (Serial.available() >= dataLength)
{
Serial.readBytes(data, dataLength);
return true;
}
return false;
}
void OkAction()
{
}
void setup()
{
Serial.begin(9600);
}
void loop()
{
if(TryGetSerialData(data, NUM_BYTES))
{
OkAction();
}
}
¿Qué hacemos con los bytes recibidos?
Perfecto, ya sabemos enviar y recibir secuencias de bytes. Pero ¿Cómo generamos estos paquetes a partir de nuestras variables? y ¿Cómo los convertirnos otra vez en variables cuando los hemos recibido? Aquí llega la parte algo “molesta”, que lleva siendo un problema en la informática desde que el mundo es mundo (bueno, al menos el mundo informático).
En función de la arquitectura del procesador que estemos empleando, las distintas variables están representadas por un diferente número de bytes. Por ejemplo, en un procesador una variable int puede tener 16 bits, y en otro 32 bits. Esta pequeña diferencia puede suponer muchos quebraderos de cabeza, aunque podemos controlarlo fácilmente siempre que mantengamos este punto en mente.
Por ese motivo es recomendable emplear las variables más específicas como uint8_t, uint16_t, que indican inequívocamente el número de bits empleados para almacenar la variable.
En Arduino UNO, Nano y Mega, int es equivalente a uint16_t y long a uint36_t.
Para hacer más fácil el envío podemos crearnos funciones para enviar de 1, 2 y 4 bytes.
void sendBytes(uint8_t value)
{
Serial.write(value);
}
void sendBytes(uint16_t value)
{
Serial.write(highByte(value));
Serial.write(lowByte(value));
}
void sendBytes(uint32_t value)
{
int temp = value & 0xFFFF;
sendBytes(temp);
temp = value >> 16;
sendBytes(temp);
}
Y aquí tenemos las funciones equivalentes para convertir grupos de bytes a variables de 1, 2 y 4 bytes respectivamente.
uint8_t byteToInt(byte byte1)
{
return (uint8_t)byte1;
}
uint16_t byteToInt(byte byte1, byte byte2)
{
return (uint16_t)byte1 << 8 | (uint16_t)byte2;
}
uint32_t byteToLong(byte byte1, byte byte2, byte byte3, byte byte4)
{
return byte1 << 24
| (uint32_t)byte2 << 16
| (uint32_t)byte3 << 8
| (uint32_t)byte4;
}
No obstante, existen formas más rápidas y convenientes de realizar estas conversiones, como el cast directo. Para ello será necesario que controlemos bien que el tamaño de las variables está consensuado entre emisor y receptor. Consensuado significa que no es necesario que sea idéntico, pero entonces alguno de los agentes (el emisor o el receptor) tendrá que realizar la conversión en consecuencia.
En futuras entradas profundizaremos sobre estos aspectos.
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.