Here is the general idea of my saving/loading process (with functions/variables renamed for clarity). It’s probably best not to follow some of my style preferences, but hopefully this can help. This isn’t enough to compile as-is, but it should have all the important pieces:
/* Required include files */
#include "SDFileSystem.h"
#include "FATFileHandle.h"
/* My custom save error codes, for debugging */
#define SAVE_ERR_NONE 0
#define SAVE_ERR_CDIR 1
#define SAVE_ERR_OPEN 2
uint8_t save_errno;
SDFileSystem *sd;
FATFileHandle *fp;
DirHandle *dh;
/* Where you actually save your save files */
const char save_dir[] = "/save_directory_name_goes_here";
void initialization_code(){
...
sd = new SDFileSystem(P0_9, P0_8, P0_6, P0_7, "sd");
sd->mount();
sd->mkdir(save_dir, 0777);
...
}
/* Try to open file for saving. If attempt fails, retry one more time and if
it still fails give up. This retry allows for a more elegant recovery from an
accidentally removed SD card, as removing the SD card will result in an error
even if it has been re-inserted before attempting to save. */
bool openSave(const char* name){
fp = (FATFileHandle*) sd->open(name, O_WRONLY | O_CREAT);
if( fp == NULL ){
/* Try to re-initialize SD card connection */
sd->unmount();
sd->mount();
/* Try once more */
if( (fp = (FATFileHandle*) sd->open(name, O_WRONLY | O_CREAT)) != NULL ){
save_errno = SAVE_ERR_NONE;
return true;
}
save_errno = SAVE_ERR_OPEN;
return false;
}
save_errno = SAVE_ERR_NONE;
return true;
}
void writeSave(uint8_t* start, uint16_t len){
fp->write(start, len);
}
void closeSave(){
fp->close();
}
/* I have less error handling for loading, but you could probably use almost the
same code as for saving to have better error handling. I just wasn't as worried
about a failed load and I haven't bothered to update and test it. */
bool openLoad(const char* name){
fp = (FATFileHandle*) sd->open(name, O_RDONLY);
if( fp == NULL ){
return false;
}
return true;
}
void readLoad(uint8_t* start, uint16_t len){
fp->read(start, len);
}
void closeLoad(){
fp->close();
}
And here’s an example snippet using the save/load code, with some of my code that I use to create versions of my save data in case I make incompatible changes:
uint8_t save_version[] = "HEADER BYTES INDICATING VERSION GO HERE";
bool save_game(){
if( !openSave("SAVE FILE FILENAME") ){
return false;
}
writeSave(save_version,sizeof(save_version));
writeSave(&GAME_DATA_TO_SAVE,sizeof(GAME_DATA_TO_SAVE));
closeSave();
return true;
}
bool load_game(){
uint8_t load_version[] = {0,0,0,0,...};
/* Check if file can be opened */
if( !openLoad("SAVE FILE FILENAME") ){
return false;
}
readLoad(load_version, sizeof(save_version));
/* Verify version matches. If not, do not load. */
if( strncmp((char*)save_version,(char*)load_version,sizeof(save_version)) != 0 ){
closeLoad();
return false;
}
readLoad(&GAME_DATA_TO_LOAD,sizeof(GAME_DATA_TO_LOAD));
closeLoad();
return true;
}
Some of the wrapper functions (like writeSave and closeSave) exist because they are actually part of an abstraction layer I wrote to keep all platform-specific code in one file per platform, so you don’t necessarily need to have those wrappers in your version. I think this example would require more changes and polish to be a proper tutorial, and I don’t feel like I should recommend the way I personally prefer to code, which is why I hadn’t considered making a tutorial about this before. This should hopefully be enough to get things working, though.