STM32 I2C Master Mode a Livello di Registri: Timing SCL, Start/Stop e Trasferimenti Multi-Byte

2026-06-03 · Davide Carrese
STM32 · I2C · STM32F4 · Embedded

L'I2C è ovunque nei sistemi embedded — sensori, EEPROM, port expander, ADC, controller display. La HAL STM32 fa sembrare l'I2C semplice, ma quando un sensore smette di rispondere su un lotto di produzione, o il bus va in timeout in modo intermittente, la HAL non ti dice perché. Capire il periferico I2C a livello di registri è l'unico modo per fare debug del timing SCL, della gestione NACK e degli errori di bus con sicurezza. Questo articolo analizza la modalità master I2C su STM32F4 usando direttamente i registri, dalla configurazione del clock ai trasferimenti multi-byte.

Panoramica del periferico I2C (legacy su STM32F4)

Il periferico I2C degli STM32F4 è il blocco I2C legacy (non la versione I2C v2 che trovi su STM32G0/H7/L5/U5). Ha quattro registri chiave per il funzionamento in master:

La regola fondamentale per il blocco I2C legacy: SR2 va letto dopo ogni evento di indirizzo. Questa lettura pulisce il flag ADDR e rivela la direzione del trasferimento in TRA (bit 2) e BUSY (bit 1). Dimenticare questa lettura è l'errore singolo più comune nel codice I2C a livello di registri.

Timing SCL: calcolare CCR e TRISE

L'I2C su STM32F4 deriva il suo SCL dal clock APB1 (PCLK1). Su un tipico F401/F411 che gira a 84 MHz, PCLK1 è 42 MHz (prescaler APB1 = 2). Il registro CCR codifica il periodo low/high di SCL, e TRISE tiene conto del tempo di salita del bus.

Standard mode (100 kHz)

// PCLK1 = 42 MHz, SCL target = 100 kHz
// CCR = PCLK1 / (2 * SCL_target) = 42e6 / (2 * 100e3) = 210
// TRISE = tempo di salita massimo in cicli PCLK1
// Specifica I2C: max rise = 1000 ns → 1000e-9 * 42e6 = 42 cicli
I2C1->CCR  = 210;        // Standard mode, 100 kHz
I2C1->TRISE = 42;         // 1000 ns @ 42 MHz

Fast mode (400 kHz)

// PCLK1 = 42 MHz, SCL target = 400 kHz
// Fast mode: CCR = PCLK1 / (3 * SCL_target) con DUTY = 0
// oppure CCR = PCLK1 / (25 * SCL_target) con DUTY = 1 (duty 16:9)
// Con DUTY = 0: CCR = 42e6 / (3 * 400e3) = 35
// Con DUTY = 1 (16:9): CCR = 42e6 / (25 * 400e3) = 4 (min CCR è 1)
// TRISE: max rise = 300 ns → 300e-9 * 42e6 = 12.6 → 13
I2C1->CCR   = 35;          // Fast mode, DUTY=0, 400 kHz
I2C1->CCR  |= 0x8000;      // Imposta CCR[15] per Fast Mode
I2C1->TRISE = 13;          // 300 ns @ 42 MHz

Preferisco sempre DUTY = 0 a 400 kHz perché il duty cycle 1:1 della formula CCR = PCLK1 / (3 * target) è più intuitivo da tarare. Se il bus ha un carico capacitivo elevato, usa DUTY = 1 (rapporto 16:9) per dare più margine su SCL low: CCR = PCLK1 / (25 * target_SCL).

Verifica dei valori

Dopo aver impostato CCR e TRISE, verifica sondando SCL con un oscilloscopio o un analizzatore logico. Le tolleranze della specifica I2C sono strette: 100 kHz ± 1 %, 400 kHz ± 1 %. Un errore software di ±1 in CCR può portare SCL fuori specifica su un bus lungo.

Sequenza di trasmissione master

Una scrittura master a livello di registri verso uno slave a 7 bit segue questa sequenza esatta:

// Assume I2C1 già configurato: CR1_PE=1, CR2_FREQ impostato

void i2c_master_write(uint8_t slave_addr, uint8_t *data, int len) {
    // 1. Genera condizione START
    I2C1->CR1 |= I2C_CR1_START;

    // 2. Aspetta evento SB (Start Bit)
    while (!(I2C1->SR1 & I2C_SR1_SB));

    // 3. Invia indirizzo slave (7 bit, allineato a sinistra, scrittura)
    I2C1->DR = slave_addr << 1;  // LSB = 0 per scrittura

    // 4. Aspetta flag ADDR, poi pulisci leggendo SR2
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR2;  // ← CRITICO: pulisce il flag ADDR

    // 5. Trasmetti byte di dati
    for (int i = 0; i < len; i++) {
        while (!(I2C1->SR1 & I2C_SR1_TXE));  // Aspetta TXE
        I2C1->DR = data[i];
    }

    // 6. Aspetta che l'ultimo byte sia effettivamente trasmesso (BTF)
    while (!(I2C1->SR1 & I2C_SR1_BTF));

    // 7. Genera condizione STOP
    I2C1->CR1 |= I2C_CR1_STOP;
}

Il passo 4 è dove si rompe la maggior parte del codice a livello di registri: SR1 & ADDR indica che l'indirizzo è stato riconosciuto, ma per pulire ADDR devi leggere SR1 e poi leggere SR2. Il cast (void)I2C1->SR2 è deliberato — l'hardware richiede la lettura, anche se ignoriamo il valore. Se lo salti, ADDR rimane impostato, la macchina a stati si blocca e nessun interrupt successivo viene generato.

Sequenza di ricezione master

Una lettura master è simile, ma devi inviare NACK sull'ultimo byte e generare uno STOP prima che lo slave possa rilasciare il bus:

void i2c_master_read(uint8_t slave_addr, uint8_t *buf, int len) {
    // 1. Genera START
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB));

    // 2. Invia indirizzo slave (7 bit, lettura — LSB = 1)
    I2C1->DR = (slave_addr << 1) | 0x01;

    // 3. Aspetta ADDR, pulisci leggendo SR2
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR2;

    // 4. Ricevi byte
    for (int i = 0; i < len; i++) {
        if (i == len - 1) {
            // Disabilita ACK prima di ricevere l'ultimo byte
            I2C1->CR1 &= ~I2C_CR1_ACK;
        }
        // Aspetta RXNE per tutti i byte tranne l'ultimo
        while (!(I2C1->SR1 & I2C_SR1_RXNE));
        buf[i] = I2C1->DR;
    }

    // 5. Genera STOP (dopo RXNE per l'ultimo byte)
    I2C1->CR1 |= I2C_CR1_STOP;

    // 6. Riabilita ACK per il prossimo trasferimento
    I2C1->CR1 |= I2C_CR1_ACK;
}

La temporizzazione NACK/STOP è critica: se pulisci ACK troppo presto (prima che il penultimo byte sia stato ricevuto), lo slave potrebbe interpretare male la transazione; troppo tardi, e lo slave emette un impulso di clock extra. La sequenza sopra — pulisci ACK per i == len - 1, aspetta RXNE, poi STOP immediato — è quella che ho validato su dozzine di slave I2C in progetti F4, F0, L4 e G4.

Condizioni di errore e recupero

Il blocco I2C legacy segnala tre errori hardware in SR1 che richiedono gestione immediata:

AF — Acknowledge Failure

Impostato quando lo slave non risponde con ACK all'indirizzo o a un byte di dati. In una scrittura master, un NACK sui dati significa che lo slave è occupato o il conteggio byte supera il buffer dello slave. In una lettura master, lo slave controlla ACK, quindi AF non dovrebbe mai scattare sui byte di dati. Cause tipiche: indirizzo sbagliato, slave spento, contesa sul bus, o slave ancora impegnato ad elaborare il comando precedente.

if (I2C1->SR1 & I2C_SR1_AF) {
    I2C1->SR1 &= ~I2C_SR1_AF;  // Pulisce AF scrivendo 0
    I2C1->CR1 |= I2C_CR1_STOP;  // Genera STOP per rilasciare il bus
    return -1;  // Riporta NACK al chiamante
}

BERR — Bus Error

Indica una condizione START o STOP fuori posto rilevata dal periferico. Succede quando un glitch o un altro master corrompe il bus. L'unico recupero affidabile è resettare il periferico e riconfigurarlo.

if (I2C1->SR1 & I2C_SR1_BERR) {
    I2C1->CR1 &= ~I2C_CR1_PE;   // Disabilita periferico
    I2C1->CR1 |= I2C_CR1_SWRST; // Software reset
    I2C1->CR1 &= ~I2C_CR1_SWRST;
    i2c_init();  // Re-inizializza tutti i registri
    return -2;
}

ARLO — Arbitration Lost

Un altro master su un bus multi-master ha iniziato a trasmettere contemporaneamente. Pulisci il flag e attendi che il bus diventi libero prima di riprovare.

Timeout: perché l'hardware può bloccarsi

Ogni ciclo while(!(SR1 & FLAG)) ha bisogno di un timeout. Uno slave I2C che tiene il clock basso (clock stretching) può bloccare il firmware all'infinito. Il pattern che uso nei progetti dei clienti:

static int wait_flag(volatile uint32_t *sr, uint32_t mask, uint32_t timeout_us) {
    uint32_t start = micros();  // Oppure DWT->CYCCNT su Cortex-M
    while (!(*sr & mask)) {
        if (*sr & (I2C_SR1_AF | I2C_SR1_BERR | I2C_SR1_ARLO)) {
            return -1;  // Errore durante l'attesa
        }
        if ((micros() - start) > timeout_us) {
            return -2;  // Timeout
        }
    }
    return 0;  // OK
}

Uso un timeout di 5 ms per gli eventi di indirizzo e 1 ms per byte per i trasferimenti dati. Il clock stretching raramente supera qualche centinaio di microsecondi sui sensori moderni; un timeout di 5 ms cattura blocchi reali senza falsi positivi.

Esempio pratico: lettura di un sensore di temperatura via I2C a livello di registri

Ecco una lettura completa da un sensore di temperatura LM75 (indirizzo 7 bit 0x48) usando solo registri:

#define LM75_ADDR  0x48
#define LM75_TEMP  0x00  // Puntatore registro temperatura

float lm75_read_temp(void) {
    uint8_t cmd = LM75_TEMP;
    uint8_t raw[2];

    // Scrive il byte puntatore
    i2c_master_write(LM75_ADDR, &cmd, 1);

    // Legge 2 byte (temperatura)
    i2c_master_read(LM75_ADDR, raw, 2);

    // Conversione: valore signed 11 bit, 0.125 °C per LSB
    int16_t temp = (raw[0] << 8) | raw[1];
    temp >>= 5;  // Allinea a destra il valore a 11 bit
    if (temp & 0x0400) {
        temp |= 0xF800;  // Estensione del segno
    }
    return temp * 0.125f;
}

Questo è lo stesso flusso funzionale della HAL, ma senza l'overhead della macchina a stati della HAL, la logica di interleaving dei timeout, o le 150+ chiamate di funzione per transazione. Su un STM32F401 con risorse limitate, l'approccio a registri riduce di ~800 byte lo spazio codice ed elimina la macchina a stati I2C della HAL (che ha già causato problemi in più di un rilascio di produzione con race condition sottili tra modalità master e slave sulla stessa istanza I2C).

Checklist pratica

Come lo affronterei su un progetto cliente

Su un progetto firmware di produzione, non mescolo mai codice I2C a livello di registri e codice I2C della HAL sulla stessa istanza di bus — le macchine a stati sono in conflitto. Scelgo un modello fin dall'inizio:

Se il cliente insiste per la HAL, sovrascrivo l'MSP init della HAL I2C per configurare i pin con la corretta alternate function, pull-up e velocità direttamente — la debole configurazione GPIO di default della HAL ha causato più guasti I2C intermittenti di qualsiasi altra singola causa nei progetti STM32F4 che ho revisionato.

Aggiungo anche una breve sequenza di bus recovery in i2c_init(): toggla SCL 9 volte mentre SDA è alta, poi genera uno START/STOP. Questo recupera gli slave bloccati in una transazione incompleta dopo un reset del watchdog, senza richiedere un ciclo di alimentazione.

Fonti e approfondimenti

💬 Rispondi via email

Se hai correzioni, un approccio diverso, o una storia vera di guerre I2C, inviala a blog@carrese.eu. Leggo ogni messaggio e aggiorno l'articolo quando imparo qualcosa di nuovo.