display_gif_embedded: port to LVGL 9.6.0 (20.6 -> 23.4 fps)

- lvgl/lvgl@9.6.0 via lib_deps, lv_conf.h from the 9.6 template
- scr_st77916.h: lv_display/lv_indev API, RGB565 byte swap in flush,
  lv_tick_set_cb
- FPS counter via display refresh events
- loligod.c: lv_image_dsc_t descriptor

GIF now plays at its own frame rate; decode time halved by LVGL 9.6's
AnimatedGIF decoder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
patriandClaude Opus 5.5 committed 2026-10-23 01:00:18 +02:00
1 parent 70114f73aa
commit 9684ab39f2
7 files changed
+170574 -168624

No files matched your search

+44 -44
View File
@@ -1,44 +1,44 @@
# OBD Display (2024 experiments) # OBD Display (2024 experiments)
Earlier attempt to show car speed from an ELM327 OBD-II adapter on the JC3636W518. Earlier attempt to show car speed from an ELM327 OBD-II adapter on the JC3636W518.
Because the ESP32-S3 has no Bluetooth Classic, the design used a second (classic) ESP32 as a bridge: Because the ESP32-S3 has no Bluetooth Classic, the design used a second (classic) ESP32 as a bridge:
``` ```
ELM327 --(BT Classic SPP)--> classic ESP32 (elm327_wifi_bridge) --(WiFi AP, HTTP /speed)--> JC3636W518 (display_speed_wifi) ELM327 --(BT Classic SPP)--> classic ESP32 (elm327_wifi_bridge) --(WiFi AP, HTTP /speed)--> JC3636W518 (display_speed_wifi)
``` ```
Split out of `2-Program Example/Demo_Arduino/ST77916_LVGL_DEMO/` on 2026-10-22 and converted to PlatformIO. History is in git. Split out of `2-Program Example/Demo_Arduino/ST77916_LVGL_DEMO/` on 2026-10-22 and converted to PlatformIO. History is in git.
## Projects (all PlatformIO) ## Projects (all PlatformIO)
| Folder | Runs on | What it does | Was | | Folder | Runs on | What it does | Was |
|---|---|---|---| |---|---|---|---|
| `display_speed_wifi/` | JC3636W518 | GIF background from SD (`A:wallpaper.gif`) + joins AP `ESP32-Access-Point`, polls `http://192.168.4.1/speed`, shows it in a label | `ST77916_LVGL_DEMO/8.4/` | | `display_speed_wifi/` | JC3636W518 | GIF background from SD (`A:wallpaper.gif`) + joins AP `ESP32-Access-Point`, polls `http://192.168.4.1/speed`, shows it in a label | `ST77916_LVGL_DEMO/8.4/` |
| `display_gif_sd/` | JC3636W518 | GIF from SD (`A:wallpaper.gif`) + static text label | `ST77916_LVGL_DEMO/working version/` | | `display_gif_sd/` | JC3636W518 | GIF from SD (`A:wallpaper.gif`) + static text label | `ST77916_LVGL_DEMO/working version/` |
| `display_gif_embedded/` | JC3636W518 | GIF compiled into firmware (`src/loligod.c`, 2.2 MB GIF → 3.4 MB firmware, uses `default_16MB.csv` with 6.25 MB app slots). WiFi code commented out. Stops in `setup()` if no SD card | `ST77916_LVGL_DEMO/` (top level) | | `display_gif_embedded/` | JC3636W518 | GIF compiled into firmware (`src/loligod.c`, 2.2 MB GIF → 3.4 MB firmware, uses `default_16MB.csv` with 6.25 MB app slots). **LVGL 9.6.0**, plays at full GIF speed (~23.4 fps), prints FPS once per second. GIF starts without SD card. WiFi code commented out. | `ST77916_LVGL_DEMO/` (top level) |
| `elm327_wifi_bridge/` | **classic ESP32** (not S3) | BT Classic → ELM327 named `OBDII` (PIN 1234), WiFi AP `ESP32-Access-Point` / `123456789`, serves `/speed` (kph) | `ST77916_LVGL_DEMO/esp23_elm327_to_wifi/` | | `elm327_wifi_bridge/` | **classic ESP32** (not S3) | BT Classic → ELM327 named `OBDII` (PIN 1234), WiFi AP `ESP32-Access-Point` / `123456789`, serves `/speed` (kph) | `ST77916_LVGL_DEMO/esp23_elm327_to_wifi/` |
| `elmulator/` | classic ESP32 | Fake ELM327 for testing without a car. Library in `lib/ELMulator`, envs `simple`, `custom`, `wifi` (BT name `OBDII`) | `ST77916_LVGL_DEMO/ELMulator-master/` | | `elmulator/` | classic ESP32 | Fake ELM327 for testing without a car. Library in `lib/ELMulator`, envs `simple`, `custom`, `wifi` (BT name `OBDII`) | `ST77916_LVGL_DEMO/ELMulator-master/` |
| `assets/gifs/` | — | GIF files (`dump/` = unused collection) | `ST77916_LVGL_DEMO/gifs/` | | `assets/gifs/` | — | GIF files (`dump/` = unused collection) | `ST77916_LVGL_DEMO/gifs/` |
| `_archive/lvgl9_broken/` | — | Unfinished LVGL 9 port, does not compile, not converted | `ST77916_LVGL_DEMO/9.1/` | | `_archive/lvgl9_broken/` | — | Unfinished LVGL 9 port, does not compile, not converted | `ST77916_LVGL_DEMO/9.1/` |
## Build / upload ## Build / upload
Open a project folder in VS Code with the PlatformIO extension, or from a terminal: Open a project folder in VS Code with the PlatformIO extension, or from a terminal:
``` ```
pio run -d display_speed_wifi # build pio run -d display_speed_wifi # build
pio run -d display_speed_wifi -t upload # flash (COM21) pio run -d display_speed_wifi -t upload # flash (COM21)
pio device monitor -d display_speed_wifi # serial monitor pio device monitor -d display_speed_wifi # serial monitor
pio run -d elmulator -e wifi -t upload --upload-port COMxx pio run -d elmulator -e wifi -t upload --upload-port COMxx
``` ```
(`pio` = `%USERPROFILE%\.platformio\penv\Scripts\pio.exe` if not on PATH.) (`pio` = `%USERPROFILE%\.platformio\penv\Scripts\pio.exe` if not on PATH.)
## Configuration ## Configuration
- **JC3636W518 projects:** pioarduino platform `51.03.05` (= Arduino core 3.0.5), board `esp32-s3-devkitc-1` with 16 MB QIO flash, OPI PSRAM, `huge_app.csv` (3 MB app), USB-CDC on boot, COM21. - **JC3636W518 projects:** pioarduino platform `51.03.05` (= Arduino core 3.0.5), board `esp32-s3-devkitc-1` with 16 MB QIO flash, OPI PSRAM, `huge_app.csv` (3 MB app), USB-CDC on boot, COM21.
Libraries come from the vendor folder via `lib_extra_dirs = ../../2-Program Example/Demo_Arduino/library`. Libraries come from the vendor folder via `lib_extra_dirs = ../../2-Program Example/Demo_Arduino/library`.
Each project uses its **own `include/lv_conf.h`** (`-DLV_CONF_INCLUDE_SIMPLE -Iinclude`): Each project uses its **own `include/lv_conf.h`** (`-DLV_CONF_INCLUDE_SIMPLE -Iinclude`):
- `display_speed_wifi`: LVGL heap 4 MB in PSRAM (`ps_malloc`) - `display_speed_wifi`: LVGL heap 4 MB in PSRAM (`ps_malloc`)
- `display_gif_sd`: LVGL heap 7 MB in PSRAM - `display_gif_sd`: LVGL heap 7 MB in PSRAM
- **Classic ESP32 projects:** official `espressif32@6.10.0` (Arduino core 2.0.17), board `esp32dev`, `huge_app.csv`. Set `upload_port` to the classic ESP32's COM port. - **Classic ESP32 projects:** official `espressif32@6.10.0` (Arduino core 2.0.17), board `esp32dev`, `huge_app.csv`. Set `upload_port` to the classic ESP32's COM port.
Bridge libraries: `powerbroker2/ELMduino@3.4.1`, `esphome/ESPAsyncWebServer-esphome@3.4.0`. Bridge libraries: `powerbroker2/ELMduino@3.4.1`, `esphome/ESPAsyncWebServer-esphome@3.4.0`.
## Notes ## Notes
- `src/montserrat_100.c` (100 px digits font) is present but not referenced by any sketch yet. - `src/montserrat_100.c` (100 px digits font) is present but not referenced by any sketch yet.
- SD-based GIFs use LVGL drive `A:` = `LV_USE_FS_FATFS` (reads through ESP-IDF FatFS on the SD_MMC mount). Not yet verified on hardware. - SD-based GIFs use LVGL drive `A:` = `LV_USE_FS_FATFS` (reads through ESP-IDF FatFS on the SD_MMC mount). Not yet verified on hardware.
File diff suppressed because it is too large. Load diff
@@ -9,10 +9,9 @@
#define SCREEN_RES_HOR 360 #define SCREEN_RES_HOR 360
#define SCREEN_RES_VER 360 #define SCREEN_RES_VER 360
static lv_color_t *disp_draw_buf; static uint8_t *disp_draw_buf;
static lv_color_t *disp_draw_buf2; // second buffer: LVGL renders into one while the other is sent to the LCD static uint8_t *disp_draw_buf2; // second buffer: LVGL renders into one while the other is sent to the LCD
static lv_disp_draw_buf_t draw_buf; static lv_display_t *disp = NULL;
static lv_disp_drv_t disp_drv;
static lv_indev_t *indev_touchpad; static lv_indev_t *indev_touchpad;
static ESP_PanelBacklight *backlight = NULL; static ESP_PanelBacklight *backlight = NULL;
static ESP_PanelLcd *lcd = NULL; static ESP_PanelLcd *lcd = NULL;
@@ -251,20 +250,25 @@ const esp_lcd_panel_vendor_init_cmd_t lcd_init_cmd[] = {
#define TFT_SPI_FREQ_HZ (50 * 1000 * 1000) #define TFT_SPI_FREQ_HZ (50 * 1000 * 1000)
static void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) // LVGL tick source (replaces LV_TICK_CUSTOM of LVGL 8)
static uint32_t lv_tick_millis(void)
{ {
ESP_PanelLcd *lcd = (ESP_PanelLcd *)disp->user_data; return millis();
const int offsetx1 = area->x1; }
const int offsetx2 = area->x2;
const int offsety1 = area->y1; static void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
const int offsety2 = area->y2; {
lcd->drawBitmap(offsetx1, offsety1, offsetx2 - offsetx1 + 1, offsety2 - offsety1 + 1, (const uint8_t *)color_p); ESP_PanelLcd *lcd = (ESP_PanelLcd *)lv_display_get_user_data(disp);
const int w = lv_area_get_width(area);
const int h = lv_area_get_height(area);
// LVGL 9 renders little-endian RGB565, the ST77916 expects big-endian (LVGL 8 used LV_COLOR_16_SWAP)
lv_draw_rgb565_swap(px_map, w * h);
lcd->drawBitmap(area->x1, area->y1, w, h, (const uint8_t *)px_map);
} }
IRAM_ATTR bool onRefreshFinishCallback(void *user_data) IRAM_ATTR bool onRefreshFinishCallback(void *user_data)
{ {
lv_disp_drv_t *drv = (lv_disp_drv_t *)user_data; lv_display_flush_ready((lv_display_t *)user_data);
lv_disp_flush_ready(drv);
return false; return false;
} }
@@ -330,9 +334,9 @@ void set_brightness(uint8_t bri)
backlight->setBrightness(bri); backlight->setBrightness(bri);
} }
static void touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data)
{ {
ESP_PanelTouch *tp = (ESP_PanelTouch *)indev_drv->user_data; ESP_PanelTouch *tp = (ESP_PanelTouch *)lv_indev_get_user_data(indev);
ESP_PanelTouchPoint point; ESP_PanelTouchPoint point;
int read_touch_result = tp->readPoints(&point, 1); int read_touch_result = tp->readPoints(&point, 1);
@@ -352,12 +356,11 @@ static lv_indev_t *indev_init(ESP_PanelTouch *tp)
{ {
ESP_PANEL_CHECK_FALSE_RET(tp != nullptr, nullptr, "Invalid touch device"); ESP_PANEL_CHECK_FALSE_RET(tp != nullptr, nullptr, "Invalid touch device");
ESP_PANEL_CHECK_FALSE_RET(tp->getHandle() != nullptr, nullptr, "Touch device is not initialized"); ESP_PANEL_CHECK_FALSE_RET(tp->getHandle() != nullptr, nullptr, "Touch device is not initialized");
static lv_indev_drv_t indev_drv_tp; lv_indev_t *indev = lv_indev_create();
lv_indev_drv_init(&indev_drv_tp); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
indev_drv_tp.type = LV_INDEV_TYPE_POINTER; lv_indev_set_read_cb(indev, touchpad_read);
indev_drv_tp.read_cb = touchpad_read; lv_indev_set_user_data(indev, (void *)tp);
indev_drv_tp.user_data = (void *)tp; return indev;
return lv_indev_drv_register(&indev_drv_tp);
} }
void scr_lvgl_init() void scr_lvgl_init()
@@ -418,22 +421,20 @@ void scr_lvgl_init()
size_t lv_cache_rows = 72; size_t lv_cache_rows = 72;
disp_draw_buf = (lv_color_t *)heap_caps_malloc(lv_cache_rows * SCREEN_RES_HOR * 2, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); size_t buf_bytes = lv_cache_rows * SCREEN_RES_HOR * 2; // RGB565
disp_draw_buf2 = (lv_color_t *)heap_caps_malloc(lv_cache_rows * SCREEN_RES_HOR * 2, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); disp_draw_buf = (uint8_t *)heap_caps_malloc(buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
disp_draw_buf2 = (uint8_t *)heap_caps_malloc(buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
lv_init(); lv_init();
lv_disp_draw_buf_init(&draw_buf, disp_draw_buf, disp_draw_buf2, SCREEN_RES_HOR * lv_cache_rows); lv_tick_set_cb(lv_tick_millis);
lv_disp_drv_init(&disp_drv); disp = lv_display_create(SCREEN_RES_HOR, SCREEN_RES_VER);
disp_drv.hor_res = SCREEN_RES_HOR; lv_display_set_flush_cb(disp, my_disp_flush);
disp_drv.ver_res = SCREEN_RES_VER; lv_display_set_buffers(disp, disp_draw_buf, disp_draw_buf2, buf_bytes, LV_DISPLAY_RENDER_MODE_PARTIAL);
disp_drv.flush_cb = my_disp_flush; lv_display_set_user_data(disp, (void *)lcd);
disp_drv.draw_buf = &draw_buf;
disp_drv.user_data = (void *)lcd;
lv_disp_t *disp = lv_disp_drv_register(&disp_drv);
if (lcd->getBus()->getType() != ESP_PANEL_BUS_TYPE_RGB) if (lcd->getBus()->getType() != ESP_PANEL_BUS_TYPE_RGB)
{ {
lcd->attachRefreshFinishCallback(onRefreshFinishCallback, (void *)disp->driver); lcd->attachRefreshFinishCallback(onRefreshFinishCallback, (void *)disp);
} }
indev_touchpad = indev_init(touch); indev_touchpad = indev_init(touch);
} }
@@ -23,8 +23,10 @@ build_flags =
-DLV_CONF_INCLUDE_SIMPLE -DLV_CONF_INCLUDE_SIMPLE
-Iinclude -Iinclude
; vendor-bundled libraries: ESP32_Display_Panel 0.1.4, ESP32_IO_Expander 0.0.2, lvgl 8.3.11 ; vendor-bundled libraries: ESP32_Display_Panel 0.1.4, ESP32_IO_Expander 0.0.2
; (the vendor folder also has lvgl 8.3.11 - ignored here, this project uses LVGL 9 from lib_deps)
lib_extra_dirs = ../../2-Program Example/Demo_Arduino/library lib_extra_dirs = ../../2-Program Example/Demo_Arduino/library
lib_deps = lvgl/lvgl@9.6.0
upload_port = COM21 upload_port = COM21
monitor_port = COM21 monitor_port = COM21
File diff suppressed because it is too large. Load diff
@@ -12,14 +12,31 @@ const char* password = "123456789";
String httpGETRequest(const char* serverName); String httpGETRequest(const char* serverName);
// FPS measurement: LVGL calls this after every screen refresh (= one GIF frame here) // FPS measurement: counts screen refreshes that actually rendered something (= one GIF frame here)
static volatile uint32_t refr_count = 0; static volatile uint32_t refr_count = 0;
static volatile uint32_t refr_time_ms = 0; static volatile uint32_t refr_time_ms = 0;
static uint32_t busy_us = 0; // time spent inside lv_timer_handler() static uint32_t busy_us = 0; // time spent inside lv_timer_handler()
static void refr_monitor_cb(lv_disp_drv_t* drv, uint32_t time, uint32_t px) { static void refr_event_cb(lv_event_t* e) {
refr_count++; static uint32_t refr_start_ms = 0;
refr_time_ms += time; static bool rendered = false;
switch (lv_event_get_code(e)) {
case LV_EVENT_REFR_START:
refr_start_ms = millis();
rendered = false;
break;
case LV_EVENT_RENDER_START:
rendered = true;
break;
case LV_EVENT_REFR_READY:
if (rendered) {
refr_count++;
refr_time_ms += millis() - refr_start_ms;
}
break;
default:
break;
}
} }
void print_fps() { void print_fps() {
@@ -44,9 +61,9 @@ void my_print(const char* buf) {
void display_animation() { void display_animation() {
lv_obj_t* img; lv_obj_t* img;
LV_IMG_DECLARE(loligod); LV_IMAGE_DECLARE(loligod);
img = lv_gif_create(lv_scr_act()); img = lv_gif_create(lv_screen_active());
Serial.print("Available Heap before: "); Serial.print("Available Heap before: ");
Serial.println(esp_get_free_heap_size()); Serial.println(esp_get_free_heap_size());
@@ -94,7 +111,7 @@ void display_text() {
// lv_style_set_shadow_ofs_x(&style_text, 0); // lv_style_set_shadow_ofs_x(&style_text, 0);
// lv_style_set_shadow_ofs_y(&style_text, 0); // lv_style_set_shadow_ofs_y(&style_text, 0);
label1 = lv_label_create(lv_scr_act()); label1 = lv_label_create(lv_screen_active());
// Apply the style to the label // Apply the style to the label
lv_obj_add_style(label1, &style_text, LV_PART_MAIN); lv_obj_add_style(label1, &style_text, LV_PART_MAIN);
@@ -130,14 +147,12 @@ void setup() {
delay(200); delay(200);
Serial.begin(115200); Serial.begin(115200);
scr_lvgl_init(); scr_lvgl_init();
lv_disp_get_default()->driver->monitor_cb = refr_monitor_cb; lv_display_add_event_cb(lv_display_get_default(), refr_event_cb, LV_EVENT_ALL, NULL);
lv_init();
//lv_log_register_print_cb(my_print); // Register print function for LVGL logs //lv_log_register_print_cb(my_print); // Register print function for LVGL logs
// Get the active screen (root object for the display) // Get the active screen (root object for the display)
lv_obj_t* screen = lv_scr_act(); lv_obj_t* screen = lv_screen_active();
// Set the background color of the entire screen to black // Set the background color of the entire screen to black
lv_obj_set_style_bg_color(screen, lv_color_black(), 0); // 0 is the part ID for the main part lv_obj_set_style_bg_color(screen, lv_color_black(), 0); // 0 is the part ID for the main part
+110 -108
View File
@@ -1,108 +1,110 @@
# JC3636W518EN — Project Notes # JC3636W518EN — Project Notes
Vendor kit and documentation for the **JC3636W518** (Shenzhen Jingcai / JCZN, Guition), a round 1.8" ESP32-S3 touch-display puck. Vendor kit and documentation for the **JC3636W518** (Shenzhen Jingcai / JCZN, Guition), a round 1.8" ESP32-S3 touch-display puck.
Goal: write a **new custom program** for this device. Goal: write a **new custom program** for this device.
## Working agreement (from the user) ## Working agreement (from the user)
- Before starting any task, **ask questions first** and make sure requirements are clearly understood. - Before starting any task, **ask questions first** and make sure requirements are clearly understood.
- While working, **describe what is being done** at each step. - While working, **describe what is being done** at each step.
- Keep this file updated with new findings. - Keep this file updated with new findings.
- The folder is a **git repo** (local only, no remote). Commit `9ee85bf` = original kit snapshot before any cleanup. - The folder is a **git repo** (local only, no remote). Commit `9ee85bf` = original kit snapshot before any cleanup.
## Hardware summary ## Hardware summary
| Item | Detail | | Item | Detail |
|---|---| |---|---|
| MCU | ESP32-S3R8 (dual-core LX7 @ 240 MHz, 512 KB SRAM, **8 MB OPI PSRAM** in-package) | | MCU | ESP32-S3R8 (dual-core LX7 @ 240 MHz, 512 KB SRAM, **8 MB OPI PSRAM** in-package) |
| Flash | **16 MB confirmed** (esptool flash_id: mfr 0x85, dev 0x2018, quad mode). Chip rev v0.2, MAC f0:9e:9e:1e:60:40 | | Flash | **16 MB confirmed** (esptool flash_id: mfr 0x85, dev 0x2018, quad mode). Chip rev v0.2, MAC f0:9e:9e:1e:60:40 |
| Radio | WiFi + **BLE 5 only — NO Bluetooth Classic** (ESP32-S3 has no BR/EDR, so no SPP / `BluetoothSerial`) | | Radio | WiFi + **BLE 5 only — NO Bluetooth Classic** (ESP32-S3 has no BR/EDR, so no SPP / `BluetoothSerial`) |
| Connection | Board shows up on **COM21** on this PC. esptool: `%LOCALAPPDATA%\Arduino15\packages\esp32\tools\esptool_py\4.6\esptool.exe` | | Connection | Board shows up on **COM21** on this PC. esptool: `%LOCALAPPDATA%\Arduino15\packages\esp32\tools\esptool_py\4.6\esptool.exe` |
| Display | 1.8" round IPS, **360×360**, driver **ST77916** over **QSPI**, RGB565 used (panel is 262K/18-bit) | | Display | 1.8" round IPS, **360×360**, driver **ST77916** over **QSPI**, RGB565 used (panel is 262K/18-bit) |
| Touch | **CST816S** capacitive, I2C (addr 0x15), single touch + gestures | | Touch | **CST816S** capacitive, I2C (addr 0x15), single touch + gestures |
| Audio out | **PCM5100A** I2S DAC → 3.5 mm line-out jack (no speaker amp on board) | | Audio out | **PCM5100A** I2S DAC → 3.5 mm line-out jack (no speaker amp on board) |
| Microphone | **MSM261S4030H0R** I2S MEMS mic ("pickup port") | | Microphone | **MSM261S4030H0R** I2S MEMS mic ("pickup port") |
| Storage | microSD (TF) slot, 4-bit SD_MMC | | Storage | microSD (TF) slot, 4-bit SD_MMC |
| Button | BOOT button on GPIO0 (only user button) | | Button | BOOT button on GPIO0 (only user button) |
| Power | 5 V USB-C (~180 mA), plus **Qi wireless receiver** (standalone, no MCU signals) | | Power | 5 V USB-C (~180 mA), plus **Qi wireless receiver** (standalone, no MCU signals) |
| USB | Native USB (GPIO19/20) — USB-CDC serial, no separate USB-UART chip | | USB | Native USB (GPIO19/20) — USB-CDC serial, no separate USB-UART chip |
| Expansion | CN2 4-pin connector: **5V, GPIO39, GPIO38, GND** (the only free GPIOs) | | Expansion | CN2 4-pin connector: **5V, GPIO39, GPIO38, GND** (the only free GPIOs) |
| Size | 58 × 58 × 11 mm metal case, active area 45.68 × 45.68 mm | | Size | 58 × 58 × 11 mm metal case, active area 45.68 × 45.68 mm |
## Pin map (from `pincfg.h`, cross-checked against schematic) ## Pin map (from `pincfg.h`, cross-checked against schematic)
``` ```
LCD QSPI : CS=10 SCK=9 D0=11 D1=12 D2=13 D3=14 RST=47 BL=15 (PWM, LEDC, via AO3400 MOSFET) LCD QSPI : CS=10 SCK=9 D0=11 D1=12 D2=13 D3=14 RST=47 BL=15 (PWM, LEDC, via AO3400 MOSFET)
LCD_TE is on the FPC; schematic net near GPIO47/RST is hard to read — treat TE as unused LCD_TE is on the FPC; schematic net near GPIO47/RST is hard to read — treat TE as unused
Touch I2C: SDA=7 SCL=8 INT=41 RST=40 Touch I2C: SDA=7 SCL=8 INT=41 RST=40
SD_MMC : CLK=3 CMD=4 D0=2 D1=1 D2=6 D3=5 SD_MMC : CLK=3 CMD=4 D0=2 D1=1 D2=6 D3=5
DAC I2S : BCK=18 WS/LRCK=16 DOUT=17 MCK=21 (code uses -1, PCM5100A PLLs from BCK) MUTE/XSMT=48 (LOW = mute) DAC I2S : BCK=18 WS/LRCK=16 DOUT=17 MCK=21 (code uses -1, PCM5100A PLLs from BCK) MUTE/XSMT=48 (LOW = mute)
Mic I2S : SCK=42 WS=45 SD=46 Mic I2S : SCK=42 WS=45 SD=46
Button : GPIO0 (BOOT) Button : GPIO0 (BOOT)
Free : GPIO38, GPIO39 (on CN2 header) Free : GPIO38, GPIO39 (on CN2 header)
USB : D-=19 D+=20 USB : D-=19 D+=20
``` ```
## PlatformIO setup (preferred build system, since 2026-10-23) ## PlatformIO setup (preferred build system, since 2026-10-23)
- All projects are **PlatformIO projects** (`platformio.ini`, `src/main.cpp`, `include/`). They no longer open in the Arduino IDE. - All projects are **PlatformIO projects** (`platformio.ini`, `src/main.cpp`, `include/`). They no longer open in the Arduino IDE.
- PlatformIO IDE extension is installed in VS Code. CLI: `%USERPROFILE%\.platformio\penv\Scripts\pio.exe` (Core 6.2.0, not on PATH). - PlatformIO IDE extension is installed in VS Code. CLI: `%USERPROFILE%\.platformio\penv\Scripts\pio.exe` (Core 6.2.0, not on PATH).
- **JC3636W518 (ESP32-S3)**: platform `https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip` (pioarduino = Arduino core 3.0.5; the official PIO espressif32 only has core 2.x, which ESP32_Display_Panel 0.1.4 can't use). - **JC3636W518 (ESP32-S3)**: platform `https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip` (pioarduino = Arduino core 3.0.5; the official PIO espressif32 only has core 2.x, which ESP32_Display_Panel 0.1.4 can't use).
Board `esp32-s3-devkitc-1` + `board_build.arduino.memory_type = qio_opi`, `flash_mode = qio`, `board_upload.flash_size = 16MB`, `partitions = huge_app.csv`, `-DBOARD_HAS_PSRAM -DARDUINO_USB_CDC_ON_BOOT=1`, upload/monitor COM21 @115200. Board `esp32-s3-devkitc-1` + `board_build.arduino.memory_type = qio_opi`, `flash_mode = qio`, `board_upload.flash_size = 16MB`, `partitions = huge_app.csv`, `-DBOARD_HAS_PSRAM -DARDUINO_USB_CDC_ON_BOOT=1`, upload/monitor COM21 @115200.
Libraries: `lib_extra_dirs` → vendor `2-Program Example/Demo_Arduino/library`. Libraries: `lib_extra_dirs` → vendor `2-Program Example/Demo_Arduino/library`.
**LVGL config:** `-DLV_CONF_INCLUDE_SIMPLE -Iinclude` — the `-Iinclude` is required, otherwise the LVGL library compiles without the project's `lv_conf.h` (symptom: `undefined reference to lv_gif_create`). **LVGL config:** `-DLV_CONF_INCLUDE_SIMPLE -Iinclude` — the `-Iinclude` is required, otherwise the LVGL library compiles without the project's `lv_conf.h` (symptom: `undefined reference to lv_gif_create`).
- **Classic ESP32** projects (bridge, ELMulator): `espressif32@6.10.0` (core 2.0.17), board `esp32dev`, `huge_app.csv`. - **Classic ESP32** projects (bridge, ELMulator): `espressif32@6.10.0` (core 2.0.17), board `esp32dev`, `huge_app.csv`.
- Template for a new JC3636W518 project: copy `10-OBD-Display-2024/display_speed_wifi/platformio.ini` (+ `include/pincfg.h`, `include/scr_st77916.h`, `include/lv_conf.h`). - Template for a new JC3636W518 project: copy `10-OBD-Display-2024/display_speed_wifi/platformio.ini` (+ `include/pincfg.h`, `include/scr_st77916.h`, `include/lv_conf.h`).
- When converting `.ino` → `main.cpp`: add `#include <Arduino.h>` and prototypes for functions used before their definition; don't `#include` `.c` files that are also in `src/`. - When converting `.ino` → `main.cpp`: add `#include <Arduino.h>` and prototypes for functions used before their definition; don't `#include` `.c` files that are also in `src/`.
## Software stack used by the vendor demo ## Software stack used by the vendor demo
- **Arduino IDE** + **arduino-esp32 core ≥ 3.0.1** (core **3.0.5 already installed** on this PC under `%LOCALAPPDATA%\Arduino15`). - **Arduino IDE** + **arduino-esp32 core ≥ 3.0.1** (core **3.0.5 already installed** on this PC under `%LOCALAPPDATA%\Arduino15`).
- Libraries (bundled in `2-Program Example/Demo_Arduino/library/`): - Libraries (bundled in `2-Program Example/Demo_Arduino/library/`):
- `ESP32_Display_Panel` **0.1.4** (Espressif) — ST77916 QSPI LCD + CST816S touch + backlight - `ESP32_Display_Panel` **0.1.4** (Espressif) — ST77916 QSPI LCD + CST816S touch + backlight
- `ESP32_IO_Expander` **0.0.2** (dependency) - `ESP32_IO_Expander` **0.0.2** (dependency)
- `lvgl` **8.3.11** bundled (instructions say "8.4.0 and below"; LVGL 9 is NOT supported by the demo) - `lvgl` **8.3.11** bundled (instructions say "8.4.0 and below"; LVGL 9 is NOT supported by the demo)
- `lv_conf.h` must be copied next to the `lvgl` library folder (`Documents\Arduino\libraries\lv_conf.h`); for the LVGL demos, move `lvgl/demos` into `lvgl/src/`. - `lv_conf.h` must be copied next to the `lvgl` library folder (`Documents\Arduino\libraries\lv_conf.h`); for the LVGL demos, move `lvgl/demos` into `lvgl/src/`.
- Key `lv_conf.h` settings: `LV_COLOR_DEPTH 16`, `LV_COLOR_16_SWAP 1`, `LV_MEM_CUSTOM 1` (uses malloc → can use PSRAM), `LV_TICK_CUSTOM 1`, `LV_USE_GIF 1`, `LV_USE_PNG 1`, `LV_USE_SJPG 1`, Montserrat 48 enabled. - Key `lv_conf.h` settings: `LV_COLOR_DEPTH 16`, `LV_COLOR_16_SWAP 1`, `LV_MEM_CUSTOM 1` (uses malloc → can use PSRAM), `LV_TICK_CUSTOM 1`, `LV_USE_GIF 1`, `LV_USE_PNG 1`, `LV_USE_SJPG 1`, Montserrat 48 enabled.
- None of these libraries are currently in `Documents\Arduino\libraries` (only unrelated Adafruit libs). No arduino-cli / PlatformIO / ESP-IDF installed. Python 3.12 and git are available. - None of these libraries are currently in `Documents\Arduino\libraries` (only unrelated Adafruit libs). No arduino-cli / PlatformIO / ESP-IDF installed. Python 3.12 and git are available.
### Arduino board settings (from `Arduino set up.png`) ### Arduino board settings (from `Arduino set up.png`)
Board **ESP32S3 Dev Module**, USB CDC On Boot **Enabled**, CPU 240 MHz, Flash **QIO 120 MHz**, Flash Size **16 MB**, Board **ESP32S3 Dev Module**, USB CDC On Boot **Enabled**, CPU 240 MHz, Flash **QIO 120 MHz**, Flash Size **16 MB**,
Partition **Huge APP (3MB No OTA/1MB SPIFFS)**, PSRAM **OPI PSRAM**, Upload Mode UART0/Hardware CDC, Upload speed 921600. Partition **Huge APP (3MB No OTA/1MB SPIFFS)**, PSRAM **OPI PSRAM**, Upload Mode UART0/Hardware CDC, Upload speed 921600.
If upload fails: hold BOOT (GPIO0) while plugging in / resetting. If upload fails: hold BOOT (GPIO0) while plugging in / resetting.
## Display driver pattern (`scr_st77916.h`) ## Display driver pattern (`scr_st77916.h`)
`scr_lvgl_init()` does everything: LEDC backlight → CST816S on I2C @400 kHz → QSPI bus @50 MHz → `ESP_PanelLcd_ST77916(bus, 16bpp, RST)` → `invertColor(true)` → LVGL draw buffer of **72 rows** (360×72×2 = ~52 KB, internal RAM) → flush callback with DMA-done callback → touch indev. `scr_lvgl_init()` does everything: LEDC backlight → CST816S on I2C @400 kHz → QSPI bus @50 MHz → `ESP_PanelLcd_ST77916(bus, 16bpp, RST)` → `invertColor(true)` → LVGL draw buffer of **72 rows** (360×72×2 = ~52 KB, internal RAM) → flush callback with DMA-done callback → touch indev.
Helpers: `setRotation(0..3)`, `set_brightness(0..100)`, `screen_switch(bool)`. Helpers: `setRotation(0..3)`, `set_brightness(0..100)`, `screen_switch(bool)`.
Note: `scr_lvgl_init()` already calls `lv_init()`; the demo `.ino` calls it again (redundant). Note: `scr_lvgl_init()` already calls `lv_init()`; the demo `.ino` calls it again (redundant).
## Folder contents ## Folder contents
| Folder | What it is | | Folder | What it is |
|---|---| |---|---|
| `1-Instructions` | Spec PDF (duplicate), AIDA64 remote-sensor layout `aida_remote_1.85.rslcd`, MJPEG conversion tool (Windows .NET, converts video to 360×360 MJPEG 25 fps) | | `1-Instructions` | Spec PDF (duplicate), AIDA64 remote-sensor layout `aida_remote_1.85.rslcd`, MJPEG conversion tool (Windows .NET, converts video to 360×360 MJPEG 25 fps) |
| `2-Program Example/Demo_Arduino` | **Vendor only:** `library/` (3 libs), `lv_conf.h` (vendor original), `sd/` (SD listing example; its lv_conf.h has FATFS edits), `docs/` (setup screenshots + compile instructions). The vendor's original LVGL demo sketch no longer exists unmodified. | | `2-Program Example/Demo_Arduino` | **Vendor only:** `library/` (3 libs), `lv_conf.h` (vendor original), `sd/` (SD listing example; its lv_conf.h has FATFS edits), `docs/` (setup screenshots + compile instructions). The vendor's original LVGL demo sketch no longer exists unmodified. |
| `10-OBD-Display-2024` | **Earlier OBD speed-display project**, split into separate sketches — see its `README.md`: `display_speed_wifi`, `display_gif_sd`, `display_gif_embedded`, `elm327_wifi_bridge` (classic ESP32), `elmulator`, `assets/gifs`, `_archive/lvgl9_broken` | | `10-OBD-Display-2024` | **Earlier OBD speed-display project**, split into separate sketches — see its `README.md`: `display_speed_wifi`, `display_gif_sd`, `display_gif_embedded`, `elm327_wifi_bridge` (classic ESP32), `elmulator`, `assets/gifs`, `_archive/lvgl9_broken` |
| `3-Specification` | Spec sheet (factory firmware features, interface photo) | | `3-Specification` | Spec sheet (factory firmware features, interface photo) |
| `4-Chip manual` | ESP32-S3R8 datasheet, PCM5100A datasheet | | `4-Chip manual` | ESP32-S3R8 datasheet, PCM5100A datasheet |
| `5-Dimension` | Mechanical drawings (JPG + DWG) | | `5-Dimension` | Mechanical drawings (JPG + DWG) |
| `6-Schematic diagram` | 5 pages: power/USB/SD/mic, ESP32-S3, DAC, LCD FPC, Qi receiver | | `6-Schematic diagram` | 5 pages: power/USB/SD/mic, ESP32-S3, DAC, LCD FPC, Qi receiver |
| `7-User's manual` | "Getting started" — Arduino IDE setup, library install steps (16 pages) | | `7-User's manual` | "Getting started" — Arduino IDE setup, library install steps (16 pages) |
| `8-Tool` | Network debugging assistant, SSCOM serial tool (Chinese Windows tools, .rar) | | `8-Tool` | Network debugging assistant, SSCOM serial tool (Chinese Windows tools, .rar) |
| `9-Burn` | Espressif Flash Download Tool 3.9.3, factory demo `1.85_demo.bin` (1.7 MB, flash at 0x0), screenshots. `logs/` are old (2022) logs from other boards — ignore | | `9-Burn` | Espressif Flash Download Tool 3.9.3, factory demo `1.85_demo.bin` (1.7 MB, flash at 0x0), screenshots. `logs/` are old (2022) logs from other boards — ignore |
## Factory firmware (for reference) ## Factory firmware (for reference)
AIDA64 secondary screen (18 styles), mic spectrum, MP3 player (`/music` on SD), photo frame (`/pic`, 360×360 JPEG), MJPEG player (`/mjpeg`), weather, theme clocks, WiFi provisioning AP "My App"/12345678 + web config at device IP. AIDA64 secondary screen (18 styles), mic spectrum, MP3 player (`/music` on SD), photo frame (`/pic`, 360×360 JPEG), MJPEG player (`/mjpeg`), weather, theme clocks, WiFi provisioning AP "My App"/12345678 + web config at device IP.
Re-flash it with Flash Download Tool: ESP32-S3, `1.85_demo.bin` @ 0x0. Re-flash it with Flash Download Tool: ESP32-S3, `1.85_demo.bin` @ 0x0.
## OBD adapter (user's) ## OBD adapter (user's)
AliExpress "ELM327 Mini V2.1 Bluetooth OBD **Dual Mode** 5.1" — dual mode = Bluetooth Classic SPP **and BLE**, so the ESP32-S3 should be able to connect directly over BLE (no bridge ESP32). AliExpress "ELM327 Mini V2.1 Bluetooth OBD **Dual Mode** 5.1" — dual mode = Bluetooth Classic SPP **and BLE**, so the ESP32-S3 should be able to connect directly over BLE (no bridge ESP32).
Cheap dual-mode clones typically expose BLE service `FFF0` (notify `FFF1`, write `FFF2`) — **not yet verified** on this adapter; scan it first. Adapter is powered from the car's OBD port (pin 16, 12 V). Cheap dual-mode clones typically expose BLE service `FFF0` (notify `FFF1`, write `FFF2`) — **not yet verified** on this adapter; scan it first. Adapter is powered from the car's OBD port (pin 16, 12 V).
## Gotchas / observations ## Gotchas / observations
- **LVGL GIF bug (patched locally):** LVGL 8.3.11 `gifdec.c` (`gd_open_gif`) filled the canvas *opaque* with the GIF's background color. GIFs with a transparent background (e.g. `loligod.gif`, bg = RGB 28,29,49) showed that dark gray except where "restore to background" frames had cleared to transparent (→ screen black). **Patched** in the vendor copy `2-Program Example/Demo_Arduino/library/lvgl/src/extra/libs/gif/gifdec.c`: initial canvas alpha 0x00 (marked `LOCAL PATCH`). Re-apply if LVGL is ever replaced/upgraded. - **LVGL GIF bug (patched locally):** LVGL 8.3.11 `gifdec.c` (`gd_open_gif`) filled the canvas *opaque* with the GIF's background color. GIFs with a transparent background (e.g. `loligod.gif`, bg = RGB 28,29,49) showed that dark gray except where "restore to background" frames had cleared to transparent (→ screen black). **Patched** in the vendor copy `2-Program Example/Demo_Arduino/library/lvgl/src/extra/libs/gif/gifdec.c`: initial canvas alpha 0x00 (marked `LOCAL PATCH`). Re-apply if LVGL is ever replaced/upgraded.
- Most GIFs in `assets/gifs` are optimized (transparent unchanged pixels, disposal 1/2); `loligod.gif` has a fully transparent background (~84 % of each frame). - Most GIFs in `assets/gifs` are optimized (transparent unchanged pixels, disposal 1/2); `loligod.gif` has a fully transparent background (~84 % of each frame).
- **GIF performance** (360×360 loligod, embedded, measured with a `monitor_cb` FPS counter in `display_gif_embedded`): - **GIF performance** (360×360 loligod, embedded, measured with a `monitor_cb` FPS counter in `display_gif_embedded`):
baseline 15.4 fps (decode 27 ms + refresh 31 ms) → no LVGL log / no per-loop prints: no change → `vTaskDelay(1)`: 16.5 → **double draw buffer** (2×72 rows internal RAM): 19.5 (refresh 21 ms) → **`-O2`** instead of `-Os`: **20.6 fps** (refresh 19 ms, decode ~24 ms). GIF's own rate is 23.7 fps. From SD card it was only ~1.4 fps. baseline 15.4 fps (decode 27 ms + refresh 31 ms) → no LVGL log / no per-loop prints: no change → `vTaskDelay(1)`: 16.5 → **double draw buffer** (2×72 rows internal RAM): 19.5 (refresh 21 ms) → **`-O2`** instead of `-Os`: **20.6 fps** (refresh 19 ms, decode ~24 ms). GIF's own rate is 23.7 fps. From SD card it was only ~1.4 fps.
Remaining limit is LVGL's single-threaded GIF decode; going further would need decoding on the other core or a lighter format (e.g. pre-converted frames/MJPEG). Remaining limit is LVGL's single-threaded GIF decode; going further would need decoding on the other core or a lighter format (e.g. pre-converted frames/MJPEG).
- **SD card (user's 487 MB SDSC)** intermittently fails to init (`sdmmc_init_ocr ... 0x107`) after resets; once stuck only a power cycle helps (card VDD is hard-wired to 3V3). Recommend a newer SDHC card. - **SD card (user's 487 MB SDSC)** intermittently fails to init (`sdmmc_init_ocr ... 0x107`) after resets; once stuck only a power cycle helps (card VDD is hard-wired to 3V3). Recommend a newer SDHC card.
- Reading boot logs: USB-CDC — reset via DTR=0/RTS pulse, then reopen the port (it re-enumerates). - Reading boot logs: USB-CDC — reset via DTR=0/RTS pulse, then reopen the port (it re-enumerates).
- LVGL version: all JC3636W518 projects use the vendor-bundled **LVGL 8.3.11**. LVGL 9 would require porting `scr_st77916.h` (display/touch driver glue). - **LVGL 9.6.0 port** (`display_gif_embedded`, 2026-10-23): **23.4 fps** (= the GIF's own 23.7 fps; decode ~12 ms, refresh ~24 ms) vs 20.6 fps tuned LVGL 8.3. LVGL 9.6 replaced gifdec with bitbank2 AnimatedGIF (2× faster decode, correct transparency — no local patch needed).
- Display is round: corners of the 360×360 framebuffer are not visible — design UI within a 360 px circle. Port notes: `lib_deps = lvgl/lvgl@9.6.0` (takes priority over the vendor lvgl 8 in `lib_extra_dirs`); `lv_conf.h` generated from the 9.6 template (CLIB malloc, refresh 15 ms, GIF on, examples off); in 9.6 public headers live in `include/lvgl/` and `LV_COLOR_DEPTH` is deprecated → `LV_COLOR_FORMAT_DEFAULT` (RGB565); no LV_COLOR_16_SWAP → `lv_draw_rgb565_swap()` in the flush cb; no LV_TICK_CUSTOM → `lv_tick_set_cb(millis)`; no `monitor_cb` → display events `LV_EVENT_REFR_START/RENDER_START/REFR_READY`; images are `lv_image_dsc_t` with `LV_IMAGE_HEADER_MAGIC`. `lv_gif_set_color_format()` can switch the GIF buffer from ARGB8888 to RGB565 (untested).
- Compiling a 13.9 MB GIF into the firmware will not fit "Huge APP" (3 MB); large media should live on SD (or in a custom partition). - LVGL version: the other JC3636W518 projects use the vendor-bundled **LVGL 8.3.11**. LVGL 9 would require porting `scr_st77916.h` (display/touch driver glue).
- The top-level demo `setup()` returns early if no SD card is present, before showing anything. - Display is round: corners of the 360×360 framebuffer are not visible — design UI within a 360 px circle.
- Draw buffer is in internal RAM; PSRAM is available for larger buffers / images. - Compiling a 13.9 MB GIF into the firmware will not fit "Huge APP" (3 MB); large media should live on SD (or in a custom partition).
- The top-level demo `setup()` returns early if no SD card is present, before showing anything.
- Draw buffer is in internal RAM; PSRAM is available for larger buffers / images.