Skip to content

Bus_SPI: wait for SPI_UPDATE to complete when generating the 1-bit dummy clock - #308

Merged
lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:spi_dummy_update
Sep 22, 2026
Merged

lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:spi_dummy_update

Conversation

@ainyan03

@ainyan03 ainyan03 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On chips with SPI_UPDATE (ESP32-C3/S3/C2/C6/C5/C61/H2/P4) a single dummy clock cannot be transferred, so
Bus_SPI::beginRead(1) inverts SPI_CK_IDLE_EDGE, issues SPI_UPDATE, restores the polarity and issues SPI_UPDATE
again to move the clock line once. The updates were issued back to back without waiting for the synchronization into
the SPI clock domain to finish (and the preceding beginRead() had already issued one more without waiting). When the
restore overtakes the inversion the clock never moves, the panel sees no dummy clock and every read comes back shifted
by one bit: RDDID 0x000000E3 reads as 0x000080F1.

The dummy_read_bits = 1 default of Panel_LCD::readCommand() and the display autodetect's panel ID read go through
this path, so on an ESP32-C5 board the autodetect failed at random after a reset (13 of 25 resets needed a retry,
sometimes every retry failed and the board fell back to a display-less identity).

Measurement

With M5.begin()/init() omitted and the panel ID read every 5 ms from boot on an ESP32-C5 board:

  • unmodified: only the first read after bus init was correct, every later one was shifted (400 reads × 12 resets)
  • 0 dummy bits (readData skipped): the same shifted value every time
  • 2 hardware dummy bits: the value shifted the other way every time
  • with the waits: 4800 correct reads out of 4800

The first read presumably wins only because the code is not yet in cache, which also explains the boot-to-boot randomness.

Fix

Wait for SPI_UPDATE to self-clear before each write that changes the polarity: once before the inversion (covering the
update the preceding beginRead() issued) and once before the restore. No wait is added after the restore: the value is
already in the register when that update is issued, and the following readData() starts the transfer with USR|UPDATE,
which carries the same value, exactly as every other transfer after a bare update does. The wait itself is the one
ESP-IDF's spi_ll_apply_config() uses on every chip that has the bit.

Verification

  • ESP32-C5 board: autodetect succeeded on the first read on 30 of 30 resets and on 10 of 10 resets issued in the middle
    of a transfer (unmodified: 13 of 25 needed retries, 3 of 10 mid-transfer)
  • Repeated readCommand(0x04) at 8 / 16 MHz and readRect comparison on ESP32-S3 (4 boards), ESP32 and ESP32-C5;
    ESP32-H2 / ESP32-C6 boot (no display)
  • Builds: Arduino 3.1.1 / 2.0.17, ESP-IDF 5.1 / 5.2 / 5.3 / 5.5 / 6.0 across all supported targets

…mmy clock

On chips with SPI_UPDATE (ESP32-C3/S3/C2/C6/C5/C61/H2/P4) a single dummy clock
cannot be transferred, so beginRead(1) inverts SPI_CK_IDLE_EDGE, issues
SPI_UPDATE, restores the polarity and issues SPI_UPDATE again. The two updates
were issued back to back without waiting for the first synchronization into
the SPI clock domain to finish. When the restore lands first, the clock line
never moves, the panel sees no dummy clock and every read comes back shifted
by one bit (RDDID 0x000000E3 read as 0x000080F1).

Measured on an ESP32-C5 board: with M5.begin() omitted and the ID read every
5 ms from boot, only the first read after bus init was correct and every later
one was shifted (400 reads x 12 resets); with a 0-bit dummy the same shifted
value appeared every time, with a hardware 2-bit dummy the value was shifted
the other way every time. The first read presumably wins only because the
code is not yet in cache, which also explains why the display autodetect on
that board failed at random (13 of 25 resets needed a retry, sometimes all
retries failed and the board fell back to the display-less identity).

Wait for the SPI_UPDATE bit to self-clear before each write that changes the
polarity: once before the inversion (the preceding beginRead() issues an
update of its own without waiting) and once before the restore. No wait is
added after the restore: the value is already in the register when that
update is issued, and the readData() that follows starts the transfer with
USR|UPDATE, which carries the same value, exactly as every other transfer
after a bare update does. This keeps the rule of waiting before a change
rather than after it.

With the two waits the same measurement gives 4800 correct reads out of
4800, and the autodetect succeeded at the first read on 30 of 30 resets plus
10 of 10 resets issued in the middle of a transfer.
@lovyan03
lovyan03 merged commit 334e9a1 into m5stack:develop Sep 22, 2026
28 checks passed
@ainyan03
ainyan03 deleted the spi_dummy_update branch September 22, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants