-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Comments
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:
|
Looks like this is still happening: https://forums.adafruit.com/viewtopic.php?f=60&t=189442 |
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: |
RP2040 Itsy 5V D5 does not have this glitch, and is using PIO for precise timing. RP2040 times in ns: from the far end Neopixel of an 8-NeoPixel SK6812 string: |
Datasheet timings:
Note that T1H especially varies quite a bit, and if we got by the datasheets, we need to choose a happy medium. |
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 |
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@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.
The text was updated successfully, but these errors were encountered: