8th December 2022
The Lisp Star is a star-shaped pendant that you can program in Lisp to make its six coloured LEDs twinkle in different patterns:
The Lisp Star is a pendant based on an ATtiny3227 that you can program in Lisp.
The pendant is based on the ATtiny3227, a microcontroller with 32Kbytes of flash memory and 3Kbytes of RAM, enough memory to run an integer version of my Lisp interpreter, uLisp. To program it you connect to it from a computer via the six pads on the front of the pendant, and you can then enter a program written in uLisp, or edit an existing program, via a serial terminal or the Serial Monitor in the Arduino IDE.
I must admit that my original motivation for designing this was the incongruity of an item of jewellery running a high-level programming language. But it has a more serious application: many non-technical people who would like to learn about programming are not particularly motivated by "Hello World" programs, or programs that perform mathematical calculations, but they might be inspired by the idea of designing their own pattern of flashing LEDs on a pendant.
A few months ago I designed the Twinkling Pendant, a star-shaped pendant you could program in C to control six coloured LEDs. When Microchip released their 2-series ATtiny chips I realised that they now provided enough flash memory and RAM to run my Lisp interpreter, uLisp, and I thought it would be amusing to make a pendant that ran a high-level language that allowed you to program the pattern of LEDs.
Another factor that makes the ATtiny3227 ideal for this application is that the 20 and 24-pin 2-series parts are the only new ATtiny parts that provide a separate reset pin. This makes it convenient to use the Optiboot bootloader with these parts, to upload programs via serial, because it uses a connection between DTR and RESET to initiate the upload.
Here's the specification of the Lisp running on the Lisp Star pendant:
Memory available: 513 Lisp objects (2052 bytes).
Clock speed: 5 MHz.
Current consumption: Approx. 5mA, or 0.1µA in sleep, giving negligible battery drain.
Permanent program storage: There's not enough EEPROM available on the ATtiny3227 to allow programs to be stored using the uLisp save-image and load-image functions as on other platforms, but the examples below describe how to use the LispLibrary facility to store Lisp programs as text in the flash memory, and have them load automatically on reset.
uLisp, a subset of Common Lisp, with approximately 170 Lisp functions and special forms. For a full definition see uLisp Language Reference.
Types supported: list, symbol, integer, character, string, and stream.
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.
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.
Includes a mark and sweep garbage collector. Garbage collection takes 2 msec.
Separate I/O pins are connected to 6 LEDs, accessed as Arduino pin numbers 0 to 5 using the Lisp function digitalwrite.
LEDs 0 and 1 are capable of analogue output, using the Lisp function analogwrite.
Here's the circuit of the Lisp Star:
Circuit of the Lisp Star, based on an ATtiny3227.
The ATtiny3227 comes in a 4mm square QFN24 package and the LEDs, resistors, and capacitors are all 0805 size. I recommend choosing red, yellow, or orange LEDs as they have a lower forward voltage of 2.2V or less and will give the longest battery life. I chose a mixture of all three colours.
The Schottky diode protects the button cell if you leave it in place when connecting the Lisp Star to the 5V on the FTDI connector. It's not critical; the one I chose has a voltage drop of about 200mV.
The pendant is designed to work with a 3V 12mm coin cell type CR1225, which has a capacity of about 50mAh. You could also use types CR1216 or CR1220. The coin cell fits in an SMD 12mm coin cell holder .
|2||LED||Orange||0805||Orange 0805 Surface Mount LED|
|2||LED||Red||0805||Red 0805 Surface Mount LED|
|2||LED||Yellow||0805||Yellow 0805 Surface Mount LED|
|1||Push button||SMD||6mm||Mini Push Button Switch SMD|
|6||Resistor||220Ω||0805||Yageo SMD resistor 220 ohm|
|2||Capacitor||100nF||0805||Yageo SMD capacitor 0.1µF 25 V|
|1||Schottky diode||BAT54HT||SOD-323||BAT54HT1G Schottky Diode 200mA|
|1||Coin cell Holder||Coin Cell||10mm||Coin Cell Battery Holder - 12mm (SMD)|
|1||PCB||Star-shaped||36.6 x 35.7mm||See end of article|
I designed a star-shaped board in Eagle and sent it to PCBWay for fabrication.
To solder the ATtiny3227's QFN package you'll need a hot air gun or reflow oven. I used a Youyue 858D+ hot air gun set to 275°C.
Once all the SMD components were soldered on the front of the board I finally soldered the coin cell holder onto the back of the board using a conventional soldering iron:
The coin cell holder on the back of the Lisp Star.
Note that if you are making the Lisp Star for a child, please bend the tabs on the coin cell holder, after inserting the cell, so there's no chance of the coin cell being removed and swallowed.
I've brought the Serial FTDI programming connections to a standard 6-pin pad so you can connect to it with a set of six pogo pins. The most convenient way to do this is with a 6-pin Pogo Pin Probe Clip, available from Adafruit  (or direct from AliExpress ):
Programming the Lisp Star using a Pogo Pin Probe Clip.
An FTDI board or FTDI cable will plug directly onto the pogo pins at the top of the Pogo Pin Probe Clip.
Installing a bootloader
The first step in installing uLisp on the Lisp Star board is to install a bootloader on the ATtiny3227 chip via the UPDI pin. Once you've done this you will be able to subsequently upload uLisp from the Arduino IDE via the FTDI port, which is more convenient than having to switch back to the UPDI programmer if you want to upload uLisp again.
First install Spence Konde's megaTinyCore from GitHub: see megaTinyCore.
Then, in the Arduino IDE:
- Choose the ATtiny3227/1627/827/427 w/Optiboot option under the megaTinyCore heading on the Board menu.
- Check that the subsequent options are set as follows (ignore any other options):
Board: "ATtiny3227/1627/827/427 w/Optiboot"
Clock Speed: "5 MHz internal"
Specifying a 5MHz clock ensures that the ATtiny3227 will continue to run down to a supply voltage of 1.8V.
The default settings define PB4 as a reset pin, for use by the Optiboot bootloader when uploading programs by FTDI.
- Connect a UPDI programmer to the UPDI pad on the Lisp Star board, and the GND and +5V pins on the FTDI connector:
- Set Programmer to the first of the "SerialUPDI - 230400 baud" options.
- Select the USB port corresponding to the USB to Serial board in the Port menu.
- Choose Burn Bootloader from the Arduino IDE Tools menu to install the bootloader in the ATtiny3227.
Now you've installed a bootloader you can upload uLisp via the serial port, which is accessed via the row of six pads on the Lisp Star.
Leave the Arduino IDE set to the same ATtiny3227/1627/827/427 w/Optiboot option as before, with the same settings. In particular, the clock speed should be left at 5MHz, as this will determine the timing of delay. This time the Programmer option is irrelevant.
Connect an FTDI board or FTDI cable to the FTDI header on the Lisp Star board. This can be the same FTDI board you used for the UPDI programming.
Connect other end of the FTDI cable to your computer's USB port, and select it from the Port menu.
Click Upload to upload uLisp to the ATtiny3227.
You should then be able open the Serial Monitor window and see the uLisp prompt:
The number before the prompt shows how many Lisp objects you have left available in the uLisp workspace.
Running programs on the Lisp Star
The following tutorial assumes you've got the Lisp Star connected to the USB port of a computer via an FTDI cable attached to the six FTDI pins on the Lisp Star, and that you can see the uLisp prompt in the Arduino IDE's Serial Monitor.
Entering a command
To enter a command, type it in the text box at the top of the Serial Monitor, and press return.
For example, enter these Lisp commands:
(pinmode 5 :output) (digitalwrite 5 :high)
LED 5, the one at the 2 o'clock position, should light up. The first command defines pin 5 as an output, and the second command turns it on. To turn it off again, enter:
(digitalwrite 5 :low)
Writing a function
Those commands took effect immediately. You can also define a function, which specifies commands to be stored and executed later. Try entering this:
(defun led (pin) (pinmode pin :output) (digitalwrite pin :high) (delay 250) (digitalwrite pin :low) (delay 750))
The command defun means "define function", and the name led is the name we've chosen for the function. This flashes a specified LED for 100 milliseconds and then waits for 300 milliseconds.
The name pin is called a parameter. It will get replaced by the number of the LED we want to flash.
When you enter this defun nothing happens to the LEDs, but the function is stored in the Lisp Star. Now enter the command:
This should flash the LED at 10 o'clock. So now you have a general function that let's you flash any of the six LEDs by specifying a parameter from 0 to 5.
Running a loop
The last example shows how you can write a program to automate a sequence of operations. Enter this program:
(defun blinks () (dotimes (x 1500) (led (random 6))))
This defines a new function, blinks. It doesn't take a parameter, so we give an empty pair of brackets.
The dotimes command repeats the subsequent commands a specified number of times, in this case 1500 times. The call:
gives a random number from 0 to 5. It calls the function led we defined earlier with this number as a parameter, to flash a random LED.
To run the program give the command:
To stop the program, enter ~ and press return. Otherwise it will stop after 600 random blinks, or 10 minutes.
If you want to change the definition of led or blinks just type in a new defun specifying the same name, and it will replace the old version.
Making your program permanent
Currently your programs led and blinks are stored in the Lisp Star's workspace, but will be lost if you press reset, or remove the battery.
However, uLisp has a feature that allows you to store uLisp programs in flash memory, and make them load and run automatically when you press reset. To do this you put them as a C string at the start of the uLisp source after the comment:
// Lisp Library
Then you need to:
- Start with the line:
const char LispLibrary =
- Don't include the word PROGMEM; it's not needed for the ATtiny3227.
- Enclose every line in double quotes, so it is treated as a C string.
- End with a semicolon.
You also need to escape double quotes in the program, but there aren't any in the following examples.
For example, for the functions we've already defined you should put:
const char LispLibrary = "(defun led (pin)" "(pinmode pin :output)" "(digitalwrite pin :high)" "(delay 100)" "(digitalwrite pin :low)" "(delay 300))" "(defun blinks ()" "(dotimes (x 1500)" "(led (random 6))))" "(blinks)" ;
Finally, you need to uncomment the line:
so the functions are run when uLisp starts. The last line causes blinks to run as soon as the programs are loaded.
Making the pushbutton work as an on/off switch
There's one further refinement that makes it so you can press the pushbutton to start the LED display, and press it again to turn it off.
To do this, define a new function run that makes use of a special memory location :flag:
(defun run (fun) (register :flag (1+ (register :flag))) (when (oddp (register :flag)) (funcall fun)) (dotimes (x 21) (pinmode x :output) (digitalwrite x 0)) (sleep))
The first line adds one to the contents of the memory location :flag. If :flag is odd, the second line calls the function we've passed as a parameter. It then defines all the I/O pins as low outputs, to keep the power consumption at a minimum, and calls sleep to put the processor to sleep.
If you don't press the pushbutton, the processor will go to sleep after the specified function has finished running, in the case of blinks about 10 minutes.
To call it with blinks give the command:
Here's the whole LispLibrary version of the blinks program with the pushbutton feature:
"(defun led (pin)" "(pinmode pin :output)" "(digitalwrite pin :high)" "(delay 100)" "(digitalwrite pin :low)" "(delay 300))" "(defun blinks ()" "(dotimes (x 1500)" "(led (random 6))))" "(defun run (fun)" "(register :flag (1+ (register :flag)))" "(when (oddp (register :flag)) (funcall fun))" "(dotimes (x 21) (pinmode x :output) (digitalwrite x 0))" "(sleep))" "(run blinks)"
The following sections give some more programs for the Lisp Star.
The firebug program creates a flickering light that appears to jump from LED to LED, a bit like a firebug flying around in the night:
(defun firebug () (dotimes (x 6) (pinmode x :output)) (let ((bug 0)) (dotimes (x 24000) (digitalwrite bug nil) (incf bug (1- (random 3))) (setq bug (max (min bug 5) 0)) (digitalwrite bug t) (delay 25))))
It first defines all the LEDs as outputs. It keeps track of the last LED it lit up in the variable bug. Each time around the dotimes loop it turns off the LED corresponding to the previous value of bug, adds -1, 0, or 1 to bug, makes sure the value is in the range 0 to 5, and then turns on that LED for 25 milliseconds.
Here's the LispLibrary version:
"(defun firebug ()" "(dotimes (x 6) (pinmode x :output))" "(let ((bug 0))" "(dotimes (x 24000)" "(digitalwrite bug nil)" "(incf bug (1- (random 3)))" "(setq bug (max (min bug 5) 0))" "(digitalwrite bug t)" "(delay 25))))" "(defun run (fun)" "(register :flag (1+ (register :flag)))" "(when (oddp (register :flag)) (funcall fun))" "(dotimes (x 21) (pinmode x :output) (digitalwrite x 0))" "(sleep))" "(run firebug)"
The program flashes the LEDs 24000 times, equivalent to about 10 minutes, before going to sleep.
This program makes each LED flash at a regular rate, but the rates of all six LEDs are slightly different, producing a pattern that seems never to repeat:
(defun twinkle () (dotimes (x 6) (pinmode x :output)) (dotimes (n 6000) (let ((m (+ n 200))) (dotimes (i 6) (digitalwrite i (zerop (mod m (+ 30 i)))))) (delay 100)))
The twinkle program works as follows:
First it defines all the LEDs as outputs. Then it counts an integer m up from 200 to 6200. The lights are assigned the numbers 30, 31, 32, 33, 34, and 35. When n is exactly divisible by a light's number the light is illuminated; otherwise it is off. There's then a delay of 100 milliseconds. So the first light flashes at a rate of 10/30Hz, or once every three seconds. Likewise, the second light flashes at 10/31Hz, and so on up to 10/35Hz for the last light.
The result is that each light flashes at a regular rate, but because the numbers 30, 31, 32, 33, 34, and 35 are coprime, no two lights ever flash in synchrony, and the pattern never repeats.
Here's the LispLibrary version:
"(defun twinkle ()" "(dotimes (x 6) (pinmode x :output))" "(dotimes (n 6000)" "(let ((m (+ n 200)))" "(dotimes (i 6)" "(digitalwrite i (zerop (mod m (+ 30 i))))))" "(delay 100)))" "(defun run (fun)" "(register :flag (1+ (register :flag)))" "(when (oddp (register :flag)) (funcall fun))" "(dotimes (x 21) (pinmode x :output) (digitalwrite x 0))" "(sleep))" "(run twinkle)"
The program flashes the lights 6000 times, equivalent to about 10 minutes, before going to sleep.
Ringing the changes
This program, ring, flashes the LEDs in each of the possible sequences, starting with 012345, 012354, 012435, and so on up to 543210. It's called ring because it's like the traditional way of ringing church bells in every possible permutation, called "ringing the changes".
This is the most complicated of the programs given here, and I'll explain how each of the components work.
The number of possible permutations of n items is n factorial. This is calculated by the following recursive routine fac:
(defun fac (n) (if (zerop n) 1 (* n (fac (1- n)))))
For example, to calculate the number of permutations of six items:
> (fac 6) 720
Remove an item from a list
The next routine rmv removes an item from a list:
(defun rmv (x lst) (mapcan #'(lambda (y) (unless (eq x y) (list y))) lst))
> (rmv 1 '(0 1 2 3 4 5)) (0 2 3 4 5)
Find the nth permutation of a list
Now we can write per, which finds the nth permutation of a list of items in lst:
(defun per (n lst) (when lst (let* ((k (fac (1- (length lst)))) (div (truncate n k)) (elt (nth div lst)) (m (mod n k))) (cons elt (per m (rmv elt lst))))))
396> (per 0 '(0 1 2 3 4 5)) (0 1 2 3 4 5) 396> (per 1 '(0 1 2 3 4 5)) (0 1 2 3 5 4)
and the last permutation is:
396> (per 719 '(0 1 2 3 4 5)) (5 4 3 2 1 0)
The routine per works as follows:
Imagine the 720 permutations of (0 1 2 3 4 5) arranged in alphabetical order. The first 720/6 or 120 of these will start with 0, the next 120 will start with 1, and so on. So the nth permutation will start with:
(truncate n 720)
That gives us the first item in the nth permutation. To get the next item we remove that item from the list, and recursively call per to find the mth item in the remaining list, where m is:
(mod n 720)
The items are joined together with cons to give the resulting permutation.
Flash an LED
The routine led flashes a specified LED:
(defun led (pin) (digitalwrite pin :high) (delay 100) (digitalwrite pin :low) (delay 300))
Ring the changes
Finally, here's the program ring:
(defun ring () (dotimes (x 6) (pinmode x :output)) (dotimes (x (fac 6)) (mapc #'led (per x '(0 1 2 3 4 5))) (delay 600)))
It calls per to find the next permutation of the starting list, and then maps led to that to flash the LEDs in the corresponding sequence.
Here's the LispLibrary version:
"(defun fac (n)" "(if (zerop n) 1 (* n (fac (1- n)))))" "(defun rmv (x lst)" "(mapcan #'(lambda (y) (unless (eq x y) (list y))) lst))" "(defun per (n lst)" "(when lst" "(let* ((k (fac (1- (length lst))))" "(div (truncate n k))" "(elt (nth div lst))" "(m (mod n k)))" "(cons elt (per m (rmv elt lst))))))" "(defun led (pin)" "(digitalwrite pin :high)" "(delay 100)" "(digitalwrite pin :low)" "(delay 300))" "(defun ring ()" "(dotimes (x 6) (pinmode x :output))" "(dotimes (x (fac 6))" "(mapc #'led (per x '(0 1 2 3 4 5)))" "(delay 600)))" "(defun run (fun)" "(register :flag (1+ (register :flag)))" "(when (oddp (register :flag)) (funcall fun))" "(dotimes (x 21) (pinmode x :output) (digitalwrite x 0))" "(sleep))" "(run ring)"
It takes about 36 minutes to flash the 720 permutations before going to sleep.
Get the Eagle files for the PCB here: https://github.com/technoblogy/lisp-star.
Or order boards from OSH Park here: Lisp Star.
Or order boards from PCBWay here: Lisp Star Pendant.
- ^ Mini Push Button Switch - SMD on SparkFun.
- ^ Mini Push Button Switch (SMD) on Proto-PIC.
- ^ Coin Cell Battery Holder - 12mm (SMD) on Farnell.
- ^ Pogo Pin Probe Clip - 6 Pins on Adafruit.
- ^ Pogo Pin Probe Clip on AliExpress.
- ^ SparkFun FTDI Basic Breakout - 5V on Sparkfun.
- ^ FTDI Serial TTL-232 USB Cable on Adafruit.
blog comments powered by Disqus