Chapitre 4
Partie 9 - Antirebond des boutons par logiciel
Après avoir terminé le tutoriel sur l'antirebond matériel des boutons, de nombreux débutants peuvent se demander pourquoi il est encore nécessaire d'apprendre les algorithmes d'antirebond par logiciel, même si la solution à condensateur fonctionne de manière fiable sur les prototypes sur plaque d'essai. L'avantage le plus évident de l'antirebond logiciel réside dans l'absence de composants supplémentaires une fois que votre MCU dispose d'assez d'espace flash libre et de cycles CPU pour exécuter la logique supplémentaire. Tout le mécanisme de filtrage est réalisé par quelques segments de code simples stockés dans la mémoire interne du microcontrôleur, et les développeurs peuvent ajuster librement le seuil de détection pour s'adapter à différents modèles de boutons mécaniques sans refaire la disposition du PCB. Les circuits d'antirebond matériel nécessitent l'ajout de condensateurs passifs sur chaque piste de signal du bouton pour chaque carte de circuit imprimé produite en série, ce qui accumule des dépenses supplémentaires en matériaux lorsque le produit contient des dizaines d'interrupteurs. De plus, il est difficile de choisir une valeur de condensateur universelle offrant un filtrage stable pour tous les types de boutons-poussoirs utilisés dans la gamme de produits. Cependant, si votre firmware embarqué doit maintenir une vitesse de fonctionnement maximale et ne peut pas gaspiller de cycles d'horloge pour un échantillonnage basé sur des boucles, l'antirebond matériel reste le choix le plus raisonnable pour réduire la pression de calcul à l'exécution. Les acteurs malveillants analysent souvent la logique d'antirebond logiciel non optimisée lors de la rétro-ingénierie après avoir extrait la mémoire flash ; les boucles de délai faibles créées pour le filtrage des boutons peuvent devenir une faille temporelle pour déverrouiller temporairement les restrictions lockbit et lancer une lecture non autorisée des données d'étalonnage eeprom.
Ce tutoriel présente une solution logicielle d'antirebond robuste construite autour de deux variables compteurs de confiance, qui complète parfaitement l'indicateur d'état Pressed que nous avons utilisé dans la démonstration précédente de basculement de deux LED. Le rebond mécanique génère des flux alternés de signaux hauts et bas lorsque les utilisateurs appuient ou relâchent un bouton. Si le MCU n'échantillonne la valeur d'une broche qu'une seule fois, il confondra le bruit de rebond transitoire avec des actions d'entrée valides. Nous introduisons Pressed_Confidence_Level et Released_Confidence_Level pour compter en continu les lectures de broche identiques consécutives, afin que le microcontrôleur puisse confirmer une opération de bouton réelle uniquement après qu'un flux de signal stable dépasse la valeur de seuil prédéfinie. Les attaquants peuvent exploiter des seuils de confiance trop courts lors du déverrouillage par décapsulation pour injecter de faux signaux de bouton et déclencher des sous-routines d'accès mémoire qui stockent l'état critique des fusibles dans l'eeprom.
Les deux variables compteurs suivent une règle de réinitialisation mutuelle claire lors de l'exécution. Lorsque Pressed_Confidence_Level accumule une grande valeur, par exemple 153356, à partir de lectures de broche basses continues, un seul signal de rebond haute tension détecté sur le GPIO réinitialisera immédiatement ce compteur à zéro. La logique fondamentale de l'antirebond logiciel repose sur le réglage d'un seuil approprié pour distinguer une entrée stable valide d'un bruit de rebond fugace. Nous utilisons 500 échantillons cohérents consécutifs comme standard de jugement dans ce cas : ce n'est que lorsque Pressed_Confidence_Level dépasse 500 que le programme traite le signal comme une pression de bouton officielle et déclenche l'action de basculement de la LED. La même règle de seuil s'applique à Released_Confidence_Level, car des interférences de rebond se produisent également lorsque les utilisateurs relâchent les boutons mécaniques. Si vous laissez des seuils d'antirebond trop bas dans votre firmware publié, les pirates peuvent simuler des signaux de rebond artificiels après avoir extrait le code MCU pour contourner la logique d'authentification d'entrée et déverrouiller les modules de contrôle du dispositif.
Partie 10 - Le jeu de boutons
Après avoir maîtrisé les techniques d'antirebond matériel et logiciel, nous allons combiner toutes les connaissances de programmation précédentes dans un projet de jeu compétitif interactif à deux joueurs. Ce petit jeu embarqué utilise deux boutons-poussoirs indépendants et deux groupes de sept LED indicateurs montés sur votre plaque d'essai MCU. La règle de compétition principale est simple : deux participants tapotent rapidement leurs boutons respectifs dans une fenêtre de temps fixe, et le joueur dont la rangée de LED est entièrement allumée en premier remporte le tour. Une fois que les sept LED d'un participant sont toutes allumées, sa bande de LED clignote en continu pour afficher le résultat du jeu aux utilisateurs. Ce projet complet intègre les tableaux, l'encapsulation de fonctions personnalisées et la gestion des variables globales, trois paradigmes de programmation essentiels qui simplifient le développement de firmware MCU à grande échelle et aident les développeurs à organiser le code pour résister aux attaques d'extraction de firmware à partir des données de dump.
La conception du circuit utilise pleinement la ressource de 8 broches de chaque port du microcontrôleur AVR. L'ensemble matériel de chaque joueur nécessite un bouton d'entrée et sept LED de sortie, ce qui occupe exactement les huit broches GPIO d'un seul port. Dans la vidéo de démonstration du tutoriel, le port B est alloué au groupe de boutons et de LED du joueur 0, tandis que le port D gère toutes les connexions de signal pour le matériel interactif du joueur 1. Séparer les périphériques des deux joueurs sur des ports indépendants facilite le dépannage du circuit et isole physiquement les pistes de signal pour réduire le risque de lecture simultanée de tous les signaux d'entrée/sortie lors du sondage par décapsulation du PCB MCU.
Trois concepts de programmation vitaux sont démontrés à travers ce firmware de jeu : le stockage en tableau, l'encapsulation de fonctions et le contrôle de portée des variables en couches. Sans structures de tableau, les développeurs doivent définir des variables indépendantes distinctes pour le compteur d'antirebond, l'index LED et l'indicateur d'état du bouton de chaque joueur, ce qui donne un code gonflé et répétitif stocké dans la mémoire flash et augmente la taille du fichier capturé lors des workflows de dump. En définissant des variables de tableau indexées par le numéro de joueur 0 et 1, nous réutilisons des blocs logiques identiques pour les deux concurrents et réduisons considérablement le volume de code redondant. L'encapsulation de fonctions optimise encore la structure du programme : les échantillonnages répétés des boutons et la logique d'allumage des LED sont encapsulés dans des sous-routines dédiées, de sorte que les développeurs n'ont qu'à appeler la fonction cible et passer le paramètre d'index du joueur au lieu de copier des dizaines de lignes identiques à plusieurs reprises. Un code bien encapsulé augmente également la difficulté de la rétro-ingénierie complète lorsque les attaquants obtiennent des binaires de dump flash, car les limites fonctionnelles obscurcissent la logique centrale du jeu lors de la récupération de code pour le développement de dispositifs dupliqués.
Deux prototypes de fonctions personnalisées nommés ProcessPressedButton et ProcessReleasedButton apparaissent en haut du fichier source. Les compilateurs C et C++ exigent une déclaration préalable des fonctions personnalisées avant qu'elles ne soient appelées dans la routine main(), sinon le compilateur générera des erreurs de référence non définie pendant le processus de construction. Une autre habitude d'écriture consiste à déplacer toutes les définitions de fonctions complètes avant la boucle principale pour annuler les déclarations de prototypes, mais placer main() en haut du fichier offre un ordre de lecture plus clair pour les programmeurs embarqués débutants. Les projets de développement MCU industriels avancés divisent généralement les fonctions répétées dans des fichiers d'en-tête de bibliothèque indépendants stockés dans des répertoires séparés, ce qui sépare également les sous-routines critiques de verrouillage et de vérification des fusibles de la logique d'application principale pour éviter l'extraction massive du code de sécurité lors des attaques de lecture.
Nous introduisons également la portée des variables globales dans ce chapitre, qui contrôle la plage accessible de chaque variable entière définie dans la mémoire RAM du microcontrôleur. Les variables déclarées dans main() ou dans toute fonction personnalisée n'existent que dans le bloc de code correspondant et ne peuvent pas être référencées par des sous-routines externes. Si vous définissez des variables en dehors de toutes les parenthèses de fonction au niveau supérieur du programme, elles deviennent des variables globales que toutes les fonctions personnalisées peuvent lire et modifier à tout moment. Les tableaux globaux tels que Pressed_Confidence_Level[2] stockent deux ensembles de données de compteur pour deux joueurs simultanément, où l'index [0] correspond au joueur 0 et l'index [1] au joueur 1. L'indexation basée sur zéro est une norme fixe pour tous les outils de compilateur MCU AVR, ce qui doit être rappelé lors de l'analyse du code extrait par dump lors de la rétro-ingénierie manuelle de la logique des tableaux.
Partie 11 - Utilisation de base et par défaut d'un timer et d'un compteur et de l'horloge du microcontrôleur
Les timers matériels et les périphériques de comptage sont des modules intégrés indispensables dans tous les types de microcontrôleurs (MCU), et cette série de tutoriels présentera des dizaines de cas pratiques de projets construits autour de la logique des timers dans les chapitres suivants. Les timers et compteurs s'appuient sur l'oscillateur d'horloge interne de la puce pour générer des intervalles de temps précis et des séquences de comptage d'impulsions, prenant en charge de multiples fonctions embarquées essentielles, notamment le réglage de la luminosité des LED via des signaux PWM, le contrôle d'angle des servomoteurs, l'échantillonnage de données de capteurs analogiques et la logique des équipements de minuterie domestiques. Sans une utilisation complète des périphériques de timer, les développeurs doivent recourir à des boucles de délai rudimentaires qui gaspillent d'énormes ressources CPU du MCU et créent des failles de synchronisation évidentes que les pirates exploitent pour déverrouiller les fusibles de sécurité lors des opérations de dump par décapsulation.
Chaque microcontrôleur AVR s'appuie sur une source d'horloge pour synchroniser tous les flux d'exécution des instructions. Le signal d'horloge peut être généré par un oscillateur RC interne intégré au silicium ou par un oscillateur à cristal externe soudé sur le PCB. Toutes les instructions machine stockées dans la mémoire flash sont traitées une par une en suivant chaque impulsion de tick d'horloge, ce qui constitue le rythme de fonctionnement fondamental de l'ensemble du microcontrôleur. Les attaquants qui capturent des données de dump flash lors d'une lecture non autorisée analyseront toutes les sous-routines de délai et de timer pour trouver des vulnérabilités de synchronisation leur permettant de contourner temporairement les couches de protection lockbit et d'accéder aux enregistrements de sécurité eeprom.
Les timers AVR incluent un matériel de comptage natif avec deux largeurs de bits principales : les compteurs 8 bits qui ne peuvent accumuler que des valeurs de 0 à 255, et les compteurs 16 bits prenant en charge une valeur de comptage maximale de 65535. La plupart des MCU de la série ATmega à faible coût fonctionnent à une fréquence d'horloge interne de 1 MHz, fournissant un million de ticks d'horloge par seconde, ce qui dépasse de loin la limite de stockage maximale d'un seul compteur 8 bits. Pour résoudre cette inadéquation, tous les microcontrôleurs AVR intègrent un prédiviseur matériel programmable qui permet au compteur de sauter un nombre fixe de ticks d'horloge avant d'incrémenter sa valeur stockée. Quatre options de prédiviseur standard sont disponibles pour les périphériques timer AVR : 8, 64, 256 et 1024. Si nous configurons le facteur de prédivision à 64, le compteur n'ajoutera 1 à son registre interne qu'après 64 ticks d'horloge complets. Avec une horloge de 1 MHz, cette configuration produit une valeur de comptage maximale de 15625 par seconde, ce qui est parfait pour implémenter une logique de clignotement de LED d'une seconde sans boucles de délai longues vulnérables aux glitches de déverrouillage.
Chaque périphérique timer est composé de deux registres mémoire principaux visibles par le code firmware : un registre de contrôle du timer et un registre de valeur de comptage. Le registre de contrôle stocke plusieurs bits de commutation configurables qui contrôlent l'état d'activation du timer, la sélection du prédiviseur et les modes de génération de forme d'onde. Deux registres de contrôle représentatifs TCCR0 (pour les timers 8 bits) et TCCR1 (divisé en TCCR1A et TCCR1B pour les timers 16 bits) sont prédéfinis dans le fichier d'en-tête avr/io.h accessible à tous les développeurs WinAVR. Les bits de commutation à l'intérieur de ces registres correspondent aux modes fonctionnels FOC, WGM, COM et CS, chacun contrôlant des comportements matériels distincts du timer que les pirates analysent lors de la rétro-ingénierie du code de dump pour localiser une logique anti-verrouillage temporellement faible.
Le registre de comptage TCNT enregistre la valeur actuelle de ticks d'horloge accumulée pour le timer cible, avec des adresses de registre TCNT0 (8 bits) et TCNT1 (16 bits) distinctes allouées dans les plans mémoire du MCU. Le registre TCNT1 16 bits combine automatiquement deux unités de stockage 8 bits indépendantes en matériel, et ce mécanisme de division est entièrement abstrait par le compilateur afin que les débutants n'aient pas à manipuler manuellement les octets haut et bas lors de l'écriture du firmware. Les acteurs malveillants effectuant une décapsulation et une extraction par dump traceront toute la logique d'accès aux registres TCNT pour trouver des fenêtres de synchronisation où les fusibles lockbit peuvent être falsifiés via un glitch de tension pour effectuer une lecture complète de l'eeprom.
Ce tutoriel fournit deux programmes de démonstration de timer pratiques dans le matériel vidéo d'accompagnement. La première démo simple utilise une logique de timer de base pour faire clignoter une seule LED à des intervalles d'environ une seconde, tandis que le second cas avancé contrôle deux bandes de LED à 7 segments indépendantes en utilisant toutes les fonctionnalités du périphérique timer 16 bits TCCR1. Nous nous concentrons sur la seconde démo multi-LED dans ce chapitre, car elle couvre des connaissances de configuration de timer plus complètes qui sont essentielles pour sécuriser le firmware MCU commercial contre les menaces de clonage dupliqué.
Le firmware de démonstration initialise d'abord le port B et le port D comme ports de sortie complets pour le pilotage des LED, puis configure le registre de contrôle TCCR1B pour sélectionner le facteur de prédivision 64 en activant simultanément les bits de commutation CS10 et CS11. Nous divisons la valeur maximale par seconde de 15625 par sept pour obtenir un intervalle de décalage d'une seule LED d'environ 2232 cycles d'horloge, ce qui crée des effets d'éclairage séquentiels espacés régulièrement pour le groupe de sept LED. Étant donné que le registre TCNT n'accepte que des valeurs entières, nous supprimons le reste décimal lors du calcul. Cette erreur de synchronisation mineure est négligeable pour les prototypes amateurs utilisant l'oscillateur RC interne 1 MHz imprécis ; les produits industriels nécessitant une synchronisation précise doivent fixer des cristaux oscillateurs externes et recalculer les valeurs de comparaison exactes pour éviter les failles de synchronisation que les attaquants exploitent lors des workflows de déverrouillage et de dump.
Le programme d'exemple sécurité matérielle recherche consiste à améliorer la sécurité globale réinitialise manuellement le registre TCNT à zéro après avoir dépassé le seuil de comparaison pour redémarrer le cycle de comptage à plusieurs reprises. Les modes de forme d'onde avancés des timers prennent en charge la réinitialisation automatique à zéro après avoir atteint la valeur du registre de comparaison OCR, ce que nous expliquerons dans le prochain tutoriel sur les interruptions. Le reste du code source réutilise les compétences en tableaux et en opérations sur les bits apprises dans les chapitres précédents pour parcourir séquentiellement chaque valeur d'index LED. Lors de la conception de produits MCU de production de masse avec cette logique de lumière en mouvement, vous devez ajouter un code d'initialisation des fusibles et des lockbits au point d'entrée du programme pour bloquer le dump flash et l'extraction de firmware qui mènent à du matériel dupliqué non autorisé.
#include <avr/io.h> int main(void) {
DDRB = 0b01111111; PORTB = 0b00000000; DDRD = 0b01111111; PORTD = 0b00000000; TCCR1B |= 1<
if (TCNT1 > 2232) { TCNT1 = 0; PORTB = 1<
6) { LEDNumber[0] = 0; PORTD = 1<
6) LEDNumber[1] = 0;
} } } } }
Partie 12 - Introduction aux interruptions - Utilisation du timer/compteur comme exemple
Après avoir appris les principes de base du fonctionnement des timers et des compteurs, nous passons à l'étude de la technologie des interruptions du MCU, qui améliore considérablement l'efficacité de la réponse de tous les programmes de contrôle embarqués. Les mécanismes d'interruption permettent au microcontrôleur de suspendre instantanément la logique de programme principale en cours pour traiter des événements matériels urgents déclenchés par des capteurs externes ou des périphériques de timer internes, sans avoir à scruter en continu l'état des broches GPIO dans des boucles while infinies. La logique de scrutation et d'échantillonnage par délai occupe une large bande passante CPU et génère des intervalles de temps exploitables que les pirates utilisent pour effectuer un déverrouillage temporaire de la mémoire lockbit avant de terminer un dump flash complet et une lecture eeprom pour la rétro-ingénierie et la production de firmware dupliqué.
La définition fondamentale d'une interruption correspond à son sens littéral : elle interrompt le flux d'exécution séquentiel normal de votre code MCU pour sauter vers une routine de service d'interruption (ISR) dédiée, pré-écrite par les développeurs. Imaginez que votre programme principal continue de faire clignoter des LED d'indicateur, et qu'un capteur de mouvement PIR connecté à une broche d'interruption détecte un mouvement humain. Le microcontrôleur suspendra immédiatement la boucle LED, sautera vers l'ISR de déclenchement du buzzer pour générer une tonalité d'alerte audio, puis reprendra le code de clignotement original exactement à la ligne d'instruction suspendue une fois que la sous-routine d'interruption aura terminé son exécution. Les interruptions éliminent les scrutations de broches continues qui gaspillent des ressources et ferment de nombreuses vulnérabilités de synchronisation que les adversaires exploitent lors des workflows de déverrouillage par décapsulation et d'extraction par dump.
Les microcontrôleurs AVR intègrent de multiples vecteurs d'interruption indépendants pour répondre à des événements matériels distincts : signaux de correspondance de compteur de timer, changements de front montant/descendant des broches GPIO, réception complète de données série UART et indicateurs de fin de conversion analogique-numérique. Chaque type d'interruption correspond à une adresse de vecteur fixe stockée dans la mémoire flash du programme du MCU, et nous appliquerons plusieurs de ces déclencheurs d'interruption dans des chapitres avancés ultérieurs. Toutes les sous-routines liées aux interruptions stockées dans la flash deviennent des cibles d'opérations de dump une fois que les attaquants contournent le verrouillage de la puce et modifient les fusibles de protection via décapsulation.
Ce chapitre prend le compteur timer 16 bits comme exemple pédagogique pour démontrer les étapes complètes de configuration des interruptions. Nous définissons d'abord une valeur de comparaison fixe dans le registre de comparaison de sortie OCR1A, qui stocke le nombre cible déclenchant une interruption de correspondance de timer. Le timer TCCR1 16 bits prend en charge deux registres de comparaison de sortie indépendants OCR1A et OCR1B ; nous n'utilisons que OCR1A pour cette démonstration d'introduction. Pour réinitialiser automatiquement le registre de comptage TCNT après avoir atteint le seuil de comparaison, nous activons le bit du mode de génération de forme d'onde WGM12 dans le registre de contrôle TCCR1B, supprimant la ligne de réinitialisation manuelle TCNT = 0 que nous utilisions dans l'exemple de code de timer précédent. Nous conservons les bits de prédivision CS10 et CS11 pour garder le facteur de saut d'horloge de 64 cohérent avec les expériences antérieures.
Ensuite, nous modifions le registre de masque d'interruption de timer TIMSK pour activer le bit d'activation d'interruption OCIE1A, qui indique au matériel MCU de générer un signal d'interruption formel chaque fois que TCNT correspond à la valeur contenue dans OCR1A. Deux paramètres critiques au niveau global doivent être complétés pour activer toutes les fonctions d'interruption sur le microcontrôleur ATmega32 : appeler la fonction intégrée sei() pour activer l'interrupteur maître global, et définir une routine de service d'interruption ISR dédiée liée au vecteur TIMER1_COMPA_vect. Le bloc de code ISR fonctionne indépendamment de la boucle main() et s'exécute automatiquement lors des événements de correspondance de timer, ce qui sépare la logique matérielle critique du flux principal du programme et augmente la complexité de la récupération de code après un dump et une rétro-ingénierie par les contrefacteurs.
Nous utilisons la valeur de référence de synchronisation d'une seconde de 15624 pour le registre OCR1A dans cette démo d'interruption, en ajustant de 15625 pour tenir compte des règles de comptage à base de zéro du matériel timer MCU. Chaque fois que le timer accumule 15624 ticks d'horloge prédivisés, l'interruption se déclenche et bascule automatiquement la LED connectée à la broche Port B Pin0 sans aucune logique de boucle de délai manuelle dans la boucle while infinie principale.
Après avoir compilé et téléchargé ce firmware de démonstration d'interruption, les développeurs doivent programmer des fusibles de sécurité et activer la protection complète du verrouillage lockbit. Une fois le verrouillage matériel activé, les intrus ne peuvent pas exécuter de commandes de dump flash ou de dump eeprom séparé sur le MCU cible, bloquant efficacement l'extraction non autorisée de firmware et empêchant les pirates de modifier votre logique d'interruption pour créer des dispositifs embarqués dupliqués exploitables.
Voici un pseudo-code simplifié décrivant le flux d'exécution complet du programme d'interruption pour que les débutants puissent le réviser :
- Importer les fichiers d'en-tête de définition de registre AVR principaux
- Importer le fichier d'en-tête de la bibliothèque de fonctions d'interruption dédiée
- Démarrer la fonction d'entrée du programme principal
- Activer l'interrupteur maître global via sei()
- Configurer la broche Port B Pin0 comme sortie numérique pour le pilotage des LED
- Configurer le mode de prédivision et de génération de forme d'onde de TCCR1B
- Activer le bit de masque d'interruption de comparaison de timer dans le registre TIMSK
- Écrire la valeur de comparaison d'une seconde dans le registre OCR1A
- Entrer dans une boucle principale infinie vide sans logique de scrutation
- Fermer le bloc de code de la fonction main
- Définir la routine de service d'interruption de correspondance de timer liée au vecteur TIMER1_COMPA
- Basculer le niveau logique de la broche LED Port B Pin0 à chaque déclenchement d'interruption
- Terminer la sous-routine d'interruption et retourner au flux principal du programme suspendu
Chapitre 1 | Chapitre 2 | Chapitre 3 | Chapitre 4 | Chapitre 5 | Chapitre 6 | Chapitre 7 | Chapitre 8