Skip to content

perf(tft): clock clk_peri from clk_sys; the panel's SPI at 66.5 MHz - #278

Merged
angeloINTJ merged 3 commits into
mainfrom
perf/tft-spi-clk-peri
Oct 16, 2026
Merged

angeloINTJ merged 3 commits into
mainfrom
perf/tft-spi-clk-peri

Conversation

@angeloINTJ

Copy link
Copy Markdown
Owner

perf(tft): clock clk_peri from clk_sys; the panel's SPI at 66.5 MHz

The panel's SPI ran at 24 MHz on the build measured (arduino-pico 5.6.1), whatever SIMUT_TFT_SPI_HZ asked for
(62.5 MHz since v2.1.6, 31.25 before); the 5.4.x builds before it carry the same clock code and were not measured.
The framework's main( ) calls set_sys_clock_khz( 133000, true ), and the SDK's set_sys_clock_pll( ) answers by
moving clk_peri to the 48 MHz USB PLL (PICO_CLOCK_ADJUST_PERI_CLOCK_WITH_SYS_CLOCK is 0, and the SDK is prebuilt in
libpico.a). The PL022 divides clk_peri by an even prescale times a post-divider, so the ceiling there is 24 MHz.

Measured on the bench, 2026-10-15, study build: clk_sys 133 MHz, clk_peri 48 MHz, SPI0 24 MHz after a push
(spi_get_baudrate( spi0 )); a 320x40 strip 9.5 ms, a full-screen DMA clear 56.7 ms. v2.1.6's "31.25 -> 62.5 MHz"
did not reach the wire on any build measured: its GRAM readback proved the pixels, not the speed. The read-clock sweep of 2026-09-19 had
already drawn the 48 MHz ladder (10 = 8, 16 = 20 = 12) and MODELO_DE_RECURSOS R-H18 inferred it on 2026-09-25.

Change (six images: pico_w_release, pico_w_test, pico_w_test_https, pico_w_asserts, pico2_w_release, pico_wiretft)

  • src/main.cpp: setup( ) clocks clk_peri from clk_sys as its first statement, skipped when it already is
    (the Pico 2 W never calls set_sys_clock_khz( )). if ( SIMUT_DISPLAY_TFT ), a constant condition: alpha, Air and
    the plain Wire compile none of it.
  • SIMUT_TFT_SPI_HZ = 66.5 MHz, the ceiling of 133 MHz. On the RP2350 (150 MHz) the same request gives 37.5 MHz
    (prescale 2, post-divider 2), as 62.5 did; noted at the constant. One constant, as before. Next rung down: 33.25 MHz.
  • src/display/Pl022Baud.h: the SDK's spi_set_baudrate( ) as a pure function, with 12 native cases.
  • GET /api/status sys.clk_peri and sys.tft_hz (panel images only). tft_hz is read from the PL022 registers in
    dmaTftOpen( ) after the panel's transaction begins, 0 until the first push. Added, nothing renamed.
  • The two false "clk_peri at 125 MHz" comments are rewritten, and the wire times in DisplayManager_Dashboard.cpp;
    CHANGELOG (Unreleased, both languages) corrects v2.1.6's claim; the manual, FEATURES and MODELO_DE_RECURSOS follow
    (second commit; MANUAL.pt-BR.html is regenerated). A third commit answers a review: the bench results, the claim
    narrowed to the builds measured (one build of arduino-pico 5.6.1; the 5.4.x builds, v2.1.6 among them, have the same
    clock code and were not measured), docs/analysis/ESPELHO_DELTA.md (Living) at 66.5 MHz, and two edges of the
    divider pinned by asserts (each was a surviving mutant).

Nothing before the call configures a clk_peri block

Direct call graph from the disassembly of main's six panel images, from every static initializer (13 to 16 per image),
the SDK's runtime init and main( ) up to setup( ): no path to spi_init, uart_init, SerialUART::begin or
SPIClassRP2040::begin. The clock code reached is the SDK's own. (The call graph, the mutant runs and the differential run against the SDK
are kept outside the repository; this description is their record.) The first user is
AppManager::setup( )'s uart_init( uart1, 115200 ), a few statements after; Core 1 starts later. Indirect calls are
invisible to that walk; the constructors' source was read too.

Every clk_peri user, before -> after (computed, not measured)

user asks before (48 MHz) after (133 MHz)
panel write, SPI0 (library, DMA push) 66.5 MHz (was 62.5) 24.000 MHz 66.500 MHz
panel GRAM read, SPI0 (screenshot, mirror) 12 MHz 12.000 MHz 11.083 MHz (about 8 % slower)
XPT2046 touch, SPI0 2 MHz 2.000 MHz 1.956 MHz
UART1 boot log 115200 115176 baud 115201 baud
ENC28J60 (pico_wiretft) its 10 MHz default PIO from clk_sys: unchanged unchanged
hardware I2C, CYW43, PIO sensors, PWM, ADC, USB clk_sys / clk_adc / clk_usb unchanged

Pico 2 W (clk_peri = clk_sys = 150 MHz before and after): 37.5, 10.714 and 1.974 MHz, 115207 baud. Unchanged.

The ENC28J60 is not a PL022 user: PicoENC28J60 1.3.1 runs its SPI in a PIO state machine clocked from
clk_sys (spi_clkdiv256( spi_hz, clock_get_hz( clk_sys ) )), and enc28j60Config( ) sets no spi_hz. It is not a
PL022 user, so it has no PL022 case and the network does not need a retest for this change.

Tests (written first)

  • test_validators 288 -> 300 cases, suite total 689 -> 701, the READMEs follow (check_readme_numbers.py agrees).
    Red first: with the old constant the constant's case failed.
  • Mutants on a copy of the header, real suite built with g++ and Unity (the worktree untouched):
    prescale += 1 fails test_pl022_prescale_is_even_and_in_range; post-divider off by one fails 9 cases (the touch's,
    "never exceeds the request" among them); > as >= fails 8 (the constant's ceiling, "a request that is a rung" among
    them); -DSIMUT_TFT_SPI_HZ=62500000u fails exactly the constant's case; unmutated 300 of 300.
  • The model against the SDK itself: spi_set_baudrate( ) cut out of the installed spi.c, run over fake registers,
    2,607,539 (clock, request) pairs for 12, 48, 100, 125, 133, 150 and 200 MHz: 0 differences.

Sizes and bytes

image flash used (main -> here) .bin OTA slack
pico_w_release 1,022,516 -> 1,022,732 (+216) 1,034,764 5,379 B
pico_w_test 1,026,948 -> 1,027,156 (+208) 1,039,188 955 B (was 1,163)
pico_w_test_https 1,032,004 -> 1,032,212 (+208) 1,044,244 exempt
pico_w_asserts 1,024,820 -> 1,025,028 (+208) 1,037,060 3,083 B
pico2_w_release 998,624 -> 998,848 (+224) 998,872 slot
pico_wiretft 774,380 -> 774,588 (+208) 786,624 253,519 B

pico_wire, pico_w_alpha and pico_w_air are byte for byte main's (sha256 3fe223cb, 22ba2fae, 0d6adc66).
The first cut had the status fields behind if ( SIMUT_DISPLAY_TFT ); that left those three two bytes different in
handleApiStatus( ) (a cmp operand order), so they are macros like STATUS_SLOT_*. The price is one #if site:
tools/feature_sprawl.json, SIMUT_DISPLAY_TFT in WebManager_Api.cpp, 0 -> 1.

CI gates run locally (ci_gates_local.py): 47 of 47 steps pass (the eleven native suites, the sanitizer pass, cppcheck, 60 s of fuzzing, the README numbers, the sprawl mark, the OTA table). check_flash_budget.py passes on all nine images and --table agrees.

On the bench (2026-10-15, plain Pico + ENC28J60 + ILI9341, the pico_wiretft wiring, one panel module)

what before after
sys.clk_peri (48000000, read on a study build) 133000000, read from this branch's pico_wiretft
sys.tft_hz (24000000, read on a study build) 66500000, read from this branch's pico_wiretft
six 320x40 strips pushed by DMA (a full frame) 57.1 ms (9.5 ms a strip) 21.2 ms (3.5 ms a strip)
full-screen DMA clear 56.4 ms 20.6 ms

The two timing rows come from a study build of the Ângulo UI driver (branch feat/ui-drivers, not on main) that times
each strip and carries the same two changes (clk_peri from clk_sys, 66.5 MHz); the original UI has no per-strip
timer. Pixel integrity at 66.5 MHz, read back from the panel's GRAM (the 11 MHz read path, its own transaction):
five round trips of the calendar's month (60 strips rewritten), every readback identical to the first frame, 0
pixels different
. An A/B walk of the original UI by touch, main 80c3c88 against this branch, both on pico_wiretft
(ab_original.py): 19 captures equal, 1 different only in a live humidity reading (a step where both runs had left for
the dashboard), 18 skipped in both, as in the earlier A/B of 2026-10-15. The touch and the network (HTTPS over the
ENC28J60, every capture of both walks) worked throughout.

Not run on hardware: a Pico W with the panel (pico_w_release, pico_w_test) and the Pico 2 W (pico2_w_release),
and the boot log on UART1. They share the clock code; the Pico 2 W's clk_peri is already clk_sys, so its request
gives 37.5 MHz as before.

66.5 MHz stays one constant; the next rung down is -DSIMUT_TFT_SPI_HZ=33250000u.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx

Ângelo Moisés Alves and others added 3 commits October 15, 2026 22:15
The panel's SPI has run at 24 MHz in every build, not at the 62.5 MHz that
SIMUT_TFT_SPI_HZ asked for, nor at the 31.25 before it. The framework's
main( ) calls set_sys_clock_khz( 133000, true ) (F_CPU of rpipico and
rpipicow), and the SDK's set_sys_clock_pll( ) answers by moving clk_peri to
the 48 MHz USB PLL (PICO_CLOCK_ADJUST_PERI_CLOCK_WITH_SYS_CLOCK is 0, and the
SDK is prebuilt in libpico.a, out of reach of our flags). The PL022 divides
clk_peri by an even prescale times a post-divider, so its ceiling there is
24 MHz. Read on the bench on 2026-10-15, on a study build: clk_sys 133 MHz,
clk_peri 48 MHz, SPI0 24 MHz after a push; a 320x40 strip 9.5 ms, a
full-screen DMA clear 56.7 ms. v2.1.6's "31.25 -> 62.5 MHz" never reached the
wire: its GRAM readback proved the pixels, not the speed. The read-clock
sweep of 2026-09-19 had already drawn the 48 MHz ladder (10 = 8 and
16 = 20 = 12), and MODELO_DE_RECURSOS R-H18 inferred it on 2026-09-25.

What changes, on the six images that drive the panel (pico_w_release,
pico_w_test, pico_w_test_https, pico_w_asserts, pico2_w_release,
pico_wiretft; the macro is SIMUT_DISPLAY_TFT):
- src/main.cpp: setup( ) clocks clk_peri from clk_sys first, with
  clock_configure( clk_peri, 0, CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS,
  f, f ), f = clock_get_hz( clk_sys ), and skips it when clk_peri already is
  clk_sys (the Pico 2 W: its F_CPU is the SDK's own, so main( ) never calls
  set_sys_clock_khz( )). It is `if ( SIMUT_DISPLAY_TFT )`, not an #if, so the
  entanglement gate gets no new site there.
- src/simut_config.h: SIMUT_TFT_SPI_HZ 62.5 -> 66.5 MHz, the ceiling of
  clk_peri = 133 MHz. On the RP2350 (clk_sys 150 MHz) the same request gives
  37.5 MHz (prescale 2, post-divider 2), as 62.5 did; its ceiling would be
  75 MHz, not tried. The next rung down on the Pico W is 33.25 MHz; the
  manual's 31.25 is no rung of this clock (22.17). The comments that said
  "clk_peri at 125 MHz" (simut_config.h, DisplayManager.cpp) are rewritten,
  and so are the wire times in DisplayManager_Dashboard.cpp.
- src/display/Pl022Baud.h: the SDK's spi_set_baudrate( ) as a pure function
  (prescale, post-divider, baud for a clk_peri and a request).
- GET /api/status: sys.clk_peri and sys.tft_hz, on the panel images only. The
  second is read from the PL022 registers in dmaTftOpen( ) after the panel's
  transaction begins (spi_get_baudrate( spi0 )); 0 until the first push. Read
  on the bench before this change: 48000000 and 24000000. They are macros like
  STATUS_SLOT_*: an `if ( SIMUT_DISPLAY_TFT )` there left alpha, Air and
  pico_wire two bytes different in handleApiStatus( ) (a cmp operand order),
  and those three must stay main's bytes. That is one new #if site:
  tools/feature_sprawl.json, SIMUT_DISPLAY_TFT in WebManager_Api.cpp, 0 -> 1.

Nothing before the call configures a clk_peri block. The direct call graph
read from the disassembly of main's six panel images, from every static
initializer (13 to 16 per image), from the SDK's runtime init and from main( )
up to setup( ), reaches no spi_init, uart_init, SerialUART::begin or
SPIClassRP2040::begin; the clock code it reaches is the SDK's own
(runtime_init_clocks and, on the RP2040 images, set_sys_clock_pll from main( )).
The first user is AppManager::setup( )'s uart_init( uart1, 115200), a few
statements after the call; Core 1 starts later still. Indirect calls are
invisible to that walk; the constructors' source was read as well.

Every clk_peri user, before -> after (clk_peri 48 -> 133 MHz; computed with
Pl022Baud.h and the SDK's uart_set_baudrate( ) formula, not measured):
  panel write, SPI0 (library begin( ), DMA push)  asks 66.5 (was 62.5):
                                              24.000 -> 66.500 MHz
  panel GRAM read, SPI0 (screenshot, mirror)  asks 12:   12.000 -> 11.083 MHz
  XPT2046 touch, SPI0                         asks 2:     2.000 ->  1.956 MHz
  UART1 boot log, uart_init( uart1, 115200 )  115176 -> 115201 baud
  ENC28J60 (pico_wiretft): not a PL022 user. PicoENC28J60 1.3.1 runs its SPI
    in PIO from clk_sys (spi_clkdiv256( spi_hz, clock_get_hz( clk_sys ) )),
    asking its 10 MHz default; unchanged.
  hardware I2C: clk_sys in the SDK (i2c.c). SIMUT's sensors are PIO. Unchanged.
  CYW43 (PIO), PWM, ADC, USB: clk_sys / clk_adc / clk_usb. Unchanged.
  Pico 2 W (clk_peri = clk_sys = 150 MHz before and after): 37.5, 10.714,
    1.974 MHz, 115207 baud. Unchanged.
Every one is inside its part's range. The read goes about 8 % slower; it was
validated at exactly 12 MHz, so a slower read is on the safe side.

Tests, written first (the plan was written before the code; the first run had
the constant's case red). test_validators 288 -> 300 cases, the suite total
689 -> 701, the three READMEs. On a copy of the header, with the real suite
compiled by g++ and Unity: prescale += 1 fails
test_pl022_prescale_is_even_and_in_range; the post-divider off by one fails 9
cases (the touch's, "never exceeds the request", the 125/133/150 MHz rungs);
`>` as `>=` fails 8 (the constant's ceiling, "a request that is a rung", the
touch); -DSIMUT_TFT_SPI_HZ=62500000u fails exactly the constant's case; the
unmutated copy passes 300 of 300. The model against the SDK's own function,
cut out of the installed framework's spi.c and run over fake registers:
2,607,539 (clk_peri, request) pairs for 12, 48, 100, 125, 133, 150 and 200 MHz,
0 differences.

Sizes, flash used / .bin (main 80c3c88 -> here):
  pico_w_release     1,022,516 -> 1,022,732 (+216) / 1,034,548 -> 1,034,764
  pico_w_test        1,026,948 -> 1,027,156 (+208) / 1,038,980 -> 1,039,188
  pico_w_test_https  1,032,004 -> 1,032,212 (+208) / 1,044,036 -> 1,044,244
  pico_w_asserts     1,024,820 -> 1,025,028 (+208) / 1,036,852 -> 1,037,060
  pico2_w_release      998,624 ->   998,848 (+224) /   998,648 ->   998,872
  pico_wiretft         774,380 ->   774,588 (+208) /   786,416 ->   786,624
pico_wire, pico_w_alpha and pico_w_air are byte for byte main's, by sha256
(3fe223cb, 22ba2fae, 0d6adc66). pico_w_test is left 955 B under the OTA
ceiling (it was 1,163). Budgets, "bin" records and the OTA headroom table
(PLANO_STABLE.md 3.1) moved to match; check_flash_budget.py passes on all
nine and --table agrees.

Not verified on hardware: nothing here ran on a board. The bench checks are
in the pull request: sys.clk_peri 133000000 and sys.tft_hz 66500000 (Pico 2 W:
150000000 and 37500000), a 320x40 strip and a full-screen clear against the
9.5 and 56.7 ms above, a GRAM readback of a still frame at 66.5 MHz (fall back
to -DSIMUT_TFT_SPI_HZ=33250000u if a pixel is wrong), touch, and the boot log
on UART1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx
…list and the resource model state it

The documents that quote the clock follow the code. docs/_manual (chapters 2,
3, 26 and 30) and docs/FEATURES in three languages say 66.5 MHz where they said
62.5; chapter 3's compile-time example for a panel that shows stray pixels now
passes 33250000u, the next rung (31250000u is no rung of clk_peri = 133 MHz and
runs at 22.17 MHz); chapter 2 says the web mirror's read-back runs at about
11 MHz (it asks 12 and gets 11.08); chapter 26 and docs/MANUAL.md list the two
new /api/status fields. docs/MANUAL.pt-BR.html is regenerated by
tools/build_manual.py from the chapters, and its diff is those lines.
MODELO_DE_RECURSOS.md closes R-H18: it inferred on 2026-09-25 that clk_peri
was 48 MHz, and the bench read it on 2026-10-15.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx
…sured; test: two divider edges pinned

Bench, 2026-10-15, plain Pico + ENC28J60 + ILI9341 (the pico_wiretft wiring), one panel module:
- this branch's pico_wiretft reports sys.clk_peri 133000000 and sys.tft_hz 66500000 (48000000 and 24000000
  read before the change on a study build);
- a full frame of six 320x40 strips is pushed in 21.2 ms (57.1 before), a full-screen DMA clear takes 20.6 ms
  (56.4), timed by a study build of the Angulo UI driver that carries the same two changes;
- pixel integrity at 66.5 MHz: five round trips of the calendar's month, 60 strips rewritten, every GRAM readback
  identical to the first frame, 0 pixels different;
- an A/B walk of the original UI by touch, main 80c3c88 against this branch: 19 captures equal, 1 different only in a
  live humidity reading (a step where both runs had left for the dashboard), 18 skipped in both; the touch and the
  network (HTTPS over the ENC28J60) worked throughout.
Not run on hardware: a Pico W with the panel, the Pico 2 W, the boot log on UART1.

Review of a433044..94cac27:
- CHANGELOG (both languages) said the bus had run at 24 MHz "in every build since the TFT was added"; measured is
  one build (arduino-pico 5.6.1) plus the read-clock sweep of 2026-09-19. It now says so, and that the 5.4.x builds,
  v2.1.6 among them, carry the same clock code unmeasured.
- docs/analysis/ESPELHO_DELTA.md is Living (docs/README.md) and still named 62.5 MHz twice; it names 66.5 MHz and the
  24 MHz history now.
- Two mutants of Pl022Baud.h survived the 300 cases: the product prescale x 256 x request in 32 bits (it passes 2^32 at
  2 x 256 x 8,391,750) and a `>= 254` refusal. Two asserts in
  test_pl022_a_request_below_what_the_divider_makes_is_refused pin both: clean 300 of 300; the 32-bit product fails
  it ("Expected 2 Was 16"), the `>= 254` fails it ("Expected 254 Was 0"). The case count stays 300.
- docs/analysis/MODELO_DE_RECURSOS.md's anchors into simut_config.h moved with the constants (115-147, 147, 190).
- simut_config.h's comment says the 66.5 MHz figure was validated on this module and wiring, and what to do on another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyLm7Q1o7kCHngE63BnoTx
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests Testing infrastructure and test coverage display Display, TFT, touchscreen, and themes tools Build tools, scripts, and developer tooling code Firmware source changes (src/, WebUI.h) labels Oct 16, 2026
@angeloINTJ
angeloINTJ merged commit 2d6650b into main Oct 16, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code Firmware source changes (src/, WebUI.h) display Display, TFT, touchscreen, and themes documentation Improvements or additions to documentation tests Testing infrastructure and test coverage tools Build tools, scripts, and developer tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant