Domanda:
Implementazione I2C non bloccante su STM32
Alexey Malev
2016-10-24 14:22:05 UTC
view on stackexchange narkive permalink

La maggior parte degli articoli che sono in grado di trovare che sono essenzialmente "I2C per i manichini sul microcontrollore XXX" suggeriscono di bloccare la CPU in attesa degli eventi di conferma.Per l ' esempio, (i commenti e la descrizione sono in russo, ma in realtà non importa).Ecco l'esempio di "blocco" di cui sto parlando:

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * attendi conferma * /
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
 

Questi cicli while sono presenti dopo ogni operazione, essenzialmente.Infine, la mia domanda è: come implementare I2C senza MCU "sospeso" con questi cicli while?Ad un livello molto alto capisco che devo usare gli interrupt invece di whiles, ma non riesco a trovare alcun esempio di codice.Potete aiutarmi per favore in questo?

Si noti che è anche possibile utilizzare DMA con I²C per ottenere alcuni vantaggi in termini di prestazioni e ridurre il numero di interruzioni da gestire.
@AlexeyMalev Ecco un esempio di implementazione I2C non bloccante per AVR (sebbene principi simili si applichino a STM32): https://github.com/scttnlsn/avr-twi Questa particolare implementazione utilizza interrupt per gestire una macchina a stati e chiamare una funzione di callback quandola trasmissione è terminata.
Cinque risposte:
jonk
2016-10-24 14:46:21 UTC
view on stackexchange narkive permalink

Certo.

  1. Configura i tuoi driver in termini di una coppia superiore e inferiore , separate da un buffer condiviso. La funzione lower è un bit di codice guidato da interrupt che risponde agli eventi di interruzione, si occupa del lavoro immediato necessario per servire l'hardware e aggiunge dati nel buffer condiviso (se è un ricevitore) o estrae il bit successivo di dati dal buffer condiviso per continuare a servire l'hardware (se si tratta di un trasmettitore). La funzione upper viene chiamata dal codice normale e accetta i dati da aggiungere a un buffer in uscita oppure else controlla ed estrae i dati dal buffer in entrata. Associando cose come questa e fornendo un buffering adeguato alle tue esigenze, questa soluzione può funzionare tutto il giorno senza problemi e disaccoppia il codice principale dalla manutenzione dell'hardware.
  2. Usa una macchina a stati nel tuo driver di interrupt. Ogni interruzione legge lo stato corrente, elabora un passaggio, quindi passa a uno stato diverso o rimane nello stesso stato, quindi esce. Questo può essere complesso o semplice, a seconda delle necessità. Spesso, questo viene escluso da un evento timer. Ma non deve essere così.
  3. Crea un sistema operativo cooperativo. Questo può essere facile come impostare alcuni piccoli stack allocati usando malloc () e consentire alle funzioni di chiamare cooperativamente una funzione switch () quando hanno finito con qualche compito immediato per ora. Hai impostato un thread separato per la tua funzione I2C, ma quando decide che non c'è niente da fare in questo momento, chiama semplicemente switch () per causare un cambio di stack su un altro thread. Questo può essere fatto round-robbin finché non torna indietro e ritorna dalla chiamata switch () che hai fatto. Quindi torni alla tua condizione while, che controlla di nuovo. Se ancora non c'è niente da fare, il mentre chiama switch () di nuovo. Etc. In questo modo, il codice non diventa molto più complesso da mantenere ed è semplice inserire chiamate switch () ovunque se ne sente la necessità. Non è nemmeno necessario preoccuparsi della prelazione delle funzioni di libreria, poiché si sta solo passando da uno stack all'altro in corrispondenza del limite di una chiamata di funzione, quindi è impossibile interrompere una funzione di libreria. Ciò rende l'implementazione molto semplice.
  4. Thread preventivi. Questo è molto simile al punto 3 tranne che non è necessario chiamare la funzione switch (). La prelazione avviene in base a un timer, oppure un thread può anche scegliere liberamente di rilasciare il suo tempo. La difficoltà qui sta nel trattare con la prelazione di routine di libreria e / o hardware specializzato dove possono esserci sequenze di istruzioni specifiche generate dal compilatore che non possono essere interrotte (I / O back-to-back che deve recuperare un byte alto seguito da un byte basso dallo stesso indirizzo mappato in memoria, ad esempio.)

Penso che le prime due opzioni siano probabilmente le tue scommesse migliori, però. Tuttavia, molto dipende da quanto si dipende dal codice della libreria scritto da altri. È del tutto possibile che il codice della libreria non sia progettato per essere suddiviso in componenti di livello superiore e inferiore. Ed è anche possibile che tu non possa chiamarli in base a eventi timer o altri eventi basati su macchine a stati.

Tendo a presumere che se non mi piace il modo in cui la libreria fa il lavoro (solo cicli di attesa occupati), allora sono bloccato a scrivere il mio codice per fare il lavoro.Ciò significa che sono libero di utilizzare uno dei metodi sopra indicati.

Ma è necessario dare un'occhiata più da vicino al codice della libreria e vedere se ha già funzionalità che consentono di evitare il comportamento di attesa occupata.È possibile che la libreria supporti qualcosa di diverso.Quindi questo è il primo posto da controllare, per ogni evenienza.In caso contrario, gioca con l'idea di utilizzare le funzioni esistenti come parte di una divisione del driver superiore / inferiore oppure come processo guidato da una macchina a stati.È possibile che tu possa risolvere anche questo.

Non ho altri suggerimenti che mi vengono in mente rapidamente, in questo momento.

Per favore correggimi se ho sbagliato l'idea del # 1 - supponiamo che io abbia un codice che legge alcuni dati dal bus I2C e che fa un po 'di matematica pesante, ad esempio calcola il fattoriale di un certo numero.Dato che non ho thread qui, il mio codice principale che calcola il fattoriale dovrebbe "mettere in pausa" a volte (invece di cedere all'interruzione della gestione del thread in un linguaggio di alto livello) e controllare se ci sono dati nel buffer che hai menzionato, aggiunti lì dal gestore degli interrupt?
@AlexeyMalev Quel codice fattoriale dovrebbe probabilmente essere nella funzione principale, che chiama il codice di livello _upper_.Il codice di livello inferiore non fa cose che richiedono un'interruzione nel mezzo di esso.Se si verificano più cose mentre il codice principale chiama il codice _upper_ o viene elaborato dopo averlo chiamato, l'interrupt verrà comunque servito e inserirà i valori nel buffer per te.Ciò non si ferma anche se stai facendo calcoli.Non è necessario controllare più dati memorizzati nel buffer, se non hai completato l'altro lavoro.Se non riesci a tenere il passo, hai comunque un problema.
@AlexeyMalev Il lavoro di calcolo più lungo dovrebbe essere nel codice principale.Se hai bit più brevi che devono essere eseguiti tra le volte, puoi impostarli su eventi timer che interrompono il tuo lungo codice di calcolo per svolgere le loro attività brevi.Queste brevi attività a tempo possono chiamare la metà di livello _upper_ di qualsiasi driver per ottenere anche dati memorizzati nel buffer.È solo questione di tracciare un diagramma temporale.Lascia abbastanza tempo tra i passaggi per eseguire i calcoli più lunghi e anche tutti i tempi di interruzione.Questo è il tuo processo principale.Puoi usare la mia opzione n. 3, però, e salare il tuo codice principale con chiamate switch ().
Scusa, ma devo votare in negativo.Questa lunga risposta non sembra fornire alcuna informazione utile su * come * implementare una macchina a stati I²C basata su interrupt su un STM32.
La domanda era: come implementare l'interazione i2c non bloccante, ho menzionato gli interrupt come opzione.Basato su interrupt in uno solo dei possibili approcci.
@AlexeyMalev Non conosco specificamente l'ambiente STM32, ma ho cercato di fornire risposte generali che generalmente si applicano, non sono bloccanti e l'ho fatto subito.
@jonk Nono, mi riferivo al commento di JimmyB, non importa :)
followed Monica to Codidact
2016-10-24 14:52:41 UTC
view on stackexchange narkive permalink

Ci sono esempi nelle librerie STM32Cube .Procurati quello appropriato per la tua famiglia di controller (ad es. STM32CubeF4 o STM32CubeL1 ) e cerca examples / I2C / I2C_TwoBoards_ComDMA in Projects sottodirectory.

AnoE
2016-10-24 18:44:59 UTC
view on stackexchange narkive permalink

Motivo

Bene, il motivo è semplice: bloccare è semplicemente facile e a prima vista sembra funzionare. Guai a te se intanto vuoi fare qualcos'altro.

Quindi, senza entrare in molti dettagli, poiché non conosco l'STM32, in genere puoi risolvere questo problema in due modi, a seconda delle tue esigenze.

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * attendi conferma * /
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT));
 

Converti in non bloccante

O implementi un timeout per tutti i tuoi cicli while . Ciò significa:

  I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
/ * attendi conferma * /
static unsigned long start = now ();
while (! I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT) && now () - avvia < TIMEOUT);
if (now () - start > = TIMEOUT) {return ERROR_TIMEOUT; }
 

(Questo è ovviamente uno pseudocodice, hai un'idea. Sentiti libero di ottimizzare o adattare le tue preferenze di codifica, se necessario.)

Devi controllare i codici di ritorno quando sali sulla pila e scegliere il punto corretto in cui eseguire la gestione del timeout. Nota che aiuta anche impostare una variabile globale i2c_timeout_occured = 1 o qualsiasi altra cosa in modo da poter interrompere rapidamente ulteriori chiamate I2C senza dover passare troppi argomenti.

Questo cambiamento è piuttosto indolore, si spera.

Dentro e fuori

Se, invece, hai davvero bisogno di fare altre elaborazioni mentre aspetti quell'evento, allora devi sbarazzarti completamente del ciclo while interno. Lo fai in questo modo:

  void main_loop () {
   do_i2c_stuff (); // non deve mai bloccare
   do_other_stuff ();
   ...
}

// Non deve mai bloccare. Supponendo che nemmeno tutte le funzioni I2C _... si blocchino.
void do_i2c_stuff () {
  stato int statico = ...;

  if (state == 0) {
    I2C_GenerateSTART (HMC5883L_I2C, ENABLE);
    stato = 1;
  } else if (state == 1) {
    if (I2C_CheckEvent (HMC5883L_I2C, I2C_EVENT_MASTER_MODE_SELECT))
      stato = 2;
} altro ...
}
 

Non è necessariamente così complicato, dipende dalla tua altra logica. Puoi fare molto con il corretto rientro / commento / formattazione in modo da non perdere traccia di ciò che stai programmando.

Il modo in cui funziona è creare una macchina a stati . Se guardi il tuo codice originale, sembra questo:

  codice non bloccante
while (! nonblocking_function_call1 ());
codice non bloccante
while (! nonblocking_function_call2 ());
 

Per trasformarlo in una macchina a stati, hai uno stato per ciascuno:

  stato 0: codice non bloccante
stato 1: nonblocking_function_call1 ()
stato 2: codice non bloccante
stato 3: nonblocking_function_call2 ()
 

Quindi, come mostrato nell'esempio sopra, chiami questo codice in un ciclo infinito (il tuo ciclo principale) ed esegui solo il codice che corrisponde al tuo stato corrente (tracciato in una variabile state statica) . Il codice non bloccante è banale, non è cambiato rispetto a prima. Il codice di blocco è sostituito da una variante che non blocca, ma aggiorna solo lo state al termine.

Nota che i singoli cicli while sono spariti; li hai sostituiti dal fatto che hai comunque il tuo ciclo principale di livello superiore, che chiama ripetutamente la tua macchina a stati.

Questa soluzione può essere dolorosa quando si dispone di molto codice legacy poiché non è possibile adattare semplicemente la funzione di blocco più interna, come nella prima soluzione. Risplende quando inizi a scrivere codice nuovo e vai in questo modo dall'inizio. Combinalo con molte altre cose che un µC potrebbe fare (ad esempio, attendere la pressione dei pulsanti, ecc.); se ti abitui a farlo in questo modo tutto il tempo, ottieni gratuitamente abilità multitasking arbitrarie.

Interruzioni

Francamente, per qualcosa di simile (cioè, sbarazzarmi del blocco infinito) farei del mio meglio per stare lontano dalle interruzioni a meno che tu non abbia esigenze di tempismo estreme.Gli interrupt lo rendono complicato, veloce, potresti non averne abbastanza comunque, e si ridurrà comunque a codice abbastanza simile, poiché non vuoi fare molto di più all'interno dell'interrupt se non impostare alcuni flag.

bella soluzione (non ottimale) qui - ho pensato che una sorta di interruzione sarebbe sempre stata necessaria.
L'idea era di sbarazzarsi dei cicli "while".Ho una domanda sul tuo ultimo esempio di codice, non sono sicuro di cosa proponi esattamente.Il problema è che: `I2C_CheckEvent` restituisce` true` dopo un po 'di tempo e non si blocca, il che significa che in generale non è sufficiente chiamarlo una volta, cosa che (come ho capito) suggerisci.Puoi spiegarci qualcosa per favore?
@AlexeyMalev, le mie soluzioni eliminano infiniti cicli "while".Il primo li ha ancora ma aggiungi un timeout.Il secondo li elimina completamente (supponendo che tu abbia un ciclo di livello superiore che viene eseguito in un ciclo perpetuo, comunque).Ho aggiunto alcune spiegazioni, come richiesto.i2C_CheckEvent * viene * chiamato spesso, ma il tuo metodo ritorna immediatamente, indipendentemente dal risultato, quindi dando anche alle altre parti del tuo programma il tempo di essere eseguito.
Questo non è "non bloccante".Nonblocking significa che la tua CPU va in sleep quando non è necessario eseguire alcun lavoro.Questo viene in genere fatto utilizzando uno scheduler che consente all'attività di dormire su un semaforo invece di bloccare la CPU in un ciclo while.Quando si verifica anche i2c, la routine di interrupt fornisce il semaforo e l'attività che era in attesa sul semaforo si sveglia e viene eseguita di nuovo.QUESTO è non bloccante.Le soluzioni delineate in questo post non lo sono.
@Martin, ci sono diversi aspetti del blocco.La mia risposta riflette la mia comprensione della domanda, che chiede come evitare di bloccare il flusso del programma quando si entra in una chiamata i2c.OP descrive una variante di blocco e la mia soluzione delineata è la corrispondente non bloccante.La mia soluzione risolve il problema immediato ed è semplice da generalizzare all'interruzione o al semaforo se necessario.
Tranne che la tua prima soluzione non è non bloccante e la tua seconda soluzione RICHIEDE il 100% di utilizzo della CPU mentre è in corso un'operazione i2c.Quindi entrambe le soluzioni non sono adatte per qualsiasi codice di livello di produzione serio.
@Martin, Sento che la mia risposta è abbastanza ampia da consentire al lettore di vedere a cosa sto andando.Non sto dicendo che la mia soluzione risolva tutti i problemi del mondo.Non vedo nulla sul risparmio di energia della CPU nella domanda.Il mio approccio risolve un problema specifico (come affermato): come intercalare questo tipo di comunicazione con altre elaborazioni.Se preferisci una soluzione diversa, non esitare a scriverne una.Se hai miglioramenti concreti sul mio approccio che non equivalgono a una riscrittura completa, sentiti libero di commentare e ne terrò conto.
Bence Kaulics
2016-10-24 19:28:22 UTC
view on stackexchange narkive permalink

Di seguito è riportato un elenco di richieste di interruzione \ $ \ small I ^ 2C \ $ disponibili su un STM32.Poiché non conosco il chip esatto che stai utilizzando, si consiglia di controllare il manuale di riferimento se ci sono differenze, ma dubito che ci saranno.

enter image description here

Per rimanere sul codice di esempio, se è necessaria una versione non bloccante, è necessario abilitare l'evento Start bit inviato (Master) con il bit di controllo ITEVFEN e controllare SB flag dell'evento all'interno dell'ISR.

JimmyB
2016-10-24 20:55:53 UTC
view on stackexchange narkive permalink

Considerando l'estratto di @BenceKaulics da un foglio dati, lo pseudo-codice per una routine di servizio di interruzione (ISR) potrebbe essere simile a questo:

  i2c_event_isr () {
  switch (i2c_event) {

    case master_start_bit_sent:

      send_address (...);
      rompere;

    case master_address_sent:
    case data_byte_finished:

      if (has_more_data ()) {
        send_next_data_byte ();
      } altro {
        send_stop_condition ();
      }

      rompere;
    ...
  }
}
 


Questa domanda e risposta è stata tradotta automaticamente dalla lingua inglese. Il contenuto originale è disponibile su stackexchange, che ringraziamo per la licenza cc by-sa 3.0 con cui è distribuito.
Loading...