Loading and saving images

The uLisp functions (save-image) and (load-image) allow you to save the Lisp workspace to EEPROM, and then load it at a later time, optionally automatically executing a specified function when the image has been loaded.

The image is defined by the struct_image typedef:

typedef struct {
  unsigned int eval;
  unsigned int datasize;
  unsigned int globalenv;
  unsigned int tee;
  char data[];
} struct_image;

struct_image EEMEM image;

This consists of the workspace data, plus the following additional image parameters:

  • image.eval, the address of a function to be run at startup.
  • image.datasize, the size of workspace to be saved, in 4-byte objects.
  • image.globalenv, the address of the global environment in the image.
  • image.tee, the address of the t symbol in the image.

Compacting the image

In general the AVR processors have less EEPROM space than RAM space, so we need to compact the used objects in the workspace to ensure that they will fit in the EEPROM. This is achieved by calling compactimage():

int compactimage (object **arg) {
  markobject(tee);
  markobject(GlobalEnv);
  markobject(GCStack);
  object *firstfree = workspace;
  while (marked(firstfree)) firstfree++;

  for (int i=0; i<workspacesize; i++) {
    object *obj = &workspace[i];
    if (marked(obj) && firstfree < obj) {
      car(firstfree) = car(obj);
      cdr(firstfree) = cdr(obj);
      unmark(obj);
      movepointer(obj, firstfree);
      if (GlobalEnv == obj) GlobalEnv = firstfree;
      if (GCStack == obj) GCStack = firstfree;
      if (tee == obj) tee = firstfree;
      if (*arg == obj) *arg = firstfree;
      while (marked(firstfree)) firstfree++;
    }
  }
  sweep();
  return firstfree - workspace;
}

This first calls markobject() to mark all the used objects, and then moves each used object to the first free slot in the workspace. After each move it calls movepointer() to change any pointers affected by the move. Then finally it calls sweep() to collect up all the unused cells into free space.

The saveimage() function then simply saves the image and image parameters to EEPROM:

int saveimage (object *arg) {
  unsigned int imagesize = compactimage(&arg);
  // Save to EEPROM
  if ((imagesize*4+8) > EEPROMsize) {
    Serial.print(F("Error: Image size too large: "));
    Serial.println(imagesize+2);
    GCStack = NULL;
    longjmp(exception, 1);
  }
  eeprom_write_word(&image.datasize, imagesize);
  eeprom_write_word(&image.eval, (unsigned int)arg);
  eeprom_write_word(&image.globalenv, (unsigned int)GlobalEnv);
  eeprom_write_word(&image.tee, (unsigned int)tee);
  eeprom_write_block(workspace, image.data, imagesize*4);
  return imagesize+2;
}

The loadimage() function loads the image and image parameters from EEPROM and performs a garbage-collect to reset the free space after the load:

int loadimage () {
  unsigned int imagesize = eeprom_read_word(&image.datasize);
  if (imagesize == 0 || imagesize == 0xFFFF) error(F("No saved image"));
  GlobalEnv = (object *)eeprom_read_word(&image.globalenv);
  tee = (object *)eeprom_read_word(&image.tee) ;
  eeprom_read_block(workspace, image.data, imagesize*4);
  gc(NULL, NULL);
  return imagesize+2;
}

Note that a saved image will no longer work if uLisp has been edited and recompiled, and the position of the workspace or function lookup table has changed. If you need to disable autorun because an invalid image gets loaded on reset, comment out the line:

#define resetautorun

at the start of the uLisp source and recompile it.