Así que a estas alturas de la película ya sabes de Arduino y otros MCUs. Las entradas y salidas analógicas no tienen secretos para ti, y controlas como nadie un PWM, los LED y mover motorcicos. ¡Enhorabuena, ya sabes picar líneas de código en Arduino!
Pero tú, que siempre quieres mejorar, ahora quieres aprender a programar (bien) en Arduino. Y, amigo/a mío, programar es mucho más que juntar un montón de líneas de código que «simplemente funcionan». Si quieres jugar en primera división, tu código tiene que estar limpio.
Hablando en general del mundo de la programación, cada vez más gente programa. Lo cual es bueno… pero también conlleva que la calidad media de la programación está bajando.
Dentro del mundo de Arduino y resto de MCUs, y todas las muchas (¡muchísimas cosas!) que debemos a este genial ecostistema, lo cierto es que la falta de calidad media roza lo escandalosos.
Esto se debe, en buena parte, a una existencia muy marcada y extendida de dos tipos de perfil de usuarios:
- Usuario novel:, que aprende con esfuerzo por sus propios medios, lee tutoriales por Internet, y evoluciona como buenamente puede.
- Usuario experto: que aprendió hace 40 años, usando un PIC que tenía un bit y medio, y sigue manteniendo las mismas prácticas.
Pero, como decíamos, tú quieres programar mejor. Y eso te ha traído a esta entrada donde vamos a ver unos pocos consejos y pautas para que puedas mejorar tu forma de programar.
1) MANTÉN TU CÓDIGO LIMPIO Y ORDENADO
Tu código tiene que estar limpio, ordenado, y tener un orden lógico. Un código limpio contribuye a menores tiempos de desarrollo, menos errores, mejor mantenibilidad, mayor facilidad para ampliar…
Que no te importe de vez en cuando parar y refactorizar, es decir, reescribir y reordenar partes de tu programa para que haga lo mismo, pero con un código más limpio y estructurado.
Vamos a ver algunos consejos sobre limpieza y orden de código.
SÉ CONSISTENTE CON TU ESTILO
Cada maestrillo tiene su librillo, y no seré yo quien abra ahora el debate de las normas de estilo. Me da igual que uses 3 espacios o una tabulación, que pongas la ‘{‘ en la misma línea o en la de abajo, que llames las constantes EN_MAYUSCULAS o AsiEscritas.
Pero eso sí, sea las que seas las que elijas, úsalas siempre igual en todo tu programa. No mezcles estilos, y ahora esto así, y ahora esto asa.
FUNCIONES CORTAS
El consejo más importante que te voy a dar hoy, mantén tus funciones cortas. No hace falta más que ver un programa formado por un loop de 5 pantallas de largo, para saber que tu código no está bien.
A este tipo de código largo, entremezclado, sin estructura, se le llama Spaghetti Code.
Una función larga supone más dificultad para entender y mantener, tanto para el siguiente como para para ti mismo dentro de unos meses, cuando vuelvas a abrir tu código y te toque entender lo que hoy tienes tan claro y has escrito de tirón «to’seguido».
Pero, además, denota en sí mismo un problema de concepto. Una función debería ser encargada de realizar una única acción en el programa. Divide tu código en funciones cortas como GetTemperatura(), FillScreenBuffer(), ParseMessage()…
DIVIDE TU CÓDIGO EN ARCHIVOS
¡Divide y vencerás! Una de las pauta de programación por excelencia. El IDE de Arduino permite trabajar con código en varios archivos. Por supuesto, otros IDEs más avanzados también. Así que divide tu código en ficheros, y añádelos con #include.
En el mejor de los casos tu Sketch principal debería estar prácticamente vacío. Si no os apetece complicaros con ficheros .h, y .cpp y salvaguardas, etc, pues no lo hagáis. Simplemente crear un fichero .hpp, movéis a él parte de vuestra funciones, e «includazo» que te cascas.
Al dividir el código en ficheros, intenta que estén agrupados por bloques lógicos. Por ejemplo, la parte relacionada con medir temperaturas en un fichero, la parte de RF en otro, la parte de comunicación en otra, la de mostrar en pantalla en otra.
Y, si es posible, intenta crear módulos reaprovechables, agrupando en algunos archivos funciones que no dependan de tu programa en particular. Así podrás reaprovechar el código para el siguiente programa con poca o ninguna modificación.
CREA UN FICHERO DE CONSTANTES
Una buena costumbre es crear un fichero Constans.h que contenga todos los valores fijos que intervienen en tu programa.
// ejemplo
const int NUM_CHANNELS = 3;
const int NUM_MEASURES = 10;
const float SENSOR_GAIN = 3.5;
// .... más constantes
No solo mejora le legibilidad del código, si no que te permite controlar el comportamiento de todo tu programa modificando un único fichero.
PUEDES USAR OBJETOS
El uso de objetos es perfectamente posible en Arduino. A diferencia de lo que se suele creer, no es necesario crear una librería para ello. Puedes crear un objeto en cualquier fichero .hpp, e incluso en el propio .ino de tu programa.
2) EL BUEN CÓDIGO SE ENTIENDE AL LEERLO
Filosofía Agile pura y dura. El buen programador hace código que se entiende al leerlo. A esto contribuye mucho lo que hemos visto en el código anterior de que tu código no sea Spaghetti, las funciones sean cortas, y sean responsables de una única acción.
Lo vamos a ver en estos consejitos que ayudarán a que tu código sea más Agile.
MINIMIZA LOS COMENTARIOS
¿¿¿Cooooomoooo??? Pero si los comentarios son buenos, ¡a mí me han dicho que el código tiene que estar comentado! Bien, pues te han mentido. Los comentarios son una parte más de código que ocupa espacio y hay que mantener.
Hay pocas cosas más ridículas que:
// leo la temperatura del sensor
int temperatura = analogRead(pin_sensor)
// multiplico la temperatura por dos
temperatura = temperatura * 2;
En el mejor de los casos los únicos comentarios que hacen falta son los que documentan clases y funciones, justo encima de ellas. Pero cada vez que usas un comentario dentro de una función, un gatito developer llora.
Si sigues el resto de consejos de esta sección y la anterior, no necesitarás usar comentarios, porque el objetivo es que tu código se explique solo.
USA NOMBRES DE VARIABLES REPRESENTATIVOS
Esta me encanta, insistiré hasta el fin de los días, y no me cansaré de ser cansino con esto. No me seáis vagos, y poner nombres representativos a las variables.
// nope
int v = analogRead(ps);
float r = v * f;
float t = r * fs;
// sipe
int voltaje = analogRead(PIN_SENSOR);
float raw_temperature = voltaje * CONVERSION_C_MV;
float temperature = raw_temperature * FACTOR_SENSOR;
¿Comparamos legibilidad? En primero ni idea de que está haciendo, y en el segundo se puede hasta entender ¡Y eso que es un ejemplo inventado! Pues eso, no ahorréis letras en los nombres de las variables, o las acabaréis gastando en comentarios.
USA NOMBRES DE FUNCIONES REPRESENTATIVOS
Primo hermano del anterior, lo mismo es aplicable para los nombres de funciones. Aunque, en general, este error es menos común y la gente suele ahorrase menos letras.
// nope
int DoMagic(int ps) { … }
// sipe
int GetTemperature(int pin_sensor) { … }
Pero misma filosofía, pon nombre a las las funciones que expliquen que van a hacer. Ayuda mucho, cuando las funciones son cortas y hacen una única acción, como hemos visto antes.
LIMPIEZA EN PARÁMETROS DE FUNCIONES
Respecto a los parámetros de función, aparte de que a estas alturas no os va a sorprender que digamos que pongáis nombres representativos y autoexplicativos a los parámetros.
// nope
int GetTemperatura(int ps, int i, int d) { … }
// pseeee
int GetTemperature(int pin_sensor, int interval, int delay) { … }
// sipe
int GetTemperature(Config configuration) { … }
Pero, además, también conviene que uséis el menor número de parámetros. Cero parámetros es perfecto. Uno, está bien. Dos pasable. Tres, piénsatelo. Más de tres, no. Si necesitas tres o más, plantéate crear una estructura que los contenga.
EVITA LOS NÚMEROS «MÁGICOS»
Un número mágico es ese que ves en mitad del código y no sabes de donde viene. Evita… no… huye como un loco de ellos. Siempre encapsúlalos en una constante que la identifique, te permita buscarla, y cambiarla desde un único sitio.
//nope
int temperatura = tension * 3 * 1.674;
//sipe
int temperatura = tension * NUM_CHANNELS * CONVERSION_C_MV
NO TE EVITES VARIABLES INTERMEDIAS
Típico fallo de novato, encadenar llamadas de función para evitar crear variables intermedias. Es más, en general, evita las líneas excesivamente largas.
// ejemplo 1
//nope
bool result = Validate(GetTemperature(GetVoltaje(PIN_SENSOR)));
//sipe
int voltage = GetVoltaje(PIN_SENSOR);
float temperature = GetTemperature(voltage);
bool isValid = Validate(temperature);
// ejemplo 2
//nope
if(GetMeasure(pin_sensor) { …}
//sipe
auto isValid = GetMeasure(PIN_SENSOR);
if(isValid) { … }
Al usar variables intermedias me estás «contando la historia» de lo que estás haciendo. No necesitas comentarios, lo leo y lo entiendo. Si te preocupa la eficiencia, el compilador hará su magia y ambas funciones serán exactamente iguales al pasar al procesador.
USA ENUMERACIONES
Las Enumeraciones están para algo, y en Arduino parece que a veces se les tenga fobia. Te cuesta 5 segundos hacerte una enumeración.
// nope
if( Key == 0) MotorUp();
// sipe
enum Control_Key { UP, DOWN, LEFT, RIGHT }
if( Key == Control_Key.UP) MotorUp();
No solo es que se entienda mejor, y me estés contando lo que quieres hacer sólo con leerlo. Es que, además, me evitas que se me ocurra poner un 5 porque no sé qué narices es un Key. Y si un día tienes que cambiar las asignaciones de las teclas, sólo tienes que modificar la enumeración y todo el código que hagas seguirá funcionando.
UNA ÚNICA SALIDA POR FUNCIÓN
Evitad las funciones que tengan múltiples puntos de retorno. Combinadas con funciones largas, hacen muy difícil entender el código. Si tenéis una función en la que, dependiendo de muchas cosas, devuelve un valor u otro, crear una variable resultado y así dejáis claro lo que estáis haciendo.
// nope
int miFuncion()
{
if(esto) return 5;
//... más codigo
for(loquesea)
{
if(otraCosa) return 10;
}
if(aún otro condicional) return 12;
//... mucho más codigo
}
//sipe
int miFuncion()
{
int result;
if(esto) result = 5;
//... más codigo
for(loquesea)
{
if(otraCosa) result = 10;
}
if(aún otro condicional) result = 12;
//... mucho más codigo
return result;
}
Admitimos como excepción (daría para debate) los return de rechazo. Es decir, un return puesto al principio de la función que «rebota» la ejecución, siempre que esté muy claro lo que hace de un vistazo.
// pseee aceptable
void MoverMotor(int parametro)
{
if(parametro < 10) return;
if(parametro > 210) return;
//... mover motor
}
AJUSTA EL SCOPE DE LAS VARIABLES
Seguramente habréis leído que el uso de variables globales es malvado. En algunos sentidos esto es cierto, y en programación se tiende a evitarlas. O, al menos, a disimular para que parezca que no las estamos usamos (tojó, tojó… patrón singleton… tojó).
En el caso de un MCU no tenemos tantas herramientas para evitar el uso de variables globales, pero está claro que abusar de ellas está mal. Por ejemplo:
// nope
float raw;
float a;
float b;
float temperature;
void CalculateTemperatura()
{
temperatura = a * raw + b
}
// sipe
float CalculateTemperature(float raw, float a, float b)
{
return = a * raw + b
}
Sin embargo, por otro lado, estáis programando un MCU que no tiene la memoria y la potencia de un ordenador. Así que si tienes una variable/objeto (digamos) un servo, o un httpClient, y realmente es algo que forma parte de todo tu programa, que tampoco se te caigan los anillos para definirlos como variable global (sin abusar).
3) KEEP IT SIMPLE
KISS, otra pauta de programación (y no solo de la programación). Cuanto más sencillo sea algo, mejor. Menos probabilidades de fallos, de que alguien se equivoque, de perder tiempo en entender lo que otro ha escrito, o tú mismo cuando lo abres tres años más tarde y dices… ¿iba borracho o qué?
Métetelo en la cabeza, más simple es mejor. Y vamos a ver unos consejitos al respecto.
EVITA LAS OPERACIONES DE BIT Y LAS MÁSCARAS
No hay nada que le resulte más ‘fashion’ a un programador old schooool, o a uno que se ha dejado seducir por la locura matemática de su profe, que cascarte un código tipo
int T = (REG >> 2 & b00001001 ) || ( TMCR_6 >> 3 & b00100101 );
Me da igual el código que sea, ese en concreto me lo he inventado. Cada vez que veáis algo así, seguro que podía sustituirse con 2-3 líneas de operaciones aritméticas y un condicional.
EVITA LOS PUNTEROS
Otro ¿coooomooo? Pero si C está basado en punteros, y me han dicho que es «el top» de programar en Arduino. Sí, pero Wiring de Arduino está basado en C++, no en C. Y parte de las mejoras que tuvo C++ fue minimizar el uso de punteros.
De hecho, la mayor parte de la informática moderna parece estar destinada a evitar decir la palabra «puntero», precisamente porque se convierten rápidamente en un infierno de legibilidad.
C++ incorporó las referencias, y el 80% de las cosas que haces con punteros puedes hacerlas con una referencia. Así que no te acostumbres a usar punteros «porque sí». Al contrario, usa punteros sólo cuando «no quede otro remedio».
EVITA TOCAR LOS TIMERS
En cualquier programación de MCUs tradicional, los Timer eran una parte fundamental para conseguir distintas funcionalidades.
Sin embargo, Arduino, al menos los Atmega 328p 32u2, etc, van muuuuy excasos de Timers. Además, estos son usados por muchas librerías y funciones del ecosistema (millis y delay, sin ir más lejos) tanto internas como externas.
Así que hacerme caso, no juguéis con los Timers si no queréis que os pasen cosas raras. Además, podéis hacer casi todo lo que podríais hacer manipulando un Timer de otras formas.
Si de verdad necesitas un ajuste fino (¡muy fino!) del tiempo, elije un MCU con más Timers. Y aún así, tendrás librerías para que nunca, jamás de los jamases, tengas que ir jugando con los registros.
PRECREMENTO Y POSTCREMENTO
El operador ++i y i++ es una peste que arrastra la programación, posiblemente solo superada por el desastre de null (otro día os cuento eso). Son fuente frecuente de error ¡y lo «divertido» que es hasta que lo encuentras!
//nope
var miValor = miVector[i++] * 5.7;
//asi si
i++;
auto miValor = miVector[i] * 5.7;
USA LOS BUCLES QUE TOCAN
Parece bastante evidente, pero tienes tres tipos de bucles. De lejos el más usado es el for, seguido de while y después dowhile (y el primero sería foreach, pero por hablar en MCUs lo vamos a dejar).
Usa cada uno para lo que son, y evita hacer «genialidades» como:
// nope, don't do that
for(;; condition)
{
}
Tócatelos. Solo comentaré al respecto que más adelante ver el apartado «porque algo funcione no significa que esté bien».
USA LOS CONDICIONALES QUE TOCAN
Similar al anterior, tenéis el condicional if de toda la vida, el switch, y el operador ternario ?. Usa cada uno cuando convenga, y evita poner condicionales anidados como un loco.
USA LA CLASE STRING
Es increíble los chorizos de código que se ven a veces por algo que podría haber sido resuelto en una línea con la clase String. La clase String tiene una inmerecida mala fama por dar problemas con la memoria dinámica.
Parte de razón tiene, String es un objeto, y para crear un nuevo String hace falta posicionarlo en la memoria. Si nos ponemos a crear String como posesos, y como Arduino no tiene un gestor de memoria como un ordenador normal, vas creando objetos en distintas posiciones de memoria. Y, aunque los liberes, vas dejándola como un queso de gruyere y al final, aunque tienes memoria disponible, en ninguna te cabe tu nuevo String.
Entonces tienes un bonito colapso de memoria, que Arduino soluciona reiniciando y librándose de la pequeña tortura que ha supuesto tu sketch volviendo a iniciarse desde con la memoria limpita y a estrenar.
Eso es así, y no se puede discutir. Pero la culpa no es de la clase String, es tuya por ponerte a crear objetos como un loco. La clase String, bien usada, es un gran aliado que os evitará enormes cantidades de código.
4) NO TE EMPARANOIES CON LA EFICIENCIA
Este es el punto más polémico y, a muchos, el que más les cuesta entender. Limpieza de código frente a eficiencia de ejecución. Estaremos de acuerdo en que las dos son deseables, y siempre hay que tener un ojo sobre ambas.
La buena noticia, muchas muchas veces las dos van de la mano, y el código limpio es más eficiente. Pero (tenía que haber un pero), también con frecuencia tendréis un compromiso entre limpieza y eficiencia.
Atención, esto puede hacer petarle la cabeza a alguno. Cuando tengas que elegir entre limpieza y eficiencia debes elegir la limpieza, salvo en ocasiones que puedes contar con los dedos de la mano.
OPTIMIZA CON CABEZA
¿Qué sentido tiene escribir un fragmento de código mucho más complejo para ahorrar 10 nano segundos, si justo a continuación le vas a meter un delay(…)?
// un aplauso para esta falta de sentido común
int T = (REG >> 2 & b00001001 ) || ( TMCR_6 >> 3 & b00100101 );
delay(5000);
O, qué sentido tiene que te estés matando por rascar nano segundos aquí, cuando luego pasas un objeto por la pila y esa operación es un millón de veces más lenta que todo lo que hayas podido arañar anteriormente.
En cualquier programa, tanto de Arduino como otro, la mayor parte del tiempo se dedica en ciertas operaciones especialmente lentas. Es en esas en las que debes prestar atención. El resto, no pierdas el tiempo con ellas.
DEJA AL COMPILADOR HACER SU TRABAJO
Hace mucho tiempo, los compiladores no eran tan listos como ahora y los programadores hacíamos muchos guarradas truquitos en el código para mejorar la eficiencia. No quedaba otra. Pero, los tiempos han cambiado, y los compiladores son mucho más inteligentes de lo que eran.
No intentes «hacer trampas» para intentar hacer que tu programa sea más rápido. Es más, si intentas «hacer el trabajo» del compilador, a menudo encontrarás que tu código acaba siendo más lento y no más rápido, porque has cortado ciertas optimizaciones que podía hacer, pero como lo has escrito en plan raruno no ha podido hacerlas.
Confía en el compilador y déjale hacer su trabajo. Escribe tu programa de la forma más limpia, y deja el lenguaje máquina para las máquinas.
EVITA USAR #DEFINE
#define es una directiva de precompilador. Sirve para decirle al compilador, no compiles esto, compila esto otro, y generar programas distintos para el mismo código. Sin embargo, en Arduino estarás hiper acostumbrados a usar #define para definir constantes. No lo hagas.
// nope
#define pin_sensor 5;
// sipe
const int PIN_SENSOR = 5;
¿Diferencias? Bueno, pues aparte de una conceptual importante, porque para ti PIN_SENSOR realmente es una constante, la mayor diferencia que con #define el compilador va donde pone pin_sensor a empotrarle un 5. Sin hacer comprobación de tipado, ni advertencias de cast, etc.
Por otro lado, si estáis usando un compilador más avanzado que el IDE estándard (como Visual Studio) el IDE te sugerirá el nombre de la variable, te permitirá renombrarla, al ponerte encima te dirá el tipo que es, entre otras muchas ventajas que tienes cuando usas cada cosa para lo que es.
Dejar los #define para cuando realmente quieras modificar el programa, por ejemplo, para compilar una rama si es un Arduino y otra rama para un ESP32. Para el resto, const.
PLANTÉATE SI ES LA MÁQUINA ADECUADA
Llegados a este punto, si realmente estás haciendo un programa en el que Arduino va muy muy justo de potencia… igual deberías plantearte en vez de gastarte 1.5, gastarte 2.5 y comprarte algo más potente.
Suele decirse, el hierro (hardware) es barato, los programadores son caros (teniendo en cuenta el tiempo de desarrollo, mantenimiento, etc). Sin que te sirva de excusa para hacer código lento, la verdad es que es cierto.
También ha tener en cuenta, no es lo mismo algo que haces en una tirada de 1-10 unidades, que cuando vas a fabricar 10 mil. Ahí, si te conviene matarte en eficiencia, porque cada céntimo cuenta.
¿Y si el tiempo es el personal, porque lo haces en tu casa por disfrutar, y no hay un «coste»? Pues más aún. No gastes tu tiempo en optimizar algo para que quepa en un procesador de 1.5, si sería más fácil en uno más grande.
Por no decir, que todos estos procesadores van a evolucionar. Dentro de X años (ya está pasando) se parecerán más a programación «normal» (pc, web) que de MCUs. ¿A que prefieres dedicar tu tiempo, a algo que desaparecerá en unos años? Invierte tu tiempo en programar bien. Si la máquina te limita demasiado, no descartes cambiar de máquina.
QUE ALGO FUNCIONE NO SIGNIFICA QUE ESTÉ BIEN
El código tiene que estar limpio, no vale con que funcione. Si no está limpio, está mal, igual que si no funcionará. Grábate esto en la cabeza. Si no te lo crees, repítelo una y otra vez, hasta que te convenzas.
No vale de excusa «hace 30 años que lo hago así», razón de más para que te pienses que igual la película ha cambiado bastante. Tampoco sirve el «lo he visto en Internet» o «mi profesor me lo dijo». Desarrolla tu propia opinión.
Tampoco vale como excusa el «yo es que mi código super complejo lo entiendo» porque eres muy listo. En primer lugar… no es verdad. Te has pegado 30 minutos probando un código de una línea, cuando si lo hubieras escrito en 3 líneas te hubiera costado 1 minuto.
Además, si otro tiene que leerlo, no va a entender nada. Pero, es más, tú mismo cuando lo vuelvas a coger, te preguntarás «qué narices hace esa línea». Cada vez que tu programa falle, volverás a plantearte… ¿será esta parte de aquí? Y eso, no es programar bien.
Y si, es verdad, funciona. Ese código larguísimo con variables de dos letras, sin funciones ni objetos, con números por medio, arrejuntado con cinta americana, funciona. Pero no, no está bien.
Programar es más que hacer código complicado que funciona. Es estructurar, abstraer, y hacer un código limpio que se explica por sí mismo. Sigue los consejos de esta entrada, funciones cortas, variables con nombres, código que se entienda, no repetitivo, etc.
CONCLUSIÓN
Hemos visto algunas pautas y consejos para programar más limpio, y ayudar a mejorar la calidad de tu código como programador. Por supuesto, son eso consejos, no leyes.
Sabemos que no es lo mismo cuando un programador de Adafruit usa un bucle largo en una librería porque lo necesita realmente, a que lo haga alguien que está aprendiendo porque no sabe que conviene evitarlo.
Tampoco es lo mismo cuando haces un prototipo de 1 unidad, o una serie de 10 mil. Ni cuando haces un código para enseñar en una clase, que es lógico que haya más comentarios. Ni es igual que sepas cómo hacer código limpio y tengas que saltártelo en un trozo, a hacerlo por desconocimiento.
La mayoría de las pautas que hemos visto provienen del mundo del Agile, y recogen algunas de las principales corrientes que hace años imperan en la programación de «normal» (PC, web). Dan para debate, pero, en general, tienen un elevado grado de consenso.
En la programación de Arduino y MCUs en general, estas corrientes les cuesta más llegar. Parte del motivo es el perfil de usuario (muy senior, o muy novel). Otra parte importante es la diferencia de recursos disponibles en un MCU frente a un ordenador.
Sin embargo, las fronteras entre ambos mundos cada vez se desdibujan más. Y, de igual manera que es importante programar «limpio» en desarrollo «normal» (PC, Web), también es importante que os acostumbréis a hacerlo en MCUs, Arduinos, etc.
Consejo final, conviene que veas otros lenguajes de programación. Aprended C# o Java, aprended desarrollo Web. Veréis cómo se estructura el código en un lenguaje de alto nivel, o en un framework como React, Angular o VueJs. Esto cambiará tu forma de programar, y te hará mejorar la calidad de tus programas en Arduino y otros MCUs.
www.luisllamas.es