Overview

If you’ve used NeoPixels extensively you’ve likely seen some issues in how they interact with other code. The Arduino millis() and micros() functions lose accuracy, the Servo library may stutter, or refreshing a very long NeoPixel strip can slow a program down.

Sometimes you can shift a task to another part of the microcontroller, as in this NeoPixels-and-Servos guide. That project’s code is specific to the 8-bit AVR microcontroller used in “classic” Arduino-type boards…it won’t work on more modern 32-bit boards. But the core idea — exploiting a microcontroller’s specific peripherals to handle a task — still applies. It’s just a different microcontroller with different peripherals.

This guide solves the problem from the other direction: using device peripherals to drive the NeoPixels rather than the servos. But really it’s applicable to all kinds of problems, not just servos…any NeoPixel-heavy task involving accurate timekeeping or handling interrupts.

The code here works only on boards based on the ARM Cortex-M0 microcontroller, such as the Adafruit Feather M0, Circuit Playground Express or Arduino Zero, using these devices’ DMA-driven SPI capability. (Other boards, like the Teensy 3, may offer their own solutions…see the NeoPixel Überguide’s “Advanced Coding” page.)

Setting Up

This requires installing four libraries. Three of these are new and experimental, so you won’t find them in the normal Arduino Library Manager…you’ll need to download and install these manually in your Documents/Arduino/Libraries folder. The fourth — Adafruit_NeoPixel — is in the Library Manager, so you can install that way (usually easier) or use the fourth link below.

Using the Adafruit_NeoPixel_ZeroDMA Library

We’ll assume some familiarity with the regular Adafruit_NeoPixel library. If you’ve never used NeoPixels before, it’s best to start with the examples included with that library.

Usage is nearly identical to the regular Adafruit_NeoPixel library…only the #include line and the object type for the strip declaration are different (but the parameters are the same):

#include <Adafruit_NeoPixel_ZeroDMA.h>

Adafruit_NeoPixel_ZeroDMA strip(60, 5, NEO_GRB);

All other functions (begin(), show() and so forth) are invoked exactly the same as the regular Adafruit_NeoPixel library…so rather than repeat all that information here, just look at any existing NeoPixel example code to see how it works. Only the above two lines are different!

No Free Lunch

There are a few “gotchas” to be aware of. One…because hardware peripherals are used, and these are a finite resource, this alternate NeoPixel library only works with specific pins:

Board(s)

Supported Pins

Adafruit Feather M0

Metro Express

Arduino Zero

5, 11, A5, 23 (MOSI on 6-pin programming header)

Circuit Playground Express

Pin 8 (onboard NeoPixels), A2, A7 (TX)

Gemma M0

Pin 0

Also, the technique used in this library requires considerably more RAM for the NeoPixels…a little over four times as much. Where the “classic” library might use 180 bytes for 60 RGB NeoPixels, the new library consumes about 800 bytes for the same.

Keep in mind that the M0 chip is a 3.3 Volt device, while NeoPixels want 5V logic. As explained in the NeoPixel Überguide, a logic level shifter may be required, or you can use tricks such as a slightly lower supply voltage for the NeoPixels (e.g. 4.5V from three alkaline cells, or 3.7V from a LiPoly battery).

The Payoff

If you can work with the above constraints — compatible board, pin number and RAM usage — your code may run noticeably faster, plus you can now use the regular Arduino Servo library, and any code that relies on the millis() or micros() functions will not lose track of time.

Additionally, getPixelColor() works correctly when used in combination with setBrightness(). In the classic NeoPixel library, changing brightness is a “destructive” operation and the value returned by getPixelColor() is only an approximation.

NeoMatrix Too…

There’s also a variant of the Adafruit_NeoMatrix library that uses the same methodology:

NeoMatrix is already explained in the NeoPixel Überguide, so we won’t cover the basics here.

Just like Adafruit_NeoPixel_ZeroDMA, the alternate NeoMatrix library requires just a couple changes to existing sketches — the #include line and object declaration:

#include <Adafruit_NeoMatrix_ZeroDMA.h>

Adafruit_NeoMatrix_ZeroDMA matrix(5, 8, PIN,
  NEO_MATRIX_TOP     + NEO_MATRIX_RIGHT +
  NEO_MATRIX_COLUMNS + NEO_MATRIX_PROGRESSIVE,
  NEO_GRB);

Everything else works the same as the regular library.

The same limitations that apply to Adafruit_NeoPixel_ZeroDMA also apply to Adafruit_NeoMatrix_ZeroDMA: it’s specific to Cortex-M0 boards, works only on certain pins, and has a considerable RAM overhead (12 bytes per pixel for RGB NeoPixels, 16 bytes for RGBW pixels).

How Does It Work?

Magic!

Okay, not really.

As the library name implies, DMA (direct memory access, the ability of this chip to move data directly between peripherals and memory without CPU intervention) is part of the solution. But this particular ARM chip does not support DMA to the GPIO PORT registers which set pins “high” or “low” (akin to Arduino’s digitalWrite() function).

The chip’s SPI peripherals (of which there are several, explained in this guide about SERCOMs) do support DMA. By pulling some shenanigans, we can persuade SPI to issue a bit pattern that resembles NeoPixel data. The duty cycle doesn’t precisely match the datasheet, but it’s close enough to work. (The availability of SPI SERCOMs is why this library only works on specific pins.)

To do this, we expand every bit of NeoPixel color data by a factor of 3 — with a bit pattern of 100 to represent a NeoPixel “zero” bit, and 110 to represent a “one” bit. We then send this data out the SPI bus at 2.4 MHz…with the 3:1 bit expansion, this matches the NeoPixels’ desired 800 KHz data rate.

This 3:1 expansion explains the additional RAM use in this library. There’s the original NeoPixel color data in memory (3 or 4 bytes per pixel for RGB or RGBW), then a 3-fold bit-expanded copy to send over SPI.

Geting the end-of-data latch (300 microseconds of a LOW logic state) proved tricky, because the SPI idle state is a HIGH logic level. Switching quickly between SPI and GPIO at the beginning and end of each transaction didn’t quite work…this would always “glitch” the first bit out and mess up the first NeoPixel’s color. Solution was to end the NeoPixel data transfer with 300 microseconds worth of zero bits (about 90 bytes worth), then set DMA to run in an endless loop. So…even when the pixels aren’t changing, data is being sent again and again regardless. This doesn’t cost us anything time-wise, there’s zero impact on program speed, because this is all handled through DMA.

Last updated on 2017-08-31 at 09.12.40 PM Published on 2017-07-08 at 07.24.49 PM