Saving and loading images

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

Update

4th April 2023: This description has been updated to reflect uLisp 4.4.

Platform differences

Depending of the capabilities of each platform, save-image and load-image are implemented using the following mechanisms:

uLisp Version Platform Implementation
AVR-Nano All EEPROM
AVR Arduino Mega 2560 EEPROM
  ATmega1284P Flash memory
  AVR128DA48 and AVR128DB48 Flash memory
ARM ATSAMD21 Flash memory
  ATSAMD21 with DataFlash * DataFlash
  ATSAMD51 with DataFlash DataFlash
  nRF51822 n/a †
  nRF52840 with DataFlash DataFlash
  RP2040 Flash memory using LittleFS
  MAX32620 n/a †
  Teensy 4.0 and 4.1 Flash memory using LittleFS
ESP ESP8266 EEPROM emulation
  ESP32, ESP32-S2, ESP32-S3 Flash memory using LittleFS
  ESP32-C3 Flash memory using LittleFS
RISC-V Sipeed MAiX boards n/a †

* Adafruit's Express boards include a DataFlash chip, in which case save-image uses this.
† All platforms support an SD-card interface, in which case images can be saved to an SD card.

Introduction

For simplicity in the following descriptions I've omitted the additional code needed to save the symbol table and SymbolTop pointer.

The image is defined by the struct_image typedef:

typedef struct {
  unsigned int eval;
  unsigned int datasize;
  unsigned int globalenv;
  unsigned int gcstack;
  object data[IMAGEDATASIZE/4];
} struct_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.gcstack, the address of the gcstack list 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++;
  object *obj = &Workspace[WORKSPACESIZE-1];
  while (firstfree < obj) {
    if (marked(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 (*arg == obj) *arg = firstfree;
      while (marked(firstfree)) firstfree++;
    }
    obj--;
  }
  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 movepointer() function scans through the workspace for address cells matching the moved cell, and if found, updates them to point to the new position. Because the characters in a string could be confused for an address, it then traces down each of the strings restoring any that have been changed in the previous step:

void movepointer (object *from, object *to) {
  for (int i=0; i<WORKSPACESIZE; i++) {
    object *obj = &Workspace[i];
    unsigned int type = (obj->type) & MARKMASK;
    if (marked(obj) && (type >= STRING || type==ZERO)) {
      if (car(obj) == (object *)((unsigned int)from | MARKBIT)) 
        car(obj) = (object *)((unsigned int)to | MARKBIT);
      if (cdr(obj) == from) cdr(obj) = to;
    }
  }
  // Fix strings
  for (int i=0; i<WORKSPACESIZE; i++) {
    object *obj = &Workspace[i];
    if (marked(obj) && ((obj->type) & MARKMASK) == STRING) {
      obj = cdr(obj);
      while (obj != NULL) {
        if (cdr(obj) == to) cdr(obj) = from;
        obj = (object *)((unsigned int)(car(obj)) & MARKMASK);
      }
    }
  }
}

Saving and loading an image

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 > IMAGEDATASIZE) {
    pfstring(F("Error: Image size too large: "));
    pint(imagesize); pln();
    GCStack = NULL;
    longjmp(exception, 1);
  }
  eeprom_update_word(&image.datasize, imagesize);
  eeprom_update_word(&image.eval, (unsigned int)arg);
  eeprom_update_word(&image.globalenv, (unsigned int)GlobalEnv);
  eeprom_update_word(&image.gcstack, (unsigned int)GCStack);
  eeprom_update_block(Workspace, image.data, imagesize*4);
  return imagesize;
}

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);
  GCStack = (object *)eeprom_read_word(&image.gcstack);
  eeprom_read_block(Workspace, image.data, imagesize*4);
  gc(NULL, NULL);
  return imagesize;
}

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 and upload it.