Accesso a Struct come fosse array di byte

Ciao a tutti.
Sono 2 giorni che cerco di venirne a capo ma proprio non ci riesco. Alcuni argomenti del C mi sono sempre rimasti indigesti e adesso ne pago le conseguenze…

Faccio una piccola premessa per inquadrare il problema:
voglio trasmettere dei dati tra 2 arduino e sto facendo degli esperimenti con i moduli HC12. L’idea è quella di crearmi 2 funzioni (una per trasmettere e una per ricevere) da poter utilizzare alla bisogna in vari progetti.
Da qui l’idea di infilare i dati in una struct e inviarla un byte alla volta come fosse un “array”. In questo modo anche se cambia la struct la funzione di invio e ricezione rimane la stessa.

In pratica ho pensato ad una struttura tipo

typedef struct {
    uint32_t idmsg;
    uint8_t  destinatario;
    uint32_t valore1;
    uint32_t valore2;
    // ecc
} t_dati;

#define DIMDATI (sizeof(t_dati))

typedef struct {
  t_dati dati;
  uint32_t crc;
} t_pack;

#define DIMPACK (sizeof(t_pack))

t_pack pacchetto;

Quindi inserisco i miei dati dentro pacchetto.dati poi calcolo il CRC e poi invio il tutto.

La funzione che calcola il CRC è questa (sempre grazie a Guglielmo per averla condivisa in altre discussioni)

unsigned long calc_crc32(unsigned char* buf, unsigned int len)
{
    unsigned long crc;
    unsigned int i;
    
    crc = 0xffffffff;
    for (i = 0; i < len; i++)
      #if defined(__SAM3X8E__)
        crc = ((crc >> 8) & 0x00ffffff) ^ crctab32[(crc ^ *buf++) & 0xff];
      #else
        crc = ((crc >> 8) & 0x00ffffff) ^ pgm_read_dword_near(&crctab32[(crc ^ *buf++) & 0xff]);
      #endif
    return (crc ^ 0xffffffff);
}

Nella mia idea vorrei poter chiamare la funzione del CRC più o meno così:

pacchetto.crc = calc_crc32(pacchetto.dati, DIMDATI);

e inviare i dati più o meno così:

  for (byte i=0; i < DIMPACK; i++) {
    HC12.write(pacchetto[i]);
  }

So che scritto così il codice non funziona, è solo per farvi capire.
Come dovrei passare il primo parametro alla funzione CRC?
Come posso accedere alla struttura pacchetto nel for per inviare un byte alla volta?

E’ venuto un papiro… scusate.
Grazie in anticipo a chi avrà tempo e voglia di leggere tutto il post.

EDIT: ho dimenticato di scrivere che so che il tutto è fattibile con delle union.
Ho provato e funziona, ma si complica la struttura. Se ci fosse il modo di farlo senza union sarebbe più semplice.

Alla seconda tisana alla canapa mi è venuto un colpo di cu.. ehm genio...
Per il crc così SEMBRA funzionare

trasmissione.crc = calc_crc32((unsigned char *)&trasmissione.dati,DIMDATI);

Non ho ben chiaro il perché... mi sa che devo dare una ripassata ai puntatori...

Con avr la dimensione in bit dell'indirizzo in cui si trova il puntatore è sembra di 16 bit indipendentemente dal dato a cui punta. (unsigned char *) &miaVarStruct leggile da destra verso sinistra.
miaVarStruct il contenitore
& estrai indirizzo della 1 cella del contenitore
(unsigned char *) reinterpreta il tipo di puntatore che anziché puntare a tipo struct, punta a byte.

Allora buff++ punta al prossimo byte e così in ciclo fino che non si raggiunge il valore della variabile len.

Ciao.

Grazie della spiegazione. Ma da quello che leggo capisco più che altro le mie lacune in materia... Devo ripescare il k&r...

fratt:
Alla seconda tisana alla canapa mi è venuto un colpo di cu.. ehm genio...
Per il crc così SEMBRA funzionare

trasmissione.crc = calc_crc32((unsigned char *)&trasmissione.dati,DIMDATI);

Non ho ben chiaro il perché... mi sa che devo dare una ripassata ai puntatori...

Il compilatore controlla il tipo di dato del parametro. Quella funzione si aspetta un puntatore.
Ma cosa deve puntare ? una zona dati (array) dove ci si aspetta di avere singoli elementi unsigned char.
trasmissione.dati è un array, Con & prendi l'indirizzo prima cella e con il cast forzi al tipo di dato aspettato.
Di base interessa che gli passiamo un puntatore quindi un indirizzo. In teoria un puntatore è un puntatore, sono tutti uguali. Quello che cambia è a COSA puntano.
Pensa alla memcpy(dest,source,quantibyte);
copi dei byte da source a dest. Se poi il source è un array di interi , alla memcpy frega nulla, tanto copia a byte (e parametri sono void * quindi non serve cast)
Il tipo di puntatore è importante solo nel momento in cui fai matematica sul puntatore.
Cioè se hai un puntatore a char, punt+1 aumenti indirizzo di 1 byte se puntatore a int, punt+1 aumenti indirizzo di 2 byte
Gli array vengono trasformati dal compilatore in puntatori, quindi vChar[1] o vInt[1] dove vChar e vInt sono array/puntatori ma il 1 di vChar ti fa andare al secondo byte, mentre il 1 di vInt ti fa andare al terzo byte (la prima cella è int quindi 2 byte) per pescare il secondo int C Pointer Arithmetic | Studytonight
Spero di non averti confuso di più le idee

Intanto grazie anche a te.
Dirti che ho capito tutto tutto sarei un bugiardo... stasera riprendo il trattamento alla canapa e mi rileggo con calma sia il tuo post che quello di Mauro, magari capisco qualcosa di più.

Vi chiedo un'altra cortesia... nel Serial.write del for come dovrei passare il puntatore per scorrere tuta la struttura un byte alla volta? Nello stesso modo?

Serial.write((unsigned char *)&trasmissione.dati + i);

In questo caso la matematica sul puntatore è corretta?

Ti confondo ancora un po' di più le idee ... :smiley: :smiley: :smiley:

Su AVR a 8 bit non dovrebbe accadere, ma un'altra cosa che devi tenere conto è che il compilatore, per ragioni di ottimizzazione, può manipolare la "struct" facendo "padding", ovvero allinenado alla lunghezza della "parola" della MCU il dato.

Questo è specialmente vero su MCU a 32 bit dove spesso il tipo 'int' a 32 bit viene allineato all'indirizzo di memoria da 32 bit ... in pratica accade una cosa del genere:

struct mystruct_A {
    char a;
    int b;
    char c;
} x;

... in realtà viene trasformato da compilatore in:

struct mystruct_A {
    char a;
    char gap_0[3]; /* inserito dal copilatore per allineare b */
    int b;
    char c;
    char gap_1[3]; /* inserito dal copilatore per allineare l'intera struttura e ciò che segue */
} x;

La cosa può essere evitata forzando il compilatore a fare "packing" ovvero ... a NON allineare (peggiorando le performances del programma) scrivendo in questo modo:

struct __attribute__((__packed__)) mystruct_A {
    char a;
    int b;
    char c;
};

... che, anche su una MCU a 32 bit ('int' da 32 bit), forza un'occupazione di soli 6 bytes :slight_smile:

Guglielmo

P.S.: Piccola annotazione: l'accesso alla memoria non allineata è più lento sulle architetture che lo consentono (come x86 e amd64), ma è esplicitamente proibito su architetture ad allineamento "rigoroso" come SPARC.
P.P.S.: Un interessante lettura sull'argomento: The Lost Art of Structure Packing

Ok, è ufficiale... Non c'ho capito un ca...
Spero di avere abbastanza tisana alla canapa per riuscire a digerire anche questo post...

Grazie Guglielmo, stasera me lo rileggo con più calma.

fratt:
Serial.write((unsigned char *)&trasmissione.dati + i);
In questo caso la matematica sul puntatore è corretta?

Sinceramente non mi ricordo la priorità degli operatori in questo caso. mi pare corretto, non ambiguo.
Per sicurezza farei un passaggio intermedio:

unsigned char * p=(unsigned char *)&trasmissione.dati;
for(int i=0;i<=...
{ Serial.write(p+i);

P.S. la Serial.write() ha anche il metodo a due parametri, buf+len
Serial.write((unsigned char *)&trasmissione.dati, quantibyte);

nid69ita:
P.S. la Serial.write() ha anche il metodo a due parametri, buf+len
Serial.write((unsigned char *)&trasmissione.dati, quantibyte);

Azz... se l'hanno riportato pari pari anche nella software serial allora sono a cavallo.
Stasera provo il tutto.
Grazie

Occhio che con l'esempio di nid69ita stampi l'indirizzo di memoria, mentre se vuoi stampare il valore della cella di memoria puntata ti serve aggiungere l'asterisco così:

Serial.write( *(p++) );

Devo ripescare il k&r..

Ottima idea, però questa volta usa carta e matita e disegnati 4 celle di memoria assegnagli indirizzo sequenziale e dentro ogni cella ci metti i valori.

Per assurdo la variabile puntatore risiede in un cella di memoria (2 byte) per cui anche questa cella ha un suo indirizzo e questo vuol dire che si possono creare puntatori a puntatori ecc.

Il k&r ti serve principalmente per la sintassi, ma non basta.

Ciao.

Hm, non so se semplifico troppo (concedetemi e perdonatemi qualche "imprecisione", ma è voluta...) ma forse così riesci ad inquadrare meglio il discorso.

Il concetto di base (non solo del C ovviamente) è che quando tu definisci una variabile, di fatto hai un INDIRIZZO al quale corrisponde il valore al quale punta. E fin qui, ci siamo no?

Quindi se definisci ad esempio

byte valore = 3;

questo significa che all'INDIRIZZO associato al nome simbolico "valore" (ossia l'indirizzo "fisico" che il compilatore ha assegnato, e che per te è trasparente, ma puoi ottenere premettendo l'operatore "&" al nome ossia "&valore" che leggi come "indirizzo al quale punta valore") c'è UN byte che vale 3.
Di fatto è come "un array di un solo byte" diciamo.

Se lo cambi in

int valore = 3;

all'indirizzo corrispondente a "valore" ci saranno DUE byte che varranno 3 (ossia 0x0003, ovvero un byte 00 seguito da un byte 03). sotto un certo punto di vista è un array di 2 byte. E come il valore viene memorizzato a te non interessa, ci pensa il compilatore.

Se definisci una stringa, ossia

char str[] = "ABC";

Il simbolo "str" è invece esplicitamente un puntatore, ossia un indirizzo, cosa evidenziata anche dalla sintassi equivalente (dove "char*" lo leggi "puntatore a char" :

char* str = "ABC";

A tale indirizzo ci saranno 4 byte: 0x41 (ovvero 65 decimale che è il codice ASCII del carattere 'A'), 0x42 ('B0), 0x43 ('C'), 0x00 (il terminatore di stringa).

Si può accedere ad un elemento di un array di questo tipo (in questo caso un carattere) con le quadre:

Serial.print(str[1]); // Stamperà 'B'

ma trattandosi di indirizzi, di fatto equivale a:

Serial.print(*(str+1)); // Stamperà 'B'

Quindi tieni a mente che ogni variabile è, un modo o nell'altro, "un indirizzo", e quale sia il valore, quanti byte lo compongono e come sono strutturati, dipende dal tipo di dato che hai specificato al compilatore ("byte" = 1 byte, "int" = 2 byte, "char [10]" = 10 byte...), la cui lunghezza effettiva la ottieni sempre con "sizeof()".

Nel tuo caso una struttura è quindi una sequenza di byte che corrispondono ai tipi che hai specificato:

typedef struct {
    uint32_t idmsg;
    uint8_t  destinatario;
    uint32_t valore1;
    uint32_t valore2;
    // ecc
} t_dati;
...
t_dati dati;

Quindi "dati" (o, meglio, "&dati") rappresenta l'indirizzo di una istanza di quella struttura, composta da 4 byte per il primo campo, 1 per il secondo, e 4 per ciascuno degli altri due, in totale un'area di memoria di 13 byte. Il tutto assumendo, come si diceva, una architettura 8 bit.

Detto questo, ecco perché se vuoi mandare byte per byte quella struttura, si possono (devono) usare quei simboli "*" (per ottenere ciò che è puntato), "&" (per ottenere l'indirizzo), e "sizeof()" (per ottenere la dimensione dell'oggetto memorizzato).

Spero di aver descritto in modo corretto (se sbalio mi corigerete :D), chiaro e semplice (ok, semplificato...) quello che immagino tu abbia avuto come "indigesti del C"... :wink:

Maurotec:
Occhio che con l'esempio di nid69ita stampi l'indirizzo di memoria, mentre se vuoi stampare il valore della cella di memoria puntata ti serve aggiungere l'asterisco così:

Mi son rifatto all'esempio di @fratt.
e mi lascia il dubbio, perchè non ho ancora capito che parametri ha la write() essendo ereditata dalla Print e da tutta la compagnia cantante che ci stà dietro alla Stream.
Di base la write ha 3 metodi overloaded, dal sito Arduino:

  1. valore int valX=10; Serial.write(valX);
  2. string Serial.write(str);
  3. buf+len Serial.write(buf,10);
    ma non capisco se vuole un indirizzo, tipo la memcpy() oppure una stringa o un array o ancora dei singoli valori (non trovo la dichiarazione dei vari overload nel core). E non capisco come fa a discriminare un array da string (che è sempre un array)

Nel caso 3 (utile a fratt) gli devi passare p e non *p
Non capisco come invece distingua tra caso 3 e caso 2, dal fatto che str è char pointer ?

docdoc:
Se definisci una stringa, ossia

char str[] = "ABC";

Il simbolo "str" è invece esplicitamente un puntatore, ossia un indirizzo, cosa evidenziata anche dalla sintassi equivalente (dove "char*" lo leggi "puntatore a char" :

char* str = "ABC";

Non è vero....

Purtroppo la dimostrazione di questo fatto è troppo lunga per il margine di questa pagina, come disse il mio amico Pietro

Stasera dopo cena, compero un nuovo foglio di carta

nid69ita:
Mi son rifatto all'esempio di @fratt. ....

Devi guardare anche in Print.h, perché alcuni overload del metodo write() sono li dentro oltre che in Print.cpp e comunque ...
... o ha un valore, o ha un solo parametro char * e quindi assume che sia una stringa classica del 'C' o ha due valori, un puntatore ed un intero ed allora assume che sia un buffer di bytes di determinata lunghezza.

Guglielmo

Standardoil:
Non è vero....
Purtroppo la dimostrazione di questo fatto è troppo lunga per il margine di questa pagina ...

... dai, volendo non è lunghissima ... ma non voglio rubarti la scena con una scarna descrizione :smiley: :smiley: :smiley:

Guglielmo

Purtroppo serve aspettare il dopocena...
Qui ho solo il furbofono

Se qualcuno mi ruba la scena, amen
Per lui intendo:
Ne consolerò la vedova
Lo perdonerò :slight_smile:

Ringazio tutti per le spiegazioni.
Forse è la volta buona che capisco i puntatori...
Più tard vi faccio sapere a chi mandare la parcella per la consulenza.

Visto che siete tutti qua ne approfitto...

Serial.write( *(p++) );

Questo vale anche al contrario? Cioè in fase di assegnazione?
Tipo

*(p++) = Serial.read();

Qui non ho un pc con l'ide installato per fare delle prove.

Standardoil:
...
Ne consolerò la vedova
Lo perdonerò :slight_smile:

:smiley: :smiley: :smiley: :smiley: ... va beh, dico solo che basta guardare il warning che da il compilatore su Arduino per capire :wink:

Guglielmo