PROGMEM: dois-je copier les données du flash vers la RAM pour les lire?

8

J'ai des difficultés à comprendre la gestion de la mémoire.

La documentation Arduino dit, il est possible de garder des constantes comme des chaînes ou tout ce que je ne veux pas changer pendant l'exécution dans la mémoire du programme. Je pense qu'il est intégré quelque part dans le segment de code, ce qui doit être assez possible dans une architecture von-Neumann. Je veux en profiter pour rendre mon menu d'interface utilisateur sur un écran LCD possible.

Mais je suis déconcerté par ces instructions pour simplement lire et imprimer les données de la mémoire du programme:

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

Pourquoi diable dois-je copier le fichu contenu sur la RAM avant d'y accéder? Et si c'est vrai, qu'advient-il alors de tout le code? Est-il également chargé dans la RAM avant exécution? Comment est alors géré le code (32 ko) avec seulement 2 ko de RAM? Où sont ces petits gobelins portant des disquettes?

Et encore plus intéressant: qu'arrive-t-il aux constantes littérales comme dans cette expression:

a = 5*(10+7)

les 5, 10 et 7 sont-ils vraiment copiés dans la RAM avant de les charger dans les registres? Je ne peux pas croire ça.

Ariser - réintégrer Monica
la source
Une variable globale est chargée en mémoire et n'en est jamais libérée. Le code ci-dessus copie uniquement les données en mémoire lorsque cela est nécessaire et les libère une fois terminé. Notez également que le code ci-dessus ne lit qu'un octet dans le string_tabletableau. Ce tableau peut être de 20 Ko et ne tient jamais en mémoire (même temporairement). Vous ne pouvez cependant charger qu'un seul index en utilisant la méthode ci-dessus.
Gerben
@Gerben: C'est un vrai inconvénient sur les variables globales, je n'en ai pas encore pris en compte. J'ai des maux de tête maintenant. Et l'extrait de code n'était qu'un exemple de la documentation. Je me suis abstenu de programmer qch. moi-même avant d'avoir des éclaircissements sur les concepts. Mais j'ai un aperçu maintenant. Merci!
Ariser - réintègre Monica
J'ai trouvé la documentation quelque peu confuse lorsque je l'ai lue pour la première fois. Essayez également de regarder des exemples concrets (comme par exemple une bibliothèque).
Gerben

Réponses:

10

L'AVR est une famille d' architecture Harvard modifiée , donc le code est stocké en flash uniquement, tandis que les données existent principalement dans la RAM lors de leur manipulation.

Dans cet esprit, répondons à vos questions.

Pourquoi diable dois-je copier le fichu contenu sur la RAM avant d'y accéder?

Vous n'avez pas besoin de le faire en soi, mais par défaut, le code suppose que les données sont dans la RAM sauf si le code est modifié pour le rechercher spécifiquement en flash (comme avec strcpy_P()).

Et si c'est vrai, qu'advient-il alors de tout le code? Est-il également chargé dans la RAM avant exécution?

Nan. Architecture de Harvard. Voir la page Wikipedia pour tous les détails.

Comment est alors géré le code (32 ko) avec seulement 2 ko de RAM?

Le préambule généré par le compilateur copie les données qui doivent être modifiables / modifiées dans SRAM avant d'exécuter le programme réel.

Où sont ces petits gobelins portant des disquettes?

Je ne sais pas. Mais si vous les voyez, je ne peux rien faire pour vous aider.

... les 5, 10 et 7 sont-ils vraiment copiés dans la RAM avant de les charger dans les registres?

Non. Le compilateur évalue l'expression au moment de la compilation. Tout ce qui se passe dépend des lignes de code qui l'entourent.

Ignacio Vazquez-Abrams
la source
D'accord, je ne savais pas qu'AVR était harvard. Mais je connais ce concept. Mis à part les gobelins, je pense que je sais quand utiliser ces fonctions de copie maintenant. Je dois restreindre l'utilisation de PROGMEM aux données qui sont rarement utilisées pour économiser les cycles CPU.
Ariser - réintègre Monica
Ou modifiez votre code pour l'utiliser directement depuis flash.
Ignacio Vazquez-Abrams
Mais à quoi ressemblerait ce code? disons que j'ai plusieurs tableaux de uint8_t représentant des chaînes que je veux mettre sur un écran LCD via 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};comment amener ces données à flasher et plus tard dans la fonction SPI.transfer (), qui prend un uint8_t par appel.
Ariser - réintègre Monica
8

Voici comment Print::printimprime à partir de la mémoire du programme dans la bibliothèque 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*est une classe vide qui permet aux fonctions surchargées comme print de différencier un pointeur pour programmer la mémoire d'une à la mémoire normale, car les deux sont vues comme const char*par le compilateur (voir /programming/16597437/arduino-f- qu'est-ce-qu'il-fait-réellement )

Vous pouvez donc surcharger la printfonction de votre écran LCD pour qu'elle prenne un __FlashStringHelper*argument, l'appelle LCD::print, puis utilise lcd.print(F("this is a string in progmem"));' to call it.F () `est une macro qui garantit que la chaîne est dans la mémoire du programme.

Pour prédéfinir la chaîne (pour être compatible avec l'impression Arduino intégrée), j'ai utilisé:

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

Je pense qu'une alternative serait quelque chose comme

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;
}

ce qui éviterait le __FlashStringHelpercasting.

geometrikal
la source
2

La documentation Arduino dit, il est possible de garder des constantes comme des chaînes ou tout ce que je ne veux pas changer pendant l'exécution dans la mémoire du programme.

Toutes les constantes sont initialement dans la mémoire du programme. Où seraient-ils ailleurs lorsque le courant est coupé?

Je pense qu'il est intégré quelque part dans le segment de code, ce qui doit être assez possible dans une architecture von-Neumann.

C'est en fait l'architecture de Harvard .

Pourquoi diable dois-je copier le fichu contenu sur la RAM avant d'y accéder?

Non. En fait, il existe une instruction matérielle (LPM - Load Program Memory) qui déplace les données directement de la mémoire du programme dans un registre.

J'ai un exemple de cette technique en sortie Arduino Uno sur un moniteur VGA . Dans ce code, une police bitmap est stockée dans la mémoire du programme. Il est lu à partir de cela à la volée et copié dans la sortie comme ceci:

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

Un démontage de ces lignes montre (en partie):

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

Vous pouvez voir qu'un octet de mémoire de programme a été copié dans R30, puis immédiatement stocké dans le registre USART UDR0. Aucune RAM impliquée.


Il existe cependant une complexité. Pour les chaînes normales, le compilateur s'attend à trouver des données dans la RAM et non PROGMEM. Ce sont des espaces d'adressage différents, et donc 0x200 dans la RAM est quelque chose de différent de 0x200 dans PROGMEM. Ainsi, le compilateur se donne la peine de copier des constantes (comme des chaînes) dans la RAM au démarrage du programme, il n'a donc pas à se soucier de connaître la différence plus tard.

Comment est alors géré le code (32 ko) avec seulement 2 ko de RAM?

Bonne question. Vous ne vous en sortirez pas avec plus de 2 Ko de chaînes constantes, car il n'y aura pas de place pour les copier toutes.

C'est pourquoi les gens qui écrivent des choses comme des menus et d'autres trucs verbeux, prennent des mesures supplémentaires pour donner aux chaînes l'attribut PROGMEM, ce qui les désactive d'être copiées dans la RAM.

Mais je suis déconcerté par ces instructions pour simplement lire et imprimer les données de la mémoire du programme:

Si vous ajoutez l'attribut PROGMEM, vous devez prendre des mesures pour informer le compilateur que ces chaînes se trouvent dans un espace d'adressage différent. Faire une copie complète (temporaire) est un moyen. Ou imprimez simplement directement à partir de PROGMEM, un octet à la fois. Un exemple de cela est:

// 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 vous transmettez à cette fonction un pointeur sur une chaîne dans PROGMEM, elle effectue la "lecture spéciale" (pgm_read_byte) pour extraire les données de PROGMEM plutôt que de RAM et les imprime. Notez que cela prend un cycle d'horloge supplémentaire, par octet.

Et encore plus intéressant: qu'arrive-t-il aux constantes littérales comme dans cette expression a = 5*(10+7)les 5, 10 et 7 sont-ils vraiment copiés dans la RAM avant de les charger dans les registres? Je ne peux pas croire ça.

Non, car ce n'est pas obligatoire. Cela se compilerait en une instruction "charger le littéral dans le registre". Cette instruction est déjà dans PROGMEM, donc le littéral est maintenant traité. Pas besoin de le copier dans la RAM puis de le relire.


J'ai une longue description de ces choses sur la page Mettre des données constantes dans la mémoire du programme (PROGMEM) . Cela a un exemple de code pour configurer des chaînes et des tableaux de chaînes, assez facilement.

Il mentionne également la macro F () qui est un moyen simple d'imprimer simplement à partir de PROGMEM:

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

Un peu de complexité du préprocesseur permet de compiler dans une fonction d'assistance qui extrait les octets de la chaîne de PROGMEM un octet à la fois. Aucune utilisation intermédiaire de RAM n'est requise.

Il est assez facile d'utiliser cette technique pour des choses autres que série (par exemple votre écran LCD) en dérivant l'impression LCD de la classe d'impression.

Par exemple, dans l'une des bibliothèques LCD que j'ai écrites, j'ai fait exactement cela:

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

Le point clé ici est de dériver de Print et de remplacer la fonction "écriture". Maintenant, votre fonction remplacée fait tout ce dont elle a besoin pour sortir un caractère. Puisqu'il est dérivé de Print, vous pouvez maintenant utiliser la macro F (). par exemple.

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