Skip to content

Commit f3d3127

Browse files
bitcoin3usclaude
andcommitted
boards: Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 display and touch
The board file brings up the 4.3" 480x800 panel over 2-lane MIPI-DSI and the GT911 touch controller, replacing the stage-1 placeholder: - drivers/display/st7701_dsi: an ST7701 driver for lvgl_micropython's lcd_bus.DSIBus (video mode). The bus owns two screen-sized PSRAM frame buffers that LVGL renders into directly and the P4's DPI DMA scans out; the driver's job is the hardware reset and the panel's DCS register setup (Waveshare's vendor table, preceded by the bank-0 MADCTL/COLMOD writes Espressif's esp_lcd_st7701 sends), sent before the bus starts the video stream on the first flush. Rotation is not supported (no row/column exchange on the video interface); the board runs the panel in its native portrait orientation. - Backlight: LEDC PWM on GPIO26, active low (Waveshare's BSP drives its LEDC channel inverted), kept off until the first frame is out. - Touch: GT911 polled at whichever I2C address its strap selected (0x5D or 0x14); neither its reset nor its interrupt line is wired to the P4 here, so drivers/indev/gt911.py no longer tries to build a Pin from interrupt_pin=None. - build_mpos.sh (esp32p4): PSRAM at 200 MHz and the 256 KB L2 cache with 128-byte lines, as in Waveshare's and Espressif's P4 display examples. The panel is scanned out of PSRAM continuously (46 MB/s at 60 Hz) and MicroPython's default 20 MHz PSRAM clock cannot feed it. Needs lvgl_micropython's DSIBus (MicroPythonOS/lvgl_micropython#16), which main's lvgl_micropython pin includes since ed9e643. Verified on the board: DCS reads answer (RDDPM 0x9C, RDDMADCTL 0x00, RDDCOLMOD 0x50), LVGL renders the launcher into the panel buffers and full-screen refreshes complete in one frame period (14-17 ms), GT911 reports product 911 / firmware 0x1060 / 480x800, no DMA underruns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 688a6f7 commit f3d3127

6 files changed

Lines changed: 320 additions & 10 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# ST7701 panel driven over MIPI-DSI (video mode) through lvgl_micropython's
2+
# lcd_bus.DSIBus. Used by the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3.
3+
from .st7701_dsi import ST7701_DSI
4+
5+
__all__ = [
6+
'ST7701_DSI',
7+
]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ST7701 register setup for the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 panel
2+
# (480x800, 2-lane MIPI-DSI, RGB565). The bank/gamma/power/timing values are
3+
# the vendor's, transcribed from vendor_specific_init_default in Waveshare's
4+
# BSP (waveshare/esp32_p4_wifi6_touch_lcd_4_3 1.0.1, esp32_p4_wifi6_touch_lcd_4_3.c);
5+
# the first three writes are the bank-0 MADCTL/COLMOD setup Espressif's
6+
# esp_lcd_st7701 MIPI driver sends ahead of the vendor table.
7+
8+
import time
9+
10+
# (command, parameters or None, delay in ms after the command)
11+
_SEQ = (
12+
(0xFF, b'\x77\x01\x00\x00\x00', 0), # Command2 off: bank 0
13+
(0x36, b'\x00', 0), # MADCTL: RGB order, no mirroring
14+
(0x3A, b'\x55', 0), # COLMOD: 16 bits per pixel (RGB565)
15+
16+
(0xFF, b'\x77\x01\x00\x00\x13', 0), # Command2 BK3
17+
(0xEF, b'\x08', 0),
18+
19+
(0xFF, b'\x77\x01\x00\x00\x10', 0), # Command2 BK0
20+
(0xC0, b'\x63\x00', 0), # display line setting
21+
(0xC1, b'\x0D\x02', 0), # porch control
22+
(0xC2, b'\x17\x08', 0), # inversion + frame rate
23+
(0xCC, b'\x10', 0),
24+
(0xB0, b'\x40\xC9\x94\x0E\x10\x05\x0B\x09\x08\x26\x04\x52\x10\x69\x6B\x69', 0), # positive gamma
25+
(0xB1, b'\x40\xD2\x98\x0C\x92\x07\x09\x08\x07\x25\x02\x0E\x0C\x6E\x78\x55', 0), # negative gamma
26+
27+
(0xFF, b'\x77\x01\x00\x00\x11', 0), # Command2 BK1
28+
(0xB0, b'\x5D', 0), # VOP amplitude
29+
(0xB1, b'\x4E', 0), # VCOM amplitude
30+
(0xB2, b'\x87', 0), # VGH
31+
(0xB3, b'\x80', 0),
32+
(0xB5, b'\x4E', 0), # VGL
33+
(0xB7, b'\x85', 0), # power control 1
34+
(0xB8, b'\x21', 0), # power control 2
35+
(0xB9, b'\x10\x1F', 0),
36+
(0xBB, b'\x03', 0),
37+
(0xBC, b'\x00', 0),
38+
(0xC1, b'\x78', 0), # source pre-drive timing
39+
(0xC2, b'\x78', 0), # source EQ2
40+
(0xD0, b'\x88', 0), # MIPI setting 1
41+
(0xE0, b'\x00\x3A\x02', 0),
42+
(0xE1, b'\x04\xA0\x00\xA0\x05\xA0\x00\xA0\x00\x40\x40', 0),
43+
(0xE2, b'\x30\x00\x40\x40\x32\xA0\x00\xA0\x00\xA0\x00\xA0\x00', 0),
44+
(0xE3, b'\x00\x00\x33\x33', 0),
45+
(0xE4, b'\x44\x44', 0),
46+
(0xE5, b'\x09\x2E\xA0\xA0\x0B\x30\xA0\xA0\x05\x2A\xA0\xA0\x07\x2C\xA0\xA0', 0),
47+
(0xE6, b'\x00\x00\x33\x33', 0),
48+
(0xE7, b'\x44\x44', 0),
49+
(0xE8, b'\x08\x2D\xA0\xA0\x0A\x2F\xA0\xA0\x04\x29\xA0\xA0\x06\x2B\xA0\xA0', 0),
50+
(0xEB, b'\x00\x00\x4E\x4E\x00\x00\x00', 0),
51+
(0xEC, b'\x08\x01', 0),
52+
(0xED, b'\xB0\x2B\x98\xA4\x56\x7F\xFF\xFF\xFF\xFF\xF7\x65\x4A\x89\xB2\x0B', 0),
53+
(0xEF, b'\x08\x08\x08\x45\x3F\x54', 0),
54+
55+
(0xFF, b'\x77\x01\x00\x00\x00', 0), # back to bank 0
56+
(0x11, None, 120), # SLPOUT, then the mandatory 120 ms
57+
(0x29, None, 0), # DISPON
58+
)
59+
60+
61+
def init(self):
62+
for cmd, params, delay in _SEQ:
63+
if params is None:
64+
self.set_params(cmd)
65+
else:
66+
self.set_params(cmd, params)
67+
if delay:
68+
time.sleep_ms(delay) # NOQA
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import time
2+
3+
from micropython import const # NOQA
4+
import lvgl as lv
5+
import display_driver_framework
6+
7+
STATE_HIGH = display_driver_framework.STATE_HIGH
8+
STATE_LOW = display_driver_framework.STATE_LOW
9+
STATE_PWM = display_driver_framework.STATE_PWM
10+
11+
BYTE_ORDER_RGB = display_driver_framework.BYTE_ORDER_RGB
12+
BYTE_ORDER_BGR = display_driver_framework.BYTE_ORDER_BGR
13+
14+
_RDDID = const(0x04) # Read Display ID
15+
16+
17+
class ST7701_DSI(display_driver_framework.DisplayDriver):
18+
"""ST7701 over MIPI-DSI in video mode (lcd_bus.DSIBus).
19+
20+
The DSI bus owns two screen-sized frame buffers that LVGL renders into
21+
directly and the SoC's DPI DMA scans out continuously, so unlike the
22+
SPI/I80 panels there is no window addressing (CASET/RASET/RAMWR) and no
23+
per-flush pixel transfer here. What this driver does is bring the panel
24+
up: hardware reset, then the DCS register setup (bank selects, gamma,
25+
power, timing, SLPOUT, DISPON) over the same DSI link. The bus starts
26+
the video stream on the first flush, i.e. after that setup, the order
27+
Espressif's own esp_lcd_st7701 MIPI driver uses.
28+
29+
Rotation is not supported: the ST7701 has no row/column exchange for
30+
the video interface, so the display runs in the panel's native
31+
orientation.
32+
"""
33+
34+
STATE_HIGH = STATE_HIGH
35+
STATE_LOW = STATE_LOW
36+
STATE_PWM = STATE_PWM
37+
BYTE_ORDER_RGB = BYTE_ORDER_RGB
38+
BYTE_ORDER_BGR = BYTE_ORDER_BGR
39+
40+
_INVOFF = 0x20 # Color Inversion Off
41+
_INVON = 0x21 # Color Inversion On
42+
43+
def __init__(
44+
self,
45+
data_bus,
46+
display_width,
47+
display_height,
48+
frame_buffer1,
49+
frame_buffer2=None,
50+
reset_pin=None,
51+
reset_state=STATE_LOW,
52+
power_pin=None,
53+
power_on_state=STATE_HIGH,
54+
backlight_pin=None,
55+
backlight_on_state=STATE_HIGH,
56+
color_byte_order=BYTE_ORDER_RGB,
57+
color_space=lv.COLOR_FORMAT.RGB565,
58+
):
59+
self.panel_id = None
60+
61+
super().__init__(
62+
data_bus=data_bus,
63+
display_width=display_width,
64+
display_height=display_height,
65+
frame_buffer1=frame_buffer1,
66+
frame_buffer2=frame_buffer2,
67+
reset_pin=reset_pin,
68+
reset_state=reset_state,
69+
power_pin=power_pin,
70+
power_on_state=power_on_state,
71+
backlight_pin=backlight_pin,
72+
backlight_on_state=backlight_on_state,
73+
offset_x=0,
74+
offset_y=0,
75+
color_byte_order=color_byte_order,
76+
color_space=color_space,
77+
rgb565_byte_swap=False,
78+
_cmd_bits=8,
79+
_param_bits=8,
80+
_init_bus=True, # creates the DSI bus, the DBI command IO and the DPI panel (frame buffers)
81+
)
82+
83+
def init(self, type=None): # NOQA
84+
# Hardware reset: reset_state is the active level (the framework
85+
# holds it for 120 ms), then give the controller time to come up.
86+
self.reset()
87+
time.sleep_ms(120) # NOQA
88+
89+
# The ID read is the first traffic over the link, a cheap check that
90+
# the panel answers before the long register setup.
91+
try:
92+
buf = bytearray(3)
93+
self.get_params(_RDDID, buf)
94+
self.panel_id = bytes(buf)
95+
except Exception as e: # NOQA
96+
print('ST7701_DSI: panel ID read failed: %s' % e)
97+
98+
from . import _st7701_dsi_init
99+
_st7701_dsi_init.init(self)
100+
101+
# No window to set: the whole frame buffer is what the panel shows.
102+
self._initilized = True
103+
104+
def set_params(self, cmd, params=None):
105+
self._data_bus.tx_param(cmd, params)
106+
107+
def get_params(self, cmd, params):
108+
self._data_bus.rx_param(cmd, params)
109+
110+
def _set_memory_location(self, x1, y1, x2, y2): # NOQA
111+
# video-mode panel: no CASET/RASET/RAMWR
112+
return -1
113+
114+
def _on_size_change(self, _):
115+
# Track LVGL's idea of the size only; MADCTL cannot rotate this panel.
116+
self._rotation = self._disp_drv.get_rotation()
117+
self._width = self._disp_drv.get_horizontal_resolution()
118+
self._height = self._disp_drv.get_vertical_resolution()

‎internal_filesystem/lib/drivers/indev/gt911.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ def __init__(
9191

9292
if isinstance(interrupt_pin, int) and _USE_INTERRUPTS:
9393
interrupt_pin = machine.Pin(interrupt_pin, machine.Pin.IN)
94-
else:
94+
elif isinstance(interrupt_pin, int):
9595
interrupt_pin = machine.Pin(interrupt_pin, machine.Pin.OUT)
96+
# else: INT not wired to the MCU (e.g. Waveshare ESP32-P4 4.3), leave it None
9697

9798
self._reset_pin = reset_pin
9899
self._interrupt_pin = interrupt_pin

‎internal_filesystem/lib/mpos/board/waveshare_esp32_p4_wifi6_touch_lcd_4_3.py‎

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,124 @@
22

33
logger = logging.getLogger(__name__)
44

5-
# Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3 — bring-up stage 1 (#307):
6-
# ESP32-P4 + ESP32-C6 Wi-Fi co-processor over SDIO (handled by MicroPython's
7-
# C6_WIFI board variant), 4.3" 480x800 MIPI-DSI panel (ST7701, 2 lanes),
8-
# GT911 touch on I2C (SDA=7, SCL=8), ES8311/ES7210 audio, SD card, camera.
5+
if __debug__: logger.debug("waveshare_esp32_p4_wifi6_touch_lcd_4_3.py initialization")
6+
# Hardware initialization for the Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3:
7+
# ESP32-P4 (RISC-V) with an ESP32-C6 Wi-Fi 6 / BLE co-processor over SDIO
8+
# (esp_hosted), 32 MB flash, 32 MB PSRAM, 4.3" 480x800 portrait IPS over
9+
# 2-lane MIPI-DSI (ST7701), GT911 capacitive touch, ES8311 codec + ES7210
10+
# microphone ADC, TF card slot (4-bit SDIO), MIPI-CSI camera connector,
11+
# USB host port.
912
#
10-
# The MIPI-DSI display needs a DSI bus in lvgl_micropython that is not
11-
# wired up yet, so this stage registers no display: MicroPythonOS stops at
12-
# init_rootscreen() and leaves the REPL usable for the Wi-Fi/boot work.
13-
# Display, touch, audio follow in later stages.
13+
# Manufacturer's wiki: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4.3
14+
# Pins, panel timings and the ST7701 register table come from Waveshare's
15+
# ESP-IDF BSP (waveshare/esp32_p4_wifi6_touch_lcd_4_3 1.0.1).
16+
#
17+
# The panel is used in its native portrait orientation (480 wide, 800 high):
18+
# the ST7701 cannot swap rows and columns for the video interface, and the
19+
# enclosure's cable/button placement suits portrait.
20+
21+
import time
22+
23+
import i2c
24+
import lcd_bus
25+
import lvgl as lv
26+
import machine
27+
import mpos.ui
28+
from drivers.display.st7701_dsi import ST7701_DSI
29+
import drivers.indev.gt911 as gt911
30+
from mpos import InputManager
31+
32+
LCD_WIDTH = 480
33+
LCD_HEIGHT = 800
34+
LCD_RST = 27
35+
LCD_BL = 26 # backlight enable, PWM dimmable, ACTIVE LOW (the BSP drives its LEDC channel inverted)
36+
37+
I2C_SDA = 7
38+
I2C_SCL = 8
39+
I2C_FREQ = 400000
40+
41+
TOUCH_ADDRS = (0x5D, 0x14) # GT911; the strap at reset picks one
42+
43+
# === BACKLIGHT ===
44+
# Kept off (pin high) until the panel shows its first frame, so the boot does
45+
# not flash a white screen.
46+
_backlight = machine.PWM(machine.Pin(LCD_BL), freq=5000, duty_u16=65535)
47+
_backlight_percent = 0
48+
49+
50+
def _set_backlight(percent):
51+
global _backlight_percent
52+
percent = max(0, min(100, int(percent)))
53+
_backlight_percent = percent
54+
_backlight.duty_u16(65535 - percent * 65535 // 100) # active low: 0% = pin high
55+
56+
57+
def _get_backlight():
58+
return _backlight_percent
59+
60+
61+
# === DISPLAY (ST7701 over MIPI-DSI) ===
62+
# Video mode: the DPI DMA scans two screen-sized frame buffers (in PSRAM) out
63+
# to the panel continuously, LVGL renders straight into them. The DSI PHY is
64+
# powered by the P4's internal LDO channel 3.
65+
display_bus = lcd_bus.DSIBus(
66+
bus_id=0,
67+
data_lanes=2,
68+
freq=500, # lane bit rate, Mbps
69+
dpi_clock_freq=30, # DPI pixel clock, MHz
70+
virtual_channel=0,
71+
hsync_back_porch=42,
72+
hsync_pulse_width=12,
73+
hsync_front_porch=42,
74+
vsync_back_porch=2,
75+
vsync_pulse_width=8,
76+
vsync_front_porch=60,
77+
phy_ldo_channel=3,
78+
phy_ldo_voltage_mv=2500,
79+
)
80+
81+
_FB_SIZE = const(480 * 800 * 2) # RGB565, whole screen: 768000 bytes
82+
fb1 = display_bus.allocate_framebuffer(_FB_SIZE, lcd_bus.MEMORY_SPIRAM)
83+
fb2 = display_bus.allocate_framebuffer(_FB_SIZE, lcd_bus.MEMORY_SPIRAM)
84+
85+
mpos.ui.main_display = ST7701_DSI(
86+
data_bus=display_bus,
87+
frame_buffer1=fb1,
88+
frame_buffer2=fb2,
89+
display_width=LCD_WIDTH,
90+
display_height=LCD_HEIGHT,
91+
reset_pin=LCD_RST,
92+
reset_state=ST7701_DSI.STATE_LOW,
93+
color_space=lv.COLOR_FORMAT.RGB565,
94+
color_byte_order=ST7701_DSI.BYTE_ORDER_RGB,
95+
) # triggers lv.init()
96+
mpos.ui.main_display.init()
97+
if __debug__: logger.debug("ST7701 panel ID: %s", mpos.ui.main_display.panel_id)
98+
99+
mpos.ui.main_display.set_backlight = _set_backlight
100+
mpos.ui.main_display.get_backlight = _get_backlight
101+
102+
# Push a first frame now: the bus starts the DSI video stream on the first
103+
# flush, and the backlight only goes on once the panel has something to show.
104+
try:
105+
lv.refr_now(None)
106+
except Exception as e:
107+
logger.error("initial refresh failed: %s" % (e))
108+
mpos.ui.main_display.set_backlight(100)
109+
110+
# === TOUCH (GT911) ===
111+
# Neither its reset nor its interrupt line is wired to the P4 on this board,
112+
# so the controller is polled at whichever address its strap selected.
113+
i2c_bus = i2c.I2C.Bus(host=0, scl=I2C_SCL, sda=I2C_SDA, freq=I2C_FREQ, use_locks=False)
114+
try:
115+
present = i2c_bus.scan()
116+
touch_addr = next((a for a in TOUCH_ADDRS if a in present), None)
117+
if touch_addr is None:
118+
raise RuntimeError("GT911 not found on I2C (devices: %s)" % [hex(a) for a in present])
119+
touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=touch_addr, reg_bits=gt911.BITS)
120+
indev = gt911.GT911(touch_dev)
121+
InputManager.register_indev(indev)
122+
except Exception as e:
123+
logger.error("GT911 init failed: %s" % (e))
14124

15-
logger.warning("Waveshare ESP32-P4-WIFI6-Touch-LCD-4.3: stage-1 board file, no display driver yet")
125+
if __debug__: logger.debug("waveshare_esp32_p4_wifi6_touch_lcd_4_3.py finished")

‎scripts/build_mpos.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,12 @@ if [ "$target" == "esp32" -o "$target" == "esp32s3" -o "$target" == "unphone" -o
400400
BOARD_VARIANT=C6_WIFI
401401
partition_size=4194304
402402
flash_size="32"
403+
# PSRAM at 200 MHz (ESP-IDF 5.5 still labels that speed experimental)
404+
# and the 256 KB L2 cache with 128-byte lines, as in Waveshare's and
405+
# Espressif's P4 display examples: the MIPI-DSI panel is scanned out
406+
# of PSRAM continuously (480x800 RGB565 at 60 Hz is 46 MB/s) and
407+
# MicroPython's default 20 MHz PSRAM clock cannot feed it.
408+
extra_configs="CONFIG_IDF_EXPERIMENTAL_FEATURES=y CONFIG_SPIRAM_SPEED_200M=y CONFIG_CACHE_L2_CACHE_256KB=y CONFIG_CACHE_L2_CACHE_LINE_128B=y"
403409
else # esp32s3 or unphone
404410
if [ "$target" == "unphone" ]; then
405411
flash_size="8"

0 commit comments

Comments
 (0)