Table des matières

Arduino 33 BLE : modulation de largeur d'impulsion

1. Introduction

Ce document montre comment générer un signal à modulation de largeur d'impulsion (MLI) sur un Arduino nano 33 BLE. Nous verrons tout d'abord comment utiliser l'interface de programmation MBED, avant de décrire en profondeur l'utilisation du périphérique PWM du microcontrôleur Nordic nRF52840.

Les signaux MLI sont principalement utilisés en électronique de puissance pour piloter des transistors. Par exemple, un signal MLI peut commander un pont en H alimentant une charge inductive.

2. Utilisation de l'API MBED

L'arduino 33 BLE fait tourner le système d'exploitation MBED OS. L'API (Application Programming Interface) MBED permet d'accéder aux périphériques, comme l'API Arduino mais de manière plus complète. L'utilisation de MBED a un double avantage : il offre la possibilité de faire de la programmation multi-tâche et le code C++ peut être compilé (avec quelques adaptations mineures) pour tous les systèmes embarqués compatibles MBED.

Un microcontrôleur est composé d'un microprocesseur (par ex. Cortex M4 pour le Nordic nRF52840) et de périphériques, parmi lesquels les générateurs PWM. L'API MBED permet de programmer les générateurs PWM via la classe PwmOut. Les fonctions de cette classe permettent de choisir la période du signal et le rapport cyclique. Pour effectuer la modulation, il faut modifier le rapport cyclique à intervalle de temps régulier. Nous utilisons pour cela la classe Ticker, qui permet d'exécuter une fonction périodiquement, à intervalle de temps régulier.

mbed_pwm.ino
#include "Arduino.h"
#include "mbed.h"

#define NSAMPLES 100
#define OUT_1 2
#define OUT_2 3

float table_1[NSAMPLES];
float table_2[NSAMPLES];

mbed::PwmOut pwm_1(digitalPinToPinName(OUT_1));
mbed::PwmOut pwm_2(digitalPinToPinName(OUT_2));
mbed::Ticker tick;
uint16_t indice;

void update() {
  pwm_1.write(table_1[indice]);
  pwm_2.write(table_2[indice]);
  indice += 1;
  if (indice==NSAMPLES) indice=0;
}

void setup() {
  for (indice=0; indice<NSAMPLES; indice++) {
    table_1[indice] = 0.5+0.4*sin(2*PI*indice/NSAMPLES);
    table_2[indice] = 0.5+0.4*cos(2*PI*indice/NSAMPLES);
  }
  pwm_1.period_us(100);
  pwm_1.write(0.5);
  pwm_2.period_us(100);
  pwm_2.write(0.5);
  indice = 0.0;
  tick.attach_us(&update,1000);
}

void loop() {
  

}     
    	     

Ce programme génère deux signaux PWM (sur les sorties D2 et D3) de fréquence 10kHz modulés chacun par une sinusoïde de fréquence 10Hz. Les rapports cycliques correspondants à ces deux modulations sont stockés dans deux tables contenant 100 échantillons. Après avoir lancé les deux PWM avec une période de 100 microsecondes, on attache la fonction update au Ticker. Cette fonction, exécutée toutes les millisecondes, effectue la mise à jour des deux rapports cycliques en utilisant les valeurs stockées dans les tables.

Pour vérifier le bon fonctionnement de la modulation du rapport cyclique, on place en sortie un filtre RC passe-bas. Avec R=3,3 et C=470nF, la fréquence de coupure est fc=100Hz, soit 100 fois moins que la fréquence du PWM (10kHz), ce qui est largement suffisant (atténuation de -40 dB). La fréquence de la modulation (10Hz) est bien dans la bande passante. Voici le signal obtenu en sortie du filtre :

oscillo

Ce programme fonctionne très bien également à plus haute fréquence. Par exemple, voici le résultat pour une fréquence de PWM de 100kHz et une fonction update appelée toutes les 100 microsecondes, soit une fréquence de modulation de 100Hz :

oscillo

L'amplitude est réduite car la fréquence de modulation est égale à la fréquence de coupure du filtre passe-bas. Une analyse spectrale confirme que la modulation se fait bien à la fréquence de 100Hz, ce qui confirme que l'utilisation de Ticker est assez précise pour cette application. Nous avons cependant constaté (par une analyse précise non présentée ici) que l'intervalle de temps entre deux appels de la fonction update peut varier de manière très importante (jusqu'à 10 pour cent) mais que le système s'arrange pour compenser un retard par une avance, ce qui fait que l'intervalle de temps moyen est bien celui programmé. Le code exécuté dans la fonction update doit être le plus court possible et ne doit pas faire appel à des fonctions lourdes comme print ou des fonctions d'allocation de mémoire. Bien évidemment, l'exécution du code de cette fonction doit se faire en une durée plus petite que la période de son appel.

3. Programmation du générateur PWM

3.a. Principe

L'utilisation de Ticker pour effectuer la modulation du rapport cyclique (méthode exposée ci-dessus) fonctionne assez bien pour une période d'interruption de l'ordre de 100 microsecondes ou plus, mais peut poser problème pour des périodes plus petites, surtout si d'autres tâches sont demandées au processeur en parallèle. Des appels trop fréquents de la fonction update peuvent occuper le système au point de rendre impossible d'autres opérations. Dans ce cas, il est préférable de faire fonctionner le générateur PWM en autonomie complète. Pour cela, il faut faire une programmation directe du générateur PWM du microcontrôleur nRF52840, au moyen des regitres d'accès à ce périphérique. L'inconvénient de cette méthode est que le code n'est plus portable vers d'autres microcontrôleurs, contrairement au code ci-dessus qui utilise l'API MBED.

3.b. Fonctionnement du PWM

La notice d'utilisation du microcontrôleur nRF52840 (nRF52840 Product Specification) détaille la programmation des périphériques associés au microprocesseur, en particulier le générateur PWM (Pulse Width Modulation).

Le nRF52840 possède 4 générateurs PWM, chacun pouvant générer jusqu'à 4 voies et chacune de ces voies peut être dirigée vers une borne D1 à D14 de l'arduino. Le signal servant à piloter la modulation de largeur d'impulsion est stocké en mémoire RAM et lu directement par le PWM grace au DMA (Direct Memory Access).

Un générateur PWM fonctionne avec un compteur 15 bits (valeurs de 0 à 32768), qui est incrémenté à la fréquence suivante :

fc=16MHz2prescaler(1)

Le facteur de division (prescaler), est un entier codé sur 3 bits. La plus petite fréquence d'incrémentation du compteur est donc 125kHz.

Un registre 15 bits nommé COUNTERTOP contient la plus grande valeur du compteur. Lorsque celle-ci est atteinte, le compteur est soit initialisé à zéro (Up counter), soit mis dans un état de décrémentation (Up and Down Counter). À chaque incrémentation (ou décrémentation), la valeur du compteur est comparée à 4 registres nommés COMP0,COMP1,COMP2,COMP3, chacun perrmettant de générer un signal carré dont le rapport cyclique est connu. Le principe est montré sur la figure suivante :

compteur.svgFigure pleine page

On se limitera au cas Up Counter. La période du signal généré est :

T=COUNTERTOPfc

Le rapport cyclique est :

r=1-COMP0COUNTERTOP

Pour une période T choisie, on a intérêt à avoir une valeur de COUNTERTOP la plus grande possible, de manière à pouvoir moduler finement le rapport cyclique. Il faut donc choisir le prescaler le plus petit possible. Considérons par exemple une fréquence de signal de 10kHz, obtenue avec prescaler=0 et COUNTERTOP=1600; la précision de modulation est de l'ordre de 10 bits, ce qui est largement suffisant pour la plupart des applications. Si la fréquence est 100kHz (qui est le maximum supportable par un pont de transistor DMOS), on a COUNTERTOP=160, soit une précision de modulation de 7 bits. Dans ce cas, la précision peut être insuffisante pour certaines applications, par exemple pour générer un signal audio.

Le mode Up and Down Counter (que nous n'utiliserons pas), nécessite une valeur deux fois plus petite de COUNTERTOP pour la même période de signal, ce qui fait que la modulation est deux fois moins précise.

Il est aussi possible de changer la polarité de la sortie, ce qui donne un rapport cyclique COMP0/COUNTERTOP.

3.c. Programmation du PWM

La configuration d'un périphérique se fait via des registres. Nous proposons une classe C++ NRF52_PWM qui permet d'utiliser facilement un générateur PWM depuis un programme Arduino. L'intitulé NRF52 indique que cette classe fonctionne probablement sur tous les microcontrôleurs de la famille nRF52xxx, bien que nous l'ayons testé seulement sur le nRF52840. Voici le fichier entête :

nrf52_pwm.h
#include "nrf.h"

#ifndef _NRF52_PWM_
#define _BRF52_PWM_

class NRF52_PWM {
  private:
    const uint8_t pins[14] = {0,0,11,12,15,13,14,23,21,27,2,1,8,13};
    const uint8_t ports[14] = {0,0,1,1,1,1,1,0,0,0,1,1,1,0};
    uint16_t seq[4] = {0,0,0,0};
    uint16_t *sequence=NULL;

  public:
    NRF52_PWM();
    ~NRF52_PWM();
    void fixe_quatre_sorties(uint8_t nsorties, uint8_t *sorties, uint8_t prescaler, uint16_t countertop, float *rapports);
    void modulation_trois_sorties_fmod(uint8_t nsorties, uint8_t *sorties, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t *countertop, float *rapports_1, float *rapports_2, float *rapports_3);
    void modulation_une_sortie(uint8_t sortie, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, float *rapports);
    void modulation_une_sortie(uint8_t sortie, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, uint16_t *sequence_0, uint16_t *sequence_1);
    void modulation_deux_sorties(uint8_t sortie_1, uint8_t sortie_2, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, float *rapports_1, float *rapports_2);
    void modulation_quatre_sorties(uint8_t sortie_1, uint8_t sortie_2, uint8_t sortie_3, uint8_t sortie_4, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, float *rapports_1, float *rapports_2, float *rapports_3, float *rapports_4);
    void stop();
    void attendre();
};

#endif
    	      

Le tableau pins contient les bornes de sortie du microcontôleur pour chaque numéro de borne de la platine Arduino (voir plan de brochage). Le tableau ports donne les numéros de port. Le tableau seq contiendra les quatres valeurs de COMP pour la génération de 4 voies sans modulation. Le pointeur sequence pointera un tableau d'entiers 16 bits, qui servira à stocker une séquence de valeurs de COMP, qui sera lue par le générateur PWM afin d'effectuer une modulation du rapport cyclique.

Le constructeur de la classe ne fait rien. Le desctructeur libère l'emplacement mémoire occupé par le tableau sequence.

nrf52_pwm.cpp
#include "Arduino.h"
#include "nrf.h"
#include "nrf52_pwm.h"

NRF52_PWM::NRF52_PWM() {
   
}

NRF52_PWM::~NRF52_PWM() {
   if (sequence!=NULL) free(sequence);
}
   	       
    	       

La fonction fixe_quatre_sorties permet de générer un signal PWM sur 4 sorties avec un rapport cyclique fixe. On choisit le nombre de sorties (maximum 4) que l'on veut effectivement générer et les numéros de borne de ces sorties.

void NRF52_PWM::fixe_quatre_sorties(uint8_t nsorties, uint8_t *sorties, uint8_t prescaler, uint16_t countertop, float *rapports) {
   for (uint8_t j=0; j<nsorties; j++) {
      NRF_PWM3->PSEL.OUT[j] = pins[sorties[j]] | (ports[sorties[j]] << 5);
      seq[j] = (1-rapports[j])*countertop; // COMPj
   }
   NRF_PWM3->ENABLE = 1;
   NRF_PWM3->MODE = 0; // Up Counter, edge-aligned PWM duty cycle
   NRF_PWM3->PRESCALER = prescaler;
   NRF_PWM3->COUNTERTOP = countertop;
   NRF_PWM3->LOOP = 1;
   NRF_PWM3->DECODER = 2; // individual, la séquence donne COMP0, COMP1, COMP2, COMP3.
   NRF_PWM3->SEQ[0].PTR = ((uint32_t)(seq)); // adresse en mémoire de la séquence
   NRF_PWM3->SEQ[0].CNT = 4; // nombre de mots de 16 bits dans la séquence
   NRF_PWM3->SEQ[0].REFRESH = 0;
   NRF_PWM3->SEQ[0].ENDDELAY = 0;
   NRF_PWM3->TASKS_SEQSTART[0] = 1;  
}  	        
    	        

Les arguments de cette fonction sont :

  • nsorties : nombre de sorties générées (1 à 4).
  • sorties : tableau contenant les numéros de borne des sorties, par exemple 2 pour la sortie D2 de la carte arduino.
  • prescaler : puissance pour la division de la fréquence d'horloge (de 0 à 7).
  • countertop : valeur maximale du compteur (codée sur 15 bits, valeur de 3 à 32767).
  • rapports : tableau contenant les rapports cycliques des 4 voies (flottant entre 0 et 1).

Remarque : nous utilisons le dernier des quatre générateurs PWM (numéro 3), de manière à laisser la possibilité d'utiliser trois fois la fonction analogWrite. La fonction analogWrite génère un PWM de fréquence 500Hz et ne permet pas de faire de la modulation. Elle convient néanmoins pour les usages simples comme alimenter une LED ou une résistance chauffante (via un transistor).

La fonction modulation_une_sortie permet de générer une voie avec modulation du rapport cyclique.

 void NRF52_PWM::modulation_une_sortie(uint8_t sortie, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, float *rapports) {
   uint16_t i,j;
   if (sequence!=NULL) free(sequence);
   sequence = (uint16_t*) malloc(nechant*sizeof(uint16_t));
   for (i=0; i<nechant; i++) sequence[i] = (1-rapports[i])*countertop; 
   NRF_PWM3->PSEL.OUT[0] = pins[sortie] | (ports[sortie] << 5);
   for (j=1; j<3; j++) NRF_PWM3->PSEL.OUT[j] = (1<<31);
   NRF_PWM3->ENABLE = 1;
   NRF_PWM3->MODE = 0; // up counter, edge-aligned PWM duty cycle
   NRF_PWM3->PRESCALER = prescaler;
   NRF_PWM3->COUNTERTOP = countertop;
   NRF_PWM3->LOOP = boucle/2;
   NRF_PWM3->DECODER = 0; // common : 1st half word (16 bits) used in all PWM channels 0..3
   NRF_PWM3->SEQ[0].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[0].CNT = nechant;
   NRF_PWM3->SEQ[0].REFRESH = refresh-1;
   NRF_PWM3->SEQ[0].ENDDELAY = 0;
   NRF_PWM3->SEQ[1].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[1].CNT = nechant;
   NRF_PWM3->SEQ[1].REFRESH = refresh-1;
   NRF_PWM3->SEQ[1].ENDDELAY = 0;
   NRF_PWM3->TASKS_SEQSTART[0] = 1;  
}         
    	         

Les arguments de cette fonction sont :

  • sortie : numéro de la borne de sortie, par exemple 2 pour la sortie D2 de la carte arduino.
  • prescaler : puissance pour la division de la fréquence d'horloge (de 0 à 7).
  • refresh : nombre (24 bits) de périodes au bout duquel la valeur de COMP est changé, ce qui permet de préciser la période d'échantillonnage de variation du rapport cyclique.
  • nechant : nombre d'échantillons du rapport cyclique.
  • boucle : nombre (15 bits) d'application en boucle de la séquence (supérieur ou égal à 2).
  • countertop : valeur maximale du compteur (codée sur 15 bits, valeur de 3 à 32767).
  • rapports : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques.

Le générateur PWM applique deux séquences de valeurs de COMP, nommées SEQ[0] et SEQ[1]. Ces deux séquences sont répétées un nombre de fois défini dans le registre LOOP. La valeur la plus petite de boucle est deux, auquel cas les deux séquences sont appliquées une fois (car LOOP=1). Nous avons choisi d'opérer avec deux séquences identiques, calculées à partir du tableau rapports fourni. Cette méthode convient pour la modulation périodique mais ne convient pas pour la modulation aléatoire.

Supposons que l'on souhaite générer une modulation sinusoïdale de 10Hz au moyen d'un signal PWM de 10kHz, soit une période T=0,1ms. On choisit pour cela prescaler=0 et countertop=1600. Le tableau rapports est rempli avec les valeurs du rapport cyclique pour une période de modulation (voir plus loin). La période de modulation est :

Tm=T*refresh*nechant

Avec nechant=100, on doit choisir refresh=10 (ce qui est le minimum pour obtenir effectivement une modulation). Le nombre de périodes générées est boucle, dont la valeur maximale est 32767. Si la fréquence du PWM est 100kHz alors le même signal est obtenu avec refresh=100.

La génération de signaux aléatoires nécessite d'utiliser deux séquence différentes. La second version de la fonction modulation_une_sortie permet de définir deux séquences différentes. Elles sont fournies sous la forme d'entiers 16 bits donnant directement les valeurs de COMP (et non pas de rapport cyclique) et les pointeurs sur les deux tableaux sont fournis en argument.

void NRF52_PWM::modulation_une_sortie(uint8_t sortie, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, uint16_t *sequence_0, uint16_t *sequence_1) {
   uint16_t i,j;
   NRF_PWM3->PSEL.OUT[0] = pins[sortie] | (ports[sortie] << 5);
   for (j=1; j<3; j++) NRF_PWM3->PSEL.OUT[j] = (1<<31);
   NRF_PWM3->ENABLE = 1;
   NRF_PWM3->MODE = 0; // up counter, edge-aligned PWM duty cycle
   NRF_PWM3->PRESCALER = prescaler;
   NRF_PWM3->COUNTERTOP = countertop;
   NRF_PWM3->LOOP = boucle/2;
   NRF_PWM3->DECODER = 0; // common : 1st half word (16 bits) used in all PWM channels 0..3
   NRF_PWM3->SEQ[0].PTR = ((uint32_t)(sequence_0));
   NRF_PWM3->SEQ[0].CNT = nechant;
   NRF_PWM3->SEQ[0].REFRESH = refresh-1;
   NRF_PWM3->SEQ[0].ENDDELAY = 0;
   NRF_PWM3->SEQ[1].PTR = ((uint32_t)(sequence_1));
   NRF_PWM3->SEQ[1].CNT = nechant;
   NRF_PWM3->SEQ[1].REFRESH = refresh-1;
   NRF_PWM3->SEQ[1].ENDDELAY = 0;
   NRF_PWM3->TASKS_SEQSTART[0] = 1; 
}           
    	           

La fonction modulation_deux_voies permet de générer deux sorties avec modulation du rapport cyclique.

void NRF52_PWM::modulation_deux_sorties(uint8_t sortie_1, uint8_t sortie_2, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, float *rapports_1, float *rapports_2) {
  uint16_t i,j;
   if (sequence!=NULL) free(sequence);
   sequence = (uint16_t*) malloc(nechant*2*sizeof(uint16_t));
   for (i=0; i<nechant; i++) {
      sequence[2*i] = (1-rapports_1[i])*countertop;
      sequence[2*i+1] = (1-rapports_2[i])*countertop;
   }
   NRF_PWM3->PSEL.OUT[0] = pins[sortie_1] | (ports[sortie_1] << 5);
   NRF_PWM3->PSEL.OUT[1] = (1<<31);
   NRF_PWM3->PSEL.OUT[2] = pins[sortie_2] | (ports[sortie_2] << 5);
   NRF_PWM3->PSEL.OUT[3] = (1<<31);  
   NRF_PWM3->ENABLE = 1;
   NRF_PWM3->MODE = 0; // up counter, edge-aligned PWM duty cycle
   NRF_PWM3->PRESCALER = prescaler;
   NRF_PWM3->COUNTERTOP = countertop;
   NRF_PWM3->LOOP = boucle/2;
   NRF_PWM3->DECODER = 1; //Grouped : 1st half word (16-bit) used in channel 0..1; 2nd word in channel 2..3
   NRF_PWM3->SEQ[0].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[0].CNT = nechant*2;
   NRF_PWM3->SEQ[0].REFRESH = refresh-1;
   NRF_PWM3->SEQ[0].ENDDELAY = 0;
   NRF_PWM3->SEQ[1].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[1].CNT = nechant*2;
   NRF_PWM3->SEQ[1].REFRESH = refresh-1;
   NRF_PWM3->SEQ[1].ENDDELAY = 0;
   NRF_PWM3->TASKS_SEQSTART[0] = 1; 
}           
    	           

Les arguments de cette fonction sont :

  • sortie_1 : numéro de la première sortie.
  • sortie_2 : numéro de la seconde sortie.
  • prescaler : puissance pour la division de la fréquence d'horloge (de 0 à 7).
  • refresh : nombre (24 bits) de périodes au bout duquel la valeur de COMP est changé, ce qui permet de préciser la période d'échantillonnage de variation du rapport cyclique.
  • nechant : nombre d'échantillons du rapport cyclique.
  • boucle : nombre d'application en boucle de la séquence.
  • countertop : valeur maximale du compteur (codée sur 15 bits, valeur de 3 à 32767).
  • rapports_1 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la première sortie.
  • rapports_2 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la seconde sortie.

La fonction modulation_quatre_sorties permet de générer quatre sorties avec modulation du rapport cyclique :

void NRF52_PWM::modulation_quatre_sorties(uint8_t sortie_1, uint8_t sortie_2, uint8_t sortie_3, uint8_t sortie_4, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t countertop, float *rapports_1, float *rapports_2, float *rapports_3, float *rapports_4) {
   uint16_t i,j;
   if (sequence!=NULL) free(sequence);
   sequence = (uint16_t*) malloc(nechant*4*sizeof(uint16_t));
   for (i=0; i<nechant; i++) {
      sequence[4*i] = (1-rapports_1[i])*countertop;
      sequence[4*i+1] = (1-rapports_2[i])*countertop;
      sequence[4*i+2] = (1-rapports_3[i])*countertop;
      sequence[4*i+3] = (1-rapports_4[i])*countertop;
   }
   NRF_PWM3->PSEL.OUT[0] = pins[sortie_1] | (ports[sortie_1] << 5);
   NRF_PWM3->PSEL.OUT[1] = pins[sortie_2] | (ports[sortie_2] << 5);
   NRF_PWM3->PSEL.OUT[2] = pins[sortie_3] | (ports[sortie_3] << 5);
   NRF_PWM3->PSEL.OUT[3] = pins[sortie_4] | (ports[sortie_4] << 5);
   NRF_PWM3->ENABLE = 1;
   NRF_PWM3->MODE = 0; // up counter, edge-aligned PWM duty cycle
   NRF_PWM3->PRESCALER = prescaler;
   NRF_PWM3->COUNTERTOP = countertop;
   NRF_PWM3->LOOP = boucle/2;
   NRF_PWM3->DECODER = 2; //Individual : 1st half word (16-bit) in ch.0; 2nd in ch.1; ...; 4th in ch.3
   NRF_PWM3->SEQ[0].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[0].CNT = nechant*4;
   NRF_PWM3->SEQ[0].REFRESH = refresh-1;
   NRF_PWM3->SEQ[0].ENDDELAY = 0;
   NRF_PWM3->SEQ[1].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[1].CNT = nechant*4;
   NRF_PWM3->SEQ[1].REFRESH = refresh-1;
   NRF_PWM3->SEQ[1].ENDDELAY = 0;
   NRF_PWM3->TASKS_SEQSTART[0] = 1; 
}    	        
    	        

Les arguments de cette fonction sont :

  • sortie_1 : numéro de la première sortie.
  • sortie_2 : numéro de la deuxième sortie.
  • sortie_3 : numéro de la troisième sortie.
  • sortie_4 : numéro de la quatrième sortie.
  • prescaler : puissance pour la division de la fréquence d'horloge (de 0 à 7).
  • refresh : nombre (24 bits) de périodes au bout duquel la valeur de COMP est changé, ce qui permet de préciser la période d'échantillonnage de variation du rapport cyclique.
  • nechant : nombre d'échantillons du rapport cyclique.
  • boucle : nombre d'application en boucle de la séquence.
  • countertop : valeur maximale du compteur (codée sur 15 bits, valeur de 3 à 32767).
  • rapports_1 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la première sortie.
  • rapports_2 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la deuxième sortie.
  • rapports_3 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la troisième sortie.
  • rapports_4 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la quatrième sortie.

La fonction modulation_trois_sorties_fmod permet de générer jusqu'à trois sorties avec modulation du rapport cyclique et modulation de la fréquence (par modulation de la valeur de COUNTERTOP).

void NRF52_PWM::modulation_trois_sorties_fmod(uint8_t nsorties, uint8_t *sorties, uint8_t prescaler, uint16_t refresh, uint16_t nechant, uint16_t boucle, uint16_t *countertop, float *rapports_1, float *rapports_2, float *rapports_3) {
  uint16_t i,j;
  if (sequence!=NULL) free(sequence);
  sequence = (uint16_t*) malloc(nechant*4*sizeof(uint16_t));
  for (i=0; i<nechant; i++) sequence[4*i+3] = countertop[i];
  for (j=0; j<nsorties; j++) {
    NRF_PWM3->PSEL.OUT[j] = pins[sorties[j]] | (ports[sorties[j]] << 5);
  }
  for (j=nsorties; j<3; j++) NRF_PWM3->PSEL.OUT[j] = (1<<31);
  if (rapports_1!=NULL) for (i=0; i<nechant; i++) sequence[4*i] = (1-rapports_1[i])*countertop[i];
  if (rapports_2!=NULL) for (i=0; i<nechant; i++) sequence[4*i+1] = (1-rapports_2[i])*countertop[i];
  if (rapports_3!=NULL) for (i=0; i<nechant; i++) sequence[4*i+2] = (1-rapports_3[i])*countertop[i];
  NRF_PWM3->ENABLE = 1;
   NRF_PWM3->MODE = 0; 
   NRF_PWM3->PRESCALER = prescaler;
   NRF_PWM3->LOOP = boucle/2;
   NRF_PWM3->DECODER = 3; //Waveform 1st half word (16-bit) in ch.0; 2nd in ch.1; ...; 4th in COUNTERTOP
   NRF_PWM3->SEQ[0].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[0].CNT = nechant*4;
   NRF_PWM3->SEQ[0].REFRESH = refresh-1;
   NRF_PWM3->SEQ[0].ENDDELAY = 0;
   NRF_PWM3->SEQ[1].PTR = ((uint32_t)(sequence));
   NRF_PWM3->SEQ[1].CNT = nechant*4;
   NRF_PWM3->SEQ[1].REFRESH = refresh-1;
   NRF_PWM3->SEQ[1].ENDDELAY = 0;
   NRF_PWM3->TASKS_SEQSTART[0] = 1; 
}
    	        
    	        

Les arguments de cette fonction sont :

  • nsorties : nomrbe de sortie (1,2 ou 3).
  • sorties : tableau contenant les numéros des sorties.
  • prescaler : puissance pour la division de la fréquence d'horloge (de 0 à 7).
  • refresh : nombre (24 bits) de périodes au bout duquel la valeur de COMP est changé, ce qui permet de préciser la période d'échantillonnage de variation du rapport cyclique.
  • nechant : nombre d'échantillons du rapport cyclique.
  • boucle : nombre d'application en boucle de la séquence.
  • countertop : tableau contenant la séquence de valeurs de countertop (codées sur 15 bits, valeurs de 3 à 32767).
  • rapports_1 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la première sortie.
  • rapports_2 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la deuxième sortie.
  • rapports_3 : tableau de flottants (nechant éléments) contenant la séquence de rapports cycliques pour la troisième sortie.

Cette fonction peut être utilisée pour générer un signal FSK (modulation de fréquence) pour la transmission de données.

La fonction attendre permet d'attendre que la séquence PWM précédemment programmée soit terminée.

void NRF52_PWM::attendre() {
  while (NRF_PWM3->EVENTS_LOOPSDONE==0);
  NRF_PWM3->EVENTS_LOOPSDONE = 0;
}   	        
    	        

La fonction stop permet de stopper la séquence PWM en cours, même si elle n'est pas terminée, et d'annuler la tension des sorties.

void NRF52_PWM::stop() {
  NRF_PWM3->TASKS_STOP = 1;
  while (NRF_PWM3->EVENTS_STOPPED==0) {;}
  NRF_PWM3->EVENTS_STOPPED = 0;
}   	        
    	        

4. Programme de test

Dans cette partie, nous présentons un exemple d'utilisation de la classe précédente, qui permet de tester ses différentes fonctions.

nano33ble_test_pwm.ino
#include "Arduino.h"
#include "nrf.h"
#include "nrf52_pwm.h"	   
    	   

On choisit 100 échantillons pour définir les formes d'onde de la modulation.

#define MODULATION
#define NSAMPLES 100
uint8_t sorties[4] = {3,4,5,6};
float sinus_1[NSAMPLES];
float sinus_2[NSAMPLES];
uint16_t countertop[NSAMPLES];
uint8_t prescaler = 0; // horloge à 16 MHz
uint16_t ct = 1600; // 10 kHz
uint16_t refresh = 10; // période d'échantillonnage = 1 ms, période = 1 ms*NSAMPLES = 100 ms
uint16_t boucle = 20; // 20 périodes (2 secondes)

NRF52_PWM pwm; 	   
    	   

Dans la fonction setup, on configure les bornes utilisées en sortie puis en rempli les tableaux pour la modulation.

void setup() {
  Serial.begin(115200);
  for (int i=0; i<4; i++) pinMode(sorties[i],OUTPUT);  
  for (int i=0; i<NSAMPLES; i++) {  
      sinus_1[i] = 0.5+0.4*sin(2*PI*i/NSAMPLES);
      sinus_2[i] = 0.5+0.4*cos(2*PI*i/NSAMPLES);
      fixe[i] = 0.5;
  }
  for (int i=0; i<NSAMPLES/2; i++) countertop[i] = ct;
  for (int i=NSAMPLES/2; i<NSAMPLES; i++) countertop[i] = 1500;
  
}    	   
    	   

Dans la première fonction loop, on déclenche le PWM avec un rapport cylique fixe sur 2 sorties, avec la fonction fixe_quatre_sorties. La génération du signal se poursuit tant qu'on ne stoppe pas le PWM (même comportement que la fonction analogWrite). On place donc un délai pour maintenir la génération pendant une seconde puis on stoppe le PWM. Si on ne souhaite pas arrêter le signal mais changer son rapport cyclique, il suffit d'appeler à nouveau la fonction fixe_quatre_sorties.

#ifndef MODULATION
void loop() {
  float rapports[2] = {0.3,0.5};
  pwm.fixe_quatre_sorties(2,sorties,prescaler,ct,rapports);
  delay(1000);
  pwm.stop();
  delay(1000);
}

    	    

La seconde fonction loop (compilée lorsque la macro MODULATION est définie) permet d'effectuer une génération avec modulation. Dans ce cas, la séquence de modulation est exécutée un nombre de fois défini par boucle. Pour attendre que la séquence soit terminée, il suffit d'appeler attendre, mais on peut bien sûr effectuer d'autres opérations avant d'appeler cette fonction. On peut aussi interrompre la génération avant que la séquence soit terminée, en appelant stop. L'appel de la fonction stop juste après l'attente de la terminaison permet de stopper la génération du signal (la sortie s'annule). Si on ne place pas cet appel, la génération continue avec le dernier rapport cyclique.

#else
void loop() {
    pwm.modulation_une_sortie(sorties[0],prescaler,refresh,NSAMPLES,boucle,ct,sinus_1);
    //pwm.modulation_deux_sorties(sorties[0],sorties[1],prescaler,refresh,NSAMPLES,boucle,ct,sinus_1,sinus_2);
    //pwm.modulation_quatre_sorties(sorties[0],sorties[1],sorties[2],sorties[2],prescaler,refresh,NSAMPLES,boucle,ct,sinus_1,sinus_2,sinus_1,sinus_2);
    //pwm.modulation_trois_sorties_fmod(3,sorties,prescaler,refresh,NSAMPLES,boucle,countertop,fixe,sinus_1,sinus_2);
    pwm.attendre();
    pwm.stop();
    delay(1000);
}
#endif
    	     

Pour vérifier le bon fonctionnement de la modulation du rapport cyclique, on place en sortie un filtre RC passe-bas. Avec R=3,3 et C=470nF, la fréquence de coupure est fc=100Hz, soit 100 fois moins que la fréquence du PWM (10kHz), ce qui est largement suffisant (atténuation de -40 dB). La fréquence de la modulation (10Hz) est bien dans la bande passante. Voici le signal obtenu en sortie du filtre :

oscillo

La période est bien 100ms comme prévu. Le rapport cyclique le plus haut est 0,9. Sachant que la tension de sortie est de 3,3V, la tension maximale en sortie du filtre est 2,97V.

5. Génération d'un signal stochastique

Pour générer un signal stochastique, nous devons fournir une séquence de valeurs de COMP aléatoires. Nous utilisons pour cela le générateur aléatoire du nRF52840, qui génère une suite de nombres vraiment aléatoires, c'est-à-dire non périodique (à partir du bruit thermique). On utilise la version de la modulation_une_sortie qui permet de fournir directement deux séquences de valeurs de COMP. On choisit COUNTERTOP=0x7F=0b01111111 et on se sert de COUNTERTOP comme masque pour obtenir les 7 premiers bits de NRF_RNG->VALUE.

random_pwm.ino
 
#include "Arduino.h"
#include "nrf.h"
#include "nrf52_pwm.h"

#define NSAMPLES 1000
#define SORTIE 3

uint16_t sequence_0[NSAMPLES];
uint16_t sequence_1[NSAMPLES];
uint8_t prescaler = 0; // horloge à 16 MHz
uint16_t ct = 0x7F; // 125 kHz
uint16_t refresh = 10; 
uint16_t boucle = 2;

NRF52_PWM pwm;

void random_gen() {                                   
  uint16_t i;
  NRF_RNG->TASKS_START = 1;
  for (i=0; i<NSAMPLES; i++) {
    sequence_0[i] = NRF_RNG->VALUE & ct;
    delayMicroseconds(5);
    sequence_1[i] = NRF_RNG->VALUE & ct;
    delayMicroseconds(5);
  }
  NRF_RNG->TASKS_STOP = 1;
}

void setup() {
  Serial.begin(115200);
  pinMode(SORTIE,OUTPUT);
  NRF_RNG->CONFIG |= 1;
  random_gen();
}

void loop() {
  pwm.modulation_une_sortie(SORTIE,prescaler,refresh,NSAMPLES,boucle,ct,sequence_0,sequence_1);
  random_gen();
  pwm.attendre();
}   	      
    	      

Voici le signal en sortie du filtre RC passe-bas :

oscillo

et son spectre :

oscillo

6. Génération d'un signal binaire

Il s'agit de générer un signal constitué d'une suite de bits enchaînés à une fréquence fixe. Un bit 1 doit se traduire pas un niveau de tension haut et un bit 0 par une tension nulle. Pour cela, il faut que COMP soit nul (bit = 1) ou égal à COUNTERTOP (bit = 0) et que le rapport cyclique soit modifié à chaque cycle du compteur (refresh=1). La suite de bits peut être générée aléatoirement, ou bien à partir d'une liste de bits prédéfinies (auquel cas le signal est périodique).

pwm_bit_sequence.ino
#include "Arduino.h"
#include "nrf.h"
#include "nrf52_pwm.h"

#define NSAMPLES 256
#define SORTIE 3
//#define RANDOM

uint16_t sequence_0[NSAMPLES];
uint16_t sequence_1[NSAMPLES];
uint8_t prescaler = 0; // horloge à 16 MHz
uint16_t ct = 160; // 100 kHz
uint16_t refresh = 1; 
uint16_t boucle = 2;
uint8_t bits[16] = {0,0,1,0,1,1,0,1,0,0,0,1,0,1,1,1};

NRF52_PWM pwm;



void random_bits_gen() {                                   
  uint16_t i;
  uint8_t rnd;
  NRF_RNG->TASKS_START = 1;
  for (i=0; i<NSAMPLES; i++) {
    rnd = NRF_RNG->VALUE;
    if (rnd < 0x80) sequence_0[i] = ct;
    else sequence_0[i] = 0;
    delayMicroseconds(5);
    rnd = NRF_RNG->VALUE;
    if (rnd < 0x80) sequence_1[i] = ct;
    else sequence_1[i] = 0;
    delayMicroseconds(5);
  }
  NRF_RNG->TASKS_STOP = 1;
}



void setup() {
  Serial.begin(115200);
  pinMode(SORTIE,OUTPUT);
  NRF_RNG->CONFIG |= 1;
  int j=0;
  for (int i=0; i<NSAMPLES; i++) {
    if (bits[j]==1) {
      sequence_0[i] = 0;
      sequence_1[i] = 0;
    }
    else {
      sequence_0[i] = ct;
      sequence_1[i] = ct;
    }
    j += 1;
    if (j==16) j=0;
  }
#ifdef RANDOM
  random_bits_gen();
#endif
}

void loop() {
  pwm.modulation_une_sortie(SORTIE,prescaler,refresh,NSAMPLES,boucle,ct,sequence_0,sequence_1);
#ifdef RANDOM
  random_bits_gen();
#endif
  pwm.attendre();
}              
    	     

Voici le signal dans le cas de la suite de 16 bits, pour une fréquence de 100kHz (débit de 100 kbits par seconde). :

oscillo
Creative Commons LicenseTextes et figures sont mis à disposition sous contrat Creative Commons.