[test in progress] I2C non bloccante

hola, qualche tempo fà si parlava di avere una I2C asincrona... oggi avevo un paio di ore di voglia e ho fatto le modifiche. ho finito or ora, il codice compila (va in conflitto con la libreria Wire originale, essa va rimossa dalla cartella "libraries" di arduino), ma non è ancora testata. è stata mantenuta la retrocompatibilità con la vecchia libreria, in questo modo spero che quando funzionerà potrà essere integrata nelle future release di arduino :)

Inizio a postarla anche se non testata perchè magari a qualuno interessa e mi da una mano a testare. Per testarla basta rimuovere la vera Wire, mettere nel .INO il proprio codice Wire già funzionante su un altro sensore, et vuilà, non userete le funzioni asincrone nella loro potenzialità, ma le usate dietro le quinte.

In oltre annuncio la nascita del repositoy git in cui metterò i miei codici vari di arduino, è direttamente collegata alla mia cartella skecth quindi ci troverete dentro un di tutto (appena e se riesco a recuperare qualcosa dal vecchio HDD)

quindi se vi interessa il codice: https://github.com/lestofante/arduino/tree/master/asincI2C

ottimo. ci spieghi meglio in che senso è "asincrona" così magari riesco a farla inserire nelle prossime release.

m

lesto:
hola, qualche tempo fà si parlava di avere una I2C asincrona…

La I2C è una seriale di tipo sincrono, è per via della presenza del clock e fa parte della definizione di questa tipologia di seriale, pertanto definirla di tipo asincrono è un grossolano errore.
Spiega meglio cosa fa di diverso questa tua gestione della I2C rispetto a quella di serie di Arduino.

x iscrizione

Nel file twi.c scrivi:

  unsigned long frequency = 100000;
  if (fastI2C){
    frequency = 400000;
  }else{
    frequency = 100000;
  }

Setti la variabile a 100'000 e poi ripeti con else l'attribuzione. Non sarebbe più semplice così?

  unsigned long frequency = 100000;
  if (fastI2C) frequency = 400000;

Ho visto anche che hai sostituito tutti i while con altro codice. In questo modo in caso di errore di comunicazione (distacco dei cavi, ecc.) il codice non si blocca in un ciclo infinito. Però non ho capito molto il funzionamento del nuovo codice; potresti spiegarlo?

EDIT: Un'altra cosa... In Wire.cpp scrivi:

void TwoWire::begin(boolean pullUp, boolean fast)
{
  rxBufferIndex = 0;
  rxBufferLength = 0;

  txBufferIndex = 0;
  txBufferLength = 0;

  twi_init(pullUp, fast);
}

ma se non erro con le pullup interne non si può raggiungere la frequenza di 400khz, o almeno cosi ho sempre pensato. Da quel che so servirebbero resistenze da 4,7kohm, mentre le interne sono da 20kohm. Se cosi fosse i comandi pullup e fast si annullano a vicenda, ovvero se metti pullup non mettu fast e se metti fast disattivi le pullup.

P.S. io la chiamerei NewI2C. I2C asincrona è un non-sense. :D

Mi spiego meglio.
Attualmente la classe Wire è bloccante; ciò vuol dire che se scrivi o leggi dei dati, rimani bloccato in un paio di cicli che attendono che lo status di READY ridiventi disponibile. (edit: esattamente quelli di cui parla PaoloP)
Io non ho fatto altro che smembrare le funzioni in presenza di questi cicli in due differenti funzioni; in questo modo, anzichè attendere l’esecuzione della i2c bloccati in un ciclo si può proseguire l’esecuzione del programma.
Ovviamente ho aggiunto i debiti controlli (e relativi messaggi di errore) nel caso si tenti erroneamente di chiamare letture/scritture che si accavallano, ed ho anche aggiunto una funzione per leggere lo “stato” della comunicazione.
Quindi dal punto di vista del programmatore ora la comunicazione è asincrona, poichè non più loccante. Fose il termine è forzato, suggeritene uno che sia più corretto.
Infine ho sistemato le vecchie read e write per rimanere bloccanti, e quindi essere retrocompatibili.

Invece volendo usare la potenzialità della nuova libreria il pseudocodice è:

  • Wire.begin()
  • controllare che lo stato sia i2c ready
  • fare una richiesta di lettura/scrittura ask(read/write), usando a priori come al solito beginTransmission o requestFrom
  • se e quando voluto controllare che sia arrivata la risposta

In questo modo in caso di errore di comunicazione (distacco dei cavi, ecc.) il codice non si blocca in un ciclo infinito.

esatto, questo è uno dei vantaggi nell’avere funzioni non bloccanti

hai ragione anche per quanto riguarda l’asseganzione, all’inizio pensavo di permettere frequenze arbritarie, poi ho deciso di harcodare le 2 frequenze principali (e uniche?) ed il codice è rimasto “sporco”.
Credo che frequenze arbitrarie siano fattibili, magari prima di fare la pull-request al team arduino avrò tempo e voglia di fare i test ed includere una serie di begin apposite. (quà mi piacerebbe sentire il vostro parere)

riedit: devo scrivere una fuinzione di “reset”, CREDO basti rifare la begin e azzerarre qualche variabile quà e là, ma se conoscete qualche trucco fatemi sapere!

ma se non erro con le pullup interne non si può raggiungere la frequenza di 400khz, o almeno cosi ho sempre pensato. Da quel che so servirebbero resistenze da 4,7kohm, mentre le interne sono da 20kohm. Se cosi fosse i comandi pullup e fast si annullano a vicenda, ovvero se metti pullup non mettu fast e se metti fast disattivi le pullup.

non so, credo che dipenda più dalla lunghezza dei cavi

P.S. io la chiamerei NewI2C. I2C asincrona è un non-sense.

più che un nome di librerira (visto che comunque è ideata per RIMPIAZZARE la wire), quello che serve è una parola da anteporre alle funzioni. per ora ho messo ASK per le richieste e AsincRead/AsincWrite per le funzioni che verificano che la transazione sia completata ed in caso positivo ritornino il risulatato

lesto:

ma se non erro con le pullup interne non si può raggiungere la frequenza di 400khz, o almeno cosi ho sempre pensato. Da quel che so servirebbero resistenze da 4,7kohm, mentre le interne sono da 20kohm. Se cosi fosse i comandi pullup e fast si annullano a vicenda, ovvero se metti pullup non mettu fast e se metti fast disattivi le pullup.

non so, credo che dipenda più dalla lunghezza dei cavi

Si, dipende dalla capacitanza del Bus a cui bisogna associare il valore delle resistenze. A pag. 327 (o 321, dipende dalla versione del datasheet) del datasheet dell'atmega328 ci sono le formule per calcolare i valori. (1000ns/Cb per 100khz oppure max 300ns/Cb per 400khz) La massima capacitanza del bus non può superare i 400pF.

Riguardo alla velocità del Bus c'è anche scritto (pag. 223):

Bit Rate Generator Unit This unit controls the period of SCL when operating in a Master mode. The SCL period is controlled by settings in the TWI Bit Rate Register (TWBR) and the Prescaler bits in the TWI Status Register (TWSR). Slave operation does not depend on Bit Rate or Prescaler settings, but the CPU clock frequency in the Slave must be at least 16 times higher than the SCL frequency. Note that slaves may prolong the SCL low period, thereby reducing the average TWI bus clock period. The SCL frequency is generated according to the following equation: SCL frequency = CPU Clock frequency / [16 + 2(TWBR) ? (PrescalerValue)]

Interfacce seriali sincrone hanno un clock che dice quando il segnale dati è valido. Interfacce seriali asincrone non hanno questo clock e la sincroniyyayione avviene con altri metodi. I2C è sincrona. La libreria attuale non necessita il controllo se il bus è libero. La Tua proposta ha come particolarità che devi controllare se l'ayione precedente è terminata per recuperare i dati ricevfuti o per spedire altri dati. Non vedo in generale grandi vantaggi in questa politica di funzionamento. Ciao Uwe

La libreria attuale non necessita il controllo se il bus è libero

perchè ti tiene bloccato finchè non lo è, nel caso di una richiesta, e finchè non si libera ( hai ricevuto tutti i dati) nel caso di una risposta

Non vedo in generale grandi vantaggi in questa politica di funzionamento

  • non bloccare l'arduino in caso di "failure" dell'i2c (es. cavi staccati, disturbi, sensore guasto)
  • di conseguenza permettere l'inserimeto/staccamento plug-and play di sensori i2c
  • risparmio di cicli di clock (stima di 160 per bit a 100.000 e 40 a bit per 400.000)
  • retrocompatibilità con la wire originale

se vogliamo ci son stati molti meno vantaggi a trasformare la seriale in non bloccante, visto che i primi 2 punti non sono un problema per il protocollo, il 3° ha significato se si usa meno di 115.200 (ed ho personalmente usato la seriare arduino UNO senza modifiche a 921.600 baud), ed il 4° non è stato rispettato ( vedi flush() )

iniziato i test!

risolto un piccolo bug, testato con un ADXL345 funziona alla perfezione usando pull-up interne, velocità standard (100.000Hz), e chiamate “retrocompatibili”.

Ah, dimenticavo di sottolineare che le chiamate “sbloccate” e chiamate bloccanti possono essere mischiate tra loro senza problemi.

Quindi a breve un test con le chimate sbloccanti molto semplice: incrementerò 2 variabili all’interno del loop, una ad ogni loop ed una ad ogni lettura, poi le stamperò ed azzererò ogni secondo.
In questo modo nel caso delle chiamate bloccanti dovremmo vedere le due variabili identiche.
Nel caso delle sbloccanti dovremmo vedere le sue varibili sensibilmente diverse tra loro.
Mi aspetto che la variabile rappresentante il numero di letture al secondo sia pressochè uguale, magari un poco in svantaggio per la “sbloccante”, ed invece la variabile che conta i loop sensibilmente più grande.

Infine dopo il primo giro di test aggiungerò alle chiamate bloaccanti un timeout (di default ad infinito, per mentenere la retrocompatibilità), oltre che rendere disponibile tramite get l’ultima operazione “ask”, in modo da rendere facile da implementare per l’itente un timeout anche per le operazioni sbloccanti.

trovato in giro per internet anche un ottimo codice di reset della Wire: TWCR=0, poi può essere tranquillamente richiamata la wire.begin(). Quindi più che Wire.reset() la chiamero Wire.end()

edit: editato titolo

test 0 ovvero test di riferimento: numero di loop al secondo facendo un semplice variabile+1 ogni loop. In ogni test la seriale è a 9600 baud

x: 0 y: 0 z: 0
loopC: 89276
readC: 89276
x: 0 y: 0 z: 0
loopC: 89366
readC: 89366

test n°1: sensore con libreria mia ma chiamate bloccanti (classiche):

x: -7 y: 3 z: -128
loopC: 896
readC: 896
x: -7 y: 3 z: -128
loopC: 896
readC: 896
x: -6 y: 2 z: -127
loopC: 895
readC: 895

si notano i valori letti, il numero di cicli di loop al secondo e di letture al secondo. essendo bloccante notare come i cicli di lettura e loop siano la stessa cifra.

ed infine test n°2: sensore con libreria mia & uso di chiamate non bloccanti, notare che ho mosso il sensore durante il test :)

x: -2 y: -126 z: 220
loopC: 56994
readC: 908
status: 2
x: -8 y: -105 z: 160
loopC: 56929
readC: 907
status: 0
x: -8 y: 3 z: 127
loopC: 56989
readC: 909
status: 0
x: -7 y: 2 z: 129
loopC: 56940
readC: 907
status: 1

conclusioni: possiamo ora tracciare qualche conclusione. a scapito di un codice più complesso (posterò di seguito i 2 codici con un analisi delle differenze) l'uso della chiamata non bloccante ci consente di recuperare circa il 64% della potenza CPU! per considerare il risparmio ho fatto 56940(loop test2)/89276(loop test 0), si può dire che il sistema è grezzo, ma sicuramente funzionale per farsi un'idea. devo ammettere che il guadagno è più alto di quanto mi aspettassi. che ne dite?

Bello, anche se non ho capito i test a cosa si riferivano XD

codice sincrono:

void readFrom(byte address, int num, byte _buff[]) {
  Wire.beginTransmission(DEVICE); // start transmission to device 
  Wire.write(address);             // sends address to read from
  Wire.endTransmission();         // end transmission

  Wire.beginTransmission(DEVICE); // start transmission to device
  Wire.requestFrom(DEVICE, num);    // request 6 bytes from device

  int i = 0;
  while(Wire.available())         // device may send less than requested (abnormal)
  { 
    _buff[i] = Wire.read();    // receive a byte
    i++;
  }
  Wire.endTransmission();         // end transmission

  int x = (((int)_buff[1]) << 8) | _buff[0];   
  int y = (((int)_buff[3]) << 8) | _buff[2];
  int z = (((int)_buff[5]) << 8) | _buff[4];
}

insomma, esattamente lo stesso identico codice che usiamo normalmente con la Wire. (infatti è un copia-incolla da Jens C Brynildsen http://www.flashgamer.com)

versione non bloccante:

void readFrom(byte address, int num, byte _buff[]) {
  if ( stato == 0 && Wire.asincBeginTransmission(DEVICE) ){ // start transmission to device 
    Wire.write(address);             // sends address to read from
    Wire.asincEndTransmission(true);         // end transmission
    stato = 1;
  }

  if ( stato == 1 && Wire.asincBeginTransmission(DEVICE) ){ // start transmission to device
    Wire.asincRequestFrom(DEVICE, num);    // request 6 bytes from device
    stato = 2;
  }

  if (stato == 2 && Wire.asincReady(num-1) ){
    Wire.asincRead(_buff, num-1); // receive a bytes
    Wire.asincEndTransmission(true);         // end transmission
    stato = 0;
    x = (((int)_buff[1]) << 8) | _buff[0];   
    y = (((int)_buff[3]) << 8) | _buff[2];
    z = (((int)_buff[5]) << 8) | _buff[4];
  }
}

prima di tutto notiamo che esistono 3 stati:
stato 0: se l’i2c è libera (Wire.asincBeginTransmission(DEVICE) ritorna true), setta il registro da leggere sul sensore. Viene usata la normale write perchè non è essa ad essere bloccante, ma la Wire.endTransmission, che riattende lo stato di i2c libera (ma come vedremo a noi non serve)
stato 1: se l’i2c è libera richiede di leggere “num” byte dal device. Notare che la comunicazione rimane aperta in attesa di risposta.
stato 2: se sono presenti “num-1” (da sistemare, in effetti a “num”), legge in botta tutti i dati. Fa richiesta di chiusura comunicazione.

In pratica in questo modo evitiamo:

  1. di rimanere in attesa ad ogni BeginTransmission per lo stato di ready (molto utile per letture ricorrenti)
  2. di rimanere in attesa ad ogni EndTransmission per lo stato di ready (molto utile in caso di write)
  3. di rimanere in attesa dell’arrivo dei dati (utile sempre)

leo72: Bello, anche se non ho capito i test a cosa si riferivano XD

ho editato il post mano a mano che facevo i test, hai letto la conclusione? In pratica ho calcolato quanta CPU si risparmia ad usare la mia Wire modificata per non essere bloccante (il 60%!)

in realtà devo anche capire da dove saltino fuori quelle 10 letture in più tar sincrona e asincrona, probabilmente dal fatto che la endTrasmission tra la lettura stato 3 e la richiesta stato 0 è completata in parallelo con le operazioni di calcolo x, y e z... e quindi rende le richieste molto più incalzanti rispetto alla Wire.

Se vi chiedere come mai passo true alla AsinEndTrasmission, sappiate che è per indicare se inviare il segnale di stop. Esiste anche per la normale EndTrasmission, e se non specificata (come nell'esempio) è sempre true. In pratica non nviare il segnale di stop rende la comunicazione più veloce, ma è incompatibile con vari sensori. Semplicemente non ho implementato la chiamata che passa di default TRUE, ma ci vogliono 5 secondi di numero per farlo :)

Sì, ho notato. Sono curioso di vedere il codice della lib.

Ah, a proposito. Perché invece di modificare la Wire inserendo i nuovi metodi non crei una nuova Wire, denominata Wire2 o aWire o quel che vuoi? Così da separare (al momento) la tua lib da quella di Arduino.

perchè le modifiche non sono solo nella Wire ma anche, anzi sopratutto, nella twi. Mentre per la Wire la soluzione sarebbe quella di estendere la classe, per la TWI vedo soluzione. Certo potrei fare un lib a parte, ma visto che l'idea è di proporla come modifica ufficiale della Wire mi porto aventi col lavoro. Tra l'altro potrei metterci dentro pure il codice contro il bug della write(0), già che ci sono.. Il codice della libreria lo trovi su github, il link è nel primo post oppure nella mia firma... quì il link diretto all'esempio che usa il codice asincrono, con le lib. https://github.com/lestofante/arduino/tree/master/asincI2Ctest2_asinc

ps. ti consiglio di imparare a usare git, per esempio nel progetto di arduino puoi vedere tutte le pull-request degli utenti per risolvere i bug; https://github.com/arduino/Arduino/pulls

Ho cominciato a "spippolare" su Git. Ho anch'io un paio di repo, ho messo su la swRTC ed il leOS: https://github.com/leomil72

ottimo, ho messoil wath sui tuoi repo così dovrebbe avvisarmi quando fai qualcosa.

nel frattempo sto sistemando la libreria per sostituire le chiamate asincXXX() con askXXX(), e le chiamate bloccanti che usano le funzioni Wire non blocanti al posti che chiare direttamente la twi. Ovviamente per ora l’ho rotta e quindi non la posto.

stavo però pensando a creare un sistema per mettere in coda più di una richiesta, attraverso di un array di strutture usata sia per la richiesta che per la risposta. In questo modo elimino al 100% i tempi morti, se riesco a legarmi direttamente agli interrupt… devo verificare la fattibilità, voi che ne dite?

lesto: ottimo, ho messoil wath sui tuoi repo così dovrebbe avvisarmi quando fai qualcosa.

nel frattempo sto sistemando la libreria per sostituire le chiamate asincXXX() con askXXX(), e le chiamate bloccanti che usano le funzioni Wire non blocanti al posti che chiare direttamente la twi. Ovviamente per ora l'ho rotta e quindi non la posto.

stavo però pensando a creare un sistema per mettere in coda più di una richiesta, attraverso di un array di strutture usata sia per la richiesta che per la risposta. In questo modo elimino al 100% i tempi morti, se riesco a legarmi direttamente agli interrupt... devo verificare la fattibilità, voi che ne dite?

Che mi riservo il giudizio dopo una prova, prova che a breve non posso però condurre.... intanto massimo apprezzamento per il lavoro svolto. ;)