TP 2 — PWM et couleurs

Objectifs

  • comprendre le principe de la modulation de largeur d’impulsion (PWM) ;

  • régler un timer pour produire un signal PWM, et faire varier la luminosité d’une LED en fondu ;

  • rencontrer un défaut de conception réel de la carte : une couleur branchée sur une broche sans timer ;

  • le contourner par un PWM logiciel, puis mélanger les trois couleurs.

Durée indicative : une à deux séances de 3 h. Prérequis : TP 1 — Allumer une LED, lire un bouton.

Ce qu’il faut savoir

Le voyant RGB

La carte porte huit LED RGB, montées en parallèle : elles affichent toutes la même couleur. Chaque LED contient trois puces, rouge, verte et bleue, à anode commune reliée au +3,3 V. Chaque couleur passe par une résistance, puis les huit LED d’une même couleur sont reliées à la masse par un transistor MOSFET, commandé par une broche du microcontrôleur :

Figure made with TikZ

Le voyant RGB : chaque couleur est commutée côté masse par un transistor, commandé par une broche.

La broche à 1 rend le transistor passant : la couleur s’allume. C’est encore une commande active à l’état haut. Le transistor est nécessaire parce que huit LED ensemble demandent plus de courant qu’une broche ne peut en fournir.

Question

  1. Les trois résistances n’ont pas la même valeur. Pourquoi ? (Indice : à courant égal, les trois couleurs n’ont ni la même tension de seuil, ni la même luminosité perçue.)

Réponse à la question 1

1. Trois résistances différentes. Le courant dans une LED vaut I = (Valim − Vseuil) / R. Or la tension de seuil dépend de la couleur, c’est-à-dire du semi-conducteur :

Couleur

Seuil typique

Tension restante sur R

Courant par LED

rouge (AlGaInP)

≈ 2,0 V

3,3 − 2,0 = 1,3 V

1,3 / 330 ≈ 3,9 mA

vert (InGaN)

≈ 3,0 V

0,3 V

0,3 / 100 ≈ 3 mA

bleu (InGaN)

≈ 3,0 V

0,3 V

0,3 / 68 ≈ 4,4 mA

Avec la même résistance partout, le rouge recevrait beaucoup plus de courant que les deux autres et écraserait les mélanges. Les valeurs ont été choisies pour donner des courants voisins, puis ajustées à la luminosité perçue : l’œil est bien plus sensible au vert (vers 555 nm) qu’au bleu, si bien qu’à courant égal le vert paraît plus lumineux.

Remarque : pour le vert et le bleu, il ne reste que 0,3 V aux bornes de la résistance. Une petite variation du seuil d’une LED à l’autre (quelques dixièmes de volt) change donc beaucoup le courant : c’est la limite d’une alimentation en 3,3 V pour des LED bleues et vertes.

Huit LED en parallèle font environ 8 × 4 mA ≈ 30 mA par couleur : trop pour une broche du microcontrôleur, d’où le transistor, qui ne demande à la broche que de charger sa grille.

Le principe du PWM

Une broche numérique ne connaît que deux états, 0 et 1. Pour obtenir une luminosité intermédiaire, on allume et on éteint la LED très vite : si elle est allumée 30 % du temps, l’œil, trop lent pour suivre, perçoit une LED à environ 30 % de sa luminosité.

  • La période est la durée d’un cycle allumé-éteint ; la fréquence est son inverse. Au-dessus d’une centaine de hertz, l’œil ne voit plus le clignotement.

  • Le rapport cyclique est la part de la période où la sortie est à 1, de 0 % (éteint) à 100 % (allumé en permanence).

Figure made with TikZ

Le timer compte de 0 à ARR, puis recommence ; la sortie est à 1 tant que le compteur est sous CCR.

Un timer qui produit un PWM

Un timer compte au rythme de son horloge. Trois registres le règlent :

  • le prédiviseur (Prescaler, PSC) divise l’horloge : le compteur avance à fhorloge / (PSC + 1) ;

  • la période (Counter Period, ARR) : le compteur va de 0 à ARR, puis repart de 0 ;

  • la valeur de comparaison (Pulse, CCR) : la sortie est à 1 tant que le compteur est sous CCR, à 0 au-delà.

D’où :

fréquence du PWM = f_horloge / ((PSC + 1) × (ARR + 1))
rapport cyclique = CCR / (ARR + 1)

Avec l’horloge HSI à 64 MHz, PSC = 249 et ARR = 255, le compteur avance à 256 kHz et fait un tour en 256 pas : un PWM à 1 kHz, dont le rapport cyclique se règle de 0 à 255, comme une composante de couleur sur 8 bits.

Le timer TIM1 de la carte a ses voies 1 et 2 sur PE9 et PE11 : le rouge et le vert peuvent être commandés par un PWM « matériel », que le timer produit seul sans occuper le processeur.

Avertissement

N’activez pas la voie 4 de TIM1 : sur cette carte, sa broche PE14 est reliée à un bouton (SCREEN_LEFT) qui la met au +3,3 V. Une sortie PWM sur cette broche serait mise en court-circuit à chaque appui.

Étape 1 — Les huit couleurs en tout ou rien

  1. Créez un projet tp02_pwm comme au TP 1 (STM32H750VBTx, SYS → Debug : Serial Wire, horloge par défaut).

  2. Configurez PE9, PE11 et PC5 en GPIO_Output, labels RGB_RED, RGB_GREEN et RGB_BLUE.

  3. Faites défiler, une seconde chacune, les huit combinaisons possibles des trois couleurs : rouge, vert, bleu, jaune (rouge et vert), magenta, cyan, blanc, éteint.

Question

  1. Les mélanges (jaune, cyan, magenta, blanc) ont-ils la couleur attendue ? Le blanc est-il vraiment blanc ? Qu’en déduire sur l’équilibre des trois couleurs ?

Solution de l’étape 1

On numérote les huit combinaisons sur trois bits : bit 0 pour le rouge, bit 1 pour le vert, bit 2 pour le bleu. Une table donne l’ordre demandé :

static const uint8_t seq[8] = { 1u, 2u, 4u, 3u, 5u, 6u, 7u, 0u };
/*                     rouge vert bleu jaune magenta cyan blanc éteint */
static uint32_t i = 0u;

HAL_GPIO_WritePin(RGB_RED_GPIO_Port,   RGB_RED_Pin,   (seq[i] & 1u) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(RGB_GREEN_GPIO_Port, RGB_GREEN_Pin, (seq[i] & 2u) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(RGB_BLUE_GPIO_Port,  RGB_BLUE_Pin,  (seq[i] & 4u) ? GPIO_PIN_SET : GPIO_PIN_RESET);
i = (i + 1u) % 8u;
HAL_Delay(1000);

seq[i] & 2u isole le bit 1 : le résultat est non nul si le vert fait partie de la combinaison. Trois bits donnent exactement 2³ = 8 combinaisons : c’est le nombre de couleurs qu’on peut afficher sans PWM.

Réponse à la question 2

2. Les mélanges. Le jaune, le cyan et le magenta sont en général reconnaissables, mais le blanc tire vers une couleur (souvent le bleu ou le vert) : à pleine puissance, les trois composantes n’ont pas la même intensité perçue (question 1). Un blanc neutre demande de réduire les composantes trop fortes : impossible en tout ou rien, possible avec un rapport cyclique réglable — c’est l’objet de la suite du TP.

On voit aussi, de près, les trois puces séparées : le mélange ne se fait qu’à distance, ou derrière un diffuseur.

Étape 2 — Le rouge en fondu

  1. Dans CubeMX, cliquez sur PE9 et choisissez TIM1_CH1 (la broche quitte le mode GPIO).

  2. Dans Timers → TIM1, réglez Channel1 sur PWM Generation CH1. Dans Parameter Settings : Prescaler = 249, Counter Period = 255, Pulse = 0.

  3. Générez le code. Dans USER CODE BEGIN 2, démarrez le PWM :

    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
    
  4. Dans la boucle, faites varier la valeur de comparaison de 0 à 255 puis de 255 à 0, par exemple de 1 toutes les 4 ms :

    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, valeur);
    

Questions

  1. Vérifiez par le calcul la fréquence du PWM. Si vous avez un oscilloscope, mesurez-la sur la grille du transistor.

  2. Le fondu paraît-il régulier ? La luminosité semble-t-elle augmenter autant entre 10 et 20 qu’entre 200 et 210 ? Pourquoi ?

Solution de l’étape 2

Dans USER CODE BEGIN 2 (avant la boucle) :

HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

Dans la boucle :

static int32_t v  = 0;     /* rapport cyclique courant, 0 à 255 */
static int32_t dv = 1;     /* sens : +1 en montée, -1 en descente */

__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, (uint32_t)v);
v += dv;
if ((v >= 255) || (v <= 0)) { dv = -dv; }   /* demi-tour aux bornes */
HAL_Delay(4);

Un aller-retour fait 2 × 255 pas de 4 ms, soit environ 2 s.

Ce que fait le matériel. __HAL_TIM_SET_COMPARE écrit simplement dans le registre CCR1 du timer. À chaque pas du compteur CNT, le timer compare CNT à CCR1 et met la sortie à 1 tant que CNT < CCR1 (mode PWM 1). Tout se fait dans le timer : une fois lancé, le PWM tourne sans que le processeur intervienne.

Pourquoi HAL_TIM_PWM_Start est indispensable. TIM1 est un timer « avancé » (prévu pour la commande de moteurs). Ses sorties ne sont réellement connectées aux broches que si le bit MOE (Main Output Enable) du registre BDTR est à 1, une sécurité qui coupe toutes les sorties d’un coup. HAL_TIM_PWM_Start active la voie (CCER), positionne MOE, et lance le compteur (CR1.CEN). Écrire CCR1 sans cet appel ne produit rien sur la broche.

Pourquoi la nouvelle valeur ne crée pas de défaut. CCR1 est « préchargé » : la valeur écrite n’est prise en compte qu’au début de la période suivante. On ne coupe jamais une impulsion en plein milieu.

Réponses aux questions 3 et 4

3. La fréquence. Le compteur de TIM1 reçoit 64 MHz. Le prédiviseur divise par PSC + 1 = 250 : le compteur avance à 256 kHz. Il compte de 0 à ARR = 255, soit 256 pas par période :

f = 64 000 000 / (250 × 256) = 1 000 Hz       période : 1 ms

À l’oscilloscope sur la grille du transistor : un signal carré de 0 à 3,3 V, de période 1 ms, dont la largeur d’impulsion varie de 0 à 1 ms au rythme du fondu (3,9 µs par pas de rapport cyclique).

4. Un fondu irrégulier. Non : la luminosité semble monter très vite au début, puis presque plus du tout vers la fin. Le rapport cyclique, lui, augmente pourtant régulièrement. C’est l’œil qui n’est pas linéaire : sa sensation de luminosité suit à peu près le rapport des intensités, pas leur différence (loi de Weber-Fechner ; la clarté perçue varie environ comme la racine cubique de l’intensité). Passer de 10 à 20 double la lumière (+100 %) ; passer de 200 à 210 ne l’augmente que de 5 %.

Pour un fondu régulier à l’œil, on applique une correction gamma : on envoie au timer non pas v, mais environ 255 × (v/255)2,2. C’est un des exercices supplémentaires, et ce que font tous les écrans.

Étape 3 — Le vert, et le problème du bleu

  1. Faites de même pour le vert : PE11 en TIM1_CH2, Channel2 en PWM Generation CH2, et un fondu décalé de celui du rouge.

  2. Essayez maintenant le bleu : cliquez sur PC5 dans CubeMX et cherchez une fonction de timer.

Il n’y en a pas. Les fonctions d’une broche sont fixées par le microcontrôleur, et la fiche technique les liste dans son tableau des fonctions alternatives (alternate functions) : PC5 peut servir d’entrée analogique, de broche d’Ethernet ou de mémoire, mais aucun timer ne sort sur PC5. Au dessin de la carte, le bleu a été relié à cette broche sans le vérifier : le rouge et le vert ont un PWM matériel, le bleu n’en a pas. Sur une carte déjà fabriquée, ce genre d’erreur se rattrape par logiciel.

Question

  1. Dans la fiche technique du STM32H750 (tableau Alternate function), trouvez les broches qui portent les voies de TIM1. Laquelle aurait-il fallu choisir pour le bleu ?

Solution de l’étape 3

HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); dans USER CODE BEGIN 2, puis la même boucle qu’à l’étape 2 pour TIM_CHANNEL_2, avec une seconde variable. Pour le décalage, il suffit de partir d’une autre valeur (128 par exemple), ou de parcourir le fondu dans l’autre sens (dv = -1 au départ, à partir de 255) : les deux couleurs se croisent, et la LED passe par toutes les nuances de rouge, orange, jaune et vert.

Les deux voies partagent le même compteur : même fréquence, même période, seuls les registres CCR1 et CCR2 diffèrent. Un timer offre ainsi jusqu’à quatre PWM synchrones, de rapports cycliques indépendants.

Réponse à la question 5

5. Les broches de TIM1. Le tableau des fonctions alternatives de la fiche technique (Alternate function AF1 pour TIM1) donne, en boîtier LQFP100 :

Voie

Broches possibles

Usage sur la carte

TIM1_CH1

PA8, PE9

PA8 : I²C3 (centrale) ; PE9 : rouge

TIM1_CH2

PA9, PE11

PA9 : bouton SW2 ; PE11 : vert

TIM1_CH3

PA10, PE13

PA10 : bus Dynamixel ; PE13 : bouton de l’écran

TIM1_CH4

PA11, PE14

PA11 : bus Dynamixel ; PE14 : bouton de l’écran

Toutes les broches de TIM1 sont déjà prises. Il aurait fallu, au dessin de la carte, réserver PE13 (TIM1_CH3) au bleu, et mettre le bouton ailleurs : les trois couleurs auraient eu trois voies du même timer. D’autres timers (TIM8, TIM3…) offrent aussi des voies, mais pas sur PC5.

La leçon : choisir les broches en commençant par les fonctions rares (timers, bus, ADC), et seulement ensuite les simples entrées et sorties, qui vont n’importe où. CubeMX le permet : on assigne les fonctions, il propose les broches.

Étape 4 — Un PWM logiciel pour le bleu

Un PWM logiciel reproduit à la main ce que fait le timer : une interruption périodique fait avancer un compteur de 0 à 255, et le programme met la broche à 1 tant que le compteur est sous la valeur voulue.

Chaque pas coûte une interruption. Pour un PWM à 100 Hz sur 256 pas, il en faut 100 × 256 = 25 600 par seconde, une toutes les 39 µs : à 64 MHz, c’est 2 500 cycles d’horloge entre deux interruptions, assez pour que le reste du programme continue de tourner.

  1. Remettez PC5 en GPIO_Output (label RGB_BLUE).

  2. Activez TIM7 (Timers → TIM7, Activated), un timer simple sans sortie : Prescaler = 0, Counter Period = 2499, ce qui donne 64 MHz / 2 500 = 25,6 kHz. Dans NVIC Settings, cochez TIM7 global interrupt.

  3. Démarrez-le avec interruption, dans USER CODE BEGIN 2 :

    HAL_TIM_Base_Start_IT(&htim7);
    
  4. Écrivez la fonction appelée à chaque interruption du timer. Elle est prévue par la HAL ; il suffit de la définir, dans USER CODE BEGIN 4 :

    volatile uint8_t bleu = 0;   /* 0 à 255, réglé par la boucle principale */
    
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
        if (htim->Instance == TIM7)
        {
            /* à compléter : avancer un compteur de 0 à 255, et mettre
             * RGB_BLUE à 1 tant que le compteur est sous « bleu » */
        }
    }
    
  5. Faites un fondu du bleu, depuis la boucle principale, en changeant la variable bleu.

Questions

  1. Pourquoi la variable bleu est-elle déclarée volatile ?

  2. Pourquoi ne pas faire aussi le PWM logiciel à 1 kHz, comme le rouge et le vert ? Combien d’interruptions par seconde faudrait-il, et combien de cycles resterait-il entre deux ?

  3. Regardez le bleu de côté, en bougeant la carte : voit-on un scintillement que le rouge n’a pas ? Pourquoi ?

Solution de l’étape 4

Dans USER CODE BEGIN 2 : HAL_TIM_Base_Start_IT(&htim7);. Puis, dans USER CODE BEGIN 4 :

volatile uint8_t bleu = 0u;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    static uint8_t pas = 0u;          /* compteur du PWM logiciel */

    if (htim->Instance == TIM7)
    {
        if (pas < bleu)
        {
            RGB_BLUE_GPIO_Port->BSRR = RGB_BLUE_Pin;                 /* 1 */
        }
        else
        {
            RGB_BLUE_GPIO_Port->BSRR = (uint32_t)RGB_BLUE_Pin << 16; /* 0 */
        }
        pas++;                        /* uint8_t : repasse seul de 255 à 0 */
    }
}

et dans la boucle, le même fondu qu’à l’étape 2, sur la variable bleu.

Le chemin d’une interruption. TIM7 compte à 64 MHz jusqu’à 2 499 : il déborde toutes les 2 500 / 64 MHz = 39 µs. Le débordement lève le drapeau UIF ; le NVIC interrompt le programme et appelle TIM7_IRQHandler (fichier stm32h7xx_it.c), qui appelle HAL_TIM_IRQHandler, qui efface le drapeau et appelle enfin HAL_TIM_PeriodElapsedCallback. Cette fonction est déclarée « faible » (__weak) dans la HAL : en la définissant, on remplace la version vide.

Le test htim->Instance. Le même rappel sert à tous les timers qui génèrent une interruption de débordement. Le test garantit qu’on ne réagit qu’à TIM7.

BSRR plutôt que HAL_GPIO_WritePin. L’interruption revient 25 600 fois par seconde : chaque cycle compte. Écrire dans BSRR met la broche à 1 (bits 0 à 15) ou à 0 (bits 16 à 31) en une seule instruction, sans appel de fonction ni vérification de paramètres.

Le compteur sur 8 bits. Déclaré uint8_t, pas revient de lui-même de 255 à 0 : 256 pas par période, exactement comme ARR = 255 pour le rouge.

Réponses aux questions 6 à 8

6. volatile. La variable bleu est écrite par la boucle principale et lue par l’interruption. Le compilateur, lui, analyse chaque fonction séparément et ne sait pas qu’une interruption peut s’exécuter entre deux instructions. Sans volatile, il a le droit d’optimiser : garder la variable dans un registre du processeur au lieu de la relire en mémoire, ou supprimer une écriture qu’il juge inutile. volatile l’oblige à faire chaque lecture et chaque écriture en mémoire, à l’endroit écrit. Le défaut ne se voit souvent qu’avec l’optimisation activée (-O2) : un programme qui marche en mise au point, mais plus en version finale.

7. Un PWM logiciel à 1 kHz. Il faudrait 1 000 × 256 = 256 000 interruptions par seconde, une toutes les 3,9 µs, soit 250 cycles à 64 MHz. Or entrer dans une interruption et en sortir coûte déjà une vingtaine de cycles au cœur, et le chemin de la HAL (HAL_TIM_IRQHandler, tests des drapeaux, appel du rappel) en ajoute une centaine. Le processeur passerait plus de la moitié de son temps dans l’interruption, et le reste du programme ralentirait d’autant. C’est le prix du PWM logiciel : sa fréquence et sa résolution sont limitées par la charge du processeur, alors que le PWM matériel est gratuit.

8. Le scintillement. Oui, en bougeant la carte ou en balayant le regard, on voit le bleu se décomposer en traits pointillés : à 100 Hz, la LED s’allume et s’éteint toutes les 10 ms, et quand l’image se déplace sur la rétine, les éclairs successifs tombent à des endroits différents (effet stroboscopique). Le rouge, à 1 kHz, laisse des traits dix fois plus serrés, qu’on ne distingue plus. Un téléphone qui filme la LED le montre aussi, par des bandes sur l’image.

Étape 5 — Toutes les couleurs

Avec trois composantes de 0 à 255, la LED peut afficher 256 × 256 × 256, soit plus de 16 millions de couleurs.

  1. Écrivez une fonction rgb_set(uint8_t r, uint8_t g, uint8_t b) qui règle les trois composantes : deux par le timer TIM1, la troisième par la variable du PWM logiciel.

  2. Faites défiler les couleurs de l”arc-en-ciel en fondu : rouge, jaune, vert, cyan, bleu, magenta, puis retour au rouge. Entre deux couleurs voisines, une seule composante change à la fois.

Solution de l’étape 5
static void rgb_set(uint8_t r, uint8_t g, uint8_t b)
{
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, r);   /* rouge : TIM1 */
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, g);   /* vert : TIM1 */
    bleu = b;                                         /* bleu : logiciel */
}

/* Roue des couleurs : pos de 0 à 1535, six segments de 256 pas.
 * Dans chaque segment, une seule composante change. */
static void rgb_wheel(uint32_t pos)
{
    uint8_t up   = (uint8_t)(pos & 0xFFu);   /* 0 → 255 dans le segment */
    uint8_t down = (uint8_t)(255u - up);     /* 255 → 0 */

    switch ((pos >> 8) % 6u)                 /* numéro du segment */
    {
    case 0:  rgb_set(255u, up,   0u);   break;   /* rouge → jaune   */
    case 1:  rgb_set(down, 255u, 0u);   break;   /* jaune → vert    */
    case 2:  rgb_set(0u,   255u, up);   break;   /* vert → cyan     */
    case 3:  rgb_set(0u,   down, 255u); break;   /* cyan → bleu     */
    case 4:  rgb_set(up,   0u,   255u); break;   /* bleu → magenta  */
    default: rgb_set(255u, 0u,   down); break;   /* magenta → rouge */
    }
}

Dans la boucle :

static uint32_t pos = 0u;

rgb_wheel(pos);
pos = (pos + 1u) % 1536u;
HAL_Delay(4);                 /* 1 536 × 4 ms ≈ 6 s par tour */

Le découpage. Les six couleurs de l’arc-en-ciel sont les sommets d’un hexagone : à chaque sommet, une ou deux composantes sont à 255. Entre deux sommets voisins, une seule composante monte ou descend. pos >> 8 (la division par 256) donne le numéro du segment, pos & 0xFF (le reste) la position dans le segment. C’est le principe du modèle de couleur TSV (teinte, saturation, valeur), où la teinte fait le tour de cette roue.

Tout le code de la correction

main_user_code.c — compilé sans avertissement contre la HAL du STM32H7.

Ce n’est pas un main.c complet : ce sont les extraits à placer dans les blocs USER CODE du main.c généré par CubeMX, chacun sous le commentaire qui indique son bloc. La boucle while (1) contient une variante par étape, encadrée par #if 0 / #if 1 : n’en activer qu’une à la fois. La configuration CubeMX attendue est rappelée en tête du fichier, et les réponses aux questions à la fin.

Exercices supplémentaires

Ces exercices n’ont pas de correction.

  • Respiration : une seule couleur qui s’allume et s’éteint lentement, comme une veille, avec une courbe agréable à l’œil (essayez une progression au carré plutôt que linéaire).

  • Correction gamma : rendez le fondu de l’étape 2 régulier à l’œil, à l’aide d’une table de 256 valeurs calculée à l’avance.

  • Couleur au bouton : SW1, SW2 et SW3 augmentent chacun une composante, SW4 remet tout à zéro.

  • Équilibre du blanc : trouvez les trois valeurs qui donnent le blanc le plus neutre, et comparez-les aux résistances de la question 1.

  • Plus de PWM logiciel : faites passer le rouge et le vert aussi en PWM logiciel, et mesurez le temps que le processeur passe dans l’interruption (en basculant une LED de façade à l’entrée et à la sortie, et en la regardant à l’oscilloscope).

Après le TP

Pour rendre la carte au robot, voir Remettre la carte en état RatBot.