ARM NeoPixel driver using assembler
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'. However the pulses are very short; a zero is defined as having a maximum width of 500ns, which is just 8 cycles on a 16 MHz CPU, or 12 cycles on a 24 MHz CPU. Most NeoPixel libraries therefore use assembler routines tailored to each processor, at least for the low-level pulse generation. This is therefore an excellent application for the built-in assember in uLisp.
This page gives a NeoPixel routine for a single NeoPixel display on boards based on the ATSAMD21, such as the Adafruit QT-Py or Adafruit Neo Trinkey.
Introduction
NeoPixel is the name given by Adafruit to the WS2812, a chainable RGB LED invented in 2013 by the Shenzhen-based company Worldsemi. You can power them with 5V or 3.3V, and chain them together, tying the data-out pin of one NeoPixel to the data-in of the next one in the chain. There's an excellent overview of NeoPixels on the Adafruit site [1].
You can get single NeoPixels in a variety of shapes and sizes: Through-hole: 8mm [2], and 5mm [3]; SMD: 5050 [4], 3535 [5], 2427 [6], and 1515 [7]. You can also get them already mounted in almost any configuration, including strips, discs, and matrixes.
The NeoPixel protocol
The datasheet [8] specifies these timings as the ideal timings for the zero and one bits, and the gaps after each bit:
Here is a table showing the number of clock cycles that each of these timings correspond to with the standard 48MHz clock on the ATSAMD21. 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 NeoPixel display by calling the routine with a parameter specifying the colour of the display. For example, to set the display to yellow:
(neopixel #x070700)
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
The following routine assumes that the NeoPixel display is driven by a pin on port A. 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)
The actual pin number is defined as follows; for example, on the Adafruit QT-Py the NeoPixel is connected to PA18:
(defvar pin 18)
On the Adafruit Neo Trinkey the NeoPixels are connected to PA5, so change this to:
(defvar pin 5)
Finally, here’s the whole assembler routine:
(defcode neopixel (a) ($push '(lr r5 r4)) ($ldr 'r4 porta) ($mov 'r1 1) ($lsl 'r3 'r1 pin) ; NeoPixel pin ($str 'r3 '(r4 dirset)) ; make pin an output ($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) ($pop '(r4 r5 pc)) porta ($word #x41004400))
The colour parameter is passed to the assembler routine in r0. The bits in this parameter are tested one at a time, starting with bit 23, and then the appropriate code at the labels zero or one is executed to generate a pulse with the appropriate timing.
$cpsid and $cpsid instructions are used 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.
- ^ NeoPixel Überguide on Adafruit.
- ^ NeoPixel Diffused 8mm Through-Hole LED on Adafruit.
- ^ NeoPixel Diffused 5mm Through-Hole LED on Adafruit.
- ^ NeoPixel RGB 5050 LED on Adafruit.
- ^ Mini 3535 RGB LEDs on Adafruit.
- ^ NeoPixel Nano 2427 RGB LEDs on Adafruit.
- ^ NeoPixel Addressible 1515 LEDs on Adafruit.
- ^ WS2812 datasheet on Adafruit.