Skip to content

sdboot: fix the SD card SPI clock (initialization speed, divisor rounding, bus, stale image) - #2386

Open
cwssemi wants to merge 2 commits into
ucb-bar:mainfrom
cwssemi:sdboot-spi-clock
Open

cwssemi wants to merge 2 commits into
ucb-bar:mainfrom
cwssemi:sdboot-spi-clock

Conversation

@cwssemi

@cwssemi cwssemi commented Oct 2, 2026

Copy link
Copy Markdown

The FPGA boot ROM (sdboot, used by the VCU118, VC707 and ZCU104 targets) can drive the SD card at the
wrong SPI clock, for four independent reasons. Any one of them is enough to break or slow SD boot.
This PR fixes all four, and lets a design set the divisors directly when its SPI controller is not on
the peripheral bus.

Related: #1875 and #2119 report SD timeouts and slow or failed boots on the VCU118; this is a likely
cause. The diff posted in #2119 (2025-06-15), which lowers the initialization clock, points the same
way; this PR derives both clocks from the configuration instead of fixing divisors.

1. The card is initialized at full speed. sd_poweron() sets SCKDIV to the 25 MHz divisor
before CMD0. The SD specification requires the identification phase (CMD0 to ACMD41) at 100–400 kHz.
Since #1685, both SCKDIV writes use SPI_DIV, and SPI_CLK was then raised to 25 MHz, so from 1.11.0
the card is initialized at 25 MHz or more. #1875 reports SD worked with 1.8.1. Initialization now uses
SPI_INIT_CLK, 250 kHz, which keeps a margin from both ends of the range; SPI_DIV is used from the
copy onward. The card's own power-up, not the SPI clock, sets how long identification takes (the
ACMD41 loop polls until the card is ready), so the slower clock costs a few milliseconds.

2. The divisor rounds down, so SCLK rounds up. The controller produces SCLK = F / (2 (div + 1)).
Integer division truncated div, which runs the card above SPI_CLK whenever the clock is not a
multiple of 50 MHz: 30 MHz at 120 MHz, 31.25 MHz at 125 MHz, 37.25 MHz at 149 MHz. The divisor now
rounds up, so SCLK never exceeds the requested frequency.

3. The ROM is built from the system bus clock. The SPI controller sits on the peripheral bus
(SPIAttachParams.controlWhere = PBUS), so its divisor must come from the peripheral bus frequency.
They agree only when all buses run at one frequency; with WithPeripheryBusFrequency below the system
bus, the card is clocked too slowly. A design that attaches the SPI controller another way, to another
bus or across a clock crossing, can now set both divisors itself:

new WithSDBootSPIDivisors(init = 249, copy = 3)

Each is checked against the controller's sckdiv width (SPIParams.divisorBits) at elaboration.

4. The image is cached across configurations. The make rule depended only on the sources, not on
PBUS_CLK, so the first configuration elaborated in a tree fixed the ROM for every later one,
whatever its clock, and, through the vc707/zcu104 symlinks, whatever its board. The image builds in
about 0.1 s, so it is now always rebuilt. The make invocation moves into one helper,
SDBoot.makeVars, which the three boards share, as they share the sources.

Evidence. Divisors written by the built image (riscv64-unknown-elf-gcc 13.2.0), read from its
disassembly, before and after:

clock init SCLK, before → after copy SCLK, before → after
50 MHz 25.000 → 0.250 MHz 25.000 → 25.000 MHz
100 MHz 25.000 → 0.250 MHz 25.000 → 25.000 MHz
120 MHz 30.000 → 0.250 MHz 30.000 → 20.000 MHz
125 MHz 31.250 → 0.250 MHz 31.250 → 20.833 MHz
149 MHz 37.250 → 0.250 MHz 37.250 → 24.833 MHz
150 MHz 25.000 → 0.250 MHz 25.000 → 25.000 MHz

Cause 4, reproduced in one tree: make bin PBUS_CLK=50 then make bin PBUS_CLK=125 leaves the 50 MHz
image before this change; after it, the second image matches a fresh build at 125 MHz.

Elaborated with RocketVCU118Config (100 MHz): the ROM writes divisors 199 (250 kHz) and 1 (25 MHz).
With WithSDBootSPIDivisors(150, 5) it writes 150 and 5; with (5000, 5) elaboration fails with
"each divisor must fit the SPI controller's 12-bit sckdiv (0 to 4095)". Checked on a 1.14.0 tree, whose
sdboot, vcu118 and vc707 sources match main; zcu104 is not in 1.14.0, so its two-line change
is not compiled here.

A more general option, not in this PR. The boot ROM could read the SPI controller's clock from the
device tree appended to it (the sifive,spi0 node's clocks), which makes the image independent of
the configuration and right for any attachment. We have a working version; it adds about 1.6 KB to the
image, which for a typical device tree takes the ROM past 8 KiB to its next power of two. Happy to
offer it separately if that tradeoff is wanted.

Testing. Found while preparing an FPGA bring-up for hardware we are still waiting on, so this has
not yet been tested on a board. The evidence above is from the built images, the make flow and
elaboration. We will add a board result when the hardware arrives; in the meantime, testing on a VCU118
that currently times out would be very welcome.

Type of change: bug fix. Impact: software (RISC-V boot ROM) and build system.

The FPGA boot ROM (sdboot, shared by vcu118, vc707 and zcu104 through
symlinks) can drive the SD card at the wrong SPI clock for four independent
reasons. Each is enough on its own; this fixes all of them.

1. The card is initialized at full speed. sd_poweron() set SCKDIV to the
   25 MHz divisor before CMD0, but the SD specification requires the
   identification phase at 100-400 kHz. Since ucb-bar#1685 both divisor writes have
   used SPI_DIV, and SPI_CLK was then raised to 25 MHz. Initialization now
   uses SPI_INIT_CLK, 250 kHz, with margin from both ends of the range; SPI_DIV
   is used from the copy onward. The card's own power-up, not the SPI clock,
   sets how long identification takes, so this costs a few milliseconds.

2. The divisor rounds down, so SCLK rounds up. SCLK = F / (2 (div + 1)), and
   integer division truncated div, running the card above SPI_CLK whenever
   the clock is not a multiple of 50 MHz: 30 MHz at 120 MHz, 31.25 MHz at
   125 MHz, 37.25 MHz at 149 MHz. The divisor now rounds up, so SCLK never
   exceeds the requested frequency.

3. The ROM was built from the system bus clock. The SPI controller sits on
   the peripheral bus (SPIAttachParams.controlWhere = PBUS), so its divisor
   must come from the peripheral bus frequency. They only agree when all
   buses run at one frequency.

4. The image is cached across configs. The make rule depended only on the
   sources, not on PBUS_CLK, so the first config elaborated in a tree fixed
   the ROM for every later one, whatever its clock (and, through the
   symlinks, whatever its board). The image takes about 0.1 s to build, so
   it is now always rebuilt.

Divisors written by the built image, before and after:

  clock    init SCLK (before -> after)    copy SCLK (before -> after)
   50 MHz    25.000 -> 0.250 MHz            25.000 -> 25.000 MHz
  100 MHz    25.000 -> 0.250 MHz            25.000 -> 25.000 MHz
  120 MHz    30.000 -> 0.250 MHz            30.000 -> 20.000 MHz
  125 MHz    31.250 -> 0.250 MHz            31.250 -> 20.833 MHz
  149 MHz    37.250 -> 0.250 MHz            37.250 -> 24.833 MHz
  150 MHz    25.000 -> 0.250 MHz            25.000 -> 25.000 MHz
The divisors are derived from the peripheral bus clock, which is the SPI
controller's clock when it is attached through PeripherySPIKey. A design
that attaches the controller another way, to another bus or across a clock
crossing, can now set both divisors instead:

  new WithSDBootSPIDivisors(init = 249, copy = 3)

`init` is used for card identification (100-400 kHz), `copy` for the
payload. Each is checked against the controller's sckdiv width
(SPIParams.divisorBits). sd.c takes them as SPI_INIT_DIV and SPI_DIV when
defined at build time, and derives them otherwise; the Makefile passes
either through when set.

The make invocation for sdboot moves into one helper, SDBoot.makeVars,
shared by vcu118, vc707 and zcu104, since the three build the same sources
through symlinks.
@iansseijelly

Copy link
Copy Markdown
Contributor

Thank you for this PR. Can you kindly let us know when you test this on a hardware board? Thank you!

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