Skip to content

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 15:06
· 131 commits to main since this release

Fixed

  • sd::Sd::init after sdio::Sdio::init reported an empty slot. The
    two drivers share one controller and mux it between two pin groups, and
    only one of them was tidying up: Sdio::init routes GPIO48-53 off the
    controller so it drives the wireless pins alone, while Sd::init
    routed GPIO48-53 on without taking GPIO34-39 off. Both groups on one
    controller is the radio and the card driving the same CMD/DAT
    lines, so the card never answered and the slot read as empty with a
    card in it — Error::NoCard.

    Only reachable in that order, which is why it had not been seen: every
    path so far read the card first and then gave it up for good. Asking
    the chip what it is before going to the card for its firmware is what
    found it.

Added

  • wifi::Wifi::set_all_multicast, the counterpart to the Ethernet
    drivers' method of the same name. A board that runs the same protocols
    over either interface needs the same thing turned on for both, and only
    one of them had it.

    Worth knowing what its absence looks like, because nothing fails.
    Broadcast still arrives, so DHCP takes a lease. Transmit is unaffected,
    so an mDNS responder announces itself and fills every cache on the
    network — and avahi-resolve then answers from that cache, which reads
    as a working responder. Unicast is untouched, so the board answers a
    ping and a query sent straight to its address. What never arrives is
    anything addressed to a group, so the responder answers no question
    anyone actually asks it.

    Set it after associating: the firmware resets it on every
    association, the same way it resets the power-management setting.

  • sdio drives the BCM43455 as well as the BCM43430, which is the
    radio on a Pi 3 B+ and a Pi 4. Before this, load_firmware on one of
    those stopped at CoreNotFound before a byte was written.

    It was not a chip id to accept. scan_cores looked for an ARM Cortex-M3
    and a SOCRAM core, and a 43455 has neither: its CPU is an ARM CR4 with
    the RAM tightly coupled inside it. The walk was reading the enumeration
    ROM perfectly correctly and rejecting a chip for not being the one it
    knew.

    So ChipCores now carries a Cpu, and that one enum is what the whole
    download hangs off. Four things differ between the families and they are
    not independent: where the image is written (zero, against a
    chip-specific base far up the address space), whether anything has to be
    brought out of reset before RAM is writable (SOCRAM, against nothing),
    how RAM is measured (two different register maps that agree on the idea
    and on no offset, mask or field width), and how the CPU is started. A
    CR4 also needs its reset vector copied down to address 0 by hand, since
    its image does not land where it fetches that from — the one step with
    no counterpart at all on the older part.

    UnsupportedChip { chip_id } is new and deliberately not
    CoreNotFound: it means the chip was read correctly and every core
    found, and what is missing is a RAM-base constant. That is a much
    shorter thing to go and fix, and the two should not present alike.

    The Cortex-M3 path is unchanged, including where it is arguably
    incomplete — the SDIO core's intstatus is cleared only on the new
    path. Adding a step to a sequence that is verified on hardware, to no
    end, is not a trade worth taking while the new one is unproven.

    A Zero 2 W's 43436 is a third case and is not covered.

    The Wi-Fi examples' firmware buffers went from 512KB to 1MB with it.
    Not a detail: a 43455's image is over 600KB, and a file that does not
    fit is read as far as the buffer goes and its truncated length
    reported — so a short image downloads, starts, and never answers, with
    nothing reported anywhere.

  • usb::ethernet::EthernetAsync, with EthernetRx/EthernetTx for
    the two halves — the async counterpart to Ethernet, carrying
    split() and the awaited bring-up. Behind the async feature.

    Separate from Ethernet rather than part of it, because they are not
    the same promise: one says a chip can move frames, the other that its
    transfers can be awaited, which needs the interrupt wiring
    usb::dwc2::asynch documents. Splitting them is also what let the
    blocking trait land while only one driver had an async half.

    The hazard the trait documents is worth repeating here: Ethernet::start
    followed by EthernetRx::receive_frames_async is neither a compile error
    nor a failure. A driver may configure the chip's empty-receive answer
    differently between the two bring-ups — parking needs a NAK, polling
    needs not-a-NAK — and the mismatch is a receive future that resolves
    immediately with nothing, forever. Making the drivers interchangeable
    makes that easier to reach by accident, so it is stated on the trait
    rather than only per-driver.

    examples/usb_ethernet_async.rs now claims either chip and runs generic
    over the trait, holding the split halves for the whole session the way an
    embassy-net adapter does.

  • usb::lan7800's async half — the interrupt-driven twins of its
    transfer methods, and split() into borrow-disjoint receive and
    transmit halves, matching what usb::lan9514 has had. Behind the
    async feature. This is what an embassy-net adapter for the chip
    needs; without it a Pi 3B+ could move frames but only by spinning.

    start_async configures the chip's empty-receive response the
    opposite way to start, on purpose.
    The blocking path clears
    USB_CFG0.BIR, so a bulk IN with no frame waiting is answered at once
    and an idle poll returns immediately. The async path sets it, so an
    empty FIFO NAKs — and the DWC2 retries a NAK'd bulk channel in hardware
    without halting it, which leaves the transfer parked until a frame
    arrives. That is what makes the receive interrupt-driven rather than
    polled, and it is why the two bring-ups differ.

    Picking the wrong twin is neither a compile error nor a failure:
    start followed by receive_frames_async yields a future that resolves
    immediately with nothing, every time, and a caller looping on it spins
    its executor instead of sleeping. Documented at both ends for that
    reason.

    examples/usb_ethernet_async.rs is the test of exactly that. It drives
    the async path with no pacing delay and counts the receives that come
    back empty; on hardware the count stayed under ten across a run while
    real frames arrived, which is parking working rather than being hoped
    for.

  • usb::ethernet::Ethernet, one description both Ethernet drivers
    answer to.
    usb::lan9514 and usb::lan7800 were written to the same
    surface on purpose so that a consumer ports between them by changing a
    type name; this makes that a type rather than a convention, so code can
    be written once and compiled for either board without being told which.

    The receive iterator is what needed it. Each driver returns its own
    Frames — the same item type, different types — so a function handling
    both had nothing it could return and had to consume the frames where it
    produced them. A generic associated type (type Frames<'a>) removes
    that, and examples/usb_ethernet.rs lost 208 lines of per-method
    dispatch to it: run_ethernet is now generic over the trait and its
    receive loop reads like either driver's own.

    IdRevision moved here and is re-exported from both drivers, since it
    is the same two numbers in the same places and only the ID that means a
    working part differs. usb::lan9514::IdRevision still resolves.

    This is the blocking surface only. The split into borrow-disjoint halves
    that an executor needs will be a separate EthernetAsync extension —
    it is not here because only one of the two drivers has an async half so
    far, and a trait with one implementation would be guessing at the shape.

  • usb::lan7800, a driver for the Pi 3B+'s Ethernet — the Microchip
    LAN7800 half of the LAN7515 that board carries in place of a Pi 2B/3B's
    SMSC LAN9514 (issue #101).
    Until now a 3B+ had no wired network under this crate at all.

    The same surface as usb::lan9514 — from_device, start,
    is_link_up, set_all_multicast, send_frame, receive_frames — so a
    consumer ports between them by changing a type name. The contents share
    nothing: a different register map, a different PHY bring-up, an 8-byte
    transmit header against the LAN9514's, and a 10-byte receive header whose
    padding is computed on the frame length plus two rather than on the
    length alone. That last one is worth knowing, because getting it wrong
    doesn't lose one frame, it loses every frame behind it in the transfer.

    examples/usb_ethernet.rs now drives whichever chip the board has,
    offering each enumerated device to both drivers and taking the one that
    claims it, so a single image covers a 2B/3B and a 3B+. It also walks the
    bus with usb::Bus rather than usb::enumerate, and has to: on a 3B+
    the LAN7800 sits behind two cascaded hubs and attaches seconds after
    power-on, so a one-shot walk finishes before it exists.

    set_promiscuous is here and has no LAN9514 counterpart yet, because
    this chip filters unicast against a perfect-address table by default and
    the bit to turn that off is a natural pair with set_all_multicast.

    The link is 100BASE-TX, not gigabit, and that is deliberate. Left
    advertising 1000BASE-T, the link never comes up at all: base pages
    exchange and the partner acknowledges them, there is no parallel-detect
    or master/slave fault, but 1000BASE-T training never converges — the
    remote-receiver-OK bit never sets — so negotiation restarts and fails
    indefinitely without ever falling back. Training needs analogue setup
    particular to this PHY; Linux gets gigabit because phylib binds
    drivers/net/phy/microchip.c to the LAN88xx core inside the chip, which
    programs DSP and MDIX registers lan78xx.c never mentions. Withdrawing
    gigabit costs little here, since the chip reaches the host over USB 2.0
    and a gigabit line could not be filled through it anyway.

    Two things caught only on hardware, both recorded in the code. The
    receive header's padding is computed on the frame length plus two, not
    on the length alone. And USB_CFG0.BIR has the opposite sense to the
    LAN9514 bit of the same name: here setting it selects NAK and clearing
    it selects the zero-length reply, so carrying the polarity across left
    every idle poll blocked in a NAK'd transfer until it timed out, dropping
    most of the traffic that arrived meanwhile.

  • usb::Bus, which keeps the bus state a walk used to throw away —
    so a device that attaches after startup can still be brought up, and one
    that is unplugged is reported rather than silently left addressed
    (issue #102).

    enumerate is a snapshot, and the reason it could not be anything else
    is that it owned the two things bringing a device up requires and then
    dropped them: the next free address, and the hub topology every split
    target is derived from. A caller that later noticed a connection had
    neither. Bus holds both across calls, so Bus::poll does what
    Bus::enumerate did, later and for one port.

    poll reads every port of every tracked hub with GET_PORT_STATUS and
    compares what the hub reports against what it last recorded there. A
    port that has gained a device gets the full bring-up, including being
    walked in turn if it is a hub with devices already on it; a port that
    has lost one reports every device that was behind it, deepest first, and
    frees their addresses. The comparison is against the port's present
    state rather than its change bits, so a missed sweep is harmless where a
    missed edge would not be.

    A hub's status-change endpoint would name just the ports that moved, and
    that is what a full host polls. It is deliberately not used here. This
    crate's DWC2 driver cannot yet schedule high-speed periodic transfers
    reliably, and on the bench that endpoint both returned FrameOverrun
    and — worse — completed successfully with an all-zero bitmap while the
    hub's own port status showed a connection change outstanding, so a
    device plugged into that hub was never seen at all. Reading the ports
    says the same thing over transfers that work. Linux arranges the same
    safety net from the other side: its hub driver runs on that endpoint,
    but hub_activate() reads every port directly on init, resume and
    reset, and ten consecutive endpoint errors force exactly that reset.
    Hub::status_endpoint is exposed for a caller who wants it, and for
    poll to build on once the periodic scheduling is fixed — which would
    not change what poll promises.

    A port whose device fails to come up is tried once and then left until
    the port's connection changes again, so a device this can't talk to
    costs one attempt rather than a port reset on every sweep.

    This is what a late device needs, and no amount of waiting substitutes:
    on a Pi 3B+ the soldered LAN7800 Ethernet attaches seconds after
    power-on, long after any debounce, and a snapshot taken at boot simply
    does not contain it.

    Addresses are now a 128-bit bitmap rather than a counter, because they
    come back when a device is unplugged and a bus that is replugged all day
    would otherwise run out. A freed address can be reused immediately,
    which is why Event::Detached carries it — a driver still talking to it
    afterwards is talking to whatever arrived next.

    usb::enumerate is unchanged and now a thin wrapper over
    Bus::enumerate, so nothing that only needs the one-shot walk has to
    move. usb::hub::Hub gained status_endpoint (located while
    configure was already reading the configuration descriptor) and is now
    Copy.

  • usb::enumerate walks hubs plugged into hubs. It used to stop one
    level down: a hub in one of the board's ports was reported as a device
    and everything behind it was invisible, so a keyboard on a hub simply
    did not exist as far as this crate was concerned
    (issue #15). The
    per-port bring-up — reset, probe, address, and now configure-and-descend
    when the descriptor says class 9 — is applied to each hub it meets, in
    depth-first order, down to the four levels below the root hub that USB
    2.0 §4.1.1 allows.

    usb::Device gained hub_address and depth to say where on the bus
    the device was found, which a flat walk had no need to report. The
    callback is generic, so the recursion goes through a dyn FnMut
    internally: monomorphizing a generic callback per nesting level is a
    recursion the compiler does not terminate.

    What actually makes a device several levels down reachable is the split
    target, and it is not simply "the hub it is plugged into". A transaction
    translator lives in a high-speed hub, so a full-speed hub below one
    has none of its own — everything under it is on the same full-speed
    segment and belongs to the translator further up, which is now what
    usb::hub::Hub::split_target returns. Getting this wrong is not a
    degraded transfer, it is a transfer addressed to a translator that isn't
    there.

Changed

  • The hub port requests in usb::control take the hub's
    ControlEndpoint instead of its address and low-speed flag
    —
    set_port_power, get_port_status, set_port_reset and
    clear_port_feature. Breaking, and it is what makes the recursion above
    possible: those requests are addressed to the hub, so reaching a hub
    that is itself behind another hub means carrying that hub's own split
    target, which an address and a bool cannot express.

  • usb::hub::Hub::configure waits the connect debounce on top of
    bPwrOn2PwrGood
    , 100ms more per hub. The two waits are for different
    things: the power-good delay is the hub's rail reaching the port, and
    only once it has does the attached device start up and drive the pull-up
    that announces it — which USB 2.0 §7.1.7.3 allows a further 100ms
    (TATTDB) to settle. This is conformance rather than a fix for anything
    observed; no device on the bench was found to need it. It is also not a
    general answer to a device that attaches late, which no fixed delay is:
    that is what a hub's status-change endpoint is for, and enumerate
    remains a one-shot snapshot of the bus.

  • usb::hub::Hub::configure takes a high_speed flag and exposes it
    as a field, alongside a new Hub::endpoint accessor. Breaking.
    ControlEndpoint distinguishes only low speed from the rest, because
    that is the one bit a transfer puts on the wire — but whether a hub has
    a transaction translator at all turns on high speed versus full, so the
    caller that reset the port (and read its speed bits) has to pass it in.