diff options
| author | Your Name <you@example.com> | 2026-05-18 18:34:33 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-18 18:34:33 +0530 |
| commit | 067a5700e26518d9a3f8bd92966c8957d17f0817 (patch) | |
| tree | 1379c544731f0edd9c52016c117221085713887d | |
| parent | 8f00db9d57e9e06df7fbf3e7285ae9256d21d716 (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.md | 164 | ||||
| -rw-r--r-- | Makefile | 43 | ||||
| -rw-r--r-- | components/axs15231b/axs15231b.c | 174 | ||||
| -rw-r--r-- | components/axs15231b/include/axs15231b.h | 4 | ||||
| -rw-r--r-- | main/display.c | 31 |
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 | |||
| 12 | Register 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 | |||
| 28 | The 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 | |||
| 30 | ArduinoGFX 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 | |||
| 32 | Our 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 | |||
| 42 | ArduinoGFX 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 | |||
| 47 | Our 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 | |||
| 49 | A 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 | |||
| 60 | All 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 | |||
| 97 | At init, allocate a static buffer: | ||
| 98 | ```c | ||
| 99 | #define FLUSH_CHUNK_PIXELS 2048 // 4096 bytes, fits in internal DMA RAM | ||
| 100 | static uint8_t *s_swap_buf = NULL; | ||
| 101 | |||
| 102 | // In axs15231b_init(): | ||
| 103 | s_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 | ||
| 109 | void 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 | |||
| 150 | Only re-render when: | ||
| 151 | - `s_force_render` is set (state change, init) | ||
| 152 | - QR mode cycles (every 5s in READY state) | ||
| 153 | |||
| 154 | This 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 | | ||
| @@ -9,6 +9,7 @@ PROJECT_DIR := $(shell pwd) | |||
| 9 | BUILD_DIR := $(PROJECT_DIR)/build | 9 | BUILD_DIR := $(PROJECT_DIR)/build |
| 10 | PORT_A ?= /dev/ttyACM1 | 10 | PORT_A ?= /dev/ttyACM1 |
| 11 | PORT_B ?= /dev/ttyACM2 | 11 | PORT_B ?= /dev/ttyACM2 |
| 12 | PORT_C ?= /dev/ttyACM0 | ||
| 12 | PORT ?= $(PORT_A) | 13 | PORT ?= $(PORT_A) |
| 13 | BAUD ?= 460800 | 14 | BAUD ?= 460800 |
| 14 | TARGET ?= esp32s3 | 15 | TARGET ?= esp32s3 |
| @@ -45,6 +46,14 @@ define require_lock_b | |||
| 45 | fi | 46 | fi |
| 46 | endef | 47 | endef |
| 47 | 48 | ||
| 49 | define 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 | ||
| 55 | endef | ||
| 56 | |||
| 48 | define _require_board_lock | 57 | define _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 | ||
| 88 | help: | 97 | help: |
| 89 | @echo "TollGate ESP32 — Makefile" | 98 | @echo "TollGate ESP32 — Makefile" |
| @@ -186,7 +195,7 @@ setup: | |||
| 186 | 195 | ||
| 187 | flash: build | 196 | flash: 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 | ||
| 192 | flash-a: build | 201 | flash-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 | ||
| 211 | flash-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 | |||
| 202 | build: | 216 | build: |
| 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 | ||
| 230 | monitor-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") | |||
| 379 | lock-b: ## Acquire Board B lock (set PHASE="description") | 397 | lock-b: ## Acquire Board B lock (set PHASE="description") |
| 380 | $(call _acquire_lock,board-b) | 398 | $(call _acquire_lock,board-b) |
| 381 | 399 | ||
| 400 | lock-c: ## Acquire Board C lock (set PHASE="description") | ||
| 401 | $(call _acquire_lock,board-c) | ||
| 402 | |||
| 382 | unlock-a: ## Release Board A lock | 403 | unlock-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 | ||
| 417 | unlock-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 | |||
| 396 | force-unlock-a: ## Force-release Board A lock | 424 | force-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 | ||
| 442 | force-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 | |||
| 414 | lock-status: ## Show all board lock statuses | 451 | lock-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 | |||
| 32 | static spi_device_handle_t s_spi = NULL; | 36 | static spi_device_handle_t s_spi = NULL; |
| 33 | static uint16_t *s_fb = NULL; | 37 | static uint16_t *s_fb = NULL; |
| 34 | static int s_width = AXS15231B_WIDTH; | 38 | static 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 | ||
| 44 | static esp_err_t send_cmd(uint8_t cmd) { | 48 | static 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; | 52 | static inline void cs_high(void) { |
| 49 | return spi_device_polling_transmit(s_spi, &t); | 53 | gpio_set_level(AXS15231B_PIN_CS, 1); |
| 54 | } | ||
| 55 | |||
| 56 | static 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 | |||
| 68 | static 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 | ||
| 52 | static esp_err_t send_data(const uint8_t *data, int len) { | 80 | static 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 | ||
| 61 | static esp_err_t send_cmd_data(uint8_t cmd, const uint8_t *data, int len) { | 92 | static 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 | |||
| 105 | static 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 | |||
| 121 | static 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 | ||
| 68 | static const uint8_t init_bb[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x5A,0xA5}; | 136 | static 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) { | |||
| 242 | void axs15231b_flush(void) { | 305 | void 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 | ||
| 282 | int axs15231b_get_width(void) { return s_width; } | 342 | int 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; | |||
| 21 | static bool s_initialized = false; | 21 | static bool s_initialized = false; |
| 22 | static int64_t s_last_qr_switch = 0; | 22 | static int64_t s_last_qr_switch = 0; |
| 23 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; | 23 | static display_qr_mode_t s_qr_mode = DISPLAY_QR_WIFI; |
| 24 | static display_state_t s_rendered_state = DISPLAY_BOOT; | ||
| 25 | static display_qr_mode_t s_rendered_qr_mode = DISPLAY_QR_WIFI; | ||
| 26 | static bool s_force_render = true; | ||
| 24 | 27 | ||
| 25 | static int qr_version_from_strlen(int len) { | 28 | static 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 | ||
| 146 | static void render_boot_screen(void) { | 149 | static 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 | ||
| 183 | static void render_payment_screen(void) { | 186 | static 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 | ||
| 190 | static void render_error_screen(void) { | 193 | static 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 | ||
| 197 | static void display_task(void *pvParameters) { | 200 | static 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 | ||
| 248 | void display_set_state(display_state_t state) { | 254 | void display_set_state(display_state_t state) { |
| 249 | s_state = state; | 255 | s_state = state; |
| 256 | s_force_render = true; | ||
| 250 | } | 257 | } |
| 251 | 258 | ||
| 252 | void display_update(const char *ap_ssid, int active_clients, | 259 | void display_update(const char *ap_ssid, int active_clients, |