upleb.uk

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

summaryrefslogtreecommitdiff
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
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
-rw-r--r--DISPLAY_FIX_PLAN.md164
-rw-r--r--Makefile43
-rw-r--r--components/axs15231b/axs15231b.c174
-rw-r--r--components/axs15231b/include/axs15231b.h4
-rw-r--r--main/display.c31
5 files changed, 342 insertions, 74 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 |
diff --git a/Makefile b/Makefile
index 044ad6b..05c004e 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,7 @@ PROJECT_DIR := $(shell pwd)
9BUILD_DIR := $(PROJECT_DIR)/build 9BUILD_DIR := $(PROJECT_DIR)/build
10PORT_A ?= /dev/ttyACM1 10PORT_A ?= /dev/ttyACM1
11PORT_B ?= /dev/ttyACM2 11PORT_B ?= /dev/ttyACM2
12PORT_C ?= /dev/ttyACM0
12PORT ?= $(PORT_A) 13PORT ?= $(PORT_A)
13BAUD ?= 460800 14BAUD ?= 460800
14TARGET ?= esp32s3 15TARGET ?= esp32s3
@@ -45,6 +46,14 @@ define require_lock_b
45 fi 46 fi
46endef 47endef
47 48
49define require_lock_c
50 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-c.lock" ]; then \
51 echo "$(RED)$(BOLD)Board C not locked — run 'make lock-c PHASE=\"description\"' first$(RESET)"; \
52 echo "$(YELLOW)Another LLM session may be using Board C.$(RESET)"; \
53 exit 1; \
54 fi
55endef
56
48define _require_board_lock 57define _require_board_lock
49 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-$(BOARD).lock" ]; then \ 58 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-$(BOARD).lock" ]; then \
50 echo "$(RED)$(BOLD)Board $(BOARD) not locked — run 'make lock-$(BOARD) PHASE=\"description\"' first$(RESET)"; \ 59 echo "$(RED)$(BOLD)Board $(BOARD) not locked — run 'make lock-$(BOARD) PHASE=\"description\"' first$(RESET)"; \
@@ -83,7 +92,7 @@ endef
83.PHONY: tokens wallet-setup wallet-info wallet-balance mint-token send-token 92.PHONY: tokens wallet-setup wallet-info wallet-balance mint-token send-token
84.PHONY: clean erase-nvs reset serial-log bootstrap-config 93.PHONY: clean erase-nvs reset serial-log bootstrap-config
85.PHONY: cvm-pubkey cvm-test-tool cvm-announce 94.PHONY: cvm-pubkey cvm-test-tool cvm-announce
86.PHONY: lock-a lock-b unlock-a unlock-b force-unlock-a force-unlock-b lock-status 95.PHONY: lock-a lock-b lock-c unlock-a unlock-b unlock-c force-unlock-a force-unlock-b force-unlock-c lock-status
87 96
88help: 97help:
89 @echo "TollGate ESP32 — Makefile" 98 @echo "TollGate ESP32 — Makefile"
@@ -186,7 +195,7 @@ setup:
186 195
187flash: build 196flash: build
188 @echo "=== Flashing to $(PORT) ===" 197 @echo "=== Flashing to $(PORT) ==="
189 @echo "$(RED)Error: use 'make flash-a' or 'make flash-b' (per-board lock required)$(RESET)" 198 @echo "$(RED)Error: use 'make flash-a', 'make flash-b', or 'make flash-c' (per-board lock required)$(RESET)"
190 @exit 1 199 @exit 1
191 200
192flash-a: build 201flash-a: build
@@ -199,6 +208,11 @@ flash-b: build
199 @echo "=== Flashing to $(PORT_B) (Board B) ===" 208 @echo "=== Flashing to $(PORT_B) (Board B) ==="
200 . $(IDF_PATH)/export.sh && idf.py -p $(PORT_B) -b $(BAUD) flash 209 . $(IDF_PATH)/export.sh && idf.py -p $(PORT_B) -b $(BAUD) flash
201 210
211flash-c: build
212 $(call require_lock_c)
213 @echo "=== Flashing to $(PORT_C) (Board C / Display) ==="
214 . $(IDF_PATH)/export.sh && idf.py -p $(PORT_C) -b $(BAUD) flash
215
202build: 216build:
203 @echo "=== Building $(TARGET) ===" 217 @echo "=== Building $(TARGET) ==="
204 . $(IDF_PATH)/export.sh && \ 218 . $(IDF_PATH)/export.sh && \
@@ -213,6 +227,10 @@ monitor-b:
213 $(call require_lock_b) 227 $(call require_lock_b)
214 . $(IDF_PATH)/export.sh && idf.py -p $(PORT_B) monitor 228 . $(IDF_PATH)/export.sh && idf.py -p $(PORT_B) monitor
215 229
230monitor-c:
231 $(call require_lock_c)
232 . $(IDF_PATH)/export.sh && idf.py -p $(PORT_C) monitor
233
216# ────────────────────────────────────────────── 234# ──────────────────────────────────────────────
217# Testing 235# Testing
218# ────────────────────────────────────────────── 236# ──────────────────────────────────────────────
@@ -379,6 +397,9 @@ lock-a: ## Acquire Board A lock (set PHASE="description")
379lock-b: ## Acquire Board B lock (set PHASE="description") 397lock-b: ## Acquire Board B lock (set PHASE="description")
380 $(call _acquire_lock,board-b) 398 $(call _acquire_lock,board-b)
381 399
400lock-c: ## Acquire Board C lock (set PHASE="description")
401 $(call _acquire_lock,board-c)
402
382unlock-a: ## Release Board A lock 403unlock-a: ## Release Board A lock
383 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-a.lock" ]; then \ 404 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-a.lock" ]; then \
384 echo "$(YELLOW)Board A not locked.$(RESET)"; exit 0; \ 405 echo "$(YELLOW)Board A not locked.$(RESET)"; exit 0; \
@@ -393,6 +414,13 @@ unlock-b: ## Release Board B lock
393 rm -f $(HARDWARE_LOCK_DIR)/board-b.lock; \ 414 rm -f $(HARDWARE_LOCK_DIR)/board-b.lock; \
394 echo "$(GREEN)Board B lock released.$(RESET)" 415 echo "$(GREEN)Board B lock released.$(RESET)"
395 416
417unlock-c: ## Release Board C lock
418 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-c.lock" ]; then \
419 echo "$(YELLOW)Board C not locked.$(RESET)"; exit 0; \
420 fi; \
421 rm -f $(HARDWARE_LOCK_DIR)/board-c.lock; \
422 echo "$(GREEN)Board C lock released.$(RESET)"
423
396force-unlock-a: ## Force-release Board A lock 424force-unlock-a: ## Force-release Board A lock
397 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-a.lock" ]; then \ 425 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-a.lock" ]; then \
398 echo "$(YELLOW)Board A not locked.$(RESET)"; exit 0; \ 426 echo "$(YELLOW)Board A not locked.$(RESET)"; exit 0; \
@@ -411,8 +439,17 @@ force-unlock-b: ## Force-release Board B lock
411 rm -f $(HARDWARE_LOCK_DIR)/board-b.lock; \ 439 rm -f $(HARDWARE_LOCK_DIR)/board-b.lock; \
412 echo "$(GREEN)Board B force-released.$(RESET)" 440 echo "$(GREEN)Board B force-released.$(RESET)"
413 441
442force-unlock-c: ## Force-release Board C lock
443 @if [ ! -f "$(HARDWARE_LOCK_DIR)/board-c.lock" ]; then \
444 echo "$(YELLOW)Board C not locked.$(RESET)"; exit 0; \
445 fi; \
446 echo "$(RED)$(BOLD)WARNING: Force-releasing Board C!$(RESET)"; \
447 cat $(HARDWARE_LOCK_DIR)/board-c.lock | sed 's/^/ /'; \
448 rm -f $(HARDWARE_LOCK_DIR)/board-c.lock; \
449 echo "$(GREEN)Board C force-released.$(RESET)"
450
414lock-status: ## Show all board lock statuses 451lock-status: ## Show all board lock statuses
415 @for board in a b; do \ 452 @for board in a b c; do \
416 if [ -f "$(HARDWARE_LOCK_DIR)/board-$$board.lock" ]; then \ 453 if [ -f "$(HARDWARE_LOCK_DIR)/board-$$board.lock" ]; then \
417 echo "$(YELLOW)Board $$board: LOCKED$(RESET)"; \ 454 echo "$(YELLOW)Board $$board: LOCKED$(RESET)"; \
418 cat $(HARDWARE_LOCK_DIR)/board-$$board.lock | sed 's/^/ /'; \ 455 cat $(HARDWARE_LOCK_DIR)/board-$$board.lock | sed 's/^/ /'; \
diff --git a/components/axs15231b/axs15231b.c b/components/axs15231b/axs15231b.c
index 50be305..d64c9bc 100644
--- a/components/axs15231b/axs15231b.c
+++ b/components/axs15231b/axs15231b.c
@@ -29,6 +29,10 @@ static const char *TAG = "axs15231b";
29#define MADCTL_MV 0x20 29#define MADCTL_MV 0x20
30#define MADCTL_RGB 0x00 30#define MADCTL_RGB 0x00
31 31
32#define QSPI_CMD_REG_WRITE 0x02
33#define QSPI_CMD_DATA_WRITE 0x32
34#define QSPI_DATA_ADDR 0x003C00
35
32static spi_device_handle_t s_spi = NULL; 36static spi_device_handle_t s_spi = NULL;
33static uint16_t *s_fb = NULL; 37static uint16_t *s_fb = NULL;
34static int s_width = AXS15231B_WIDTH; 38static int s_width = AXS15231B_WIDTH;
@@ -41,28 +45,92 @@ typedef struct {
41 uint16_t delay_ms; 45 uint16_t delay_ms;
42} init_cmd_t; 46} init_cmd_t;
43 47
44static esp_err_t send_cmd(uint8_t cmd) { 48static inline void cs_low(void) {
45 spi_transaction_t t = {0}; 49 gpio_set_level(AXS15231B_PIN_CS, 0);
46 t.length = 8; 50}
47 t.tx_data[0] = cmd; 51
48 t.flags = SPI_TRANS_USE_TXDATA; 52static inline void cs_high(void) {
49 return spi_device_polling_transmit(s_spi, &t); 53 gpio_set_level(AXS15231B_PIN_CS, 1);
54}
55
56static void cs_init(void) {
57 gpio_config_t cfg = {
58 .pin_bit_mask = (1ULL << AXS15231B_PIN_CS),
59 .mode = GPIO_MODE_OUTPUT,
60 .pull_up_en = GPIO_PULLUP_DISABLE,
61 .pull_down_en = GPIO_PULLDOWN_DISABLE,
62 .intr_type = GPIO_INTR_DISABLE,
63 };
64 gpio_config(&cfg);
65 gpio_set_level(AXS15231B_PIN_CS, 1);
66}
67
68static void qspi_write_command(uint8_t lcd_cmd) {
69 spi_transaction_ext_t t = {0};
70 t.base.flags = SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR;
71 t.base.cmd = QSPI_CMD_REG_WRITE;
72 t.base.addr = ((uint32_t)lcd_cmd) << 8;
73 t.base.tx_buffer = NULL;
74 t.base.length = 0;
75 cs_low();
76 spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t);
77 cs_high();
50} 78}
51 79
52static esp_err_t send_data(const uint8_t *data, int len) { 80static void qspi_write_cmd_data8(uint8_t lcd_cmd, uint8_t d) {
53 if (len == 0) return ESP_OK; 81 spi_transaction_ext_t t = {0};
54 spi_transaction_t t = {0}; 82 t.base.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR;
55 t.length = len * 8; 83 t.base.cmd = QSPI_CMD_REG_WRITE;
56 t.tx_buffer = data; 84 t.base.addr = ((uint32_t)lcd_cmd) << 8;
57 t.flags = 0; 85 t.base.tx_data[0] = d;
58 return spi_device_polling_transmit(s_spi, &t); 86 t.base.length = 8;
87 cs_low();
88 spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t);
89 cs_high();
59} 90}
60 91
61static esp_err_t send_cmd_data(uint8_t cmd, const uint8_t *data, int len) { 92static void qspi_write_cmd_data16(uint8_t lcd_cmd, uint16_t d) {
62 esp_err_t ret = send_cmd(cmd); 93 spi_transaction_ext_t t = {0};
63 if (ret != ESP_OK) return ret; 94 t.base.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR;
64 if (len > 0) ret = send_data(data, len); 95 t.base.cmd = QSPI_CMD_REG_WRITE;
65 return ret; 96 t.base.addr = ((uint32_t)lcd_cmd) << 8;
97 t.base.tx_data[0] = d >> 8;
98 t.base.tx_data[1] = d & 0xFF;
99 t.base.length = 16;
100 cs_low();
101 spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t);
102 cs_high();
103}
104
105static void qspi_write_cmd_bytes(uint8_t lcd_cmd, const uint8_t *data, int len) {
106 if (len == 0) {
107 qspi_write_command(lcd_cmd);
108 return;
109 }
110 spi_transaction_ext_t t = {0};
111 t.base.flags = SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR;
112 t.base.cmd = QSPI_CMD_REG_WRITE;
113 t.base.addr = ((uint32_t)lcd_cmd) << 8;
114 t.base.tx_buffer = data;
115 t.base.length = len * 8;
116 cs_low();
117 spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t);
118 cs_high();
119}
120
121static void qspi_write_cmd_d16d16(uint8_t lcd_cmd, uint16_t d1, uint16_t d2) {
122 spi_transaction_ext_t t = {0};
123 t.base.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR;
124 t.base.cmd = QSPI_CMD_REG_WRITE;
125 t.base.addr = ((uint32_t)lcd_cmd) << 8;
126 t.base.tx_data[0] = d1 >> 8;
127 t.base.tx_data[1] = d1 & 0xFF;
128 t.base.tx_data[2] = d2 >> 8;
129 t.base.tx_data[3] = d2 & 0xFF;
130 t.base.length = 32;
131 cs_low();
132 spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t);
133 cs_high();
66} 134}
67 135
68static const uint8_t init_bb[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5}; 136static const uint8_t init_bb[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5};
@@ -145,9 +213,12 @@ esp_err_t axs15231b_init(void) {
145 }; 213 };
146 214
147 spi_device_interface_config_t devcfg = { 215 spi_device_interface_config_t devcfg = {
216 .command_bits = 8,
217 .address_bits = 24,
218 .dummy_bits = 0,
148 .clock_speed_hz = 40 * 1000 * 1000, 219 .clock_speed_hz = 40 * 1000 * 1000,
149 .mode = 0, 220 .mode = 0,
150 .spics_io_num = AXS15231B_PIN_CS, 221 .spics_io_num = -1,
151 .queue_size = 7, 222 .queue_size = 7,
152 .flags = SPI_DEVICE_HALFDUPLEX, 223 .flags = SPI_DEVICE_HALFDUPLEX,
153 }; 224 };
@@ -164,6 +235,10 @@ esp_err_t axs15231b_init(void) {
164 return ret; 235 return ret;
165 } 236 }
166 237
238 spi_device_acquire_bus(s_spi, portMAX_DELAY);
239
240 cs_init();
241
167 size_t fb_size = (size_t)s_width * s_height * 2; 242 size_t fb_size = (size_t)s_width * s_height * 2;
168 s_fb = heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); 243 s_fb = heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
169 if (!s_fb) { 244 if (!s_fb) {
@@ -182,40 +257,28 @@ esp_err_t axs15231b_init(void) {
182 }; 257 };
183 gpio_config(&bl_cfg); 258 gpio_config(&bl_cfg);
184 259
185 send_cmd(SWRESET); 260 qspi_write_command(SWRESET);
186 vTaskDelay(pdMS_TO_TICKS(200)); 261 vTaskDelay(pdMS_TO_TICKS(200));
187 262
188 for (int i = 0; i < INIT_CMD_COUNT; i++) { 263 for (int i = 0; i < INIT_CMD_COUNT; i++) {
189 ret = send_cmd_data(s_init_cmds[i].cmd, s_init_cmds[i].data, s_init_cmds[i].data_len); 264 qspi_write_cmd_bytes(s_init_cmds[i].cmd, s_init_cmds[i].data, s_init_cmds[i].data_len);
190 if (ret != ESP_OK) {
191 ESP_LOGE(TAG, "Init cmd 0x%02X failed: %s", s_init_cmds[i].cmd, esp_err_to_name(ret));
192 return ret;
193 }
194 if (s_init_cmds[i].delay_ms > 0) { 265 if (s_init_cmds[i].delay_ms > 0) {
195 vTaskDelay(pdMS_TO_TICKS(s_init_cmds[i].delay_ms)); 266 vTaskDelay(pdMS_TO_TICKS(s_init_cmds[i].delay_ms));
196 } 267 }
197 } 268 }
198 269
199 uint8_t madctl_val = MADCTL_MX | MADCTL_MV | MADCTL_RGB; 270 uint8_t madctl_val = MADCTL_RGB;
200 ret = send_cmd_data(MADCTL, &madctl_val, 1); 271 qspi_write_cmd_data8(MADCTL, madctl_val);
201 if (ret != ESP_OK) {
202 ESP_LOGE(TAG, "Failed to set rotation: %s", esp_err_to_name(ret));
203 return ret;
204 }
205 272
206 uint8_t colmod_val = 0x55; 273 uint8_t colmod_val = 0x55;
207 ret = send_cmd_data(COLMOD, &colmod_val, 1); 274 qspi_write_cmd_data8(COLMOD, colmod_val);
208 if (ret != ESP_OK) {
209 ESP_LOGE(TAG, "Failed to set pixel format: %s", esp_err_to_name(ret));
210 return ret;
211 }
212 275
213 axs15231b_fill_screen(0x0000); 276 axs15231b_fill_screen(0x0000);
214 axs15231b_flush(); 277 axs15231b_flush();
215 278
216 axs15231b_set_backlight(true); 279 axs15231b_set_backlight(true);
217 280
218 ESP_LOGI(TAG, "AXS15231B initialized: %dx%d landscape", s_width, s_height); 281 ESP_LOGI(TAG, "AXS15231B initialized: %dx%d portrait", s_width, s_height);
219 return ESP_OK; 282 return ESP_OK;
220} 283}
221 284
@@ -242,41 +305,38 @@ void axs15231b_fill_rect(int x, int y, int w, int h, uint16_t color) {
242void axs15231b_flush(void) { 305void axs15231b_flush(void) {
243 if (!s_spi || !s_fb) return; 306 if (!s_spi || !s_fb) return;
244 307
245 uint8_t buf[4]; 308 ESP_LOGI(TAG, "Flush %dx%d", s_width, s_height);
246 buf[0] = 0;
247 buf[1] = 0;
248 buf[2] = (s_width - 1) >> 8;
249 buf[3] = (s_width - 1) & 0xFF;
250 send_cmd_data(CASET, buf, 4);
251 309
252 buf[0] = 0; 310 qspi_write_cmd_d16d16(CASET, 0, s_width - 1);
253 buf[1] = 0; 311 qspi_write_cmd_d16d16(RASET, 0, s_height - 1);
254 buf[2] = (s_height - 1) >> 8;
255 buf[3] = (s_height - 1) & 0xFF;
256 send_cmd_data(RASET, buf, 4);
257
258 send_cmd(RAMWR);
259 312
260 int total_bytes = s_width * s_height * 2; 313 int total_bytes = s_width * s_height * 2;
261 int chunk_size = 32768; 314 int chunk_size = 32768;
262 int offset = 0; 315 int offset = 0;
263 uint8_t *fb_bytes = (uint8_t *)s_fb; 316 uint8_t *fb_bytes = (uint8_t *)s_fb;
317 bool first = true;
264 318
319 cs_low();
265 while (offset < total_bytes) { 320 while (offset < total_bytes) {
266 int remaining = total_bytes - offset; 321 int remaining = total_bytes - offset;
267 int this_chunk = remaining < chunk_size ? remaining : chunk_size; 322 int this_chunk = remaining < chunk_size ? remaining : chunk_size;
268 323
269 spi_transaction_ext_t t = {0}; 324 spi_transaction_ext_t t = {0};
270 t.base.length = this_chunk * 8; 325 if (first) {
271 t.base.tx_buffer = fb_bytes + offset; 326 t.base.flags = SPI_TRANS_MODE_QIO;
272 t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_MULTILINE_CMD; 327 t.base.cmd = QSPI_CMD_DATA_WRITE;
273 esp_err_t ret = spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t); 328 t.base.addr = QSPI_DATA_ADDR;
274 if (ret != ESP_OK) { 329 first = false;
275 ESP_LOGE(TAG, "Flush transfer failed at offset %d: %s", offset, esp_err_to_name(ret)); 330 } else {
276 return; 331 t.base.flags = SPI_TRANS_MODE_QIO | SPI_TRANS_VARIABLE_CMD |
332 SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY;
277 } 333 }
334 t.base.tx_buffer = fb_bytes + offset;
335 t.base.length = this_chunk * 8;
336 spi_device_polling_transmit(s_spi, (spi_transaction_t *)&t);
278 offset += this_chunk; 337 offset += this_chunk;
279 } 338 }
339 cs_high();
280} 340}
281 341
282int axs15231b_get_width(void) { return s_width; } 342int axs15231b_get_width(void) { return s_width; }
diff --git a/components/axs15231b/include/axs15231b.h b/components/axs15231b/include/axs15231b.h
index 5ec017c..cddea98 100644
--- a/components/axs15231b/include/axs15231b.h
+++ b/components/axs15231b/include/axs15231b.h
@@ -5,8 +5,8 @@
5#include <stdint.h> 5#include <stdint.h>
6#include <stdbool.h> 6#include <stdbool.h>
7 7
8#define AXS15231B_WIDTH 480 8#define AXS15231B_WIDTH 320
9#define AXS15231B_HEIGHT 320 9#define AXS15231B_HEIGHT 480
10 10
11#define AXS15231B_PIN_CS 45 11#define AXS15231B_PIN_CS 45
12#define AXS15231B_PIN_CLK 47 12#define AXS15231B_PIN_CLK 47
diff --git a/main/display.c b/main/display.c
index 2b6cc88..1085d4a 100644
--- a/main/display.c
+++ b/main/display.c
@@ -21,6 +21,9 @@ static uint64_t s_wallet_balance = 0;
21static bool s_initialized = false; 21static bool s_initialized = false;
22static int64_t s_last_qr_switch = 0; 22static int64_t s_last_qr_switch = 0;
23static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; 23static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI;
24static display_state_t s_rendered_state = DISPLAY_BOOT;
25static display_qr_mode_t s_rendered_qr_mode = DISPLAY_QR_WIFI;
26static bool s_force_render = true;
24 27
25static int qr_version_from_strlen(int len) { 28static int qr_version_from_strlen(int len) {
26 if (len <= 17) return 1; 29 if (len <= 17) return 1;
@@ -145,8 +148,8 @@ void display_render_qr(const char *text) {
145 148
146static void render_boot_screen(void) { 149static void render_boot_screen(void) {
147 axs15231b_fill_screen(0x0000); 150 axs15231b_fill_screen(0x0000);
148 display_render_text(140, 100, "TollGate", 0xF79F, 0x0000, 3); 151 display_render_text(64, 210, "TollGate", 0xF79F, 0x0000, 3);
149 display_render_text(140, 140, "starting...", 0xB5B6, 0x0000, 2); 152 display_render_text(72, 250, "starting...", 0xB5B6, 0x0000, 2);
150 axs15231b_flush(); 153 axs15231b_flush();
151} 154}
152 155
@@ -182,24 +185,33 @@ static void render_ready_screen(void) {
182 185
183static void render_payment_screen(void) { 186static void render_payment_screen(void) {
184 axs15231b_fill_screen(0x07E0); 187 axs15231b_fill_screen(0x07E0);
185 display_render_text(140, 100, "Paid!", 0x0000, 0x07E0, 3); 188 display_render_text(100, 220, "Paid!", 0x0000, 0x07E0, 3);
186 display_render_text(130, 140, "Access granted", 0x0000, 0x07E0, 2); 189 display_render_text(48, 260, "Access granted", 0x0000, 0x07E0, 2);
187 axs15231b_flush(); 190 axs15231b_flush();
188} 191}
189 192
190static void render_error_screen(void) { 193static void render_error_screen(void) {
191 axs15231b_fill_screen(0xF800); 194 axs15231b_fill_screen(0xF800);
192 display_render_text(120, 100, "No upstream", 0xFFFF, 0xF800, 3); 195 display_render_text(28, 220, "No upstream", 0xFFFF, 0xF800, 3);
193 display_render_text(130, 140, "Check config", 0xFFFF, 0xF800, 2); 196 display_render_text(64, 260, "Check config", 0xFFFF, 0xF800, 2);
194 axs15231b_flush(); 197 axs15231b_flush();
195} 198}
196 199
197static void display_task(void *pvParameters) { 200static void display_task(void *pvParameters) {
198 ESP_LOGI(TAG, "Display task started"); 201 ESP_LOGI(TAG, "Display task started");
202 vTaskDelay(pdMS_TO_TICKS(500));
199 203
200 while (1) { 204 while (1) {
201 display_state_t state = s_state; 205 display_state_t state = s_state;
202 206
207 if (state == DISPLAY_READY) {
208 int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
209 if ((now - s_last_qr_switch) >= QR_CYCLE_MS) {
210 s_qr_mode = (s_qr_mode == DISPLAY_QR_WIFI) ? DISPLAY_QR_PORTAL : DISPLAY_QR_WIFI;
211 s_last_qr_switch = now;
212 }
213 }
214
203 switch (state) { 215 switch (state) {
204 case DISPLAY_BOOT: 216 case DISPLAY_BOOT:
205 render_boot_screen(); 217 render_boot_screen();
@@ -217,12 +229,6 @@ static void display_task(void *pvParameters) {
217 break; 229 break;
218 } 230 }
219 231
220 int64_t now = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
221 if (state == DISPLAY_READY && (now - s_last_qr_switch) >= QR_CYCLE_MS) {
222 s_qr_mode = (s_qr_mode == DISPLAY_QR_WIFI) ? DISPLAY_QR_PORTAL : DISPLAY_QR_WIFI;
223 s_last_qr_switch = now;
224 }
225
226 vTaskDelay(pdMS_TO_TICKS(1000)); 232 vTaskDelay(pdMS_TO_TICKS(1000));
227 } 233 }
228} 234}
@@ -247,6 +253,7 @@ esp_err_t display_init(void) {
247 253
248void display_set_state(display_state_t state) { 254void display_set_state(display_state_t state) {
249 s_state = state; 255 s_state = state;
256 s_force_render = true;
250} 257}
251 258
252void display_update(const char *ap_ssid, int active_clients, 259void display_update(const char *ap_ssid, int active_clients,