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.
-
Install the PlatformIO cli tools via the installer script.
-
Include
~/.platformio/penv/bin
in yourPATH
environment variable. -
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
-
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¶
-
Clone the ulisp-esp32 git repository or just download the
ulisp-esp32.ino
file. -
Then copy
ulisp-esp32.ino
to thesrc
directory of your PlatformIO project and change the file extension tocpp
.$ 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.
-
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
-
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¶
-
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); ^ ...
-
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.
-
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>
-
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 thepserial
andpfl
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 ();
-
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¶
-
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. -
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!