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 SAMD21 and SAMD51 microcontrollers, such as the Adafruit Feather M0 and M4, Grand Central, 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 three libraries. Two 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 third — Adafruit_NeoPixel — is in the Library Manager, so you can install that way (usually easier) or use the third 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):

Download: file
#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 Metro M0

Arduino Zero

5, 12, MOSI*

Metro M4

6, 11, A3, MOSI*

Metro M4 AirLift

6, 11, MOSI*

Grand Central

11, 14, 23, MOSI*

Feather M0 Basic Proto

5, 6, 12, MOSI*

Feather M0 Express

6, 12, MOSI*

Feather M4

12, A2, A4, MOSI*

ItsyBitsy M0

5, 12, MOSI*

ItsyBitsy M4

2, 5, 12, MOSI*

HalloWing M0

4 (NEOPIX), 6, MOSI*

HalloWing M4

6, 8, A5, MOSI*

MONSTER M4SK

2

PyPortal (all versions)

3 (SENSE)

PyGamer (all versions)

12, A4

PyBadge

A4, MOSI*

Trellis M4

10 (keypad NeoPixels)

Circuit Playground Express

A2

Trinket M0

4**

Gemma M0

D0**

* If using the MOSI pin on these boards, the normal SPI peripheral can’t be used for other things. Occasionally SPI is used for display add-ons, SD cards and so forth. But if you’re not using any shields or FeatherWings, that pin is fair game.

** DMA NeoPixels on these boards can not be used simultaneously with I2C, Serial1 or SPI. Regular GPIO in/out is OK.

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 these ARM chips are 3.3 Volt devices, 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 now keeps track of time correctly.

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:

Download: file
#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 SAMD21 and SAMD51 (aka M0 and M4) 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.

This guide was first published on Jul 08, 2017. It was last updated on Jul 08, 2017.
This page (Overview) was last updated on Oct 25, 2020.