Skip to content

sd-crc32: ask the card what it already holds, so a serial update sends only what changed #11

Description

@joeferner

Installing a bundle over the network makes the transfer nearly free, so
it is fine that a bundle carries every file on the card whether it
changed or not. Over the cable that assumption inverts: the wire is the
expensive part, and most of what crosses it is already on the card, byte
for byte.

Prerequisite for a serial install being pleasant rather than merely
possible (#10), and worth having on its own.

What a real card looks like

The water sensor's bundle, which is the one that motivated this:

entry size changes
start.elf 3.02 MB with a Raspberry Pi firmware release
bootcode.bin 52 KB with a Raspberry Pi firmware release
fixup.dat 7 KB with a Raspberry Pi firmware release
kernel7.img 1.69 MB every build
www/, config.txt ~22 KB occasionally
bundle 4.79 MB

3.08 MB of that — 64% — is Raspberry Pi's boot firmware, which changes
about once a year.
The part that is actually different on a normal
update is the 1.69 MB kernel.

At 1.5 Mbaud, taking #1's figure of ~27 ms per 4 KB chunk, the whole
bundle is ~32 s of wire time and ~20 s of it is spent sending files the
card already holds. If the bytes go per-file through sd-write rather
than into a staged buffer, the ~167 ms per chunk that issue measures its
estimates against makes it minutes instead. Those are arithmetic on a
baud rate, like #1's, not a measurement of this loader.

The device already computes the answer and throws it away

apply's per-entry install asks on_card before writing anything —
length from the directory entry first, then a CRC-32 of the file — and
skips the write when it matches. A bundle full of unchanged firmware
already installs without writing that firmware.

So the card read this needs is not new work; it happens on every update
today. What makes a serial install slow is only that the bytes were
shipped before the question was asked. Ask first, in one short
command, and the transfer disappears at no extra cost to the card.

The command

  • CMD_SD_CRC32 = 11, PROTOCOL_VERSION 2 → 3. Takes a path the way
    the other sd-* commands do; answers u32 length and u32 CRC-32, or
    the not-found code that already exists.
  • CRC-32/ISO-HDLC via bundle::checksum, so host and device produce the
    same number by construction rather than by two implementations
    agreeing. Same reason the container has one implementation.
  • No new weakening: a checksum instead of a byte compare is the trade
    apply already documents and takes, and this asks the identical
    question.
  • rpi-loader sd-crc32 <remote> as a subcommand too. It earns its
    place without OTA — confirming a card holds the image you think it does
    currently means sd-reading 3 MB back over the wire and comparing on
    the host.
  • If Apply an OTA bundle over the serial cable, for a board the network cannot reach #10 lands as "the firmware links rpi-loader-ota", the device side
    is on_card with its data argument replaced by a (len, crc) pair.
    Publish that from the crate; do not write the second copy.

A batch form — host sends N × (path, len, crc), device answers a bitmap —
is the obvious next optimization and should wait for a measurement. N is
a few dozen, and the per-entry cost is a card read, so round trips are
unlikely to be the term that hurts.

Trimming the bundle

The host asks about every entry, drops the ones that match, and
re-encodes what is left before sending it.

  • Only the copy that goes over the cable is trimmed. The .bundle on
    disk stays whole; a bundle you can archive and apply to a blank card is
    most of the point of having a container.
  • Safe by construction, because the predicate is "the card's bytes
    already are the bundle's bytes". The end state is identical whether the
    entry is sent or skipped, which is why this is not a delta format and
    needs no reasoning about what a file used to be.
  • Trap: Bundle::parse enforces the start*.elf/fixup*.dat
    pairing, and it enforces it on the device.
    Trim start.elf on its
    own and the whole bundle comes back UnpairedFirmware. So the trim
    treats the pair as one unit: both match, both go; either differs, send
    both. That costs nothing real — they are released together and change
    together — and it respects a check that exists to stop a board being
    bricked instead of sneaking past it.
  • If every entry matches, send nothing and say so.

A format v3 alternative would be an entry carrying a checksum and no
data, meaning "the card should already have this": the device could then
re-verify what was skipped, and the pairing rule would see both halves.
Worth writing down and not worth doing yet — it costs a format version,
and while the loader is the only thing running there is nothing that
could change the card between the question and the install.

The network path wants this too

A 4.79 MB upload over Wi-Fi is not free either, and the same trim would
cut it to the kernel. The query there is application-shaped — an HTTP
endpoint answering the same question — but the predicate is the crate's
in both cases, so this should be built as a crate-level question with two
transports rather than as a serial feature.

Acceptance

On hardware: install a bundle over the cable, rebuild only the kernel,
install again — the second transfer carries ~1.7 MB instead of ~4.8 MB
and the board boots the new kernel. Then touch fixup.dat alone and
confirm both firmware halves are resent and the bundle is accepted.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions