Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine neopixel timing #4102

Closed
dhalbert opened this issue Jan 30, 2021 · 6 comments · Fixed by #6312
Closed

Refine neopixel timing #4102

dhalbert opened this issue Jan 30, 2021 · 6 comments · Fixed by #6312
Assignees
Milestone

Comments

@dhalbert
Copy link
Collaborator

Users have seen some glitches writing NeoPixel strings on SAMD51 with CircuitPython. I took some timings in July 2020 of the NeoPixel one and zero waveforms, both for the Arduino code and neopixel_write. It seems like some SAMD51 times could be adjusted

image

@PaintYourDragon You mentioned recently about the "1/3 - 2/3" rule of thumb for NeoPixels, if I remember right. Do you see marginal values above, especially in the SAMD51 CircuitPython columns? Thanks.

@dhalbert dhalbert added this to the Long term milestone Jan 30, 2021
@PaintYourDragon
Copy link

PaintYourDragon commented Jan 30, 2021

Revisiting NeoPixel timing is One Of Those Things I’ve Been Meaning To Get Around To But There’s Always More Pressing Issues. The “1/3 - 2/3 rule” isn’t so much a rule as just a hacky thing that seemed to work for Adafruit_NeoPixel_ZeroDMA and Adafruit_NeoPXL8 but isn’t grounded in science, datasheets, or anything else. It might fall apart on really long runs, I haven’t checked.

Because datasheets lie, and because NeoPixel-alikes reshape the signal on the way out, a winning strategy would most likely be to time the output from the first pixel rather than the microcontroller. I’d be inclined to average the measurements over several specimens of each type (WS2812, WS2812B and SK6812, maybe others) and then model the software timing after that. At least we know 800 KHz is a Known And Valid Constant Of The Universe.

The SAMD51 first-pixel issue might be related to caching. It looks like common_hal_neopixel_write() is disabling instruction and data caches on certain SAMD devices, which is good and desirable for deterministic timing … I’d just double-check that it’s actually being compiled in on whatever the test device is. The Arduino library does some dirty pool with the SYSTICK counter instead, so it’s immune to caching and overclock settings, but I’m guessing CircuitPython avoided that strategy on purpose because Reasons.

If experiencing glitches, I’d suggest testing the same pattern with Adafruit_CircuitPython_NeoPixel_SPI if one can afford the RAM. If this works, then yes, it’s likely a timing issue (regardless, I think this will fix the first-pixel problem). If it still glitches, then “bit timing” is a red herring. Other things that can cause glitching include lack of level shifting (WS2812 variants are right at the threshold of often-working with 3.3V logic, but really should be level-shifted, while SK6812’s fare better at this) and sagging power, in which case:

  1. Try a less bright set of data to the pixels. If that doesn’t glitch, then it’s likely a too-small power supply.
  2. Try adding a 50 to 100 uF cap every meter or so.
  3. If powering a long strip from one end, don’t do that.

@tannewt
Copy link
Member

tannewt commented Mar 23, 2022

Looks like this is still happening: https://forums.adafruit.com/viewtopic.php?f=60&t=189442

@tannewt tannewt modified the milestones: Long term, 7.3.0 Mar 23, 2022
@dhalbert
Copy link
Collaborator Author

dhalbert commented Apr 13, 2022

Looking at the output of a NeoPixel on Itsy SAMD51 at 5V D5 pin, with the current code, I see 1.28uS total bit time:
1: 320ns high + 960ns low
0: 160ns high + 1.12us low
I see some 1.12us bit times

@dhalbert dhalbert self-assigned this Apr 13, 2022
@dhalbert
Copy link
Collaborator Author

dhalbert commented Apr 13, 2022

RP2040 Itsy 5V D5 does not have this glitch, and is using PIO for precise timing. RP2040 times in ns:
input zero: 416 high / 832 low
input one: 832 low / 416 high

from the far end Neopixel of an 8-NeoPixel SK6812 string:
output zero: 268 high / 978 low (varies slightly by a few ns)
output one: 698 high / 550 low (varies slightly)

@dhalbert
Copy link
Collaborator Author

Datasheet timings:

// From the SK6812 datasheet:
// T0H 0 code, high level time 0.3µs ±0.15µs
// T1H 1 code, high level time 0.6µs ±0.15µs
// T0L 0 code, low level time 0.9µs ±0.15µs
// T1L 1 code, low level time 0.6µs ±0.15µs
// Trst Reset code,low level time 80µs

// From the WS2812 datasheet:
// T0H 0 code, high voltage time 0.35us ±150ns
// T1H 1 code, high voltage time 0.7us ±150ns
// T0L 0 code, low voltage time 0.8us ±150ns
// T1L 1 code, low voltage time 0.6us ±150ns
// RES low voltage time Above 50µs

// From the WS28212B datasheet:
// T0H 0 code, high voltage time 0.4us ±150ns
// T1H 1 code, high voltage time 0.8us ±150ns
// T0L 0 code, low voltage time 0.85us ±150ns
// T1L 1 code, low voltage time 0.45us ±150ns
// RES low voltage time Above 50µs

Note that T1H especially varies quite a bit, and if we got by the datasheets, we need to choose a happy medium.

@dhalbert
Copy link
Collaborator Author

From @PaintYourDragon's comment here: #6312 (comment).

I collected some data like this in an ad hoc manner while working on #6312, but this is a good approach if/when we need to revisit this.

"""
NEOPIXEL IN-VS-OUT SIGNAL COMPARATOR.
This does NOT generate NeoPixel color data, doesn't have to. Rather, it's
used to observe how a pulse train is reshaped on its way though pixels.
Connect A0 & A1 to potentiometers to adjust pulse train frequency
(600-1000 KHz) and duty cycle (0-100%). Connect D13 to NeoPixel input.
Connect two channels of oscilloscope or logic analyzer to DATA IN before
NeoPixel and DATA OUT after.

This could be super insightful in providing robust, any-make-or-model
timing for WS2812-like pixels. Two key takeaways from this are:
1. Datasheet figures are fantasy.
2. "Low time" is absolutely a red herring and should be ignored;
   ONLY "high time" matters. ONLY. HIGH. TIME. MATTERS.

The pulse-to-pulse period IN and OUT of a pixel will always match...
high time + low time is never reshaped, it CAN'T, as that would cause
overflow or underflow problems as each pixel is on its own clock and there
will be parts tolerances of several percent. You can adjust the frequency
and observe this on the scope: the period changes the same both in and out,
low time is NOT reshaped on output...it fills whatever time necessary
between the "high" state and the pulse interval. It's reasonable to pick a
single frequency for the sake of testing...one of the pots could even be
eliminated...800 KHz is the prescribed rate, and software or peripheral
implementations should aim for that. If an exact match isn't possible,
that's fine, there's ample slop (all else equal, I'd aim for faster rather
than slower, since NeoPixel output ties up the CPU in many implementations).

With frequency fixed, you can then observe what happens with varying the
duty cycle. I haven't tested every WS2812-like variant, but on the original
6-pin WS2812 pixels and RGB SK6812's, it can be seen that there are two
important thresholds for the high time of pulses:
1. Minimum duration for the pulse to register (at all, whether a '0' or '1'
   pulse doesn't matter -- below this, output remains low).
2. Some time threshold that distinguishes a '0' pulse from a '1'.
There does NOT appear to be a third threshold producing an output-remains-
high state. '1' pulses can be quite long if needed.

THESE THRESHOLDS DO NOT VARY WITH FREQUENCY. DUTY CYCLE IS IRRELEVANT.
ONLY PULSE DURATION MATTERS.

As long as a software/peripheral implementation stays well away from
these two thresholds, and sticks close to 800 KHz, it should prove robust,
regardless what numbers are given in various pixels' datasheets.

Here's how I would recommend testing this:

- Several of each make and model should be tested, as there will be design
  tolerances. At least three, maybe up to five seems a good number.
- The tests should be repeated with pixels cooled (perhaps a fan) and warmed
  (perhaps a space heater). Sounds like a joke but isn't: the performance of
  addressable LEDs is known to vary with temperature (incl. PWM rate), did a
  video on this.
- The minimum-registering-pulse and the 0-vs-1 times (in uS) should be noted
  for every single pixel, cool and warm. Yes, would be hundreds of numbers.
- After everything's tallied, take the MAXIMUM of all minimum-registering-
  pulse times, and the MINIMUM of all 0-vs-1 thresholds, and find the
  MIDPOINT between these. This will be the MOST ROBUST ZERO PULSE time
  that software should aim for (if it can't meet it exactly, that's OK,
  closest value will do).
- Then take the MAXIMUM of those 0-vs-1-thresholds and the ideal pixel
  clock interval (1.250 uS) and again take the MIDPOINT. This will be the
  MOST ROBUST ONE PULSE time that software should aim for.
- Averages or medians aren't needed...midpoints between mins/maxes should
  prove most immune to various factors.
- The resulting two numbers ('0' and '1' pulse high times) -- that's what
  all of this distills down to, two numbers -- will almost certainly vary
  from datasheet figures or prior software implementations. That's fine.
  This is empirical data with "best" fudge factors applied, least
  susceptible to false positives/negatives. Should be possible to throw
  ANY make and model of strip, any temperature, drive a truck over it and
  it keeps working.
- Numbers may need revision if new devices come out that take wider
  liberties with timing.
"""

import board
import analogio
import pwmio

freq_pin = analogio.AnalogIn(board.A0)
duty_pin = analogio.AnalogIn(board.A1)
out_pin = pwmio.PWMOut(
    board.D13, frequency=800000, duty_cycle=16000, variable_frequency=True
)

freq_in, duty_in = freq_pin.value, duty_pin.value
freq_prev = duty_prev = -1

# Read both pots w/some filtering. If any change, update PWM out & info shown
while True:
    freq_in = int(freq_in * 0.9 + freq_pin.value * 0.1 + 0.5)
    duty_in = int(duty_in * 0.9 + duty_pin.value * 0.1 + 0.5)
    if freq_in != freq_prev or duty_in != duty_prev:
        hz_out = 600000 + int(400000 * freq_in / 0xFFF0 + 0.5)  # 600-1000 KHz
        out_pin.frequency = hz_out
        out_pin.duty_cycle = int(65535 * duty_in / 0xFFF0 + 0.5)
        period_out = 1000000 / hz_out
        print(
            "%.3f uS high, %.3f uS period (%d Hz)"
            % (period_out * duty_in / 0xFFF0, period_out, hz_out)
        )
        freq_prev, duty_prev = freq_in, duty_in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment