TRAVAUX PRATIQUES — SYSTÈMES EMBARQUÉS STM32

ADC 12-bit · Potentiomètre
→ Alarme LED Rouge

// STM32F411CEU6 Black Pill · STM32CubeIDE · HAL Library
MCU · STM32F411CEU6
ADC · 12 bits · 3 modes
ENTRÉE · PA1 – ADC1_IN1
ALARME · LED PC13
DURÉE · 3H
SIMULATEUR ADC EN TEMPS RÉEL
2047
Tension : 1.65 V
Binaire : 011111111111
Hex : 0x7FF
◀ Tourner le potentiomètre ▶
LED Rouge : OFF
Seuil alarme : ADC > (0–4095)
// INTRODUCTION

Objectifs du TP

Maîtriser l'ADC 12 bits du STM32F411 en 3 modes : Polling, Interruption, DMA.

Ce que vous allez apprendre
Compétences visées

📐 THÉORIE

  • Principe de la conversion A/N
  • Résolution 12 bits, LSB, pleine échelle
  • Formule tension ↔ valeur numérique
  • Clock ADC et temps de conversion

⚙️ MODES ADC

  • Mode Polling (bloquant)
  • Mode Interruption (non-bloquant)
  • Mode DMA (automatique)
  • Comparaison des 3 approches

🔴 APPLICATION

  • Lire un potentiomètre 10kΩ
  • Convertir valeur ADC → Tension
  • Seuil personnalisable en code
  • Alarme LED + debug UART
MATÉRIEL REQUIS
Carte STM32F4 Black Pill (STM32F411CEU6) · Potentiomètre 10 kΩ · LED rouge + résistance 220Ω · Câble ST-Link V2 · PC avec STM32CubeIDE 1.12+
// PARTIE 1 — RAPPELS THÉORIQUES

L'ADC 12 bits du STM32F411

Comprendre la conversion analogique-numérique avant de coder.

T1
Principe de la Conversion Analogique-Numérique
Signal continu → valeur discrète 12 bits

L'ADC (Analog-to-Digital Converter) transforme une tension analogique continue en un nombre entier discret. Le STM32F411 dispose d'un ADC 12 bits successifs approximations (SAR).

Signal Analogique (continu) ADC 12-bit 082016382457 204832762867 30721500 Valeur Numérique 12 bits (0 à 4095) V (0–3.3V) ADC (0–4095)
FIG. 1 — Conversion analogique → numérique : signal continu discretisé en valeurs 12 bits

Caractéristiques STM32F411 ADC

ParamètreValeur
Résolution12 bits (défaut)
Niveaux discrets4096 (2¹²)
Tension référenceVREF+ = 3.3 V
LSB (1 pas)0.806 mV
Canaux16 canaux externes
Vitesse max2.4 MSPS
TypeSAR (Successive Approx.)
ModesSingle / Cont. / Scan

Formule de conversion

TENSION → VALEUR ADC
ADC_val = (V_in / 3.3) × 4095
Ex : V=1.65V → ADC = (1.65/3.3)×4095 = 2047
VALEUR ADC → TENSION
V_in = (ADC_val / 4095.0) × 3.3
Ex : ADC=2047 → V = (2047/4095)×3.3 = 1.65 V
LSB (1 pas minimum)
LSB = VREF / (2¹² - 1) = 3.3 / 4095
LSB = 0.000806 V ≈ 0.8 mV
T2
Comparaison des 3 modes ADC
Polling vs Interrupt vs DMA
Critère POLLING INTERRUPT DMA
PrincipeCPU attend la finIRQ signale la finTransfert auto mémoire
CPU bloqué ?✗ OUI✓ NON✓ NON
ComplexitéSimpleMoyennePlus complexe
PerformanceFaibleBonneExcellente
Usage typiqueDébutant / testSystème réactifMulti-canaux / haute freq.
Fonction HALHAL_ADC_PollForConversionHAL_ADC_Start_ITHAL_ADC_Start_DMA
CallbackAucunHAL_ADC_ConvCpltCallbackHAL_ADC_ConvCpltCallback
CONSEIL PÉDAGOGIQUE
Commencer par le Polling pour comprendre le principe, puis progresser vers Interrupt et DMA pour les systèmes temps réel.
// PARTIE 2 — CRÉATION DU PROJET

Nouveau Projet dans STM32CubeIDE

01
Créer un nouveau projet STM32
File → New → STM32 Project
STM32CubeIDE — Eclipse IDE
File
Edit
New
Project
Run
Debug
📁 New → STM32 Project...
BOARD SELECTOR / MCU SELECTOR
Series:
STM32F4
Part Number:
STM32F411CEU6
✓ STM32F411CEU6 — 512KB Flash, 128KB RAM, 100MHz, LQFP48
  1. Ouvrir STM32CubeIDE → FileNewSTM32 Project
  2. Dans l'onglet MCU/MPU Selector, chercher : STM32F411CEU6
  3. Sélectionner le MCU dans la liste → Cliquer Next
  4. Nom du projet : ADC_BlackPill → Language : CFinish
  5. Répondre Yes à "Open associated perspective?"
RÉSULTAT
Le fichier ADC_BlackPill.ioc s'ouvre dans STM32CubeMX intégré. Le pinout du STM32F411CEU6 s'affiche.
02
Configurer l'horloge système
Clock Configuration — 100 MHz
IMPORTANT
La fréquence de l'ADC dépend de l'horloge APB2. Configurer correctement le clock tree pour obtenir une fréquence ADC ≤ 36 MHz.
HSE / HSI25 MHz PLL×8 SYSCLK100MHz AHB÷1=100MHz APB2÷2=50MHz ADC CLK25MHz ÷2 prescaler
FIG. 2 — Clock tree STM32F411 : SYSCLK 100MHz → APB2 50MHz → ADC CLK 25MHz
  1. Dans CubeMX → onglet Clock Configuration
  2. Input Frequency → 25 MHz (HSE externe)
  3. PLL Source → HSE · PLLM=25 · PLLN=200 · PLLP=2
  4. System Clock Mux → PLLCLK
  5. HCLK → 100 MHz → Appuyer Enter pour valider
  6. APB2 Prescaler → /2 → ADC Prescaler → /2 (= 25 MHz ✓)
// PARTIE 3 — CONFIGURATION CUBEMX

Configurer ADC1 + GPIO via CubeMX

03
Configurer la broche PA1 en ADC
Pinout & Configuration → ADC1
BLACK PILL STM32F411CEU6 STM32 F411CEU6 PA1 PC13 ADC1_IN1 Analog Input GPIO_Output LED Rouge POT 10 kΩ LED + 220Ω
FIG. 3 — Black Pill : PA1 (ADC1_IN1) ← Potentiomètre · PC13 (GPIO OUT) → LED Rouge
  1. Dans CubeMX → onglet Pinout & Configuration
  2. Cliquer sur la broche PA1 sur le schéma → sélectionner ADC1_IN1
  3. Cliquer sur la broche PC13 → sélectionner GPIO_Output
  4. Dans le panneau gauche → AnalogADC1

Paramètres ADC1 à configurer

ParamètreValeurExplication
ModeSingle EndedMesure par rapport à GND
ChannelChannel 1 (PA1)Entrée analogique PA1
Resolution12 bits0 à 4095
Continuous Conv. ModeENABLEConversion continue en boucle
Sampling Time480 CyclesTemps d'échantillonnage (le plus lent = plus précis)
Data AlignmentRight alignmentBits de poids fort à gauche
Scan Conversion ModeDISABLE1 seul canal pour ce TP
End Of ConversionEOC flag at end of single conv.Drapeau fin de conversion

Configurer l'UART pour le debug

  1. Dans CubeMX → ConnectivityUSART1
  2. Mode → Asynchronous
  3. Baud Rate → 115200 · Word Length → 8 bits · Stop Bits → 1
GÉNÉRER LE CODE
ProjectGenerate Code ou cliquer l'icône ⚙️
STM32CubeIDE génère automatiquement le code d'initialisation HAL.
// PARTIE 4 — MODE POLLING

ADC en Mode Polling — Bloquant

Le CPU attend activement la fin de la conversion. Simple, idéal pour débuter.

04
Code ADC Mode Polling
main.c — dans la boucle while(1)
MODE : POLLING
HAL_ADC_Start() HAL_ADC_PollForConversion() ⏳ CPU bloqué ici ! HAL_ADC_GetValue() CalculTension AlarmeLED ? Boucle while(1)
FIG. 4 — Flux d'exécution Mode Polling : CPU bloqué pendant PollForConversion()
C — main.c · Mode Polling
/* ============================================================
   TP ADC Black Pill — Mode POLLING
   MCU : STM32F411CEU6
   PA1 → ADC1_CH1 (Potentiomètre)
   PC13 → LED Rouge (alarme)
   ============================================================ */

#include "main.h"
#include <stdio.h>
#include <string.h>

/* ─── Variables globales ─── */
ADC_HandleTypeDef hadc1;
UART_HandleTypeDef huart1;

uint32_t adc_value   = 0;      /* Valeur brute ADC : 0 à 4095       */
float    voltage     = 0.0f;   /* Tension calculée en Volts          */
char     uart_buf[64];          /* Buffer pour les messages UART      */

/* ─── SEUIL D'ALARME PERSONNALISABLE ─── */
#define ALARM_THRESHOLD   2048   /* Modifier ici : 0 à 4095            */
#define VREF              3.3f   /* Tension de référence ADC (Volts)   */

/* ─── Prototypes ─── */
void SystemClock_Config(void);
static void MX_ADC1_Init(void);
static void MX_USART1_UART_Init(void);
static void MX_GPIO_Init(void);

/* ─── Redirection printf → UART ─── */
int __io_putchar(int ch) {
    HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100);
    return ch;
}

int main(void) {

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init();
    MX_USART1_UART_Init();

    printf("=== TP ADC Black Pill — Mode Polling ===\r\n");

    while (1) {

        /* Étape 1 : Démarrer la conversion ADC */
        HAL_ADC_Start(&hadc1);

        /* Étape 2 : Attendre la fin de conversion (timeout 100 ms) */
        if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) {

            /* Étape 3 : Lire la valeur numérique 12 bits */
            adc_value = HAL_ADC_GetValue(&hadc1);

            /* Étape 4 : Convertir en tension réelle */
            voltage = ((float)adc_value / 4095.0f) * VREF;

            /* Étape 5 : Logique d'alarme LED rouge */
            if (adc_value > ALARM_THRESHOLD) {
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); /* LED ON (actif bas) */
            } else {
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);   /* LED OFF */
            }

            /* Étape 6 : Afficher via UART */
            printf("ADC=%4lu | V=%.3f V | Seuil=%d | LED=%s\r\n",
                   adc_value, voltage, ALARM_THRESHOLD,
                   (adc_value > ALARM_THRESHOLD) ? "ON" : "OFF");
        }

        HAL_ADC_Stop(&hadc1);
        HAL_Delay(200);  /* 200 ms entre chaque mesure */
    }
}
AVANTAGES POLLING
✓ Code simple et linéaire
✓ Facile à déboguer
✓ Idéal pour apprendre
INCONVÉNIENTS POLLING
✗ CPU bloqué pendant la conversion
✗ Impossible de faire autre chose
✗ Non adapté aux systèmes temps réel
// PARTIE 5 — MODE INTERRUPTION

ADC en Mode Interruption — Non Bloquant

Le CPU est libéré. La conversion termine → une IRQ appelle automatiquement le callback.

05
Code ADC Mode Interrupt (IT)
HAL_ADC_Start_IT + Callback
MODE : INTERRUPT
HAL_ADC_Start_IT() CPU libre !Autres tâches... ADC IRQ ! ConvCpltCallback() GetValue()+ Alarme Re-lance automatiquement
FIG. 5 — Mode Interrupt : le CPU est libre, l'IRQ déclenche le callback à chaque fin de conversion
C — main.c · Mode Interrupt
/* ============================================================
   TP ADC Black Pill — Mode INTERRUPTION
   ============================================================ */

#include "main.h"
#include <stdio.h>

ADC_HandleTypeDef hadc1;
UART_HandleTypeDef huart1;

/* ─── Variables partagées (volatile = accès ISR + main) ─── */
volatile uint32_t adc_value    = 0;
volatile uint8_t  conv_complete = 0;   /* Drapeau : conversion terminée */

float voltage = 0.0f;

#define ALARM_THRESHOLD   2048   /* ← Modifier le seuil ici */
#define VREF              3.3f

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init();
    MX_USART1_UART_Init();

    printf("=== TP ADC Black Pill — Mode Interrupt ===\r\n");

    /* Démarrer la première conversion en mode interruption */
    HAL_ADC_Start_IT(&hadc1);   /* Lance l'ADC — non bloquant ! */

    while (1) {

        /* Le CPU peut faire autre chose ici... */
        /* (calculs, communication, gestion d'écran, etc.) */

        /* Vérifier si une conversion est prête */
        if (conv_complete) {
            conv_complete = 0;

            voltage = ((float)adc_value / 4095.0f) * VREF;

            if (adc_value > ALARM_THRESHOLD) {
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
            } else {
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
            }

            printf("[IT] ADC=%4lu | V=%.3f V | LED=%s\r\n",
                   adc_value, voltage,
                   (adc_value > ALARM_THRESHOLD) ? "ON" : "OFF");
        }

        HAL_Delay(100);
    }
}

/* ─── CALLBACK : appelé automatiquement par l'IRQ ADC ─── */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        /* Lire la valeur ADC depuis l'ISR */
        adc_value    = HAL_ADC_GetValue(hadc);
        conv_complete = 1;                   /* Signaler au main() */

        /* Relancer la prochaine conversion */
        HAL_ADC_Start_IT(hadc);
    }
}
POINT CLEF — volatile
Les variables partagées entre le main() et l'ISR doivent être déclarées volatile. Sans cela, le compilateur peut les optimiser incorrectement et le programme aura un comportement indéfini.
// PARTIE 6 — MODE DMA

ADC en Mode DMA — Transfert Direct Mémoire

Le DMA transfère automatiquement les valeurs ADC en RAM sans intervention du CPU.

06
Activer le DMA dans CubeMX
Configuration préalable obligatoire
MODE : DMA
CONFIGURATION CUBEMX DMA
Dans CubeMX → ADC1 → onglet DMA Settings :
Cliquer Add → Sélectionner ADC1 → Direction : Peripheral to Memory → Mode : Circular → Data Width : Word
ATTENTION
En mode DMA Circular, la conversion tourne en boucle infinie automatiquement. Le buffer en RAM est mis à jour continuellement sans intervention du CPU.
07
Code ADC Mode DMA
Buffer circulaire — Transfert automatique
POTAnalogique ADC1DR Register DMA auto CPU = libre ! adc_buf[4] RAM Buffer [0]=CH1 [1]=CH2 ... CPU litadc_buf quand voulu Circular mode : MAJ en continu
FIG. 6 — Mode DMA Circular : ADC → RAM automatiquement, CPU lit le buffer quand nécessaire
C — main.c · Mode DMA
/* ============================================================
   TP ADC Black Pill — Mode DMA (Circular)
   ============================================================ */

#include "main.h"
#include <stdio.h>

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
UART_HandleTypeDef huart1;

#define ADC_BUF_SIZE      4      /* Taille du buffer DMA (nb conversions) */
#define ALARM_THRESHOLD   2048   /* ← Modifier le seuil ici */
#define VREF              3.3f

/* ─── Buffer DMA : rempli automatiquement ─── */
volatile uint32_t adc_buf[ADC_BUF_SIZE] = {0};
volatile uint8_t  dma_ready = 0;

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();       /* Init DMA AVANT ADC ! */
    MX_ADC1_Init();
    MX_USART1_UART_Init();

    printf("=== TP ADC Black Pill — Mode DMA ===\r\n");

    /* Lancer ADC+DMA en mode circulaire — 1 seul appel ! */
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);

    while (1) {

        /* Le buffer adc_buf[] est mis à jour automatiquement */
        /* Le CPU peut lire les valeurs à tout moment         */

        if (dma_ready) {
            dma_ready = 0;

            /* Calculer la moyenne du buffer pour réduire le bruit */
            uint32_t avg = 0;
            for (int i = 0; i < ADC_BUF_SIZE; i++) avg += adc_buf[i];
            avg /= ADC_BUF_SIZE;

            float voltage = ((float)avg / 4095.0f) * VREF;

            if (avg > ALARM_THRESHOLD) {
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
            } else {
                HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
            }

            printf("[DMA] ADC(moy)=%4lu | V=%.3fV | LED=%s\r\n",
                   avg, voltage,
                   (avg > ALARM_THRESHOLD) ? "ON" : "OFF");
        }

        HAL_Delay(200);
    }
}

/* ─── Callback DMA : buffer completement rempli ─── */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        dma_ready = 1;   /* Signaler que le buffer est frais */
    }
}
AVANTAGE DMA — MOYENNAGE
En utilisant un buffer de 4 valeurs et en calculant la moyenne, on réduit le bruit ADC causé par les parasites électriques. C'est une technique appelée oversampling.
// PARTIE 7 — LOGIQUE D'ALARME

Seuil Personnalisable + LED Rouge

08
Personnaliser le seuil d'alarme
Modifier ALARM_THRESHOLD dans le code

Le seuil est défini par la macro #define ALARM_THRESHOLD en haut de chaque fichier. Il suffit de modifier cette valeur et de recompiler.

MI-COURSE (50%)

#define ALARM_THRESHOLD 2048
Potentiomètre à mi-course
V = 1.65V

75% (HAUT)

#define ALARM_THRESHOLD 3072
LED s'allume à 3/4 tour
V = 2.475V

25% (BAS)

#define ALARM_THRESHOLD 1024
Très sensible : LED dès 1/4
V = 0.825V
C — Logique d'alarme complète avec hysteresis
/* ─── Seuils avec hystérésis (évite les oscillations) ─── */
#define ALARM_THRESHOLD     2048   /* Seuil montant : LED s'allume */
#define ALARM_HYSTERESIS    100    /* Marge : LED s'éteint à 1948 */

/* Variable d'état de la LED */
static uint8_t alarm_active = 0;

void update_alarm(uint32_t adc_val) {

    if (!alarm_active && adc_val > ALARM_THRESHOLD) {
        /* Seuil dépassé → Alarme ON */
        alarm_active = 1;
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
        printf("[ALARME] ⚠ Seuil dépassé ! ADC=%lu\r\n", adc_val);

    } else if (alarm_active && adc_val < (ALARM_THRESHOLD - ALARM_HYSTERESIS)) {
        /* En dessous du seuil - hystérésis → Alarme OFF */
        alarm_active = 0;
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
        printf("[ALARME] ✓ Retour normal. ADC=%lu\r\n", adc_val);
    }
}
C'EST QUOI L'HYSTÉRÉSIS ?
Sans hystérésis, si l'ADC oscille autour du seuil (ex: 2047-2049-2048...), la LED clignoterait rapidement. Avec une marge de 100 LSB : la LED s'allume à 2048 mais ne s'éteint qu'à 1948. Cela évite les oscillations parasites.
// PARTIE 8 — CÂBLAGE ET TEST

Schéma de Câblage + Vérification

09
Schéma de câblage complet
Black Pill + Potentiomètre + LED
BLACK PILL STM32F411CEU6 STM32 PA1 GND 3.3V PC13 POT 10 kΩ 3.3V GND Curseur Signal analogique 220Ω GND LED Rouge GND commun 3.3V
FIG. 7 — Schéma complet : Potentiomètre 10kΩ sur PA1 · LED Rouge + 220Ω sur PC13 · GND commun

Tableau de connexions

ComposantBroche composantBroche Black PillCouleur filRemarque
PotentiomètrePatte gauche (VCC)3.3VOrangeAlimentation du pot
PotentiomètrePatte droite (GND)GNDRougeMasse du pot
PotentiomètreCurseur (milieu)PA1VioletSignal ADC
Résistance 220ΩPatte 1PC13RougeLimite le courant LED
LED RougeAnode (+)Résistance 220ΩRougePatte longue = anode
LED RougeCathode (−)GNDNoirPatte courte = cathode
⚠ PC13 ACTIF BAS sur BLACK PILL
La LED intégrée de la Black Pill sur PC13 est active basse : GPIO_PIN_RESET = LED allumée, GPIO_PIN_SET = LED éteinte. C'est déjà pris en compte dans les codes fournis.
10
Vérification UART + debug en temps réel
Sortie série attendue dans le terminal
UART OUTPUT — Terminal série (115200 baud)
=== TP ADC Black Pill — Mode Polling ===
ADC=   0 | V=0.000 V | Seuil=2048 | LED=OFF
ADC= 512 | V=0.412 V | Seuil=2048 | LED=OFF
ADC=1024 | V=0.825 V | Seuil=2048 | LED=OFF
ADC=2047 | V=1.650 V | Seuil=2048 | LED=OFF
ADC=2049 | V=1.651 V | Seuil=2048 | LED=ON   ← Seuil franchi !
ADC=3072 | V=2.475 V | Seuil=2048 | LED=ON
ADC=4095 | V=3.300 V | Seuil=2048 | LED=ON

Points de vérification

  1. Potentiomètre à zéro → ADC = 0, Tension = 0.000V, LED = OFF
  2. Potentiomètre à mi-course → ADC ≈ 2047, Tension ≈ 1.650V
  3. Dépasser le seuil → ADC > ALARM_THRESHOLD → LED rouge s'allume
  4. Potentiomètre au maximum → ADC = 4095, Tension = 3.300V
  5. Valeur ADC stable (pas de bruit important) → câblage correct ✓
BRUIT ADC — NORMAL OU PAS ?
Une variation de ±2 à ±5 LSB quand le potentiomètre est immobile est normale (bruit thermique). Si la variation dépasse ±20 LSB, vérifier : fils trop longs, absence de condensateur de découplage (100nF entre VDDA et GNDA), ou alimentation instable.
Bilan et questions pour aller plus loin

✅ ACQUIS DU TP

  • Principe ADC 12 bits compris
  • Formule Tension ↔ ADC maîtrisée
  • 3 modes codés et testés
  • Seuil personnalisable
  • Alarme LED fonctionnelle
  • Debug UART opérationnel

📝 QUESTIONS

  • Quelle résolution choisir pour ADC 10 bits ?
  • Quel est le temps de conversion à 25 MHz avec 480 cycles ?
  • Pourquoi utiliser DMA pour 4 canaux simultanés ?
  • Comment ajouter un filtre passe-bas logiciel ?
POUR ALLER PLUS LOIN
→ Afficher la valeur sur un écran OLED SSD1306 via I2C
→ Réaliser un voltmètre numérique avec affichage LCD
→ Utiliser 4 canaux ADC en DMA Scan Mode simultanément
→ Tracer la courbe tension en temps réel avec Python + matplotlib