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:

NeoPixelBits.gif

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:

NeoPixelCodes.gif 

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 dirsetoutset, 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.


  1. ^ NeoPixel Überguide on Adafruit.
  2. ^ NeoPixel Diffused 8mm Through-Hole LED on Adafruit.
  3. ^ NeoPixel Diffused 5mm Through-Hole LED on Adafruit.
  4. ^ NeoPixel RGB 5050 LED on Adafruit.
  5. ^ Mini 3535 RGB LEDs on Adafruit.
  6. ^ NeoPixel Nano 2427 RGB LEDs on Adafruit.
  7. ^ NeoPixel Addressible 1515 LEDs on Adafruit.
  8. ^ WS2812 datasheet on Adafruit.