Guardar datos complejos en la SD [INFO]

Hola,
como es un tema bastante recurrente, ahí va un ejemplo de cómo guardar y recuperar datos complejos en una SD, porque casi siempre los ejemplos se suelen reducir a datos en forma de texto.

La mejor opción para ello, a mi modo de ver, es utilizar una estructura de datos, donde se pueden incluir toda clase de variables (no punteros).

Por ejemplo, si se define una estructura como esta

struct datos
{
  int id;  
  char nombre[11];
  float temperatura;
  float humedad;
} datos, datos2;

Se puede escribir y leer la estructura como un todo en la SD. Para probar, poner primero contenido en la estructura,

   datos.id = 33;
  datos.temperatura = 25.3;
  datos.humedad = 77.7;
  strcpy(datos.nombre, "lugar_A");

Para escibir, como la función write pide un buffer del tipo const uint8_t hay que hacer una conversión explícita

  myFile = SD.open("test.txt", FILE_WRITE);
  if(myFile)
  {
     Serial.println("abierto para escribir 'test.txt'");
     myFile.write((const uint8_t*)(&datos), sizeof(datos));
     myFile.close();
  }

Y para leer, en el ejemplo se utiliza datos2 para comprobar que los datos se cargan desde el fichero, aunque se podría haber usado datos

     myFile = SD.open("test.txt");
     if(myFile)
     {
        Serial.println("abierto para leer 'test.txt'");

        myFile.read(&datos2, sizeof(datos2));

        // y para mostrar por la consola
        Serial.print("id: ");
        Serial.println(datos2.id, DEC);
        Serial.print("nombre: ");
        Serial.println(datos2.nombre);
        
        Serial.print("temperatura: ");
        Serial.println(datos2.temperatura, DEC);  
        Serial.print("humedad: ");
        Serial.println(datos2.humedad, DEC);  
        
        myFile.close();
     }

Tan solo queda un detalle por arreglar: la salida por consola me da

temperatura: 25.2999992370
humedad: 77.6999969482

Ahí va el sketch entero por si alguien lo quiere probar

/* 
Conexiones para un Arduino Uno:
uint8_t const SS_PIN   = 10;
uint8_t const MOSI_PIN = 11;
uint8_t const MISO_PIN = 12;
uint8_t const SCK_PIN  = 13;

*/

#include <SD.h>

File myFile;

int kont;

struct datos
{
  int id;
  float temperatura;
  float humedad;
  char nombre[11];
} datos, datos2;

void setup()
{
  kont = 0;
  Serial.begin(9600);
  Serial.print("Initializing SD card...");
  pinMode(10, OUTPUT);
 
  if (!SD.begin(10)) 
  {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
   
  datos.id = 33;
  datos.temperatura = 25.3;
  datos.humedad = 77.7;
  strcpy(datos.nombre, "lugar_A");
 
  myFile = SD.open("test.txt", FILE_WRITE);
  if(myFile)
  {
     Serial.println("abierto para escribir 'test.txt'");
     myFile.write((const uint8_t*)(&datos), sizeof(datos));
     myFile.close();
       
     myFile = SD.open("test.txt");
     if(myFile)
     {
        Serial.println("abierto para leer 'test.txt'");

        myFile.read(&datos2, sizeof(datos2));
        Serial.print("id: ");
        Serial.println(datos2.id, DEC);
        Serial.print("nombre: ");
        Serial.println(datos2.nombre);
        
        Serial.print("temperatura: ");
        Serial.println(datos2.temperatura, DEC);  
        Serial.print("humedad: ");
        Serial.println(datos2.humedad, DEC);  
        
        myFile.close();
     }
     else
     {
         Serial.println("error read test.txt");
     }
  }
  else 
  {
      // if the file didn't open, print an error:
      Serial.println("error FILE_WRITE test.txt");
  }
}


void loop()
{
  
}

Como te lo has currado :slight_smile:
+1

enhorabuena por el post. muy util !

Podrias dar mas detalles de estas lineas=

myFile.write((const uint8_t*)(&datos), sizeof(datos));
  1. datos se supone que es una STRUCTURA, y lo estas convirtiendo (cast) a un "unsigned integer 8 bits" ?
  2. El "&" se usa para que el cast se realice sobre los elementos que integran la Structura, en lugar de tener que referirse a cada elemento por separado?
  3. Sizeof, simplemente le dice cuantos elementos van a escribirse, es decir, los que hay dentro de la estructura?
    por lo que realmente la funcion write, aquí es algo compleja y hace una lectura de todos los elementos de la estructura de uno en uno?

He entendido algo bien? puedes corregirme donde me equivoco?
muchas gracias

Sergegsx:
enhorabuena por el post. muy util !

Podrias dar mas detalles de estas lineas=

myFile.write((const uint8_t*)(&datos), sizeof(datos));
  1. datos se supone que es una STRUCTURA, y lo estas convirtiendo (cast) a un "unsigned integer 8 bits" ?
  2. El "&" se usa para que el cast se realice sobre los elementos que integran la Structura, en lugar de tener que referirse a cada elemento por separado?
  3. Sizeof, simplemente le dice cuantos elementos van a escribirse, es decir, los que hay dentro de la estructura?
    por lo que realmente la funcion write, aquí es algo compleja y hace una lectura de todos los elementos de la estructura de uno en uno?

He entendido algo bien? puedes corregirme donde me equivoco?
muchas gracias

Hola. Efectivamente datos es una estructura que contiene la información que deseamos guardar.
El & es el operador de indirección, y lo que hace es devolvernos un puntero a esa estructura, es decir, la dirección de memoria donde está guardada; osea que &datos es un puntero a nuestra struct de datos.
Como la función write necesita un (puntero a uint8) se realiza el casting a (uint8 *) para evitar un toque de atención del compilador. Así que lo que hace write es escribir (sizeof(datos)) bytes (uint8) a partir de la dirección de memoria donde está la estructura, sin importarle qué tipo de datos contiene.
Como añadido, señalar que este sistema es sencillo y efectivo, pero hay que tener en cuenta que nuestra struct no contenga punteros, porque copiará eso, el puntero y no el contenido (por ejemplo si tenemos un char *cadena dentro de nuestra estructura deberemos ocuparnos de copiar la cadena aparte).

Hola, Sergegsx
@noter lo ha explicado a la perfección.
Lo que no sé por qué es necesaria esa conversión explícita a uint8 ('unsigned int de 8 bites'), cuando se podría hacer lo mismo con un unsigned char o un byte, porque lo que se transmite cada vez por el puerto serie es eso, una serie de 8 bites.

muchisimas gracias noter, me ha quedado clarisimo y creo que por fin he entendido al menos 1 uso practico de los punteros, hasta ahora nunca habia entendido para que usarlo.
mil gracias

curro92 gracias por el post, parece sencillo pero creo que seria buena idea meterlo en el playground si no lo esta ya.

curro92:
Hola, Sergegsx
@noter lo ha explicado a la perfección.
Lo que no sé por qué es necesaria esa conversión explícita a uint8 ('unsigned int de 8 bites'), cuando se podría hacer lo mismo con un unsigned char o un byte, porque lo que se transmite cada vez por el puerto serie es eso, una serie de 8 bites.

Creo que los tres datos son equivalentes, y de hecho también compila sin errores (byte )&datos y (unsigned char)&datos. A mí, personalmente, me gusta usar la notación byte, pero para otros tal vez sea más explícito el uint8 (para gustos los colores :)).

se podria hacer lo mismo pero introduciendole tu los datos de id y los de nombre? por puerto serie,sin tener k estar ya prefijados?
gracias

Sí se puede. Lo único es que los datos (la estructura guardada) deben tener la misma longitud. Por ello no se puede hacer con punteros; es decir, no puedes poner en tu estructura algo así:

struct datos {
unsigned long id;
char *nombre;
};

porque nombre no son datos, sino un puntero hacia unos datos. Sí podrías hacerlo así:

struct datos {
unsigned long id;
char nombre[50]; //la longitud máxima que podríamos introducir es de 50 caracteres.
}

Pero creo que mejor volvemos a tu post, y explicas un poco más exactamente qué quieres hacer.

curro92:
Hola,
como es un tema bastante recurrente, ahí va un ejemplo de cómo guardar y recuperar datos complejos en una SD, porque casi siempre los ejemplos se suelen reducir a datos en forma de texto.

La mejor opción para ello, a mi modo de ver, es utilizar una estructura de datos, donde se pueden incluir toda clase de variables (no punteros).

Por ejemplo, si se define una estructura como esta

struct datos

{
  int id; 
  char nombre[11];
  float temperatura;
  float humedad;
} datos, datos2;




Se puede escribir y leer la estructura como un todo en la SD. Para probar, poner primero contenido en la estructura,



datos.id = 33;
  datos.temperatura = 25.3;
  datos.humedad = 77.7;
  strcpy(datos.nombre, "lugar_A");




Para escibir, como la función **write** pide un buffer del tipo **const uint8_t** hay que hacer una conversión explícita



myFile = SD.open("test.txt", FILE_WRITE);
  if(myFile)
  {
    Serial.println("abierto para escribir 'test.txt'");
    myFile.write((const uint8_t*)(&datos), sizeof(datos));
    myFile.close();
  }




Y para leer, en el ejemplo se utiliza **datos2** para comprobar que los datos se cargan desde el fichero, aunque se podría haber usado **datos** 



myFile = SD.open("test.txt");
    if(myFile)
    {
        Serial.println("abierto para leer 'test.txt'");

myFile.read(&datos2, sizeof(datos2));

// y para mostrar por la consola
        Serial.print("id: ");
        Serial.println(datos2.id, DEC);
        Serial.print("nombre: ");
        Serial.println(datos2.nombre);
       
        Serial.print("temperatura: ");
        Serial.println(datos2.temperatura, DEC); 
        Serial.print("humedad: ");
        Serial.println(datos2.humedad, DEC); 
       
        myFile.close();
    }




Tan solo queda un detalle por arreglar: la salida por consola me da


temperatura: 25.2999992370
humedad: 77.6999969482





Ahí va el sketch entero por si alguien lo quiere probar



/*
Conexiones para un Arduino Uno:
uint8_t const SS_PIN  = 10;
uint8_t const MOSI_PIN = 11;
uint8_t const MISO_PIN = 12;
uint8_t const SCK_PIN  = 13;

*/

#include <SD.h>

File myFile;

int kont;

struct datos
{
  int id;
  float temperatura;
  float humedad;
  char nombre[11];
} datos, datos2;

void setup()
{
  kont = 0;
  Serial.begin(9600);
  Serial.print("Initializing SD card...");
  pinMode(10, OUTPUT);

if (!SD.begin(10))
  {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
 
  datos.id = 33;
  datos.temperatura = 25.3;
  datos.humedad = 77.7;
  strcpy(datos.nombre, "lugar_A");

myFile = SD.open("test.txt", FILE_WRITE);
  if(myFile)
  {
    Serial.println("abierto para escribir 'test.txt'");
    myFile.write((const uint8_t*)(&datos), sizeof(datos));
    myFile.close();
     
    myFile = SD.open("test.txt");
    if(myFile)
    {
        Serial.println("abierto para leer 'test.txt'");

myFile.read(&datos2, sizeof(datos2));
        Serial.print("id: ");
        Serial.println(datos2.id, DEC);
        Serial.print("nombre: ");
        Serial.println(datos2.nombre);
       
        Serial.print("temperatura: ");
        Serial.println(datos2.temperatura, DEC); 
        Serial.print("humedad: ");
        Serial.println(datos2.humedad, DEC); 
       
        myFile.close();
    }
    else
    {
        Serial.println("error read test.txt");
    }
  }
  else
  {
      // if the file didn't open, print an error:
      Serial.println("error FILE_WRITE test.txt");
  }
}

void loop()
{
 
}

Ya si queremos rizar el rizo podríamos usar estructuras dinámicas para dar un respiro a la limitada memoria de nuestros controladores:

typedef struct s { 
     int id;  
     char nombre[11];
     float temperatura;   
     struct s *siguiente; 
 } elemento; 

elemento *c; 

elemento *nuevo_ elemento(void)  { 
    elemento *q; 
    q = (elemento *)malloc(sizeof(elemento)); 
    if(!q) {
        Serial. Println ("No se ha reservado memoria para el nuevo nodo”); 
    return q; 
 }

void loop () { 
    elemento *q; //puntero a un nodo 
    q = NULL; //lista vacía 
    //Reserva dinámica de mem 
    q = nuevo_elemento(); //q apunta al nodo recién creado 
    q->sig = NULL; //fin de lista 
 
     // Aquí operaciones de escritura
 
     //Liberación de memoria reservada 
     free(q); 
 }

Si analizas que quien pregunta tiene problemas para enviar un dato por el puerto serie y le respondes con punturos por dios!!! imagina donde va a esconderse?

Excelente tu aporte ahernandovalbuena pero fuera de lugar para este caso.
A mi me gusta tu sugerencia pero en otro contexto.

De todas formas para todo esto yo prefiero usar EDB Extended Data base Library. Todo hecho para EEPROM interna o externa o para SD.

surbyte:
Si analizas que quien pregunta tiene problemas para enviar un dato por el puerto serie y le respondes con punturos por dios!!! imagina donde va a esconderse?

Excelente tu aporte ahernandovalbuena pero fuera de lugar para este caso.
A mi me gusta tu sugerencia pero en otro contexto.

EDITO post anterior haciendo cita al primero para evitar estas confusiones.

De todas formas para todo esto yo prefiero usar EDB Extended Data base Library. Todo hecho para EEPROM interna o externa o para SD.

Entendí que el post era de carácter informativo y solo busque aportar mayor precisión, stry si alguien se confundió con mis palabras.

De la misma manera, mirare la librería que dices pero algo hay claro, el uso de punteros en este caso es fundamental.

Un saludo.

No te lo discuto ahernandovalbuena y me parece excelente tu aporte.
Solo mira esto y entenderás mi respuesta

se podria hacer lo mismo pero introduciendole tu los datos de id y los de nombre? por puerto serie,sin tener k estar ya prefijados?

salvewis:
se podria hacer lo mismo pero introduciendole tu los datos de id y los de nombre? por puerto serie,sin tener k estar ya prefijados?
gracias

Buenas tardes,

Para la duda que me comentas se me ocurre la posibilidad de hacer una lectura del puerto serial, introduciendo los datos una vez reconozcamos la palabra clave INSERT:

#define TOTAL 20

char Texto[TOTAL];
char Caracter=-1; 
unisgned int Indice= 0; 


char Comparador(char* CadenaThis) {
    while (Serial.available() > 0) {
        if(Indice< TOTAL - 1) {
            Caracter= Serial.read(); 
            Texto[Indice] = Caracter; 
            Indice++;
            Texto[Indice] = '\0'; 
        }
    }

    if (strcmp(Texto,CadenaThis)  == 0) {
        for (int i=0;i<TOTAL - 1;i++) {
            Texto[i]=0;
        }
        Indice=0;
        return(0);
    } else {
        return(1);
    }
}

void loop() {
    if (Comparador("INSERT")==0) {
        //Volveriamos a leer del serial pero en este caso el nombre o la cadena.

    } 
}

No sé, lo he escrito un poco rápido y a lo mejor no tiene mucho sentido. Habría que darle una pensada.

Un saludo!