STM32 I2C Master Mode a Livello di Registri: Timing SCL, Start/Stop e Trasferimenti Multi-Byte
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:
- I2C_CR1 (Control 1): abilita/disabilita il periferico, genera START/STOP, abilita ACK, abilita interrupt.
- I2C_CR2 (Control 2): frequenza del clock periferico in MHz (FREQ[5:0]), abilitazione DMA, abilitazione IT.
- I2C_CCR (Clock Control): divisore del clock SCL e configurazione del duty cycle.
- I2C_TRISE (TRISE): tempo di salita massimo di SCL in cicli di clock del periferico.
- I2C_SR1 / I2C_SR2 (Status 1 & 2): flag di evento — SB, ADDR, TXE, RXNE, BTF, AF, ARLO, BERR, direzione Rx/Tx.
- I2C_DR (Data): registro dati di trasmissione/ricezione.
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
- Verifica la frequenza PCLK1: leggi RCC->CFGR per confermare il prescaler APB1 prima di calcolare CCR.
- Convalida CCR e TRISE con un oscilloscopio su SCL prima di collegare gli slave.
- Leggi sempre SR2 dopo ADDR: dimenticare questa lettura è il bug #1 nell'I2C a livello di registri.
- Aggiungi timeout a ogni ciclo di polling — uno slave bloccato può congelare l'intero sistema.
- In caso di errore (AF/BERR/ARLO), emetti uno STOP per rilasciare il bus, poi resetta il periferico.
- Mantieni ACK abilitato per tutti i byte ricevuti tranne l'ultimo (lettura master).
- Per letture multi-byte più lunghe di 1 byte, usa il flag BTF (Byte Transfer Finished) in aggiunta a RXNE per un corretto pacing dei byte.
- Per transazioni miste scrittura+lettura (es. lettura di registri di sensori), usa una condizione RESTART invece di STOP+START per evitare desincronizzazione dello slave su bus multi-master.
- Disabilita il clock stretch holdoff sul lato slave se controlli il firmware slave — riduce l'occupazione del bus per transazione.
- Controlla SDA/SCL con un oscilloscopio durante il bring-up: una resistenza di pull-up mancante sembra traffico zero, mentre un pull-up debole causa transizioni ritardate in modo casuale ad alta velocità.
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:
- A livello di registri per semplici cicli di polling di sensori dove la dimensione del codice conta (applicazioni STM32F0/G0 con budget flash ridotti), o dove serve un controllo deterministico del timing su SCL stretch e STOP.
- HAL con interrupt (o DMA) per bus multi-master, transazioni lunghe, o integrazione FreeRTOS dove si preferisce I2C non bloccante.
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
- STM32F4 Reference Manual (RM0090), Capitolo 23 — I2C — descrizione dei registri e formule di temporizzazione.
- AN4234: Guida alla configurazione del timing I2C per STM32F4 — esempi pratici di calcolo CCR/TRISE.
- UM1725: HAL I2C driver per STM32F4 — codice sorgente della macchina a stati HAL per confronto.
- Esempi I2C STM32CubeF4 (Projects/STM32F429I-Discovery/Examples/I2C/) — implementazione di riferimento ST.
- Specifica e manuale utente del bus I2C (UM10204), NXP — il riferimento definitivo per timing e protocollo SCL.

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.