Making a LilyGO T-Deck uLisp Machine


#1

This post describes firmware to convert the LilyGO T-Deck into a self-contained handheld Lisp computer:

It has an 320x240 colour TFT display that gives a scrolling display of 24 lines of 53 characters, and an integrated 35-key Blackberry-type keyboard. It’s available from AliExpress, or direct from LilyGO, for around $50/£50.

Update

19th October 2023: This description has been updated to reflect changes to Release 4 of the uLisp T-Deck firmware.

Introduction

The LilyGO T-Deck is based on an ESP32-S3 dual-core LX7 microprocessor, with 2.4 GHz Wi-Fi & Bluetooth 5 (LE). It has 16M bytes of flash and 8M bytes of PSRAM.

The display is a 2.8 inch ST7789 SPI Interface IPS LCD with a resolution of 320 x 240. When used in conjunction with the Arduino GFX library it gives impressive graphics performance; for example, clearing the screen takes 35 ms. The display doesn’t support hardware scrolling in the vertical direction, but the display is plenty fast enough to support software scrolling.

The T-Deck includes a socket for a 3.7V lithium cell, and this can be charged from the USB port.

Here’s the full specification:

uLisp T-Deck – Specification

Size: 68mm x 100mm (2.7" x 3.9").

Display: 53 characters x 24 lines.

Keyboard: Integrated 35-key I2C keyboard providing upper and lower-case characters, digits, and most symbols required by uLisp. The ‘@’ and ‘_’ keys are translated into the characters ‘~’ and ‘\’ required by uLisp.

Memory available: 22000 Lisp cells (176000 bytes).

Flash: 1.5M bytes of flash are reserved for use to save the Lisp workspace using save-image.

Processor: ESP32-S3

Clock speed: 240 MHz.

Trackball: The trackball is used as an Escape key - press it in to escape from a running program.

Language

uLisp, a subset of Common Lisp, with approximately 200 Lisp functions and special forms, including graphics extensions and Wi-Fi extensions.

It also provides keywords, such as :input and :output, as a convenient way of entering Arduino constants.

For a full definition see uLisp Language Reference, Graphics extensions, and Wi-Fi extensions.

Types supported: list, symbol, integer, floating-point, character, string, stream, and array.

An integer is a sequence of digits, optionally prefixed with “+” or “-”. Integers can be between -32768 and 32767. You can enter numbers in hexadecimal, octal, or binary with the notations #x2A, #o52, or #b101010, all of which represent 42.

The floating-point numbers have full 32-bit precision.

User-defined symbol names can have arbitrary names. Any sequence that isn’t an integer can be used as a symbol; so, for example, 12a is a valid symbol.

There is one namespace for functions and variables; in other words, you cannot use the same name for a function and a variable.

uLisp includes a mark and sweep garbage collector. Garbage collection takes about 0.9 msec.

Entering programs

You can enter commands and programs by typing them at the keyboard, and pressing ENTER. A keyboard buffer is provided that buffers a full screen of text, and you can use the DEL key to delete characters and correct typing mistakes. The line editor includes parenthesis matching which automatically highlights matching brackets in green as you type in a program. This makes it much easier to enter a program correctly, and is especially helpful in telling you how many closing brackets to type at the end of defining a function.

Connecting to a computer

You can also connect the T-Deck to a computer via a USB cable. You can then use the Serial Monitor in the Arduino IDE to enter and edit programs as described in Using uLisp.

Installing the T-Deck uLisp firmware

The following instructions describe how to upload the uLisp firmware using the Arduino IDE. I used Arduino 1.8.19 on a Mac, but it should be possible to use other versions of the IDE, and other platforms.

Requirements

In addition to the Arduino IDE you also need to install the following cores and libraries:

  • Use the Boards Manager to install the ESP32 core. I used 2.0.14.

Note that version 3.0.0-alpha2 of the ESP32 core will not work.

Ensure that you are installing the version by Espressif Systems, not the Arduino version that only supports the Arduino Nano ESP32.

Note that this is a modified version of the Bodmer TFT_eSPI library, and it won’t work if you install the one from Bodmer’s GitHub repository, or from the Library Manager.

Installation

  • Download the T-deck uLisp firmware from https://github.com/technoblogy/ulisp-tdeck.
  • Choose the ESP32S3 Dev Module option under the ESP32 Arduino heading on the Board option on the Tools menu.
  • Check that the subsequent options are set as follows (leave the other options at their defaults):

USB CDC On Boot: “Enabled”
Flash Size: “16MB (128Mb)”
Partition Scheme: "Default 4MB with spiffs (1.2MB APP/1.5MB SPIFFS)"

  • Connect the computer’s USB port to the T-Deck and select the port from the Tools menu Port submenu.
  • Check that Compiler warnings is set to Default in the Arduino IDE Preferences panel, otherwise the compilation may fail on warnings.
  • Choose Upload from the Sketch menu to upload uLisp.

You should then see the uLisp prompt on the T-Deck display, and be able to enter Lisp programs at the keyboard. You should also be able to enter Lisp programs from the Arduino IDE Serial Monitor; you may have to select the USB port from the Port submenu again.

Resources

The uLisp T-deck firmware on GitHub: https://github.com/technoblogy/ulisp-tdeck

LilyGO’s T-Deck repository on GitHub: T-Deck.

LilyGO’s T-Deck information page: T-Deck.

Acknowledgements

This firmware builds on the work posted on the uLisp Forum by @hasn0life in porting uLisp to the T-Deck, and @nanomonkey in adding SD card support.


T-Deck, esp32-s3 lisp machine
#2

A couple of other tips:

I prefer black text on a white background.

Change the line:

Arduino_ST7789 tft = Arduino_ST7789(bus, GFX_NOT_DEFINED, 1, true);

to:

Arduino_ST7789 tft = Arduino_ST7789(bus, GFX_NOT_DEFINED, 1, false);

I’d like to fit 30 lines of text on the display.

Change the line:

const int Leading = 10;

to

const int Leading = 8;

What’s the demo program in the photograph?

(defun demo (a b)
  (dotimes (y 160)
    (let ((c (+ (ash y a) (ash y b)))
          (j (- 319 (* y 2))))
    (fill-triangle 0 y (* y 2) 159 0 159 c)
    (fill-triangle 319 (- 159 y) j 0 319 0 c))))

#3

This looks like a great little device and nice project.
You’re giving more and more people pocket sized Lisp device options!


#4

A couple more tips:

How do I turn on/off the keyboard backlight?

Press Alt-B.

How do I clear the display?

Press Alt-C.


#5

Until I work out how to change the keyboard processing code, here’s a fix to translate ‘@’ and ‘_’, which aren’t used by Lisp, into ‘~’ and ‘\’ respectively:

In the routine gserial () change the block after:

if ((temp != 0) && (temp !=255)) {

so it reads:

if ((temp != 0) && (temp !=255)) {
  if (temp == '@') temp = '~';
  if (temp == '_') temp = '\\';
  ProcessKey(temp);
}

This is incorporated into Release 2 on GitHub.


#6

This documentation may be of help with getting the graphics to work with the sdcard: Sharing SD card SPI documentation for ESP.

On another note, I’ve noticed that my first few commands at the TDeck repl respond immediately, while everything after that come one character at a time. I’m assuming this isn’t intentional, and is instead some problem with the buffer or garbage collection that is causing the output stream to slow down. Can we get back to the immediate response time with a flush?

Thanks again.


#8

I found the same problem, such as with the command:

(apropos "")

However it seems to be fixed as a result of some other improvements I’ve made. Please see if it still happens with the Release 2 I’ve just uplaoded to GitHub.


#9

The problem only happens when using the keyboard, and not when connected to the device with a serial terminal. In fact I cannot reproduce the problem when a serial terminal is connected.

I’ll try out the newest code when I get a chance.


#10

The problem only happens when using the keyboard, and not when connected to the device with a serial terminal.

That’s what I found too, with Release 1, but I think it’s fixed in Release 2.


#11

The problem still persists after pulling the newest version (4.4d).

Connecting a serial terminal to the device seems to clear out the problem, even while it is occurring. So if you start up the device without a serial connection and run (apropos "") you’ll see the behavior. The first few lines show up immediately, then the rest come one character at a time. While this is happening if you connect up a serial terminal connection the results will resume their normal response speed, filling the screen and then returning a line at a time as it scrolls the screen. If you start the device with a serial connection the problem doesn’t appear to manifest.


#13

OK, I can get it to happen if I do:

  • Open the Serial Monitor and give the command (apropos ""). It works fine.
  • Now close the Serial Monitor and give the command again. Now it scrolls slowly.

#14

OK please try this:

  • Comment out the following line at the start of the source file like this:

    // #define serialmonitor
    
  • Replace the first line of ulispreset() with:

    #if defined (serialmonitor)
    delay(100); while (Serial.available()) Serial.read();
    #endif
    

This will then disable the serial monitor.


#15

This fixes the problem, as far as I can see. It’s too bad that it also disables the ability to send commands via the serial terminal connection.


#16

As I described in the section Limitations in my original post above, there are problems with the serial support in the ESP32 Arduino core, and I think this is probably a symptom of that. At least this provides a workaround.


#17

I was able to get the SD card reader to work with the graphics library and tft display.

You can view the changes I made at:

Hope this helps, now that we can store files it’s feeling closer to a full fledge lisp machine.


#18

Excellent - I’ll try it out tomorrow.


#19

There is now a third release of the T-Deck firmware, with the following improvements:

SD Card

The T-Deck SD card socket can now be used to read from and write to SD cards from uLisp, and to save an image of the Lisp workspace. To return a list of the files on an SD card give the command directory:

21827> (directory)
("T1.bmp" "A1" "F2" "T2" "PIC.BMP" "ULISP.IMG" "Greeting.txt" "PIC2.BMP" "X1")

For details of using SD cards see SD card interface.

If you don’t want to use the SD card interface, upload the firmware with the following line commented out with:

// #define sdcardsupport

The (save-image) command will then use the flash memory to save the workspace.

Sound

You can now play notes through the internal speaker using the uLisp note function. On the T-Deck you must supply the duration, in milliseconds, as a fourth argument. For example, the following function plays a scale of C:

(defun scale () 
  (mapc 
   (lambda (n) (note 3 n 4 500))
   '(0 2 4 5 7 9 11 12)))

The beep character now beeps the speaker:

(write-byte 7)

Graphics performance

The graphics performance is now improved. With a default 240MHz clock the uLisp T-deck firmware clears the screen in about 33ms:

21827>
(time (write-byte 12))
nil
Time: 33 ms

Scrolling the screen is now optimised, depending on the content, but takes about 30ms:

21827> (time (terpri))
nil
Time: 30 ms

Keyboard

The ‘@’ and ‘_’ keys are now translated into the characters ‘~’ and ‘\’ required by uLisp.

Resources

Download it here: https://github.com/technoblogy/ulisp-tdeck

Full details of the firmware: LilyGO T-Deck uLisp Machine

Acknowledgements

The firmware builds on the work done by @hasn0life in porting uLisp to the T-Deck, and @nanomonkey in adding SD card support.


TTGO T-Display setup and speed problem
#20

I’ve just noticed that in Release 3 the tak benchmark runs much slower than expected. I traced this to the current definition of testescape() which is checking for a keypress, and this takes a significant time on the I2C interface.

I suggest replacing the definition of testescape() with:

void testescape () {
#if defined serialmonitor
  if (Serial.available() && Serial.read() == '~') error2(PSTR("escape!"));
#endif
  if (digitalRead(0) == LOW) {
    pinMode(0, INPUT_PULLUP); 
    if (digitalRead(0) == LOW) error2(PSTR("escape!")); // Trackball pushed
  }
}

You can now escape from a running program by pressing in the trackball.

With this change (tak 18 12 6) runs in 2.4 s.

This is incorporated into Release 4 if the uLisp T-Deck firmware at:

https://github.com/technoblogy/ulisp-tdeck.