Writing and reading whole structs on SD.

I prepared an example for a friend and thought I’d drop a copy here.
I will try to answer questions more specific than “how does it work?” with more details than “read the code”.

This uses an LCSOFT SD adapter wired direct to an UNO. It will work fine with a home-rolled SD too.
I can’t say for any shield but differences should not be much beyond connections and CS pin number.

The “big deal” is in addressing the struct with a byte pointer and writing/reading sizeof() the struct bytes.
You can transfer many variables (settings, SRAM image, etc) with very little code this way.

// Testing SD Module connected to Arduino UNO 
// This UNO is running IDE version 1.03

// Arduino SPI library and SD library are imported using the
// Sketch->Import Library pop-down menu. Both are standard.
// SD Lib reference: http://arduino.cc/en/Reference/SD

// This sketch demonstrates writing a struct to file as a block and
// reading it back to a struct.

// Connections:
// SD Module pin --- UNO pin
// GND --- GND 
// 3.3V --- 3.3V
// 5V --- 5V
// CS --- 10
// MOSI --- 11
// SCK --- 13
// MISO --- 12
// GND --- GND 

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <SPI.h>
#include <SD.h>

#define CARD_CS 10

struct myVars
{
  byte  b0;
  byte  b1;
  int   i2;
  int   i3;
  char  s4[ 20 ];
};

// ====================================================================

void setup(void)
{
  pinMode( CARD_CS, OUTPUT );
  digitalWrite( CARD_CS, LOW );

  Serial.begin( 9600 );

  byte  count; 

  struct  myVars  example; // creates space for struct example in SRAM
  struct  myVars  copyto; // creates space for struct example in SRAM
  byte    *buff = (byte *) &example; // to access example as bytes

  example.b0 = 1;           // fill example with test data, copyto stays empty
  example.b1 = 20;
  example.i2 = 3000;
  example.i3 = 4000;
  strcpy( example.s4, "file is test.txt" );

  File  sdfile;      // File object
  char  fname[ 14 ]; // even # of bytes big enough to hold 8.3 filename and terminator
  strcpy( fname, "test.txt" );


  // ========== SD begin ====================================
  byte flag = SD.begin( ); // test if card is recognized

  Serial.print( F("SD card status " ));
  Serial.print( flag, DEC ); // show success as 1, fail as 0
  if ( !flag )
  {
    Serial.println( F( " Failed to recognize SD card. Goodbye." )); 
    while( 1 );    // if fail then sketch ends
  }
  else
  {
    Serial.println( F( " is good to go." )); 
  }

  // ========== SD open/create file to write ============================
  strcpy( fname, "test.txt" );
  sdfile = SD.open( fname, FILE_WRITE );
  if ( !sdfile )
  {
    Serial.print( F( "Unable to open for write: " ));
    Serial.println( fname );
    while( 1 ); // sketch stops if reach here
  }

  // ========== SD write struct to file =================================
  // this file will only contain 1 struct although it could hold many
  // since opening a file that already exists for write will start at the end
  // we will set the read/write pointer at the start so as not to grow the file

  if ( !sdfile.seek( 0 ))
  {
    Serial.print( F( "Unable to set seek: " ));
    Serial.println( fname );
    while( 1 ); // sketch stops if reach here
  } 

  count = sdfile.write( buff, sizeof( myVars ));
  if ( count != sizeof( myVars ))
  {
    Serial.print( F( "Unable to write the example block to " ));
    Serial.println( fname );
    Serial.print( count, DEC );
    Serial.println( F( " bytes written." ));
    while( 1 ); // sketch stops if reach here
  }

  // ========== SD close file ===========================================

  sdfile.close(); // I could error check this. If it fails the next open will too.

  // ========== SD open file to read ====================================

  sdfile = SD.open( fname, FILE_READ ); // try to open text.txt
  if ( !sdfile ) // if test.txt not found, notify and change state
  {
    Serial.print( "Unable to open for read: " );
    Serial.println( fname );
    while( 1 );
  }

  // ========== SD read file to struct ==================================

  buff = (byte *) &copyto; // to access copyto as bytes


  for ( count = 0; count < sizeof( myVars ); count++ )
  {

    if ( sdfile.available()) 
    {
      *( buff + count ) = sdfile.read();
    }
    else
    {
      Serial.print( F( "Unable to read the example block in " ));
      Serial.println( fname );
      Serial.print( count, DEC );
      Serial.println( F( " bytes read." ));
      while( 1 ); // sketch stops if reach here
    }
  }
  
  // ========== SD close file ===========================================

  sdfile.close(); // I could error check this too.

  // ========== print retrieved data ====================================
  
      Serial.print( F( "\n Data in copyto block from " ));
      Serial.println( fname );
      Serial.print( F( "\ncopyto byte b0 " ));
      Serial.println( copyto.b0, DEC );
      Serial.print( F( "copyto byte b1 " ));
      Serial.println( copyto.b1, DEC );
      Serial.print( F( "copyto byte i2 " ));
      Serial.println( copyto.i2, DEC );
      Serial.print( F( "copyto byte i3 " ));
      Serial.println( copyto.i3, DEC );
      Serial.print( F( "copyto string s4 " ));
      Serial.println( copyto.s4 );

  // ========== finished ================================================
  
}

void loop(void)
{
}

Basically, a struct is stored in memory in sequential bytes. The code says, treat your struct as if it is an array of bytes by typecasting pointers:

struct someStruct;
byte* pointer = (byte*)&someStruct;
byte length = sizeof(someStruct);

The first line creates an instance of your struct. The second line says "get the address in memory of the struct [the &someStruct bit], and then pretent it is the address of an array of bytes [the (byte*) bit]" The third line says "get the size in bytes of the structure at compile time"

Nice example. I prefer the union way of doing it though, both result in the same thing, e.g.

typedef struct{
  byte  b0;
  byte  b1;
  int   i2;
  int   i3;
  char  s4[ 20 ];
} myVarsStruct;
typedef union {
   myVarsStruct vars;
   byte bytes [sizeof(myVarsStruct)];
} myVars;

and then you can do:

myVars example;
example.vars.b0 = 1;
... whatever...
sdfile.write(example.bytes, sizeof(myVars));

It's all bytes. The compiler is the one that needs convincing.

It's all bytes. The compiler is the one that needs convincing.

So, why not do it here:

  count = sdfile.write( buff, sizeof( myVars ));

rather than here:

  byte    *buff = (byte *) &example; // to access example as bytes

In my mind:

  count = sdfile.write(  (byte *) &example, sizeof( example));

is much more obvious. It makes it clear that it is the struct instance called example that is being written, and that it is the size of the instance that matters.

I guess I could.

This is for someone I'm trying to ease into Classes. He's not a programmer but he can learn. I figured to keep it 1 new idea per line as much as possible but hey he's pretty new to pointers.

That and there's much I am re-learning from before, at times I feel like I'd lost more than I know. It took me over 2 hours just to crank that little bit out. Ever get stuck on one word for 10 minutes?

@GoForSmoke

Anyway … THANK YOU VERY MUCH for sharing this code, it was very useful for me 8)

GreetZzz

I just spent a lot of time debugging issues reading and writing to an SD card..

My problem ended up being that I had Strings in my struct and it was initializing the struct size to something smaller than it was later in the program after I had added data and began reading/writing to the SD card.

It worked fine adding and removing at first while it was on, but after a reboot and trying to repopulate my data from the SD card, it all fell apart.

I had to change to char arrays.

Just thought I'd point it out for future readers.

Thanks @GoForSmoke for the code! I also found it extremely helpful for what I'm doing! :)