embedded-graphics

Hardware notes

What running the embedded-graphics backend on real panels established, kept here so the code can state the conclusion in a sentence.

Solid fills on a parallel bus: why PacedFill exists

An 8080-style parallel panel latches a byte on the rising edge of its write strobe, and the controller has a minimum write cycle — 66 ns on the ST7789v — that the strobe has to respect. When every pixel of a run has the same byte in both halves, mipidsi sends the word once and then loops on the strobe alone:

wr.set_low();
wr.set_high();

Two register stores. On a 125 MHz RP2040 in release that is a write cycle of roughly 24–40 ns — inside the controller's minimum, so it mislatches, and the fill arrives as noise.

What costs enough time on the ordinary path is the call, not the pins. Generic8BitBus::set_value returns early when the value is unchanged (mipidsi's interface/parallel.rs: "quite common for multiple consecutive values to be identical … so let's optimize for that case"), so the pins are skipped there too. send_word stays out of line at opt-level = "z", and that call is what stretches the cycle — incidental codegen, worth knowing because inlining it would bring the fault back with nothing changed in the wrapper.

The shortcut triggers whenever the two bytes of the pixel are identical, which for Rgb565 is 256 values and not two — 0x0000 and 0xFFFF among them, but 0x1818 is a dark blue that takes the same path. Ink and background are two of the 256, so on a monochrome-styled panel every filled rectangle and every screen clear breaks while text, drawn pixel by pixel, comes out perfectly. A panel showing crisp type over static is this fault, and it reads like a framework bug rather than a timing one.

PacedFill routes fill_solid and clear through fill_contiguous, which has a colour for every pixel and therefore no run to shorten. A whole 320x240 screen is instruction-counted at roughly 120 ms that way — a loop interval is 10 ms, so this is not free — and that figure has not been measured on the board. Measure it before quoting it.

The wrapper lives in this crate rather than beside the firmware that needs it because it names no HAL, no pin and no board: it is a DrawTarget that wraps a DrawTarget. Beside the firmware it is untestable, and the property it exists for — that two methods are not forwarded — is invisible on the host and in every snapshot, and only appears on hardware.

Edit this page on GitHubIt lives in xpui-backends; a correction goes there.