FMC Hardware Bring-Up
The panel is a Riverdi RVA35HI carrying an ILI9488 controller, driven over an 8-bit FMC parallel bus (Intel 8080 style) rather than SPI. This page documents the hardware side of that driver: bus configuration, addressing, DMA, and the tearing-effect line, plus the debugging story behind the choices, because the failure modes here were unusual enough to be worth recording.
Why 8-bit, not 16-bit
The ILI9488 supports several parallel interface widths, selected by three DIP switches on the panel (see the truth table in Track Operations). This board runs MCU8 (8-bit) because:
- It matches the known-good reference project this driver was built against, so a known-working configuration was available to diff against at every step.
- It only needs data lines D0-D7, leaving D8-D15 unused (the schematic still routes them, but
fmc.conly configures D0-D7 as FMC alternate function). - 16-bit mode was tried early on and worked for raw coverage, but mangled color output on this panel and forced a slower CPU-driven transfer instead of DMA.
FMC addressing
The FMC exposes two address windows for this bank:
FMC_BANK1_REG = 0x60000000: the command window. DCX (data/command select,FMC_A20on PE4) is low here.FMC_BANK1_MEM = 0x60000000 | 0x100000: the data window. DCX is high here.
In 8-bit bus width, the CPU address maps directly to the external address lines with no shift (unlike 16-bit width, which right-shifts by one bit). That's why the data window offset is 0x100000 (bit 20 set, matching DCX on FMC_A20) rather than the 0x200000 used by an earlier, incorrect 16-bit-derived version of this driver. The MPU region covering this bank is configured Device, non-cacheable with a limit just past 0x60100000, which is enough since every write in this driver targets one of exactly two fixed addresses (the panel's own internal write pointer auto-increments, not the CPU's).
GPDMA configuration
Pixel data moves over GPDMA1_Channel6 as a memory-to-memory transfer: source is the framebuffer block in RAM (halfword-wide RGB565 pixels), destination is the fixed FMC_BANK1_MEM address. The channel is configured with DMA_EXCHANGE_DEST_BYTE, which splits each 16-bit source halfword into two 8-bit destination writes automatically, so a single DMA descriptor can stream RGB565 pixels onto an 8-bit bus without any CPU involvement per byte.
Tearing effect (TE)
TE is on PF13/EXTI13 on this board (the reference project uses PE13, which is FMC_D10 here, so it had to move). gpio.c configures it IT_RISING_FALLING and stm32h5xx_it.c routes EXTI13_IRQHandler to it. See TouchGFX Integration for how it paces rendering.
The debugging story
Worth recording because the failure signature was misleading for a long time. Early symptoms: the panel only ever rendered a small, fixed fraction of the screen (initially around 5 to 16 percent), the rest stayed blank, and what did render was streaky and discolored. That looked exactly like a software pacing bug (partial framebuffer allocator stalling, TE not actually working), and a lot of time went into that theory: bypassing TE gating, synthesizing vsync, switching to a single full framebuffer with a CPU blit. None of it fixed the coverage, and the single-framebuffer detour made image quality worse, not better, since it also gave up DMA.
The actual diagnosis process, once it went back to first principles:
- Confirmed TE and DMA both genuinely work. Breakpoints on
HAL_GPIO_EXTI_Rising_Callback,DMA_TxCpltCallback, andtearingEffectCountall fired correctly and repeatedly. This ruled out the "TE is dead" theory that had driven the earlier single-framebuffer detour. - Reproduced the same coverage limit with the partial-framebuffer/allocator/TouchGFX layer completely bypassed. A raw, single-shot full-screen DMA fill (no TouchGFX, no block allocator, no TE gating) showed the same fixed-fraction coverage. This ruled out the rendering strategy entirely: whatever was wrong, it was below TouchGFX.
- Reproduced it again with DMA replaced by a plain CPU write loop. Same result, which ruled out anything GPDMA-descriptor-specific (like a transfer-size register limit).
- Slowed the FMC bus timing roughly tenfold and lowered GPIO drive strength. Zero change in either case. This ruled out signal timing (setup/hold) and slew-rate/ringing as the cause, which are the two most common "flaky parallel bus" explanations.
- Switched from a solid-color test fill to a position-encoded pattern (a repeating 8-color band keyed to column position, later a top/bottom split keyed to row position) instead of a single flat color. This is the key technique: a solid fill only tells you how much rendered, not where the cutoff is or whether it's tied to a specific screen location versus a specific point in the transfer. The position-encoded pattern showed the same physical region always responding, regardless of which axis (row or column) carried the pattern, and regardless of the wrong-bus-width DIP settings tried as a control.
- That "same physical region regardless of the data sent" result pointed away from timing, addressing, and software entirely, toward a physical connection problem: a marginal solder joint or FPC seating issue on the display connector (
J200). Reflowing the connector produced a measurable, immediate improvement (previously noisy/discolored output in the working region became clean), confirming the diagnosis.
The lesson generalized to the checklist in Track Operations: a coverage limit that doesn't move when you change the data pattern, the transfer duration, the DMA vs. CPU path, or the bus timing is very unlikely to be a software bug at all.
Open item: red/blue swap
An earlier debugging pass suspected the panel's red and blue channels were swapped (MADCTL BGR bit), based on phone-camera photos with a visible color cast. That conclusion is not trustworthy: phone cameras apply their own color correction, and the observation was never repeated with a clean, by-eye, solid-primary test. Before touching MADCTL or adding any swap logic, run a full-screen solid red (0xF800) then solid blue (0x001F) test and judge by eye, not camera. If a swap is confirmed, fix it at the MADCTL register or in TouchGFX asset generation (BGR export), not with a per-pixel CPU swap in the blit path, since that would force giving up DMA.
Regenerating from CubeMX
See Track Operations for the general rule. Concretely for this board: the .ioc FMC block width and the hfdcan1.Init.StdFiltersNbr field are both fully replaced by CubeMX on every regeneration, since they live outside USER CODE markers. Set the actual FMC bus width and CAN filter count in the CubeMX GUI before generating, not just in hand-edited source, or the next regeneration will quietly overwrite the fix.
