Using uLisp on the Adafruit Neo Trinkey


#1

The Neo Trinkey is a USB-key sized board from Adafruit containing an ATSAMD21 ARM M0+ chip running at 48MHz, and four NeoPixel WS2812 serial addressable RGB displays:

For more information see:

Adafruit Neo Trinkey - SAMD21 USB Key with 4 NeoPixels

Installing uLisp on the Neo Trinkey

To install the ARM version of uLisp on the Neo Trinkey choose the Adafruit Gemma M0 board setting in the Adafruit SAMD Boards core. The performance is similar to the other M0 boards.

Having got uLisp running I thought it would be interesting to use the uLisp ARM assembler to write a routine to control the four NeoPixel displays.

Installing the assembler

The assembler itself is written in Lisp to make it easy to extend it or add new instructions. For example, you could write assembler macros in Lisp as shown below. For more information see: ARM Assembler Overview

Get the assembler here: ARM assembler in uLisp. To add it to uLisp: do Select All and Copy, Paste it into the field at the top of the Arduino IDE Serial Monitor window, and press Return .

Controlling the NeoPixels

NeoPixels use a non-standard protocol consisting of a serial stream of pulses, and the width of each pulse determines whether it is a ‘0’ or a ‘1’:

Here is a table showing the number of clock cycles that each of these timings correspond to with the 48MHz clock on the Neo Trinkey. All the timings have a tolerance of ±150 ns:

Time Cycles
T0H 350 ns 16.8
T0L 800 ns 38.4
T1H 700 ns 33.6
T1L 600 ns 28.4

The colour for each NeoPixel display is specified by a stream of 24 bits:

The following assembler routine lets you control the four displays on the Neo Trinkey by calling the routine with four parameters, each specifying the colour of one display. For example, to set the displays to yellow, cyan, magenta, white:

(neopixel #x070700 #x070007 #x000707 #x070707)

I’ve purposely kept the displays at a low brightness because at full brightness they can be a bit dazzling.

Delay macro

The key section of code in all these routines is a delay4 macro written in Lisp. This generates a list of the instruction words which will be inserted in the assembler at the appropriate point to get a delay of n clock cycles:

(defun delay4 (n)
  (list
   ($mov 'r5 n)
   ($sub 'r5 1)
   ($bne (- *pc* 2))))

This uses r5 as a counter to execute the loop a number of times, specified by the constant n specified in the parameter, and the total execution time is n * 4 cycles.

Assembler routine

On the Neo Trinkey the chain of four NeoPixel displays are connected to PA05. The base address of Port A is #x41004400, and offsets from this give access to the dirset, outset, and outclr registers to define a pin as an output, set a pin high, and set a pin low respectively. These are defined by the following defvar statements:

(defvar dirset #x08)
(defvar outset #x18)
(defvar outclr #x14)

Finally, here’s the whole assembler routine:

(defcode neopixel (a b c d)
  ($push '(lr r5 r4 r3 r2 r1 r0))
  ($ldr 'r4 porta)
  ($mov 'r1 1)
  ($lsl 'r3 'r1 5)
  ($str 'r3 '(r4 dirset)) ; make pin an output
  ($mov 'r2 4)
  nextled
  ($pop '(r0)) ; get bytes
  ($mov 'r1 1)
  ($lsl 'r1 23)
  nextbit
  ($tst 'r0 'r1) ; test if bit is 1
  ($bne one)
  zero
  ($cpsid 3)
  ($str 'r3 '(r4 outset))
  (delay4 4)             
  ($str 'r3 '(r4 outclr))
  (delay4 10)
  ($cpsie 3)
  ($b next)
  one
  ($str 'r3 '(r4 outset))
  (delay4 8)
  ($str 'r3 '(r4 outclr))
  (delay4 7)
  next
  ($lsr 'r1 1)
  ($bne nextbit)
  ($sub 'r2 1)
  ($bne nextled)
  ($pop '(r4 r5 pc))
  porta
  ($word #x41004400))

The four parameters are passed to the assembler routine in r0, r1, r2, and r3. The routine accesses these by pushing them all to the stack, and then popping them off one at a time as they are needed. I also pushed r4 and r5 as I used these in the routine.

The bits in each parameter are tested one at a time, and then the appropriate code at the labels zero or one is executed to generate a pulse with the appropriate timing.

Interrupts are disabled around the most time critical part of the routine, which generates the ‘0’ pulse, to prevent them for affecting the pulse timings.

You can write programs in uLisp to call the neopixel routine and generate animated colour displays.

Updates

29th April 2021: Changed the name of the macro to delay4 to avoid a conflict with the built-in uLisp function delay.

5th May 2021: Added $cpsid and $cpsid instructions to disable interrupts around the most time-critical part of the routine, which generates the ‘0’ pulse, to prevent them for affecting the pulse timings when you animate the NeoPixels.


#2

These are about US$6.95. I’ve ordered a couple and we’ll look at them when they arrive, probably next week.


#3

Interesting. As a proof of concept I changed the code to work with a different pin and got it to work on a Circuit Playground Express - which has ten pixels. (It uses the same chip as the ItsyBitsy M0, and has the two megabyte flash, so was trivial enough to compile for.)

I’ll need to mess with it a little more to use the lot of them. :)


#4

Glad to hear it works on that! With ten NeoPixels you will probably want to go for the list approach I took with the AVR version:

NeoPixel driver using assembler


#5

FYI, there is a board selection for the Neo Trinkey in Arduino 1.8.13 but the ulisp-arm code
will not compile, bailing out with:
ulisp-arm:173:2: error: #error “Board not supported!”
173 | #error “Board not supported!”
| ^~~~~

    -Rusty-

#6

Yes, I haven’t added a board definition for the Neo Trinkey to uLisp yet, but the Adafruit Gemma M0 board setting is almost identical, so use that one.


#7

Yeah. Just thought I’d try it and see what happens. :)


#8

Looking good! Received three of the little buggers this afternoon.
Definitely don’t turn on the LEDs full brightness!! Well, if you’d like
to keep your ability to SEE! Spiffy! Will play more after supper.
Thanks!

    -Rusty-

#9

The latest version of ARM uLisp, Version 3.6b, now lets you save the workspace to the Neo Trinkey’s flash memory using save-image. So you can save the ARM assembler and neopixel routine, and have it available when you next connect power.

This also lets you create a Lisp program that runs automatically when you connect power, as described below:

Creating animated displays

You can write programs in uLisp to call the neopixel routine and generate animated colour displays. For example, the following function animate takes a list of four colour definitions, and cycles them around the four displays repeatedly:

(defun animate ()
  (let ((lst '(#x000707 #x070707 #x070700 #x070007)))
    (loop
     (eval (cons 'neopixel lst))
     (delay 200)
     (setq lst (append (cdr lst) (list (car lst)))))))

Making the animation run automatically

You can use uLisp’s autorun feature to make the animate program run automatically when power is applied to the Neo Trinkey.

  • Uncomment the compile option:

    #define resetautorun
    
  • Upload uLisp (ARM Version 3.6b or later) to the Neo Trinkey board.

  • Copy and paste in the Lisp definitions for the ARM Assembler.

  • Copy and paste in the Lisp definitions for the neopixel routine and animate .

  • Check that animate works, and then type a ‘~’ to escape.

  • Save the Lisp image with the command:

    (save-image 'animate)
    
  • Unplug the Neo Trinkey, and then plug it in again.

The animate program should run automatically.

Powering the Neo Trinkey from a LiPo cell

I’ve made a dongle from a USB socket breakout board that lets you power the Neo Trinkey from a rechargeable LiPo battery:

Note: Do not attempt to recharge the LiPo cell from the 5V USB power; you need a specialised LiPo charger.