37- PROGMEM. Trabajando con la memoria Arduino

A menudo es necesario almacenar una gran cantidad de datos en la memoria del microcontrolador que no cambiarán durante el funcionamiento, por ejemplo:

  • Matriz de calibración
  • Texto del nombre de elementos del menú
  • Algo de texto
  • Trigonometría calculada (seno, coseno)
  • Imágenes para visualización (mapa de bits)
  • Y mucho más

Almacenar dichos datos en RAM (como una variable ordinaria) no es la mejor idea, porque no cambiarán, ¡pero ocuparán espacio! Permítanme recordarles que la RAM es siempre mucho menor que la memoria del programa (Flash): en el mismo ATmega328 (Arduino UNO / Nano / Pro mini) – 32 KB Flash y 2 KB SRAM, ¡SRAM es 16 veces menor!  Por lo tanto, es mucho más eficiente almacenar dichos datos en Flash, también conocida como memoria del programa (también conocida como PROGMEM). ¿Pero cómo?

Estamos acostumbrados al hecho de que podemos cambiar variables durante la ejecución del programa, por eso son variables, por eso la memoria se llama dinámica. Pero con la memoria Flash, todo no es tan simple: solo un programador puede escribir en él, con el que se carga el código del programa, o un cargador de arranque, que prácticamente realiza la función de un programador. Por cierto, existe un cargador modificado que permite acceder a la memoria Flash directamente desde el programa, pero en estas lecciones consideramos herramientas estándar, en este caso, la utilidad PROGMEM. Para trabajar con PROGMEM, se usa la biblioteca incorporada avr / pgmspace.h , no necesita conectarla, se conectará sola (en versiones Arduino IDE superiores a 1.0).


Grabación.

La palabra clave PROGMEM (modificador de variable) permite escribir datos en la memoria Flash. La sintaxis es:

const tipo de datos data[] PROGMEM = {};  
const PROGMEM tipo de datos data[] = {};   

¡Todos! Los datos, en el caso mostrado, los tipos de datos de las matrices se colocarán en la memoria Flash. PROGMEM puede trabajar con todos los tipos de enteros (8, 16, 32, 64 bits), float y char.

¡Un punto importante! El modificador PROGMEM solo se puede aplicar a global (definido fuera de funciones) o variables estátic (global o local, pero con la palabra estátic)! Lea la lección sobre tipos de datos en Arduino si lo olvidó. 


Lectura.

Si, todo es más simple que con la escritura (se agrega UNA palabra clave), entonces con la lectura es mucho más interesante: se lleva a cabo usando una función especial. La función principal de la lectura de progmem espgm_read_type ( dirección ). Podemos usar estos 4 tipos:

  • pgm_read_byte ( datos ) ; – para el primer 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 (long, unsigned long, int32_t, int32_t)
  • pgm_read_float ( datos ) ; – para números en coma flotante

¡Donde datos es la dirección (o puntero) del bloque de datos almacenado! Recuerde la lección sobre punteros para entender de qué se trata.

La lista completa de características de pgmspace se puede encontrar en la documentación.

Números únicos

Consideremos un ejemplo simple: escribir y leer números individuales:

const uint16_t data PROGMEM = 125;
const int16_t signed_data PROGMEM = -654;
const float float_data PROGMEM = 3.14;
void setup() {
  Serial.begin(9600);
  Serial.println(pgm_read_word(&data)); // imprime 125
  uint16_t *dataPtr = &data;  //prueba con un puntero
  Serial.println(pgm_read_word(dataPtr));  // imprime 125
  Serial.println(pgm_read_word(&signed_data));  // imprime 64882
  Serial.println((int16_t)pgm_read_word(&signed_data));  // imprime -654
  Serial.println(pgm_read_float(&float_data));  // imprime 3.14
}
void loop() {}

Lo que es importante recordar aquí: al leer números negativos (por ejemplo, tipos int y long) se deben convertir, porque PROGMEM almacena números en representación sin signo. Presta atención a la lectura de signed_data del ejemplo anterior, sin convertir a int ¡el número se mostró incorrectamente!

Matrices unidimensionales

Con matrices de números, todo es bastante lógico:

const uint8_t data[] PROGMEM = {10, 20, 30, 40};
void setup() {
  Serial.begin(9600);
  for (byte i = 0; i < 4; i++) {
    Serial.println(pgm_read_byte(&data[i]));
  }
  // imprime 10 20 30 40
}
void loop() {}

Matrices 2D

Al crear una matriz bidimensional, asegúrese de especificar el tamaño de al menos una de las dimensiones.

const uint16_t data[][5] PROGMEM = {
  {10, 20, 30, 40, 50},
  {60, 70, 80, 90, 100},
  {110, 120, 130, 140, 150},
};
void setup() {
  Serial.begin(9600);
  // imprime 70, segunda fila, segunda columna
  Serial.println(pgm_read_word(&data[1][1]));
  // imprime 150, tercera fila quinta columna
  Serial.println(pgm_read_word(&data[2][4]));
}
void loop() {}

Matriz de matrices

Puede almacenar varias matrices en una, la llamada, tabla de matrices.

// matrices
const uint16_t data0[] PROGMEM = {10, 20, 30, 40, 50};
const uint16_t data1[] PROGMEM = {60, 70, 80, 90, 100};
const uint16_t data2[] PROGMEM = {110, 120, 130, 140, 150};
const uint16_t data3[] PROGMEM = {160, 170, 180, 190, 200};
// tabla de matrices
const uint16_t* const data_array[] PROGMEM = {data0, data1, data2, data3};
void setup() {
  Serial.begin(9600);
  // muestra 170, el segundo elemento de la cuarta matriz
  Serial.println(pgm_read_word(&data_array[3][1]));
}
void loop() {}

Cadenas de caracteres

PROGMEM permite guardar cadenas como matrices de caracteres, char:

const char data_message[] PROGMEM = {"Hello!"};
void setup() {
  Serial.begin(9600);
  for (byte i = 0; i < strlen_P(data_message); i++) {
    Serial.print((char)pgm_read_byte(&data_message[i]));
  }
  //imprime ¡Hola!
}
void loop() {}

La lectura se realiza carácter a carácter; al leer, debemos convertir el tipo char. También puede haber notado que para calcular la longitud de la matriz de caracteres, usamos la función strlen_P (), esto es análogo a strlen () (vea la lección sobre cadenas), pero se usa especialmente para cadenas PROGMEM. En la documentación se puede encontrar un conjunto de herramientas para trabajar con cadenas en PROGMEM, hay muchas de ellas.

Matrices de cadenas

A veces es conveniente almacenar varias líneas con el mismo nombre, por ejemplo, para los elementos del menú Arduino. En este caso, puede usar una matriz de cadenas (una matriz de matrices de caracteres), hablamos de ello en la lección sobre cadenas. El mecanismo es el siguiente: creamos cadenas, las ponemos en PROGMEM. Creamos una «tabla de enlaces» para estas líneas. ¡Leemos cualquier fila seleccionada de la tabla!

//declaramos nuestras "cadenas"
const char array_1[] PROGMEM = "Period";
const char array_2[] PROGMEM = "Work";
const char array_3[] PROGMEM = "Stop";
//  declara la tabla de enlaces
const char* const names[] PROGMEM = {
  array_1, array_2, array_3,
};
void setup() {
  Serial.begin(9600);
 // muestra la línea # 1 (texto "Work")
  // strlen_P (names [1]) - la longitud de esta cadena
  for (byte i = 0; i < strlen_P(names[1]); i++) {
     // ¡acceder al elemento será como una matriz bidimensional!
    // nombres [1] [0] - letra W
    Serial.print((char)pgm_read_byte(&(names[1][i]))); //mostrará Work
  }
}
void loop() {}

La tarea se vuelve más complicada, ¿no? =) Puedes ir al revés: un búfer char en el que copiar la línea completa de PROGMEM usando la función strcpy_P () que copia los datos especificados de PROGMEM en una matriz regular. Obtenemos una matriz regular de caracteres, que incluso se pueden enviar directamente al puerto:

// declaramos nuestras "cadenas"
const char array_1[] PROGMEM = "Period";
const char array_2[] PROGMEM = "Work";
const char array_3[] PROGMEM = "Stop";
//  declara la tabla de enlaces
const char* const names[] PROGMEM = {
  array_1, array_2, array_3,
};
void setup() {
  Serial.begin(9600);
  char arrayBuf[10];  // crea un búfer
  
  // copiar a arrayBuf usando strcpy_P
  strcpy_P(arrayBuf, pgm_read_word(&(names[1])));
  Serial.println(arrayBuf); // mostrará Work
  strcpy_P(arrayBuf, pgm_read_word(&(names[2])));
  Serial.println(arrayBuf); // mostrará Stop
}
void loop() {}

Consideremos otro ejemplo en el que sacaremos una cadena de la memoria sin funciones pesadas adicionales. Además, este ejemplo funciona correctamente cuando se genera a través de un bucle (a diferencia de los ejemplos anteriores).

//  declaramos nuestras "cadenas"
const char array_1[] PROGMEM = "Period";
const char array_2[] PROGMEM = "Work";
const char array_3[] PROGMEM = "Stop";
// declara la tabla de enlaces
const char* const names[] PROGMEM = {
  array_1, array_2, array_3,
};
void setup() {
  Serial.begin(9600);  
  for (int i = 0; i < 3; i++) {   // bucle
    uint16_t ptr = pgm_read_word(&(names[i]));//  obtén la dirección de la tabla de enlaces
    while (pgm_read_byte(ptr) != NULL) {      // cadena completa hasta cero caracteres  
      Serial.print(char(pgm_read_byte(ptr))); // imprimir en el monitor o donde necesitemos
      ptr++;                                  // siguiente caracter
    }
    Serial.println();
  }
}
void loop() {}

La función para imprimir líneas al serial o al display, de PROGMEM se puede hacer en una función separada. Ejemplo final:

// declaramos nuestras "cadenas"
const char array_1[] PROGMEM = "Period";
const char array_2[] PROGMEM = "Work";
const char array_3[] PROGMEM = "Stop";
// declara la tabla de enlaces
const char* const names[] PROGMEM = {
  array_1, array_2, array_3,
};
void setup() {
  Serial.begin(9600);
  for (int i = 0; i < 3; i++) {
    printFromPGM(&names[i]);
    Serial.println();
  }
}
void loop() {
}
// función para imprimir desde PROGMEM
void printFromPGM(int charMap) {
  char buffer[10];      //  búfer para almacenar la cadena
  uint16_t ptr = pgm_read_word(charMap); // obtener la dirección de la tabla de enlaces
  uint8_t i = 0;        // variable - índice de la matriz de búfer
  do {
    buffer[i] = (char)(pgm_read_byte(ptr++)); //lee un carácter del PGM en una ubicación de búfer, mueve el puntero
  } while (buffer[i++] != NULL);              // repetir hasta que el carácter leído no sea cero, mover el índice del búfer 
  Serial.print(buffer); // imprimir la línea terminada
}

Macro F().

La llamada «macro F ()» hace que sea muy fácil almacenar cadenas (matrices de caracteres) en la memoria Flash sin tener que recurrir al uso de PROGMEM:

// esta salida (línea, texto) ocupa 18 bytes en RAM
Serial.println("Hello <username>!");
//esta salida no ocupa nada en RAM, gracias a F ()
Serial.println(F("Type /help to help"));

¡Versatil! Pero PROGMEM le brinda más opciones, especialmente con una tabla de referencias, donde se accede a múltiples filas usando un solo nombre y número común. La macro F ()  funciona muy bien en los casos en que hay que mostrar texto sin formato ( lcd.print(F(«Hello»)) ) y para programas con control de consola.


Deja un comentario