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.