Table des matières

Onduleur triphasé à modulation de largeur d'impulsion

1. Introduction

Ce document décrit le fonctionnement d'un onduleur triphasé permettant d'alimenter trois bobines. Ce type d'onduleur est utilisé pour alimenter le stator d'un moteur triphasé (synchrone ou asynchrone). La forme d'onde sinusoïdale des courants dans les bobines est obtenue au moyen de la commande des transistors par modulation de largeur d'impulsion (MLI).

On verra comment générer les trois signaux de commande avec un Arduino MEGA puis un exemple d'onduleur fonctionnant avec des transistors MOSFET.

2. Fonctionnement du convertisseur

Le convertisseur DC-AC est un hacheur constitué de 6 transistors que l'on modélisera par des interrupteurs, notés $K_1,K_2,K_3,K_4,K_5,K_6$ en parallèle avec des diodes (diodes de roue libre ajoutées ou diodes drain-source des transistors MOSFET). La paire d'interrupteurs $K_1$et $K_2$ constitue le bras de pont A, la paire $K_3,K_4$ le bras de pont B, la paire $K_5,K_6$ le bras de pont C.

Dans un bras de pont, un interrupteur est toujours dans l'état contraire de l'autre. Par exemple pour le bras de pont A, si l'interrupteur $K_1$ est fermé alors l'interrupteur $K_2$ est ouvert (et inversement). Si $K_1$ est ouvert, la tension du point $A$ par rapport à la masse est égale à $V_s$, la tension délivrée par la source. Si au contraire $K_1$ est ouvert et $K_2$ fermé, la tension du point $A$ est nulle.

pont3phases-fig.svgFigure pleine page

Les trois bobines sont connectées en étoile. La bobine A a une borne reliée à la sortie du demi-point A et l'autre reliée au nœud N (neutre). La bobine B a une borne reliée à la sortie du demi-point B et l'autre reliée au point N. La bobine C a une borne reliée à la sortie du demi-point C et l'autre reliée au point N. Chaque bobine est modélisée par une auto-inductance L en série avec une résistance R. L'impédance complexe de la bobine est donc Z̲=R+jLω . Si V désigne l'amplitude d'une tension sinusoïdale aux bornes d'une bobine, l'amplitude complexe de l'intensité du courant sinusoïdal qui la traverse est :

I̲=V̲R+jLω(1)

Le courant se déduit donc de la tension par un filtrage passe-bas. La fréquence de la porteuse doit être grande devant celle du signal modulant (au moins un facteur 100). Supposons de plus que la fréquence de modulation soit dans la bande atténuante (la période de modulation est inférieure L/R). Dans le courant, la porteuse est donc atténuée d'au moins -40 dB par rapport à la modulation. Pour une modulation sinusoïdale, le calcul de l'amplitude d'oscillation du courant se fait donc avec la relation ci-dessus appliquée à la fréquence de la modulation. Il s'en suit que l'amplitude de l'oscillation du courant diminue lorsque la fréquence augmente.

import numpy as np
from matplotlib.pyplot import *
                

L'état de sortie d'un bras de pont est défini par un nombre compris entre 0 et 1, qui correspond au rapport cyclique du signal carré qui commande les deux transistors. On représente l'état de chaque bras de pont en fonction de la phase exprimée de degrés. Voici l'état des bras de pont pour une amplitude maximale (égale à 1) :

N = 360*2
theta = np.arange(N)
amplitude = 1.0
etat_A = (1+np.sin(theta*np.pi/180))*0.5*amplitude
etat_B = np.roll(etat_A,120)
etat_C = np.roll(etat_B,120)
xtic = [0,60,120,180,240,300,360]
figure(figsize=(12,6))
plot(theta,etat_A,"r",label="A")
plot(theta,etat_B,"b-",label="B")
plot(theta,etat_C,"g-",label="C")
xticks(xtic)
grid()
xlabel("phase (degrés)")
legend(loc="upper right")         
                 
fig1fig1.pdf

Les tensions VA,VB,VC (si la sortie est en circuit ouvert) sont trois sinusoïdes positives déphasées deux à deux de 120 degrés.

Les tensions entre les bornes des bobines s'en déduisent (il s'agit des tensions permettant de calculer les courants) :

Vab = etat_A-etat_B
Vbc = etat_B-etat_C
Vca = etat_C-etat_A
figure(figsize=(12,6))
subplot(311)
plot(theta,Vab,"r-")
xticks(xtic)
grid()
ylabel("Vab")
subplot(312)
plot(theta,Vbc,"b-")
xticks(xtic)
grid()
ylabel("Vbc")
subplot(313)
plot(theta,Vca,"g")
xticks(xtic)
grid()
ylabel("Vca")
xlabel("degrés")                
                 
fig2fig2.pdf

Ces trois tensions sinusoïdales sont alternatives, contrairement aux tensions en A,B et C.

Les intensités des courants dans les trois bobines se calculent de la manière suivante (les intensités sont normalisées par l'amplitude à la fréquence de la modulation) :

Vs = 1
R = 1
VN = Vs*(etat_A+etat_B+etat_C)/3
IA = (etat_A-VN)/R
IB = (etat_B-VN)/R
IC = (etat_C-VN)/R
figure(figsize=(12,6))
subplot(311)
plot(theta,IA,"r")
xticks(xtic)
grid()
ylabel("IA")
subplot(312)
plot(theta,IB)
xticks(xtic)
grid()
ylabel("IB")
subplot(313)
plot(theta,IC,"g-")
xticks(xtic)
grid()
ylabel("IC")
xlabel("degrés")
                 
fig3fig3.pdf

Voyons à présent le champ magnétique qui est généré si chaque bobine est en fait une paire de bobines et si les trois paires sont disposées à 120 degrés l'une de l'autre, comme montré sur la figure suivante :

bobines.svgFigure pleine page

Le champ magnétique au centre est la somme des champs créés par les trois bobines. Ces trois vecteurs, dont la direction et le sens sont indiqués sur la figure pour un courant positif dans les bobines (remarquer l'angle de 120 degrés entre ces vecteurs), ont la même norme. Ce système modélise le champ tournant généré par un stator de moteur triphasé bipolaire.

Voici le calcul des composantes du champ magnétique puis de sa direction par rapport à l'axe (Ox) :

a = np.pi*2/3
Bx = IA+(IB+IC)*np.cos(a)
By = (IC-IB)*np.sin(a)
angleB = np.angle(Bx+1j*By)*180/np.pi
figure(figsize=(12,6))
plot(theta,angleB,"k-")
grid()
xticks(xtic)
                  
fig4fig4.pdf

On obtient un champ tournant dans le sens horaire.

3. Programme Arduino

On suppose que le convertisseur comporte une seule commande pour chaque bras de pont. Par exemple, la commande du bras de pont A permet d'actionner simultanément $K_1$ et $K_2$ en opposition. Dans ce cas, le microcontrôleur doit générer trois signaux de commande, un pour chaque bras de pont.

Le programme décrit fonctionne sur Arduino MEGA, qui dispose de 4 Timers 16 bits. Un seul suffit pour générer les trois signaux de commande car un Timer possède trois sorties (OCnA, OCnB et OCnC).

Le Timer 1 est utilisé pour générer les trois signaux à modulation de largeur d'impulsion (MLI), comme expliqué en détail dans Génération d'un signal par modulation de largeur d'impulsion. Les sorties A,B et C du Timer 1 sont reliées aux sortie D11, D12 et D13 de l'Arduino MEGA.

La figure suivante montre comment un signal PWM est généré avec le compteur TCNT1 du Timer 1 sur la sortie A.

../bobines/pwmTimer.svgFigure pleine page

Le compteur a une phase de croissance jusqu'à ICR1 puis une phase de décroissance. La période de découpage T (la période de la porteuse du signal MLI) est ajustée à la fois par la durée d'un top d'horloge et par la valeur de ICR1. La sortie OC1A passe à 0 lorsque le compteur passe au dessus de OCR1A et passe à 1 lorsque le compteur passe en dessous de OCR1A. Le rapport de OCR1A par ICR1 fixe donc le rapport cyclique.

Le signal modulant le rapport cyclique est une sinusoïde dont les valeurs d'étendent de zéro à une valeur maximale, choisie en fonction de l'intensité maximale du courant que l'on souhaite faire passer dans les bobines. Les valeurs de OCR1A, OCR1B et OCR1C sont générées avec une table et un accumulateur de phase. À chaque période T, le rapport cyclique est modifié en fonction de la valeur de la sinusoïde à cet instant.

Le pilotage se fait depuis le PC auquel l'Arduino est relié au moyen d'un script Python. Le protocole de communication est détaillé dans Échanges de données avec une Arduino. Le pilotage consiste à modifier la fréquence ou l'amplitude de la sinusoïde.

generateurPWM-3Phases.ino
#include "Arduino.h"
#define PHASE_A 11
#define PHASE_B 12
#define PHASE_C 13
#define NECHANT 128
#define SHIFT_ACCUM 25
#define GET_DATA 10
#define SET_DATA 11
#define DATA_0_SIZE 4 // fréquence, flottant 32 bits
#define DATA_1_SIZE 4 // amplitude, flottant 32 bits
uint8_t data_0[DATA_0_SIZE];
uint8_t data_1[DATA_1_SIZE];
bool data_0_ready = true;
bool data_0_request = false;
bool data_1_ready = true;
bool data_1_request = false;
float frequence = 1; // data_0
float amp = 0.5;
uint32_t period_pwm = 1000; // en microsecondes (10 kHz)

uint32_t icr;
uint32_t table_onde[NECHANT];
uint32_t indexA,indexB,indexC;
uint32_t accum1,accum2,accum3,increm;
uint16_t diviseur[6] = {0,1,8,64,256,1024};            
                 

La fonction suivante programme le Timer 1 avec une période en microsecondes (période de la porteuse). Une interruption est déclenchée lorsque le compteur atteint sa valeur maximale (ICR1).

void init_pwm_timer1(uint32_t period) {
    char clockBits;
    TCCR1A = 0;
    TCCR1A |= (1 << COM1A1); //Clear OCnA/OCnB/OCnC on compare match, set OCnA/OCnB/OCnC at BOTTOM (non-inverting mode)
    TCCR1A |= (1 << COM1B1);
    TCCR1A |= (1 << COM1C1);
    TCCR1B = 1 << WGM13; // phase and frequency correct pwm mode, top = ICR1
    int d = 1;
    icr = (F_CPU/1000000*period/2);
    while ((icr>0xFFFF)&&(d<6)) { // choix du diviseur d'horloge
        d++;
        icr = (F_CPU/1000000*period/2/diviseur[d]);
   } 
   clockBits = d;
   ICR1 = icr; // valeur maximale du compteur
   TIMSK1 = 1 << TOIE1; // overflow interrupt enable
   sei(); // activation des interruptions
   TCNT1 = 0; // mise à zéro du compteur
   TCCR1B |= clockBits; // déclenchement du compteur
}
             
                 

Voici le gestionnaire d'interruption, appelé à chaque cycle (lorsque TNCT1=ICR1). Les trois accumulateurs de phase sont incrémentés puis on actualise les valeurs de OCR1A, OCR1B et OCR1C avec la valeur correspondante dans la table d'onde.

ISR(TIMER1_OVF_vect) { // Timer 1 Overflow interrupt
  accum1 += increm;
  accum2 += increm;
  accum3 += increm;
  indexA = accum1 >> SHIFT_ACCUM;
  OCR1A = table_onde[indexA];
  indexB = accum2 >> SHIFT_ACCUM;
  OCR1B = table_onde[indexB];
  indexC = accum3 >> SHIFT_ACCUM;
  OCR1C = table_onde[indexC];
}
                 

Voici la foncton qui remplit la table d'onde pour une amplitude donnée (comprise entre 0 et 1) :

void set_sinus_table() {
  int i;
  float dt = 2*3.1415926/NECHANT;
  for(i=0; i<NECHANT; i++) {
    table_onde[i] = icr*(amp*sin(i*dt)+amp)*0.5;
  }  
}           
                 

La fonction suivante initialise l'incrément des accumulateurs de phase, en fonction de la fréquence de la sinusoïde et de celle du signal MLI.

void set_frequence() {
  increm = (uint32_t) (((float)(0xFFFFFFFF))*((float)(frequence)*1e-6*(float)(period_pwm))); // incrément de l'accumulateur de phase
}            
                 

Voici la fonction setup. Le Timer 1 est déclenché puis la table est remplie (dans cet ordre car il faut disposer de la valeur de ICR pour remplir la table). Les accumulateurs de phase sont initialisés de telle sorte qu'il y ait une différence de 120 degrés entre B et A et entre C et B.

void setup() {
  Serial.begin(115200);
  Serial.setTimeout(0);
  char c = 0;
  Serial.write(c);
  c = 255;
  Serial.write(c);
  c = 0;
  Serial.write(c);
  pinMode(PHASE_A,OUTPUT);
  pinMode(PHASE_B,OUTPUT);
  pinMode(PHASE_C,OUTPUT);
  pinMode(ENA,OUTPUT);
  digitalWrite(ENA,HIGH);
  pinMode(ENB,OUTPUT);
  digitalWrite(ENB,HIGH);
  pinMode(7,OUTPUT);
  digitalWrite(7,LOW);
  init_pwm_timer1(period_pwm);
  set_sinus_table();
  accum1 = 0;
  accum2 = ((uint32_t)(NECHANT * 0.3333333)) << SHIFT_ACCUM;
  accum3 = ((uint32_t)(NECHANT * 0.6666666)) << SHIFT_ACCUM;
  set_frequence();

}
        

La fonction set_data modifie la fréquence ou l'amplitude suite à une demande provenant du PC. La modification de la fréquence est très rapide car elle consiste simplement à modifier la valeur de l'incrément des accumulateurs de phase (la fréquence d'incrémentation est celle du signal MLI). La modification de l'amplitude est plus longue car elle nécessite le calcul complet de la table.

void set_data() {
  char n;
  while (Serial.available()<1) {};
  n = Serial.read();
  if (n==0) {
    while (Serial.available()<DATA_0_SIZE) {};
    Serial.readBytes(data_0,DATA_0_SIZE);
    memcpy(&frequence,data_0,DATA_0_SIZE);
    set_frequence();
  }
  else if (n==1) {
    while (Serial.available()<DATA_1_SIZE) {};
    Serial.readBytes(data_1,DATA_1_SIZE);
    memcpy(&amp,data_1,DATA_1_SIZE);
    set_sinus_table();
  }
}                
                

Les fonctions get_data et send_data permettent au PC d'obtenir les valeurs de la fréquence et de l'amplitude en cours, ce qui peut être utile si ces valeurs son modifiées par l'Arduino d'une autre manière que par la demande du PC.

void get_data() {
  char n;
  while (Serial.available()<1) {};
  n = Serial.read();
  if (n==0) {
    data_0_request = true;
    memcpy(data_0,&frequence,DATA_0_SIZE);
  }
  if (n==1) {
    data_1_request = true;
    memcpy(data_1,&amp,DATA_1_SIZE);
  }
}
void send_data() {
  if ((data_0_ready)&&(data_0_request)) {
      data_0_ready = false;
      data_0_request = false;
      Serial.write(data_0,DATA_0_SIZE);
  }
  if ((data_1_ready)&&(data_1_request)) {
      data_1_ready = false;
      data_1_request = false;
      Serial.write(data_1,DATA_1_SIZE);
  }
}
               
                

La fonction read_serial lit le port série pour détecter une éventuelle demande provenant du PC.

void read_serial() {
   char com;
   if (Serial.available()>0) {
        com = Serial.read();
        if (com==GET_DATA) get_data();
        else if (com==SET_DATA) set_data();
   }
}	           
                

Voici la fonction loop :

void loop() {
  read_serial();
  send_data();
}             
                  

La classe Python qui permet de gérer les communications est dans le fichier Arduino.py. L'utilisation de cette classe est décrite dans Échanges de données avec un Arduino.

Voici un exemple simple de script de pilotage :

pilotageOnduleur.py
import numpy as np
from Arduino import Arduino
ard = Arduino('COM4',[4,4])
amp = 0.95
ard.write_float(1,amp)
freq = 5.0
ard.write_float(0,freq)
while True:
    r = input("Frequence (Hz), amplitude (0 à 1) ?")
    if r=='n': break
    if ',' in r:
        v = r.split(',')
        f = float(v[0])
        a = float(v[1])
        ard.write_float(0,f)
        ard.write_float(1,a)
    else:
        f = float(r)
        ard.write_float(0,f)           
                

Après établissement de la communication avec l'Arduino, des valeurs de l'amplitude et de la fréquence sont attribuées. Dans la boucle, l'utilisateur peut modifier la valeur de la fréquence, ou bien la fréquence et l'amplitude (séparées par une virgule), ou bien entrer 'n' s'il souhaite terminer le pilotage.

Pour vérifier le bon fonctionnement avec un oscilloscope à deux voies, on branche sur deux des sorties (par exemple D11 et D12) deux filtres RC passe-bas de fréquence de coupure 106 Hz. Voici les tensions de ces deux sorties filtrées pour une fréquence de porteuse de 10 kHz et une modulation à 10 Hz avec l'amplitude maximale :

[t,VA,VB] = np.loadtxt("signauxAB.txt",unpack=True,skiprows=1)
figure(figsize=(12,6))
plot(t,VA,"r-",label=r"$V_A$")
plot(t,VB,"b-",label=r"$V_B$")
grid()
xlabel("t (s)")
ylabel("Volts")
ylim(0,5)
legend(loc="upper right")
                   
fig5fig5.pdf

Voici ces tensions pour une amplitude de modulation deux fois moindre, qui permettra de diviser par deux l'intensité du courant dans les bobines :

[t,VA,VB] = np.loadtxt("signauxAB-0,5.txt",unpack=True,skiprows=1)
figure(figsize=(12,6))
plot(t,VA,"r-",label=r"$V_A$")
plot(t,VB,"b-",label=r"$V_B$")
grid()
xlabel("t (s)")
ylabel("Volts")
ylim(0,5)
legend(loc="upper right")
                   
fig6fig6.pdf

4. Réalisation du convertisseur

Le convertisseur peut être réalisé avec deux ponts en H (de préférence à transistors MOSFET). Il faut cependant que les deux bras de pont de chaque pont soient commandables séparément, ce qui est rarement le cas sur les ponts en H vendus pour les moteurs à courant continu. Nous réalisons le convertisseur au moyen de deux circuits intégrés ST L6203. le LT6203 est un pont en H à transistors DMOS (un type particulier de MOSFET) acceptant une tension jusqu'à 48 V et pouvant délivrer un courant continu jusqu'à 4 A. La fréquence de découpage peut aller jusqu'à 100 kHz (il est rare que les ponts pour moteur puisse atteindre une fréquence aussi élevée). Voici le schéma simplifié du L6203 :

L6203

Lorsque ENABLE=1, la commande IN1 permet de piloter le bras de pont 1, dont la sortie est OUT1. La commande IN2 permet de piloter le bras de pont 2, dont la sortie est OUT2. Pour notre usage, OUT1 est la sortie A, OUT2 est la sortie B et nous devons bien-sûr utiliser un bras de pont du second pont pour la sortie C. Les deux transistors d'un bras de pont sont commandés par un seul signal. L'électronique de commande interne du L6203 tient compte du temps de commutation des transistors et fait en sorte que les deux transistors ne soient jamais conducteurs en même temps (ce qui augmenterait les pertes dans le pont). Ce pont comporte bien deux commandes différentes pour les deux bras de pont, ce qui est indispensable pour réaliser l'onduleur triphasé (beaucoup de ponts vendus pour les moteurs n'ont qu'une seule commande). Notons que si ENABLE=0, les transistors sont tous ouverts quels que soient les états de IN1 et IN2. Lorsqu'on utilise ce pont pour piloter un moteur à courant continu (avec balais), le moteur est branché entre OUT1 et OUT2, les commandes IN1 et IN2 servent à déterminer le sens de rotation du moteur et le signal MLI est envoyé sur ENABLE.

La sortie SENSE peut être reliée directement à la masse ou bien reliée à la masse par une résistance de puissance (non bobinée) de faible valeur (par ex. 0,2 ohms) afin de mesurer le courant circulant dans le pont. Pour l'onduleur triphasé, cette mesure a peu d'intérêt donc nous relions SENSE à la masse. Il faut noter que l'alimentation du L6203 se fait uniquement par l'alimentation de puissance qui délivrera le courant aux bobines et qu'il n'est donc pas nécessaire d'ajouter une alimentation pour la partie logique.

L'utilisation du LT6203 est très simple. Les seuls composants externes nécessaires sont les deux condensateurs notés CBOOT1 et CBOOT2 (Bootstrap capacitors), qui servent pour la polarisation du transistor supérieur de chaque bras de pont. La capacité de ces deux condensateurs doit être supérieure à 10 nF.

Nous utilisons le L6203 en boitier Multiwatt11 (montage traversant). L'ajout d'un radiateur est indispensable si l'on souhaite alimenter des bobines avec un courant de 3 ou 4 A. Le L6203 possède un circuit de détection d'un échauffement excessif, qui coupe son fonctionnement si la température dépasse 150 degrés.

Voici le schéma du circuit comportant les deux L6203, le branchement avec les sorties de l'Arduino MEGA, et le branchement des trois phases du moteur triphasé (ou des trois bobines montées en étoile).

onduleur3phasesL6203.svgFigure pleine page

Les deux ponts (L6203) sont notés X et Y. Les sorties D11 et D12 de l'Arduino pilotent les deux bras de pont du pont X (phases A et B). La sortie D13 pilote un bras de pont du pont Y (phase C). Pour activer les ponts, il faut bien sûr ajouter les lignes suivantes au code de l'Arduino (dans la fonction setup):

#define ENAX 7
#define ENAY 8
#define INY2 9 
pinMode(ENAX,OUTPUT)
digitalWrite(ENAX,HIGH)
pinMode(ENAY,OUTPUT)
digitalWrite(ENAY,HIGH)
pinMode(INY2,OUTPUT)
digitalWrite(INY2,LOW) // fixer l'état du bras de pont non utilisé
             
                        

L'alimentation de puissance est représentée par la source de tension Vs. En réalité, la tension délivrée par l'alimentation est susceptible de subir des ondulations au cours du découpage donc l'ajout d'un gros condensateur de filtrage est nécessaire. La tension d'alimentation minimale est d'environ 12 V et la tension maximale est de 48 V. Le courant maximal dans les bobines dépend à la fois de cette tension d'alimentation et du rapport cyclique maximal de la sinusoïde programmée.

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