Repository navigation
v0.8.0
Fixed
-
sd::Sd::initaftersdio::Sdio::initreported 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::initroutes GPIO48-53 off the
controller so it drives the wireless pins alone, whileSd::init
routed GPIO48-53 on without taking GPIO34-39 off. Both groups on one
controller is the radio and the card driving the sameCMD/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 — andavahi-resolvethen 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. -
sdiodrives the BCM43455 as well as the BCM43430, which is the
radio on a Pi 3B+and a Pi 4. Before this,load_firmwareon one of
those stopped atCoreNotFoundbefore a byte was written.It was not a chip id to accept.
scan_coreslooked 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
ChipCoresnow carries aCpu, 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'sintstatusis 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, withEthernetRx/EthernetTxfor
the two halves — theasynccounterpart toEthernet, carrying
split()and the awaited bring-up. Behind theasyncfeature.Separate from
Ethernetrather 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::asynchdocuments. 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 byEthernetRx::receive_frames_asyncis 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.rsnow claims either chip and runs generic
over the trait, holding the split halves for the whole session the way an
embassy-netadapter does. -
usb::lan7800'sasynchalf — the interrupt-driven twins of its
transfer methods, andsplit()into borrow-disjoint receive and
transmit halves, matching whatusb::lan9514has had. Behind the
asyncfeature. This is what anembassy-netadapter for the chip
needs; without it a Pi 3B+ could move frames but only by spinning.start_asyncconfigures the chip's empty-receive response the
opposite way tostart, 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:
startfollowed byreceive_frames_asyncyields 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.rsis 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::lan9514andusb::lan7800were 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, andexamples/usb_ethernet.rslost 208 lines of per-method
dispatch to it:run_ethernetis now generic over the trait and its
receive loop reads like either driver's own.IdRevisionmoved 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::IdRevisionstill resolves.This is the blocking surface only. The split into borrow-disjoint halves
that an executor needs will be a separateEthernetAsyncextension —
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.rsnow 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 withusb::Busrather thanusb::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_promiscuousis 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 withset_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.cto the LAN88xx core inside the chip, which
programs DSP and MDIX registerslan78xx.cnever 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. AndUSB_CFG0.BIRhas 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).enumerateis 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.Busholds both across calls, soBus::polldoes what
Bus::enumeratedid, later and for one port.pollreads 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 returnedFrameOverrun
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,
buthub_activate()reads every port directly on init, resume and
reset, and ten consecutive endpoint errors force exactly that reset.
Hub::status_endpointis exposed for a caller who wants it, and for
pollto build on once the periodic scheduling is fixed — which would
not change whatpollpromises.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 whyEvent::Detachedcarries it — a driver still talking to it
afterwards is talking to whatever arrived next.usb::enumerateis unchanged and now a thin wrapper over
Bus::enumerate, so nothing that only needs the one-shot walk has to
move.usb::hub::Hubgainedstatus_endpoint(located while
configurewas already reading the configuration descriptor) and is now
Copy. -
usb::enumeratewalks 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::Devicegainedhub_addressanddepthto 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 adyn 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_targetreturns. 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::controltake the hub's
ControlEndpointinstead of its address and low-speed flag —
set_port_power,get_port_status,set_port_resetand
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::configurewaits 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, andenumerate
remains a one-shot snapshot of the bus. -
usb::hub::Hub::configuretakes ahigh_speedflag and exposes it
as a field, alongside a newHub::endpointaccessor. Breaking.
ControlEndpointdistinguishes 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.