From 9b9a580af8246b31e3f411b0cbfe873a870700c0 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 21 Oct 2024 12:34:41 -0500 Subject: [PATCH 1/2] Add current_control, restore software brightness functionality --- adafruit_tm1814.py | 96 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/adafruit_tm1814.py b/adafruit_tm1814.py index d83eb8a..1d8a7a5 100644 --- a/adafruit_tm1814.py +++ b/adafruit_tm1814.py @@ -60,25 +60,68 @@ """ ) +TM1814_MIN_CURRENT = 6.5 +TM1814_MAX_CURRENT = 38 +TM1814_CURRENT_SCALE = 2 -def _convert_brightness(x): - x = int(x * 63) + 13 - x |= x << 8 - return x | (x << 16) + +def _convert_one_current(value): + if value < TM1814_MIN_CURRENT or value > TM1814_MAX_CURRENT: + raise ValueError("Current control out of range") + return round((value - TM1814_MIN_CURRENT) * TM1814_CURRENT_SCALE) + + +def _current_control_word(arg): + if isinstance(arg, (int, float)): + arg = arg, arg, arg, arg + result = [_convert_one_current(value) for value in arg] + result += [value ^ 0xFF for value in result] + return result class TM1814PixelBackground( # pylint: disable=too-few-public-methods adafruit_pixelbuf.PixelBuf ): - def __init__(self, pin, n, *, brightness=1.0, pixel_order="WRGB"): + """ + A sequence of TM1814 addressable pixels + + Except as noted, provides all the functionality of + `adafruit_pixelbuf.PixelBuf`, particularly + `adafruit_pixelbuf.PixelBuf.fill` and + `adafruit_pixelbuf.PixelBuf.__setitem__`. + + As the strip always auto-written, there is no need to call the `show` method. + + :param ~microcontroller.Pin pin: The pin to output neopixel data on. + :param int n: The number of neopixels in the chain + :param float brightness: Brightness of the pixels between 0.0 and 1.0 where 1.0 is full + brightness. This brightness value is software-multiplied with raw pixel values. + :param float|tuple[float,float,float] current_control: TM1814 current + control register. See documentation of the ``current_control`` property + below. + :param str pixel_order: Set the pixel color channel order. WRGB is set by + default. Only 4-bytes-per-pixel formats are supported. + """ + + def __init__( # noqa: PLR0913 + self, + pin, + n: int, + *, + brightness: float = 1.0, + pixel_order: str = "WRGB", + current_control: float | tuple[float, float, float, float] = 38.0, + ): + if len(pixel_order) != 4: + raise ValueError("Invalid pixel_order") + byte_count = 4 * n bit_count = byte_count * 8 + 64 # count the 64 brightness bits - self._brightness = brightness - raw_brightness = _convert_brightness(brightness) + self._current_control = current_control # backwards, so that dma byteswap corrects it! - header = struct.pack(">LLL", bit_count - 1, raw_brightness, raw_brightness ^ 0xFFFFFFFF) + header = struct.pack(">L8B", bit_count - 1, *_current_control_word(current_control)) trailer = struct.pack(">L", 38400) # Delay is about 3ms self._sm = StateMachine( @@ -94,14 +137,42 @@ def __init__(self, pin, n, *, brightness=1.0, pixel_order="WRGB"): self._buf = None super().__init__( n, - brightness=1.0, + brightness=brightness, byteorder=pixel_order, auto_write=False, header=header, trailer=trailer, ) - self.show() + super().show() + + def show(self) -> None: + """Does nothing, because the strip is always auto-written""" + + @property + def current_control(self) -> float | tuple[float, float, float, float]: + """Access the current control register of the TM1814 + + The TM1814 has a per-channel current control register that is shared across + the entire strip. + + The current regulation range is from 6.5mA to 38mA in 0.5mA increments. + Out of range values will throw ValueError. + + The relationship between human perception & LED current value is highly + nonlinear: The lowest setting may appear only slightly less bright than the + brightest setting, not 6x brighter as you might expect. + + If this property is set to a single number, then the same value is used for + each channel. Otherwise, it must be a tuple of 4 elements where each element + is applied to a different channel. + """ + return self._current_control + + @current_control.setter + def current_control(self, value: float | tuple[float, float, float]) -> None: + struct.pack_into("8B", self._buf, 4, *_current_control_word(value)) + self._current_control = value def deinit(self) -> None: """Deinitialize the object""" @@ -119,11 +190,6 @@ def auto_write(self) -> bool: def auto_write(self, value: bool) -> None: pass - @property - def brightness(self) -> float: - """Returns the strip brightness (read-only)""" - return self._brightness - def _transmit(self, buf: bytes) -> None: self._buf = buf self._sm.background_write(loop=memoryview(buf).cast("L"), swap=True) From f1333209fe2eb44216b30732e52246af6a173fc0 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Mon, 21 Oct 2024 12:46:35 -0500 Subject: [PATCH 2/2] Add support for inverting the polarity of the output --- adafruit_tm1814.py | 52 ++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/adafruit_tm1814.py b/adafruit_tm1814.py index 1d8a7a5..e4cc616 100644 --- a/adafruit_tm1814.py +++ b/adafruit_tm1814.py @@ -34,31 +34,7 @@ # Datasheet high times for a "1" bit are 650 (min) 720 (typ) 1000 (max) ns # # Operating PIO at 14x the bit clock lets us achieve nominal 357ns and 714ns -_program = Program( - f""" -.side_set 1 -.wrap_target - pull block side 1 - out y, 32 side 1 ; get count of pixel bits - -bitloop: - pull ifempty side 1 ; drive low - out x 1 side 1 [4] - jmp !x do_zero side 0 [3] ; drive low and branch depending on bit val - jmp y--, bitloop side 0 [3] ; drive low for a one (long pulse) - jmp end_sequence side 1 ; sequence is over - -do_zero: - jmp y--, bitloop side 1 [3] ; drive high for a zero (short pulse) - -end_sequence: - pull block side 1 ; get fresh delay value - out y, 32 side 1 ; get delay count -wait_reset: - jmp y--, wait_reset side 1 ; wait until delay elapses -.wrap - """ -) +_pio_source = () TM1814_MIN_CURRENT = 6.5 TM1814_MAX_CURRENT = 38 @@ -101,6 +77,7 @@ class TM1814PixelBackground( # pylint: disable=too-few-public-methods below. :param str pixel_order: Set the pixel color channel order. WRGB is set by default. Only 4-bytes-per-pixel formats are supported. + :param bool inverted: True to invert the polarity of the output signal. """ def __init__( # noqa: PLR0913 @@ -111,10 +88,35 @@ def __init__( # noqa: PLR0913 brightness: float = 1.0, pixel_order: str = "WRGB", current_control: float | tuple[float, float, float, float] = 38.0, + inverted: bool = False, ): if len(pixel_order) != 4: raise ValueError("Invalid pixel_order") + _program = Program(f""" + .side_set 1 + .wrap_target + pull block side {not inverted:1d} + out y, 32 side {not inverted:1d} ; get count of pixel bits + + bitloop: + pull ifempty side {not inverted:1d} ; drive low + out x 1 side {not inverted:1d} [4] + jmp !x do_zero side {inverted:1d} [3] ; drive low and branch depending on bit val + jmp y--, bitloop side {inverted:1d} [3] ; drive low for a one (long pulse) + jmp end_sequence side {not inverted:1d} ; sequence is over + + do_zero: + jmp y--, bitloop side {not inverted:1d} [3] ; drive high for a zero (short pulse) + + end_sequence: + pull block side {not inverted:1d} ; get fresh delay value + out y, 32 side {not inverted:1d} ; get delay count + wait_reset: + jmp y--, wait_reset side {not inverted:1d} ; wait until delay elapses + .wrap + """) + byte_count = 4 * n bit_count = byte_count * 8 + 64 # count the 64 brightness bits