upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/DISPLAY_FIX_PLAN.md
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-18 18:34:33 +0530
committerYour Name <you@example.com>2026-05-18 18:34:33 +0530
commit067a5700e26518d9a3f8bd92966c8957d17f0817 (patch)
tree1379c544731f0edd9c52016c117221085713887d /DISPLAY_FIX_PLAN.md
parent8f00db9d57e9e06df7fbf3e7285ae9256d21d716 (diff)
Document QSPI protocol findings and display fix plan
- DISPLAY_FIX_PLAN.md: root cause analysis, byte-order mismatch, PSRAM cache coherency issue, reference implementations studied - axs15231b.c: QSPI protocol rewrite with correct ArduinoGFX framing (cmd=0x02 for regs, cmd=0x32/addr=0x003C00 for pixels) - display.c: portrait centering for 320x480, render-on-change logic - Makefile: Board C support (flash-c, lock-c) - axs15231b.h: QSPI command/address constants Display shows recognizable text, colors wrong due to byte-swap
Diffstat (limited to 'DISPLAY_FIX_PLAN.md')
-rw-r--r--DISPLAY_FIX_PLAN.md164
1 files changed, 164 insertions, 0 deletions
diff --git a/DISPLAY_FIX_PLAN.md b/DISPLAY_FIX_PLAN.md
new file mode 100644
index 0000000..dfacad2
--- /dev/null
+++ b/DISPLAY_FIX_PLAN.md
@@ -0,0 +1,164 @@
1# Display Fix Plan — AXS15231B QSPI Driver
2
3## Board Info
4
5- **Board:** Guition JC3248W535C_I_Y (Board C, `/dev/ttyACM0`)
6- **Display IC:** AXS15231B (QSPI, 4 data lines)
7- **Resolution:** 320x480 portrait (native), 480x320 landscape (via rotation)
8- **Pins:** CS=45, CLK=47, D0=21, D1=48, D2=40, D3=39, BL=1
9
10## QSPI Protocol (from ArduinoGFX source)
11
12Register writes and pixel data use different QSPI framing:
13
14| Operation | cmd | addr | flags | Data format |
15|-----------|-----|------|-------|-------------|
16| Register write (C8D8) | `0x02` | `LCD_CMD << 8` | `MULTILINE_CMD \| MULTILINE_ADDR` | Big-endian |
17| Register write (C8D16) | `0x02` | `LCD_CMD << 8` | `MULTILINE_CMD \| MULTILINE_ADDR` | Big-endian |
18| Register write (C8D16D16) | `0x02` | `LCD_CMD << 8` | `MULTILINE_CMD \| MULTILINE_ADDR` | Big-endian |
19| Pixel data (first chunk) | `0x32` | `0x003C00` | `SPI_TRANS_MODE_QIO` | Big-endian (byte-swapped) |
20| Pixel data (continuation) | — | — | `MODE_QIO \| VAR_CMD \| VAR_ADDR \| VAR_DUMMY` | Big-endian (byte-swapped) |
21
22- CS: Manual GPIO control (`spics_io_num = -1`)
23- Bus: permanently acquired via `spi_device_acquire_bus()`
24- SPI config: `command_bits=8, address_bits=24, dummy_bits=0, mode=0, HALFDUPLEX`
25
26## Root Cause: Byte-Order Mismatch
27
28The ESP32-S3 is little-endian. The framebuffer stores RGB565 pixels as `[low_byte, high_byte]`. The AXS15231B expects pixels in big-endian order `[high_byte, low_byte]` over QSPI.
29
30ArduinoGFX handles this by byte-swapping each pixel in `writePixels()` and `writeRepeat()` using the `MSB_16_SET(var, val)` macro: `var = (val >> 8) | (val << 8)`.
31
32Our driver was sending raw little-endian pixels, causing the display to interpret the byte-swapped values as colors. Example:
33
34| Intended color | RGB565 hex | Display sees (no swap) | Display sees (with swap) |
35|---------------|-----------|----------------------|------------------------|
36| Pink 0xF79F | `[9F, F7]` | R=19, G=63, B=23 (green) | R=30, G=60, B=31 (pink/white) |
37| Red 0xF800 | `[00, F8]` | R=0, G=0, B=0 (black!) | R=31, G=0, B=0 (red) |
38| Cyan 0x07FF | `[FF, 07]` | R=31, G=63, B=7 (yellow) | R=0, G=63, B=31 (cyan) |
39
40## Root Cause: PSRAM Cache Coherency
41
42ArduinoGFX allocates its pixel transfer buffer in **internal DMA SRAM**:
43```cpp
44_buffer = (uint8_t *)heap_caps_aligned_alloc(16, ESP32QSPI_MAX_PIXELS_AT_ONCE * 2, MALLOC_CAP_DMA);
45```
46
47Our framebuffer lives in PSRAM (8MB). When we modified the PSRAM framebuffer in-place (byte-swap), the CPU cache held the modified values but the SPI DMA controller read stale data from physical PSRAM. Result: black screen.
48
49A separate allocation (even in PSRAM) works because it gets clean, freshly-written cache lines.
50
51## Reference Implementations Studied
52
53| Repo | Chip | Bus | Notes |
54|------|------|-----|-------|
55| [me-processware/JC3248W535-Driver](https://github.com/me-processware/JC3248W535-Driver) | AXS15231B | Arduino_ESP32QSPI | Arduino_Canvas wrapper, same pins |
56| [F1ATB/JC3248W535-Demo](https://github.com/F1ATB/JC3248W535-Demo) | AXS15231B | Arduino_ESP32QSPI | Minimal demo, rotation=1 landscape |
57| [AudunKodehode/JC3248W535EN-Touch-LCD](https://github.com/AudunKodehode/JC3248W535EN-Touch-LCD) | AXS15231B | Arduino_ESP32QSPI | Full library, QR codes, JPEG, coordinate transforms |
58| [ArduinoGFX Arduino_ESP32QSPI.cpp](https://github.com/moononournation/Arduino_GFX) | — | — | Reference QSPI protocol implementation |
59
60All use identical pin assignments and bus configuration.
61
62## Checklist
63
64### Done
65- [x] Created worktree on branch `feature/display-fix`
66- [x] Tracked untracked display files into branch
67- [x] Added Board C support to Makefile (`flash-c`, `lock-c`, etc.)
68- [x] Diagnosed root cause: QSPI protocol, not standard SPI
69- [x] Fetched and analyzed ArduinoGFX QSPI source code
70- [x] Discovered correct QSPI framing: `cmd=0x02` for regs, `cmd=0x32/addr=0x003C00` for pixels
71- [x] Rewrote driver with correct QSPI protocol
72- [x] Build succeeds, flash succeeds
73- [x] Display shows recognizable text ("TollGate", "starting") — protocol confirmed working
74- [x] Identified byte-swap requirement (green text = wrong byte order)
75- [x] Identified PSRAM cache coherency issue (in-place swap = black screen)
76- [x] Studied 3 reference implementations + ArduinoGFX source
77- [x] Text positions adjusted for 320x480 portrait centering
78
79### In Progress
80- [ ] Implement byte-swap using internal DMA buffer (like ArduinoGFX)
81
82### TODO
83- [ ] Restore render-on-change logic (proven correct, black screen was from swap not logic)
84- [ ] Use saturated colors: cyan `0x07FF`, yellow `0xFFE0`, white `0xFFFF`
85- [ ] Build, flash, verify correct colors and stable text
86- [ ] Verify QR code rendering in READY state
87- [ ] Verify payment/error screen states
88- [ ] Remove debug log from flush
89- [ ] Run `make test-unit` to check for regressions
90- [ ] Commit working display driver
91- [ ] Push to remote
92
93## Implementation Plan
94
95### 1. Internal DMA swap buffer in `axs15231b.c`
96
97At init, allocate a static buffer:
98```c
99#define FLUSH_CHUNK_PIXELS 2048 // 4096 bytes, fits in internal DMA RAM
100static uint8_t *s_swap_buf = NULL;
101
102// In axs15231b_init():
103s_swap_buf = heap_caps_aligned_alloc(16, FLUSH_CHUNK_PIXELS * 2, MALLOC_CAP_DMA);
104```
105
106### 2. Byte-swap flush loop
107
108```c
109void axs15231b_flush(void) {
110 // ... CASET, RASET ...
111
112 int total_pixels = s_width * s_height;
113 int offset = 0;
114 bool first = true;
115
116 cs_low();
117 while (offset < total_pixels) {
118 int chunk = min(FLUSH_CHUNK_PIXELS, total_pixels - offset);
119
120 // Byte-swap from PSRAM framebuffer into DMA buffer
121 uint8_t *src = (uint8_t *)(s_fb + offset);
122 for (int i = 0; i < chunk * 2; i += 2) {
123 s_swap_buf[i] = src[i + 1];
124 s_swap_buf[i + 1] = src[i];
125 }
126
127 // Send via QSPI
128 spi_transaction_ext_t t = {0};
129 if (first) {
130 t.base.flags = SPI_TRANS_MODE_QIO;
131 t.base.cmd = 0x32;
132 t.base.addr = 0x003C00;
133 first = false;
134 } else {
135 t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
136 SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
137 }
138 t.base.tx_buffer = s_swap_buf;
139 t.base.length = chunk * 16;
140 spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t);
141
142 offset += chunk;
143 }
144 cs_high();
145}
146```
147
148### 3. Render-on-change in `display.c`
149
150Only re-render when:
151- `s_force_render` is set (state change, init)
152- QR mode cycles (every 5s in READY state)
153
154This eliminates the 1Hz full-screen redraw that caused text to "move around."
155
156### 4. Color choices
157
158| Element | Old color | New color | Reason |
159|---------|-----------|-----------|--------|
160| Boot title | `0xF79F` (near-white) | `0x07FF` (cyan) | High contrast on black |
161| Boot subtitle | `0xB5B6` (gray) | `0xFFE0` (yellow) | Visible, warm accent |
162| Ready label | `0xB5B6` | `0x07FF` | Consistent accent |
163| Payment bg | `0x07E0` (green) | `0x07E0` | Keep — bright green is clear |
164| Error bg | `0xF800` (red) | `0xF800` | Keep — bright red is clear |