Running uLisp on an ESP32


Introduction

Recently I wanted to try out uLisp on my ESP32 board. Programming in Scheme and Clojure is a lot of fun so it's great to be able to use a Lisp when tinkering with hardware. The conventional method for running uLisp on an ESP32 (or similar) is to use the Arduino IDE. I wanted to be able to use my familiar environment of Vim and the command line and found a way to do so with PlatformIO.

This blog post details how to get uLisp on an ESP32 board and run some small programs from the REPL. Please note, the steps listed in this post are for Linux. PlatformIO is also available for Windows and Mac so it shouldn't be too difficult to follow along if you use either of those platforms.

Pre-requisites

Ensure your user is a member of the dialout group. This is required because your Linux user won't have permission to write to /dev/ttyUSB0 (the device for your board may be different). As you can see below, the dialout group does have permission to write to the device.

$ ls -l /dev/ttyUSB0
crw-rw----. 1 root dialout 188, 0 Sep  6 16:36 /dev/ttyUSB0

Add yourself to the dialout group by running the following command:

$ sudo usermod -a -G dialout $USER

After running this command you'll need to log out and back in again. I actually needed to reboot the computer for these changes to take effect so keep that in mind. If your user is not in the dialout group you will get a permissions error when uploading the uLisp binary to the board.

Installing and setting up PlatformIO

There are a few different ways to install PlatformIO. One popular way is to download the PlatformIO IDE which is based on Microsoft's VSCode. I wanted to use PlatformIO via the command line so I installed PlatformIO Core, which provides the cli tools.

  1. Install the PlatformIO cli tools via the installer script.

  2. Include ~/.platformio/penv/bin in your PATH environment variable.

  3. Determine the ID of your ESP32 board.

    $ pio boards esp32
    
    Platform: espressif32
    =======================================================================
    ID            MCU    Frequency    Flash  RAM     Name
    ------------- -----  -----------  ------ ------  ----------------------
    esp32cam      ESP32  240MHz       4MB    320KB   AI Thinker
    alksesp32     ESP32  240MHz       4MB    320KB   ALKS ESP32
    featheresp32  ESP32  240MHz       4MB    320KB   Adafruit ESP32 Feather
    espea32       ESP32  240MHz       4MB    320KB   April Brother
    bpi-bit       ESP32  160MHz       4MB    320KB   BPI-Bit
    
  4. Create a new PlatformIO project.

    $ mkdir ulisp
    $ cd ulisp
    $ pio project init --board featheresp32
    

    The initial project will have the following structure:

    .
    ├── get-platformio.py
    ├── include
    │   └── README
    ├── lib
    │   └── README
    ├── platformio.ini
    ├── src
    ├── test
    │   └── README
    └── ulisp-esp.ino
    

Downloading uLisp

  1. Clone the ulisp-esp32 git repository or just download the ulisp-esp32.ino file.

  2. Then copy ulisp-esp32.ino to the src directory of your PlatformIO project and change the file extension to cpp.

     $ cp ~/Downloads/uslip-esp32.ino ulisp/src/ulisp-esp32.cpp
    

Configuring uLisp

There's a couple of nice features in uLisp which are disabled by default. Both of these improve the editing experience in the REPL and are documented on the uLisp website under the Using uLisp from a terminal section.

  1. Line editor

    Without the line editor enabled it's not possible to use the backspace key and therefore mistakes cannot be corrected. Input is also executed automatically when the final closing parenthesis is entered. By enabling the line editor you can correct mistakes and execute the input when you are ready by pressing the enter key. The line editor can be enabled by uncommenting line 18 in ulisp/src/uslip-esp32.cpp.

    #define lineeditor
    
  2. Parenthesis matching

    This feature will highlight the opening parenthesis when you type its corresponding closing parenthesis. It can be enabled by uncommenting line 19 in ulisp/src/uslip-esp32.cpp.

    #define vt100
    

Building uLisp

  1. If you try and run the project you'll see a number of errors printed to the console.

    $ pio run
    ...
    Building in release mode
    Compiling .pio/build/featheresp32/src/ulisp-esp.cpp.o
    src/ulisp-esp.cpp:9:26: error: expected initializer before 'PROGMEM'
    const char LispLibrary[] PROGMEM = "";
                          ^
    src/ulisp-esp.cpp: In function 'void errorsub(symbol_t, const char*)':
    src/ulisp-esp.cpp:245:7: error: 'pserial' was not declared in this scope
       pfl(pserial); pfstring(PSTR("Error: "), pserial);
           ^
    ...
    
  2. The PlatformIO FAQ explains how to convert an Arduino file to C++. This involes two steps:

    i. Adding #include <Arduino.h> to the top of the source file.

    ii. Adding a forward declaration for each custom function.

  3. Following these instructions, at the very top of the ulisp-esp32.cpp file, after the first block comment, add the following line:

    #include <Arduino.h>
    
  4. Then we just need to add a forward declaration for the custom functions in this source file. You can gather a list of these functions from reading through the errors in the output of pio run. The errors are all quite similar, stating that a particular function "was not declared in this scope." The following snippet taken from the output shows the pserial and pfl functions need forward declarations.

    src/ulisp-esp.cpp: In function 'void errorsub(symbol_t, const char*)':
    src/ulisp-esp.cpp:245:7: error: 'pserial' was not declared in this scope
       pfl(pserial); pfstring(PSTR("Error: "), pserial);
           ^
    src/ulisp-esp.cpp:245:14: error: 'pfl' was not declared in this scope
       pfl(pserial); pfstring(PSTR("Error: "), pserial);
                  ^
    

    I added the declarations for these functions beneath the other forward declarations in ulisp-esp32.cpp, located on line 204. The following is a list of the functions I needed to declare:

    void checkminmax (symbol_t name, int nargs);
    int glibrary ();
    int gserial ();
    int listlength (symbol_t name, object *list);
    bool listp (object *x);
    inline int maxbuffer (char *buffer);
    char nthchar (object *string, int n);
    void pfstring (PGM_P s, pfun_t pfun);
    void pint (int i, pfun_t pfun);
    void pinthex (uint32_t i, pfun_t pfun);
    void pfl (pfun_t pfun);
    inline void pln (pfun_t pfun);
    void pserial (char c);
    void prin1object (object *form, pfun_t pfun);
    void printstring (object *form, pfun_t pfun);
    void pstring (char *s, pfun_t pfun);
    int subwidthlist (object *form, int w);
    void supersub (object *form, int lm, int super, pfun_t pfun);
    void testescape ();
    
  5. Now the project can be run.

    $ pio run
    ...
    Building in release mode
    Retrieving maximum program size .pio/build/featheresp32/firmware.elf
    Checking size .pio/build/featheresp32/firmware.elf
    Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
    RAM:   [===       ]  32.0% (used 104792 bytes from 327680 bytes)
    Flash: [======    ]  55.3% (used 724534 bytes from 1310720 bytes)
    =================== [SUCCESS] Took 1.43 second ===================
    

Uploading uLisp

  1. With the project compiling successfully, we can upload uLisp to the ESP32 by running the following command.

    $ pio run --target upload
    Processing featheresp32 (platform: espressif32; board: featheresp32; framework: arduino)
    ---------------------------------------------------------------------
    Verbose mode can be enabled via `-v, --verbose` option
    CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/featheresp32.html
    PLATFORM: Espressif 32 (1.12.4) > Adafruit ESP32 Feather
    ...
    Writing at 0x00074000... (96 %)
    Writing at 0x00078000... (100 %)
    Wrote 724656 bytes (433944 compressed) at 0x00010000 in 10.5 seconds (effective 552.8 kbit/s)...
    Hash of data verified.
    
    Leaving...
    Hard resetting via RTS pin...
    ================== [SUCCESS] Took 14.71 seconds ==================
    

    If you get a permissions error when trying to upload the uLisp binary you may need to add your user to the dialout group. See the Pre-requisites section for details.

  2. We can test uLisp by connecting to the device with screen. We need to tell screen which device to connect to, in this case /dev/ttyUSB0, along with the baud rate.

    $ screen /dev/ttyUSB0 9600
    (print "Hello, world!")
    
    "Hello, world!" 
    "Hello, world!"
    
    7999> (+ 1 1)
    2
    
    7999> (defun sq (x) (* x x))
    sq
    
    7999> (sq 47)
    2209
    

    Great! We're able to execute lisp code on our ESP32 :]

Blinking an LED

Let's try interacting with the hardware from the REPL. The uLisp website has a nice example of how to blink the red LED that is connected to digital pin 13 on ESP32 boards.

With a screen session running, enter the following code. A function is defined that enables pin 13, writes either true or false to the pin, and then sleeps for 1 second, after which it calls itself again.

(defun blink (x)
  (pinmode 13 t)
  (digitalwrite 13 x)
  (delay 1000)
  (blink (not x)))

Once defined, the function can be run by typing:

(blink t)

You should see the red LED blinking on and off.

Conclusion

Thanks to PlatformIO it's quite simple to compile uLisp and upload it to the ESP32 board. With just a couple of small experiments in the REPL you can really get a feel for how nice the development experience is with lisp. I have a temperature and humidity sensor lying around so I'm very keen to take some readings using lisp. Sounds like a good topic for another blog post!