Add support for unPhone 9 (#74)

https://unphone.net/

What worked:

- hx8357d Display and XPT2046 touch screen works
- Turn display backlight on/off via TCA9555 chip
- Buttons

TODOs:

- Use LEDs
- LoRa
- IR

`.../lib/drivers/display/hx8357d/` is a not modified copy from
https://github.com/lvgl-micropython/lvgl_micropython/tree/main/api_drivers/common_api_drivers/display/hx8357d

`.../lib/drivers/indev/xpt2046.py` based on
https://github.com/lvgl-micropython/lvgl_micropython/blob/main/api_drivers/common_api_drivers/indev/xpt2046.py
but is modified: Because of the shared SPI bus for SPI for hx8357d display and xpt2046 touch
controller. For this i add the management of `CS` pins for reading the touch controller.
Let's discuss how to add this to upstream in
https://github.com/lvgl-micropython/lvgl_micropython/issues/536
This commit is contained in:
Jens Diemer
2026-03-11 12:38:52 +01:00
committed by GitHub
parent 5ded4b40f6
commit 2f4adfcace
7 changed files with 814 additions and 5 deletions
@@ -0,0 +1,26 @@
import sys
from . import hx8357d
from . import _hx8357d_init
# Register _hx8357d_init in sys.modules so __import__('_hx8357d_init') can find it
# This is needed because display_driver_framework.py uses __import__('_hx8357d_init')
# expecting a top-level module, but _hx8357d_init is in the hx8357d package subdirectory
sys.modules['_hx8357d_init'] = _hx8357d_init
# Explicitly define __all__ and re-export public symbols from hx8357d module
__all__ = [
'HX8357D',
'STATE_HIGH',
'STATE_LOW',
'STATE_PWM',
'BYTE_ORDER_RGB',
'BYTE_ORDER_BGR',
]
# Re-export the public symbols
HX8357D = hx8357d.HX8357D
STATE_HIGH = hx8357d.STATE_HIGH
STATE_LOW = hx8357d.STATE_LOW
STATE_PWM = hx8357d.STATE_PWM
BYTE_ORDER_RGB = hx8357d.BYTE_ORDER_RGB
BYTE_ORDER_BGR = hx8357d.BYTE_ORDER_BGR
@@ -0,0 +1,93 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser
import time
from micropython import const # NOQA
import lvgl as lv # NOQA
import lcd_bus # NOQA
_SWRESET = const(0x01)
_SLPOUT = const(0x11)
_DISPON = const(0x29)
_COLMOD = const(0x3A)
_MADCTL = const(0x36)
_TEON = const(0x35)
_TEARLINE = const(0x44)
_SETOSC = const(0xB0)
_SETPWR1 = const(0xB1)
_SETRGB = const(0xB3)
_SETCOM = const(0xB6)
_SETCYC = const(0xB4)
_SETC = const(0xB9)
_SETSTBA = const(0xC0)
_SETPANEL = const(0xCC)
_SETGAMMA = const(0xE0)
def init(self):
param_buf = bytearray(34)
param_mv = memoryview(param_buf)
time.sleep_ms(300) # NOQA
param_buf[:3] = bytearray([0xFF, 0x83, 0x57])
self.set_params(_SETC, param_mv[:3])
param_buf[0] = 0x80
self.set_params(_SETRGB, param_mv[:1])
param_buf[:4] = bytearray([0x00, 0x06, 0x06, 0x25])
self.set_params(_SETCOM, param_mv[:4])
param_buf[0] = 0x68
self.set_params(_SETOSC, param_mv[:1])
param_buf[0] = 0x05
self.set_params(_SETPANEL, param_mv[:1])
param_buf[:6] = bytearray([0x00, 0x15, 0x1C, 0x1C, 0x83, 0xAA])
self.set_params(_SETPWR1, param_mv[:6])
param_buf[:6] = bytearray([0x50, 0x50, 0x01, 0x3C, 0x1E, 0x08])
self.set_params(_SETSTBA, param_mv[:6])
param_buf[:7] = bytearray([0x02, 0x40, 0x00, 0x2A, 0x2A, 0x0D, 0x78])
self.set_params(_SETCYC, param_mv[:7])
param_buf[:34] = bytearray([
0x02, 0x0A, 0x11, 0x1d, 0x23, 0x35, 0x41, 0x4b, 0x4b, 0x42, 0x3A,
0x27, 0x1B, 0x08, 0x09, 0x03, 0x02, 0x0A, 0x11, 0x1d, 0x23, 0x35,
0x41, 0x4b, 0x4b, 0x42, 0x3A, 0x27, 0x1B, 0x08, 0x09, 0x03, 0x00, 0x01])
self.set_params(_SETGAMMA, param_mv[:34])
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 = 0x55
else:
raise RuntimeError(
f'{self.__class__.__name__} IC only supports '
'lv.COLOR_FORMAT.RGB565'
)
param_buf[0] = pixel_format
self.set_params(_COLMOD, param_mv[:1])
param_buf[0] = 0x00
self.set_params(_TEON, param_mv[:1])
param_buf[:2] = bytearray([0x00, 0x02])
self.set_params(_TEARLINE, param_mv[:2])
time.sleep_ms(150) # NOQA
self.set_params(_SLPOUT)
time.sleep_ms(50) # NOQA
self.set_params(_DISPON)
@@ -0,0 +1,15 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser
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
class HX8357D(display_driver_framework.DisplayDriver):
pass
@@ -0,0 +1,127 @@
# Copyright (c) 2024 - 2025 Kevin G. Schlosser
import lvgl as lv # NOQA
from micropython import const # NOQA
import micropython # NOQA
import machine # NOQA
import pointer_framework
import time
_CMD_X_READ = const(0xD0) # 12 bit resolution
_CMD_Y_READ = const(0x90) # 12 bit resolution
_CMD_Z1_READ = const(0xB0)
_CMD_Z2_READ = const(0xC0)
_MIN_RAW_COORD = const(10)
_MAX_RAW_COORD = const(4090)
class XPT2046(pointer_framework.PointerDriver):
touch_threshold = 400
confidence = 5
margin = 50
def __init__(
self,
device: machine.SPI.Bus,
display_width: int,
display_height: int,
lcd_cs: int,
touch_cs: int,
touch_cal=None,
startup_rotation=lv.DISPLAY_ROTATION._0,
debug=False,
):
self._device = device # machine.SPI.Bus() instance, shared with display
self._debug = debug
self.lcd_cs = machine.Pin(lcd_cs, machine.Pin.OUT, value=0)
self.touch_cs = machine.Pin(touch_cs, machine.Pin.OUT, value=1)
self._width = display_width
self._height = display_height
self._tx_buf = bytearray(3)
self._tx_mv = memoryview(self._tx_buf)
self._rx_buf = bytearray(3)
self._rx_mv = memoryview(self._rx_buf)
self.__confidence = max(min(self.confidence, 25), 3)
self.__points = [[0, 0] for _ in range(self.__confidence)]
margin = max(min(self.margin, 100), 1)
self.__margin = margin * margin
super().__init__(
touch_cal=touch_cal, startup_rotation=startup_rotation, debug=debug
)
def _read_reg(self, reg, num_bytes):
self._tx_buf[0] = reg
self._device.write_readinto(self._tx_mv[:num_bytes], self._rx_mv[:num_bytes])
return ((self._rx_buf[1] << 8) | self._rx_buf[2]) >> 3
def _get_coords(self):
try:
self.lcd_cs.value(1) # deselect LCD to avoid conflicts
self.touch_cs.value(0) # select touch chip
z1 = self._read_reg(_CMD_Z1_READ, 3)
z2 = self._read_reg(_CMD_Z2_READ, 3)
z = z1 + ((_MAX_RAW_COORD + 6) - z2)
if z < self.touch_threshold:
return None # Not touched
points = self.__points
count = 0
end_time = time.ticks_us() + 5000
while time.ticks_us() < end_time:
if count == self.__confidence:
break
raw_x = self._read_reg(_CMD_X_READ, 3)
if raw_x < _MIN_RAW_COORD:
continue
raw_y = self._read_reg(_CMD_Y_READ, 3)
if raw_y > _MAX_RAW_COORD:
continue
# put in buff
points[count][0] = raw_x
points[count][1] = raw_y
count += 1
finally:
self.touch_cs.value(1) # deselect touch chip
self.lcd_cs.value(0) # select LCD
if not count:
return None # Not touched
meanx = sum([points[i][0] for i in range(count)]) // count
meany = sum([points[i][1] for i in range(count)]) // count
dev = (
sum(
[
(points[i][0] - meanx) ** 2 + (points[i][1] - meany) ** 2
for i in range(count)
]
)
/ count
)
if dev >= self.__margin:
return None # Not touched
x = pointer_framework.remap(
meanx, _MIN_RAW_COORD, _MAX_RAW_COORD, 0, self._orig_width
)
y = pointer_framework.remap(
meany, _MIN_RAW_COORD, _MAX_RAW_COORD, 0, self._orig_height
)
if self._debug:
print(
f"{self.__class__.__name__}_TP_DATA({count=} {meanx=} {meany=} {z1=} {z2=} {z=})"
) # NOQA
return self.PRESSED, x, y
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -16,7 +16,7 @@ def init_rootscreen():
# Initialize DisplayMetrics with actual display values
DisplayMetrics.set_resolution(width, height)
DisplayMetrics.set_dpi(dpi)
DisplayMetrics.set_dpi(dpi)
print(f"init_rootscreen set resolution to {width}x{height} at {dpi} DPI")
# Show logo
@@ -92,6 +92,10 @@ def detect_board():
import machine
unique_id_prefixes = machine.unique_id()[0:3]
print("unPhone ?")
if unique_id_prefixes == b'00\xf9': # '30:30:F9'
return "unphone"
print("(emulated) lilygo_t_display_s3 ?")
if unique_id_prefixes == b'\x10\x01\x00' or unique_id_prefixes == b'\xc0\x4e\x30':
return "lilygo_t_display_s3" # display gets confused by the i2c stuff below
+12 -4
View File
@@ -14,6 +14,7 @@ if [ -z "$target" ]; then
echo "Example: $0 macOS"
echo "Example: $0 esp32"
echo "Example: $0 esp32s3"
echo "Example: $0 unphone"
exit 1
fi
@@ -97,12 +98,18 @@ popd
echo "Refreshing freezefs..."
"$codebasedir"/scripts/freezefs_mount_builtin.sh
if [ "$target" == "esp32" -o "$target" == "esp32s3" ]; then
if [ "$target" == "esp32" -o "$target" == "esp32s3" -o "$target" == "unphone" ]; then
partition_size="4194304"
flash_size="16"
extra_configs=""
if [ "$target" == "esp32" ]; then
if [ "$target" == "esp32" ]; then
BOARD=ESP32_GENERIC
BOARD_VARIANT=SPIRAM
else # esp32s3
else # esp32s3 or unphone
if [ "$target" == "unphone" ]; then
partition_size="3900000"
flash_size="8"
fi
BOARD=ESP32_GENERIC_S3
BOARD_VARIANT=SPIRAM_OCT
# These options disable hardware AES, SHA and MPI because they give warnings in QEMU: [AES] Error reading from GDMA buffer
@@ -131,7 +138,8 @@ if [ "$target" == "esp32" -o "$target" == "esp32s3" ]; then
# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
# CONFIG_ADC_MIC_TASK_CORE=1 because with the default (-1) it hangs the CPU
# CONFIG_SPIRAM_XIP_FROM_PSRAM: load entire firmware into RAM to reduce SD vs PSRAM contention (recommended at https://github.com/MicroPythonOS/MicroPythonOS/issues/17)
python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=$BOARD BOARD_VARIANT=$BOARD_VARIANT \
# python3 make.py --ota --partition-size=$partition_size --flash-size=$flash_size esp32 BOARD=$BOARD BOARD_VARIANT=$BOARD_VARIANT \
python3 make.py --optimize-size --partition-size=$partition_size --flash-size=$flash_size esp32 BOARD=$BOARD BOARD_VARIANT=$BOARD_VARIANT \
USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake \
USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake \
USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake \