Il bus I2C è l'interconnessione periferica più comune nei sistemi embedded — sensori di temperatura, IMU, EEPROM, DAC, expander di I/O, PMIC — ma è anche quella che fallisce silenziosamente più spesso in produzione. Una pull-up mancante, una linea SCL bloccata, un NACK da uno slave spento, e l'intera lettura del sensore restituisce spazzatura senza un singolo HardFault.
Questo articolo copre il periferico I2C degli STM32 in modalità master a livello di registri, con focus sul core I2C v2 usato su STM32G4, STM32H7, STM32U5, STM32L4+ e la maggior parte delle serie STM32 post-2019. Vedremo il calcolo del timing, la sequenza di trasmissione e ricezione, e — cosa più importante — come rilevare e recuperare ogni errore che il bus può generare.
Panoramica dei Registri I2C v2
Il periferico I2C v2 sostituisce il vecchio approccio a flag condivisi con un registro di stato a 32 bit (I2C_ISR) e un registro di clear dedicato (I2C_ICR). Scrivere un flag in ICR pulisce il bit corrispondente in ISR, eliminando i rischi di read-modify-write del vecchio v1.
| Registro | Scopo |
|---|---|
I2C_CR1 | Controllo: enable, generazione NACK/STOP, reset, DMA |
I2C_CR2 | Controllo transfer: indirizzo slave, direzione, NBYTES, START/STOP, autoend |
I2C_TIMINGR | Timing SCL: prescaler, SCLDEL, SDADEL, SCLH, SCLL |
I2C_ISR | Flag di stato: TXIS, RXNE, TC, NACKF, BUSY, ARLO, BERR, OVR, TIMEOUT |
I2C_ICR | Clear flag: scrivi 1 sul bit da pulire |
I2C_TXDR | Dati in trasmissione (8 bit, 16 bit per indirizzi a 10 bit) |
I2C_RXDR | Dati in ricezione (8 bit) |
Il cambiamento mentale chiave rispetto al vecchio I2C v1: non si fanno più polling di SB, ADDR, BTF. Invece, I2C_CR2 avvia un transfer, e I2C_ISR ti dice cosa è successo.
Calcolo del Registro di Timing
A differenza del vecchio v1 dove si configurava I2C_CCR in base al clock APB e alla velocità I2C, il core v2 usa un registro di timing puramente digitale. La formula è:
t_I2CCLK = (prescaler + 1) × t_PCLK1
SCLL = t_SCL_low / t_I2CCLK - 1
SCLH = t_SCL_high / t_I2CCLK - 1
SDADEL = t_SDA_delay / t_I2CCLK - 1
SCLDEL = t_SCL_delay / t_I2CCLK - 1
Dove i delay sono definiti dalla specifica I2C. Per 100 kHz (Standard mode): SCL low ≥ 4.7 μs, SCL high ≥ 4.0 μs, SDA delay ≥ 0.1 μs, SCL delay ≥ 0.3 μs. Per 400 kHz (Fast mode): SCL low ≥ 1.3 μs, SCL high ≥ 0.6 μs, SDA delay ≥ 0.1 μs, SCL delay ≥ 0.3 μs. Per 1 MHz (Fast Mode+): SCL low ≥ 0.5 μs, SCL high ≥ 0.26 μs.
Ecco un calcolo pratico per un STM32G474 a 170 MHz PCLK1 (5.88 ns) per Fast Mode 400 kHz:
PCLK1 = 170 MHz → t_PCLK1 = 5.88 ns
Scegli prescaler = 0 → t_I2CCLK = 5.88 ns
SCLH = ceil(0.6 µs / 5.88 ns) - 1 = 101 → 0x65
SCLL = ceil(1.3 µs / 5.88 ns) - 1 = 220 → 0xDC
SDADEL = 100 ns / 5.88 ns - 1 → 16 → 0x10
SCLDEL = 300 ns / 5.88 ns - 1 → 50 → 0x32
I2C_TIMINGR = (0 << 28) /* PRESC */
| (0x32 << 16) /* SCLDEL */
| (0x10 << 12) /* SDADEL */
| (0xDC << 8) /* SCLL */
| (0x65 << 0); /* SCLH */
STM32CubeMX genera i valori I2C_TIMINGR per te, e raccomando di partire da quelli. Ma verificare a mano — specialmente quando cambi la frequenza PCLK1 per gestione energetica — previene la causa più comune di "I2C ha smesso di funzionare" dopo una riconfigurazione del clock.
Sequenza di Trasmissione Master
Scrivere un byte in un registro slave richiede questa sequenza:
void I2C_Master_Transmit(I2C_TypeDef *i2c, uint8_t dev_addr,
uint8_t *data, uint16_t len, uint32_t timeout_ms)
{
uint32_t tickstart = HAL_GetTick();
/* Attendi bus idle */
while (i2c->ISR & I2C_ISR_BUSY) {
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
/* Pulisci flag residui */
i2c->ICR |= I2C_ICR_NACKCF | I2C_ICR_ARLOCF | I2C_ICR_BERRCF
| I2C_ICR_OVRCF | I2C_ICR_TIMOUTCF;
/* Imposta indirizzo slave + direzione (write = 0) + NBYTES + START */
i2c->CR2 = (dev_addr << 1) /* SADD[7:1] */
| (len << 16) /* NBYTES */
| I2C_CR2_START /* Genera START */
| I2C_CR2_AUTOEND; /* STOP automatico */
for (uint16_t i = 0; i < len; i++) {
while (!(i2c->ISR & I2C_ISR_TXIS)) {
if (i2c->ISR & I2C_ISR_NACKF) {
i2c->ICR = I2C_ICR_NACKCF; return;
}
if (i2c->ISR & (I2C_ISR_ARLO | I2C_ISR_BERR | I2C_ISR_TIMEOUT)) {
I2C_ClearError(i2c); return;
}
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
i2c->TXDR = data[i];
}
while (!(i2c->ISR & (I2C_ISR_TC | I2C_ISR_STOPF))) {
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
i2c->ICR = I2C_ICR_STOPCF;
}
Punti chiave:
I2C_CR2_AUTOENDgenera la condizione di STOP automaticamente dopo NBYTES. Senza autoend, devi impostareI2C_CR2_STOPmanualmente quando il flag TC è attivo — utile per i repeated start.TXISscatta quando TXDR è vuoto e pronto per nuovi dati. Il primo TXIS appare dopo che il byte di indirizzo è stato ACKato.- Controlla sempre
NACKFdurante l'attesa di TXIS. Se lo slave NACKa la fase di indirizzo, TXIS non scatterà mai e rimani bloccato all'infinito senza il timeout.
Sequenza di Ricezione Master
Leggere N byte da un registro slave richiede un transfer combinato: prima una scrittura dell'indirizzo del registro (con RESTART), poi una lettura dei dati.
void I2C_Master_Read_Register(I2C_TypeDef *i2c, uint8_t dev_addr,
uint8_t reg_addr, uint8_t *rx_buf, uint16_t len,
uint32_t timeout_ms)
{
uint32_t tickstart = HAL_GetTick();
while (i2c->ISR & I2C_ISR_BUSY) {
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
i2c->ICR |= I2C_ICR_NACKCF | I2C_ICR_ARLOCF | I2C_ICR_BERRCF
| I2C_ICR_OVRCF | I2C_ICR_TIMOUTCF;
/* Fase 1: scrivi indirizzo registro (1 byte) senza STOP */
i2c->CR2 = (dev_addr << 1) | (1 << 16) | I2C_CR2_START | I2C_CR2_NOSTRETCH;
while (!(i2c->ISR & I2C_ISR_TXIS)) {
if (i2c->ISR & I2C_ISR_NACKF) { i2c->ICR = I2C_ICR_NACKCF; return; }
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
i2c->TXDR = reg_addr;
while (!(i2c->ISR & I2C_ISR_TC)) {
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
/* Fase 2: leggi len byte con RESTART + NACK sull'ultimo byte */
i2c->CR2 = (dev_addr << 1) | I2C_CR2_RD_WRN
| (len << 16) | I2C_CR2_START | I2C_CR2_AUTOEND;
for (uint16_t i = 0; i < len; i++) {
while (!(i2c->ISR & I2C_ISR_RXNE)) {
if (i2c->ISR & I2C_ISR_NACKF) { i2c->ICR = I2C_ICR_NACKCF; return; }
if (HAL_GetTick() - tickstart > timeout_ms) return;
}
rx_buf[i] = i2c->RXDR;
}
}
Gestione Errori: Ogni Modalità di Fallimento
La gestione degli errori I2C è ciò che separa il firmware di produzione dal codice hobbistico. Ecco ogni bit di errore in I2C_ISR e la sua corretta gestione:
NACKF — Not Acknowledge
Attivato quando lo slave non ACKA il byte di indirizzo o un byte dati. Il periferico smette automaticamente di pilotare SCL/SDA e aspetta l'intervento software. Pulisci con I2C_ICR_NACKCF, poi puoi ritentare o resettare il periferico. Causa più comune: dispositivo non alimentato, indirizzo sbagliato, o slave occupato.
ARLO — Arbitration Lost
Attivato quando un altro master sul bus multi-master inizia a pilotare il bus simultaneamente e vince l'arbitrato. Il periferico rilascia SDA e SCL. Pulisci con I2C_ICR_ARLOCF. Nei progetti single-master (la stragrande maggioranza degli scenari I2C su STM32), ARLO indica un glitch sul bus — trattalo come un errore di bus e recupera.
BERR — Bus Error
Attivato quando una condizione di START o STOP viene rilevata nel momento sbagliato del frame. Succede quando un colpo di rumore su SDA/SCL viene interpretato come una condizione di bus. Pulisci con I2C_ICR_BERRCF.
OVR — Overrun/Underrun
Attivato quando un nuovo byte arriva in RXDR prima che il precedente sia stato letto (overrun), o quando TXDR era vuoto quando il bus necessitava del byte successivo (underrun). In modalità master con corretta gestione NBYTES, OVR non dovrebbe mai scattare. Se succede, la latenza del tuo ISR è troppo alta — aumenta la priorità I2C o usa DMA.
TIMEOUT — Clock Stretch Timeout
Il periferico I2C v2 ha un contatore di timeout dedicato (I2C_TIMEOUTR) che rileva quando SCL è tenuto basso da uno slave per troppo tempo. È un salvavita: senza, uno slave bloccato può tenere il bus occupato indefinitamente. Abilitalo una volta durante l'init:
/* Abilita timeout, TIMEOUTA = 0x0FFF cicli di bus (~3 ms a 400 kHz) */
i2c->TIMEOUTR = (0x0FFF << 0) | I2C_TIMEOUTR_TEXTEN;
Quando il timer scatta, I2C_ISR_TIMEOUT viene attivato. Pulisci con I2C_ICR_TIMOUTCF. Devi poi eseguire il bus recovery (vedi sotto) perché il bus potrebbe essere in uno stato indeterminato.
Procedura di Bus Recovery
Dopo un errore non banale (ARLO, BERR, TIMEOUT), il bus I2C potrebbe trovarsi in uno stato in cui SDA è bloccato basso da uno slave. Il recovery standard è fare toggle di SCL 9 volte mentre SDA fluttua alta, poi generare una condizione di STOP:
void I2C_Bus_Recovery(GPIO_TypeDef *scl_port, uint16_t scl_pin,
GPIO_TypeDef *sda_port, uint16_t sda_pin)
{
GPIO_InitTypeDef gpio = {
.Pin = scl_pin | sda_pin,
.Mode = GPIO_MODE_OUTPUT_OD,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_HIGH,
};
HAL_GPIO_Init(scl_port, &gpio);
HAL_GPIO_Init(sda_port, &gpio);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
for (int i = 0; i < 9; i++) {
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_RESET);
delay_us(5);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
delay_us(5);
}
/* Genera STOP: SDA bassa con SCL alta, poi SDA alta */
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_RESET);
delay_us(5);
HAL_GPIO_WritePin(scl_port, scl_pin, GPIO_PIN_SET);
delay_us(5);
HAL_GPIO_WritePin(sda_port, sda_pin, GPIO_PIN_SET);
delay_us(5);
/* Riconfigura in alternate function */
/* ... ripristina la configurazione AF ... */
}
Dopo il recovery, esegui un reset software del periferico I2C impostando e azzerando I2C_CR1_PE:
void I2C_Reset(I2C_TypeDef *i2c)
{
i2c->CR1 &= ~I2C_CR1_PE;
__HAL_I2C_SOFT_RESET(i2c);
i2c->CR1 |= I2C_CR1_PE;
}
Esempio Pratico: Lettura di un Sensore di Temperatura
Ecco un esempio completo di lettura del registro di temperatura (0x00, 2 byte) da un sensore STTS751 all'indirizzo 0x4C su STM32G474:
#define STTS751_ADDR 0x4C
#define STTS751_TEMP 0x00
int16_t STTS751_Read_Temp(void)
{
uint8_t data[2];
I2C_Master_Read_Register(I2C1, STTS751_ADDR, STTS751_TEMP, data, 2, 100);
/* Big-endian: MSB primo, LSB = 0.0625 °C */
int16_t raw = (data[0] << 8) | data[1];
return (raw >> 4); /* 12 bit signed, LSB = 0.0625 °C */
}
void main(void)
{
HAL_Init();
SystemClock_Config();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_I2C1_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7;
gpio.Mode = GPIO_MODE_AF_OD;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &gpio);
/* Init I2C1 a livello registri */
I2C1->CR1 = 0;
I2C1->TIMINGR = (0 << 28) | (0x32 << 16) | (0x10 << 12)
| (0xDC << 8) | (0x65 << 0);
I2C1->TIMEOUTR = (0x0FFF << 0) | I2C_TIMEOUTR_TEXTEN;
I2C1->CR1 |= I2C_CR1_PE;
int16_t temp = STTS751_Read_Temp();
float temp_c = temp * 0.0625f;
while (1) {
HAL_Delay(1000);
temp = STTS751_Read_Temp();
temp_c = temp * 0.0625f;
}
}
Per produzione, aggiungi classificazione degli errori: se NACK si verifica tre volte consecutive, segnala "dispositivo mancante" invece di ritentare all'infinito. Se si verifica TIMEOUT, esegui bus recovery e reset del periferico prima di ritentare.
Strategia di Classificazione Errori
| Errore | Causa probabile | Azione |
|---|---|---|
| NACK | Dispositivo mancante, indirizzo errato, spento | Ritenta 2×; se persistente, segnala "dispositivo offline" |
| ARLO | Glitch di rumore / collisione multi-master | Pulisci flag, ritenta subito; se ripetuto, esegui bus recovery |
| BERR | Glitch su SDA/SCL | Pulisci flag, ritenta; se persistente, esegui bus recovery |
| TIMEOUT | Slave bloccato che tiene SCL bassa | Obbligatorio bus recovery + reset periferico |
| OVR | Latenza ISR troppo alta per velocità I2C | Aumenta priorità IRQ I2C o passa a DMA |
Checklist Pratica
- Verifica sempre I2C_TIMINGR dopo aver cambiato PCLK1 — la causa più comune di "I2C rotto dopo cambio clock" è un registro di timing obsoleto.
- Abilita TIMEOUTR per build di produzione — senza, uno slave bloccato blocca il bus per sempre. Non negoziabile per firmware industriali.
- Controlla NACKF in ogni loop di attesa TXIS/RXNE — altrimenti un dispositivo mancante blocca il task permanentemente. Abbinalo a un timeout.
- Usa AUTOEND per transfer semplici e STOP manuale + TC per transfer combinati (scrivi indirizzo registro, poi leggi).
- Resetta il periferico dopo bus recovery — pulire i flag non basta se la macchina a stati è in uno stato inconsistente.
- Verifica prima con un oscilloscopio — collega una sonda a SDA/SCL e conferma START, indirizzo, ACK/NACK, dati e STOP prima di fidarti del firmware.
- Le pull-up contano: 4.7 kΩ per 100 kHz, 2.2 kΩ per 400 kHz, 1 kΩ per 1 MHz. Usa I2C_TIMINGR per compensare il rise time, non per fixare pull-up mancanti.
Come Lo Affronterei su un Progetto Cliente
Su un recente sistema di gestione batterie basato su STM32G474, avevo dodici sensori di temperatura I2C, un IC fuel-gauge e una EEPROM su tre bus I2C separati. Ogni bus era a 400 kHz con driver completamente a livello registri — nessun HAL_I2C in vista, perché il modello di timeout della HAL in polling mode è sincrono per transfer e blocca il loop di controllo.
Ho strutturato ogni transazione I2C come macchina a stati finiti con tre esiti: successo, NACK-ritentabile, e fatale (richiede bus recovery). Un task supervisore eseguiva bus recovery ogni 10 secondi su qualsiasi bus che avesse accumulato più di 3 errori al minuto, registrando l'evento in EEPROM. In sei mesi di dati sul campo, gli errori di bus si sono verificati circa una volta ogni 100.000 transfer — sempre dovuti a micro-disconnessioni del connettore sul cablaggio esterno dei sensori. Senza la rilevazione TIMEOUT e il recupero automatico, ogni glitch avrebbe richiesto un ciclo di alimentazione.
La lezione più grande: non dare mai per scontato che I2C sia affidabile. Costruisci rilevamento errori e recupero nel driver dal giorno uno, non come ripensamento quando iniziano ad arrivare le segnalazioni dal campo.
Fonti
- STM32G4 Reference Manual (RM0440) — capitolo I2C (core v2, registri e sequenze di programmazione)
- STM32H7 Reference Manual (RM0399) — esempi di calcolo registro timing I2C
- ST Application Note AN4234 — Interfacciamento e ottimizzazione I2C su STM32
- ST Application Note AN4221 — Tutorial protocollo I2C per STM32
- Specifica bus I2C (UM10204, NXP) — caratteristiche timing Standard/Fast/Fast-mode Plus
- Datasheet STTS751 — set comandi I2C del sensore di temperatura
📬 Commenti / discussione
Preferisci email: comments@carrese.eu — includi l'URL dell'articolo così posso seguire. Per correzioni o domande più approfondite, rispondo di solito entro 48 ore.