Quoi de pire que des béquilles? Béquilles uniquement incomplètement documentées.

Voici une capture d'écran du dernier environnement de développement intégré officiel pour les microcontrôleurs AVR 8 bits, Atmel Studio 7, le langage de programmation C. Comme vous pouvez le voir dans la colonne Valeur, la variable my_array contient le nombre 0x8089. En d'autres termes, le tableau my_array est situé en mémoire à partir de l'adresse 0x8089.
Dans le même temps, la colonne Type nous donne des informations légèrement différentes: my_array est un tableau de 4 éléments de type int16_t situé dans la ROM (cela est indiqué par le mot prog, contrairement aux données pour RAM), à partir de l'adresse 0x18089. Arrêtez, mais après tout 0x8089! = 0x18089. Quelle est l'adresse réelle de la baie?
Langage C et architecture Harvard
Les microcontrôleurs AVR 8 bits fabriqués auparavant par Atmel, et maintenant Microchip, sont populaires, en particulier, en raison du fait qu'ils sont la base d'Arduino, construit sur l'architecture Harvard, c'est-à-dire que le code et les données sont situés dans des espaces d'adressage différents. La documentation officielle contient des exemples de code en deux langues: assembleur et C. Auparavant, le fabricant offrait un environnement de développement intégré gratuit qui ne prend en charge que l'assembleur. Mais qu'en est-il de ceux qui souhaiteraient programmer en C, voire en C ++? Il y avait des solutions payantes, par exemple, IAR AVR et CodeVisionAVR. Personnellement, je ne l'ai jamais utilisé, car lorsque j'ai commencé à programmer AVR en 2008, il y avait déjà WinAVR gratuit avec la possibilité de s'intégrer à AVR Studio 4, et il est simplement inclus dans l'actuel Atmel Studio 7.
Le projet WinAVR est basé sur le compilateur GNU GCC, qui a été développé pour l'architecture von Neumann, ce qui implique un espace d'adressage unique pour le code et les données. Lors de l'adaptation de GCC à AVR, la béquille suivante a été appliquée: les adresses 0 à 0x007fffff sont allouées pour le code (ROM, flash) et 0x00800100 pour 0x0080ffff pour les données (RAM, SRAM). Il y avait toutes sortes d'autres astuces, par exemple, des adresses de 0x00800000 à 0x008000ff représentées par des registres accessibles par les mêmes opcodes que la RAM. En principe, si vous êtes un simple programmeur, comme un Arduino débutant, et non un hacker, mélangeant assembleur et C / C ++ dans le même firmware, vous n'avez pas besoin de tout savoir.
En plus du compilateur actuel, WinAVR comprend diverses bibliothèques (faisant partie de la bibliothèque C standard et des modules spécifiques à AVR) sous la forme du projet AVR Libc. La dernière version, 2.0.0, est sortie il y a près de trois ans, et la documentation est disponible non seulement sur le site du projet lui-même, mais également sur le site du fabricant du microcontrôleur. Il existe également des traductions russes non officielles.
Données dans l'espace d'adressage du code
Parfois, dans un microcontrôleur, vous devez mettre non seulement beaucoup, mais beaucoup de données: tellement qu'elles ne rentrent tout simplement pas dans la RAM. De plus, ces données sont immuables, connues au moment du firmware. Par exemple, une image tramée, une mélodie ou une sorte de tableau. Dans le même temps, le code ne prend souvent qu'une petite fraction de la ROM disponible. Alors pourquoi ne pas utiliser l'espace restant pour les données? C'est facile! La documentation avr-libc 2.0.0 couvre un chapitre entier de 5 données dans l'espace programme. Si vous omettez la partie concernant les lignes, alors tout est extrêmement simple. Prenons un exemple. Pour la RAM, nous écrivons comme ceci:
unsigned char array2d[2][3] = {...}; unsigned char element = array2d[i][j];
Et pour ROM comme ceci:
#include <avr/pgmspace.h> const unsigned char array2d[2][3] PROGMEM = {...}; unsigned char element = pgm_read_byte(&(array2d[i][j]));
C'est si simple que cette technologie a été reprise à plusieurs reprises, même dans RuNet.
Alors quel est le problème?
Rappelez-vous que 640 Ko suffisent à tout le monde? Rappelez-vous comment vous êtes passé d'une architecture 16 bits à 32 bits et de 32 bits à 64 bits? Comment Windows 98 fonctionnait-il de manière instable sur plus de 512 Mo de RAM alors qu'il était conçu pour 2 Go? Avez-vous déjà mis à jour le BIOS pour que la carte mère fonctionne avec des disques durs supérieurs à 8 Go? Rappelez-vous les cavaliers sur les disques durs de 80 Go qui les coupent jusqu'à 32 Go?
Le premier problème m'a dépassé lorsque j'ai essayé de créer un tableau d'au moins 32 Ko dans la ROM. Pourquoi en ROM, et non en RAM? Parce qu'à l'heure actuelle, les AVR 8 bits avec plus de 32 Ko de RAM n'existent tout simplement pas. Et avec plus de 256 B - existent. C'est probablement pourquoi les créateurs du compilateur ont choisi 16 b (2 B) pour les pointeurs en RAM (et en même temps pour le type int), ce qui peut être trouvé dans la lecture du paragraphe Types de données situé au chapitre 11.14 Quels registres sont utilisés par le compilateur C? Documentation AVR Libc. Oh, et nous n'allions pas pirater, mais voici les registres ... Mais revenons au tableau. Il s'est avéré que vous ne pouvez pas créer un objet plus grand que 32 767 B (2 ^ (16 - 1) - 1 B). Je ne sais pas pourquoi il était nécessaire de rendre la longueur de l'objet significative, mais c'est un fait: aucun objet, même un tableau multidimensionnel, ne peut avoir une longueur de 32 768 B ou plus. Un peu comme une limitation de l'espace d'adressage des applications 32 bits (4 Go) dans un système d'exploitation 64 bits, n'est-ce pas?
Pour autant que je sache, ce problème n'a pas de solution. Si vous souhaitez placer un objet d'une longueur de 32 768 dans la ROM, divisez-le en objets plus petits.
Revenons au paragraphe Types de données: les pointeurs font 16 bits. Nous appliquons ces connaissances au chapitre 5 de Data in Program Space. Non, la théorie est indispensable, la pratique est nécessaire. J'ai écrit un programme de test, lancé un débogueur (malheureusement, logiciel, pas matériel) et j'ai vu que la fonction pgm_read_byte
capable de renvoyer uniquement les données dont les adresses tiennent en 16 bits (64 Ko; merci, pas 15). Puis un débordement se produit, la partie la plus ancienne est jetée. C'est logique, étant donné que les pointeurs sont 16 bits. Mais deux questions se posent: pourquoi cela n'est pas écrit dans le chapitre 5 (une question rhétorique, mais c'est lui qui m'a poussé à écrire cet article) et comment surmonter la limite de 64 Ko ROM sans passer à l'assembleur.
Heureusement, en plus du chapitre 5, il existe une autre référence de fichier 25,18 pgmspace.h, à partir de laquelle nous apprenons que la famille de fonctions pgm_read_*
n'est qu'une pgm_read_*_near
de pgm_read_*_near
, qui accepte des adresses 16 bits, et il y a aussi pgm_read_*_far
, et vous pouvez soumettre Adresse 32 bits Eureka!
Nous écrivons le code:
unsigned char element = pgm_read_byte_far(&(array2d[i][j]));
Il compile, mais ne fonctionne pas comme nous le souhaiterions (si array2d est situé après 32 Ko). Pourquoi? Oui, car l'opération &
renvoie un nombre signé de 16 bits! C'est drôle que la famille pgm_read_*_near
accepte les adresses 16 bits non signées, c'est-à-dire qu'elle est capable de travailler avec 64 Ko de données, et l'opération &
n'est utile que pour 32 Ko.
Continuons. Qu'avons-nous dans pgmspace.h à part pgm_read_*
? La fonction pgm_get_far_address(var)
, qui a déjà une demi-page de description, et remplace l'opération &
.
Probablement raison:
unsigned char element = pgm_read_byte_far(pgm_get_far_address(array2d[i][j]));
Erreur de compilation. Nous lisons la description: 'var' doit être résolu au moment de la liaison comme un symbole existant, c'est-à-dire un nom de variable de type simple, un nom de tableau (pas un élément indexé du tableau, si l'index est une constante, le compilateur ne se plaint pas mais ne parvient pas à obtenir l'adresse si l'optimisation est activée), un nom de structure ou un nom de champ de structure, un identificateur de fonction, un identificateur défini par l'éditeur de liens, ...
On met une autre béquille: on passe des indices matriciels à l'arithmétique des pointeurs:
unsigned char element = pgm_read_byte_far(pgm_get_far_address(array2d) + i*3*sizeof(unsigned char) + j*sizeof(unsigned char));
Maintenant, tout fonctionne.
Conclusions
Si vous écrivez en C / C ++ pour les microcontrôleurs AVR 8 bits à l'aide du compilateur GCC et stockez les données dans la ROM, alors:
- avec une taille de ROM ne dépassant pas 32 Ko, vous ne rencontrerez pas de problèmes en lisant uniquement le chapitre 5 Données dans l'espace programme;
- pour les ROM supérieures à 32 Ko, vous devez utiliser la famille de fonctions
pgm_read_*_far
, la fonction pgm_get_far_address
au lieu de &
, l'arithmétique du pointeur au lieu des indices de tableau, et la taille d'un objet ne peut pas dépasser 32 767 B.
Les références