Teensy APA102 POV Poi / Pixel Poi Build. Tutorial

I prefer making my own PCB to reach highest density possible (e.g. 4 LED/cm)
though still in prototype stage.

I hope you'll post some picture or share a video when it's working. :)

Quite a good number of people would probably be interested to see this!
Sure, here is the board I made, near June i think.

with 4 - apa102 2020s and an Arduino controller it worked with 3, a second prototype with careful hot-air solder did work with 4
the movie is the first prototype and works with 3,
the ruler with Blood clots shows the 4 pixels is stacked in 1 centimeter.

And should likely work with 80 or 100 LEDs, (untested with 2020)
I DID made a working POV-staff with 72 apa102s(5050)&T3.6 , power seems pretty stable with a 3500mAh 18650.
(see my AoT picture)

I made the board with Fritzing (sorry, stlil using Fritzing ;) ) with "raw pads" with correct dimensions,
I knew it would mess up (so many pad object!) when chaining with 100's
so the progress stall until Fritzing community helped out on drawing a part
(also because I ran into some very-busy performance schedule)

inside the forum you can find the Fritzing part for apa102-2020,
and my working prototype board in Fritzing.

now my latest PCB with T3.6
still working on belows (mostly 1.)

1. Designing a single button Hardware user controll system, that can swap between "programmed stock 1~10" "programmed picture with single display" "random Marquee" and e.t.c.
If anyone knows, kinda like the PodPoi (made by flowlight) single button system.

this change is HUGE, previous designed the switch button controlls the hardware POWER LINE,
and the next version the power is permanently connected and the whole device is controlled by Teensy with N-mos

I wish to use a touch-sense pad to do that button.
I know TSI needs 600~3000us and that will likely affect the POV display, periodic checks should be improper
maybe needs DMA/interrupt?
can't find much library, snoozes supports wakeup with TSI(not sure if interrupt?)
I'm still looking for solution,

help me please~~

yeah A momento button could do fine, but touch pad is coooool~~ :cool::cool::cool:

2. using the MTP library also, and not yet test with "duration" test , like, delete&copy for hundred times?
if this is okay, then the SD card would be secured and kept unreachable to user.
but most hard to fit this in 1. system.

also looking if the MTP works with second USB port, so the USB socket can place at other.

if not I'll just left the SD socket reachable.

3. The BGA chip on T3.5/3.6 cannot take too much impact so I wish to place the Teensy inside the tube in order to take less impact (that makes USB/SD unreachable)
also contacting some Japan made- super protective silicon product for the chip.
The new board awaits on 1.2.3. to complete, so can I change the, again Fritzing board, to complete :)
(Sorry, If anyone interest I could draw some hand diagram schemes)

Thx everyone, Thx Paul.
I'll keep moving.

Happy Mid-Autumn Festival, enjoy the moon.
feel like I should also sharing these:
my previous HD design with (3528RGBLED and TLC5947+DMASPI) on T3.6
not the densest so I left it behind, and the Left 32 LEDs are not working well due to some false design on current controll pin.
A POV leviwand with 72 apa102-5050 / T3.6 ,
more than 1,800 lines/sec and depends on SD card
A POV staff with 36 apa102 -5050 / Teensy LC
this one does 1,200 lines with DMASPI2,
Last edited:
I need help, I've been searching for weeks
2x 128 LED APA 102 2020
Teensy 3.6 with SD Sandisk 96MB / Sec
Patterns are displayed, colors O.K
However, the refresh rate is too low for displaying fonts.
Where can the error be
With best thanks
Here's the code

/* ---------------- Ausgang Nadelspitzen einer
This code uses the T3.6 onboard sd card slot, BMP stored on the card are light painted using a cheap strip of ws2811 leds using FastLed
code derived from lightpainting sketch:
and this post:
------> https://forum.pjrc.com/threads/40871...file-read-fail

#include <SPI.h>
#include <SD.h>
#include <FastLED.h>

File bmpFile;
const int chipSelect = BUILTIN_SDCARD;

// you can remove all Serial.print when you have your paint staff
// set up, this is just for debug

int bmpWidth, bmpHeight;

int bmpLength = 600;//these two need filling in first
int paintTime =3000;//these two need filling in first
int time = paintTime/bmpLength;
uint8_t bmpDepth, bmpImageoffset;
#define BUFFPIXEL 512

unsigned int Color(byte g, byte r, byte b); //placed here to avoid compiler error

// How many leds in your strip?
#define NUM_LEDS 128
#define BRIGHTNESS 125
#define DATA_PIN 11 //the pin that Led strip is attached to
#define CLOCK_PIN 13 //13 = second hardware spi clock
int paintSpeed = 1; //adjust this to vary image refresh rate

void setup(void) {
FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, GRB>(leds, NUM_LEDS);
//test our led strip - you can remove this to the comment line "// if you dont get ..."
for(int x=0;x<NUM_LEDS;x++){
leds[x] = CRGB::Green;}
for(int x=0;x<NUM_LEDS;x++){
leds[x] = CRGB::Red;}
for(int x=0;x<NUM_LEDS;x++){
leds[x] = CRGB::Blue;}
for(int x=0;x<NUM_LEDS;x++){
leds[x] = CRGB::Black;}
// if you dont get all leds lighting red then going off, check your wiring

Serial.print("Initializing SD card...");

if (!SD.begin(chipSelect)) {
Serial.println("initialization failed!");
Serial.println("SD OK!");

void loop() {
if (digitalRead(7) == LOW) {
Serial.println("Button is not pressed...");
} else {
Serial.println("Button pressed!!!");


bmpDraw("q995.bmp", 4000);
bmpDraw("q996.bmp", 4000);
bmpDraw("q997.bmp", 4000);
bmpDraw("q998.bmp", 4000);
bmpDraw("q999.bmp", 3000);

for(int x=0;x<NUM_LEDS;x++){
leds[x] = CRGB::Black;}

//////////////////Function to read BMP and send to Led strip a row at a time/////////////////////
void bmpDraw(char* filename, unsigned long time){ //, unsigned long time)
unsigned long currentTime = millis();
//so that the image continues to be displayed again for a set time
while (millis()< currentTime + (time)) {

File bmpFile;
int bmpWidth, bmpHeight; // W+H in pixels
uint8_t bmpDepth; // Bit depth (currently must report 24)
uint32_t bmpImageoffset; // Start of image data in file
uint32_t rowSize; // Not always = bmpWidth; may have padding
uint8_t sdbuffer[3*BUFFPIXEL]; // pixel in buffer (R+G+B per pixel)
uint32_t povbuffer[BUFFPIXEL]; // pixel out buffer (16-bit per pixel)//////mg/////this needs to be 24bit per pixel////////
uint32_t buffidx = sizeof(sdbuffer); // Current position in sdbuffer
boolean goodBmp = false; // Set to true on valid header parse
boolean flip = true; // BMP is stored bottom-to-top
int w, h, row, col;
int r, g, b;
uint32_t pos = 0, startTime = millis();
uint8_t povidx = 0;
boolean first = true;

// Open requested file on SD card
bmpFile = SD.open(filename);
// Parse BMP header
if(read16(bmpFile) == 0x4D42) { // BMP signature
Serial.print("File size: ");
(void)read32(bmpFile); // Read & ignore creator bytes
bmpImageoffset = read32(bmpFile); // Start of image data
Serial.print("Image Offset: ");
Serial.println(bmpImageoffset, DEC);
// Read DIB header
Serial.print("Header size: ");
bmpWidth = read32(bmpFile);
bmpHeight = read32(bmpFile);
if(read16(bmpFile) == 1) { // # planes -- must be '1'
bmpDepth = read16(bmpFile); // bits per pixel
Serial.print("Bit Depth: "); Serial.println(bmpDepth);
if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed

goodBmp = true; // Supported BMP format -- proceed!
Serial.print("Image size: ");

// BMP rows are padded (if needed) to 4-byte boundary
rowSize = (bmpWidth * 3 + 3) & ~3;

// If bmpHeight is negative, image is in top-down order.
// This is not canon but has been observed in the wild.
if(bmpHeight < 0) {
bmpHeight = -bmpHeight;
flip = false;

w = bmpWidth;
h = bmpHeight;

for (row=0; row<h; row++) {
if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
else // Bitmap is stored top-to-bottom
pos = bmpImageoffset + row * rowSize;
if(bmpFile.position() != pos) { // Need seek?
buffidx = sizeof(sdbuffer); // Force buffer reload

for (col=0; col<w; col++) { // For each column...
// read more pixel data
if (buffidx >= sizeof(sdbuffer)) {
povidx = 0;
bmpFile.read(sdbuffer, sizeof(sdbuffer));
buffidx = 0; // Set index to beginning
// set pixel
r = sdbuffer[buffidx++];
g = sdbuffer[buffidx++];
b = sdbuffer[buffidx++];
//Serial.print(r);Serial.print(" ");Serial.print(g);Serial.print(" ");Serial.println(b);
//we need to output GRB 24bit colour//
povbuffer[povidx++] =(g<<16) + (r<<8) +b; //original code is b r g, should be g r b?

for(int i=0;i<NUM_LEDS;i++){
delay(paintSpeed);// change the delay time depending effect required
} // end scanline

} // end goodBmp
}//end of IF BMP


//*************Support Funcitons****************//
// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.
uint16_t read16(File& f) {
uint16_t result;
((uint8_t *)&result)[0] = f.read(); // LSB
((uint8_t *)&result)[1] = f.read(); // MSB
return result;
uint32_t read32(File& f) {
uint32_t result;
((uint8_t *)&result)[0] = f.read(); // LSB
((uint8_t *)&result)[1] = f.read();
((uint8_t *)&result)[2] = f.read();
((uint8_t *)&result)[3] = f.read(); // MSB
return result;
Hi @sharky - where / how are you trying to display fonts? I can not see it from the above code.
Last edited:
q999.bmp is a picture (128x619px) Logo Coca cola the LED move properly but much too slow.


  • 999.jpg
    23.2 KB · Views: 231
Last edited:
this sounds like a limitation of accessing the that amount of data quickly enough from the SD card. in message #201 of this thread Po Ting is using 90 pixel high, and only getting 1666 per second. you should really have two full revolutions per second for for POV with poi at the least. This would give 833 frames a rotation using 90leds. You are using 128 to output data to. so you probably only get one stratched image per rotation :
"using teensy 3.6 and features below
SDio & apa102 & FastLED CRGB->DMASPI

reading from SDio ontime, can get a 1,666 FPS with 24 bit BMP (256 color bmp even gives more FPS) on 90'pixel apa102 strips."
Have you tried reading the image data once, put that data into a buffer, then displaying a line at a time from that buffer?

Reading from SD cards is slow. Reading from buffers is going to be at least a thousand times faster. I'm displaying from a buffer to 200 APA102 strips at 500 fps, with most of the time spent writing to the strips, not getting data.

If that is still too slow, then increase the data speed to the strips, by changing:
FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, GRB>(leds, NUM_LEDS);


(Or whatever data rate is compatible with your wiring. Just start at 1 and increase it until the strips start to flicker.)
Hello Mortonkopf, Hello happyinmotion!
It should be reading from the SD the problem.
Thanks for the feedback with the DATA_RATE_MHZ (24) I've already tried - no change.
Happyinmotion you can show me how you have buffered the data solved. I am not so versed with the programming.
Thank you!
#include <SD.h>

you need SDfat library for SDio if I remember correct, the reading speed have a least 2 fold difference

if you are not into complicated coding
you can change your image to a smaller version, "thicker" i meant
you will lost some details
but if your audience don't recognize your logo then all is lost.
changing image in SD is relative easy and you should try as much as you can, photo shoping/ etc
to get your best results

DMASPI is another key library
if still needed I will share the code after i complete my latest version,
maybe a few weeks later, :)
Property tested again.
The graphic (128x619 pixel) is displayed in 10 seconds 8x.
I still tried with DMASPI but my programming skills do not suffice.
Have now the logo squashed.
3x on the length logo is now recognized.To practice O.K for demonstration not O.K.
Quality is not good. Will still try.
If someone has solved similar problem please ask for info
Thank you
did you mean squashed? the POV too thin in space?

if squashed, edit your pic with photoshop or something
and give a bigger width, like turning 128x619 to 128*1238 e.t.c.
Hello Po Ting I have the logo 3x instead of 1x in the width with Corelpaint squashed. Line width is just visible yet O.K. But the ratio horizon line to vertical line is hard to correct. would make man then with all pictures! Provisional is still feasible but a permanent solution should probably lie in the acceleration of reading from the SD card. best regards
Hello Po Ting I have the logo 3x instead of 1x in the width with Corelpaint squashed. Line width is just visible yet O.K. But the ratio horizon line to vertical line is hard to correct. would make man then with all pictures! Provisional is still feasible but a permanent solution should probably lie in the acceleration of reading from the SD card. best regards

squash is something happens when you show too high frames, try to slow down or make the picture wider,
drawing too fast is relative easy to correct with proper delays

if you are talking about too slow then I still point to SDio (SDfat Library?)

and also saying experience, for good POV effect
I recommend a recognizable item 8~20 repeats per 360 degrees, (a face,a flag, a pokemon, e.t.c.)
and recognizable words 20~50 letters per circle.

I said "recognizable" means the audience knows the picture, if not then speed doesn't matter much.
Hello Po Ting
I wanted to ask if you have finished the last version of your code already. I have tried many things (DMASPI) only without success. Have given up further attempts. Would it be possible that you could provide your code to me. - Thank you
Hello Po Ting
I wanted to ask if you have finished the last version of your code already. I have tried many things (DMASPI) only without success. Have given up further attempts. Would it be possible that you could provide your code to me. - Thank you

not yet,
here are part of code I use for the DMA previously, it puts the SPI data buffer into DMA buffer.
the code is used with FASTLED , mostly for its CRGB struct.

#define Rcorrect 0xFF
#define Gcorrect 0xB0
#define Bcorrect 0xF0

//DmaSpi::Transfer trx(address, DMASIZE, nullptr, 0 , &cs);
DmaSpi::Transfer trx(address2, DMASIZE, nullptr);

void DMAshow() {
  memset(&address, 0x00, DMASIZE);
  int dmaoffset = 4;
  for (int n = 0; n < NUM_LEDS; n++)
    dmaoffset = n * 4;
    address[dmaoffset] = 0xFF;
    address[dmaoffset + 1] = (((leds[n].b * brightness) >> 8) * Bcorrect) >> 8;
    address[dmaoffset + 2] = (((leds[n].g * brightness) >> 8) * Gcorrect) >> 8;
    address[dmaoffset + 3] = (((leds[n].r * brightness) >> 8) * Rcorrect) >> 8;
  if (FPSchecker) {
    while (micros() < dur3) {
    dur2 = micros();
    dur3 = dur2 + screen;
  while (trx.busy())
  memcpy(address2, address, DMASIZE);


you can start with some examples from fastLED to make sure connections (SPI pin) and strips were fine,
and then move on DMASPI, start from the example fastLED code
Last edited:
you're welcome,
i'm currently busy and only can copy&paste
maybe I can make a DMA-version of fastLED example later.

if you still need help or want to know detail of some-line, feel free to ask
I recently received some 198 LEDs / meter APA 102 2020 strips in the mail. Holy hell these things are tiny - so much so that they aren't that tightly squashed together, and the spacing works perfectly to put two strips side by side and offset them by a half-LED, to ultimately deliver an apparent LED density of 396 LEDs per meter.

The sad part is my near-complete lack of skills to pull off the code side of things. I can certainly output to multiple strips using FastLED, but haven't the foggiest idea where to start with the meat and potatoes. How should I approach this? I figure that I should I write one function which decides which strip each array item is on (i.e. odd numbers in the array are on one strip, even on the other), and then... figure out how to program. :p

To make the crazy even crazier, the LED strips don't have accessible copper... pretty much anywhere and a 1 meter POV baton is unwieldy. I was therefore thinking that the best way to get the density I'm hoping for is to have the two adjacent LED strips fold over the top of the polycarbonate I plan on attaching them to, and to have the pattern mirrored and "backwards" on the same strip.

My hunch is that there is some absurdly elegant and simple solution to modify the code to work with the kind of physical arrangement I described. Anyone know if something like this has been done, have advice on where I should start / what to read, and/or feel like taking a stab at any piece of this puzzle?

Thank you!
do you have some pictures of the strips
so i can see if there is anyplace you can hack the strip for cutting

and, maybe if it is possible to fold the strips so you still have the density " 396 pixels /m"
but only 50cm strip?

and in such case, you can split your buffer line, lets say your read line in a
CRGB buffer[198];
CRGB leds[198]; // for strip sorting, showing

for(i=0;i<99;i++) {
leds[i] = buffer[i];
leds[197-i] = buffer[99+i];

and if for two strips, you need two CRGB for showing? I think, if by parallel. you can do it chained together then it goes to case as above.
CRGB buffer[396];
CRGB leds1[198]; // for strip sorting, showing
CRGB leds2[198]; // for strip sorting, showing
for(i=0;i<198;i++) {
leds1[i] = buffer[i*2];
leds2[i] = buffer[i*2+1];

kinda like this
Po Ting's code for splitting the two led colour values from the one buffer looks right. Also, use the call to show() only once for all leds for a single instance of FastLed.
Hi, I am wondering if anyone can help get Adafruit's pov code to work on ESP8266. I believe the problem is that PROGMEM is handled differently in ESP chips. The code compiles but does not work. I know it is wired correctly as I have tested with a simple led chase code. The POV code in this thread also works but im hoping to get ESP to work with the Adafruit code as it has brightness control and remote controls. Im also working on getting the adafruit pov code merged with web based controls over the pattern selection.

Code is here. It works on teensy 3.2 but not on ESP8266. Fairly certain it is because progmem is handled differently. https://github.com/djnamaste/FastLED-Adafruit-Supernova-esp8266