Issue: SirLefti/Python_ILI9486#6
Author: @craigerl
Date: April 27, 2026
Labels: enhancement, help wanted
The MHS35b is a popular, cheaper ILI9486-based 3.5" TFT display that is increasingly used as an alternative to the harder-to-find Waveshare ILI9486. The SirLefti Python_ILI9486 driver was written for the Waveshare variant.
@craigerl decompiled the device tree overlay from the goodtft/LCD-show project and posted the full DTS source, hoping it reveals what tweaks are needed to get the MHS35b working with this Python driver.
@SirLefti (maintainer) — Apr 27, 2026:
Hi mate, long time no see. I am not sure how I can work on that, because I do not own that display module. If you got a working patch, I'd be probably able to assist you in bringing it together in the same code base, if it makes sense to do so. I think this is related to #4 right? I developed this lib using the ILI9486 spec sheet. I see an init sequence in the file you shared, but I am not sure what it tells me.
@craigerl — Apr 27, 2026:
Thanks again for all the support here - great work. I just noticed the string of bytes for "Initialize" was very similar (almost identical) in both the dtoverlay and your ili9486 driver here. I'll keep poking at it, but will otherwise leave the dtoverlay here in case somebody thinks of a way to integrate it with this project.
@SirLefti (maintainer) — Apr 27, 2026:
Yes, I see some similarities. Do you see the
0x10000e0and0x10000e1with their following data?e0ande1are the positive and negative gamma control commands (PGAMCTL and NGAMCTL) in ILI9486. They are most likely the same thing here as well. But the other commands not known to me, and they do not appear in the spec sheet I used to implement the commands back then.If you want to try your luck, all
0x10000..bytes are most likely commands, the shorter ones are data. You can change the init sequence and see if it works. For the commands, you can maybe use just the last two digits and ignore the long0x10000part, not sure about that.
@snakefood3232 — Apr 27, 2026:
The device tree overlay you mentioned for the MHS35b ILI9486 variant has similarities that could be leveraged to tweak the existing driver. I'd handle this by examining the initialization command bytes and adjusting the driver accordingly to accommodate these differences. Can have a PR up within 2 days. I've worked on similar integrations with SPI-based displays, ensuring compatibility across various hardware variations.
@craigerl — Apr 27, 2026:
I thought you might have some good insights. I went a few bytes into the init string on the dtoverlay and started picking out LOTS of consecutive similarities. I can send you a screen too.
- SirLefti doesn't own an MHS35b — cannot test, but willing to merge a working patch
- SirLefti correctly identified that
0x10000XX= command0xXXin fbtft format - SirLefti noted the 0xF1/F2/F8/F9 registers are not in the official ILI9486L spec sheet — they're manufacturer-specific
- Related to issue #4 — likely another display variant compatibility request
- @snakefood3232 volunteered a PR within 2 days (status unknown)
The DTS init property uses the fbtft format where:
0x1000000X= send command0xXX- Following bare bytes = data for that command
0x20000XX= delay ofXXms
| Step | Command | Name | Data |
|---|---|---|---|
| 1 | 0xF1 |
Manufacturer-specific | [0x36, 0x04, 0x00, 0x3C, 0x0F, 0x8F] |
| 2 | 0xF2 |
Manufacturer-specific | [0x18, 0xA3, 0x12, 0x02, 0xB2, 0x12, 0xFF, 0x10, 0x00] |
| 3 | 0xF8 |
Manufacturer-specific | [0x21, 0x04] |
| 4 | 0xF9 |
Manufacturer-specific | [0x00, 0x08] |
| 5 | 0x36 |
MADCTL (Memory Access Control) | [0x08] |
| 6 | 0xB4 |
Display Inversion Control | [0x00] |
| 7 | 0xC1 |
Power Control 2 | [0x41] |
| 8 | 0xC5 |
VCOM Control | [0x00, 0x91, 0x80, 0x00] |
| 9 | 0xE0 |
Positive Gamma Control | [0x0F, 0x1F, 0x1C, 0x0C, 0x0F, 0x08, 0x48, 0x98, 0x37, 0x0A, 0x13, 0x04, 0x11, 0x0D, 0x00] |
| 10 | 0xE1 |
Negative Gamma Control | [0x0F, 0x32, 0x2E, 0x0B, 0x0D, 0x05, 0x47, 0x75, 0x37, 0x06, 0x10, 0x03, 0x24, 0x20, 0x00] |
| 11 | 0x3A |
Pixel Format | [0x55] (16-bit RGB565) |
| 12 | 0x11 |
Sleep Out | — |
| 13 | 0x36 |
MADCTL | [0x28] (landscape, BGR) |
| 14 | — | Delay | 255 ms |
| 15 | 0x29 |
Display On | — |
From SirLefti/Python_ILI9486 — the _init_sequence() method:
| Step | Command | Name | Data |
|---|---|---|---|
| 1 | 0xB0 |
Interface Mode Control | [0x00] |
| 2 | 0x11 |
Sleep Out | — |
| 3 | — | Delay | 20 ms |
| 4 | 0x3A |
Pixel Format | [0x66] (18-bit RGB666) |
| 5 | 0x36 |
MADCTL | [origin] (default 0x28) |
| 6 | 0xC2 |
Power Control Normal | [0x44] |
| 7 | 0xC5 |
VCOM Control | [0x00, 0x00, 0x00, 0x00] |
| 8 | 0xE0 |
Positive Gamma Control | [0x0F, 0x1F, ...] |
| 9 | 0xE1 |
Negative Gamma Control | [0x0F, 0x1B, ...] |
| 10 | 0x29 |
Display On | — |
| Register | MHS35b (DTS) | Waveshare (driver) | Impact |
|---|---|---|---|
| 0x3A (Pixel Format) | 0x55 — 16-bit RGB565 |
0x66 — 18-bit RGB666 |
CRITICAL — completely different pixel data packing |
| Power Control | 0xC1 → [0x41] |
0xC2 → [0x44] |
Different register entirely |
| 0xC5 (VCOM) | [0x00, 0x91, 0x80, 0x00] |
[0x00, 0x00, 0x00, 0x00] |
Affects contrast/voltage levels |
| 0xF1, 0xF2, 0xF8, 0xF9 | Manufacturer-specific tuning bytes | Not used | Panel calibration — likely required |
| 0xB4 (Inversion Ctrl) | [0x00] |
Not set | |
| 0xB0 (Interface Mode) | Not set | [0x00] |
|
| Gamma (0xE0/0xE1) | Different curve values | Different curve values | Color tuning |
| MADCTL (0x36) | Final value 0x28 |
Default 0x28 |
Same — orientation is compatible |
The primary blocker is the pixel format mismatch:
- Waveshare uses 18-bit RGB666 (
0x66): each pixel is 3 bytes (6 bits per channel, MSB-aligned with 2 LSBs masked) - MHS35b expects 16-bit RGB565 (
0x55): each pixel is 2 bytes (5-red, 6-green, 5-blue packed)
The current driver's image_to_data() function produces 666-format data:
# Current: masks to 6-bit per channel, outputs 3 bytes/pixel
np.dstack((pb[:, :, 0] & 0xFC, pb[:, :, 1] & 0xFC, pb[:, :, 2] & 0xFC)).flatten().tolist()For MHS35b, this would need to produce 565-format data:
# Needed: pack into 2 bytes/pixel as RGB565
color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
# Then split into high byte and low byteThe secondary blockers are the missing manufacturer-specific registers (0xF1, 0xF2, 0xF8, 0xF9) and different power/VCOM settings that the MHS35b panel requires for proper operation.
The driver already marks _init_sequence() as protected for exactly this use case:
class ILI9486_MHS35b(ILI9486):
def _init_sequence(self):
# MHS35b-specific init with 0xF1/F2/F8/F9 registers
self.command(0xF1).data([0x36, 0x04, 0x00, 0x3C, 0x0F, 0x8F])
self.command(0xF2).data([0x18, 0xA3, 0x12, 0x02, 0xB2, 0x12, 0xFF, 0x10, 0x00])
self.command(0xF8).data([0x21, 0x04])
self.command(0xF9).data([0x00, 0x08])
self.command(CMD_SLPOUT)
time.sleep(0.255)
self.command(CMD_PXLFMT).data(0x55) # RGB565
self.command(CMD_MADCTL).data(self._origin.value)
self.command(0xB4).data(0x00)
self.command(0xC1).data(0x41)
self.command(CMD_VCOMCTL).data([0x00, 0x91, 0x80, 0x00])
self.command(CMD_PGAMCTL).data([...])
self.command(CMD_NGAMCTL).data([...])
self.command(CMD_DISPON)Add a variant='waveshare'|'mhs35b' parameter to __init__ that selects the init sequence and pixel format converter.
- New pixel conversion function —
image_to_data_565()for RGB565 packing (2 bytes/pixel vs 3) - Alternate init sequence — the MHS35b register writes
- SPI throughput — RGB565 is 33% less data per frame (2 vs 3 bytes/pixel), so the MHS35b should actually be faster
From the overlay, the MHS35b uses:
- SPI bus 0, chip select 0 (
reg = <0x00>) - Reset GPIO: BCM pin 25 (active low)
- DC GPIO: BCM pin 24 (active high)
- Bus width: 8-bit
- Register width: 16-bit
- Compatible:
ilitek,ili9486(same chip, different panel tuning) - SPI speed: configurable via overlay parameter
These are the same pins the Waveshare uses by default, so no wiring changes needed.