Sometimes even a DMA-driven strand of NeoPixels isn’t enough. Sometimes you need a firehose of color!
NeoPXL8 (pronounced “NeoPixelate”) is a more intense approach to driving NeoPixels on SAMD21 and SAMD51 (aka M0 and M4) microcontrollers, with eight separate strands all running concurrently. It shares the same benefits as NeoPixel_ZeroDMA — minimal CPU use, nondestructive brightness setting, plays well with timekeeping functions and servos — and then adds:
- Faster overall refreshes. NeoPixels aren’t especially fast at receiving data, and sometimes programs must sit idle while data is issued to extremely long strands. By splitting the job across eight shorter strands, all writing at once, this wait time can be reduced significantly.
- More manageable topology for certain applications. Consider wearables: a costume incorporating NeoPixels in a single long strand may require lots of long “return wires” for data…pixels down one arm, return wire, pixels down another arm, return wire, legs and so forth…complicating the build and making it more prone to breakage. Separate strands for each area can make this easier and more robust!
Hardware
NeoPXL8 fares best on a SAMD21- or SAMD51-based board with lots of I/O pins. An Adafruit Metro Express, Metro M4 or Arduino Zero is ideal, but some Feather M0 and M4 boards can join in the fun as well…even ItsyBitsy M0 or M4 or the tiny QTPy M0 board, though you won’t get the full 8 outputs there. Don’t bother on a Circuit Playground Express or Gemma M0…with only a few I/O pins, they’re better served with a single strand and the Adafruit_NeoPixel_ZeroDMA library described on the prior page.
Keep in mind that these microcontrollers 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).
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_NeoPXL8 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 declaration are different:
#include <Adafruit_NeoPXL8.h> Adafruit_NeoPXL8 strip(60, NULL, NEO_GRB);
The first argument — the NeoPixel strand length — is similar to the original NeoPixel library, but in the case of this library there are eight times this number of pixels overall. So in the above example there are 60 pixels per strand, or 480 pixels overall. If using strands of different lengths, set this to the longest one.
The second argument can be used for assigning different pins to the 8 outputs, but there are very specific rules for this…we’ll cover that shortly. If you pass NULL here, this uses the library’s default pin assignment, which is spread across digital pins 0 through 7 (you’ll lose access to the Serial1 peripheral on pins 0 & 1 in this case).
Third argument is the pixel color order. Different generations of NeoPixel have used different byte ordering formats, and this lets you reassign them (including RGBW NeoPixels). All pixels on all strands must be the same type. You can leave this argument off if using the most common NeoPixel varieties (GRB byte order).
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 setup is different!
Pixels are addressed (via setPixelColor()) as a single sequential strand. Supposing the number-of-NeoPixels argument is 20 (meaning 160 pixels overall), the first physical strand would be pixels 0-19, second strand is 20-39, third is 40-59 and so forth, up to pixel #159 on the eighth strand.
Changing Pin Assignments
By default the library uses digital pins 0 through 7 for the eight concurrent NeoPixel strands. This works really well on the Metro Express and Arduino Zero, making the pin-to-strand wiring very intuitive. But sometimes you may have pins assigned to specific tasks (such as pins 0 & 1 for the Serial1 peripheral). Or on the Feather M0 boards, their small size means some of these pins aren’t even available.
It’s possible to reassign some of the output pins to other locations, but be aware that the options are very limited…most have one or possibly two alternate options. This is simply an artifact of how the microcontroller has been designed.
Default Pin |
Alternate Pin(s) |
0 |
12 |
1 |
10 |
2 |
MOSI or SDA |
3 |
A4 |
4 |
A3 |
5 |
SCK or SCL |
6 |
11 or MISO |
7 |
13 |
To use an alternate pin layout, first declare an array of eight signed 8-bit values, one for each output pin. Then pass this array as the second argument to the Adafruit_NeoPXL8 constructor. Here’s one possible pinout that might be used on a Feather M0 board:
int8_t pins[8] = { PIN_SERIAL1_RX, PIN_SERIAL1_TX, MISO, 13, 5, SDA, A4, A3 }; Adafruit_NeoPXL8 strip(60, pins, NEO_GRB);
This uses the SDA pin as a NeoPixel output, meaning that I2C can’t be used (any of the Wire library functions). But a lot of sensors and displays need I2C! We might instead declare it as:
int8_t pins[8] = { PIN_SERIAL1_RX, PIN_SERIAL1_TX, MISO, 13, 5, MOSI, A4, A3 };
Now I2C will work, but this knocks out SPI. On a small board like the Feather this will always be a balancing act of what peripherals you absolutely need vs. what the NeoPXL8 library is hardwarily compatible with.
Sometimes, if you need both peripherals, there’s no choice but to keep NeoPXL8 away from one or more pins entirely, in which case a value of -1 should be used. Here’s an example with only 7 outputs enabled:
int8_t pins[8] = { PIN_SERIAL1_RX, PIN_SERIAL1_TX, 11, 13, 5, -1, A4, A3 };
Note that when you do this these pixels still consume memory and are still indexed sequentially in the overall pixel buffer. Only 7 pins are in use but it always has to store the full 8 pins worth of pixel data.
The order of elements in this array determines which is the considered the “first” strand, which is the second, and so forth. Functionally it makes no difference but you might find it easier to route or plan wiring with the pins in a specific order. For example, suppose we wanted all the pins in increasing order by number, and let’s move that -1 to the end so there’s not an awkward gap in our pixel numbering:
int8_t pins[8] = { PIN_SERIAL1_RX, PIN_SERIAL1_TX, 5, 11, 13, A3, A4, -1 };
Whatever your pin arrangement, even if one or more are unused, this array must have exactly 8 elements. Fill any unused pin positions with -1. Eight elements, always, or there will be…trouble.
“Pin MUXing” is a complicated issue and over time we'll try to build up some ready-to-use examples for different boards and peripherals. You can also try picking your way through the SAMD21 datasheet or the NeoPXL8 source code for pin/peripheral assignments.
How Does it Work?
For all its nifty bells and whistles, one limitation of the SAMD21 microcontrollers that’s frustrated us repeatedly is that there’s no DMA access to the GPIO ports. There’s DMA nearly everywhere else (ADC, DAC, SPI, etc.) but for whatever reason they decided on this chip series there’d be no fiddling of GPIO bits without the CPU’s direct involvement.
Recently Ladyada spotted something in the datasheet, a side feature of one of the Timer/Counter peripherals called a “pattern generator.” This apparently has applications in motor control and such…it’s not documented very extensively, but is definitely DMA-accessible. Could this provide the sort of parallel GPIO output we’ve been wanting for various projects?
After a lot of trial-and-error, we found that this was indeed a workaround for the DMA-less GPIO output! It only provides 8 concurrent output bits (vs. up to 32 bits for the GPIO ports), but for many applications that’s sufficient.
The same Timer/Counter peripheral that provides the pattern generation also provides the 800 KHz timing required for NeoPixel data. We just need to reformat the NeoPixel data “sideways” in memory…as 8 bytes are issued concurrently, all the bit 7’s are issued in one go, then all the bit 6’s, bit 5’s and so forth. This does require a moment to process, but combined with DMA (which then requires nothing from the CPU) the overall effect can be a dramatic reduction in both CPU load and waiting-around time.
Text editor powered by tinymce.