USB¶
USB is the primary transport for embedded devices: fast, always available on a dev board, and — because WireWeaver uses a vendor-specific interface with plain bulk or interrupt endpoints — it needs no drivers on Windows, macOS or Linux. The host side is nusb, a pure-Rust USB library, so there is no libusb to ship either.
Host side¶
USB is the default feature of wire_weaver_client. A generated client connects to the first device that matches its
default filter, or to the one picked with a config closure:
let blinky = Blinky::new().connect().await?; // default config of the driver crate
let blinky = Blinky::config(|c| c.usb_vid_pid(0xC0DE, 0xCAFE).serial_eq(serial)).connect().await?;
Devices can be selected by VID:PID, by physical port (usb_port_chain, survives re-enumeration), by serial string,
user label or API (user_label_eq, implements_api), or just "any WireWeaver device" (.usb()). All of these are
read from string descriptors, so the host doesn't have to open a device to know what it is.
ww list shows every connected device with its API (install the CLI with
cargo install --git https://github.com/vhrdtech/wire_weaver wire_weaver_cli):
$ ww list
LOCATION PRODUCT SERIAL API HASH LABEL
usb 003-1 c0de:cafe Nucleo G0B1RE blinky 21002200175036344B333720 blinky_api@0.1.0 042c28cc0c9da99b Nucleo on the desk
Filters can be combined: --api blinky_api@^0.1 (name with an optional SemVer requirement), --label <label>,
--product <substring>, --serial <substring>, --usb-path 003-1, ... --all also lists devices without an API
id, and --plain prints one line per device for scripts. See Command line tool for all options and
for keeping the selection in a project ww.toml. From code, wire_weaver_client::list_usb_devices() gives the same
list.
Device side¶
wire_weaver_usb_embassy (in the mcu/ workspace) puts the generated server on top of any
embassy-usb driver. usb_init() returns the USB device to run and the server, each
runs in its own task:
const MAX_USB_PACKET_LEN: usize = 64; // 64 for Full Speed, 512 (Bulk) or 1024 (Interrupt) for High Speed
const MAX_MESSAGE_LEN: usize = 1024; // longest WireWeaver message
static USB_BUFFERS: StaticCell<UsbBuffers<MAX_USB_PACKET_LEN, MAX_MESSAGE_LEN>> = StaticCell::new();
let driver = Driver::new(p.USB, Irqs, p.PA12, p.PA11);
let link_config = LinkConfig::new(
blinky_api::BLINKY_API_FULL_GID,
server_impl::api_hash(),
ww_client_server::COMPACT_VERSION,
);
let (usb, server) = usb_init(
driver,
USB_BUFFERS.init(UsbBuffers::default()),
UsbTimings::fs_higher_speed(),
link_config,
server_impl::API_ID,
|config| {
config.product = Some("Nucleo G0B1RE blinky");
config.serial_number = Some(embassy_stm32::uid::uid_hex());
},
);
spawner.spawn(unwrap!(usb_task(usb))); // usb.run().await
spawner.spawn(unwrap!(ww_server_task(server, state))); // server.run(&mut state).await
examples_mcu/nucleo_g0b1re/src/bin/usb_blinky.rs is the complete firmware, examples/blinky the host side.
server.run() is the whole event loop; to wait on other things next to it (UART data, timers), use
server.wait() / server.handle() in your own loop, see link layer.
A driver with endpoint kinds usb_init() doesn't name (e.g. embassy-stm32's double-buffered bulk endpoints on USB
v4 parts) can use usb_init_with_endpoints() instead: same as usb_init(), with a closure allocating the OUT and
IN endpoints.
VID:PID defaults to the shared test ID 0xc0de:0xcafe seen above and can't be changed from the config_mut
closure (embassy-usb keeps Config::vendor_id/product_id crate-private). Pass your own assigned VID:PID with
usb_init_with_ids() / usb_init_with_endpoints_with_ids() instead, which take a UsbIds { vid, pid } argument
right before the closure.
UsbTimings also sets the accumulation window the host and device batch messages in (1 ms for
fs_higher_speed()): a request or reply waits for it at most, an urgent one (.urgent() on the host) not at all,
see latency and priority.
What you get¶
- Plug and play: hot-plug is handled — a device that disappears fails outstanding requests and can be re-connected without touching the rest of the application.
- High message rate: many small requests and events are packed into one USB packet, so throughput is bounded by bandwidth, not by packets per second. Large messages are transparently split across packets and reassembled.
- Full duplex: sending and receiving never block each other on the host, so a device that is busy answering cannot stall the host and vice versa (see ww_link for why this matters).
- Version and identity check during link setup, before any application data flows.
Supported MCUs¶
Any MCU with an embassy-usb driver. Tested on STM32G0B1 (Full Speed), STM32H743 (Full Speed), STM32H725 with a
ULPI PHY (High Speed, 480 Mbit/s) and RP2350, see the firmware examples.
Limitations and notes¶
- Endpoint addresses (
0x01/0x81) and interface0are currently fixed. - Isochronous endpoints are not supported (nor needed).
- On Linux no udev rule is required for enumeration, but opening the device needs read/write access to
/dev/bus/usb/...— a rule granting that to your user is the usual setup.
More: how USB works inside — device layout, identity strings, framing, transfer queues, reconnection.
See also¶
- How USB works inside
- ww_link — session lifecycle, messages, the two-task design
- Framer — packing and splitting messages
- Transport overview