From 0d82f5d8c868d7f4e93d48e400cd4d769175e3e5 Mon Sep 17 00:00:00 2001 From: Seon Rozenblum Date: Thu, 24 Oct 2019 19:20:39 +1100 Subject: [PATCH] esp32/boards/TINYPICO: Add tinypico.py, dotstar.py with custom manifest. --- ports/esp32/boards/TINYPICO/manifest.py | 2 + .../esp32/boards/TINYPICO/modules/dotstar.py | 228 ++++++++++++++++++ .../esp32/boards/TINYPICO/modules/tinypico.py | 113 +++++++++ ports/esp32/boards/TINYPICO/mpconfigboard.mk | 2 + 4 files changed, 345 insertions(+) create mode 100644 ports/esp32/boards/TINYPICO/manifest.py create mode 100644 ports/esp32/boards/TINYPICO/modules/dotstar.py create mode 100644 ports/esp32/boards/TINYPICO/modules/tinypico.py diff --git a/ports/esp32/boards/TINYPICO/manifest.py b/ports/esp32/boards/TINYPICO/manifest.py new file mode 100644 index 000000000..81fff1d7c --- /dev/null +++ b/ports/esp32/boards/TINYPICO/manifest.py @@ -0,0 +1,2 @@ +include('$(PORT_DIR)/boards/manifest.py') +freeze("modules") diff --git a/ports/esp32/boards/TINYPICO/modules/dotstar.py b/ports/esp32/boards/TINYPICO/modules/dotstar.py new file mode 100644 index 000000000..a848e8e17 --- /dev/null +++ b/ports/esp32/boards/TINYPICO/modules/dotstar.py @@ -0,0 +1,228 @@ +# DotStar strip driver for MicroPython +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Damien P. George (original Neopixel object) +# Copyright (c) 2017 Ladyada +# Copyright (c) 2017 Scott Shawcroft for Adafruit Industries +# Copyright (c) 2019 Matt Trentini (porting back to MicroPython) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +START_HEADER_SIZE = 4 +LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits + +# Pixel color order constants +RGB = (0, 1, 2) +RBG = (0, 2, 1) +GRB = (1, 0, 2) +GBR = (1, 2, 0) +BRG = (2, 0, 1) +BGR = (2, 1, 0) + + +class DotStar: + """ + A sequence of dotstars. + + :param SPI spi: The SPI object to write output to. + :param int n: The number of dotstars in the chain + :param float brightness: Brightness of the pixels between 0.0 and 1.0 + :param bool auto_write: True if the dotstars should immediately change when + set. If False, `show` must be called explicitly. + :param tuple pixel_order: Set the pixel order on the strip - different + strips implement this differently. If you send red, and it looks blue + or green on the strip, modify this! It should be one of the values above + + + Example for TinyPICO: + + .. code-block:: python + + from dotstar import DotStar + from machine import Pin, SPI + + spi = SPI(sck=Pin(12), mosi=Pin(13), miso=Pin(18)) # Configure SPI - note: miso is unused + dotstar = DotStar(spi, 1) + dotstar[0] = (128, 0, 0) # Red + """ + + def __init__(self, spi, n, *, brightness=1.0, auto_write=True, + pixel_order=BGR): + self._spi = spi + self._n = n + # Supply one extra clock cycle for each two pixels in the strip. + self.end_header_size = n // 16 + if n % 16 != 0: + self.end_header_size += 1 + self._buf = bytearray(n * 4 + START_HEADER_SIZE + self.end_header_size) + self.end_header_index = len(self._buf) - self.end_header_size + self.pixel_order = pixel_order + # Four empty bytes to start. + for i in range(START_HEADER_SIZE): + self._buf[i] = 0x00 + # Mark the beginnings of each pixel. + for i in range(START_HEADER_SIZE, self.end_header_index, 4): + self._buf[i] = 0xff + # 0xff bytes at the end. + for i in range(self.end_header_index, len(self._buf)): + self._buf[i] = 0xff + self._brightness = 1.0 + # Set auto_write to False temporarily so brightness setter does _not_ + # call show() while in __init__. + self.auto_write = False + self.brightness = brightness + self.auto_write = auto_write + + def deinit(self): + """Blank out the DotStars and release the resources.""" + self.auto_write = False + for i in range(START_HEADER_SIZE, self.end_header_index): + if i % 4 != 0: + self._buf[i] = 0 + self.show() + if self._spi: + self._spi.deinit() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.deinit() + + def __repr__(self): + return "[" + ", ".join([str(x) for x in self]) + "]" + + def _set_item(self, index, value): + """ + value can be one of three things: + a (r,g,b) list/tuple + a (r,g,b, brightness) list/tuple + a single, longer int that contains RGB values, like 0xFFFFFF + brightness, if specified should be a float 0-1 + + Set a pixel value. You can set per-pixel brightness here, if it's not passed it + will use the max value for pixel brightness value, which is a good default. + + Important notes about the per-pixel brightness - it's accomplished by + PWMing the entire output of the LED, and that PWM is at a much + slower clock than the rest of the LEDs. This can cause problems in + Persistence of Vision Applications + """ + + offset = index * 4 + START_HEADER_SIZE + rgb = value + if isinstance(value, int): + rgb = (value >> 16, (value >> 8) & 0xff, value & 0xff) + + if len(rgb) == 4: + brightness = value[3] + # Ignore value[3] below. + else: + brightness = 1 + + # LED startframe is three "1" bits, followed by 5 brightness bits + # then 8 bits for each of R, G, and B. The order of those 3 are configurable and + # vary based on hardware + # same as math.ceil(brightness * 31) & 0b00011111 + # Idea from https://www.codeproject.com/Tips/700780/Fast-floor-ceiling-functions + brightness_byte = 32 - int(32 - brightness * 31) & 0b00011111 + self._buf[offset] = brightness_byte | LED_START + self._buf[offset + 1] = rgb[self.pixel_order[0]] + self._buf[offset + 2] = rgb[self.pixel_order[1]] + self._buf[offset + 3] = rgb[self.pixel_order[2]] + + def __setitem__(self, index, val): + if isinstance(index, slice): + start, stop, step = index.indices(self._n) + length = stop - start + if step != 0: + # same as math.ceil(length / step) + # Idea from https://fizzbuzzer.com/implement-a-ceil-function/ + length = (length + step - 1) // step + if len(val) != length: + raise ValueError("Slice and input sequence size do not match.") + for val_i, in_i in enumerate(range(start, stop, step)): + self._set_item(in_i, val[val_i]) + else: + self._set_item(index, val) + + if self.auto_write: + self.show() + + def __getitem__(self, index): + if isinstance(index, slice): + out = [] + for in_i in range(*index.indices(self._n)): + out.append( + tuple(self._buf[in_i * 4 + (3 - i) + START_HEADER_SIZE] for i in range(3))) + return out + if index < 0: + index += len(self) + if index >= self._n or index < 0: + raise IndexError + offset = index * 4 + return tuple(self._buf[offset + (3 - i) + START_HEADER_SIZE] + for i in range(3)) + + def __len__(self): + return self._n + + @property + def brightness(self): + """Overall brightness of the pixel""" + return self._brightness + + @brightness.setter + def brightness(self, brightness): + self._brightness = min(max(brightness, 0.0), 1.0) + if self.auto_write: + self.show() + + def fill(self, color): + """Colors all pixels the given ***color***.""" + auto_write = self.auto_write + self.auto_write = False + for i in range(self._n): + self[i] = color + if auto_write: + self.show() + self.auto_write = auto_write + + def show(self): + """Shows the new colors on the pixels themselves if they haven't already + been autowritten. + + The colors may or may not be showing after this function returns because + it may be done asynchronously.""" + # Create a second output buffer if we need to compute brightness + buf = self._buf + if self.brightness < 1.0: + buf = bytearray(self._buf) + # Four empty bytes to start. + for i in range(START_HEADER_SIZE): + buf[i] = 0x00 + for i in range(START_HEADER_SIZE, self.end_header_index): + buf[i] = self._buf[i] if i % 4 == 0 else int(self._buf[i] * self._brightness) + # Four 0xff bytes at the end. + for i in range(self.end_header_index, len(buf)): + buf[i] = 0xff + + if self._spi: + self._spi.write(buf) diff --git a/ports/esp32/boards/TINYPICO/modules/tinypico.py b/ports/esp32/boards/TINYPICO/modules/tinypico.py new file mode 100644 index 000000000..2fc379ccd --- /dev/null +++ b/ports/esp32/boards/TINYPICO/modules/tinypico.py @@ -0,0 +1,113 @@ +# TinyPICO MicroPython Helper Library +# 2019 Seon Rozenblum, Matt Trentini +# +# Project home: +# https://github.com/TinyPICO +# +# 2019-Mar-12 - v0.1 - Initial implementation +# 2019-May-20 - v1.0 - Initial Release +# 2019-Oct-23 - v1.1 - Removed temp sensor code, prep for frozen modules + +# Import required libraries +from micropython import const +from machine import Pin, SPI, ADC +import machine, time, esp32 + +# TinyPICO Hardware Pin Assignments + +# Battery +BAT_VOLTAGE = const(35) +BAT_CHARGE = const(34) + +# APA102 Dotstar pins for production boards +DOTSTAR_CLK = const(12) +DOTSTAR_DATA = const(2) +DOTSTAR_PWR = const(13) + +# SPI +SPI_MOSI = const(23) +SPI_CLK = const(18) +SPI_MISO = const(19) + +#I2C +I2C_SDA = const(21) +I2C_SCL = const(22) + +#DAC +DAC1 = const(25) +DAC2 = const(26) + +# Helper functions + +# Get a *rough* estimate of the current battery voltage +# If the battery is not present, the charge IC will still report it's trying to charge at X voltage +# so it will still show a voltage. +def get_battery_voltage(): + """ + Returns the current battery voltage. If no battery is connected, returns 3.7V + This is an approximation only, but useful to detect of the charge state of the battery is getting low. + """ + adc = ADC(Pin(BAT_VOLTAGE)) # Assign the ADC pin to read + measuredvbat = adc.read() # Read the value + measuredvbat /= 4095 # divide by 4095 as we are using the default ADC voltage range of 0-1V + measuredvbat *= 3.7 # Multiply by 3.7V, our reference voltage + return measuredvbat + +# Return the current charge state of the battery - we need to read the value multiple times +# to eliminate false negatives due to the charge IC not knowing the difference between no battery +# and a full battery not charging - This is why the charge LED flashes +def get_battery_charging(): + """ + Returns the current battery charging state. + This can trigger false positives as the charge IC can't tell the difference between a full battery or no battery connected. + """ + measuredVal = 0 # start our reading at 0 + io = Pin(BAT_CHARGE, Pin.IN) # Assign the pin to read + + for y in range(0, 10): # loop through 10 times adding the read values together to ensure no false positives + measuredVal += io.value() + + return measuredVal == 0 # return True if the value is 0 + + +# Power to the on-board Dotstar is controlled by a PNP transistor, so low is ON and high is OFF +# We also need to set the Dotstar clock and data pins to be inputs to prevent power leakage when power is off +# This might be improved at a future date +# The reason we have power control for the Dotstar is that it has a quiescent current of around 1mA, so we +# need to be able to cut power to it to minimise power consumption during deep sleep or with general battery powered use +# to minimise unneeded battery drain +def set_dotstar_power(state): + """Set the power for the on-board Dostar to allow no current draw when not needed.""" + # Set the power pin to the inverse of state + if state: + Pin(DOTSTAR_PWR, Pin.OUT, None) # Break the PULL_HOLD on the pin + Pin(DOTSTAR_PWR).value(False) # Set the pin to LOW to enable the Transistor + else: + Pin(13, Pin.IN, Pin.PULL_HOLD) # Set PULL_HOLD on the pin to allow the 3V3 pull-up to work + + Pin(DOTSTAR_CLK, Pin.OUT if state else Pin.IN) # If power is on, set CLK to be output, otherwise input + Pin(DOTSTAR_DATA, Pin.OUT if state else Pin.IN) # If power is on, set DATA to be output, otherwise input + + # A small delay to let the IO change state + time.sleep(.035) + +# Dotstar rainbow colour wheel +def dotstar_color_wheel(wheel_pos): + """Color wheel to allow for cycling through the rainbow of RGB colors.""" + wheel_pos = wheel_pos % 255 + + if wheel_pos < 85: + return 255 - wheel_pos * 3, 0, wheel_pos * 3 + elif wheel_pos < 170: + wheel_pos -= 85 + return 0, wheel_pos * 3, 255 - wheel_pos * 3 + else: + wheel_pos -= 170 + return wheel_pos * 3, 255 - wheel_pos * 3, 0 + +# Go into deep sleep but shut down the APA first to save power +# Use this if you want lowest deep sleep current +def go_deepsleep(t): + """Deep sleep helper that also powers down the on-board Dotstar.""" + set_dotstar_power(False) + machine.deepsleep(t) diff --git a/ports/esp32/boards/TINYPICO/mpconfigboard.mk b/ports/esp32/boards/TINYPICO/mpconfigboard.mk index 485b3f165..5c96ec45a 100644 --- a/ports/esp32/boards/TINYPICO/mpconfigboard.mk +++ b/ports/esp32/boards/TINYPICO/mpconfigboard.mk @@ -4,3 +4,5 @@ SDKCONFIG += boards/sdkconfig.base SDKCONFIG += boards/sdkconfig.240mhz SDKCONFIG += boards/sdkconfig.spiram SDKCONFIG += boards/TINYPICO/sdkconfig.board + +FROZEN_MANIFEST ?= $(BOARD_DIR)/manifest.py