En esta entrada vamos a ver cómo elegir números enteros o en coma flotante por puerto serie, dentro de esta serie de entradas destinada a profundizar en el uso del puerto serie.
Al igual que en la entrada anterior, donde vimos cómo recibir caracteres y cadenas de texto, si buscamos por internet veremos muchos códigos para realizar esta acción, con mayor o menor grado de acierto. Esto es debido a que existe más de una forma de realizar el proceso, cada uno con sus ventajas y desventajas.
En esta entrada presentaremos los principales métodos para recibir un número por puerto serie, y cuando resulta más conveniente emplear uno u otro.
La funciones DEBUG están únicamente para visualizar los resultados, cuando uséis este código podéis quitar las partes relativas a su definición y uso.
Recibir un único dígito
Igual que cuando vimos cómo recibir un único carácter, recibir un único dígito es muy sencillo y eficiente. De hecho, es prácticamente idéntico a recibir un carácter, pero posteriormente eliminamos el valor de ‘0’ para convertir el char a integer.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
}
void loop()
{
if (Serial.available())
{
char data = Serial.read();
if (data >= '0' && data <= '9')
{
data -= '0';
DEBUG((int)data);
}
}
}
La eficiencia del método es máxima, ya que apenas contiene procesamiento. La mayor desventaja, lógicamente, es que únicamente podemos recibir un dígito. Aunque generalmente es insuficiente, puede ser útil para recibir una transmisión sencilla, como la opción de un menú o el número de parpadeos de un LED.
Recibir con clase Serial
Si queremos recibir números de más de un dígito, que es lo normal, tenemos varias opciones disponibles. La primera que vamos a ver es emplear las funciones de la clase Serial, Serial.ParteInt() y Serial.ParseFloat().
Así, el siguiente ejemplo recibe un número entero.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
Serial.setTimeout(50);
}
void loop()
{
if (Serial.available())
{
int data = Serial.parseInt();
DEBUG((int)data);
}
}
Mientras que la recepción de un float sería la siguiente,
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
Serial.setTimeout(50);
}
void loop()
{
if (Serial.available())
{
float data = Serial.parseFloat();
DEBUG(data);
}
}
Esta forma de recepción es muy empleada porque resulta cómoda y sencilla de integrar en nuestro código. Sin embargo, tiene bastantes desventajas que hacen que sea desaconsejable su uso.
En general, es un método bastante lento. Además, es bloqueante, es decir, el programa queda parado esperando el número hasta que salta el timeout.
Por otro lado, si recibe un único salto de línea se devolverá un cero, lo cual es un problema en un buen número de aplicaciones.
Recibir con clase String
Una forma mucho más adecuada es emplear las funciones de la clase String. Empleamos la función Serial.readStringUntil() para recibir una línea en un String, y después las funciones String.toInt() y String.toFloat() para convertir a número. El ejemplo para número entero es siguiente.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
}
void loop()
{
if (Serial.available() > 0)
{
String str = Serial.readStringUntil('\n');
int data = str.toInt();
DEBUG(data);
}
}
Mientras que el ejemplo para coma flotante sería el siguiente.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
}
void loop()
{
if (Serial.available() > 0)
{
String str = Serial.readStringUntil('\n');
float data = str.toFloat();
DEBUG(data);
}
}
La función es prácticamente igual de sencilla que el ejemplo anterior, pero normalmente es más eficiente. Además, tenemos mayor control porque podemos elegir cualquier carácter como separador (algo útil, por ejemplo, para dividir un texto separado por comas).
En general, debería ser vuestra primera opción para recibir números por puerto serie.
Recibir en un char array
Igual que cuando al recibir cadenas de texto, si no podemos o no queremos usar la clase String podemos usar un char array y las funciones atoi() y atof() para conseguir funcionalidades similares, como vimos en a la hora de convertir texto en números.
La lectura de un integer sería la siguiente
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
Serial.setTimeout(50);
}
void loop()
{
if (Serial.available())
{
char buffer[7];
Serial.readBytesUntil('\n', buffer, 7);
int data = atoi(buffer);
DEBUG(data);
}
}
Mientras que la lectura de un float se haría así.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
Serial.setTimeout(50);
}
void loop()
{
if (Serial.available())
{
char buffer[7];
Serial.readBytesUntil('\n', buffer, 7);
int data = atof(buffer);
DEBUG(data);
}
}
Sin embargo, tenemos el inconveniente de tener que definir el tamaño del buffer por nosotros mismos, algo que no es necesario con la clase String.
La velocidad es similar a la conseguida con la clase String, ya que la clase String emplea las funciones atoi y atof internamente. De hecho, es levemente superior porque requiere un menor procesamiento.
Por su parte, la capacidad de control del proceso es similar a la que obtendríamos con la clase String.
Por tanto, en general preferiremos usar la clase String, y sólo usaremos estos métodos cuando no queramos o no podamos emplear la clase String.
Recibir con método naive
Finalmente, tenemos el método “naive” que recordamos es la forma elegante de decir hacer el proceso “a mano”. Por lo menos resulta interesante para entender cómo funcionan los métodos anteriores internamente.
Aquí tenemos un código para hacer la recepción y conversión a un número entero.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
}
int data = 0;
bool isNegative = false;
void loop()
{
while (Serial.available())
{
char incomingChar = Serial.read();
if (incomingChar >= '0' && incomingChar <= '9')
data = (data * 10) + (incomingChar - '0');
else if (incomingChar == '-')
isNegative = true;
else if(incomingChar == '\n')
{
data = isNegative? -data : data;
DEBUG(data);
data = 0;
isNegative = false;
}
}
}
El código necesario para recibir un número en coma flotante es ligeramente más largo.
#define DEBUG(a) Serial.println(a);
void setup()
{
Serial.begin(9600);
}
float data = 0;
int dataReal = 0;
int dataDecimal = 0;
int dataPow = 1;
bool isDecimalStage = false;
bool isNegative = false;
void loop()
{
while (Serial.available())
{
char incomingChar = Serial.read();
if (incomingChar == '-')
isNegative = -1;
else if (incomingChar == '.' || incomingChar == ',')
isDecimalStage = true;
else if (incomingChar >= '0' && incomingChar <= '9')
{
if (isDecimalStage == false)
dataReal = (dataReal * 10) + (incomingChar - '0');
else
{
dataDecimal = (dataDecimal * 10) + (incomingChar - '0');
dataPow *= 10;
}
}
else if (incomingChar == '\n')
{
data = (float)dataReal + (float)dataDecimal / dataPow;
data = isNegative ? -data : data;
WATCH_STOP;
DEBUG(data);
dataReal = 0;
dataDecimal = 0;
dataPow = 1;
isDecimalStage = false;
sign = 1;
}
}
}
El método naive no es mucho más rápido que las funciones atoi o atof y ,por extensión, que los métodos toInt() o toFloat() de la clase String(), ya que la implementación mostrada es similar a la empleada internamente por estas funciones.
La mayor ventaja es que tenemos un control total sobre el proceso. Por ejemplo, en el caso de nuestro proceso de conversión a número con coma flotante hemos permitido que admita ’.’ y ’,’ como separador decimal, sin una pérdida sustancial de eficiencia.
Pero en general, salvo que tengamos requisitos especiales que justifiquen la conversión a mano, tendremos eficiencias similares empleando las funciones de la clase String.
Descarga el código
Todo el código de esta entrada está disponible para su descarga en Github.