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

NeoPixel: Add invertedDIN option to use single transistor driver. #8037

Closed

Conversation

cederom
Copy link
Contributor

@cederom cederom commented Nov 27, 2021

  • Patch allows using single transistor (i.e. IRLML6344) to drive WS2812B.
  • Transistor is required for MCU 3.3V signals to drive 5V WS2812B.
  • Inverted DIN and bistream hi-to-lo only is compensated by timings.
  • No underlying driver code is touched.
  • Please verify on your hardware.

How compensation timings were calculated:

    STREAM: 11001010
       BUS: 110 110 100 100 110 100 110 100
       LET: A B A B CD  CD  A B CD  A B CD
INV STREAM: 00110101
   INV BUS: 001 001 011 011 001 011 001 011
   INV LET: D C D C BA  BA  D C BA  D C BA

Signed-off-by: Tomasz 'CeDeROM' CEDRO tomek@cedro.info

@cederom
Copy link
Contributor Author

cederom commented Nov 27, 2021

Here is an example code (taken from https://github.com/micropython/micropython/blob/53145c4c5f10c3e44ffe174b1e6968792f17ea3a/drivers/neopixel/neopixel.py) that was used to create this patch on ESP32 with generic MicroPython esp32-20210902-v1.17.bin. You can play with LED count, timing and new invertedDIN parameter. Please verify :-)

from micropython import const
from machine import bitstream
import machine
import time

_BITSTREAM_TYPE_HIGH_LOW = const(0)
_TIMING_WS2818_800 = (400, 850, 800, 450)
_TIMING_WS2818_800_INV = (850, 400, 450, 800)
_TIMING_WS2818_400 = (800, 1700, 1600, 900)
_TIMING_WS2818_400_INV = (1700, 800, 900, 1600)


class cdNeoPixel:
    ORDER = (1, 0, 2, 3)

    def __init__(self, pin, n, bpp=3, timing=1, inverted=False):
        self.pin = pin
        self.n = n
        self.bpp = bpp
        self.buf = bytearray(n * bpp)
        self.inverted = inverted
        self.pin.init(pin.OUT)
        if self.inverted:
            self.timing = (
                (_TIMING_WS2818_800_INV if timing else _TIMING_WS2818_400_INV)
                if isinstance(timing, int)
                else timing
            )
        else:            
            self.timing = (
                (_TIMING_WS2818_800 if timing else _TIMING_WS2818_400)
                if isinstance(timing, int)
                else timing
            )

    def __len__(self):
        return self.n

    def __setitem__(self, index, val):
        offset = index * self.bpp
        for i in range(self.bpp):
            self.buf[offset + self.ORDER[i]] = val[i]

    def __getitem__(self, index):
        offset = index * self.bpp
        return tuple(self.buf[offset + self.ORDER[i]] for i in range(self.bpp))

    def fill(self, color):
        for i in range(self.n):
            self[i] = color

    def write(self):
        if self.inverted: pin.value(1)
        bitstream(self.pin, _BITSTREAM_TYPE_HIGH_LOW, self.timing, self.buf)
        if self.inverted: pin.value(1)


pin = machine.Pin(32, machine.Pin.OUT)
led = cdNeoPixel(pin, 3, timing=1, inverted=True)
for i in range(256):
    print(i)
    led.fill((0,255-i,0))
    led.write()
    time.sleep_ms(10)

#     STREAM: 11001010
#        BUS: 110 110 100 100 110 100 110 100
#        LET: A B A B CD  CD  A B CD  A B CD
# INV STREAM: 00110101
#    INV BUS: 001 001 011 011 001 011 001 011
#    INV LET: D C D C BA  BA  D C BA  D C BA

* Patch allows using single transistor (i.e. IRLML6344) to drive WS2812B.
* Transistor is required for MCU 3.3V signals to drive 5V WS2812B.
* Inverted DIN and bistream hi-to-lo only is compensated by timings.
* No underlying driver code is touched.
* Please verify on your hardware.

How compensation timings were calculated:
```
    STREAM: 11001010
       BUS: 110 110 100 100 110 100 110 100
       LET: A B A B CD  CD  A B CD  A B CD
INV STREAM: 00110101
   INV BUS: 001 001 011 011 001 011 001 011
   INV LET: D C D C BA  BA  D C BA  D C BA
```

Signed-off-by: Tomasz 'CeDeROM' CEDRO <tomek@cedro.info>
@jimmo
Copy link
Member

jimmo commented Nov 30, 2021

Thanks @cederom

Generally I always prefer to use buffer (e.g. 74AHCT125 or similar) but I agree supporting simpler level shifting would be good.

My inclination is to either push this into the driver (i.e. add MICROPY_MACHINE_BITSTREAM_TYPE_LOW_HIGH) or possibly to provide a neopixel_inverted.py that inherits from neopixel.py and overrides __init__ and write. That way a user doesn't have to pay the ROM/RAM cost if they're not using this feature.

FWIW, we've tried to push the code and bytecode size of neopixel.py down as much as possible, hence why there are some weird stylistic things.

Here's a sketch of what neopixel_inverted.py could look like...

import neopixel

class NeoPixelInverted(neopixel.NeoPixel):
  def __init__(self, pin, n, bpp=3, timing=1):
    super().__init__(pin, n, bpp, ((850, 400, 450, 800) if timing else (1700, 800, 900, 1600)) if isinstance(timing, int) else timing))

  def write(self):
    self.pin.value(1)
    super().write()
    self.pin.value(1)

We could provide that next to the existing (frozen) neopixel.py and then add a note to the docs to copy over neopixel_inverted.py to the filesystem if necessary.

@cederom
Copy link
Contributor Author

cederom commented Nov 30, 2021

Awsome :-) Thanks for your hints @jimmo :-)

I also considered MICROPY_MACHINE_BITSTREAM_TYPE_LOW_HIGH at first, but then it turned out that each hardware implementation would have to be touched / changed.. that would imply lots of changes in many places and consume potentially unwanted bytes.

Thus comes my experiment with observing the signals after inversion, and it turns out that when these inverted signals are generated by MCU with existing MICROPY_MACHINE_BITSTREAM_TYPE_HIGH_LOW underlying code, then after inversion by N-MOSFET, the WS2812 seems to interpret them correctly even though electrical levels are inverted. So timings seems more important than voltages for WS2812 (I also have WS2813 and will verify in a free moment) :-)

I am working on a board at the moment with complementary N+P MOSFET in TSOT-23 package to see if output of the voltage level translator works correctly between 3.3V->5V without logic inversion. Then I would verify if both software implementations work exactly the same in N+P and single N configuration. I am somehow afraid that one bit may be missing.. but colors and intensity seems to be generated correctly "by naked eye". Another test I can think of is to put one oscilloscope channel before first LED and second channel after first LED then compare. If you have other ideas on how we can verify this please share :-) Even if there is one bit missing (hopefully LSb), it is enough for most applications and benefit of having only single cheap tiny N-MOSFET transistor seems to outweights the potential resolution loss. But we should clearly state that something like this takes place if it is :-)

As you are far more familiar with the MicroPython architecture and if you have time you can freely put necessary changes on top of my commits (then squash everything if necessary) so they have perfectly small footprint and matches internal coding standards :-)

Thank you for MicroPython! I am really impressed how well it works and how much time it can save for rapid prototyping.. where time is most precious. I am working on ESP32 and ESP32-C3 (RISC-V) so they are cheaper and more resourceful than ARM anyway :-)

@jimmo
Copy link
Member

jimmo commented Dec 1, 2021

I am working on a board at the moment with complementary N+P MOSFET in TSOT-23 package

I'd definitely recommend taking a look at a dedicated level shifter part rather than DIY with mosfets. They will typically have schmitt trigger inputs, and are still fairly cheap.

Also like you say, WS2818 are far more sensitive to timing than voltage and in many cases the level shifter is not necessary (3.3V io is just on the borderline. 3V is more likely to lead to issues). The other trick is to have a "sacrificial" first WS2818 which is powered one diode-drop below 5V (i.e. 4.3V), and essentially serves as a level shifter to the rest of the strip. See https://hackaday.com/2017/01/20/cheating-at-5v-ws2812-control-to-use-a-3-3v-data-line/

If you have other ideas on how we can verify this please share

I did all the testing for the current driver with a logic analyser. You could use your scope for the same purpose.

@robert-hh
Copy link
Contributor

robert-hh commented Dec 1, 2021

My preference for this hardware problem is a hardware solution: using a proper level shifter, like a TXB0101 (https://www.ti.com/product/TXB0101). You can of course use a 2 transistor logic as well, if that meets the timing requirements.
Besides that, I connect for tests the WS2818 input directly to the 3.3V output, which works. But that's at the borderline, as @jimmo said. For workbench testing of code that migth be fine, for reliable operation at a broad range of environmental conditions this is subject to trouble.

@cederom
Copy link
Contributor Author

cederom commented Dec 1, 2021

I am working on a board at the moment with complementary N+P MOSFET in TSOT-23 package

I'd definitely recommend taking a look at a dedicated level shifter part rather than DIY with mosfets. They will typically have schmitt trigger inputs, and are still fairly cheap.

I prefer generic parts. In times where supply chains are noticeably disturbed this is important :-)

In my design I was using new component that got marked EOL around May 2021 because factory in Texas and Taiwan was closed.

Also like you say, WS2818 are far more sensitive to timing than voltage and in many cases the level shifter is not necessary (3.3V io is just on the borderline. 3V is more likely to lead to issues). The other trick is to have a "sacrificial" first WS2818 which is powered one diode-drop below 5V (i.e. 4.3V), and essentially serves as a level shifter to the rest of the strip. See https://hackaday.com/2017/01/20/cheating-at-5v-ws2812-control-to-use-a-3-3v-data-line/

No need to "sacrifice" any components. I saw that page thanks :-) Also saw the HCT ICs examples.

My solution works using cheap tiny generic transistor that fully opens around 1V (starting at 0.5V) thus it also works well in battery powered applications when voltage shifts down from stable 3.3V :-)

@cederom
Copy link
Contributor Author

cederom commented Dec 1, 2021

My preference for this hardware problem is a hardware solution: using a proper level shifter, like a TXB0101 (https://www.ti.com/product/TXB0101). You can of course use a 2 transistor logic as well, if that meets the timing requirements. Besides that, I connect for tests the WS2818 input directly to the 3.3V output, which works. But that's at the borderline, as @jimmo said. For workbench testing of code that migth be fine, for reliable operation at a broad range of environmental conditions this is subject to trouble.

Thank you for TXB0101 hint @robert-hh! This is also SOT tiny device.. but its 10x more expensive than N-MOSFET and 5x more expensve than N+P MOSFET.. also this is not a generic-trivial-to-replace-part what is important because availability may be impacted as explained in my previous reply :-)

@jimmo
Copy link
Member

jimmo commented Nov 15, 2022

Closing due to inactivity.

@cederom If you want to go ahead with this, I think adding a separate NeopixelInverted class, as described above is the way to go. Also the neopixel driver has moved to micropython-lib now.

That said, I can't convince myself that just transposing the high/low times actually does the right thing here. I think it works because the neopixels are quite forgiving and mostly it's just the high time that matters, but I don't think if you looked at the signal on the output side of your level shifter n-mos it would not match the expected timing. Would be curious to see if this works for neopixel-like devices with different timing (e.g. SK6812).

@jimmo jimmo closed this Nov 15, 2022
@cederom
Copy link
Contributor Author

cederom commented Nov 15, 2022

Thanks @jimmo for your time! :-)

Actually WS2812-MINI accepts DIN voltage levels from 2.7V so no inverter and/or level shifter is really necessary and I am using native implementation from 1.19 on ESP32 :-)

In a free moment I will dump signals both from DIN and DOUT and put here just out of curiosity.

I do not have SK6812 at hand but when I get one I will also try out that one :-)

This is not a priority anymore and in some cases even not necessary - some WS2812 can be driver straight out of the 3.3V MCU so swap in BOM seems most sensible solution.. world would have been much easier if all of them could work that way out of the box :-)

tannewt pushed a commit to tannewt/circuitpython that referenced this pull request Jun 8, 2023
fix to make buttons and leds compatible with pca10056
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants