Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ Future release (next version)
=====

Board Support:
- Waveshare ESP32-S3-Touch-LCD-3.5: ES8311 audio support (speaker output + microphone input over I2S, DAC volume default tuned by ear on hardware)
- Add Waveshare ESP32-S3-Touch-LCD-3.5 (3.5" 320x480 ST7796 over SPI with LCD reset/CS on a PCA9554 IO expander, FT6336 touch, QMI8658 IMU, AXP2101 battery management); vendors the st7796 display driver and adds a PCA9554 expander driver
- UNIHIKER K10: improve two-button navigation, correct camera preview orientation, and restore button input after camera operations
- unix/macOS/web: compiler fallback to bytecode on architectures without a native emitter (e.g. macOS on Apple Silicon) instead of runtime-loaded apps using @micropython.native/@micropython.viper failing to import with SyntaxError 'invalid micropython decorator'

Expand Down
26 changes: 26 additions & 0 deletions internal_filesystem/lib/drivers/display/st7796/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import sys
from . import st7796
from . import _st7796_init

# Register _st7796_init in sys.modules so __import__('_st7796_init') can find it
# This is needed because display_driver_framework.py uses __import__('_st7796_init')
# expecting a top-level module, but _st7796_init is in the st7796 package subdirectory
sys.modules['_st7796_init'] = _st7796_init

# Explicitly define __all__ and re-export public symbols from st7796 module
__all__ = [
'ST7796',
'STATE_HIGH',
'STATE_LOW',
'STATE_PWM',
'BYTE_ORDER_RGB',
'BYTE_ORDER_BGR',
]

# Re-export the public symbols
ST7796 = st7796.ST7796
STATE_HIGH = st7796.STATE_HIGH
STATE_LOW = st7796.STATE_LOW
STATE_PWM = st7796.STATE_PWM
BYTE_ORDER_RGB = st7796.BYTE_ORDER_RGB
BYTE_ORDER_BGR = st7796.BYTE_ORDER_BGR
129 changes: 129 additions & 0 deletions internal_filesystem/lib/drivers/display/st7796/_st7796_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser

import time
from micropython import const # NOQA

import lvgl as lv # NOQA

_SWRESET = const(0x01)
_SLPOUT = const(0x11)
_CSCON = const(0xF0)
_MADCTL = const(0x36)
_COLMOD = const(0x3A)
_DIC = const(0xB4)
_DFC = const(0xB6)
_DOCA = const(0xE8)
_PWR2 = const(0xC1)
_PWR3 = const(0xC2)
_VCMPCTL = const(0xC5)
_PGC = const(0xE0)
_NGC = const(0xE1)
_INVON = const(0x21)
_DISPON = const(0x29)

_EM = const(0xB7)


def init(self):
param_buf = bytearray(14)
param_mv = memoryview(param_buf)

self.set_params(_SWRESET)

time.sleep_ms(120) # NOQA

self.set_params(_SLPOUT)

time.sleep_ms(120) # NOQA

param_buf[0] = 0xC3
self.set_params(_CSCON, param_mv[:1])

param_buf[0] = 0x96
self.set_params(_CSCON, param_mv[:1])

param_buf[0] = (
self._madctl(
self._color_byte_order,
self._ORIENTATION_TABLE # NOQA
)
)
self.set_params(_MADCTL, param_mv[:1])

color_size = lv.color_format_get_size(self._color_space)
if color_size == 2: # NOQA
pixel_format = 0x05
elif color_size == 3:
pixel_format = 0x07
else:
raise RuntimeError(
'ST7796 IC only supports '
'lv.COLOR_FORMAT.RGB565 or lv.COLOR_FORMAT.RGB888'
)

param_buf[0] = pixel_format
self.set_params(_COLMOD, param_mv[:1])

param_buf[0] = 0xC6
self.set_params(_EM, param_mv[:1])

param_buf[0] = 0x01
self.set_params(_DIC, param_mv[:1])

param_buf[0] = 0x80
param_buf[1] = 0x02
param_buf[2] = 0x3B
self.set_params(_DFC, param_mv[:3])

param_buf[:8] = bytearray(
[
0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33
]
)
self._data_bus.tx_param(_DOCA, param_mv[:8])

param_buf[0] = 0x06
self.set_params(_PWR2, param_mv[:1])

param_buf[0] = 0xA7
self.set_params(_PWR3, param_mv[:1])

param_buf[0] = 0x18
self.set_params(_VCMPCTL, param_mv[:1])

time.sleep_ms(120) # NOQA

param_buf[:14] = bytearray(
[
0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F,
0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B
]
)
self.set_params(_PGC, param_mv[:14])

param_buf[:14] = bytearray(
[
0xF0, 0x09, 0x0B, 0x06, 0x04, 0x03, 0x2D,
0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B
]
)
self.set_params(_NGC, param_mv[:14])

time.sleep_ms(120) # NOQA

param_buf[0] = 0x3C
self.set_params(_CSCON, param_mv[:1])

param_buf[0] = 0x69
self.set_params(_CSCON, param_mv[:1])

time.sleep_ms(120) # NOQA

# IPS panels are normally-inverted: without INVON the whole display
# renders in negative (verified on the Waveshare ESP32-S3-Touch-LCD-3.5).
# The st7789 driver does the same for the same reason.
self.set_params(_INVON)

self.set_params(_DISPON)

time.sleep_ms(120) # NOQA
26 changes: 26 additions & 0 deletions internal_filesystem/lib/drivers/display/st7796/st7796.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser

from micropython import const # NOQA
import display_driver_framework


STATE_HIGH = display_driver_framework.STATE_HIGH
STATE_LOW = display_driver_framework.STATE_LOW
STATE_PWM = display_driver_framework.STATE_PWM

BYTE_ORDER_RGB = display_driver_framework.BYTE_ORDER_RGB
BYTE_ORDER_BGR = display_driver_framework.BYTE_ORDER_BGR

_MADCTL_MV = const(0x20)
_MADCTL_MX = const(0x40)
_MADCTL_MY = const(0x80)


class ST7796(display_driver_framework.DisplayDriver):

_ORIENTATION_TABLE = (
_MADCTL_MX,
_MADCTL_MV | _MADCTL_MY | _MADCTL_MX,
_MADCTL_MY,
_MADCTL_MV
)
44 changes: 44 additions & 0 deletions internal_filesystem/lib/drivers/io_expander/pca9554.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import i2c
from micropython import const


class PCA9554:
"""PCA9554 / TCA9554 8-bit I2C IO expansion chip.

Same register layout across the PCA9554/TCA9554/PCA9554A family:
input port, output port, polarity inversion, configuration.
Used e.g. on the Waveshare ESP32-S3-Touch-LCD-3.5, where the LCD
reset and chip-select lines are wired to expander pins EXIO1/EXIO2.
"""

REG_INPUT = const(0x00)
REG_OUTPUT = const(0x01)
REG_POLARITY = const(0x02)
REG_CONFIG = const(0x03)

def __init__(self, i2c_bus: i2c.I2C.Bus, dev_id: int):
self.dev = i2c.I2C.Device(bus=i2c_bus, dev_id=dev_id, reg_bits=8)
self.directions = 0xFF # all inputs by default (chip reset state)
self.output_states = 0x00

def _write_reg(self, reg, value):
self.dev.write_mem(reg, bytes([value & 0xFF]))

def _read_reg(self, reg):
buf = bytearray(1)
self.dev.read_mem(reg, buf=buf)
return buf[0]

def set_output(self, pin, value):
"""Configure pin (0-7) as output and drive it high (True) or low."""
if value:
self.output_states |= (1 << pin)
else:
self.output_states &= ~(1 << pin)
self._write_reg(self.REG_OUTPUT, self.output_states)
self.directions &= ~(1 << pin) # 0 = output
self._write_reg(self.REG_CONFIG, self.directions)

def get_input(self, pin):
"""Read pin (0-7); pin must be configured as input (the default)."""
return bool(self._read_reg(self.REG_INPUT) & (1 << pin))
Loading
Loading