fix(13e6): SPI corruption fix + setup-screen polish
- SPI corruption: lower clock to 4 MHz (matches 7.3" prod) and push from internal SRAM in 8 KB chunks instead of streaming directly from a PSRAM scratch buffer. On the S3, Arduino's SPI DMA reads RAM directly — the CPU's cache can hold writes to PSRAM that the DMA never sees, painting the panel yellow/garbage. Internal-SRAM chunks are DMA-coherent. - LittleFS partition: switch the env to default_16MB.csv. The stock partition table for esp32-s3-devkitc-1 reserves ~1.5 MB for SPIFFS; three 960 KB setup-screen .bin files need ~2.9 MB + LittleFS metadata. - Setup screens: redesigned to match the 7.3" information density — yellow header band, two-column body with vertical divider, "Connect to WiFi" heading + 5 numbered steps + manual QR + side label on the left, Step 1 / Step 2 QRs on the right. - Orientation diagrams: PORTRAIT drawn upright (ribbon-bottom, up-arrow); LANDSCAPE drawn pre-rotated 90° CCW so it snaps to upright landscape when the user rotates the frame 90° CW (ribbon-bottom + left-arrow in portrait view → ribbon-left + up-arrow after rotation). "LANDSCAPE" label runs vertically up the long edge so it reads horizontally once the frame is mounted landscape. - New helper paste_rotated_text() — PIL's text() can't rotate, so render → rotate → paste-with-alpha. Used for the vertical LANDSCAPE label. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -115,11 +115,13 @@ void epd_setup_pins() {
|
||||
digitalWrite(PIN_CS_S, HIGH);
|
||||
digitalWrite(PIN_RST, HIGH);
|
||||
|
||||
// FSPI on S3, manual CS. 10 MHz is well under the panel's documented
|
||||
// 20 MHz ceiling and gives plenty of margin on the long traces between
|
||||
// the module and the connector.
|
||||
// FSPI on S3, manual CS. Matches the 7.3" production speed (4 MHz) —
|
||||
// tried 10 MHz first and the panel showed a yellow cast across the
|
||||
// body, suggesting bit corruption on long bursts (likely PSRAM-DMA
|
||||
// cache coherency or SI margin issues on the long traces inside the
|
||||
// all-in-one module).
|
||||
SPI.begin(PIN_SCK, -1, PIN_MOSI, -1);
|
||||
SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
|
||||
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||
}
|
||||
|
||||
void epd_init() {
|
||||
@@ -193,19 +195,44 @@ void epd_sleep() {
|
||||
|
||||
// ── Draw helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Push one half's framebuffer slice to its CS line. The slice is the
|
||||
// HALF_BYTES_ROW × H bytes for that half, laid out row-major-contiguous.
|
||||
static void push_half(int cs_pin, const uint8_t* half_fb) {
|
||||
// DMA-safe scratch buffer (internal SRAM). SPI.writeBytes on ESP32-S3 uses
|
||||
// GDMA for bulk transfers; DMA can't reliably read from PSRAM-backed
|
||||
// buffers because the CPU's cache may hold writes that haven't reached
|
||||
// PSRAM yet — symptom is a yellow/random cast across the panel. Pushing
|
||||
// from internal SRAM in chunks sidesteps that entirely. 8 KB is a
|
||||
// per-half-row sweet spot (~27 rows of 300 bytes), enough that the
|
||||
// per-transfer overhead is negligible.
|
||||
static constexpr size_t DMA_CHUNK = 8192;
|
||||
static uint8_t* g_dma_scratch = nullptr;
|
||||
|
||||
static void ensure_dma_scratch() {
|
||||
if (!g_dma_scratch) {
|
||||
g_dma_scratch = (uint8_t*)heap_caps_malloc(
|
||||
DMA_CHUNK, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
|
||||
}
|
||||
}
|
||||
|
||||
// Push one half's framebuffer slice to its CS line via DMA-safe chunks.
|
||||
// half_fb points into PSRAM (the full framebuffer); we copy CHUNK bytes
|
||||
// into internal SRAM, then SPI.writeBytes that. Repeat until done.
|
||||
static void push_half(int cs_pin, const uint8_t* half_fb, size_t bytes) {
|
||||
ensure_dma_scratch();
|
||||
if (!g_dma_scratch) return;
|
||||
|
||||
begin_cmd(cs_pin, 0x10);
|
||||
send_data_n(half_fb, (size_t)HALF_BYTES_ROW * H);
|
||||
for (size_t off = 0; off < bytes; off += DMA_CHUNK) {
|
||||
size_t n = (bytes - off < DMA_CHUNK) ? (bytes - off) : DMA_CHUNK;
|
||||
memcpy(g_dma_scratch, half_fb + off, n);
|
||||
SPI.writeBytes(g_dma_scratch, n);
|
||||
}
|
||||
cs(cs_pin, HIGH);
|
||||
}
|
||||
|
||||
// Take a full-frame row-major framebuffer (BYTES_PER_ROW × H) and push
|
||||
// left-then-right halves. Needs a scratch buffer to deinterleave halves
|
||||
// from the row-major layout — the SPI bus needs contiguous bytes per CS.
|
||||
// left-then-right halves. Deinterleaves into a PSRAM scratch slice so
|
||||
// each half is row-contiguous, then push_half streams it through the
|
||||
// DMA-safe internal-SRAM chunk buffer.
|
||||
static void push_full_frame(const uint8_t* fb) {
|
||||
// Allocate a half-slice scratch buffer in PSRAM. 300 × 1600 = 480 KB.
|
||||
constexpr size_t HALF_BYTES = (size_t)HALF_BYTES_ROW * H;
|
||||
uint8_t* slice = (uint8_t*)heap_caps_malloc(HALF_BYTES, MALLOC_CAP_SPIRAM);
|
||||
if (!slice) return;
|
||||
@@ -215,14 +242,14 @@ static void push_full_frame(const uint8_t* fb) {
|
||||
fb + (size_t)y * BYTES_PER_ROW,
|
||||
HALF_BYTES_ROW);
|
||||
}
|
||||
push_half(PIN_CS_M, slice);
|
||||
push_half(PIN_CS_M, slice, HALF_BYTES);
|
||||
|
||||
for (uint16_t y = 0; y < H; y++) {
|
||||
memcpy(slice + (size_t)y * HALF_BYTES_ROW,
|
||||
fb + (size_t)y * BYTES_PER_ROW + HALF_BYTES_ROW,
|
||||
HALF_BYTES_ROW);
|
||||
}
|
||||
push_half(PIN_CS_S, slice);
|
||||
push_half(PIN_CS_S, slice, HALF_BYTES);
|
||||
|
||||
heap_caps_free(slice);
|
||||
}
|
||||
@@ -330,19 +357,41 @@ static void draw_from_lfs(const char* path, uint8_t fallback_color,
|
||||
QRCode* qr, int qr_x, int qr_y, int qr_cell) {
|
||||
File f = LittleFS.open(path, "r");
|
||||
if (!f || f.size() != FB_BYTES) {
|
||||
Serial.printf("[epd13e6] %s: open=%d size=%u -> fill 0x%X\n",
|
||||
path, (int)(bool)f, f ? (unsigned)f.size() : 0u,
|
||||
fallback_color);
|
||||
if (f) f.close();
|
||||
epd_fill(fallback_color);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t* fb = fb_alloc();
|
||||
if (!fb) { f.close(); epd_fill(fallback_color); return; }
|
||||
if (!fb) {
|
||||
Serial.printf("[epd13e6] fb_alloc FAILED (PSRAM free=%u)\n",
|
||||
(unsigned)ESP.getFreePsram());
|
||||
f.close();
|
||||
epd_fill(fallback_color);
|
||||
return;
|
||||
}
|
||||
|
||||
if (f.read(fb, FB_BYTES) != FB_BYTES) {
|
||||
Serial.println("[epd13e6] read short");
|
||||
fb_release(fb); f.close(); epd_fill(fallback_color); return;
|
||||
}
|
||||
f.close();
|
||||
|
||||
// Sample first 8 bytes of each quarter of the framebuffer so we can
|
||||
// verify what we're about to push matches the on-disk .bin. If the
|
||||
// panel shows wrong colors with these sample bytes looking right,
|
||||
// corruption is downstream of the CPU.
|
||||
Serial.printf("[epd13e6] fb sample: head=%02x%02x%02x%02x%02x%02x%02x%02x ",
|
||||
fb[0], fb[1], fb[2], fb[3], fb[4], fb[5], fb[6], fb[7]);
|
||||
size_t q = FB_BYTES / 4;
|
||||
Serial.printf("q1=%02x%02x%02x%02x q2=%02x%02x%02x%02x q3=%02x%02x%02x%02x\n",
|
||||
fb[q+0], fb[q+1], fb[q+2], fb[q+3],
|
||||
fb[2*q+0], fb[2*q+1], fb[2*q+2], fb[2*q+3],
|
||||
fb[3*q+0], fb[3*q+1], fb[3*q+2], fb[3*q+3]);
|
||||
|
||||
const int qr_px = qr->size * qr_cell;
|
||||
const int y0 = max(qr_y, 0);
|
||||
const int y1 = min(qr_y + qr_px, (int)H);
|
||||
@@ -370,13 +419,13 @@ static void draw_from_lfs(const char* path, uint8_t fallback_color,
|
||||
// firmware paints the live QR into it. Mismatch = the QR draws over
|
||||
// decorative borders or the QR placeholder shows through.
|
||||
void epd_draw_ap_screen(QRCode* qr) {
|
||||
draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 304, 220, 16);
|
||||
draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 642, 230, 14);
|
||||
}
|
||||
|
||||
void epd_draw_ap_screen_retry(QRCode* qr) {
|
||||
draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 304, 220, 16);
|
||||
draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 642, 230, 14);
|
||||
}
|
||||
|
||||
void epd_draw_setup_screen(QRCode* qr) {
|
||||
draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 313, 450, 14);
|
||||
draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 272, 490, 16);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user