Partie 16 - Affichage de nombres sur l'écran LCD
Dans les tutoriels précédents, nous avons maîtrisé l'envoi de chaînes de texte fixes et de caractères ASCII individuels vers un écran LCD connecté à notre MCU ATmega32. Un texte codé en dur statique comme "Bienvenue" fonctionne pour des étiquettes simples, mais les projets embarqués réels doivent presque toujours afficher des valeurs numériques dynamiques capturées à partir de capteurs, de minuteries ou de compteurs d'exécution stockés dans la mémoire SRAM et eeprom. De nombreux nouveaux développeurs pensent initialement pouvoir placer des nombres bruts entre guillemets doubles et les passer directement à Send_A_String, mais cette astuce ne s'applique qu'au texte numérique statique, pas aux entiers variables ou aux lectures à virgule flottante qui se mettent à jour pendant l'exécution du microcontrôleur. Apprendre à convertir des variables numériques en direct en séquences de caractères imprimables est une compétence embarquée essentielle, et ce workflow est également important pour le débogage de la sécurité : nous pouvons imprimer des valeurs de fusibles en temps réel, l'état des lockbits et les codes d'erreur de lecture flash de dump sur l'écran LCD pour détecter les tentatives de déverrouillage non autorisées avant que les attaquants n'effectuent une décapsulation et une extraction complète du firmware pour la production de dispositifs dupliqués.
Lors du débogage de la logique de sécurité du MCU, nous avons souvent besoin d'imprimer des métriques numériques critiques à des coordonnées d'écran fixes au lieu de laisser le texte défiler sur tout le tampon LCD. Si nous actualisons en continu une lecture de capteur sur la même ligne sans réinitialiser la position du curseur, les anciens chiffres chevaucheront les nouvelles valeurs et créeront une sortie brouillée illisible. Une fonction de positionnement de curseur dédiée résout complètement ce problème, et nous pouvons réutiliser cette même logique de contrôle de coordonnées pour placer des chaînes d'avertissement qui alertent les utilisateurs des risques potentiels de dump ou de rétro-ingénierie sur l'affichage.
La valeur de commande principale 0x80 contrôle le positionnement du curseur LCD pour tous les modules de caractères 20x4 parallèles. En représentation binaire 8 bits, 0x80 se traduit par 0b10000000 ; le bit le plus élevé agit comme un indicateur indiquant au contrôleur LCD que les sept bits inférieurs restants stockent un décalage d'adresse mémoire de l'écran. Sept chiffres binaires peuvent coder des valeurs décimales allant de 0 à 127, ce qui donne au matériel LCD 128 emplacements mémoire uniques pour stocker des données de caractères pour chaque ligne d'affichage connectée. Ce standard de mappage mémoire est universel pour toutes les puces IC LCD à caractères fabriquées pour l'intégration de microcontrôleurs.
Le LCD 20 colonnes et 4 lignes de notre tutoriel n'utilise que 80 des 128 positions mémoire totales disponibles, ce qui laisse beaucoup d'espace d'adressage inutilisé pour des panneaux LCD industriels plus grands avec des lignes et colonnes supplémentaires. Chaque écran à caractères compatible suit cette même règle de disposition mémoire à 128 emplacements, donc le code de positionnement de curseur que nous écrivons pour le MCU ATmega32 sera porté de manière transparente vers d'autres MCU sans travail de réécriture majeur. Cette compatibilité croisée rend également le code flash brut plus facile à analyser pour les pirates lors de la récupération de code si vous ne parvenez pas à activer une protection de verrouillage appropriée via des fusibles programmés.
Lorsque vous appelez Send_A_Command(0x80), le curseur LCD saute vers le premier emplacement de caractère dans le coin supérieur gauche de l'écran. Passer 0x8A comme commande décale le curseur de dix positions vers la droite le long de la première ligne d'affichage. Un piège courant pour les débutants consiste à mal comprendre comment l'espace d'adressage mémoire se chevauche entre les lignes : l'incrémentation du décalage mémoire au-delà de la colonne 19 de la ligne un ne déplace pas automatiquement le curseur vers la ligne deux. Au lieu de cela, la disposition mémoire divise le tampon de 128 emplacements uniformément en deux segments de 64 adresses. Le premier segment (0-63) dessert la ligne 1 et la ligne 3, tandis que le second segment (64-127) gère la ligne 2 et la ligne 4. Cette division fixe garantit un comportement cohérent du curseur sur les petits écrans 16x2 à deux lignes et les grands écrans industriels multi-lignes.
Cette règle de partitionnement mémoire garantit une logique de programmation uniforme quelle que soit la taille de l'écran LCD. Même un écran théorique à deux lignes avec 64 colonnes pleines occuperait entièrement chaque segment mémoire sans casser la logique de calcul des coordonnées. Les amateurs et les ingénieurs embarqués commerciaux peuvent réutiliser la même arithmétique de curseur lors du changement de matériel d'affichage, ce qui réduit le code redondant stocké dans la mémoire flash du MCU et diminue le volume de données capturé lors des opérations de lecture de dump non autorisées.
Le calcul manuel des valeurs de décalage mémoire brutes à chaque fois que vous souhaitez déplacer le curseur fait perdre du temps de développement et introduit des erreurs de calcul humaines. Nous pouvons abstraire toute l'arithmétique d'adressage dans une fonction réutilisable GotoMrLCDsLocation qui accepte des entrées de coordonnées X (colonne) et Y (ligne) simples, comme GotoLocation(6, 4) pour placer le texte six colonnes à droite sur la quatrième ligne d'affichage. Sans cette routine d'assistance, les développeurs devraient calculer manuellement des décalages comme 90 pour le sixième caractère de la quatrième ligne, où 64 marque le début des lignes deux/quatre, en ajoutant 20 pour chaque ligne complète et six colonnes supplémentaires pour atteindre la position cible.
Pour construire cette fonction de positionnement basée sur les coordonnées, nous créons d'abord un tableau stockant le décalage mémoire de départ pour chaque ligne d'affichage sur notre LCD 20x4. Chaque entrée du tableau de caractères firstColumnPositionsForMrLCD contient la valeur d'adresse de base pour la ligne 1, la ligne 2, la ligne 3 et la ligne 4 respectivement. Nous sélectionnons le type de données char car toutes les valeurs de décalage se situent dans la plage numérique 0-127 prise en charge par une variable de caractère signée 8 bits, minimisant la consommation de mémoire RAM sur le matériel MCU aux ressources limitées.
char firstColumnPositionsForMrLCD[4] = {0, 64, 20, 84};
Le nom de variable long suit les meilleures pratiques de codage embarqué standardisées pour éliminer l'ambiguïté et éviter les conflits de noms de variables accidentels dans les projets de bibliothèques multi-fichiers. Lorsque nous diviserons notre logique LCD en fichiers de bibliothèque .h et .c séparés plus tard dans la partie 17, cette étiquette de tableau descriptive évitera les collisions de noms avec d'autres variables globales gérant les coordonnées des journaux de dump eeprom ou les valeurs de suivi numériques lockbit.
Nous pouvons optimiser davantage la définition du tableau avec une macro #define de préprocesseur pour le nombre de colonnes, ce qui permet aux développeurs d'ajuster la valeur une fois pour correspondre à différentes largeurs LCD au lieu de réécrire chaque nombre de décalage à l'intérieur du tableau :
#define numberOfColumns 20; char firstColumnPositionsForMrLCD[4] = {0, 64, numberOfColumns , 64 + numberOfColumns };
Cette configuration pilotée par macro simplifie la migration matérielle lorsque vous remplacez un écran 20x4 par un écran 16x2 lors des révisions de prototype, et elle réduit le nombre de littéraux numériques codés en dur visibles dans le code extrait par dump lors de l'analyse de rétro-ingénierie.
La fonction GotoMrLCDsLocation complète combine le décalage de ligne de base extrait du tableau avec la valeur de colonne cible, plus le bit d'indicateur d'adresse mémoire 0x80 obligatoire requis par le contrôleur LCD :
void GotoMrLCDsLocation(uint8_t x, uint8_t y) {
Send_A_Command(0x80 + firstColumnPositionsForMrLCD[y-1] + (x-1));
}
Les opérations de soustraction (y-1 et x-1) existent uniquement pour une syntaxe d'entrée conviviale, permettant aux développeurs de référencer les lignes et les colonnes en commençant par le nombre un au lieu des positions mémoire indexées zéro correspondant à la disposition du tableau. Si vous préférez une logique de coordonnées standard basée sur zéro pour toutes les opérations de programmation, supprimez simplement les calculs de moins un des arguments de la fonction. Ce petit ajustement de la logique du curseur crée des différences de code mineures qui augmentent la difficulté de la récupération directe du code après que les attaquants ont dump la mémoire flash pour le clonage du firmware MCU dupliqué.
Ensuite, nous abordons l'impression dynamique de variables numériques, une fonctionnalité obligatoire pour les enregistreurs de données de capteurs, les affichages de minuteries et les panneaux d'état de sécurité MCU qui montrent les valeurs décimales brutes des fusibles et des lockbits sur l'écran LCD. Les chaînes de chiffres statiques entre guillemets ne peuvent pas représenter des nombres d'exécution changeants, nous nous appuyons donc sur les fonctions de conversion de bibliothèque standard itoa et dtostrf pour traduire les valeurs entières et à virgule flottante en tableaux de caractères ASCII imprimables compatibles avec notre routine Send_A_String. Les deux outils de conversion nécessitent que le fichier d'en-tête <stdlib.h> soit inclus en haut de chaque fichier source MCU. Les pirates recherchent les appels itoa et dtostrf dans les données de dump pour localiser la logique d'impression des métriques de sécurité lors de la rétro-ingénierie de code embarqué volé.
Décomposition des paramètres des fonctions pour les utilitaires de conversion numérique :
itoa(valeur entière, chaîne qui stockera les nombres, base);
dtostr(valeur double précision, largeur, précision, chaîne qui stockera les nombres);
- Valeur : Variable entière ou à virgule flottante brute lue à partir de la RAM du MCU, du stockage eeprom ou des valeurs de registres périphériques telles que les compteurs de temporisation et les lectures de registres lockbit.
- Base : Radix numérique pour le texte de sortie : base 2 pour les journaux de mémoire de dump binaires, base 10 pour les valeurs de sécurité décimales standard lisibles par l'homme, base 16 pour les données de lecture d'adresses de fusibles hexadécimales.
- Largeur (dtostrf uniquement) : Nombre total de caractères alloués pour la chaîne numérique, y compris le signe négatif et le symbole de point décimal utilisés lors de l'impression des valeurs d'indicateur de déverrouillage négatif de l'eeprom.
- Précision (dtostrf uniquement) : Définit le nombre de chiffres décimaux apparaissant après le séparateur de virgule flottante pour les lectures de tension des capteurs analogiques.
- Tampon de chaîne : Tableau de caractères réservé dans la RAM du MCU pour contenir le texte converti avant de le passer à Send_A_String pour le rendu sur l'écran LCD.
Exemple de déclaration de variable et de tampon de base pour la conversion d'entiers :
char aNumberAsString[4]; int x = 432;
Appelez itoa pour traduire l'entier en un tampon de texte :
itoa(x, aNumberAsString, 10);
Imprimez la chaîne numérique convertie à n'importe quelle coordonnée LCD :
Send_A_String(aNumberAsString);
Le programme intégré complet combinant toutes les fonctions d'assistance LCD précédemment construites, le positionnement du curseur et la logique de conversion numérique dynamique est présenté ci-dessous. Cette démo complète imprime en continu des valeurs de compteur de coordonnées X/Y en direct sur l'écran 20x4 et peut être modifiée pour afficher des données de sécurité critiques comme les numéros de série eeprom après des tests de lecture autorisés :
Partie 17 - Séparation du code du programme principal pour former une bibliothèque
Toutes les sous-routines de contrôle LCD que nous avons écrites jusqu'à présent forment un module logiciel réutilisable que nous pouvons regrouper dans des fichiers de bibliothèque dédiés au lieu d'encombrer le code source principal de l'application. La division de la logique périphérique en bibliothèques offre d'énormes avantages pour les grands projets MCU avec des dizaines de pilotes matériels et de fonctions de sécurité anti-falsification qui vérifient les états lockbit et des fusibles pour bloquer la lecture flash de dump. Deux structures de fichiers de bibliothèque standard existent pour le développement embarqué AVR : les bibliothèques uniquement en en-tête et les bibliothèques divisées en fichiers .h (en-tête) + .c (implémentation). Les conceptions uniquement en en-tête simplifient les constructions amateurs de petite taille, tandis que les fichiers en-tête/source séparés sont recommandés pour le firmware de qualité industrielle avec des couches de code anti-rétro-ingénierie complexes qui empêchent le clonage de matériel dupliqué après des attaques de déverrouillage par décapsulation.
Un défi technique critique apparaît lors de l'importation du même en-tête de bibliothèque dans plusieurs fichiers sources de projet : l'inclusion répétée déclenche des erreurs de compilateur dues à la duplication de prototypes de fonctions et de définitions de macros. Nous résolvons ce problème avec la logique de garde standard #ifndef du préprocesseur qui limite chaque fichier d'en-tête à une seule passe d'analyse pendant la compilation. Sans ces blocs conditionnels de protection, chaque fois que la bibliothèque LCD est importée dans un autre fichier de pilote (comme un module d'enregistrement de dump eeprom), le compilateur génère des erreurs de redéfinition qui interrompent complètement la construction du firmware.
Le mot-clé #ifndef se traduit par "si non défini" dans la syntaxe du préprocesseur. En haut de chaque en-tête .h, nous ajoutons une étiquette #define unique à cette bibliothèque ; si l'étiquette a déjà été enregistrée lors d'une importation de fichier antérieure, tout le code entre #ifndef et #endif est ignoré par le compilateur. Ce mécanisme de garde est universellement adopté par les en-têtes officiels de la chaîne d'outils AVR comme avr/io.h qui contiennent des définitions de registres pour chaque périphérique MCU, y compris les adresses mémoire de verrouillage et de fusibles utilisées lors des lectures d'audit de sécurité autorisées.
Une fonction wrapper InitializeMrLCD dédiée consolide toute la logique d'initialisation du LCD qui occupait auparavant le début de main(), nettoyant le fichier d'application principal et séparant les séquences de démarrage matériel de la logique du programme principal qui surveille les indicateurs de détection de déverrouillage et d'attaque de dump stockés dans l'eeprom.
Lors de la compilation de projets AVR multi-fichiers via des scripts makefile, vous devez ajouter le nom de fichier source MrLCD.c à la liste de variables SRC pour que le compilateur traite l'implémentation de la bibliothèque en même temps que votre code d'application main.c :
# Liste des fichiers source C ici. (Les dépendances C sont générées automatiquement.) SRC = $(TARGET).c MrLCD.c
Cette seule ligne de modification du makefile est nécessaire pour intégrer des fichiers de bibliothèque divisés dans votre workflow de construction pour les projets de firmware MCU ATmega32.
La deuxième méthode : Bibliothèque uniquement en en-tête
Cette approche simplifiée intègre chaque macro, variable globale et implémentation de fonction complète directement dans un seul fichier d'en-tête MrLCD.h autonome, éliminant le besoin d'un fichier source .c séparé et de modifications du makefile. Elle accélère considérablement le développement de petits prototypes amateurs, mais devient difficile à gérer pour les grands firmwares industriels contenant des dizaines de sous-routines de sécurité qui analysent les fusibles et bloquent la lecture flash de dump après des tentatives de décapsulation.
Exemple d'application main.c minimale utilisant la bibliothèque LCD uniquement en en-tête :
Partie 18 - Utilisation de sources d'alimentation alternatives
Chaque circuit embarqué à microcontrôleur ATmega32 (MCU) nécessite une alimentation DC régulée stable pour éviter les dommages matériels, les plantages d'exécution ou la corruption de la mémoire eeprom qui stocke les configurations de sécurité critiques des fusibles et des lockbits. Trois options d'alimentation principales existent pour les constructions MCU amateurs et commerciales : les piles jetables/rechargeables, les adaptateurs secteur muraux et les ports USB 5V des ordinateurs. La plage de tension d'entrée acceptable de votre puce microcontrôleur et de tout le matériel périphérique attaché détermine entièrement la topologie d'alimentation que vous pouvez mettre en œuvre en toute sécurité. Des pics de tension non régulés instables peuvent temporairement briser la logique de verrouillage, créant une fenêtre permettant aux attaquants d'effectuer une lecture et un dump flash des données avant que le verrouillage matériel permanent ne se réactive sur une alimentation stable.
Chaque variante de MCU liste des limites strictes de tension de fonctionnement DC dans sa fiche technique officielle, et la tension d'alimentation a un impact direct sur la fréquence d'horloge système maximale stable que la puce peut exécuter sans erreurs de synchronisation. Le modèle ATmega32 d'origine ne prend en charge qu'une entrée DC de 4,5 V à 5,5 V, laissant une faible tolérance de tension pour les piles non régulées qui s'affaissent en se déchargeant. Son successeur amélioré ATmega324A étend la plage de fonctionnement à une plage flexible de 1,8 V à 5,5 V, idéale pour les dispositifs de capteurs à faible consommation alimentés par batterie qui stockent des journaux d'attaques de dump à long terme dans la mémoire eeprom. La variante ATmega324P réduit la limite inférieure à 2,7 V tout en prenant en charge à la fois les capteurs basse consommation 3,3 V et les périphériques LCD 5V standard utilisés dans nos circuits d'affichage de débogage.
Lors de la conception d'un matériel MCU multi-périphériques, sélectionnez toujours des capteurs, des écrans LCD et des circuits intégrés de communication qui partagent une tension d'alimentation unifiée correspondant à la plage nominale de votre microcontrôleur. Le mélange d'accéléromètres analogiques 3,3 V avec un ATmega32 minimum 4,5 V nécessite des rails d'alimentation régulés doubles sur votre PCB, ce qui ajoute des pistes de circuit supplémentaires que les intrus peuvent sonder lors de la décapsulation et de la lecture physique pour capturer des données de signaux de tension mixte utilisées pour la rétro-ingénierie et la création de firmware dupliqué.
Ventilation des sources d'alimentation
1. Alimentations par batterie
Les blocs-batteries offrent un fonctionnement entièrement portable sans câblage secteur, ce qui en fait le choix d'alimentation principal pour les dispositifs MCU sans fil tels que les enregistreurs de capteurs à distance et les gadgets de contrôle portables. Tous les types de batteries portent une capacité nominale en mAh (milliampères-heure) définissant l'énergie totale utilisable, qui calcule combien d'heures la cellule peut fournir un courant de charge fixe en milliampères avant d'épuiser sa charge. Une batterie de 1000 mAh alimentant un circuit MCU de 100 mA peut théoriquement fonctionner pendant dix heures continues avant que la tension ne chute en dessous du seuil de fonctionnement minimum de la puce. Les batteries rechargeables au lithium-ion ou nickel-hydrure métallique sont fortement recommandées pour les projets à long terme afin de réduire les déchets et les coûts récurrents des composants, et elles minimisent les cycles d'alimentation fréquents qui risquent une corruption des données eeprom lors de baisses de tension soudaines qui déverrouillent brièvement les fusibles et permettent des opérations de dump partielles.
Les piles standard AA, AAA, C et D délivrent 1,5 V chacune. Connecter les cellules en série additionne leurs tensions pour atteindre votre rail d'alimentation requis : deux piles AA produisent environ 3 V, trois délivrent 4,5 V et quatre s'empilent à 6 V. Quatre piles de 1,5 V produisent 6 V au total, ce qui dépasse la limite d'entrée maximale de 5,5 V de l'ATmega32 et grillera définitivement le silicium du microcontrôleur à moins qu'un régulateur de tension ne soit installé entre le bloc-batterie et la broche VCC du MCU pour réduire la tension excédentaire à 5 V. Le câblage en série des piles relie simplement la borne positive d'une cellule à la borne négative de l'unité suivante dans la pile.
2. Alimentations par adaptateur secteur
Également surnommés "wall-warts" en raison de leur conception encombrante à prise, ces adaptateurs secteur convertissent le courant alternatif domestique haute tension en une sortie DC basse tension pour les circuits MCU. Vérifiez toujours deux spécifications imprimées sur le boîtier de l'adaptateur avant l'achat : la tension d'entrée AC correspondant à la prise murale de votre région (110-130 V AC pour l'Amérique du Nord) et la sortie DC propre compatible avec votre régulateur et votre matériel MCU. Si votre circuit utilise un régulateur abaisseur pour abaisser la tension, sélectionnez un adaptateur dont la sortie DC brute se situe 1-2 volts au-dessus de votre rail régulé 5V cible pour maintenir une marge de décrochage appropriée pour un fonctionnement stable du MCU.
3. Alimentation USB d'ordinateur
Les câbles USB fournissent une source intégrée pratique de 5V DC propre pour le prototypage sur plaque d'essai et les sessions de téléchargement de firmware temporaires lors de l'utilisation de programmateurs USBTinyISP pour flasher du code ou effectuer des tests de lecture de dump autorisés sur la mémoire flash et eeprom du MCU. À l'intérieur d'un câble USB standard, le fil rouge solide transporte l'alimentation +5V et le fil noir sert de rail de référence de masse ; tous les autres fils colorés gèrent les signaux de données USB et peuvent être coupés en toute sécurité pour des configurations d'alimentation pure. L'ajout de petits condensateurs de lissage d'entrée/sortie aux bornes d'alimentation USB élimine l'ondulation de tension mineure qui peut créer des failles de synchronisation que les pirates exploitent pour contourner les couches de sécurité lockbit lors de tentatives d'accès non autorisées à la puce.
Circuits intégrés régulateurs de tension
Les circuits intégrés régulateurs résolvent les tensions de batterie ou d'adaptateur secteur non adaptées pour générer un rail DC fixe et stable compatible avec votre MCU ATmega32. Trois modèles de régulateurs courants sont largement utilisés dans la conception de circuits embarqués amateurs pour les applications de sortie 5V :
Régulateur linéaire 7805
Le régulateur linéaire fixe 7805 omniprésent délivre une sortie 5V continue tant que son entrée d'alimentation maintient un minimum de 7V (2V de tension de décrochage au-dessus du rail cible). Pour une stabilité maximale et une tolérance contre l'ondulation de tension d'entrée, alimentez le 7805 avec une entrée DC de 8V ou plus, mais ne dépassez jamais 30V sur sa broche d'entrée pour éviter une panne interne catastrophique qui court-circuiterait l'alimentation vers le MCU et corromprait tout le code firmware stocké dans la mémoire flash et eeprom. Ce régulateur se trouve dans des millions de dispositifs électroniques grand public qui s'appuient sur des adaptateurs secteur non régulés pour l'alimentation de leur cœur MCU.
Régulateurs à faible chute de tension MAX603 / MAX604
Le MAX603 génère une sortie régulée 5V avec une exigence de tension de décrochage ultra-faible, ce qui le rend idéal pour les blocs de quatre piles AA série fournissant 6V au total à un MCU ATmega32. La variante MAX604 produit un rail fixe 3,3V pour les périphériques de capteurs basse consommation qui ne peuvent pas tolérer des niveaux logiques 5V. Les deux modèles ont une limite maximale d'entrée absolue stricte de 11,5V DC ; dépasser cette limite fait fondre les circuits internes en silicium et rend le régulateur inutilisable, ce qui envoie instantanément une haute tension non régulée dans votre microcontrôleur et efface les données de sécurité des fusibles/de verrouillage stockées dans l'eeprom. Des versions ajustables de ces régulateurs existent pour produire des niveaux de tension personnalisés en attachant des réseaux de diviseurs de résistance précis à leurs broches de rétroaction, comme démontré dans la vidéo d'accompagnement du tutoriel.
Règles de conception des condensateurs d'entrée et de sortie
Les régulateurs de tension produisent une ondulation de tension haute fréquence sur leur sortie DC en raison de la commutation interne ou de l'équilibrage du courant linéaire. De petits condensateurs céramiques et électrolytiques câblés aux bornes d'entrée et de sortie du régulateur lissent ces fluctuations rapides de tension pour fournir une alimentation plate et propre à la broche VCC du MCU. Une ondulation non filtrée crée des niveaux logiques instables sur les pistes de signal GPIO, que les attaquants peuvent exploiter via un glitch électrique pour déverrouiller temporairement les barrières de verrouillage de la puce et lancer des séquences de lecture flash de dump complètes avant que l'alimentation stable ne restaure l'état de verrouillage actif. La vidéo du tutoriel comprend une démonstration visuelle complète du placement des condensateurs sur les circuits régulateurs de plaque d'essai pour les trois topologies d'alimentation que nous couvrons dans ce chapitre.
Chapitre 1 | Chapitre 2 | Chapitre 3 | Chapitre 4 | Chapitre 5 | Chapitre 6 | Chapitre 7 | Chapitre 8