Mesure de la modulation de largeur d'impulsion (PWM) avec un microcontrôleur PIC
Bien que la programmation d'un Arduino soit plus simple que celle d'un PIC de Microchip Technology, ce dernier s'est avéré plus précis et plus exact dans le calcul du temps d'un signal de modulation de largeur d'impulsion (PWM), en raison de son architecture. Les défis à relever sont certainement plus difficiles, mais ils enrichiront finalement l'expérience de ceux qui aiment coder. L'instrument proposé peut mesurer simultanément les durées d'impulsion haute et basse ainsi que la période, le tout avec une résolution de 1 µs pour des durées d'impulsion allant de 10 µs à 65 535 µs pour des signaux PWM avec des fréquences allant de 7,63 Hz à 50 kHz.
La technique de modulation de largeur d'impulsion, communément désignée par l'acronyme PWM (Pulse Width Modulation), consiste à ajuster la durée des impulsions au cours du temps. Initialement développée pour les télécommunications, cette méthode est également employée dans les conversions numérique-analogique pour moduler la tension ou le courant moyen. Cette valeur est déterminée par le rapport cyclique, qui est le ratio entre la durée de l'impulsion positive et la période totale. La modulation PWM est couramment utilisée pour réguler la tension dans les alimentations à découpage, pour la commande de moteurs et les servomoteurs et même pour gérer la sortie de certains capteurs.De ce fait, la plupart des microcontrôleurs modernes disposent d'une ou de plusieurs sorties PWM. Certains transducteurs, tels que, les capteurs de distance acoustiques, ont une sortie d'impulsion numérique dont la durée est proportionnelle à l'amplitude du signal. Le but de ce projet n'est pas de générer des signaux PWM, mais de mesurer leur durée d'impulsion avec précision.
Dans cet article, je présenterai une version du projet qui utilise le PIC16F628 pour mesurer les durées des phases haute et basse des impulsions, ainsi que la période, comme illustré dans la photo d'introduction. Pour ceux désirant également calculer le rapport cyclique en pourcentage, il sera nécessaire d'utiliser un PIC16F648, car ce projet requiert une opération de division. Le programme ne nécessite que quelques instructions supplémentaires, mais le code dépasse 2 K mots, et le F628 n'est doté que de 2 K de mémoire flash. Les codes sources et les fichiers HEX compilés sont disponibles pour les deux versions . Les autres composants du projet restent identiques
Le microcontrôleur PIC
Pourquoi ce choix ? Dans mon cas, j'avais déjà réalisé un projet similaire il y a quelques années, à une époque où je privilégiais l'utilisation des PIC pour mes réalisations, avant de passer au développement avec l'EDI Arduino et MicroPython. Pour les microcontrôleurs PIC, la fréquence de l'horloge interne correspond à un quart de la fréquence de l'oscillateur à quartz. Ainsi, toutes les instructions sont exécutées en un seul cycle d'horloge, à l'exception des branches de programme. Avec un quartz de 16 MHz, l'horloge interne fonctionne à 4 MHz, le temps d'exécution des instructions n'est donc que de 0,25 µs.
Le PIC16F628 est un microcontrôleur de Microchip à architecture RISC (Reduced Instruction Set Computer) traitant des données sur 8 bits, mais ses 35 instructions sont codées chacune sur un mot de 14 bits. Pour ce modèle, la mémoire flash est de 2K mots. Parmi ses nombreuses fonctions d'entrée/sortie, ce PIC intègre une fonction de capture d'événements sur la broche CCP1 (RB3), où il enregistre le contenu du Timer1 dans deux registres, CCPR1L et CCPR1H. Une fonction similaire existe également pour l'ATmega328P de l'Arduino UNO, mais le choix d'un PIC permet de réaliser un système beaucoup plus simple et compact, avec une consommation d'énergie nettement réduite.
Bien entendu, ceux qui sont habitués à travailler avec Arduino auront plus de difficultés à concevoir des applications avec des PIC. Il n'est pas aussi facile de trouver des cartes préfabriquées ou de bénéficier de l'abondance de bibliothèques disponibles pour Arduino, qui facilitent la gestion des composants utilisés. À mon avis, se limiter à Arduino n'offre pas la possibilité d'explorer en profondeur les principes sous-jacents, ni même de comprendre précisément ce que l'on fait.
De nombreux utilisateurs d'Arduino se contentent de reproduire les connexions de projets, souvent illustrées à l'aide de Fritzing et des images montrant les composants disposés sur des plaques à essai, sans comprendre le fonctionnement sous-jacent de ces montages. Parfois, ces projets ne fonctionnent pas du tout, en raison d'erreurs de connexion.
Le compilateur utilisé pour les PIC est moins convivial que l'EDI Arduino et les bibliothèques disponibles sont beaucoup moins nombreuses. Par exemple, il manque une fonction équivalente à pulsein() pour mesurer la durée d'une impulsion. Pour ces raisons, le concepteur doit acquérir une compréhension plus approfondie du fonctionnement des microcontrôleurs. C'est ce que j'ai dû faire pour ce projet, où l'étude approfondie de la fiche technique était nécessaire pour maîtriser le fonctionnement des timers et de la capture CCP.
Le compilateur mikroPascal, que j’ai utilisé pour développer des programmes pour les PIC, est gratuit tant que la taille du code ne dépasse pas 2K mots (1 mot = 14 bits), ce qui correspond à la capacité de la mémoire flash du PIC utilisé, alors que la RAM disponible est seulement de 224 octets. Avec de telles contraintes, les possibilités sont limitées : par exemple, l’utilisation de divisions en virgule flottante peut rapidement saturer la mémoire disponible. Cependant, la compilation avec mikroPascal est nettement plus rapide que celle réalisée avec Arduino, et le code généré est souvent plus compact et optimisé.
J'ai développé le programme en Pascal en utilisant un compilateur croisé performant pour les PC sous Windows : le mikroPascal PRO pour PIC, en version 7.6.0. Une version complète peut être téléchargée depuis le site de MIKROE. Dans cette première version du programme, le code est relativement court, ce qui facilite sa compilation. Ceux qui sont plus familiers avec le langage C ou Basic peuvent télécharger les compilateurs correspondants et adapter le programme à ces langages. Si aucune modification n'est nécessaire, il n'est même pas nécessaire de télécharger un compilateur ; il suffit de disposer d'un programmateur PIC utilisant le fichier hex déjà compilé, PWMmeter.hex.
J'ai également développé une autre version du programme qui calcule le rapport cyclique. Cependant, en raison de l'utilisation de la division en virgule flottante, cette version requiert davantage de mémoire flash et un PIC16F648. Dans ce cas, si vous n'avez pas acquis la licence du compilateur, vous devrez programmer la puce en utilisant le fichier HEX fourni avec ce projet. Ces deux microcontrôleurs disposent du même brochage.
Mesure de la durée de l'impulsion
Certains microcontrôleurs dotés d'un interpréteur BASIC intégré, tels que Stamp, Picaxe ou BasicX, utilisent une routine spécifique (PULSIN) pour mesurer la durée des impulsions, mais ne peuvent pas mesurer les courtes impulsions à cause de leur lenteur relative. Les cartes Arduino utilisent la fonction pulsein(pin, level, timeout) pour mesurer la durée d'une impulsion, niveau haut ou bas, avec une résolution de l'ordre de la microseconde. Pour des durées plus longues, il existe la fonction pulseInLong(), qui utilise l'interruption et mesure des durées de 10 µs à 3 minutes. Les deux fonctions renvoient un unsigned long.
Le programme utilise deux interruptions : Timer0 est utilisé pour le temps d'échantillonnage tandis que la seconde source d'interruption est déclenchée par les événements de événements de capture CCP (capture/compare/PWM) sur les fronts montants et descendants de l'impulsion. Les durées correspondantes sont enregistrées dans le Timer1.
Timing avec Timer0
Le Timer0 génère une interruption qui sert à échantillonner les mesures et à les afficher sur un écran LCD tous les 50 événements, ce qui équivaut à environ 0,5 seconde. Le Timer0 est un compteur TMR0 de 8 bits, précédé d'un diviseur programmable de 8 bits appelé prescaler.
Le programme configure l'horloge interne à une fréquence qui est le quart de celle de l'oscillateur à quartz : 16 MHz /4 = 4 MHz. En ajustant le prescaler à sa valeur maximale (1:256), Timer0 reçoit une fréquence d'entrée de f1 = 4 MHz / 256 = 15. 625 kHz, avec une période de 64 µs ; charger 100 dans le compteur résulte en une interruption toutes les 156 (256-100) impulsions de fréquence f1, correspondant à une période de 156 × 64 = 9984 µs (environ 100 Hz).
L'interruption du Timer0 est générée lorsque le timer/compteur du registre TMR0 passe de 0xFF à 0x00, ce qui définit le bit T0IF. Cette interruption génère l'horloge utilisée pour échantillonner les mesures et les afficher sur l'écran LCD, tous les 50 événements, ce qui correspond à environ 0,5 s.
Capture d'événements avec Timer1
Lorsque le front montant est détecté sur la broche RB3, le programme bascule le mode de capture pour détecter ensuite le front descendant, et inversement. Les séquences d'impulsions peuvent alterner entre bas-haut-bas, que nous nommons LoHiLo, ou l'inverse, haut-bas-haut, dénommé HiLoHi, comme illustré dans la figure 1. Le type d'impulsion est sélectionné par Dip1 : OFF = LoHiLo, ON = HiLoHi, et sa configuration est lue par le programme au démarrage.
Pour les impulsions LoHiLo, le programme définit le registre CCP1CON := $05 avec capture sur front montant. Lorsque cet événement se produit, la routine d'interruption enregistre le contenu des registres CCPR1L et CCPR1H qui sera ensuite transféré à t1. Ensuite, le CCP1CON := $04, réglant la capture sur front descendant, à l'événement duquel les registres CCPR1L et CCPR1H sera lue à nouveau, ce qui sera ensuite transféré à t2, comme le montre la figure 1. Le temps de pulsew est est calculé à partir de la différence t2-t1.
Pour les impulsions HiLoHi, le principe est le même, mais on commence par le front descendant (t2) et on termine par le front montant (t1), dans ce cas le temps de pulsew est calculé par la différence t1-t2. Timer1 intègre un compteur de 16 bits, précédé d'un prescaler de 2 bits.
L'horloge interne de 4 MHz est également utilisée ici. Avec le prescaler = 1:1 on atteint une résolution de 0,25 µs, ce qui peut être considéré comme excessif pour un quartz ordinaire. Nous avons donc opté pour un rapport de 1:4 avec une résolution de 1 µs. Le système mesure donc des impulsions d'une durée comprise entre environ 10 µs et 65 535 µs. En dessous de 10 µs, cela ne fonctionne pas, car il y a plusieurs instructions dans la routine d'interruption, qui requièrent un temps d'exécution non négligeable. Pour mesurer des durées plus longues, avec une résolution de 2 µs, vous devriez utiliser un prescaler de 1:8. Dans ce cas, il faudra multiplier les durées par deux après les avoir converties en nombres entiers longs.
La version améliorée du programme initial permet désormais de mesurer simultanément deux durées, facilitant ainsi le calcul de la période PWM ainsi que du rapport cyclique. Dans ce cas, le programme sélectionne la capture pour le front montant, stocke le temps dans t1, met le flag firstcre à l'état haut pour distinguer ce front du dernier. Il configure ensuite la capture pour le front descendant, enregistre le temps dans t2, puis définit la capture pour le front montant suivant, qui sera enregistré dans t3, comme illustré dans la figure 2. Lors du troisième front, la routine d'interruption définit le drapeau datok pour signaler que les temps t1, t2, et t3 ont été correctement mesurés.
Il calcule donc :
pulsewHi = t2−t1, pulsewLo = t3−t2, period = pulsewHi + pulsewLo
Le calcul du rapport cyclique nécessite des opérations avec des variables flottantes (réelles en Pascal) qui dépassent les limites de la mémoire flash du 16F628, mais nous pouvons les effectuer avec une calculatrice :
Period = T = pulsewHi + pulsewLo
Duty cycle: D = pulsewHi / T × 100 [%]
Comparaison avec la fonction pulseIn() d'Arduino
Pour effectuer une comparaison, j'ai également développé un petit programme pour l'Arduino Uno, spécifiquement pour une impulsion LoHiLo :
// program pulseintest
#define pulsein 4
void setup() {
Serial.begin(115200);
pinMode(pulsein, INPUT);
}
void loop() {
unsigned long duration = pulseIn(pulsein, HIGH);
Serial.print("Pulse High [us] = ");
Serial.println(duration);
delay(500);
}
Pour vérifier les durées, j'ai utilisé mon propre circuit basé sur un quartz avec un diviseur programmable et une sortie d'onde carrée comme référence ; les résultats obtenus sont comparés à ceux de mon système basé sur un PIC dans le tableau 1. Comme on peut le constater, le système PIC est très précis.
Schéma du circuit
La figure 3 illustre le schéma du circuit. L'utilisation d'un régulateur un LM7805 (un 78L05 fera aussi l'affaire), permet d'alimenter le système comme un Arduino UNO, c'est-à-dire avec une tension de 7 V (valeur optimale) à 12 V. Toutefois, il consomme moins de 10 mA sans rétroéclairage (W2 off), avec environ 3 mA consommés par le régulateur. Une alternative plus simple consiste à utiliser trois piles ordinaires de 1,5 V. Dans ce cas, l'élément le plus critique est l'écran LCD, qui nécessite une tension nominale de 5 V, bien que des appareils similaires fonctionnent aussi généralement sous 4,5 V.
Pour l'affichage, j'ai opté pour un écran LCD standard de deux lignes de 16 caractères, doté d'une interface parallèle compatible HD44780. Le cavalier W1 sert à prévenir les conflits d'alimentation et doit être retiré lors de la programmation de la puce avec un PICKIT. Le cavalier (ou interrupteur) W2 est utilisé pour désactiver le rétroéclairage afin d'économiser de l'énergie.
Prototype
La figure 4 montre la disposition des composants sur la carte perforée utilisée pour fabriquer le prototype. L'afficheur LCD a été retiré afin de révéler les composants montés en dessous.
En haut, un petit interrupteur (W2) permet d'éteindre le rétroéclairage de l'afficheur. Sur la gauche, un connecteur RCA est utilisé pour l'entrée PWM. En bas, un connecteur à 6 broches permet la programmation de la puce, compatible avec le programmateur en circuit PICkit. Si vous disposez d'un programmateur PIC équipé de sockets ZIF, ce connecteur n'est pas nécessaire, mais il vous faudra retirer le PIC de son support pour le programmer. À proximité, toujours en bas, se trouve le connecteur d'alimentation. Il est possible de réduire encore la taille de la carte, car celle-ci a été réutilisée d'un projet antérieur. Le connecteur femelle à 5 broches situé sur la droite n'est pas utilisé.
Programme
Nous avons décrit une grande partie du programme précédemment. Les sorties sur le LCD sont plus basiques que les fonctions de la bibliothèque LiquidCrystal d'Arduino, et les messages doivent être des chaînes ou des tableaux de caractères, il est donc nécessaire de convertir les variables en chaînes et de supprimer tout espace initial.
Le travail le plus important est effectué par la routine d'interruption. Dans la première partie, elle vérifie si l'interruption est causée par un débordement du Timer0, auquel cas INTCON, T0IF = 1, met le flag int0flg, qui est utilisé pour le timing à l'état haut. Elle recharge ensuite le nombre 100 dans le compteur de façon à déclencher une interruption après 156 impulsions et remet les drapeaux d'interruption INTCON et T0IF à 0.
La deuxième partie, plus complexe, est dédiée à la gestion des captures des fronts montants et descendants et à sauvegarder le contenu des deux registres de capture, CCPR1L et CCPR1H, dans les variables t1, t2 et t3, correspondant au premier front montant, au front descendant et au deuxième front montant. Lorsque ces trois valeurs ont été capturées, le flag datok est activé, ce qui sera utilisé par le programme pour calculer les durées et les périodes.
Les durées des fronts montants et descendant du signal PWM, indiquée par '_-_' pour le front montant, indiquée par '-_-' pour le front descendant sont affichés sur la première ligne. La période PWM, c.-à-d. la somme des deux durées, est afichée sur la deuxième ligne.
En l'absence de signal, l'écran n'affiche rien. Le code complet est présenté dans le listage 1. Ce programme occupe 1 236 mots de mémoire flash (65 %) et 74 octets de mémoire vive (37 %), et la compilation a été réalisée en 187 ms.
Version de programme avec affichage du rapport cyclique
Pour cette version, il est nécessaire d'utiliser un PIC16F648A, semblable au F628 mais disposant de 4K mots de mémoire flash. Comme précédemment mentionné, pour compiler cette version, l'acquisition d'une licence du programme est requise afin de contourner la limite des 2K mots. Toutefois, un fichier hexadécimal déjà compilé pour le PIC16F648A est inclus dans le package de cet article, vous permettant de le charger directement sur le PIC.
Le programme affiche pulsewHi [µs], pulsewLo [µs], period [µs] et dutyc [%] sur l'écran.
Malheureusement, l'utilisation de la division en virgule flottante a entraîné des problèmes de mémoire lors de la compilation, le PIC16F648A s'avérant inadapté pour de telles opérations. J'ai résolu ce problème en convertissant le pourcentage du rapport cyclique en un nombre entier, ce qui a entraîné la perte des décimales.
Tests de comparaison
Les figures 5 et 6 présentent une comparaison entre les mesures réalisées avec un oscilloscope et celles obtenues par le système PIC16F648. Pour ces tests, nous avons utilisé un générateur d'impulsions TTL.
Les mesures sont très similaires.
Listage du programme PIC16F648 PWMmeter
Le programme reste largement similaire à la version précédente, à l'exception de la routine LCDprint. Nous avons ajouté des instructions pour le calcul et l'affichage du rapport cyclique. La routine modifiée est présentée dans le listage 2.
Le code modifié occupe 2 266 mots de mémoire flash (55%) et 92 octets de mémoire vive (62%), et la compilation a été réalisée en 47 ms.
Questions sur les MCU, le PWM ou l'article ?
Vous avez des questions techniques sur les microcontrôleurs, le PWM ou cet article ? Contactez la rédaction d'Elektor à l'adresse suivante redaction@elektor.fr.