diff --git a/.gitignore b/.gitignore index 9647e71..498b941 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,15 @@ *.mpy .idea +*.idea.py __pycache__ _build *.pyc .env +build* bundles *.DS_Store .eggs dist **/*.egg-info +temp/ diff --git a/adafruit_tlc59711.py b/adafruit_tlc59711.py index 5339b86..d99d2ad 100644 --- a/adafruit_tlc59711.py +++ b/adafruit_tlc59711.py @@ -1,15 +1,18 @@ # SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries +# SPDX-FileCopyrightText: 2018 Stefan Krüger s-light.eu # # SPDX-License-Identifier: MIT """ -`adafruit_tlc59711` +`adafruit_tlc59711`. + ==================================================== -CircuitPython module for the TLC59711 16-bit 12 channel LED PWM driver. See -examples/simpletest.py for a demo of the usage. +CircuitPython module for the +TLC59711 or TLC5971 16-bit 12 channel LED PWM driver. +See examples/tlc59711_simpletest.py for a demo of the usage. -* Author(s): Tony DiCola +* Author(s): Tony DiCola, Stefan Kruger Implementation Notes -------------------- @@ -18,139 +21,442 @@ * Adafruit `12-Channel 16-bit PWM LED Driver - SPI Interface - TLC59711 `_ (Product ID: 1455) + or TLC5971 **Software and Dependencies:** -* Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: +* The API is mostly compatible to the DotStar / NeoPixel Libraries + and is therefore also compatible with FancyLED. + +* Adafruit CircuitPython firmware for the ESP8622, M0 or M4-based boards: https://github.com/adafruit/circuitpython/releases """ __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_TLC59711.git" -# Globally disable invalid-name check as this chip by design has short channel -# and register names. It is confusing to rename these from what the datasheet -# refers to them as. +# pylint - globally disable +# 'invalid-name' check to allow for datasheet conform short channel and register names. # pylint: disable=invalid-name +# 'undefined-variable' to allow for micropython const definitions +# pylint: disable=undefined-variable +# 'too-many-lines' with the extra class and the api backwards compatibel things i have ~1100 lines.. +# and as it was wished to not alter the .pylint file to limit to 1100 i disable it here.. +# and yes - this code is very detailed commented. but i think that this is a good thing - +# as hopefully this way it is easier to understand desicions & +# what is going on and learn the backgrounds.. +# pylint: disable=too-many-lines -# Globally disable too many instance attributes check. Again this is a case -# where pylint doesn't have the right context to make this call. The chip by -# design has many channels which must be exposed. -# pylint: disable=too-many-instance-attributes -# Globally disable protected access. Once again pylint can't figure out the -# context for using internal decorate classes below. In these cases protectected -# access is by design for the internal class. -# pylint: disable=protected-access +import struct -# Yet another pylint issue, it fails to recognize a decorator class by -# definition has no public methods. Disable the check. -# pylint: disable=too-few-public-methods +from micropython import const -def _shift_in(target_byte, val): - # Shift in a new bit value to the provided target byte. The byte will be - # shift one position left and a new bit inserted that's a 1 if val is true, - # of a 0 if false. - target_byte <<= 1 - if val: - target_byte |= 0x01 - return target_byte +class TLC59711: + """TLC5971 & TLC59711 16-bit 12 channel LED PWM driver. + The TLC59711 & TLC5971 chip is designed to drive 4 RGB LEDs with 16-bit PWM per Color. + This Library can control 1..many chips. + The class has an interface compatible with the FancyLED library - + and the API is similar to the NeoPixel and DotStar Interfaces. -class TLC59711: - """TLC59711 16-bit 12 channel LED PWM driver. This chip is designed to - drive 4 RGB LEDs with 16-bit PWM control of each LED. The class has an - interface much like that of NeoPixels with attribute access to the 4 - RGB channels (note they are 16-bit values). Or you can access each - independent channel by name (r0, g0, b0, r1, b1, etc.) as properties for - fine-grained control. - - :param ~busio.SPI spi: An instance of the SPI bus connected to the chip. The clock and - MOSI/outout must be set, the MISO/input is unused. - :param bool auto_show: This is a boolean that defaults to True and indicates any - change to a channel value will instantly be written to the chip. You might wish to - set this to false if you desire to perform your own atomic operations of channel - values. In that case call the show function after making updates to channel state. + :param ~busio.SPI spi: An instance of the SPI bus connected to the chip. + The clock and MOSI/outout must be set, the MISO/input is unused. + Maximal data clock frequence is: + - TLC59711: 10MHz + - TLC5971: 20MHz + :param bool pixel_count: Number of RGB-LEDs (=Pixels) that are connected. (default=4) """ - class _GS_Value: - # Internal decorator to simplify exposing each 16-bit LED PWM channel. - # These will get/set the appropriate bytes in the shift register with - # the specified values. + # pylint: disable=too-many-instance-attributes + # it just does make senes to have the chip params as attributes. - def __init__(self, byte_offset): - # Keep track of the byte within the shift register where this - # 16-bit value starts. Luckily these are all aligned on byte - # boundaries. Note the byte order is big endian (MSB first). - self._byte_offset = byte_offset + # """ + # TLC5971 data / register structure + # + # some detailed information on the protocol based on + # http://www.ti.com/lit/ds/symlink/tlc5971.pdf + # 8.5.4 Register and Data Latch Configuration (page 23ff) + # 9.2.2.3 How to Control the TLC5971 (page27) + # + # How to send: + # the first data we send are received by the last device in chain. + # Device Nth (244Bit = 28Byte) + # Write Command (6Bit) + # WRCMD (fixed: 25h) + # Function Control Data (5 x 1Bit = 5Bit) + # OUTTMG 1bit + # GS clock edge select + # 1=rising edge, 0= falling edge + # EXTGCK 1bit + # GS reference clock select + # 1=SCKI clock, 0=internal oscillator + # TMGRST 1bit + # display timing reset mode + # 1=OUT forced of on latchpulse, 0=no forced reset + # DSPRPT 1bit + # display repeat mode + # 1=auto repeate + # 0=Out only turned on after Blank or internal latchpulse + # BLANK 1bit; + # 1=blank (outputs off) + # 0=Out on - controlled by GS-Data + # ic power on sets this to 1 + # BC-Data (3 x 7Bits = 21Bit) + # BCB 7bit; + # BCG 7bit; + # BCR 7bit; + # GS-Data (12 x 16Bits = 192Bit) + # GSB3 16bit; + # GSG3 16bit; + # GSR3 16bit; + # GSB2 16bit; + # GSG2 16bit; + # GSR2 16bit; + # GSB1 16bit; + # GSG1 16bit; + # GSR1 16bit; + # GSB0 16bit; + # GSG0 16bit; + # GSR0 16bit; + # Device Nth-1 (244Bit = 28Byte) + # Device .. + # Device 2 + # Device 1 + # short break of 8x period of clock (666ns .. 2.74ms) + # to generate latchpulse + # + 1.34uS + # than next update. + # """ - def __get__(self, obj, obj_type): - # Grab the 16-bit value at the offset for this channel. - return (obj._shift_reg[self._byte_offset] << 8) | obj._shift_reg[ - self._byte_offset + 1 - ] + ########################################## + # helper + ########################################## - def __set__(self, obj, val): - # Set the 16-bit value at the offset for this channel. - assert 0 <= val <= 65535 - obj._shift_reg[self._byte_offset] = (val >> 8) & 0xFF - obj._shift_reg[self._byte_offset + 1] = val & 0xFF - # Write out the new values if auto_show is enabled. - if obj.auto_show: - obj._write() - - # Define explicit GS channels (each LED PWM channel) for users to control. - # See also the __len__ and iterable dunder methods that provide a - # neopixel-like interface to the GS channel values too. Each has a - # trade-off in usage so users can decide how they choose to use the class - # (must change all 3 values at a time with neopixel-like interface vs. - # direct single channel control with these properties below). - b3 = _GS_Value(4) - g3 = _GS_Value(6) - r3 = _GS_Value(8) - - b2 = _GS_Value(10) - g2 = _GS_Value(12) - r2 = _GS_Value(14) - - b1 = _GS_Value(16) - g1 = _GS_Value(18) - r1 = _GS_Value(20) - - b0 = _GS_Value(22) - g0 = _GS_Value(24) - r0 = _GS_Value(26) - - def __init__(self, spi, *, auto_show=True): + # pylama:ignore=E0602 + # ugly workaround for pylama not knowing of micropython const thing + _CHIP_BUFFER_BYTE_COUNT = const(28) + + COLORS_PER_PIXEL = const(3) + PIXEL_PER_CHIP = const(4) + CHANNEL_PER_CHIP = const(COLORS_PER_PIXEL * PIXEL_PER_CHIP) + + _BUFFER_BYTES_PER_COLOR = const(2) + _BUFFER_BYTES_PER_PIXEL = const(_BUFFER_BYTES_PER_COLOR * COLORS_PER_PIXEL) + + # @staticmethod + # def set_bit_with_mask(v, mask, x): + # """Set bit with help of mask.""" + # # clear + # v &= ~mask + # if x: + # # set + # v |= mask + # return v + # + # @staticmethod + # def set_bit(v, index, x): + # """Set bit - return new value. + # + # Set the index:th bit of v to 1 if x is truthy, + # else to 0, and return the new value. + # https://stackoverflow.com/a/12174051/574981 + # """ + # # Compute mask, an integer with just bit 'index' set. + # mask = 1 << index + # # Clear the bit indicated by the mask (if x is False) + # v &= ~mask + # if x: + # # If x was True, set the bit indicated by the mask. + # v |= mask + # # Return the result, we're done. + # return v + + ########################################## + # class _BC(): + # BC-Data (3 x 7Bits = 21Bit). + # + # BCB 7bit; + # BCG 7bit; + # BCR 7bit; + _BC_CHIP_BUFFER_BIT_OFFSET = const(0) + _BC_BIT_COUNT = const(3 * 7) + # this holds the chip offset and + _BC_FIELDS = { + "BCR": { + "offset": 0, + "length": 7, + "mask": 0b01111111, + }, + "BCG": { + "offset": 7, + "length": 7, + "mask": 0b01111111, + }, + "BCB": { + "offset": 14, + "length": 7, + "mask": 0b01111111, + }, + } + + ########################################## + # class _FC(): + # """ + # Function Control Data (5 x 1Bit = 5Bit). + # + # OUTTMG 1bit + # GS clock edge select + # 1 = rising edge + # 0 = falling edge + # EXTGCK 1bit + # GS reference clock select + # 1 = SCKI clock + # 0 = internal oscillator + # TMGRST 1bit + # display timing reset mode + # 1 = OUT forced of on latchpulse + # 0 = no forced reset + # DSPRPT 1bit + # display repeat mode + # 1 = auto repeate + # 0 = Out only turned on after Blank or internal latchpulse + # BLANK 1bit; + # ic power on sets this to 1 + # 1 = blank (outputs off) + # 0 = Out on - controlled by GS-Data + # """ + + _FC_CHIP_BUFFER_BIT_OFFSET = const(_BC_BIT_COUNT) + _FC_BIT_COUNT = const(5) + _FC_FIELDS = { + "BLANK": { + "offset": 0, + "length": 1, + "mask": 0b1, + }, + "DSPRPT": { + "offset": 1, + "length": 1, + "mask": 0b1, + }, + "TMGRST": { + "offset": 2, + "length": 1, + "mask": 0b1, + }, + "EXTGCK": { + "offset": 3, + "length": 1, + "mask": 0b1, + }, + "OUTTMG": { + "offset": 4, + "length": 1, + "mask": 0b1, + }, + } + + ########################################## + # class _WRITE_COMMAND(): + # """WRITE_COMMAND.""" + + _WC_CHIP_BUFFER_BIT_OFFSET = const(_FC_BIT_COUNT + _BC_BIT_COUNT) + _WC_BIT_COUNT = const(6) + _WC_FIELDS = { + "WRITE_COMMAND": { + "offset": 0, + "length": 6, + "mask": 0b111111, + }, + } + WRITE_COMMAND = const(0b100101) + ########################################## + + ######## + _CHIP_BUFFER_HEADER_BIT_COUNT = const(_WC_BIT_COUNT + _FC_BIT_COUNT + _BC_BIT_COUNT) + _CHIP_BUFFER_HEADER_BYTE_COUNT = const(_CHIP_BUFFER_HEADER_BIT_COUNT // 8) + + ########################################## + + def __init__(self, spi, *, pixel_count=4): + """Init.""" self._spi = spi - # This device is just a big 28 byte long shift register without any - # fancy update protocol. Blast out all the bits to update, that's it! - self._shift_reg = bytearray(28) - # Keep track of automatically writing out the state of the PWM channels - # on any change (auto_show = True). If set to false the user must - # explicitly call the show method to write out the PWM state to the - # chip--this is useful for performing atomic updates to LEDs (i.e. - # changing all the R, G, B channels at once). - self.auto_show = auto_show - # Initialize the brightness channel values to max (these are 7-bit - # values). - self._bcr = 127 - self._bcg = 127 - self._bcb = 127 - # Initialize external user-facing state for the function control - # bits of the chip. These aren't commonly used but available and - # match the nomenclature from the datasheet. Note they won't honor - # the auto_show property and instead you must manually call show - # after changing them (reduces the need to make frivolous - # memory-hogging properties). - # Set OUTTMG, TMGRST, and DSPRPT to on like the Arduino library. + self.pixel_count = pixel_count + self.channel_count = self.pixel_count * self.COLORS_PER_PIXEL + # calculate how many chips are connected + self.chip_count = self.pixel_count // 4 + + # The chips are just a big 28 byte long shift register without any fancy update protocol. + # Blast out all the bits to update, that's it! → create raw output data + self._buffer = bytearray(_CHIP_BUFFER_BYTE_COUNT * self.chip_count) + + # Initialize the brightness channel values to max (these are 7-bit values). + self.bcr = 127 + self.bcg = 127 + self.bcb = 127 + + # Initialize external user-facing state for the function control bits of the chip. + # These aren't commonly used but available and match the nomenclature from the datasheet. + # you must manually call update_fc() after changing them + # (reduces the need to make frivolous memory-hogging properties). + # Default set: OUTTMG, TMGRST, and DSPRPT to on; like in Arduino library. + # these are set for all chips the same self.outtmg = True - self.extgclk = False + self.extgck = False self.tmgrst = True self.dsprpt = True self.blank = False + # preparation done → now initialize buffer to default values + self._init_buffer() + self._buffer_index_lookuptable = [] + self._init_lookuptable() + + def _init_buffer(self): + for chip_index in range(self.chip_count): + # set Write Command (6Bit) WRCMD (fixed: 25h) + self.chip_set_BCData(chip_index, bcr=self.bcr, bcg=self.bcg, bcb=self.bcb) + self._chip_set_FunctionControl(chip_index) + self._chip_set_WriteCommand(chip_index) + + def set_chipheader_bits_in_buffer( + self, *, chip_index=0, part_bit_offset=0, field=None, value=0 + ): + """Set chip header bits in buffer.""" + if field is None: + field = {"mask": 0, "length": 0, "offset": 0} + offset = part_bit_offset + field["offset"] + # restrict value + value &= field["mask"] + # move value to position + value = value << offset + # calculate header start + header_start = chip_index * _CHIP_BUFFER_BYTE_COUNT + # get chip header + header = struct.unpack_from(">I", self._buffer, header_start)[0] + # 0xFFFFFFFF == 0b11111111111111111111111111111111 + # create/move mask + mask = field["mask"] << offset + # clear + header &= ~mask + # set + header |= value + # write header back + struct.pack_into(">I", self._buffer, header_start, header) + + ########################################## + + def chip_set_BCData(self, chip_index, bcr=127, bcg=127, bcb=127): + """ + Set BC-Data. + + :param int chip_index: Index of Chip to set. + :param int bcr: 7-bit value from 0-127 (default=127) + :param int bcg: 7-bit value from 0-127 (default=127) + :param int bcb: 7-bit value from 0-127 (default=127) + """ + # set all bits + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_BC_CHIP_BUFFER_BIT_OFFSET, + field=self._BC_FIELDS["BCR"], + value=bcr, + ) + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_BC_CHIP_BUFFER_BIT_OFFSET, + field=self._BC_FIELDS["BCG"], + value=bcg, + ) + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_BC_CHIP_BUFFER_BIT_OFFSET, + field=self._BC_FIELDS["BCB"], + value=bcb, + ) + + def update_BCData(self): + """ + Update BC-Data for all Chips in Buffer. + + need to be called after you changed on of the BC-Data Parameters. (bcr, bcg, bcb) + """ + for chip_index in range(self.chip_count): + self.chip_set_BCData(chip_index, bcr=self.bcr, bcg=self.bcg, bcb=self.bcb) + + def _chip_set_FunctionControl(self, chip_index): + """ + Set Function Control Bits in Buffer. + + values from object global parameters are used. + + :param int chip_index: Index of Chip to set. + """ + # set all bits + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_FC_CHIP_BUFFER_BIT_OFFSET, + field=self._FC_FIELDS["OUTTMG"], + value=self.outtmg, + ) + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_FC_CHIP_BUFFER_BIT_OFFSET, + field=self._FC_FIELDS["EXTGCK"], + value=self.extgck, + ) + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_FC_CHIP_BUFFER_BIT_OFFSET, + field=self._FC_FIELDS["TMGRST"], + value=self.tmgrst, + ) + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_FC_CHIP_BUFFER_BIT_OFFSET, + field=self._FC_FIELDS["DSPRPT"], + value=self.dsprpt, + ) + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_FC_CHIP_BUFFER_BIT_OFFSET, + field=self._FC_FIELDS["BLANK"], + value=self.blank, + ) + + def update_fc(self): + """ + Update Function Control Bits for all Chips in Buffer. + + need to be called after you changed on of the Function Control Bit Parameters. + (outtmg, extgck, tmgrst, dsprpt, blank) + """ + for chip_index in range(self.chip_count): + self._chip_set_FunctionControl(chip_index) + + def _chip_set_WriteCommand(self, chip_index): + """Set WRITE_COMMAND.""" + # set all bits + self.set_chipheader_bits_in_buffer( + chip_index=chip_index, + part_bit_offset=_WC_CHIP_BUFFER_BIT_OFFSET, + field=self._WC_FIELDS["WRITE_COMMAND"], + value=self.WRITE_COMMAND, + ) + + def _init_lookuptable(self): + for channel_index in range(self.channel_count): + buffer_index = (_CHIP_BUFFER_BYTE_COUNT // _BUFFER_BYTES_PER_COLOR) * ( + channel_index // self.CHANNEL_PER_CHIP + ) + channel_index % self.CHANNEL_PER_CHIP + buffer_index *= _BUFFER_BYTES_PER_COLOR + buffer_index += _CHIP_BUFFER_HEADER_BYTE_COUNT + self._buffer_index_lookuptable.append(buffer_index) + + ########################################## + def _write(self): # Write out the current state to the shift register. try: @@ -158,140 +464,613 @@ def _write(self): while not self._spi.try_lock(): pass self._spi.configure(baudrate=self._spi.frequency, polarity=0, phase=0) - # Update the preamble of chip state in the first 4 bytes (32-bits) - # with the write command, function control bits, and brightness - # control register values. - self._shift_reg[0] = 0x25 # 0x25 in top 6 bits initiates write. - # Lower two bits control OUTTMG and EXTGCLK bits, set them - # as appropriate. - self._shift_reg[0] = _shift_in(self._shift_reg[0], self.outtmg) - self._shift_reg[0] = _shift_in(self._shift_reg[0], self.extgclk) - # Next byte contains remaining function control state and start of - # brightness control bits. - self._shift_reg[1] = 0x00 - self._shift_reg[1] = _shift_in(self._shift_reg[1], self.tmgrst) - self._shift_reg[1] = _shift_in(self._shift_reg[1], self.dsprpt) - self._shift_reg[1] = _shift_in(self._shift_reg[1], self.blank) - # Top 5 bits from BC blue channel. - self._shift_reg[1] <<= 5 - self._shift_reg[1] |= (self._bcb >> 2) & 0b11111 - # Next byte contains lower 2 bits from BC blue channel and upper 6 - # from BC green channel. - self._shift_reg[2] = (self._bcb) & 0b11 - self._shift_reg[2] <<= 6 - self._shift_reg[2] |= (self._bcg >> 1) & 0b111111 - # Final byte contains lower 1 bit from BC green and 7 bits from BC - # red channel. - self._shift_reg[3] = self._bcg & 0b1 - self._shift_reg[3] <<= 7 - self._shift_reg[3] |= self._bcr & 0b1111111 - # The remaining bytes in the shift register are the channel PWM - # values that have already been set by the user. Now write out the - # the entire set of bytes. Note there is no latch or other - # explicit line to tell the chip when finished, it expects 28 bytes. - self._spi.write(self._shift_reg) + self._spi.write(self._buffer) finally: # Ensure the SPI bus is unlocked. self._spi.unlock() def show(self): - """Write out the current LED PWM state to the chip. This is only necessary if - auto_show was set to false in the initializer. - """ + """Write out the current LED PWM state to the chip.""" self._write() - # Define properties for global brightness control channels. - @property - def red_brightness(self): - """The red brightness for all channels (i.e. R0, R1, R2, and R3). This is a 7-bit - value from 0-127. - """ - return self._bcr - - @red_brightness.setter - def red_brightness(self, val): - assert 0 <= val <= 127 - self._bcr = val - if self.auto_show: - self._write() - - @property - def green_brightness(self): - """The green brightness for all channels (i.e. G0, G1, G2, and G3). This is a - 7-bit value from 0-127. - """ - return self._bcg - - @green_brightness.setter - def green_brightness(self, val): - assert 0 <= val <= 127 - self._bcg = val - if self.auto_show: - self._write() - - @property - def blue_brightness(self): - """The blue brightness for all channels (i.e. B0, B1, B2, and B3). This is a 7-bit - value from 0-127. - """ - return self._bcb - - @blue_brightness.setter - def blue_brightness(self, val): - assert 0 <= val <= 127 - self._bcb = val - if self.auto_show: - self._write() - - # Define index and length properties to set and get each channel as + ########################################## + + @staticmethod + def calculate_Ioclmax(*, Riref=2.48): + """ + Calculate Maximum Constant Sink Current Value. + + see: + 8.4.1 Maximum Constant Sink Current Setting + http://www.ti.com/lit/ds/symlink/tlc5971.pdf#page=18&zoom=160,0,524 + + Riref = (Viref / Ioclmax) * 41 + Ioclmax = (41 / Riref) * Viref + + :param float Riref: resistor value (kΩ) (default=20) + :return tuple: Ioclmax (mA) + """ + # Riref = (Viref / Ioclmax) * 41 | / 41 + # Riref / 41 = Viref / Ioclmax | switch + # 41 / Riref = Ioclmax / Viref | * Viref + # (41 / Riref) * Viref = Ioclmax + if not 0.8 <= Riref <= 24.8: + raise ValueError("Riref {} not in range: 0.8kΩ..25kΩ".format(Riref)) + Viref = 1.21 + Ioclmax = (41 / Riref) * Viref + if not 2.0 <= Ioclmax <= 60.0: + raise ValueError("Ioclmax {} not in range: 2mA..60mA".format(Ioclmax)) + return Ioclmax + + @staticmethod + def calculate_Riref(*, Ioclmax=20): + """ + Calculate Maximum Constant Sink Current Value. + + see: + 8.4.1 Maximum Constant Sink Current Setting + http://www.ti.com/lit/ds/symlink/tlc5971.pdf#page=19&zoom=200,0,697 + + Riref = (Viref / Ioclmax) * 41 + + :param float Ioclmax: target max output current (mA) (default=20) + :return tuple: Riref (kΩ) + """ + if not 2.0 <= Ioclmax <= 60.0: + raise ValueError("Ioclmax {} not in range: 2mA..60mA".format(Ioclmax)) + Viref = 1.21 + Riref = (Viref / Ioclmax) * 41 + if not 0.8 <= Riref <= 24.8: + raise ValueError("Riref {} not in range: 0.8kΩ..25kΩ".format(Riref)) + return Riref + + @staticmethod + def calculate_BCData(*, Ioclmax=18, IoutR=17, IoutG=15, IoutB=9): + """ + Calculate Global Brightness Control Values. + + see: + 8.5.1 Global Brightness Control (BC) Function (Sink Current Control) + http://www.ti.com/lit/ds/symlink/tlc5971.pdf#page=19&zoom=200,0,697 + + Iout = Ioclmax * (BCX / 127) + BCX = Iout / Ioclmax * 127 + + :param float Ioclmax: max output current set by Riref (mA) (default=20) + :param float IoutR: max output current for red color group (mA) + (default=9) + :param float IoutG: max output current for green color (mA) + (default=15) + :param float IoutB: max output current for blue color (mA) + (default=17) + :return tuple: (bcr, bcg, bcb) + """ + # Iout = Ioclmax * (BCX / 127) | / Ioclmax + # Iout / Ioclmax = BCX / 127 | * 127 + # Iout / Ioclmax * 127 = BCX + if not 2.0 <= Ioclmax <= 60.0: + raise ValueError("Ioclmax {} not in range: 2mA..60mA" "".format(Ioclmax)) + if not 0.0 <= IoutR <= Ioclmax: + raise ValueError("IoutR {} not in range: 2mA..{}mA".format(IoutR, Ioclmax)) + if not 0.0 <= IoutG <= Ioclmax: + raise ValueError("IoutG {} not in range: 2mA..{}mA".format(IoutG, Ioclmax)) + if not 0.0 <= IoutB <= Ioclmax: + raise ValueError("IoutB {} not in range: 2mA..{}mA".format(IoutB, Ioclmax)) + bcr = int((IoutR / Ioclmax) * 127) + bcg = int((IoutG / Ioclmax) * 127) + bcb = int((IoutB / Ioclmax) * 127) + if not 0 <= bcr <= 127: + raise ValueError("bcr {} not in range: 0..127".format(bcr)) + if not 0 <= bcg <= 127: + raise ValueError("bcg {} not in range: 0..127".format(bcg)) + if not 0 <= bcb <= 127: + raise ValueError("bcb {} not in range: 0..127".format(bcb)) + return (bcr, bcg, bcb) + + ########################################## + + @staticmethod + def _convert_01_float_to_16bit_integer(value): + """Convert 0..1 Float Value to 16bit (0..65535) Range.""" + # check if value is in range + if not 0.0 <= value[0] <= 1.0: + raise ValueError("value[0] {} not in range: 0..1".format(value[0])) + # convert to 16bit value + return int(value * 65535) + + @classmethod + def _convert_if_float(cls, value): + """Convert if value is Float.""" + if isinstance(value, float): + value = cls._convert_01_float_to_16bit_integer(value) + return value + + @staticmethod + def _check_and_convert(value): + # loop + # mega slow + # for i, val in enumerate(value): + # # check if we have float values + # if isinstance(val, float): + # # check if val is in range + # if not 0.0 <= val <= 1.0: + # raise ValueError( + # "value[{}] {} not in range: 0..1".format(i, val)) + # # convert to 16bit val + # value[i] = int(val * 65535) + # else: + # if not 0 <= val <= 65535: + # raise ValueError( + # "value[{}] {} not in range: 0..65535".format(i, val)) + # discreet is way faster + # (compared with tlc59711_multi_dev.py: pixels.set_all_black()) + # check if we have float values + if isinstance(value[0], float): + # check if value is in range + if not 0.0 <= value[0] <= 1.0: + raise ValueError("value[0] {} not in range: 0..1".format(value[0])) + # convert to 16bit value + value[0] = int(value[0] * 65535) + else: + if not 0 <= value[0] <= 65535: + raise ValueError("value[0] {} not in range: 0..65535".format(value[0])) + if isinstance(value[1], float): + if not 0.0 <= value[1] <= 1.0: + raise ValueError("value[1] {} not in range: 0..1".format(value[1])) + value[1] = int(value[1] * 65535) + else: + if not 0 <= value[1] <= 65535: + raise ValueError("value[1] {} not in range: 0..65535".format(value[1])) + if isinstance(value[2], float): + if not 0.0 <= value[2] <= 1.0: + raise ValueError("value[2] {} not in range: 0..1".format(value[2])) + value[2] = int(value[2] * 65535) + else: + if not 0 <= value[2] <= 65535: + raise ValueError("value[2] {} not in range: 0..65535".format(value[2])) + + ########################################## + + def _get_channel_16bit_value(self, channel_index): + buffer_start = self._buffer_index_lookuptable[channel_index] + return struct.unpack_from(">H", self._buffer, buffer_start)[0] + + def set_pixel_16bit_value(self, pixel_index, value_r, value_g, value_b): + """ + Set the value for pixel. + + This is a Fast UNPROTECTED function: + no error / range checking is done. + + :param int pixel_index: 0..(pixel_count) + :param int value_r: 0..65535 + :param int value_g: 0..65535 + :param int value_b: 0..65535 + """ + # optimized for speed. + # the struct version leads to very slow runtime behaivor if you set + # lots of pixels. that is the reason the discreet version is used. + # you can check with the tlc59711_multi_dev.py file. + # most prominent this is visible at the set_pixel_all_16bit_value func: + # struct 157ms to 16ms (@144pixel on ItsyBitsy M4) + pixel_start = pixel_index * self.COLORS_PER_PIXEL + buffer_start = self._buffer_index_lookuptable[pixel_start + 0] + # struct.pack_into('>H', self._buffer, buffer_start, value_b) + self._buffer[buffer_start + 0] = (value_b >> 8) & 0xFF + self._buffer[buffer_start + 1] = value_b & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 1] + # struct.pack_into('>H', self._buffer, buffer_start, value_g) + self._buffer[buffer_start + 0] = (value_g >> 8) & 0xFF + self._buffer[buffer_start + 1] = value_g & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 2] + # struct.pack_into('>H', self._buffer, buffer_start, value_r) + self._buffer[buffer_start + 0] = (value_r >> 8) & 0xFF + self._buffer[buffer_start + 1] = value_r & 0xFF + + def set_pixel_float_value(self, pixel_index, value_r, value_g, value_b): + """ + Set the value for pixel. + + This is a Fast UNPROTECTED function: + no error / range checking is done. + + :param int pixel_index: 0..(pixel_count) + :param int value_r: 0..1 + :param int value_g: 0..1 + :param int value_b: 0..1 + """ + # self.set_pixel_16bit_value( + # pixel_index, + # int(value_r * 65535), + # int(value_g * 65535), + # int(value_b * 65535) + # ) + # this is again a speed optimized version: + # this cuts down from 228ms to 22ms (@144pixel on ItsyBitsy M4) + value_r = int(value_r * 65535) + value_g = int(value_g * 65535) + value_b = int(value_b * 65535) + pixel_start = pixel_index * self.COLORS_PER_PIXEL + buffer_start = self._buffer_index_lookuptable[pixel_start + 0] + self._buffer[buffer_start + 0] = (value_b >> 8) & 0xFF + self._buffer[buffer_start + 1] = value_b & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 1] + self._buffer[buffer_start + 0] = (value_g >> 8) & 0xFF + self._buffer[buffer_start + 1] = value_g & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 2] + self._buffer[buffer_start + 0] = (value_r >> 8) & 0xFF + self._buffer[buffer_start + 1] = value_r & 0xFF + + def set_pixel_16bit_color(self, pixel_index, color): + """ + Set color for pixel. + + This is a Fast UNPROTECTED function: + no error / range checking is done. + its a little bit slower as `set_pixel_16bit_value` + + :param int pixel_index: 0..(pixel_count) + :param int color: 3-tuple of R, G, B; 0..65535 + """ + # self.set_pixel_16bit_value( + # pixel_index, + # color[0], + # color[1], + # color[2] + # ) + # speed optimization: 140ms to 24ms (@144pixel on ItsyBitsy M4) + # the `color = list(color)` is the key here! (don't ask me why..) + color = list(color) + pixel_start = pixel_index * self.COLORS_PER_PIXEL + buffer_start = self._buffer_index_lookuptable[pixel_start + 0] + self._buffer[buffer_start + 0] = (color[2] >> 8) & 0xFF + self._buffer[buffer_start + 1] = color[2] & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 1] + self._buffer[buffer_start + 0] = (color[1] >> 8) & 0xFF + self._buffer[buffer_start + 1] = color[1] & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 2] + self._buffer[buffer_start + 0] = (color[0] >> 8) & 0xFF + self._buffer[buffer_start + 1] = color[0] & 0xFF + + def set_pixel_float_color(self, pixel_index, color): + """ + Set color for pixel. + + This is a Fast UNPROTECTED function: + no error / range checking is done. + its a little bit slower as `set_pixel_16bit_value` + + :param int pixel_index: 0..(pixel_count) + :param tuple/float color: 3-tuple of R, G, B; 0..1 + """ + # self.set_pixel_16bit_value( + # pixel_index, + # int(color[0] * 65535), + # int(color[1] * 65535), + # int(color[2] * 65535) + # ) + # speed optimization: 140ms to 30ms (@144pixel on ItsyBitsy M4) + color = list(color) + color[0] = int(color[0] * 65535) + color[1] = int(color[1] * 65535) + color[2] = int(color[2] * 65535) + pixel_start = pixel_index * self.COLORS_PER_PIXEL + buffer_start = self._buffer_index_lookuptable[pixel_start + 0] + self._buffer[buffer_start + 0] = (color[2] >> 8) & 0xFF + self._buffer[buffer_start + 1] = color[2] & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 1] + self._buffer[buffer_start + 0] = (color[1] >> 8) & 0xFF + self._buffer[buffer_start + 1] = color[1] & 0xFF + buffer_start = self._buffer_index_lookuptable[pixel_start + 2] + self._buffer[buffer_start + 0] = (color[0] >> 8) & 0xFF + self._buffer[buffer_start + 1] = color[0] & 0xFF + + def set_pixel(self, pixel_index, value): + """ + Set the R, G, B values for the pixel. + + this funciton hase some advanced error checking. + it is much slower than the other provided 'bare' variants.. + but therefor gives clues to what is going wrong.. ;-) + + :param int pixel_index: 0..(pixel_count) + :param tuple value: 3-tuple of R, G, B; + each int 0..65535 or float 0..1 + """ + if 0 <= pixel_index < self.pixel_count: + # convert to list + value = list(value) + # repr(value) + if len(value) != self.COLORS_PER_PIXEL: + raise IndexError( + "length of value {} does not match COLORS_PER_PIXEL (= {})" + "".format(len(value), self.COLORS_PER_PIXEL) + ) + # tested: + # splitting up into variables to not need the list.. + # this is about 0.25ms slower.. + # value_r = value[0] + # value_g = value[1] + # value_b = value[2] + + # check if we have float values + # this modifies 'value' in place.. + self._check_and_convert(value) + + # update buffer + # we change channel order here: + # buffer channel order is blue, green, red + # pixel_start = pixel_index * self.COLORS_PER_PIXEL + # buffer_start = self._buffer_index_lookuptable[pixel_start + 0] + # self._buffer[buffer_start + 0] = (value[2] >> 8) & 0xFF + # self._buffer[buffer_start + 1] = value[2] & 0xFF + # buffer_start = self._buffer_index_lookuptable[pixel_start + 1] + # self._buffer[buffer_start + 0] = (value[1] >> 8) & 0xFF + # self._buffer[buffer_start + 1] = value[1] & 0xFF + # buffer_start = self._buffer_index_lookuptable[pixel_start + 2] + # self._buffer[buffer_start + 0] = (value[0] >> 8) & 0xFF + # self._buffer[buffer_start + 1] = value[0] & 0xFF + # simpliefy code + self.set_pixel_16bit_value(pixel_index, value[0], value[1], value[2]) + else: + raise IndexError( + "index {} out of range [0..{}]" "".format(pixel_index, self.pixel_count) + ) + + def set_pixel_all_16bit_value(self, value_r, value_g, value_b): + """ + Set the R, G, B values for all pixels. + + fast. without error checking. + + :param int value_r: 0..65535 + :param int value_g: 0..65535 + :param int value_b: 0..65535 + """ + for i in range(self.pixel_count): + self.set_pixel_16bit_value(i, value_r, value_g, value_b) + + def set_pixel_all(self, color): + """ + Set the R, G, B values for all pixels. + + :param tuple 3-tuple of R, G, B; each int 0..65535 or float 0..1 + """ + for i in range(self.pixel_count): + self.set_pixel(i, color) + + def set_all_black(self): + """Set all pixels to black.""" + for i in range(self.pixel_count): + self.set_pixel_16bit_value(i, 0, 0, 0) + + # channel access + def set_channel(self, channel_index, value): + """ + Set the value for the provided channel. + + :param int channel_index: 0..channel_count + :param int value: 0..65535 + """ + if 0 <= channel_index < (self.channel_count): + # check if values are in range + if not 0 <= value <= 65535: + raise ValueError("value {} not in range: 0..65535") + # temp = channel_index + # we change channel order here: + # buffer channel order is blue, green, red + pixel_index_offset = channel_index % self.COLORS_PER_PIXEL + if pixel_index_offset == 0: + channel_index += 2 + elif pixel_index_offset == 2: + channel_index -= 2 + # set value in buffer + buffer_start = self._buffer_index_lookuptable[channel_index] + struct.pack_into(">H", self._buffer, buffer_start, value) + else: + raise IndexError( + "channel_index {} out of range (0..{})".format( + channel_index, self.channel_count + ) + ) + + # Define index and length properties to set and get each pixel as # atomic RGB tuples. This provides a similar feel as using neopixels. def __len__(self): - """Retrieve the total number of LED channels available.""" - return 4 # Always 4 RGB channels on the chip. + """Retrieve TLC5975 the total number of Pixels available.""" + return self.pixel_count def __getitem__(self, key): - # pylint: disable=no-else-return - # Disable should be removed when refactor can be tested - """Retrieve the R, G, B values for the provided channel as a - 3-tuple. Each value is a 16-bit number from 0-65535. - """ - if key == 0: - return (self.r0, self.g0, self.b0) - elif key == 1: - return (self.r1, self.g1, self.b1) - elif key == 2: - return (self.r2, self.g2, self.b2) - elif key == 3: - return (self.r3, self.g3, self.b3) + """ + Retrieve the R, G, B values for the provided channel as a 3-tuple. + + Each value is a 16-bit number from 0-65535. + """ + if 0 <= key < self.pixel_count: + pixel_start = key * self.COLORS_PER_PIXEL + return ( + self._get_channel_16bit_value(pixel_start + 0), + self._get_channel_16bit_value(pixel_start + 1), + self._get_channel_16bit_value(pixel_start + 2), + ) + # else: + raise IndexError("index {} out of range [0..{}]".format(key, self.pixel_count)) + + def __setitem__(self, key, value): + """ + Set the R, G, B values for the pixel. + + this funciton hase some advanced error checking. + it is much slower than the other provided 'bare' variants.. + but therefor gives clues to what is going wrong.. ;-) + this shortcut is identicall to `set_pixel` + + :param int key: 0..(pixel_count) + :param tuple 3-tuple of R, G, B; each int 0..65535 or float 0..1 + """ + # for a more detailed version with all the debugging code and + # comments look at set_pixel + if 0 <= key < self.pixel_count: + value = list(value) + if len(value) != self.COLORS_PER_PIXEL: + raise IndexError( + "length of value {} does not match COLORS_PER_PIXEL (= {})" + "".format(len(value), self.COLORS_PER_PIXEL) + ) + # _check_and_convert modifies value in place.. + self._check_and_convert(value) + self.set_pixel_16bit_value(key, value[0], value[1], value[2]) else: - raise IndexError - - def __setitem__(self, key, val): - """Set the R, G, B values for the provided channel. Specify a - 3-tuple of R, G, B values that are each 16-bit numbers (0-65535). - """ - assert 0 <= key <= 3 # Do this check here instead of later to - # prevent accidentally keeping auto_show - # turned off when a bad key is provided. - assert len(val) == 3 - assert 0 <= val[0] <= 65535 - assert 0 <= val[1] <= 65535 - assert 0 <= val[2] <= 65535 - # Temporarily halt auto write to perform an atomic update of all - # the channel values. - old_auto_show = self.auto_show - self.auto_show = False - # Update appropriate channel values. - if key == 0: - self.r0, self.g0, self.b0 = val - elif key == 1: - self.r1, self.g1, self.b1 = val - elif key == 2: - self.r2, self.g2, self.b2 = val - elif key == 3: - self.r3, self.g3, self.b3 = val - # Restore auto_show state. - self.auto_show = old_auto_show - # Write out new values if in auto_show state. - if self.auto_show: - self._write() + raise IndexError( + "index {} out of range [0..{}]" "".format(key, self.pixel_count) + ) + + class _ChannelDirekt: + # Internal decorator to simplify mapping. + + def __init__(self, channel): + self._channel = channel + + def __get__(self, obj, obj_type): + # Grab the 16-bit value at the offset for this channel. + return obj._get_channel_16bit_value(self._channel) + + def __set__(self, obj, value): + # Set the 16-bit value at the offset for this channel. + assert 0 <= value <= 65535 + obj.set_channel(self._channel, value) + # if obj.auto_show: + # obj._write() + + # Define explicit channels for first IC. + # → this is only for api backwards compliance. + # not recommended for new use - use *set_channel* + b0 = _ChannelDirekt(0) + g0 = _ChannelDirekt(1) + r0 = _ChannelDirekt(2) + b1 = _ChannelDirekt(3) + g1 = _ChannelDirekt(4) + r1 = _ChannelDirekt(5) + b2 = _ChannelDirekt(6) + g2 = _ChannelDirekt(7) + r2 = _ChannelDirekt(8) + b3 = _ChannelDirekt(9) + g3 = _ChannelDirekt(10) + r3 = _ChannelDirekt(11) + + +########################################## + + +class TLC59711AutoShow(TLC59711): + """TLC59711 16-bit 12 channel LED PWM driver with Auto-Show. + + This chip is designed to drive 4 RGB LEDs with 16-bit PWM per Color. + The class has an interface compatible with the FancyLED library. + and with this is similar to the NeoPixel and DotStar Interfaces. + + this TLC59711AutoShow is a subclass of TLC59711 that adds automatically + sending changed data to the chips. + this creates very slows responses on big changes. + It is mainly usefull if you only have a very small number of pixels. + + :param ~busio.SPI spi: An instance of the SPI bus connected to the chip. + The clock and MOSI/outout must be set, the MISO/input is unused. + Maximal data clock frequence is: + - TLC59711: 10MHz + - TLC5971: 20MHz + :param bool pixel_count: Number of RGB-LEDs (=Pixels) that are connected. + """ + + def __init__(self, spi, pixel_count=4): + """Init.""" + super().__init__(spi, pixel_count=pixel_count) + + ########################################## + + def set_pixel(self, pixel_index, value): + """ + Set the R, G, B values for the pixel. + + this funciton hase some advanced error checking. + it is much slower than the other provided 'bare' variants.. + but therefor gives clues to what is going wrong.. ;-) + + :param int pixel_index: 0..(pixel_count) + :param tuple value: 3-tuple of R, G, B; + each int 0..65535 or float 0..1 + """ + super().set_pixel(pixel_index, value) + self._write() + + def set_pixel_all(self, color): + """ + Set the R, G, B values for all pixels. + + :param tuple 3-tuple of R, G, B; each int 0..65535 or float 0..1 + """ + super().set_pixel_all(color) + self._write() + + def set_all_black(self): + """Set all pixels to black.""" + super().set_all_black() + self._write() + + # channel access + def set_channel(self, channel_index, value): + """ + Set the value for the provided channel. + + :param int channel_index: 0..channel_count + :param int value: 0..65535 + """ + super().set_channel(channel_index, value) + self._write() + + def __setitem__(self, key, value): + """ + Set the R, G, B values for the pixel. + + this funciton hase some advanced error checking. + it is much slower than the other provided 'bare' variants.. + but therefor gives clues to what is going wrong.. ;-) + this shortcut is identicall to `set_pixel` + + :param int key: 0..(pixel_count) + :param tuple 3-tuple of R, G, B; each int 0..65535 or float 0..1 + """ + super().__setitem__(key, value) + self._write() + + class _ChannelDirektAutoShow: + # Internal decorator to simplify mapping. + + def __init__(self, channel): + self._channel = channel + + def __get__(self, obj, obj_type): + # Grab the 16-bit value at the offset for this channel. + return obj._get_channel_16bit_value(self._channel) + + def __set__(self, obj, value): + # Set the 16-bit value at the offset for this channel. + assert 0 <= value <= 65535 + obj.set_channel(self._channel, value) + obj._write() + + # Define explicit channels for first IC. + # → this is only for api backwards compliance. + # not recommended for new use - use *set_channel* + b0 = _ChannelDirektAutoShow(0) + g0 = _ChannelDirektAutoShow(1) + r0 = _ChannelDirektAutoShow(2) + b1 = _ChannelDirektAutoShow(3) + g1 = _ChannelDirektAutoShow(4) + r1 = _ChannelDirektAutoShow(5) + b2 = _ChannelDirektAutoShow(6) + g2 = _ChannelDirektAutoShow(7) + r2 = _ChannelDirektAutoShow(8) + b3 = _ChannelDirektAutoShow(9) + g3 = _ChannelDirektAutoShow(10) + r3 = _ChannelDirektAutoShow(11) diff --git a/examples/tlc59711_dev.py b/examples/tlc59711_dev.py new file mode 100644 index 0000000..d8cdae5 --- /dev/null +++ b/examples/tlc59711_dev.py @@ -0,0 +1,499 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# CircuitPython + +# SPDX-FileCopyrightText: 2021 s-light +# SPDX-License-Identifier: MIT +# Author Stefan Krüger (s-light) + +"""TLC5971 / TLC59711 Multi Development.""" + +__doc__ = """ +TLC59711 development helper. + +this sketch contains a bunch of timing tests and other development things.. +Enjoy the colors :-) +""" + +import time + +import board +import busio + +import adafruit_tlc59711 + + +########################################## +PIXEL_COUNT = 16 * 1 + +spi = busio.SPI(board.SCK, MOSI=board.MOSI) +pixels = adafruit_tlc59711.TLC59711(spi, pixel_count=PIXEL_COUNT) + + +########################################## +# test function + +VALUE_HIGH = 1000 + + +def channelcheck_update_pixel(offset): + """Channel check pixel.""" + # print("offset", offset) + + # pixels[offset] = (VALUE_HIGH, 0, 0) + pixels.set_pixel_16bit_value(offset, VALUE_HIGH, 0, 0) + # clear last pixel + last = offset - 1 + if last < 0: + last = PIXEL_COUNT - 1 + # pixels[last] = (0, 0, 1) + pixels.set_pixel_16bit_value(last, 0, 0, 1) + # pixels[offset] = (0xAAAA, 0xBBBB, 0xCCCC) + pixels.show() + + offset += 1 + if offset >= PIXEL_COUNT: + time.sleep(0.2) + offset = 0 + print("clear") + pixels.set_pixel_all((0, 1, 0)) + pixels.show() + return offset + + +def channelcheck_update(offset): + """Channel check.""" + # print("offset", offset) + + pixels.set_channel(offset, VALUE_HIGH) + # clear last set channel + last = offset - 1 + if last < 0: + last = pixels.channel_count - 1 + pixels.set_channel(last, 0) + pixels.show() + + offset += 1 + if offset >= pixels.channel_count: + offset = 0 + print("offset overflow. start from 0") + return offset + + +########################################## + + +def timeit_call(message, test_function, loop_count=1000): + """Measure timing.""" + duration = 0 + start_time = time.monotonic() + for _index in range(loop_count): + start_time = time.monotonic() + test_function() + end_time = time.monotonic() + duration += end_time - start_time + # print( + # "duration:\n" + # "\t{}s for {} loops\n" + # "\t{:.2f}ms per call" + # "".format( + # duration, + # loop_count, + # (duration/loop_count)*1000 + # ) + # ) + # print( + # "\t{:.2f}ms per call" + # "".format((duration / loop_count) * 1000) + # ) + # "{:>8.2f}ms".format(3.56) + print( + "{call_duration:>10.4f}ms\t{message}" + "".format( + call_duration=(duration / loop_count) * 1000, + message=message, + ) + ) + + +def timeit_pixels_show(): + """Measure timing.""" + print("*** pixels show:") + loop_count = 1000 + + def _test(): + pixels.show() + + timeit_call("'pixels.show()'", _test, loop_count) + + +def timeit_pixels_set_single(): + """Measure timing pixels set.""" + print("*** pixels set single:") + loop_count = 1000 + + def _test(): + pixels[3] = (500, 40500, 1000) + + timeit_call("'pixels[3] = (500, 40500, 1000)'", _test, loop_count) + + def _test(): + pixels[3] = (0.1, 0.5, 0.9) + + timeit_call("'pixels[3] = (0.1, 0.5, 0.9)'", _test, loop_count) + + def _test(): + pixels.set_pixel(3, (500, 40500, 1000)) + + timeit_call("'pixels.set_pixel(3, (500, 40500, 1000))'", _test, loop_count) + + def _test(): + pixels.set_pixel(3, (0.1, 0.5, 0.9)) + + timeit_call("'pixels.set_pixel(3, (0.1, 0.5, 0.9))'", _test, loop_count) + + +def timeit_pixels_set_loop(): + """Measure timing pixels set.""" + print("*** pixels set loop:") + loop_count = 10 + + def _test(): + for i in range(PIXEL_COUNT): + pixels[i] = (500, 40500, 1000) + + timeit_call( + "'pixels[0..{}] = (500, 40500, 1000)'".format(PIXEL_COUNT), _test, loop_count + ) + + def _test(): + for i in range(PIXEL_COUNT): + pixels[i] = (0.1, 0.5, 0.9) + + timeit_call( + "'pixels[0..{}] = (0.1, 0.5, 0.9)'".format(PIXEL_COUNT), _test, loop_count + ) + + def _test(): + for i in range(PIXEL_COUNT): + pixels.set_pixel(i, (500, 40500, 1000)) + + timeit_call( + "'pixels.set_pixel(0..{}, (500, 40500, 1000))'".format(PIXEL_COUNT), + _test, + loop_count, + ) + + def _test(): + for i in range(PIXEL_COUNT): + pixels.set_pixel(i, (0.1, 0.5, 0.9)) + + timeit_call( + "'pixels.set_pixel(0..{}, (0.1, 0.5, 0.9))'".format(PIXEL_COUNT), + _test, + loop_count, + ) + + +def timeit_pixels_set_all(): + """Measure timing pixels set.""" + print("*** pixels set all:") + loop_count = 10 + + def _test(): + pixels.set_pixel_all((500, 40500, 1000)) + + timeit_call("'pixels.set_pixel_all((500, 40500, 1000))'", _test, loop_count) + + def _test(): + pixels.set_pixel_all((0.1, 0.5, 0.9)) + + timeit_call("'pixels.set_pixel_all((0.1, 0.5, 0.9))'", _test, loop_count) + + def _test(): + pixels.set_pixel_all_16bit_value(500, 40500, 1000) + + timeit_call( + "'pixels.set_pixel_all_16bit_value(500, 40500, 1000)'", _test, loop_count + ) + + def _test(): + pixels.set_all_black() + + timeit_call("'pixels.set_all_black()'", _test, loop_count) + + +def timeit_pixels_set_16bit(): + """Measure timing pixels set.""" + print("*** pixels set 16bit:") + loop_count = 1000 + + def _test(): + pixels.set_pixel_16bit_value(3, 500, 40500, 1000) + + timeit_call( + "'pixels.set_pixel_16bit_value(3, 500, 40500, 1000)'", _test, loop_count + ) + + def _test(): + pixels.set_pixel_16bit_color(3, (500, 40500, 1000)) + + timeit_call( + "'pixels.set_pixel_16bit_color(3, (500, 40500, 1000))'", _test, loop_count + ) + + def _test(): + for i in range(PIXEL_COUNT): + pixels.set_pixel_16bit_value(i, 500, 40500, 1000) + + timeit_call( + "'pixels.set_pixel_16bit_value(0..{}, 500, 40500, 1000)'" + "".format(PIXEL_COUNT), + _test, + 10, + ) + + def _test(): + for i in range(PIXEL_COUNT): + pixels.set_pixel_16bit_color(i, (500, 40500, 1000)) + + timeit_call( + "'pixels.set_pixel_16bit_color(0..{}, (500, 40500, 1000))'" + "".format(PIXEL_COUNT), + _test, + 10, + ) + + +def timeit_pixels_set_float(): + """Measure timing pixels set.""" + print("*** pixels set float:") + loop_count = 1000 + + def _test(): + pixels.set_pixel_float_value(3, 0.1, 0.5, 0.9) + + timeit_call("'pixels.set_pixel_float_value(3, 0.1, 0.5, 0.9)'", _test, loop_count) + + def _test(): + pixels.set_pixel_float_color(3, (0.1, 0.5, 0.9)) + + timeit_call("'pixels.set_pixel_float_color(3, (0.1, 0.5, 0.9))'", _test, loop_count) + + def _test(): + for i in range(PIXEL_COUNT): + pixels.set_pixel_float_value(i, 0.1, 0.5, 0.9) + + timeit_call( + "'pixels.set_pixel_float_value(0..{}, 0.1, 0.5, 0.9)'" "".format(PIXEL_COUNT), + _test, + 10, + ) + + def _test(): + for i in range(PIXEL_COUNT): + pixels.set_pixel_float_color(i, (0.1, 0.5, 0.9)) + + timeit_call( + "'pixels.set_pixel_float_color(0..{}, (0.1, 0.5, 0.9))'" "".format(PIXEL_COUNT), + _test, + 10, + ) + + def _test(): + for i in range(PIXEL_COUNT): + pixels.set_pixel_16bit_value( + i, int(0.1 * 65535), int(0.5 * 65535), int(0.9 * 65535) + ) + + timeit_call( + "'pixels.set_pixel_16bit_value(0..{}, f2i 0.1, f2i 0.5, f2i 0.9)'" + "".format(PIXEL_COUNT), + _test, + 10, + ) + + +def timeit_channel_set(): + """Measure timing channel set.""" + print("*** channel set:") + loop_count = 1000 + + def _test(): + pixels.set_channel(0, 10000) + + timeit_call("'set_channel(0, 10000)'", _test, loop_count) + + def _test(): + pixels.set_channel(0, 10000) + pixels.set_channel(1, 10000) + pixels.set_channel(2, 10000) + + timeit_call("'set_channel(0..2, 10000)'", _test, loop_count) + + channel_count = PIXEL_COUNT * 3 + + def _test(): + for i in range(channel_count): + pixels.set_channel(i, 500) + + timeit_call("'set_channel(for 0..{}, 10000)'" "".format(channel_count), _test, 10) + + +def timeit_channel_set_internal(): + """Measure timing channel set internal.""" + print("*** channel set internal:") + # loop_count = 1000 + # + # def _test(): + # pixels._set_channel_16bit_value(0, 10000) + # timeit_call( + # "'_set_channel_16bit_value(0, 10000)'", + # _test, + # loop_count + # ) + # + # def _test(): + # pixels._set_channel_16bit_value(0, 10000) + # pixels._set_channel_16bit_value(1, 10000) + # pixels._set_channel_16bit_value(2, 10000) + # timeit_call( + # "'_set_channel_16bit_value(0..2, 10000)'", + # _test, + # loop_count + # ) + # + # def _test(): + # for i in range(PIXEL_COUNT * 3): + # pixels._set_channel_16bit_value(i, 500) + # timeit_call( + # "'_set_channel_16bit_value(for 0..{}, 10000)'" + # "".format(PIXEL_COUNT * 3), + # _test, + # 10 + # ) + print(" must be uncommented in code to work..") + + +def timeit_pixels_get(): + """Measure timing pixels get.""" + print("*** pixels get:") + + pixels.set_pixel_all((1, 11, 111)) + + def _test(): + print("[", end="") + for i in range(PIXEL_COUNT): + print("{}:{}, ".format(i, pixels[i]), end="") + print("]") + + timeit_call("'print('{}:{}, '.format(i, pixels[i]), end='')'", _test, 1) + + +def time_measurement(): + """Measure timing.""" + print("meassure timing:") + timeit_pixels_show() + timeit_pixels_set_single() + timeit_pixels_set_loop() + timeit_pixels_set_all() + timeit_pixels_set_16bit() + timeit_pixels_set_float() + timeit_channel_set() + timeit_channel_set_internal() + timeit_pixels_get() + pixels.set_pixel_all((0, 1, 1)) + + +########################################## + + +def test_bcdata(): + """Test BC-Data setting.""" + print("test BC-Data setting:") + print("set pixel all to 100, 100, 100") + pixels.set_pixel_all((100, 100, 100)) + pixels.show() + time.sleep(2) + print( + "bcr: {:>3}\n" + "bcg: {:>3}\n" + "bcb: {:>3}\n" + "".format( + pixels.bcr, + pixels.bcg, + pixels.bcb, + ) + ) + # calculate bc values + Ioclmax = adafruit_tlc59711.TLC59711.calculate_Ioclmax(Riref=2.7) + print("Ioclmax = {}".format(Ioclmax)) + Riref = adafruit_tlc59711.TLC59711.calculate_Riref(Ioclmax=Ioclmax) + print("Riref = {}".format(Riref)) + BCValues = adafruit_tlc59711.TLC59711.calculate_BCData( + Ioclmax=Ioclmax, + IoutR=18, + IoutG=11, + IoutB=13, + ) + # (127, 77, 91) + print("BCValues = {}".format(BCValues)) + + print("set bcX") + pixels.bcr = BCValues[0] + pixels.bcg = BCValues[1] + pixels.bcb = BCValues[2] + pixels.update_BCData() + pixels.show() + print( + "bcr: {:>3}\n" + "bcg: {:>3}\n" + "bcb: {:>3}\n" + "".format( + pixels.bcr, + pixels.bcg, + pixels.bcb, + ) + ) + time.sleep(2) + + +########################################## + + +def test_main(): + """Test Main.""" + print(42 * "*", end="") + print(__doc__, end="") + print(42 * "*") + # print() + # time.sleep(0.5) + # print(42 * '*') + + pixels.set_pixel_all_16bit_value(1, 10, 100) + pixels.show() + time.sleep(0.5) + + test_bcdata() + time.sleep(0.5) + print(42 * "*") + + time_measurement() + time.sleep(0.5) + print(42 * "*") + + offset = 0 + + print("loop:") + while True: + offset = channelcheck_update(offset) + time.sleep(0.5) + print(offset) + + +########################################## +# main loop + +if __name__ == "__main__": + + test_main() diff --git a/examples/tlc59711_fancyled.py b/examples/tlc59711_fancyled.py new file mode 100644 index 0000000..7338cf9 --- /dev/null +++ b/examples/tlc59711_fancyled.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# CircuitPython + +# SPDX-FileCopyrightText: 2021 s-light +# SPDX-License-Identifier: MIT +# Author Stefan Krüger (s-light) + +"""TLC59711 & FancyLED.""" + +__doc__ = """ +TLC59711 & FancyLED. + +this is an example for combining the TLC5957 library with FancyLED. +Enjoy the colors :-) +""" + +import board + +import busio + +import adafruit_fancyled.adafruit_fancyled as fancyled +import adafruit_tlc59711 + +########################################## +print("\n" + (42 * "*") + "\n" + __doc__ + "\n" + (42 * "*") + "\n" + "\n") + +########################################## +# print(42 * "*") +# print("initialise digitalio pins for SPI") +# spi_clock = digitalio.DigitalInOut(board.SCK) +# spi_clock.direction = digitalio.Direction.OUTPUT +# spi_mosi = digitalio.DigitalInOut(board.MOSI) +# spi_mosi.direction = digitalio.Direction.OUTPUT +# spi_miso = digitalio.DigitalInOut(board.MISO) +# spi_miso.direction = digitalio.Direction.INPUT + +# print((42 * '*') + "\n" + "init busio.SPI") +spi = busio.SPI(board.SCK, MOSI=board.MOSI) + +########################################## +print(42 * "*") +print("init TLC5957") +NUM_LEDS = 16 +pixels = adafruit_tlc59711.TLC59711( + spi=spi, + pixel_count=NUM_LEDS, +) + +print("pixel_count", pixels.pixel_count) +print("chip_count", pixels.chip_count) +print("channel_count", pixels.channel_count) + + +########################################## +# main loop +print(42 * "*") +print("rainbow loop") +hue_offset = 0 +while True: + brightness = 0.8 + color = fancyled.CHSV(hue_offset, 1.0, 1.0) + color = fancyled.gamma_adjust(color, brightness=brightness) + pixels.set_pixel_all(color) + pixels.show() + + # Bigger number = faster spin + hue_offset += 0.000005 + if hue_offset >= 1: + hue_offset = 0 + print("heu_offset reset") diff --git a/examples/tlc59711_fastset.py b/examples/tlc59711_fastset.py new file mode 100644 index 0000000..85c7f1a --- /dev/null +++ b/examples/tlc59711_fastset.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# CircuitPython + +# SPDX-FileCopyrightText: 2021 s-light +# SPDX-License-Identifier: MIT +# Author Stefan Krüger (s-light) + +"""TLC5971 / TLC59711 Example.""" + +__doc__ = """ +tlc59711_fastset.py - TLC59711 fast set example. + +showcases the usage of set_pixel_16bit_value for fastest setting of values. +for speed comparision of all the available set calls +look at the tlc59711_dev.py file. + +Enjoy the colors :-) +""" + + +import time + +import board +import busio + +import adafruit_tlc59711 + + +########################################## +PIXEL_COUNT = 16 + +spi = busio.SPI(board.SCK, MOSI=board.MOSI) +pixels = adafruit_tlc59711.TLC59711(spi, pixel_count=PIXEL_COUNT) + + +########################################## +# test function + + +def channelcheck_update_pixel(offset): + """Channel check pixel.""" + # print("offset", offset) + + pixels.set_pixel_16bit_value(offset, 1000, 100, 0) + # clear last pixel + last = offset - 1 + if last < 0: + last = PIXEL_COUNT - 1 + pixels.set_pixel_16bit_value(last, 0, 0, 1) + pixels.show() + + offset += 1 + if offset >= PIXEL_COUNT: + time.sleep(0.2) + offset = 0 + print("clear") + pixels.set_pixel_all_16bit_value(0, 1, 0) + pixels.show() + return offset + + +def test_main(): + """Test Main.""" + print(42 * "*", end="") + print(__doc__, end="") + print(42 * "*") + + bcvalues = adafruit_tlc59711.TLC59711.calculate_BCData( + Ioclmax=18, + IoutR=18, + IoutG=11, + IoutB=13, + ) + print("bcvalues = {}".format(bcvalues)) + pixels.bcr = bcvalues[0] + pixels.bcg = bcvalues[1] + pixels.bcb = bcvalues[2] + pixels.update_BCData() + pixels.show() + + offset = 0 + + print("loop:") + while True: + offset = channelcheck_update_pixel(offset) + time.sleep(0.2) + + +########################################## +# main loop + +if __name__ == "__main__": + + test_main() diff --git a/examples/tlc59711_simpletest.py b/examples/tlc59711_simpletest.py index 08dc320..911694c 100644 --- a/examples/tlc59711_simpletest.py +++ b/examples/tlc59711_simpletest.py @@ -1,53 +1,40 @@ # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT -# Simple demo of the TLC59711 16-bit 12 channel LED PWM driver. -# Shows setting channel values in a few ways. +# simple demo of the TLC59711 16-bit 12 channel LED PWM driver. +# Shows the minimal usage - how to set pixel values in a few ways. # Author: Tony DiCola + import board import busio import adafruit_tlc59711 +print("tlc59711_simpletest.py") -# Define SPI bus connected to chip. You only need the clock and MOSI (output) -# line to use this chip. +# Define SPI bus connected to chip. +# You only need the clock and MOSI (output) line to use this chip. spi = busio.SPI(board.SCK, MOSI=board.MOSI) - -# Define the TLC59711 instance. -leds = adafruit_tlc59711.TLC59711(spi) -# Optionally you can disable the auto_show behavior that updates the chip -# as soon as any channel value is written. The default is True/on but you can -# disable and explicitly call show to control when updates happen for better -# animation or atomic RGB LED updates. -# leds = adafruit_tlc59711.TLC59711(spi, auto_show=False) - -# There are a couple ways to control the channels of the chip. -# The first is using an interface like a strip of NeoPixels. Index into the -# class for the channel and set or get its R, G, B tuple value. Note the -# component values are 16-bit numbers that range from 0-65535 (off to full -# brightness). Remember there are only 4 channels available too (0 to 3). -# For example set channel 0 (R0, G0, B0) to half brightness: -leds[0] = (32767, 32767, 32767) -# Dont forget to call show if you disabled auto_show above. -# leds.show() - -# Or to set channel 1 to full red only (green and blue off). -leds[1] = (65535, 0, 0) - -# You can also explicitly control each R0, G0, B0, R1, B1, etc. channel -# by getting and setting its 16-bit value directly with properties. -# For example set channel 2 to full green (i.e. G2 to maximum): -leds.g2 = 65535 -# Again don't forget to call show if you disabled auto_show above. -# leds.show() - -# The chip also supports global brightness channels to change the red, green, -# blue colors of all 4 channels at once. These are 7-bit values that range -# from 0-127. Get and set them with the red_brightness, green_brightness, -# and blue_brightness properties and again be sure to call show if you -# disabled auto_show. -# For example set the red channel to half brightness globally. -leds.red_brightness = 63 -# Don't forget to call show if you disabled auto_show above. -# leds.show() +pixels = adafruit_tlc59711.TLC59711(spi, pixel_count=16) + +# examples how to set the pixels: +# range: +# 0 - 65535 +# or +# 0.0 - 1.0 +# every pixel needs a color - +# give it just a list or tuple with 3 integer values: R G B + +# set all pixels to a very low level +pixels.set_pixel_all((10, 10, 10)) + +# every chip has 4 Pixels (=RGB-LEDs = 12 Channel) +pixels[0] = (100, 100, 100) +pixels[1] = (0, 0, 100) +pixels[2] = (0.01, 0.0, 0.01) +pixels[3] = (0.1, 0.01, 0.0) +# if you are ready to show your values you have to call +pixels.show() + +# there are a bunch of other ways to set pixel. +# have a look at the other examples. diff --git a/examples/tlc59711_singlechip_autoshow.py b/examples/tlc59711_singlechip_autoshow.py new file mode 100644 index 0000000..f4a4dce --- /dev/null +++ b/examples/tlc59711_singlechip_autoshow.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# CircuitPython + +# SPDX-FileCopyrightText: 2021 s-light +# SPDX-License-Identifier: MIT +# Author Stefan Krüger (s-light) + +"""TLC5971 / TLC59711 Example.""" + +__doc__ = """ +tlc59711_singlechip_autoshow.py - TLC59711AutoShow minimal usage example. + +simple demo of the TLC59711 16-bit 12 channel LED PWM driver. +Shows the minimal usage - how to set pixel values. +the TLC59711AutoShow class automatically writes the pixel values on each change. +this makes it very slow on lots of pixel changs but is convenient for only a handfull of pixels.. + +Author: Tony DiCola, Stefan Krueger + +Enjoy the colors :-) +""" + +import board +import busio + +import adafruit_tlc59711 + +print(__doc__) + +# Define SPI bus connected to chip. +# You only need the clock and MOSI (output) line to use this chip. +spi = busio.SPI(board.SCK, MOSI=board.MOSI) +pixels = adafruit_tlc59711.TLC59711AutoShow(spi) + +# Ways to set the values: +# just a list or tuple with 3 integer values: R G B +# each 0 - 65535 or 0.0 - 1.0 +# every chip has 4 RGB-LEDs (=12 Channel) +pixels[0] = (100, 100, 10111) +pixels[1] = (0, 0, 100) +pixels[2] = (0.01, 0.0, 0.01) +pixels[3] = (0.1, 0.01, 0.0) + +# You can also explicitly control each R0, G0, B0, R1, B1, etc. channel of the first ic +# by getting and setting its 16-bit value directly with properties. +# For example set channel 2 to 1/4 green (i.e. G2): +pixels.g2 = 65535 // 4 + +# there are a bunch of other advanced ways to set pixel. +# have a look at the other examples. diff --git a/examples/tlc59711_test_bcdata.py b/examples/tlc59711_test_bcdata.py new file mode 100644 index 0000000..2e42824 --- /dev/null +++ b/examples/tlc59711_test_bcdata.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# CircuitPython + +# SPDX-FileCopyrightText: 2021 s-light +# SPDX-License-Identifier: MIT +# Author Stefan Krüger (s-light) + +"""TLC5971 / TLC59711 Test BCData.""" + +__doc__ = """ +tlc59711_test_bcdata.py. + +test brightness correction data (BC) +""" + +import time + +import board +import busio +import supervisor + +import adafruit_tlc59711 + +########################################## +PIXEL_COUNT = 16 * 8 + +spi = busio.SPI(board.SCK, MOSI=board.MOSI) +pixels = adafruit_tlc59711.TLC59711(spi, pixel_count=PIXEL_COUNT) + +########################################## + + +def main_loop(): + """Loop.""" + new_value = input() + if "v" in new_value: + try: + value = int(new_value[1:]) + except ValueError as e: + print("Exception: ", e) + pixels.set_pixel_all_16bit_value(value, value, value) + else: + Ioclmax, IoutR, IoutG, IoutB = (18, 18, 11, 13) + try: + Ioclmax, IoutR, IoutG, IoutB = new_value.split(";") + Ioclmax = float(Ioclmax) + IoutR = float(IoutR) + IoutG = float(IoutG) + IoutB = float(IoutB) + except ValueError as e: + print("Exception: ", e) + BCValues = adafruit_tlc59711.TLC59711.calculate_BCData( + Ioclmax=Ioclmax, + IoutR=IoutR, + IoutG=IoutG, + IoutB=IoutB, + ) + pixels.bcr = BCValues[0] + pixels.bcg = BCValues[1] + pixels.bcb = BCValues[2] + print( + "bcr: {:>3}\n" + "bcg: {:>3}\n" + "bcb: {:>3}\n" + "".format( + pixels.bcr, + pixels.bcg, + pixels.bcb, + ) + ) + pixels.update_BCData() + pixels.show() + # prepare new input + print("\nenter new values:") + + +def test_main(): + """Test Main.""" + print(42 * "*", end="") + print(__doc__, end="") + print(42 * "*") + # print() + # time.sleep(0.5) + # print(42 * '*') + + print("set pixel all to 100, 100, 100") + pixels.set_pixel_all((5000, 5000, 5000)) + # calculate bc values + Ioclmax = adafruit_tlc59711.TLC59711.calculate_Ioclmax(Riref=2.7) + print("Ioclmax = {}".format(Ioclmax)) + Riref = adafruit_tlc59711.TLC59711.calculate_Riref(Ioclmax=Ioclmax) + print("Riref = {}".format(Riref)) + BCValues = adafruit_tlc59711.TLC59711.calculate_BCData( + Ioclmax=Ioclmax, + IoutR=18, + IoutG=11, + IoutB=13, + ) + # (127, 77, 91) + print("BCValues = {}".format(BCValues)) + pixels.bcr = BCValues[0] + pixels.bcg = BCValues[1] + pixels.bcb = BCValues[2] + pixels.update_BCData() + pixels.show() + time.sleep(0.1) + + if supervisor.runtime.serial_connected: + print( + "\n" + "this script offers two things to be changed:\n" + "- value for all channels\n" + "example: 'v10'\n" + "example: 'v65535'\n" + "- (global) brightness control:\n" + "use format: 'Ioclmax; IoutR; IoutG; IoutB'\n" + "example: '18; 7; 15; 17'" + "\n" + ) + while True: + if supervisor.runtime.serial_bytes_available: + main_loop() + + +########################################## +# main loop + +if __name__ == "__main__": + + test_main() diff --git a/pylama.ini b/pylama.ini new file mode 100644 index 0000000..ffd3559 --- /dev/null +++ b/pylama.ini @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2021 s-light +# SPDX-License-Identifier: Unlicense + +[pylama:pycodestyle] +max_line_length = 100 + +[pylama:pylint] +max_line_length = 100