Wrong colors on ILI9488 with 8-bit parallel (I80) interface since uDisplay refactor
After upgrading from Tasmota 15.0.1.1 to 15.3.0.3, the ILI9488 display connected via 8-bit parallel (I80) interface on an ESP32-S3 shows wrong colors: blue appears as green, and the overall color palette is severely reduced (appears to be ~16 colors instead of 65K). The same display.ini worked perfectly on Tasmota 15.0.1.1.
SPI-connected ILI9488 displays are not affected — only the parallel (I80) interface path is broken.
Symptoms:
| Symptom |
Detail |
| Blue → Green |
Pure blue backgrounds appear green |
| Reduced colors |
Display looks like ~16 colors instead of 65K (RGB565) |
| LVGL Mirror |
Shows correct colors (confirms LVGL renders correctly) |
| SPI display |
Same ILI9488 via SPI on ESP32-D0WD works correctly |
| Tasmota 15.0.1.1 |
Worked perfectly with identical display.ini |
| Tasmota 15.3.0.3 |
Broken (both master and development branch) |
Root Cause:
The uDisplay driver was refactored in PR #24007 (Oct 2025), splitting the monolithic uDisplay.cpp into panel-specific files. During this refactoring, the byte order logic for 8-bit parallel (I80) pixel pushing was inverted.
The bug is in I80Panel::pushColors() in lib/lib_display/UDisplay/src/uDisplay_I80_panel.cpp, line ~264:
bool I80Panel::pushColors(uint16_t *data, uint32_t len, bool swapped) {
pb_pushPixels(data, len, !swapped, false); // <-- BUG: the '!' inverts byte order
return true;
}
The call chain:
- LVGL flush calls
uDisplay::pushColors(data, len, not_swapped=true) — comment in uDisplay_graphics.cpp: "not_swapped is always true in call from LVGL driver!!!!"
uDisplay::pushColors calls universal_panel->pushColors(data, len, not_swapped=true)
I80Panel::pushColors(data, len, swapped=true) calls pb_pushPixels(data, len, !true=false, false)
pb_pushPixels with swap_bytes=false sends: low byte first, then high byte — but ILI9488 expects high byte first
In the old monolithic uDisplay.cpp (15.0.1.1), the PAR8 path always used pb_pushPixels(data, len, true, false) with swap_bytes=true, producing the correct byte order.
Byte order on the 8-bit bus for RGB565 — Pure Blue (0x001F):
| Code path |
1st byte on bus |
2nd byte on bus |
Display sees |
| Old code (correct) |
0x00 (RRRRRGGG) |
0x1F (GGGBBBBB) |
R=0, G=0, B=31 → Blue |
| New code (broken) |
0x1F (GGGBBBBB) |
0x00 (RRRRRGGG) |
R=0, G=56, B=0 → Green |
Note: The comment on line ~264 says "swap_bytes=true to match old driver", which confirms the intent was to have swap_bytes=true — but the ! negation produces the opposite result.
Proposed Fix:
In lib/lib_display/UDisplay/src/uDisplay_I80_panel.cpp, line ~264, remove the ! negation:
// Before (broken):
pb_pushPixels(data, len, !swapped, false);
// After (fixed):
pb_pushPixels(data, len, swapped, false);
Workaround (display.ini):
Until the fix is merged, setting swap_color bit in the :B parameter compensates for the inverted logic:
This sets bit 1 (swap_color=1) in LVGL_PARAMS_t.data, which adds an additional not_swapped = !not_swapped inversion in uDisplay::pushColors() before calling the I80 panel, canceling out the erroneous ! in the I80 code. This workaround must be reverted back to :B,20,0 once the source code fix is applied.
Affected Versions:
- Tasmota master (v15.3.0): Bug present
- Tasmota development: Bug present (identical code)
- Tasmota 15.0.1.1: Not affected (old monolithic uDisplay.cpp)
REQUESTED INFORMATION
{"NAME":"ESP32S3","ARCH":"ESP32S3","GPIO":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1],"FLAG":0,"BASE":1}
{"Module":{"1":"ESP32S3"}}
GPIO8: I2C SDA1 (640)
GPIO9: I2C SCL1 (608)
GPIO10: Switch1 (160)
GPIO11: Relay1 (224)
GPIO45: Option A3 (6210)
(all other GPIOs: None — display pins configured via display.ini, not GPIO template)
No rules — Berry/LVGL application, rules disabled in build (#undef USE_RULES)
{"Status":{"Module":1,"DeviceName":"Tasmota-DispP","FriendlyName":["Tasmota-DispP",""],"Topic":"tasmota_217DF8","ButtonTopic":"0","Power":"00","PowerLock":"00","PowerOnState":3,"LedState":1,"LedMask":"FFFF","SaveData":1,"SaveState":1,"SwitchTopic":"0","SwitchMode":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"ButtonRetain":0,"SwitchRetain":0,"SensorRetain":0,"PowerRetain":0,"InfoRetain":0,"StateRetain":0,"StatusRetain":0},"StatusPRM":{"Baudrate":115200,"SerialConfig":"8N1","GroupTopic":"tasmotas","OtaUrl":"http://ota.tasmota.com/tasmota32/release/tasmota32s3.bin","RestartReason":"Software reset CPU","Uptime":"0T00:05:50","StartupUTC":"2026-03-23T10:22:51","Sleep":50,"CfgHolder":4617,"BootCount":797,"BCResetTime":"2025-06-16T19:23:11","SaveCount":4351},"StatusFWR":{"Version":"15.3.0.3(tasmota)","BuildDateTime":"2026.03.23 09:59:06","Core":"3.3.7","SDK":"5.3.4.260127","CpuFrequency":240,"Hardware":"ESP32-S3 v0.2","CR":"423/699"},"StatusLOG":{"SerialLog":3,"WebLog":2,"MqttLog":0,"FileLog":0,"SysLog":0,"LogHost":"","LogPort":514,"SSId":["RAPTOR1",""],"TelePeriod":300,"Resolution":"558180C0","SetOption":["0000C009","0A05C80001000600003C5A0A1928FA000000","40008080","04006000","00004001","00000000"]},"StatusMEM":{"ProgramSize":2208,"Free":4959,"Heap":108,"StackLowMark":17,"PsrMax":6016,"PsrFree":5706,"ProgramFlashSize":16384,"FlashSize":16384,"FlashChipId":"184046","FlashFrequency":80,"FlashMode":"QIO","Features":["0407","87BAC7CF","001482A1","000214CF","014017F1","C0000981","000040A0","00200000","5400002C","00020080","00000004"],"Drivers":"1,2,!3,!4,!5,7,!8,9,12,13,!16,!20,!21,!24,26,27,29,!35,38,50,52,54,55,56,62,!67,!68,!121","Sensors":"1,2,3,5,6,9,10,14,29,42,127","I2CDriver":"3,5,7,10,11,15,22,26,29,47,77"},"StatusNET":{"Hostname":"tasmota-217DF8-7672","IPAddress":"192.168.168.105","Gateway":"192.168.168.1","Subnetmask":"255.255.255.0","DNSServer1":"192.168.168.131","Mac":"30:ED:A0:21:7D:F8","Webserver":2,"HTTP_API":1,"WifiConfig":4,"WifiPower":19.0},"StatusMQT":{"MqttHost":"raspberrypi5","MqttPort":1883,"MqttClientMask":"DVES_%06X","MqttClient":"DVES_217DF8","MqttUser":"master","MqttCount":1,"MqttTLS":0,"MAX_PACKET_SIZE":1200,"KEEPALIVE":30,"SOCKET_TIMEOUT":4},"StatusTIM":{"UTC":"2026-03-23T10:28:42Z","Local":"2026-03-23T11:28:42","StartDST":"2026-03-29T02:00:00","EndDST":"2026-10-25T03:00:00","Timezone":"+01:00","Sunrise":"06:46","Sunset":"19:06"},"StatusSNS":{"Time":"2026-03-23T11:28:42","Switch1":"OFF","BME280":{"Temperature":23.7,"Humidity":34.5,"DewPoint":7.1,"Pressure":989.1},"BH1750":{"Illuminance":56},"SHT3X":{"Temperature":23.2,"Humidity":40.2,"DewPoint":8.9},"SCD30":{"CarbonDioxide":975,"eCO2":998,"Temperature":25.8,"Humidity":35.1,"DewPoint":9.2},"Shutter1":{"Position":0,"Direction":0,"Target":0,"Tilt":0},"PressureUnit":"hPa","TempUnit":"C"},"StatusSTS":{"Time":"2026-03-23T11:28:42","Uptime":"0T00:05:51","UptimeSec":351,"Heap":108,"SleepMode":"Dynamic","Sleep":10,"LoadAvg":99,"MqttCount":1,"Berry":{"HeapUsed":230,"Objects":3056},"POWER1":"OFF","POWER2":"OFF","Wifi":{"AP":1,"SSId":"RAPTOR1","BSSId":"34:81:C4:F2:1C:A1","Channel":1,"Mode":"HT20","RSSI":82,"Signal":-59,"LinkCount":1,"Downtime":"0T00:00:03"},"Hostname":"tasmota-217DF8-7672","IPAddress":"192.168.168.105"}}
Not applicable — this is a pixel byte-order issue in the display driver, not a runtime error.
The display initializes and renders without errors. Colors are simply wrong due to
swapped byte order on the 8-bit parallel bus.
TO REPRODUCE
- Use an ILI9488 display with 8-bit parallel (I80) interface on ESP32-S3
- Configure display.ini with
:H,ILI9488,480,320,16,PAR,8,... and 3A,1,55 (RGB565)
- Enable LVGL (Berry or HASPmota)
- Observe that blue backgrounds appear green and the color palette is severely reduced
The issue does not occur with:
- SPI interface (
3A,1,66 + :P,18) — SPI path has its own color conversion, unaffected
- Tasmota 15.0.1.1 or earlier (before the uDisplay refactor)
EXPECTED BEHAVIOUR
Colors should be displayed correctly on 8-bit parallel (I80) ILI9488 displays, as they were in Tasmota 15.0.1.1. Blue should appear blue, not green. The full 65K color palette (RGB565) should be visible.
The one-character fix (removing ! from line ~264 in uDisplay_I80_panel.cpp) restores correct behavior:
pb_pushPixels(data, len, swapped, false); // remove the '!' before 'swapped'
SCREENSHOTS
If applicable, add screenshots to help explain your problem.
ADDITIONAL CONTEXT
Add any other context about the problem here.
(Please, remember to close the issue when the problem has been addressed)
Wrong colors on ILI9488 with 8-bit parallel (I80) interface since uDisplay refactor
After upgrading from Tasmota 15.0.1.1 to 15.3.0.3, the ILI9488 display connected via 8-bit parallel (I80) interface on an ESP32-S3 shows wrong colors: blue appears as green, and the overall color palette is severely reduced (appears to be ~16 colors instead of 65K). The same display.ini worked perfectly on Tasmota 15.0.1.1.
SPI-connected ILI9488 displays are not affected — only the parallel (I80) interface path is broken.
Symptoms:
Root Cause:
The uDisplay driver was refactored in PR #24007 (Oct 2025), splitting the monolithic
uDisplay.cppinto panel-specific files. During this refactoring, the byte order logic for 8-bit parallel (I80) pixel pushing was inverted.The bug is in
I80Panel::pushColors()inlib/lib_display/UDisplay/src/uDisplay_I80_panel.cpp, line ~264:The call chain:
uDisplay::pushColors(data, len, not_swapped=true)— comment inuDisplay_graphics.cpp:"not_swapped is always true in call from LVGL driver!!!!"uDisplay::pushColorscallsuniversal_panel->pushColors(data, len, not_swapped=true)I80Panel::pushColors(data, len, swapped=true)callspb_pushPixels(data, len, !true=false, false)pb_pushPixelswithswap_bytes=falsesends: low byte first, then high byte — but ILI9488 expects high byte firstIn the old monolithic
uDisplay.cpp(15.0.1.1), the PAR8 path always usedpb_pushPixels(data, len, true, false)withswap_bytes=true, producing the correct byte order.Byte order on the 8-bit bus for RGB565 — Pure Blue (0x001F):
RRRRRGGG)GGGBBBBB)GGGBBBBB)RRRRRGGG)Note: The comment on line ~264 says
"swap_bytes=true to match old driver", which confirms the intent was to haveswap_bytes=true— but the!negation produces the opposite result.Proposed Fix:
In
lib/lib_display/UDisplay/src/uDisplay_I80_panel.cpp, line ~264, remove the!negation:Workaround (display.ini):
Until the fix is merged, setting
swap_colorbit in the:Bparameter compensates for the inverted logic:This sets bit 1 (
swap_color=1) inLVGL_PARAMS_t.data, which adds an additionalnot_swapped = !not_swappedinversion inuDisplay::pushColors()before calling the I80 panel, canceling out the erroneous!in the I80 code. This workaround must be reverted back to:B,20,0once the source code fix is applied.Affected Versions:
REQUESTED INFORMATION
Backlog Template; Module; GPIO 255:{"NAME":"ESP32S3","ARCH":"ESP32S3","GPIO":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1],"FLAG":0,"BASE":1} {"Module":{"1":"ESP32S3"}} GPIO8: I2C SDA1 (640) GPIO9: I2C SCL1 (608) GPIO10: Switch1 (160) GPIO11: Relay1 (224) GPIO45: Option A3 (6210) (all other GPIOs: None — display pins configured via display.ini, not GPIO template)Backlog Rule1; Rule2; Rule3:Status 0:{"Status":{"Module":1,"DeviceName":"Tasmota-DispP","FriendlyName":["Tasmota-DispP",""],"Topic":"tasmota_217DF8","ButtonTopic":"0","Power":"00","PowerLock":"00","PowerOnState":3,"LedState":1,"LedMask":"FFFF","SaveData":1,"SaveState":1,"SwitchTopic":"0","SwitchMode":[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"ButtonRetain":0,"SwitchRetain":0,"SensorRetain":0,"PowerRetain":0,"InfoRetain":0,"StateRetain":0,"StatusRetain":0},"StatusPRM":{"Baudrate":115200,"SerialConfig":"8N1","GroupTopic":"tasmotas","OtaUrl":"http://ota.tasmota.com/tasmota32/release/tasmota32s3.bin","RestartReason":"Software reset CPU","Uptime":"0T00:05:50","StartupUTC":"2026-03-23T10:22:51","Sleep":50,"CfgHolder":4617,"BootCount":797,"BCResetTime":"2025-06-16T19:23:11","SaveCount":4351},"StatusFWR":{"Version":"15.3.0.3(tasmota)","BuildDateTime":"2026.03.23 09:59:06","Core":"3.3.7","SDK":"5.3.4.260127","CpuFrequency":240,"Hardware":"ESP32-S3 v0.2","CR":"423/699"},"StatusLOG":{"SerialLog":3,"WebLog":2,"MqttLog":0,"FileLog":0,"SysLog":0,"LogHost":"","LogPort":514,"SSId":["RAPTOR1",""],"TelePeriod":300,"Resolution":"558180C0","SetOption":["0000C009","0A05C80001000600003C5A0A1928FA000000","40008080","04006000","00004001","00000000"]},"StatusMEM":{"ProgramSize":2208,"Free":4959,"Heap":108,"StackLowMark":17,"PsrMax":6016,"PsrFree":5706,"ProgramFlashSize":16384,"FlashSize":16384,"FlashChipId":"184046","FlashFrequency":80,"FlashMode":"QIO","Features":["0407","87BAC7CF","001482A1","000214CF","014017F1","C0000981","000040A0","00200000","5400002C","00020080","00000004"],"Drivers":"1,2,!3,!4,!5,7,!8,9,12,13,!16,!20,!21,!24,26,27,29,!35,38,50,52,54,55,56,62,!67,!68,!121","Sensors":"1,2,3,5,6,9,10,14,29,42,127","I2CDriver":"3,5,7,10,11,15,22,26,29,47,77"},"StatusNET":{"Hostname":"tasmota-217DF8-7672","IPAddress":"192.168.168.105","Gateway":"192.168.168.1","Subnetmask":"255.255.255.0","DNSServer1":"192.168.168.131","Mac":"30:ED:A0:21:7D:F8","Webserver":2,"HTTP_API":1,"WifiConfig":4,"WifiPower":19.0},"StatusMQT":{"MqttHost":"raspberrypi5","MqttPort":1883,"MqttClientMask":"DVES_%06X","MqttClient":"DVES_217DF8","MqttUser":"master","MqttCount":1,"MqttTLS":0,"MAX_PACKET_SIZE":1200,"KEEPALIVE":30,"SOCKET_TIMEOUT":4},"StatusTIM":{"UTC":"2026-03-23T10:28:42Z","Local":"2026-03-23T11:28:42","StartDST":"2026-03-29T02:00:00","EndDST":"2026-10-25T03:00:00","Timezone":"+01:00","Sunrise":"06:46","Sunset":"19:06"},"StatusSNS":{"Time":"2026-03-23T11:28:42","Switch1":"OFF","BME280":{"Temperature":23.7,"Humidity":34.5,"DewPoint":7.1,"Pressure":989.1},"BH1750":{"Illuminance":56},"SHT3X":{"Temperature":23.2,"Humidity":40.2,"DewPoint":8.9},"SCD30":{"CarbonDioxide":975,"eCO2":998,"Temperature":25.8,"Humidity":35.1,"DewPoint":9.2},"Shutter1":{"Position":0,"Direction":0,"Target":0,"Tilt":0},"PressureUnit":"hPa","TempUnit":"C"},"StatusSTS":{"Time":"2026-03-23T11:28:42","Uptime":"0T00:05:51","UptimeSec":351,"Heap":108,"SleepMode":"Dynamic","Sleep":10,"LoadAvg":99,"MqttCount":1,"Berry":{"HeapUsed":230,"Objects":3056},"POWER1":"OFF","POWER2":"OFF","Wifi":{"AP":1,"SSId":"RAPTOR1","BSSId":"34:81:C4:F2:1C:A1","Channel":1,"Mode":"HT20","RSSI":82,"Signal":-59,"LinkCount":1,"Downtime":"0T00:00:03"},"Hostname":"tasmota-217DF8-7672","IPAddress":"192.168.168.105"}}weblogto 4 and then, when you experience your issue, provide the output of the Console log:TO REPRODUCE
:H,ILI9488,480,320,16,PAR,8,...and3A,1,55(RGB565)The issue does not occur with:
3A,1,66+:P,18) — SPI path has its own color conversion, unaffectedEXPECTED BEHAVIOUR
Colors should be displayed correctly on 8-bit parallel (I80) ILI9488 displays, as they were in Tasmota 15.0.1.1. Blue should appear blue, not green. The full 65K color palette (RGB565) should be visible.
The one-character fix (removing
!from line ~264 inuDisplay_I80_panel.cpp) restores correct behavior:SCREENSHOTS
If applicable, add screenshots to help explain your problem.
ADDITIONAL CONTEXT
Add any other context about the problem here.
(Please, remember to close the issue when the problem has been addressed)