33- Optimizaci贸n del c贸digo Arduino

Con el crecimiento de habilidades y la creaci贸n de proyectos cada vez m谩s globales, se enfrentar谩 al hecho de que 鈥淎rduino鈥 ya no podr谩 hacer frente a la cantidad de c谩lculos que desea obtener de 茅l. Es posible que simplemente no sea lo suficientemente r谩pido en los c谩lculos, la actualizaci贸n de la informaci贸n en las pantallas, el env铆o de datos y otras acciones que consumen muchos recursos, o simplemente arduino se puede quedar sin memoria. Lo peor es cuando se agota la RAM: puede pasar de forma absolutamente imperceptible, y el dispositivo empieza a comportarse de forma inapropiada, se reinicia o simplemente se congela. 驴C贸mo se puede evitar esto? 隆Necesitas optimizar tu c贸digo! Hay muy poca informaci贸n sobre esto en Internet, por lo que describir茅 todo lo que encontr茅 personalmente.

Este cap铆tulo analiza la mayor铆a de las formas existentes para optimizar la velocidad de ejecuci贸n del c贸digo, en algunas de ellas pueden ahorrarse unos pocos microsegundos (0,000001 segundos). Tambi茅n hablaremos de optimizar Flash y RAM, algunos m茅todos pueden reducir literalmente unos pocos bytes. Siempre eval煤e la conveniencia de la optimizaci贸n: si hay suficiente espacio, optimice su tiempo, 隆no lo desperdicie en reescrituras sin sentido! Pero ser谩 correcto desarrollar el h谩bito de escribir de manera 贸ptima de inmediato =)


Lo que el compilador de Arduino puede manejar.

Consideramos muchas formas diferentes de optimizar el c贸digo, pero no tomamos en cuenta lo principal: 隆el compilador en s铆 hace un buen trabajo optimiz谩ndolo! Algunos de los pasos anteriores no tienen sentido, porque el propio compilador har谩 lo mismo. Pero todav铆a los desarmamos para el desarrollo general y la comprensi贸n del proceso. Ahora veamos algunas de las acciones que realiza el compilador.

Modificador vol谩tile

El compilador optimiza acciones sobre variables que no est谩n marcadas como vol谩tile ya que este es un comando directo que dice 芦no me optimices禄. Este es un punto importante, porque las acciones con tales variables ( si es necesario ) deben optimizarse manualmente. El compilador no optimizar谩 los c谩lculos, eliminar谩 las variables no utilizadas y las construcciones que las utilicen.

Eliminando variables y funciones no utilizadas

El compilador elimina las variables del c贸digo, as铆 como la implementaci贸n de funciones y m茅todos de la clase, si no se utilizan en el c贸digo arduino. Por lo tanto, incluso si incluimos una biblioteca enorme, pero usamos solo un par de m茅todos de ella, el tama帽o de la memoria no aumentar谩 en el tama帽o de la biblioteca completa. El compilador solo tomar谩 lo que se use directa o indirectamente (por ejemplo, una funci贸n llama a otra funci贸n).

Optimizaci贸n del c贸digo

El propio compilador intenta optimizar los c谩lculos tanto como sea posible:

  • Reemplaza los tipos de datos por otros m谩s 贸ptimos siempre que sea posible y no afecte el resultado.聽por ejemplo val / = 2.8345聽tarda 4 veces m谩s que聽val / = 2.0, porque聽2. 0聽fue reemplazado por聽2.
  • Reemplaza las operaciones de聽multiplicaci贸n de enteros聽con potencias de dos ( 2 ^ n ) cambio de bits.聽Por ejemplo, val * 16聽corre dos veces m谩s r谩pido que聽val * 12 porque ser谩 reemplazado por聽val << 4;
    • Nota: para operaciones de聽divisi贸n de enteros,聽dicha optimizaci贸n no se realiza y se puede hacer manualmente: val >> 4 corre聽15 veces m谩s r谩pidoque val / 16.
  • Reemplaza las operaciones de m贸dulo聽%聽por potencias de dos con una m谩scara de bits (el resto de la divisi贸n por聽2 ^ n聽se puede calcular mediante una m谩scara de bits:聽val & n).聽As铆 por ejemplo (100 % 10) tarda聽17 veces m谩s聽que (100% 8), considera esto.
  • Precalcula todo lo que se puede calcular (constantes),聽por ejemplo val / = 7.8125聽corre igual como聽val / = ( 2.5 * 10.0 / 3.2 + 12.28 * 3.2 ), porque el compilador ha calculado y sustituido el resultado de todas las acciones con constantes de antemano;
  • Utiliza una celda de dos bytes (con signo) para multiplicar y dividir n煤meros enteros.聽Esto es muy peligroso, porque el resultado puede ser mayor, pero al compilador no le importa.聽Para expresiones cuyo resultado exceda 32’768, debe forzar al compilador a asignar m谩s memoria usando( long) o de otras formas, cubrimos esto en la聽lecci贸n sobre operaciones matem谩ticas.

Condiciones, opciones y selectores

El compilador cortar谩 una rama completa de condiciones o conmutadores si est谩 seguro de antemano sobre el resultado de la comparaci贸n o selecci贸n. 驴C贸mo convencerlo de esto? As铆 es, consideremos un ejemplo elemental: una condici贸n o un interruptor (no importa) con tres opciones:

switch (num) {
  case 0: Serial.println("Hello 0"); break;
  case 1: Serial.println("Hello 1"); break;
  case 2: Serial.println("Hello 2"); break;
}
// o este otro
if (num == 0)
  Serial.println("Hello 0");
else if (num == 1)
  Serial.println("Hello 1");
else if (num == 2)
  Serial.println("Hello 2");

Si declaras num como una variable ordinaria: toda la construcci贸n, las tres condiciones o el conmutador completo se incluir谩n en el c贸digo compilado. Si num es una constante o definida con #define– el compilador cortar谩 todo el bloque de condiciones o un conmutador y dejar谩 solo el contenido que se obtiene con un determinado num. Es muy f谩cil verificar esto compilando el c贸digo y mirando la cantidad de memoria ocupada en el registro del compilador. Con este truco, puedes acelerar algunas funciones y reducir el espacio de memoria que ocupan. Consideremos un ejemplo muy 煤til: la funci贸n de lectura r谩pida del estado de un pin digital para el ATmega328:

bool fastRead(uint8_t pin) {
  if (pin < 8) {
    return bitRead(PIND, pin);
  } else if (pin < 14) {
    return bitRead(PINB, pin - 8);
  } else if (pin < 20) {
    return bitRead(PINC, pin - 14);
  }
}

La llamada fastRead ( variable ) toma 6 ciclos de procesador (0.37 渭s), la llamada fastRead ( constante )– 1 ciclo de reloj (0,0625 渭s)! Para comparar, llamar al est谩ndar digitalRead ( variable ) toma 58 ciclos, y digitalRead ( constante )– 52 ciclos. Es decir, con la ayuda de un c贸digo 贸ptimo y la comprensi贸n de la l贸gica del compilador, puede hacer que 鈥digitalRead ()鈥 sea 58 veces m谩s r谩pido que lo que ofrece la biblioteca Arduino.h, 隆sin perder nada de su usabilidad!

Si est谩 escribiendo su propia biblioteca o clase, entonces todo ser谩 un poco m谩s dif铆cil: las constantes dentro de una clase no son una buena raz贸n para que el compilador corte condiciones y conmutadores, incluso si es const y est谩 declarado en la inicializaci贸n de la clase. Para que el compilador corte una condici贸n o cambie dentro de la implementaci贸n de m茅todos de clase, necesita una constante / dise帽o o plantilla externa modelo. Perm铆tame recordarle que la plantilla tambi茅n le permite crear una matriz de un tama帽o determinado dentro de una clase, habl茅 de esto en la lecci贸n sobre bibliotecas. En general, aqu铆 abajo en el ejemplo hay una clase de prueba con lecturas digitales de diferentes opciones y los resultados del benchmark (banco de pruebas):

#define MY_PIN 3
const byte _pinCM = 3;
template <byte PIN>
class fast {
  public:
    fast(byte pin) : _pinC(pin) {_pin = _pinV = pin;}
    bool dreadVol() {return digitalRead(_pinV);}
    bool dreadVar() {return digitalRead(_pin);}
    bool dreadConst() {return digitalRead(_pinC);}
    bool dreadDefine() {return digitalRead(MY_PIN);}
    bool dreadExtConst() {return digitalRead(_pinCM);}
    bool dreadTempConst() {return digitalRead(PIN);}
    bool fastReadVol() {return fastRead(_pinV);}
    bool fastReadVar() {return fastRead(_pin);}
    bool fastReadConst() {return fastRead(_pinC);}
    bool fastReadDefine() {return fastRead(MY_PIN);}
    bool fastReadExtConst() {return fastRead(_pinCM);}
    bool fastReadTempConst() {return fastRead(PIN);}
    bool fastReadShortVol() {return fastReadShort(_pinV);}
    bool fastReadShortVar() {return fastReadShort(_pin);}
    bool fastReadShortConst() {return fastReadShort(_pinC);}
    bool fastReadShortDefine() {return fastReadShort(MY_PIN);}
    bool fastReadShortExtConst() {return fastReadShort(_pinCM);}
    bool fastReadShortTempConst() {return fastReadShort(PIN);}
    bool fastRead(uint8_t pin) {
      if (pin < 8) {
        return bitRead(PIND, pin);
      } else if (pin < 14) {
        return bitRead(PINB, pin - 8);
      } else if (pin < 20) {
        return bitRead(PINC, pin - 14);    // Return pin state
      }
    }
    bool fastReadShort(uint8_t pin) {
      return bitRead(PIND, pin); // <8
    }
  private:
    byte _pin;
    volatile byte _pinV;
    const byte _pinC=3;
};
 volatilevariableconstantdefineexternal consttemplate const
digitalRead585858525252
pinRead666111
bitRead(PIND, pin);311111
Resultados de referencia (en ciclos de CPU).

Optimizaci贸n de la velocidad.

Antes de comenzar a optimizar los c谩lculos, debe comprender por qu茅 se ralentiza: existe el tiempo de procesador, es decir, el tiempo que el procesador de computaci贸n dedica a diversas actividades. Por ejemplo, desea hacer algunos c谩lculos y mostrarlos inmediatamente. Si hay demasiados c谩lculos, tardar谩n m谩s de lo deseado y los datos se enviar谩n lentamente. De hecho, tal situaci贸n es muy dif铆cil de lograr, de todos modos, nuestro procesador realiza operaciones a una frecuencia de 16 MHz, pero una vez encontr茅 este 鈥渦mbral鈥. El proyecto se denomin贸 cubo LED, en el que se calcul贸 el comportamiento de varias decenas de part铆culas en un plano. No se calcul贸 de todos modos, pero de acuerdo con un modelo matem谩tico, con fricci贸n, 谩ngulos de inclinaci贸n, rebotes desde el borde del avi贸n, etc., el resultado se mostr贸 en una matriz de LED. Con un aumento en la cantidad de part铆culas, me enfrent茅 al hecho de que comienzan a disminuir abiertamente, es decir, la frecuencia de actualizaci贸n de la matriz se redujo dr谩sticamente. Pasemos a optimizar los c谩lculos.

Utilice variables de tipos apropiados

El tipo de variable / constante no solo afecta la cantidad de memoria que ocupa, sino tambi茅n la velocidad de los c谩lculos. Aqu铆 hay una tabla para los c谩lculos m谩s simples no optimizados por el compilador. En c贸digo real, el tiempo puede ser menor. Nota: los tiempos se dan para cristal de 16 MHz.

Tiempos de ejecuci贸n seg煤n tipo en Arduino
Tiempos de ejecuci贸n seg煤n tipo en Arduino

Como puede ver, el tiempo de c谩lculo es diferente a veces incluso para tipos de datos enteros, por lo que siempre es necesario averiguar cu谩l ser谩 el valor m谩ximo que se almacenar谩 en una variable y seleccionar el tipo de datos apropiado. Trate de no utilizar n煤meros de 32 bits donde no sean necesarios y, si es posible, no utilice float.

Al mismo tiempo, multiplicar long por float ser谩 m谩s rentable que dividir long por un n煤mero entero. Esto puede considerarse de antemano como 1 / n煤mero y multiplique en lugar de dividir en los puntos cr铆ticos del c贸digo en tiempo de ejecuci贸n. Tambi茅n lea sobre esto a continuaci贸n.

Renunciar a 芦float禄

Tambi茅n puede averiguar en la tabla anterior que el microcontrolador dedica varias veces m谩s tiempo a operaciones con n煤meros de punto flotante en comparaci贸n con los tipos enteros. El hecho es que la mayor铆a de los microcontroladores AVR (que est谩n en Arduino) no tienen soporte de 芦hardware禄 para los c谩lculos de n煤meros float, y estos c谩lculos se realizan mediante m茅todos de software no muy 贸ptimos. Por cierto, existe ese soporte en los microcontroladores ARM. 驴Qu茅 hacer? Solo evita usar float donde el problema se puede resolver con tipos enteros.

Si necesitas multiplicar, redistribuya varios float, luego puede convertirlos a un tipo entero multiplic谩ndolos por 10-100-1000, dependiendo de la precisi贸n que se necesite, calcular y luego convertir el resultado a float. En la mayor铆a de los casos, esto es m谩s r谩pido que calcular float directamente:

// digamos que necesitamos manejar el valor float del sensor
// o almacenar una matriz de dichos valores sin desperdiciar memoria extra.
// sensorRead () devuelve la temperatura en float con una precisi贸n decimal.
// Convi茅rtelo en un n煤mero entero multiplic谩ndolo por 10:
int val = sensorRead () * 10 ;  
// ahora puede trabajar con valor entero sin perder precisi贸n de medici贸n y
// puede almacenarlo en 2 bytes en lugar de 4.
// Para volver a convertirlo en flotante, simplemente divida por 10
float val_f = val / 10.0 ;

Tambi茅n existe el punto fijo: n煤meros de punto fijo. Desde el punto de vista del usuario, son fracciones decimales ordinarias, pero de hecho son tipos enteros y se calculan en consecuencia m谩s r谩pido. No hay soporte nativo de punto fijo en Arduino, pero puede trabajar con ellos usando funciones, macros o bibliotecas escritas por usted mismo, aqu铆 debajo puede encontrar mi ejemplo de trabajo que se puede usar en la pr谩ctica:

Ejemplo de trabajo en punto fijo

// macros para trabajar con punto fijo
#define FIX_BITS 8
#define FLOAT2FIX (a) (int32_t) ((a * (1 << FIX_BITS))) // transferencia de flotante a fijo
#define INT2FIX (a) (int32_t) ((a) << FIX_BITS) // transferir de int a fijo
#define FIX2FLOAT (a) (((float) (a) / (1 << FIX_BITS))) // transferencia de fijo a flotante
#define FIX2INT (a) ((a) >> FIX_BITS) // transferencia de fijo a int
#define FIX_MUL (a, b) (((int32_t) (a) * (b)) >> FIX_BITS) // multiplicaci贸n de dos fijos
void setup() {
  Serial.begin(9600);
  float x = 8.3;
  float y = 2.34;
  float z = 0;
  // primero traducir a fijo
  int32_t a = FLOAT2FIX ( x ) ;
  int32_t b = FLOAT2FIX ( y ) ;
  int32_t c = 0;
  z = x + y;   // agregar float
  c = a + b;   // agregar fijo
  // traducir fijo de nuevo a float
  float cFloat = FIX2FLOAT ( c ) ;
  // imprime el resultado para comparar
  Serial.println(z);
  Serial.println(cFloat);
}
/ *
   Pruebas de velocidad de ejecuci贸n:
   x = 8,3; // 0,75 us - asignaci贸n a flotante
   a = FLOAT2FIX (8.3); // 0.75 us - convierte el n煤mero flotante a fijo
   a = FLOAT2FIX (x); // 14.9 us - convertir variable flotante a fija
   z = x + y; // 8.25 us - adici贸n de flotante
   c = a + b; // 2.0 us - adici贸n fija
   z = x * y; // 10.3 us - multiplicar flotante
   c = FIX_MUL (a, b); // 6.68 us - multiplicaci贸n fija
   z = FIX2FLOAT (c); // 13.37 us - conversi贸n de fijo a flotante
* /
void loop() {} 

Elija multiplicadores en potencias de dos

Como se explic贸 en el primer cap铆tulo, el compilador reemplaza las operaciones de multiplicaci贸n de enteros con ( 2 ^ n ) cambios de bits, que son mucho m谩s r谩pidos. C贸mo usarlo: si es posible, escribe tus algoritmos para que se obtengan potencias de dos en operaciones matem谩ticas (2 4 8 16 32 64 128 …). Por ejemplo, multiplicar un n煤mero por 16 es dos veces m谩s r谩pido que multiplicar por 15. Estamos hablando de unos pocos microsegundos, pero a veces esto tambi茅n es importante.

Nota: Debe ser entero aqu铆, porque para float 隆el truco no funciona!

Reemplazar divisi贸n con desplazamiento de bits

En cuanto a la divisi贸n de enteros por potencias de dos, el compilador no la reemplaza con un desplazamiento, y esto puede y debe hacerse manualmente. Por ejemplo, divisi贸n de n煤meros long por 16 (val / 16) tarda 15 veces m谩s que una operaci贸n de rotaci贸n con el mismo resultado: val >> 4 (desplaza 4 bits, 16 == 2 a la potencia de 4). Para largos obtenemos 40 渭s por divisi贸n y 2.5 渭s por rotaci贸n. 隆Ahorro!

Nota: Debe ser entero aqu铆, porque para float 隆el truco no funciona!

Reemplazar divisi贸n por multiplicaci贸n por flotante

Nuevamente, en la tabla anterior, puede ver que la divisi贸n para todos los tipos de datos lleva mucho m谩s tiempo que la multiplicaci贸n, por lo que a veces es m谩s rentable reemplazar la divisi贸n por un n煤mero entero con la multiplicaci贸n por float… En lugar de dividir …keka / 10; // tarda 14,54 渭s usaremos keka * 0,1 ; // que tarda 10,58 渭s.

keka / 10; // ejecutado a  14,54 渭s
keka * 0,1 ; //  ejecutado a 10,58 渭s

Reemplazar exponenciaci贸n con multiplicaci贸n

Para la exponenciaci贸n, tenemos una funci贸n 煤til pow ( a, b ), pero en los c谩lculos de n煤meros enteros es mejor no usarlo: lleva mucho m谩s tiempo que la multiplicaci贸n manual, por lo que funciona con float: keka = pow ( keka, 5 ) ; // corre a 20,33 us, mientras que keka = ( long) keka * keka * keka * keka * keka; //se ejecuta en 4.47 us.

keka = pow ( keka, 5 ) ;                            // corre a 20,33 us
keka = ( long ) keka * keka * keka * keka * keka;  // se ejecuta en 4.47 us.

Optimizar operaci贸n modulo %

Operaci贸n resto de divisi贸n %, toma un tiempo relativamente largo, como la divisi贸n misma (ver tabla arriba). Debe recordarse que el compilador optimiza el resto de la divisi贸n por 2 ^ n, reemplaz谩ndolo con una m谩scara de bits, que se toma en un par de ciclos de procesador, 隆que es varias decenas de veces m谩s r谩pido! por ejemplo val % 8 se optimizar谩 autom谩ticamente en val & 0b111. Si es posible, debe escribir su algoritmo de modo que el resto de la divisi贸n se busque exactamente de 2 ^ n. Por ejemplo, cuando se trabaja con un b煤fer circular, puede hacer que su tama帽o sea igual a 16, 32, 64, 128 … y acelerar la operaci贸n de saltar al principio del b煤fer, como se suele hacer en buffer_pos % y buffer_size.

Calcule previamente lo que se puede calcular

Algunos c谩lculos complejos requieren realizar los mismos pasos varias veces. Ser谩 mucho m谩s r谩pido crear una variable local, 芦count禄 y usarla en c谩lculos posteriores. 

Nota: el compilador optimiza la mayor铆a de los c谩lculos en s铆, por ejemplo, acciones con constantes y n煤meros espec铆ficos.

Otro buen ejemplo: calcular cantidades que se comportan de manera predecible, como funciones arm贸nicas sin() y cos(). Se necesita bastante tiempo para calcularlos: 隆隆隆119,46 渭s !!! En la pr谩ctica, los senos / cosenos casi nunca se calculan mediante un microcontrolador, se calculan de antemano y se almacenan como una matriz. S铆, de nuevo dos dilemas: perder el tiempo en c谩lculos o ocupar memoria con datos ya calculados.

Adem谩s, no olvide que el propio compilador optimiza los c谩lculos. y lo hace bastante bien.

No use delay() y retrasos similares

Un consejo bastante obvio: no uses delay() donde puedes prescindir de 茅l. Y esto es el 99,99% del tiempo. Use un temporizador en millis() como estudiamos en la lecci贸n de manejo de temporizaciones.

Reemplace las funciones de Arduino con sus contrapartes r谩pidas

Si el proyecto utiliza con mucha frecuencia perif茅ricos del microcontrolador (ADC, entradas / salidas digitales, generaci贸n de PWM …), entonces necesita saber una cosa: las funciones de Arduino (en especial Wiring) est谩n escritas para proteger al usuario de posibles errores, dentro de estas funciones hay un mont贸n de controles y protecciones 芦contra dumis禄, por lo que tardan mucho m谩s tiempo del que podr铆an. Adem谩s, algunos perif茅ricos del microcontrolador est谩n configurados para que funcionen muy lentamente. Ejemplo: digitalWrite () y digitalRead () se realizan aproximadamente en 3,5 渭s, cuando el trabajo directo con el puerto del microcontrolador tarda 0,5 渭s, que es casi un orden de magnitud m谩s r谩pido. analogRead () tarda 112 microsegundos, aunque si lo modifica de forma un poco diferente, funcionar谩 casi 10 veces m谩s r谩pido sin perder mucha precisi贸n.

Use switch en lugar de else if

En construcciones de ramificaci贸n con opci贸n m煤ltiple por el valor de una variable entera, debe dar preferencia a la construcci贸n switchcase, funciona m谩s r谩pido que otra cosa (revise las lecciones sobre condiciones y opciones). Pero recuerda eso, switch solo funciona con enteros! Aqu铆 debajo encontrar谩 los resultados de una prueba (no optimizada para el compilador).

// switch de prueba
// keka tiene 10
// tiempo de ejecuci贸n: 0,3 渭s (5 ciclos de reloj)
switch (keka) {
  case 10: break;  //selecciona esto
  case 20: break;
  case 30: break;
  case 40: break;
  case 50: break;
  case 60: break;
  case 70: break;
  case 80: break;
  case 90: break;
  case 100: break;
}
// keka es igual a 100
// tiempo de ejecuci贸n: 0,3 渭s (5 ciclos de reloj)
switch (keka) {
  case 10: break;
  case 20: break;
  case 30: break;
  case 40: break;
  case 50: break;
  case 60: break;
  case 70: break;
  case 80: break;
  case 90: break;
  case 100: break;  //selecciona esto
}
// prueba ELSE IF
// keka tiene 10
// tiempo de ejecuci贸n: 0,50 渭s (8 ciclos de reloj)
if ( keka == 10 ) { // seleccione esto    
} else if (keka == 20) {
} else if (keka == 30) {
} else if (keka == 40) {
} else if (keka == 50) {
} else if (keka == 60) {
} else if (keka == 70) {
} else if (keka == 80) {
} else if (keka == 90) {
} else if (keka == 100) {
}
// keka es igual a 100
// tiempo de ejecuci贸n: 2,56 渭s (41 ciclos de reloj)
if (keka == 10) {
} else if (keka == 20) {
} else if (keka == 30) {
} else if (keka == 40) {
} else if (keka == 50) {
} else if (keka == 60) {
} else if (keka == 70) {
} else if (keka == 80) {
} else if (keka == 90) {
} else if (keka == 100) {   //  seleccione esto    
}

Recuerda el orden de las condiciones

Si se verifican varias expresiones l贸gicas simult谩neamente, cuando se produce el primer resultado, en el que toda la condici贸n recibir谩 un valor conocido de forma 煤nica, el resto de las expresiones聽ni siquiera se verifican.聽Por ejemplo:

if ( flag && getSensorState () ) {    
  // alg煤n c贸digo
}

Si flag tiene el significado falso, la funci贸n getSensorState() 隆Ni siquiera se llamar谩! el If ser谩 inmediatamente omitido. Esto debe usarse colocando las condiciones en orden ascendente de tiempo de procesador que se requiere para llamarlas / ejecutarlas, si se trata de funciones. Por ejemplo, si nuestro getSensorState () tarda alg煤n tiempo en ejecutarse, lo colocamos despu茅s de la bandera, que es solo una variable. Esto ahorrar谩 tiempo a la CPU cuando la bandera sea false.

Usar operaciones bit a bit

Utilice trucos y operaciones de bits en general, ya que a menudo ayudan a acelerar su c贸digo. Lea m谩s en esta lecci贸n.

Utilice punteros y enlaces

En lugar de pasar un 芦objeto禄 como argumento a una funci贸n, p谩selo por referencia o por un puntero: el procesador no asignar谩 memoria para una copia del argumento (y crear谩 esta copia como una variable formal) – esto ahorrar谩 tiempo !聽Lea m谩s sobre punteros y enlaces聽en un tutorial separado.

Usar funciones macro e integradas

Cada funci贸n creada tiene su propia direcci贸n de memoria y, para llamarla, el procesador se dirige a esta direcci贸n, lo que lleva tiempo.聽El tiempo es聽muy corto, pero a veces incluso es cr铆tico, por lo que tales llamadas de tiempo cr铆tico pueden reemplazarse con聽funciones macro聽o聽funciones聽integradas, lea m谩s聽en la lecci贸n sobre funciones.

Usa constantes

Constantes (cons o #define) 芦Trabajan禄 mucho m谩s r谩pido que las variables cuando se pasan como argumentos a una funci贸n. 隆Haga todo constantes que no cambien mientras el programa se est谩 ejecutando! Ejemplo:

byte pin = 3;   // la frecuencia ser谩 de 128 kHz 
// byte const pin = 3; // la frecuencia ser谩 de 994 kHz 
void setup() {
  pinMode(pin, OUTPUT);
}
void loop() {
  for (;;) {
    digitalWrite(pin, 1);
    digitalWrite(pin, 0);
  }
}

驴Por qu茅 est谩 pasando esto? El compilador optimiza el c贸digo y, con argumentos constantes, puede eliminar casi todo el c贸digo innecesario de la funci贸n (si hay, por ejemplo, bloques if – if else) y se ejecutar谩 m谩s r谩pido.

Optimiza el bucle void loop()

Funci贸n loop() est谩 anidado en un ciclo externo con algunas comprobaciones adicionales, por lo que si realmente te importa el tiempo m铆nimo entre iteraciones loop() – solo introduce en tu bucle for( ;; ), por ejemplo as铆:

void loop() {
  for (;;) {
  // tu codigo
  }
}

C贸digo en ensamblador (es broma)

Arduino IDE admite inserciones de ensamblador, en las que puede dar comandos directos al procesador en el idioma del mismo nombre, lo que proporciona el c贸digo m谩s r谩pido y claro posible. Pero en nuestra familia no bromean sobre esto =)


Optimizaci贸n de la memoria.

La mayor铆a de las veces nos enfrentamos a una falta de memoria: Flash, RAM o SRAM. Despu茅s de compilar el c贸digo, recibimos un mensaje sobre la huella Flash / SRAM, que es informaci贸n valiosa. La memoria flash se puede llenar hasta en un 99%, su volumen no cambia durante el funcionamiento del dispositivo, lo que no se puede decir de SRAM. Digamos que al momento de lanzar el programa tenemos el 80% de la RAM ocupada, pero en el proceso de trabajo pueden aparecer y desaparecer variables locales, lo que acabar谩 con el volumen ocupado hasta en un 100% y lo m谩s probable es que el dispositivo se reinicie o congele. El peligro es que la 芦secci贸n禄 de RAM comience a fragmentarse, es decir aparecen peque帽os espacios vac铆os que el microcontrolador no puede llenar con los nuevos datos que aparecen. S铆, todo es como en una computadora, solo que no tenemos un bot贸n de 芦desfragmentar禄. Por lo tanto, debe aprender a manejar manualmente la administraci贸n de memoria para intentar dejar m谩s SRAM libre.

Tambi茅n adjunto un ejemplo de boceto con una funci贸n que muestra la cantidad de SRAM libre. 

/ *
   Funci贸n que devuelve la cantidad de memoria de acceso aleatorio libre (SRAM)
   Nota:  m茅todo para comprobar la RAM libre 
   no funciona correctamente en caso de fragmentaci贸n de la memoria.
* /
void setup() {
  Serial.begin(9600);
}
void loop() {
  Serial.println(memoryFree()); // imprime la cantidad de SRAM libre
  delay(1000);
}
extern int __bss_end;
extern void *__brkval;
// Funci贸n que devuelve la cantidad de RAM libre
int memoryFree() {
  int freeValue;
  if ((int)__brkval == 0)
    freeValue = ((int)&freeValue) - ((int)&__bss_end);
  else
    freeValue = ((int)&freeValue) - ((int)__brkval);
  return freeValue;
}

Utilice variables de tipos apropiados

Como recordar谩 de la lecci贸n sobre tipos de datos, cada tipo tiene un l铆mite en el valor m谩ximo almacenado, que determina directamente el peso de este tipo en la memoria. Aqu铆 est谩n todos:

NombrePesoRango
boolean1 byte0 o 1,  verdadero  o  falso
char (int8_t)1 byte-128 … 127
byte (uint8_t)1 byte0 … 255
int (int16_t)2 bytes-32 768 … 32 767
unsigned int (uint16_t)2 bytes0 … 65 535
long (int32_t)4 bytes-2147 483 648 … 2147483647
unsigned long (uint32_t)4 bytes0 … 4 294967 295
float (doble)4 bytes-3.4028235E + 38 … 3.4028235E + 38

Simplemente no use variables de tipos m谩s pesados 鈥嬧媎onde no sea necesario.

Utilice #define

Para almacenar constantes al estilo de los n煤meros de pin, algunas configuraciones y valores constantes, no use variables globales, sino #define. As铆, la constante quedar谩 almacenada en el c贸digo, en la memoria Flash, que es mucho mejor.

#define MOTOR_PIN 10
#define MOTOR_SPEED 120

Usar directivas del preprocesador

Si tiene un proyecto complejo en el que algunos fragmentos de c贸digo o bibliotecas se activan o desactivan antes que el firmware, utilice la compilaci贸n condicional mediante directivas #if, #elif, #ifdef y otras, de los que hablamos en la lecci贸n sobre compilaci贸n condicional

Usar progmem

Para almacenar grandes cantidades de datos persistentes (matriz de mapas de bits para mostrar, l铆neas con texto, 芦tablas禄 sinusoidales u otros valores de correcci贸n) utilice PROGMEM- y la capacidad de almacenar y leer datos en la memoria Flash del microcontrolador, que es mucho m谩s grande que la RAM operativa. La peculiaridad es que los datos en Flash se escriben durante el firmware, y luego no ser谩 posible cambiarlos, solo podr谩s leerlos y usarlos.

Breve guia de progmem:

// almacena varios enteros
const uint16_t ints[] PROGMEM = {65000, 32796, 16843, 10, 11234};
 // almacena algunos decimales
const float floats[] PROGMEM = {0.5, 120.25, 0.9214};
// guarda cadenas
const char message[] PROGMEM = {"Hello! Lolkek"};
void setup() {
  Serial.begin(9600);
  Serial.println(pgm_read_word(&(ints[2])));      // imprime 16843
  Serial.println(pgm_read_float(&(floats[1])));   // imprime 120.25
  for (byte i = 0; i < 13; i++)
    Serial.print((char)pgm_read_byte(&(message[i]))); 
    // imprime 隆Hola! Lolkek
}

La funci贸n principal para leer desde progmem es聽pgm_read_TYPE.聽Podemos usar estos 4:

pgm_read_byte ( datos ) ; - para 1 byte (char, byte, int8_t, uint8_t)
pgm_read_word ( datos ) ; - para 2 bytes (int, word, unsigned int, int16_t, int16_t)
pgm_read_dword ( datos ) ; - para 4 bytes (largo, largo sin firmar, int32_t, int32_t)
pgm_read_float ( datos ) ; - para n煤meros de coma flotante

隆Atenci贸n! Al leer n煤meros negativos (signed), debe convertir el tipo de datos. Ejemplo:

// guarda varios enteros con diferentes signos
const int16_t ints [] PROGMEM = { 65000, 32796, -16843 } ; 
// preparar
serial.println (( int ) pgm_read_word ( & ( ints [ 2 ]))) ;  // imprimir谩 -16843

Tambi茅n existe una forma m谩s conveniente de escribir y leer datos, implementada en bibliotecas. Mira las bibliotecas aqu铆.

Utilice la macro F ()

Si el proyecto usa salida de datos de texto al puerto COM, entonces cada car谩cter ocupar谩 un byte de RAM, esto tambi茅n se aplica a los datos de cadena y salidas de pantalla. Tenemos una herramienta incorporada que te permite almacenar cadenas en la memoria Flash, es muy f谩cil de usar, es mucho m谩s conveniente que la misma PROGMEM.

Las llamadas 芦F () macro 鈥 permite almacenar l铆neas en la memoria Flash sin ocupar espacio en SRAM. Funciona de manera muy simple y eficiente, lo que le permite crear un dispositivo con comunicaci贸n / depuraci贸n extendida a trav茅s del puerto serie y no pensar en RAM:

// esta salida (l铆nea, texto) ocupa 18 bytes en RAM
serial.println ( "隆Hola <nombre de usuario>!" ) ;
// esta salida no ocupa nada en RAM, gracias a F ()
serial.println ( F ( "Escriba / ayuda para ayudar" )) ;

Limita el uso de bibliotecas

Digamos que est谩 utilizando una biblioteca que tiene un 芦mont贸n de cosas禄 de las que necesita un par de funciones. Al compilar el c贸digo, las funciones y variables no utilizadas se cortan, pero a veces esto no es suficiente, depende de la biblioteca. Tiene sentido 芦extraer禄 el c贸digo que desea de la biblioteca y simplemente incrustarlo en su c贸digo, ahorrando as铆 un poco de espacio.

No uses float

Como comentamos en la lecci贸n de tipos de datos, la compatibilidad con el c谩lculo con datos float, el software (para AVR), es decir, en t茅rminos generales, para los c谩lculos, la biblioteca est谩 integrada. Habiendo usado una vez todas las operaciones aritm茅ticas en el c贸digo con float, la biblioteca inserta aproximadamente 1000 bytes de c贸digo en Flash para respaldar estos c谩lculos.

Tambi茅n duplicar茅 el ejemplo del cap铆tulo anterior: si necesita almacenar muchos valores float en la memoria RAM o EEPROM, es decir, tiene sentido reemplazarlos con n煤meros enteros. C贸mo hacerlo sin perder precisi贸n:

// digamos que necesitamos almacenar una matriz de tales valores sin desperdiciar memoria extra.
// deja que sensorRead () devuelva la temperatura en float con una precisi贸n decimal.
// Convi茅rtelo a un n煤mero entero multiplic谩ndolo por 10 (o 100, seg煤n la precisi贸n que sea necesaria):
vals [ 30 ] = sensorRead () * 10;
// 隆Los valores enteros int usan la mitad de la memoria!
// Para volver a convertirlos en flotantes, simplemente div铆dalos por 10.0
float val_f = vals [ 30 ] / 10.0 ;

No use objetos de las clases Serial y String

Quiz谩s las bibliotecas 芦m谩s gordas禄 en t茅rminos de consumo de memoria son los objetos est谩ndar Serial y String. Si aparece Serial en el c贸digo, toma inmediatamente al menos 998 bytes de Flash ( 3% para ATmega328) y 175 bytes de SRAM ( 8% para ATmega328). Tan pronto como comenzamos a usar Strings , nos despedimos de Flash de 1178 bytes ( 4% para ATmega328).

Si a煤n se necesita Serial, intente utilizar un an谩logo muy ligero de la biblioteca est谩ndar: G_ppUART. La biblioteca ofrece casi todas las caracter铆sticas del Serial est谩ndar, pero ocupa mucha menos memoria.

Utilice indicadores de un solo bit

Debe tener en cuenta que el tipo de datos l贸gicos que toma el booleano en la memoria Arduino es de 1 bit, como deber铆a ser, y hasta 8, es decir, 1 byte. Esta es una injusticia universal, porque de hecho podemos guardar 8 banderas en un byte cierto/falso, pero de hecho solo almacenamos uno. Pero hay una salida: empaquetar los bits manualmente en bytes, para lo cual es necesario agregar varias macros. Usar esto no es muy conveniente, pero en una situaci贸n cr铆tica, cuando cada byte es importante, puede ser rentable. Ver ejemplo:

// opci贸n para empaquetar banderas en una matriz. 
#define NUM_FLAGS 30 // n煤mero de banderas
byte flags[NUM_FLAGS / 8 + 1];      // matriz de banderas comprimidas
// ============== MACROS PARA TRABAJAR CON UN PAQUETE DE BANDERAS ==============
// levanta la bandera (paquete, n煤mero)
#define setFlag (flag, num) bitSet (flag [(num) >> 3], (num) & 0b111)
// omitir bandera (paquete, n煤mero)
#define clearFlag (flag, num) bitClear (flag [(num) >> 3], (num) & 0b111)
// escribe la bandera (paquete, n煤mero, valor)
#define writeFlag (flag, num, state) ((state)? setFlag (flag, num): clearFlag (flag, num))
// lee la bandera (paquete, n煤mero)
#define readFlag (flag, num) bitRead (flag [(num) >> 3], (num) & 0b111)
// omitir todas las banderas (paquete)
#define clearAllFlags (flag) memset (flag, 0, sizeof (flag))
// levantar todas las banderas (paquete)
#define setAllFlags (flag) memset (flag, 255, sizeof (flag))
// ============== MACROS PARA TRABAJAR CON UN PAQUETE DE BANDERAS ==============
void setup () {  
  serial.beguin ( 9600 ) ;
  clearAllFlags ( banderas ) ;
  writeFlag ( banderas, 0, 1 ) ;
  writeFlag ( banderas, 10, 1 ) ;
  writeFlag ( banderas, 12, 1 ) ;
  writeFlag ( banderas, 15, 1 ) ;
  writeFlag ( banderas, 15, 0 ) ;
  writeFlag ( banderas, 29, 1 ) ;
  // mostrar todo
  for ( byte i = 0; i < NUM_FLAGS; i ++ ) 
    serial.print ( readFlag ( banderas, i )) ;
}
void loop () {  
}

Utilice compresi贸n y empaquetado de bytes

En el p谩rrafo anterior, analizamos c贸mo empaquetar indicadores de un bit en bytes. De la misma manera, puede empaquetar cualquier otro dato de diferentes tama帽os para un almacenamiento o compresi贸n conveniente (pero primero, aprenda la lecci贸n sobre operaciones de bits ). Como ejemplo: inicialmente necesita almacenar tres colores en la memoria para cada LED, cada color tiene una profundidad de 8 bits, es decir, se gastan un total de 3 bytes por LED RRRRRRRR GGGGGGGG BBBBBBBB. Para ahorrar espacio y comodidad de almacenamiento, puede comprimir estos tres bytes en dos (tipo de datos int), perdiendo varios matices del color resultante. Por ejemplo como este: RRRRRGGG GGGBBBBB. Exprimir y empaquetar: hay tres variables de cada color, red, green, blue:

int rgb = (( r & 0b11111000 ) << 8 ) | (( g & 0b11111100 ) << 3 ) | (( b & 0b11111000 ) >> 3 ) ;   

Por lo tanto, hemos descartado los bits menos significativos (a la derecha) del rojo y el azul, esta es la compresi贸n. Cuantos m谩s bits se descarten, con menor precisi贸n ser谩 posible 芦descomprimir禄 el n煤mero nuevamente. Por ejemplo, el n煤mero 0b10101010 (170 en decimal) se comprimi贸 en tres bits, cuando se comprimi贸, obtuvimos 0b10101 000 , es decir, perdi贸 los tres bits menos significativos, y el decimal ya resulta ser 168. Para empaquetar, se usa un cambio de bit y una m谩scara, por lo que tomamos los primeros cinco bits de rojo, seis verdes y cinco azules, y los empujamos al lugares correctos en la variable de 16 bits resultante. Eso es todo, el color se comprime y se puede almacenar.

Para desempaquetar, se usa la operaci贸n inversa: seleccione los bits necesarios usando una m谩scara y vuelva a cambiarlos a un byte:

byte r = ( datos & 0b1111100000000000 ) >> 8; 
byte g = ( datos & 0b0000011111100000 ) >> 3; 
byte b = ( datos & 0b0000000000011111 ) << 3; 

Por lo tanto, puede comprimir, descomprimir y simplemente almacenar datos peque帽os en tipos de datos est谩ndar. Tomemos otro ejemplo: necesita almacenar varios n煤meros en el rango de 0 a 3 de la manera m谩s compacta posible, es decir, en representaci贸n binaria esto es 0b00, 0b01, 0b10 y 0b11. Vemos que 4 de esos n煤meros se pueden agrupar en un byte (el m谩ximo toma dos bits):

// n煤meros de ejemplo
byte val_0 = 2; // 0b10
byte val_1 = 0; // 0b00
byte val_2 = 1; // 0b01
byte val_3 = 3; // 0b11
byte val_pack = (( val_0 & 0b11 ) << 6 ) | (( val_1 & 0b11 ) << 4 ) | (( val_2 & 0b11 ) << 2 ) | ( val_3 & 0b11 ) ;   
// obtuve 0b10000111

Como en el ejemplo con LED, solo tomamos los bits necesarios (en este caso, los dos inferiores, 0b11) y los movi贸 a la distancia deseada. Para desembalar, haga en orden inverso:

byte unpack_1 = ( val_pack & 0b11000000 ) >> 6; 
byte unpack_2 = ( val_pack & 0b00110000 ) >> 4; 
byte unpack_3 = ( val_pack & 0b00001100 ) >> 2; 
byte unpack_4 = ( val_pack & 0b00000011 ) >> 0; 

Y recuperamos nuestros bytes. Adem谩s, la m谩scara se puede reemplazar con una grabaci贸n m谩s conveniente deslizando 0b11 a la distancia deseada:

byte unpack_1 = ( val_pack & 0b11 << 6 ) >> 6;  
byte unpack_2 = ( val_pack & 0b11 << 4 ) >> 4;  
byte unpack_3 = ( val_pack & 0b11 << 2 ) >> 2;  
byte unpack_4 = ( val_pack & 0b11 << 0 ) >> 0;  

Bueno, ahora, siguiendo el patr贸n, puedes crear una funci贸n o macro para leer el paquete por ti mismo:

#define UNPACK (x, y) (((x) & 0b11 << ((y) * 2)) >> ((y) * 2))

Donde x es el paquete e y es el n煤mero de secuencia del valor empaquetado. Veamos:

Serial.println(UNPACK(val_pack, 3));
Serial.println(UNPACK(val_pack, 2));
Serial.println(UNPACK(val_pack, 1));
Serial.println(UNPACK(val_pack, 0));

Selecci贸n de cargador de arranque

En una de las primeras lecciones, les dije que un cargador de arranque vive en la memoria Flash del microcontrolador, un cargador de arranque que carga el firmware a trav茅s de UART. El cargador de arranque no tiene tres l铆neas de c贸digo, sino mucho m谩s: 隆el cargador de arranque est谩ndar ocupa casi 2 KB de memoria Flash ! Para Nano / Uno, esto es un enorme 6%. Hay dos opciones: flashear un cargador de arranque m谩s moderno, que ocupa 4 veces menos espacio (512 bytes). El cargador se llama optiBoot, la informaci贸n b谩sica que contiene est谩 en su propio GitHub. Analizaremos el proceso de flashear el gestor de arranque m谩s adelante, en una lecci贸n separada. Por ahora, puede comprarse un programador AVR-ISP, aunque el cargador de arranque puede actualizarse con otro Arduino.

La segunda opci贸n es a煤n mejor: cargue el firmware directamente en el microcontrolador, sin un cargador de arranque , utilizando el programador. Entonces tendr谩 a su disposici贸n la cantidad total declarada de memoria Flash.

Abandonar la inicializaci贸n est谩ndar

Funciones est谩ndar void setup () y void loop () son obligatorios por una raz贸n: est谩n incluidos en la funci贸n m谩s importante de todo el programa: int main (). La implementaci贸n de esta funci贸n se encuentra en el n煤cleo del archivo main.cpp y tiene este aspecto:

int main(void)
{
 init();
 initVariant();
#if defined(USBCON)
 USBDevice.attach();
#endif
 
 setup();
    
 for (;;) {
  loop();
  if (serialEventRun) serialEventRun();
 }
        
 return 0;
}

隆Es aqu铆, en las inicializaciones, donde se cubren un par de cientos de bytes de memoria Flash! Y entonces viene loop() y hay una verificaci贸n del estado (es precisamente evitarlo lo que da un aumento de velocidad, escrib铆 sobre esto al final de la secci贸n 鈥渙ptimizaci贸n de velocidad鈥). En las funciones de inicializaci贸n se configura la periferia del microcontrolador: ADC, interfaces, temporizador 0 (que nos da la funci贸n millis ()) y algunas otras cosas. Si puede inicializar de forma independiente solo los perif茅ricos necesarios, esto ahorrar谩 varios cientos de bytes de flash, todo lo que necesita hacer es ingresar descaradamente su funci贸n en el boceto main() y escriba la inicializaci贸n solo de lo que se necesita. A modo de comparaci贸n: el conjunto est谩ndar de inicializaci贸n (funciones setup() y loop () en el boceto) dan 444 bytes de Flash (Arduino IDE v. 1.8.9). Si abandonamos este c贸digo y tomamos el control en main()– un boceto vac铆o ocupar谩 134 bytes, 隆que es casi 300 bytes menos! Esto, por supuesto, es una peque帽a cosa, pero ayuda. C贸mo hacerlo:

#include <Arduino.h>
int main () {  
  // nuestra "configuraci贸n" personal
  for ( ;; ) {  
    // nuestro "bucle" personal
  }
  return 0;
}

Las funciones setup() y loop() en este boceto ya no son necesarios, porque no se utilizan en nuestro personal main().

Prueba G_PPCore

Tambi茅n preste atenci贸n al n煤cleo est谩ndar que reescrib铆 para las placas basadas en ATmega328: G_PPCore. Este n煤cleo es an谩logo al est谩ndar, pero las funciones principales est谩n completamente reescritas y se ejecutan muchas veces m谩s r谩pido y ocupan mucho menos espacio en la memoria del microcontrolador.

Comprar Arduino Mega

Un consejo un poco c贸mico, pero pr谩ctico: si comenz贸 a extra帽ar Uno / Nano y ya se han realizado todas las optimizaciones posibles, solo le queda una opci贸n: comprar Arduino Mega, tiene 8 veces m谩s memoria Flash y 4 veces m谩s SRAM.


Deja un comentario