Skip to content

feat: add Hosyond / LCDWiki 3.5" ESP32-S3 Display (ES3C35P, ST77922 QSPI) board - #2950

Open
Tiporari wants to merge 7 commits into
BruceDevices:devfrom
Tiporari:feat/hosyond-35-s3
Open

Tiporari wants to merge 7 commits into
BruceDevices:devfrom
Tiporari:feat/hosyond-35-s3

Conversation

@Tiporari

@Tiporari Tiporari commented Oct 4, 2026

Copy link
Copy Markdown

Proposed Changes

Adds a new board, env hosyond-35-s3, for the LCDWiki "3.5inch ESP32-S3 Display" (models ES3C35P with speaker / ES3C35P-NS without): ESP32-S3 (16 MB flash, 8 MB OPI PSRAM), 320x480 IPS panel on an ST77922 QSPI controller with its own I2C touch, microSD (4-bit SDMMC), ES8311 codec + amplifier, WS2812B RGB LED, LiPo charger.

Hardware: https://www.lcdwiki.com/3.5inch_ESP32-S3_Display (docs, schematic, datasheets, vendor demos). Example listing: https://www.amazon.com/dp/B0H28X8SQ4

The panel needed a custom LovyanGFX panel class (boards/hosyond-35-s3/lgfx_st77922.h); there is no ST77922 driver in LovyanGFX. See the quirks below, they are the interesting part.

Shared-code changes (all small, off by default):

  • lib/HAL/display/lovyan.{h,cpp}: a LOVYAN_QSPI_BUS branch (LovyanGFX Bus_SPI with pin_io0..io3) and a LOVYAN_CUSTOM_PANEL_HEADER include hook for board-supplied panel classes. Only active when those macros are defined; no other board in the tree uses LovyanGFX.
  • src/core/configPins.h: RF_TX_DEFAULT_PIN / RF_RX_DEFAULT_PIN, overridable defaults. The default is unchanged (GROVE_SDA / GROVE_SCL), so existing boards behave exactly as before.
  • platformio.ini: a commented ;hosyond-35-s3 entry in the env list.

Types of Changes

New feature (new board). No breaking changes.

Verification

Hand-tested on real hardware (one unit, 1000 mAh LiPo attached):

  • Display: boot splash, main menu, text and icons render correctly in landscape; colors correct without the invert menu
  • Touch: taps and swipes, correct in landscape
  • microSD (4-bit SDMMC): mounts, files listed
  • Wi-Fi and Bluetooth
  • Backlight PWM / brightness menu, dim timeout and wake
  • WS2812B LED and Bruce's LED menu
  • Audio: tone and say audible on a connected speaker
  • Battery indicator tracks the cell (see quirks)
  • IR/RF pin defaults land on IO45/IO46 and all SPI radio buses are unassigned (checked in the boot config dump)

Not tested: microphone (not implemented), actual IR/RF hardware on IO45/IO46, display clocks above 40 MHz, other boards beyond a compile check of m5stack-cardputer (see Testing).

Build: pio run -e hosyond-35-s3, then flash with esptool.

Testing

No automated tests; this is hardware-bound. I tried compiling m5stack-cardputer to check the shared configPins.h change against another board. It fails here, but every error is inside FastLED's own sources (animartrix_detail/*, fft_impl), none in Bruce or this PR. Cause: that board's ini passes -DFP=1 -DFM=2 -DFG=3 (18 board inis do), while FastLED 3.10.5, which fastled/FastLED @^3.10.3 now resolves to, uses FP as a type alias (using FP = fl::s16x16). Not caused by this PR; flagging it in case CI hits it. This board avoids it by defining FP/FM/FG as static const uint8_t in its variant header instead of macros.

Linked Issues

None.

User-Facing Change

Added support for the Hosyond / LCDWiki 3.5" ESP32-S3 Display (ES3C35P, ST77922 QSPI).

Further Comments

Board quirks worth knowing (also in boards/hosyond-35-s3/README.md):

  1. ST77922 needs 4-pixel-aligned draw windows. Window x start and width must be multiples of 4 pixels (the vendor demo trims every fill the same way). Arbitrary windows (text glyphs, icons, 1 px lines) corrupt the raster (interlaced/offset smear, tinted text), while aligned solid fills look fine and hide the problem. Panel_ST77922 therefore renders into a PSRAM framebuffer (Panel_FrameBufferBase) and flushes the dirty rectangle widened to x%4. Rotation is done in software, so the panel always stays at its native portrait MADCTL.
  2. Pixel format is plain RGB565. COLMOD 0x01 and 0x55 behave the same. MADCTL bit 3 swaps R/B, so TFT_RGB_ORDER=1. INVON is the panel's natural state, so the panel class flips setInvert and the ini uses TFT_INVERTION=0.
  3. Touch (ST77922 I2C @ 0x55, SDA 38 / SCL 39). Report slots are 7 bytes, 5 slots from register 0x0014. The first byte is a C bitfield: valid is bit 7 and x_h is bits 5:0 (easy to get backwards, it made every touch look invalid). Coordinates are portrait-native panel pixels. The bus is shared with the ES8311 codec (0x18) and the external I2C header; an unidentified device also answers at 0x28.
  4. Audio. ES8311 (I2C 0x18) feeding an SC8002B/FM8002E amplifier whose shutdown pin (AUDIO_EN, GPIO1) is active low and defaults to disabled. Bruce's sound features are gated on HAS_NS4168_SPKR (plus BCLK/WCLK/DOUT/MCLK), and the board provides _setup_codec_speaker() to drive the amp enable and program the codec (adapted from the ES3C28P port). The microphone path is not implemented.
  5. Almost no free GPIO. Only IO45 / IO46, the shared I2C pair and the UART header (43/44) are exposed; the display, SD (SDMMC), audio, touch and LED consume the rest. No SPI bus is available, so SPI radios (CC1101, NRF24, W5500, ...) cannot be attached. The board leaves every radio/SD-SPI bus and the generic SPI pins unassigned (-1) so no menu can reconfigure the display bus, and maps IR TX/RX to IO45/IO46 (RF shares those two). IO45/IO46 are strapping pins; wire an IR LED to ground and the receiver output on IO46.
  6. Battery. The ADC on GPIO8 reads BAT+ through a 100k/100k divider. The code uses calibrated millivolts, a 16-sample average, an EMA and a Li-ion curve, and floors a valid reading at 1% so Bruce (which hides the indicator at 0%) always shows it. It reads terminal voltage, so it is load sensitive (LED and backlight cost ~100 mV of sag on a small cell) and reads ~4.1 V while the TP4054 is charging. There is no VBUS-sense or charge-status GPIO, so charging cannot be detected.
  7. LED. Single WS2812B (3-channel GRB) on GPIO40, fed from the 5 V rail, which is battery-fed when running without USB.
  8. Build/flash gotchas (Windows). pio run -t upload can crash on cp1252 consoles, so flash with esptool directly. PlatformIO's build cache returned a stale firmware.bin once; build with a fresh PLATFORMIO_BUILD_CACHE_DIR if results look wrong. patch.py needs PlatformIO's Scripts directory on PATH for its nested pio pkg exec. Bruce stores rotation, inversion and pins in flash (/brucePins.conf), so erase flash or delete that file when testing defaults. A tiny serial-output I2C bench was far faster for bringing up touch than debugging inside Bruce.
  9. HOSYOND_TEST_PATTERN (off by default) draws a geometry test card at boot, useful after display changes.

🤖 Generated with Claude Code

Tiporari and others added 7 commits October 3, 2026 21:26
First hardware bring-up. The ST77922 only draws correctly with draw windows
aligned to 4 pixels in x, so Panel_ST77922 renders into a PSRAM framebuffer
and flushes 4-aligned dirty rectangles (software rotation). Adds a QSPI bus
branch to the LovyanGFX HAL and a custom-panel-header hook. Touch protocol
follows the vendor driver but is not yet verified on hardware.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The valid flag is bit 7 and x_h is bits 5:0 of the first report byte (C
bitfield layout), not bit 0 / bits 7:2, so every touch was discarded. Scan
all five report slots and use the vendor 100 ms reset pulse. Verified on
hardware.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… ES8311 audio

- Define HAS_RGB_LED and LED type/order/count (WS2812B GRB, 1 pixel).
- Battery: calibrated ADC with averaging and a Li-ion voltage curve.
- Audio: use Bruce's HAS_NS4168_SPKR/ES8311_CODEC macros, drive the active-low
  amp enable on GPIO1, and add the ES8311 speaker setup hook. Verified audible
  with tone/say; microphone path not implemented.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Verified against a direct LiPo cell: the pin tracks terminal voltage
(~4.1 V charging, ~3.65 V loaded, ~3.75 V with LED off). Add an EMA to
tame load-induced sag and floor valid readings at 1% so the status bar
indicator is always drawn.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…IO45/IO46

No SPI bus is exposed on this board, so leave the generic SPI and every
radio/SD-SPI pin unassigned instead of aliasing the QSPI display pins. Map IR
TX/RX to the two spare GPIOs. Add RF_TX/RX_DEFAULT_PIN overrides in
configPins.h (default unchanged) so RF no longer defaults to the I2C pair
that carries touch and the audio codec.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@bmorcelli
bmorcelli changed the base branch from main to dev October 8, 2026 17:16
@bmorcelli

Copy link
Copy Markdown
Member

Porting method has changed. these Json files and pins_arduino.h are not required (and should not be changed)

display is using DisplayDrivers lib.

use dev brench

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants