ww_uart¶
Part of the IO trait family. ww_uart is a WireWeaver trait for bridging a UART (serial port) to
the host, and for watching both lines of a serial link without driving them. It is no_std by default and depends
on wire_weaver and ww_si (for Baud rates, the Volt reference-voltage type and Second timestamps).
Add it to a device API crate with:
Traits¶
Uart is one port: configure it, receive on rx, send on the tx credit sink, and, on ports with the hardware for
it, watch both lines of a link between two other devices on the optional monitor
stream. It composes with the rest of the API using ww_impl! and is nested as an
array when a device has more than one UART (ww_impl!(uart[]: ww_uart::Uart), as
examples/uart_api does); monitoring comes with each element, no second array or trait to keep in step.
Uart¶
#[ww_trait]
pub trait Uart {
#[counted]
stream!(rx: RxChunk<'i>);
#[credit]
sink!(tx: MaxLen<RefVec<'i, u8>, UART_TX_MAX>);
fn capabilities() -> Capabilities<'i>;
fn configure(config: UartConfig) -> Result<UartConfig, Error>;
fn set_pin_level(pin: Pin, is_high: bool) -> Result<(), Error>;
fn send_break(duration: Second) -> Result<(), Error>;
fn tx_pending() -> u32;
#[optional]
#[stream(MonitorChunk<'i>, counted)]
fn monitor(config: MonitorConfig) -> Result<MonitorConfig, Error>;
}
rx- received bytes, chunked as they arrive (seeRxChunkbelow). A chunk ends when the line stays idle for more thanUartConfig::idle_charscharacter times, when it is full, or on a line error, so each chunk is one burst of back-to-back characters (a frame of most protocols, or the part of it that fit). A counted stream, opened by calling it: its unit is a byte, so the event head carries each chunk's byte position, the device time of its first byte, and exactly how many bytes the device dropped before it (stream delivery); the chunk itself says when its last byte arrived, if the device measures it (Time below).tx- bytes to send, a credit sink (API-28): the host tracks the device's TX buffer (Capabilities::tx_buffer) and never sends more than it holds, so a large write (e.g. a file piped to the UART) can't overrun it. Its positions count bytes of the payload (API-34), so the device's position is the bytes taken for the line, andcredit_progress()on the host says how many.capabilities()- what this device supports: baud rate range, flow control modes, IO levels, buffer sizes.configure- the whole line configuration in oneUartConfig(baud rate, data bits, parity, stop bits, mode, back-feed detector, IO level, the idle time that ends a chunk), answered with what was actually set: the hardware rounds the baud rate to what its divider can do and the idle time to what its receiver can time, andio_level: Nonekeeps the level and is answered with the level in effect. One call instead of a property per setting, so a port never runs with half a configuration.set_pin_level- manual control of a pin, only inMode::Test; every other mode answersCommon(WrongState).send_break- holds TX low forduration(a break condition), after the bytes already queued.tx_pending()- bytes queued for sending and not sent yet.monitor-#[optional], a counted stream function: the host opens it with the line settings of the watched link (baud rate, data bits, parity, stop bits, IO level) and gets back what was actually set (asUart::configuredoes), then both directions of the link (rePort: RX and CTS/RX2) in one stream, in the order they arrived: eachMonitorChunksays whichMonitorLineit came from, so a request and its answer stay in order. Closing the stream stops receiving. The unit is a byte of either line. Most bridges can't sniff a link they aren't part of: such a device leaves it out withomit = ["uart.monitor"]in itsww_codegen!, and a host sees that in the device's introspection.
Time¶
Every received chunk, on rx and on monitor, says when it arrived, on the device's one clock
(ww_timebase::Clock) that also timestamps everything else the device captures:
| What | Where | |
|---|---|---|
| first byte arrived (end of its stop bit) | the event head's t |
always set |
| last byte arrived | RxChunk::end: Option<Second> |
set by devices that measure it |
The bytes of a chunk came back to back, so the time of each one follows from the first. Byte i (from 0) of a chunk
of n bytes arrived at:
first + i * (end - first) / (n - 1)whenendis set. This takes up the sender's baud rate error: UARTs accept 2-3 % off the nominal rate, which adds up to milliseconds over a long burst, so a device that can time the last byte does.first + i * frame_bits / baudwhen it isn't:frame_bitsis start + data + parity + stop bits (UartConfig::frame_bits(), 10 for 8N1) andbaudthe rateconfigureanswered. The last byte is then atfirst + (n - 1) * frame_bits / baud.
All are ww_si::Seconds since the device's clock started, in the prefix and number type the device picks (usually
a u32 of microseconds, which wraps every 71.6 minutes; the host unwraps it around Clock::now()). With them a host:
- lines up serial traffic with the device's other data: a GPIO edge, an ADC sample, an I2C transaction;
- tells a request and its answer apart in time on a
monitorlink, and measures the turnaround between them; - sees the gaps on the line: a chunk ends on a silence of more than
idle_charscharacter times, so the next chunk's first byte comes at least that much after the previous chunk's last one.
On the client the head's time is the t of StreamEvent::Item (rx.recv_event()), next to the chunk:
let actual = client.uart().configure(config).call().await??;
let char_time = actual.frame_bits().unwrap() as f64 / actual.baud_rate.to_f64();
let mut rx = client.uart().rx().call().await?;
loop {
match rx.recv_event().await? {
StreamEvent::Item { t: Some(first), value, .. } => {
let n = value.bytes.len();
// measured, or one character time per byte after the first
let last = value.end.map(|end| end.to_f64())
.unwrap_or(first.to_f64() + (n - 1) as f64 * char_time);
println!("{n} bytes from {:.6} s to {last:.6} s", first.to_f64());
}
StreamEvent::Gap { units, .. } => println!("{units} bytes lost"),
_ => {}
}
}
A device that can't time each byte (a DMA that hands over a block when the line goes idle) times the block's end
from the idle interrupt, sends it as end, and takes the first byte a character time per byte earlier at the
configured baud rate, as examples_mcu/usb_stm32h725ig does.
idle_chars defaults to IDLE_CHARS_DEFAULT (2): a gap of one character time would split the output of senders
that pause briefly between characters (a CPU feeding its UART byte by byte, a USB-serial bridge between its USB
packets), more would merge the frames of protocols that keep only a short gap between them (Modbus RTU separates
frames by 3.5 character times, which 2 still splits). A protocol with its own rule sets it in UartConfig.
Types¶
ww_si::Baud- a baud rate (Baud::base(115_200u32)), in the number type and prefix the caller picks, like every otherww_siquantity.Mode-Asynchronous(no flow control),HardwareFlowControl(RTS/CTS),SoftwareFlowControl(XON/XOFF),HighZ(all pins High-Z),Test(manual pin control throughset_pin_level).Pin-Tx,Rx,Rts,Cts, used byset_pin_level.StopBits-Stop1,Stop2.Parity-None,Odd,Even.DataBits-Eight,Seven,Nine,Six,Five.UartError<'i>-Common(CommonError)(common errors); unsized, evolvable. Each method lists the variants it returns (#[errors]); line errors of received data areRxFlagsof the chunk, not errors.RxChunk<'i>-end: Option<Second>(when the last byte arrived, if the device measures it),flags: Option<RxFlags>,bytes: MaxLen<RefVec<'i, u8>, UART_CHUNK_MAX>; when the first byte arrived is the time in the event head, see Time.MonitorChunk<'i>-line: MonitorLine(AorB, 1 bit),chunk: RxChunk<'i>.RxFlags-parity_error,framing_error,overrun,break_detected, seen while receiving a chunk.UartConfig-baud_rate: Baud,data_bits,parity,stop_bits,mode,prevent_back_feed: bool,io_level: Option<Volt>,idle_chars: u8(idle character times that end a chunk); the argument and the answer ofconfigure.frame_bits()is the bits of one character on the line (start + data + parity + stop).MonitorConfig-baud_rate,data_bits,parity,stop_bits,io_level,idle_charsof the linkUart::monitorwatches; the argument and the answer ofmonitor.Capabilities<'i>-min_baud_rate/max_baud_rate: Baud,io_levels: MaxLen<RefVec<'i, Volt>, MAX_IO_LEVELS>,io_level_min/io_level_max: Option<Volt>(an adjustable supply),hw_flow_control/sw_flow_control/high_z_mode/test_mode/back_feed_detector: bool,rx_buffer/tx_buffer: u32(device receive and transmit buffer sizes, in bytes).
All enums and structs are behind #[derive_shrink_wrap] with owned(feature = "std"), so owned (*Owned)
variants are available by enabling the crate's std feature; the plain configuration enums (Mode,
Pin, StopBits, Parity, DataBits) also derive PartialEq/Eq.
Composing into a device API¶
A device exposes a UART by implementing Uart on its API root:
#[ww_api_root]
pub trait MyDeviceApi {
ww_impl!(uart: ww_uart::Uart);
}
// a port without monitoring hardware
ww_codegen!(my_device_api::MyDeviceApi for Server, server = true, omit = ["uart.monitor"]);
tests/io_uart_api is exactly this shape (used by tests/io_uart's mock server and in-process tests); a device
with more than one UART nests it as an array instead (ww_impl!(uart[]: ww_uart::Uart), see examples/uart_api).
Client usage (examples/uart)¶
The generated client wraps wire_weaver_client::Commander; see examples/uart/src/lib.rs for the small
UartBridge wrapper type (connecting over USB with a fixed VID/PID). Sending bytes through the tx credit sink
(examples/uart/examples/simple.rs):
let device = UartBridge::new().connect().await?;
let mut tx = device.uart(0).tx().await?;
tx.send_bytes(b"abc000def").await?;
Receiving chunks from rx and configuring the port look the same as any other nested trait, method and
stream (see streams); for a non-array uart resource (as in tests/io_uart_api) the same
calls drop the index: client.uart().rx(), client.uart().configure(config).