PROGMEM: ¿tengo que copiar datos de flash a RAM para leer?

8

Tengo algunas dificultades para entender la gestión de la memoria.

La documentación de Arduino dice que es posible mantener constantes como cadenas o lo que no quiera cambiar durante el tiempo de ejecución en la memoria del programa. Creo que está incrustado en algún lugar del segmento de código, que debe ser bastante posible dentro de una arquitectura von-Neumann. Quiero hacer uso de eso para hacer posible mi menú de IU en una pantalla LCD.

Pero estoy desconcertado por esas instrucciones de solo leer e imprimir datos de la memoria del programa:

strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i]))); // Necessary casts and dereferencing, just copy. 
    Serial.println( buffer );

¿Por qué demonios tengo que copiar el maldito contenido a la RAM antes de acceder? Y si esto es cierto, ¿qué sucede con todo el código entonces? ¿También se carga en la RAM antes de la ejecución? ¿Cómo se maneja el código (32 kB) con solo 2 kB de RAM? ¿Dónde están esos pequeños duendes que llevan disquetes?

Y aún más interesante: lo que sucede con las constantes literales como en esta expresión:

a = 5*(10+7)

¿Se copian realmente 5, 10 y 7 en la RAM antes de cargarlos en los registros? No puedo creer eso.

Ariser - reinstalar a Monica
fuente
Una variable global se carga en la memoria y nunca se libera de ella. El código anterior solo copia los datos en la memoria cuando es necesario y los libera cuando finaliza. También tenga en cuenta que el código anterior solo lee un byte de la string_tablematriz. Esa matriz podría ser de 20 KB y nunca cabría en la memoria (ni siquiera temporalmente). Sin embargo, puede cargar solo un índice utilizando el método anterior.
Gerben
@Gerben: Este es un verdadero inconveniente en las variables globales, todavía no lo he tenido en cuenta. Me duele la cabeza ahora. Y el fragmento de código fue solo un ejemplo de la documentación. Me abstuve de programar algo. yo mismo antes de tener una aclaración sobre los conceptos. Pero ahora tengo una idea. ¡Gracias!
Ariser - reinstalar a Monica el
Encontré la documentación algo confusa la primera vez que la leí. Intente ver algunos ejemplos de la vida real también (por ejemplo, una biblioteca).
Gerben

Respuestas:

10

AVR es una familia de arquitectura Harvard modificada , por lo que el código se almacena solo en flash, mientras que los datos existen principalmente en la RAM cuando se manipulan.

Con eso en mente, abordemos sus preguntas.

¿Por qué demonios tengo que copiar el maldito contenido a la RAM antes de acceder?

No necesita hacerlo per se, pero por defecto el código asume que los datos están en RAM a menos que el código se modifique para buscarlo específicamente en flash (como con strcpy_P()).

Y si esto es cierto, ¿qué sucede con todo el código entonces? ¿También se carga en la RAM antes de la ejecución?

No Arquitectura de Harvard. Vea la página de Wikipedia para los detalles completos.

¿Cómo se maneja el código (32 kB) con solo 2 kB de RAM?

El preámbulo generado por el compilador copia los datos que deberían ser modificables / modificados en SRAM antes de ejecutar el programa real.

¿Dónde están esos pequeños duendes que llevan disquetes?

No sé. Pero si los ves, entonces no hay nada que pueda hacer para ayudar.

... 5, 10 y 7 realmente se copian en la RAM antes de cargarlos en los registros?

Nah El compilador evalúa la expresión en tiempo de compilación. Todo lo que suceda depende de las líneas de código que lo rodean.

Ignacio Vazquez-Abrams
fuente
Ok, no sabía que AVR era Harvard. Pero estoy familiarizado con ese concepto. Dejando a un lado los duendes, creo que sé cuándo usar esas funciones de copia ahora. Tengo que restringir el uso de PROGMEM a los datos que rara vez se usan para guardar los ciclos de la CPU.
Ariser - reinstalar a Monica el
O modifique su código para usarlo directamente desde flash.
Ignacio Vazquez-Abrams
¿Pero cómo se vería este código? Digamos que tengo varias matrices de uint8_t que representan cadenas que quiero colocar en una pantalla LCD a través de SPI. const uint8_t test1[5]= { 0x54, 0x65, 0x73, 0x74, 0x31 }; const uint8_t bla[9]= { 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x62 }; const uint8_t Menu[4]= { 0x3d, 0x65, 0x6e, 0x75};¿Cómo traigo estos datos a flash y luego a la función SPI.transfer (), que toma un uint8_t por llamada?
Ariser - reinstalar a Monica el
8

Así es como se Print::printimprime desde la memoria del programa en la biblioteca Arduino:

size_t Print::print(const __FlashStringHelper *ifsh)
{
  const char PROGMEM *p = (const char PROGMEM *)ifsh;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

__FlashStringHelper*es una clase vacía que permite funciones sobrecargadas como imprimir para diferenciar un puntero para programar la memoria de una a la memoria normal, ya que ambos son vistos const char*por el compilador (consulte /programming/16597437/arduino-f- qué-lo-hace-realmente-hace )

Por lo tanto, podría sobrecargar la printfunción de su pantalla LCD para que tome un __FlashStringHelper*argumento, lo llamemos LCD::printy luego use lcd.print(F("this is a string in progmem"));' to call it.F () `es una macro que garantiza que la cadena esté en la memoria del programa.

Para predefinir la cadena (para ser compatible con la impresión Arduino incorporada) he usado:

const char firmware_version_s[] PROGMEM = {"1.0.2"};
__FlashStringHelper* firmware_version = (__FlashStringHelper*) firmware_version_s;
...
Serial.println(firmware_version);

Creo que una alternativa sería algo como

size_t LCD::print_from_flash(const char *pgms)
{
  const char PROGMEM *p = (const char PROGMEM *) pgms;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

lo que evitaría el __FlashStringHelperreparto.

Geometrikal
fuente
2

La documentación de Arduino dice que es posible mantener constantes como cadenas o lo que no quiera cambiar durante el tiempo de ejecución en la memoria del programa.

Todas las constantes están inicialmente en la memoria del programa. ¿Dónde más estarían cuando el poder está apagado?

Creo que está incrustado en algún lugar del segmento de código, que debe ser bastante posible dentro de una arquitectura von-Neumann.

En realidad es la arquitectura de Harvard .

¿Por qué demonios tengo que copiar el maldito contenido a la RAM antes de acceder?

Usted no De hecho, hay una instrucción de hardware (LPM - Load Program Memory) que mueve los datos directamente de la memoria del programa a un registro.

Tengo un ejemplo de esta técnica en la salida Arduino Uno al monitor VGA . En ese código hay una fuente de mapa de bits almacenada en la memoria del programa. Se lee de eso sobre la marcha y se copia a la salida de esta manera:

  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));

Un desmontaje de esas líneas muestra (en parte):

  f1a:  e4 91           lpm r30, Z+
  f1c:  e0 93 c6 00     sts 0x00C6, r30

Puede ver que un byte de memoria de programa se copió en R30 y luego se almacenó inmediatamente en el registro USART UDR0. No hay RAM involucrada.


Sin embargo, hay una complejidad. Para cadenas normales, el compilador espera encontrar datos en RAM, no en PROGMEM. Son espacios de direcciones diferentes y, por lo tanto, 0x200 en RAM es algo diferente de 0x200 en PROGMEM. Por lo tanto, el compilador se toma la molestia de copiar constantes (como cadenas) en la RAM al inicio del programa, por lo que no tiene que preocuparse por saber la diferencia más adelante.

¿Cómo se maneja el código (32 kB) con solo 2 kB de RAM?

Buena pregunta. No se saldrá con la suya al tener más de 2 KB de cadenas constantes, porque no habrá espacio para copiarlas todas.

Es por eso que las personas que escriben cosas como menús y otras cosas con palabras, toman medidas adicionales para dar a las cadenas el atributo PROGMEM, que deshabilita su copia en la RAM.

Pero estoy desconcertado por esas instrucciones de solo leer e imprimir datos de la memoria del programa:

Si agrega el atributo PROGMEM, debe tomar medidas para que el compilador sepa que estas cadenas están en un espacio de direcciones diferente. Hacer una copia completa (temporal) es unidireccional. O simplemente imprima directamente desde PROGMEM, un byte a la vez. Un ejemplo de eso es:

// Print a string from Program Memory directly to save RAM 
void printProgStr (const char * str)
{
  char c;
  if (!str) 
    return;
  while ((c = pgm_read_byte(str++)))
    Serial.print (c);
} // end of printProgStr

Si pasa esta función un puntero a una cadena en PROGMEM, realiza la "lectura especial" (pgm_read_byte) para extraer los datos de PROGMEM en lugar de RAM, y la imprime. Tenga en cuenta que esto toma un ciclo de reloj adicional, por byte.

Y aún más interesante: ¿qué sucede con las constantes literales como en esta expresión que a = 5*(10+7)5, 10 y 7 realmente se copian en la RAM antes de cargarlas en los registros? No puedo creer eso.

No, porque no tienen que serlo. Eso se compilaría en una instrucción "cargar literal en registro". Esa instrucción ya está en PROGMEM, por lo que ahora se trata el literal. No es necesario copiarlo en la RAM y luego volver a leerlo.


Tengo una larga descripción de estas cosas en la página Poner datos constantes en la memoria del programa (PROGMEM) . Eso tiene un código de ejemplo para configurar cadenas y matrices de cadenas, razonablemente fácil.

También menciona la macro F (), que es una manera fácil de imprimir simplemente desde PROGMEM:

Serial.println (F("Hello, world"));

Un poco de complejidad del preprocesador permite que se compile en una función auxiliar que extrae los bytes en la cadena de PROGMEM un byte a la vez. No se requiere el uso intermedio de RAM.

Es bastante fácil usar esa técnica para otras cosas que no sean Serie (por ejemplo, su LCD) derivando la impresión LCD de la clase Print.

Como ejemplo, en una de las bibliotecas LCD que escribí, hice exactamente eso:

class I2C_graphical_LCD_display : public Print
{
...
    size_t write(uint8_t c);
};

El punto clave aquí es derivar de Imprimir y anular la función "escribir". Ahora su función anulada hace lo que sea necesario para generar un carácter. Como se deriva de Print, ahora puede usar la macro F (). p.ej.

lcd.println (F("Hello, world"));
Nick Gammon
fuente