Bibliothèque de générateur de code assembleur pour microcontrôleurs AVR. Partie 4

← Partie 3. Adressage indirect et contrôle de flux
Partie 5. Conception d'applications multi-thread.


Bibliothèque de générateur de code d'assembleur pour microcontrôleurs AVR


Partie 4. Programmation des périphériques et gestion des interruptions


Dans cette partie de l'article, nous allons, comme promis, traiter de l'un des aspects les plus populaires de la programmation des microcontrôleurs - à savoir, travailler avec des périphériques. Il existe deux approches les plus courantes de la programmation périphérique. Premièrement, le système de programmation ne sait rien des périphériques et ne fournit que des moyens d'accès aux ports de contrôle des périphériques. Cette approche n'est pratiquement pas différente de l'utilisation de périphériques au niveau de l'assembleur et nécessite une étude approfondie de l'objectif de tous les ports associés au fonctionnement d'un périphérique spécifique. Pour faciliter le travail des programmeurs, il existe des programmes spéciaux, mais leur aide, en règle générale, se termine par la génération d'une séquence d'initialisation initiale du périphérique. L'avantage de cette approche est un accès complet à toutes les capacités périphériques, et l'inconvénient est la complexité de la programmation et la grande quantité de code de programme.


La seconde - le travail avec les périphériques s'effectue au niveau des périphériques virtuels. Le principal avantage de cette approche est la simplicité de la gestion des périphériques et la possibilité de travailler avec eux sans plonger dans l'implémentation matérielle particulière. L'inconvénient de cette approche est la limitation des capacités des périphériques par le but et les fonctions du périphérique virtuel émulé.


La bibliothèque NanoRTOS implémente une troisième approche. Chaque périphérique est décrit par une classe spécialisée, dont le but est de simplifier la configuration et le fonctionnement du périphérique tout en conservant toutes ses fonctionnalités. Il est préférable de démontrer les caractéristiques de cette approche à l'aide d'exemples, alors commençons.


Commençons par le périphérique le plus simple et le plus courant - le port d'entrée / sortie numérique. Ce port combine jusqu'à 8 canaux, chacun pouvant être configuré indépendamment pour l'entrée ou la sortie. Une clarification à 8 signifie que l'architecture du contrôleur implique la possibilité d'attribuer des fonctions alternatives pour les bits de port individuels, ce qui exclut leur utilisation comme ports d'eau / de sortie, réduisant ainsi le nombre de bits disponibles. La configuration et les travaux ultérieurs peuvent être effectués à la fois au niveau d'un seul bit et au niveau du port dans son ensemble (écriture et lecture des 8 bits avec une seule commande). Le contrôleur Mega328 utilisé dans les exemples a 3 ports: B, C et D. Dans l'état initial, du point de vue de la bibliothèque, les décharges de tous les ports sont neutres. Cela signifie que pour leur activation, il est nécessaire d'indiquer le mode de leur utilisation. En cas de tentative d'accès à un port non activé, le programme générera une erreur de compilation. Ceci est fait afin d'éliminer les conflits possibles lors de l'attribution de fonctions alternatives. Pour basculer les ports en mode d'entrée / sortie, utilisez les commandes Mode pour définir le mode mono-bit et Direction pour définir le mode de tous les bits de port avec une seule commande. Du point de vue de la programmation, tous les ports sont identiques et leur comportement est décrit par une classe.


var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT;// 0   B    m.PortC.Direction(0xFF);//       m.PortB.Activate(); //   m.PortC.Activate(); //  C //   m.PortB[0].Set(); //  0   B  1 m.PortB[0].Clear();//  0   B  0 m.PortB[0].Toggle();//  0   B   m.PortC.Write(0b11000000);// 6  7        var rr = m.REG(); //     rr.Load(0xC0); m.PortC.Write(rr);//      rr var t = AVRASM.Text(m); 

L'exemple ci-dessus montre comment organiser la sortie des données via les ports. Ici, le travail avec le port B s'effectue au niveau d'une catégorie et avec le port C au niveau du port dans son ensemble. Faites attention à la commande d'activation Activate () . Son but est de générer dans le code de sortie une séquence de commandes d'initialisation de périphérique conformément aux propriétés précédemment définies. Ainsi, la commande Activate () utilise toujours l'ensemble de paramètres définis qui est en cours au moment de l'exécution. Prenons un exemple de lecture de données à partir d'un port.


  m.PortB.Activate(); //  B m.PortC.Activate(); //  C Bit dd = m.BIT(); //     Register rr = m.REG(); //     m.PortB[0].Read(dd); //  0   B m.PortC.Read(rr);//      rr var t = AVRASM.Text(m); 

Dans cet exemple, un nouveau type de données Bit est apparu. L'analogue le plus proche de ce type dans les langues de haut niveau est le type bool . Le type de données Bit est utilisé pour stocker un seul bit d'information et permet à sa valeur d'être utilisée comme condition dans les opérations de branchement. Afin d'économiser de la mémoire, les variables de bits pendant le stockage sont combinées en blocs de telle manière qu'un registre RON est utilisé pour stocker 8 variables de type Bit . En plus du type décrit, la bibliothèque contient deux autres types de données binaires: Pin , qui a la même fonctionnalité que Bit, mais utilise des registres IO et Mbit pour stocker les variables binaires dans la mémoire RAM. Voyons comment utiliser les variables de bits pour organiser les branches


 m.IF(m.PortB[0], () => AVRASM.Comment(",   = 1")); var b = m.BIT(); b.Set(); m.IF(b, () => AVRASM.Comment(",   b ")); 

La première ligne vérifie l'état du port d'entrée, et si à l'entrée 1, le code du bloc conditionnel est exécuté. La dernière ligne contient un exemple où une variable de type Bit est utilisée comme condition de branchement.


Le prochain périphérique commun et souvent utilisé peut être considéré comme un compteur / temporisateur matériel. Dans les microcontrôleurs AVR, cet appareil a un large éventail de fonctions et, selon le réglage, peut être utilisé pour générer un retard, générer un méandre avec une fréquence programmable, mesurer la fréquence d'un signal externe, et aussi comme modulateur PWM multimode. Contrairement aux ports d'E / S, chacun des temporisateurs Mega328 possède un ensemble unique de fonctionnalités. Par conséquent, chaque temporisateur est décrit par une classe distincte.


Examinons-les plus en détail. En tant que source de signal de chaque temporisateur, un signal externe et l'horloge interne du processeur peuvent être utilisés. Les paramètres matériels du microcontrôleur vous permettent de configurer l'utilisation de la fréquence complète pour les périphériques, ou d'activer le séparateur unique pour tous les périphériques par 8. Étant donné que le microcontrôleur permet un fonctionnement dans une large plage de fréquences, le calcul correct des valeurs du diviseur de minuterie pour le retard requis pendant le cadencement interne nécessite de spécifier la fréquence du processeur et le mode pré-échelle. Ainsi, la section des paramètres du minuteur prend la forme suivante


 var m = new Mega328(); m.FCLK = 16000000; //   m.CKDIV8 = false; //     //    Timer1 m.Timer1.Clock = eTimerClockSource.CLK256; //   m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); //    A m.Timer1.Mode = eWaveFormMode.CTC_OCRA; //    m.Timer1.Activate(); //    Timer1 

De toute évidence, le réglage de la minuterie nécessite d'étudier la documentation du fabricant pour sélectionner le mode correct et comprendre l'objectif de divers paramètres, mais l'utilisation de la bibliothèque rend le travail avec l'appareil plus facile et plus compréhensible, tout en conservant la possibilité d'utiliser tous les modes de l'appareil.


Maintenant, je suggère une petite distraction de la description de l'utilisation de périphériques spécifiques et avant de continuer, discuter du problème du fonctionnement asynchrone. Le principal avantage des périphériques est qu'ils sont capables d'exécuter certaines fonctions sans utiliser les ressources du processeur. La complexité peut survenir dans l'organisation de l'interaction entre le programme et le périphérique, car les événements qui se produisent pendant le fonctionnement du périphérique sont asynchrones par rapport au flux d'exécution de code dans la CPU. Les méthodes d'interaction synchrone, dans lesquelles le programme contient des cycles pour attendre l'état du périphérique souhaité, annulent presque tous les avantages de la périphérie en tant que périphériques indépendants. Le mode d'interruption est plus efficace et préféré. Dans ce mode, le processeur exécute en continu le code du thread principal et, lorsque l'événement se produit, bascule le thread d'exécution sur son gestionnaire. À la fin du traitement, le contrôle revient au thread principal. Les avantages de cette approche sont évidents, mais son utilisation peut être compliquée par la complexité de la configuration. En assembleur, pour utiliser une interruption, vous devez:


  • définir l'adresse correcte dans la table d'interruption,
  • configurer l'appareil lui-même pour fonctionner avec des interruptions,
  • Décrire la fonction de gestion des interruptions
  • prévoir la conservation de tous les registres et drapeaux utilisés afin que l'interruption n'affecte pas la progression du thread principal
  • activer les interruptions globales.

Pour simplifier la programmation du travail grâce aux interruptions, les classes de description des périphériques de la bibliothèque contiennent les propriétés d'un gestionnaire d'événements. Dans le même temps, pour organiser le travail avec un périphérique via des interruptions, il vous suffit de décrire le code de traitement de l'événement requis, et la bibliothèque effectuera toutes les autres configurations par elle-même. Revenons au réglage du temporisateur et complétons-le par la définition du code qui doit être exécuté lorsque les seuils de comparaison des canaux de comparaison du temporisateur sont atteints. Supposons que nous voulons que lorsque les seuils des canaux de comparaison sont déclenchés, certains bits des ports d'E / S soient réinitialisés en cas de débordement. En d'autres termes, nous voulons implémenter à l'aide d'un temporisateur la fonction de génération d'un signal PWM à des ports arbitraires sélectionnés avec un rapport cyclique déterminé par les valeurs OCRA pour le premier et OCRB pour le deuxième canal. Voyons à quoi ressemblera le code dans ce cas.


 var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; var bit2 = m.PortB[1]; bit2.Mode = ePinMode.OUT; m.PortB.Activate(); //  0  1   B   //     m.Timer0.Clock = eTimerClockSource.CLK; m.Timer0.OCRA = 50; m.Timer0.OCRB = 170; m.Timer0.Mode = eWaveFormMode.PWMPC_TOP8; //   m.Timer0.OnCompareA = () => bit1.Set(); m.Timer0.OnCompareB = () =>bit2.Set(); m.Timer0.OnOverflow = () => m.PortB.Write(0); m.Timer0.Activate(); m.EnableInterrupt(); //  //   m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); 

La partie concernant le réglage des modes de temporisation a été examinée plus tôt, alors passons tout de suite aux gestionnaires d'interruption. Dans l'exemple, trois gestionnaires sont utilisés pour implémenter deux canaux PWM à l'aide d'un seul temporisateur. Le code des gestionnaires est assez évident, mais la question peut se poser de savoir comment la sauvegarde d'état mentionnée précédemment est implémentée de sorte que l'appel d'interruption n'affecte pas la logique du thread principal. La solution, dans laquelle tous les registres et drapeaux sont enregistrés, semble clairement redondante, par conséquent, la bibliothèque analyse l'utilisation des ressources dans l'interruption et n'économise que le minimum nécessaire. La boucle principale vide confirme l'idée que la tâche de générer en continu plusieurs signaux PWM fonctionne sans la participation du programme principal.


Il convient de noter que la bibliothèque implémente une approche unifiée pour travailler avec les interruptions pour toutes les classes de description des périphériques. Cela simplifie la programmation et réduit les erreurs.


Nous allons continuer à étudier le travail avec les interruptions et considérer une situation dans laquelle le fait de cliquer sur les boutons attachés aux ports d'entrée devrait provoquer certaines actions de la part du programme. Dans le processeur que nous considérons, il existe deux façons de générer des interruptions lorsque l'état des ports d'entrée change. Le plus avancé est l'utilisation du mode d'interruption externe. Dans ce cas, nous pouvons générer des interruptions distinctes pour chacune des conclusions et configurer la réaction uniquement à un événement spécifique (front, récession, niveau). Malheureusement, il n'y en a que deux dans notre cristal. Une autre méthode vous permet de contrôler au moyen d'interruptions l'un des bits du port d'entrée, mais le traitement est plus compliqué en raison du fait que l'événement se produit au niveau du port lorsque le signal d'entrée de l'un des bits configurés change, et une clarification supplémentaire de la cause de l'interruption doit être effectuée au niveau de l'algorithme par le logiciel. .


A titre d'illustration, nous allons essayer de résoudre le problème du contrôle de l'état de la sortie du port à l'aide de deux boutons. L'un d'eux devrait définir la valeur du port indiqué par nous à 1, et l'autre réinitialiser. Puisqu'il n'y a que deux boutons, nous profiterons de l'occasion pour utiliser des interruptions externes.


  var m = new Mega328(); m.PortD[0].Mode = ePinMode.OUT; m.PortD.Write(0x0C); // pull-up   m.INT0.Mode = eExtIntMode.Falling; //  INT0  . m.INT0.OnChange = () => m.PortD[0].Set(); //      1 m.INT1.Mode = eExtIntMode.Falling; //  INT1  . m.INT1.OnChange = () => m.PortD[0].Clear(); //     //  m.INT0.Activate(); m.INT1.Activate(); m.PortD.Activate(); m.EnableInterrupt(); //   //  m.LOOP(m.TempL, (r, l) => m.GO(l), (r, l) => { }); 

L'utilisation d'interruptions externes nous a permis de résoudre notre problème aussi simplement et clairement que possible.


La gestion des ports externes par programmation n'est pas la seule façon possible. En particulier, les temporisateurs ont un réglage qui leur permet de contrôler directement la sortie du microcontrôleur. Pour ce faire, dans le réglage de la minuterie, vous devez spécifier le mode de contrôle de sortie


 m.Timer0.CompareModeA = eCompareMatchMode.Set; 

Après avoir activé le temporisateur, le 6e bit du port D recevra une fonction alternative et sera contrôlé par un temporisateur. Ainsi, nous sommes en mesure de générer un signal PWM à la sortie du processeur uniquement au niveau matériel, en utilisant un logiciel uniquement pour définir les paramètres du signal. En même temps, si nous essayons d'utiliser les outils de bibliothèque pour nous tourner vers le port occupé comme port d'entrée / sortie, nous obtiendrons une erreur au niveau de la compilation.


Le dernier appareil que nous examinerons dans cette partie de l'article sera le port série USART. La fonctionnalité de cet appareil est très large, mais jusqu'à présent, nous n'aborderons que l'un des cas d'utilisation les plus courants pour cet appareil.


Le cas d'utilisation le plus courant pour ce port est de connecter un terminal série aux informations textuelles d'entrée / sortie. La partie du code concernant les paramètres de port dans ce cas peut ressembler à ceci


 m.FCLK = 16000000; //   m.CKDIV8 = false; //     m.Usart.Mode = eUartMode.UART; //    UART m.Usart.Baudrate = 9600; //   9600  m.Usart.FrameFormat = eUartFrame.U8N1; //   8N1 

Les paramètres spécifiés coïncident avec les paramètres par défaut de l'USART dans la bibliothèque, par conséquent, ils peuvent être partiellement ou complètement ignorés dans le texte du programme.


Prenons un petit exemple dans lequel nous envoyons du texte statique au terminal. Afin de ne pas gonfler le code, nous nous limitons à la sortie vers le terminal du classique "Hello world!" au début du programme.


  var m = new Mega328(); var ptr = m.ROMPTR(); //      m.CKDIV8 = false; m.FCLK = 16000000; //      m.Usart.Mode = eUartMode.UART; m.Usart.Baudrate = 9600; m.Usart.FrameFormat = eUartFrame.U8N1; //         m.Usart.OnTransmitComplete = () => { ptr.MLoadInc(m.TempL); m.IF(m.TempL!=0,()=>m.Usart.Transmit(m.TempL)); }; m.Usart.Activate(); m.EnableInterrupt(); //   var str = Const.String("Hello world!"); //   ptr.Load(str); //     ptr.MloadInc(m.TempL); //    m.Usart.Transmit(m.TempL); //   . m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { }); 

Dans ce programme, à partir du nouveau, déclarant la chaîne constante str . La bibliothèque place toutes les variables constantes dans la mémoire du programme, par conséquent, pour travailler avec elles, vous devez utiliser le pointeur ROMPtr . La sortie de données vers le terminal commence par la sortie du premier caractère de la séquence de chaînes, après quoi la commande passe immédiatement à la boucle principale, sans attendre la fin de la sortie. L'achèvement du processus de transfert d'octets provoque une interruption dans le gestionnaire dont le caractère suivant de la ligne est lu. Si le caractère n'est pas égal à 0 (la bibliothèque utilise le format terminé par zéro pour stocker les chaînes), ce caractère est envoyé au port d'interface série. Si nous atteignons la fin de la ligne, le caractère n'est pas envoyé au port et le cycle d'envoi se termine.


L'inconvénient de cette approche est l'algorithme de traitement d'interruption fixe. Il ne permettra pas d'utiliser le port série autrement que pour sortir des chaînes statiques. Un autre inconvénient de cette mise en œuvre est l'absence de mécanisme de surveillance de l'occupation des ports. Si vous essayez d'envoyer plusieurs lignes séquentiellement, il peut y avoir une situation où la transmission des lignes précédentes sera interrompue ou les lignes seront mélangées.


Nous verrons dans la prochaine partie de l'article des méthodes plus efficaces pour résoudre ce problème et d'autres, ainsi que travailler avec d'autres périphériques. Dans ce document, nous examinerons de plus près la programmation à l'aide de la classe spéciale de gestion des tâches parallèles .

Source: https://habr.com/ru/post/fr463627/


All Articles