Skip to content

Commit eb7340a

Browse files
author
lif
committed
Emulated xHCI 1.0 Controller, USB 2.0 devices, and HID 1.11 tablet
Special thanks to @luqmana getting this started in early 2023: https://github.com/luqmana/propolis/commits/xhci/ The version of the standard referenced throughout the comments in this module is xHCI 1.2, but we do not implement the features required of a 1.1 or 1.2 compliant host controller - that is, we are only implementing a subset of what xHCI version 1.0 requires of an xHC, as described by version 1.2 of the *specification*. At present, the USB devices supported are: - a `HIDTabletDevice` for use as an absolute-axis pointing device for VNC clients (the RFB protocol has no relative-mouse support that would be required for using the simpler PS/2 mouse) - a `NullUsbDevice` with no actual functionality, which primarily exists for testing, such as being a means to show that USB DeviceDescriptor`s are communicated to the guest in phd-tests. Specifications used ------------------- - xHCI: https://www.intel.com/content/dam/www/public/us/en/documents/technical-specifications/extensible-host-controler-interface-usb-xhci.pdf - USB 2.0: https://www.usb.org/document-library/usb-20-specification - HID: https://www.usb.org/sites/default/files/hid1_11.pdf - HID Usage Tables: https://www.usb.org/sites/default/files/hut1_6.pdf I've made an effort to cite relevant sections of these where applicable in comments within the implementation. Usage ===== In a propolis config.toml, an xHC with an HID tablet attached can be defined thusly: ``` [dev.xhc0] driver = "pci-xhci" pci-path = "0.6.0" [dev.usb1] driver = "usb-hid-tablet" xhc-device = "xhc0" root-hub-port = 1 ``` Conventions =========== Wherever possible, the xHC represents `Trb` data through a further level of abstraction, such as enums constructed from the raw TRB bitfields before being passed to other parts of the system that use them, such that the behavior of identifying `TrbType` and accessing their fields properly according to the spec lives in a conversion function rather than strewn across implementation of other xHC functionality. The nomenclature used is generally trading the "Descriptor" suffix for "Info", e.g. the high-level enum-variant version of an `EventDescriptor` is `EventInfo` (which is passed to the `EventRing` to be converted into Event TRBs and written into guest memory). For 1-based indices defined by the spec (slot ID, port ID), we use [SlotId] and [PortId] and their respective `.as_index()` methods to index into our internal arrays of slots and ports, such that we aspire to categorically avoid off-by-one errors of omission (of `- 1`). (Note that indexing into the DCBAA is *not* done with this method, as position 0 in it is reserved by the spec for the Scratchpad described in xHCI 1.2 section 4.20) xHC Implementation ================== ``` +---------+ | PciXhci | +---------+ | has-a +-----------------------------+ | XhciState | |-----------------------------| | PCI MMIO registers | | XhciInterrupter | | DeviceSlotTable | | Usb2Ports + Usb3Ports | | CommandRing | | newly attached USB devices | +-----------------------------+ | has-a | +-------------------+ | has-a | XhciInterrupter | +-----------------+ |-------------------| | DeviceSlotTable | | EventRing | |-----------------| | MSI-X/INTxPin | | DeviceSlot(s) |___+------------------+ +-------------------+ | DCBAAP | | DeviceSlot | | Active USB devs | |------------------| +-----------------+ | TransferRing(s) | +------------------+ ``` `DeviceSlotTable` ----------------- When a USB device is attached to the xHC, it is enqueued in a list within `XhciState` along with its `PortId`. The next time the xHC runs: - it will update the corresponding **PORTSC** register and inform the guest with a TRB on the `EventRing`, and if enabled, a hardware interrupt. - it moves the USB device to the `DeviceSlotTable` in preparation for being configured and assigned a slot. When the guest xHCD rings Doorbell 0 to run an `EnableSlot` Command, the `DeviceSlotTable` assigns the first unused slot ID to it. Hot-plugging devices live (i.e. not just attaching all devices defined by the instance spec at boot time as is done now) is not yet implemented. Device-slot-related Command TRBs are handled by the `DeviceSlotTable`. The command interface methods are written as translations of the behaviors defined in xHCI 1.2 section 4.6 to Rust, with liberties taken around redundant `TrbCompletionCode` writes; i.e. when the outlined behavior from the spec describes the xHC placing a `Success` into a new TRB on the `EventRing` immediately at the beginning of the command's execution, and then overwriting it with a failure code in the event of a failure, our implementation postpones the creation and enqueueing of the event until after the outcome of the command's execution (and thus the Event TRB's values) are all known. Ports ----- Root hub port state machines (xHCI 1.2 section 4.19.1) and port registers are managed by `Usb2Port`, which has separate methods for handling register writes by the guest and by the xHC itself. A USB device may hold an `XhciPortHandle`, which is used to inform the xHC to update the Port Status Change register when a device returns from suspend (as well as to plumb completion events for transfer requests sent to the device in that port; speaking of:) TRB Rings --------- **Consumer**: The `CommandRing` and each slot endpoint's `TransferRing` are implemented as `ConsumerRing<CommandInfo>` and `ConsumerRing<TransferInfo>`. Dequeued work items are converted from raw `CommandDescriptor`s and `TransferDescriptor`s, respectively). Starting at the dequeue pointer provided by the guest, the `ConsumerRing` will consume non-Link TRBs (and follow Link TRBs, as in xHCI 1.2 figure 4-15) into complete work items. In the case of the `CommandRing`, `CommandDescriptor`s are each only made up of one `Trb`, but for the `TransferRing` multi-TRB work items are possible, where all but the last item have the `chain_bit` set. **Producer**: The only type of producer ring is the `EventRing`. Events destined for it are fed through the `XhciInterrupter`, which handles enablement and rate-limiting of PCI-level machine interrupts being generated as a result of the events. Similarly (and inversely) to the consumer rings, the `EventRing` converts the `EventInfo`s enqueued in it into `EventDescriptor`s to be written to guest memory regions defined by the `EventRingSegment` Table. Doorbells --------- The guest writing to a `DoorbellRegister` makes the host controller process a consumer TRB ring (the `CommandRing` for doorbell 0, or the corresponding slot's `TransferRing` for nonzero doorbells). The ring consumption is performed by the doorbell register write handler, in `process_command_ring` and `process_transfer_ring`. Timer registers --------------- The value of registers defined as incrementing/decrementing per time interval, such as **MFINDEX** and the `XhciInterrupter`'s **IMODC**, are simulated with `VmGuestInstant`s and `Duration`s rather than by repeated incrementation. (`VmGuestInstant` is a new type defined in this change with a similar interface to `std::time::Instant`, but using timestamps provided by the VMM.) Migration --------- The types defined for serializing the device state are admittedly undercooked; advice about what a cleaner approach could look like before committing to the 'V1' of the format are quite welcome. An inactive phd-test for migrating a guest with an active `HIDTabletDevice` is present in `phd-tests/tests/src/xhci.rs`, which was written with a temporary (reverted for this PR) hack in place to plumb the xHC payloads despite their absence in the "V0" Spec. USB Devices =========== Implementors of the `UsbDevice` trait can be attached to the xHC. When Transfer Descriptors are executed from an endpoint's `TransferRing`, the appropriate trait function is called (i.e. a Normal TRB calls `normal_transfer`, a Data Stage TRB calls `data_stage`). Note that to reduce boilerplate, TRBs as defined in the xHCI are handled directly, rather than being converted into URBs. Abstractions are provided for defining USB Descriptors and HID Report Descriptors, as well as for implementing Control Endpoints (such as the Default Control Endpoint used for requesting the Device Descriptor) and Interrupt-IN Endpoints (such as the pipe used for HID reports). HID Tablet ========== `HIDTabletDevice` implements a USB HID 1.11 pointing device that reports pointer state events provided by VNC clients connected to propolis-server. ``` +--------------------------+ | HIDTabletDevice | +--------------------------+ | | | +--------------------+ | | | ControlEndpoint | | +-----------------+ |--------------------| | | HIDTabletReport | | GET_DESCRIPTOR | | |-----------------| +--------------------+ | SET_CONFIGURATION | | | Pointer state |<---| VNC pointer events | | HID class-specific | | +-----------------+ +--------------------+ | (Set Idle, | | | | Get Report) | | | +--------------------+ | | | v +---------------------+ | InterruptInEndpoint | |---------------------| | Receive Normal TRBs | | Write HID Report | +---------------------+ ``` The pointer events suppiled by the RFB server are placed behind a Mutex shared with the Interrupt-IN EP, which has an internal state machine for filling request buffers from the guest with payload data from VNC: ``` WaitForTransferDescriptor | ^ ^ (TRB) (timeout) (written) v | | WaitForPayload-(data)>Writing ``` HID descriptors --------------- Partial definitions (enough for our Tablet) of the HID Usage Tables, and some high-level constructs for defining Report Descriptors with them, are defined in `hw::usb::usbdev::hid::report`. PHD tests --------- In addition to a simple "does the Null device enumerate under Linux" check, I've also written a `xhci_usb_tablet_vnc_pointer_events_test` that connects to the guest propois-server's RFB endpoint and wiggles the mouse in the VNC client, to see if Linux the hidraw device node reflects the expected reports. Naturally, this required making PHD capable of being a VNC client. The inactively-developed `rust-vnc` client crate has been vendored under our org to update its dependencies. (Note that it is only used in PHD, nowhere near production.) DTrace support ============== To see a trace of all MMIO register reads/writes and TRB enqueue/dequeues: ```sh pfexec ./scripts/xhci-trace.d -p $(pgrep propolis-server) ``` The name of each register as used by DTrace is `&'static`ally defined in `registers::Registers::reg_name`.
1 parent af2ec64 commit eb7340a

70 files changed

Lines changed: 16297 additions & 2399 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cargo/config.toml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ xtask = "run --package xtask --quiet --"
77
CARGO_WORKSPACE_DIR = { value = "", relative = true }
88

99
[build]
10-
# Tokio's unstable features are required by `tokio-dtrace` probes, and for
11-
# disabling the LIFO slot optimization.
12-
#
13-
# See here for details:
14-
# https://github.com/oxidecomputer/oxide-tokio-rt/blob/main/README.md#enabling-tokio_unstable-features
15-
rustflags = ["--cfg", "tokio_unstable"]
10+
rustflags = [
11+
# Tokio's unstable features are required by `tokio-dtrace` probes, and for
12+
# disabling the LIFO slot optimization.
13+
#
14+
# See here for details:
15+
# https://github.com/oxidecomputer/oxide-tokio-rt/blob/main/README.md#enabling-tokio_unstable-features
16+
"--cfg=tokio_unstable",
17+
# zerocopy's #[derive(IntoBytes)] isn't stable for union types
18+
# (used by propolis in xHCI TRB ring definitions)
19+
# https://github.com/google/zerocopy/discussions/1802
20+
# https://docs.rs/zerocopy/latest/zerocopy/derive.IntoBytes.html#unions
21+
"--cfg=zerocopy_derive_union_into_bytes",
22+
]

Cargo.lock

Lines changed: 64 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ tracing-bunyan-formatter = "0.3.3"
191191
tracing-subscriber = "0.3.14"
192192
usdt = { version = "0.6", default-features = false }
193193
uuid = "1.3.2"
194+
# used by phd-framework to test rfb in propolis-server
195+
vnc = { git = "https://github.com/oxidecomputer/rust-vnc", rev = "303dbc9db965158ba79d380ff43c3328d1f28273", version = "0.5.0" }
194196
zerocopy = "0.8.25"
195197

196198

bin/propolis-server/src/lib/initializer.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::convert::TryInto;
66
use std::fs::File;
77
use std::num::{NonZeroU8, NonZeroUsize};
88
use std::os::unix::fs::FileTypeExt;
9-
use std::sync::Arc;
9+
use std::sync::{Arc, Mutex};
1010
use std::time::{SystemTime, UNIX_EPOCH};
1111

1212
use crate::serial::Serial;
@@ -46,6 +46,8 @@ use propolis::hw::qemu::{
4646
ramfb,
4747
};
4848
use propolis::hw::uart::{LpcUart, LpcUartMetadata};
49+
use propolis::hw::usb::usbdev::vnc_tablet::HIDTabletReport;
50+
use propolis::hw::usb::{usbdev, xhci};
4951
use propolis::hw::{nvme, virtio};
5052
use propolis::intr_pins;
5153
use propolis::vmm::{self, Builder, Machine};
@@ -119,6 +121,9 @@ pub enum MachineInitError {
119121
#[error("failed to start attestation server")]
120122
AttestationServer(#[source] std::io::Error),
121123

124+
#[error("xHC USB root hub port number invalid: {0}")]
125+
UsbRootHubPortNumberInvalid(String),
126+
122127
#[cfg(feature = "falcon")]
123128
#[error("softnpu p9 device missing")]
124129
SoftNpuP9Missing,
@@ -1073,6 +1078,53 @@ impl MachineInitializer<'_> {
10731078
Ok(())
10741079
}
10751080

1081+
/// Initialize xHCI controllers, connect any USB devices given in the spec,
1082+
/// add them to the device map, and attach them to the chipset.
1083+
pub fn initialize_xhc_usb(
1084+
&mut self,
1085+
chipset: &RegisteredChipset,
1086+
hid_report: &Arc<Mutex<HIDTabletReport>>,
1087+
) -> Result<(), MachineInitError> {
1088+
for (xhc_id, xhc_spec) in &self.spec.xhcs {
1089+
info!(
1090+
self.log,
1091+
"Creating xHCI controller";
1092+
"pci_path" => %xhc_spec.pci_path,
1093+
);
1094+
1095+
let log = self.log.new(slog::o!("dev" => "xhci"));
1096+
let bdf: pci::Bdf = xhc_spec.pci_path.into();
1097+
let xhc = xhci::PciXhci::create(self.machine.hdl.clone(), log);
1098+
1099+
for (usb_id, usb) in &self.spec.usbdevs {
1100+
if *xhc_id == usb.xhc_device {
1101+
info!(
1102+
self.log,
1103+
"Attaching USB device";
1104+
"usb_id" => %usb_id,
1105+
"xhc_pci_path" => %xhc_spec.pci_path,
1106+
"usb_port" => %usb.root_hub_port_num,
1107+
);
1108+
let device_type = match usb.usb_device_type {
1109+
instance_spec::components::devices::UsbDeviceType::Null => usbdev::UsbDeviceType::Null,
1110+
instance_spec::components::devices::UsbDeviceType::HidTablet => usbdev::UsbDeviceType::HidTablet,
1111+
};
1112+
xhc.add_usb_device(
1113+
usb.root_hub_port_num,
1114+
device_type,
1115+
hid_report,
1116+
)
1117+
.map_err(MachineInitError::UsbRootHubPortNumberInvalid)?;
1118+
}
1119+
}
1120+
1121+
self.devices.insert(xhc_id.clone(), xhc.clone());
1122+
chipset.pci_attach(bdf, xhc);
1123+
}
1124+
1125+
Ok(())
1126+
}
1127+
10761128
#[cfg(feature = "failure-injection")]
10771129
pub fn initialize_test_devices(&mut self) {
10781130
use propolis::hw::testdev::{

bin/propolis-server/src/lib/migrate/destination.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,10 @@ impl<T: MigrateConn> RonV0<T> {
541541

542542
{
543543
let vm_objects = ensure_ctx.vm_objects().lock_shared().await;
544-
let migrate_ctx =
545-
MigrateCtx { mem: &vm_objects.access_mem().unwrap() };
544+
let migrate_ctx = MigrateCtx {
545+
mem: &vm_objects.access_mem().unwrap(),
546+
hid_report: vm_objects.hid_report(),
547+
};
546548
for device in devices {
547549
let key = SpecKey::from(device.instance_name.clone());
548550
info!(self.log(), "Applying state to device {key}");

bin/propolis-server/src/lib/migrate/source.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -683,8 +683,10 @@ impl<T: MigrateConn> RonV0Runner<'_, T> {
683683
let mut device_states = vec![];
684684
{
685685
let objects = self.vm.lock_shared().await;
686-
let migrate_ctx =
687-
MigrateCtx { mem: &objects.access_mem().unwrap() };
686+
let migrate_ctx = MigrateCtx {
687+
mem: &objects.access_mem().unwrap(),
688+
hid_report: objects.hid_report(),
689+
};
688690

689691
// Collect together the serialized state for all the devices
690692
objects.for_each_device_fallible(|name, devop| {

bin/propolis-server/src/lib/spec/api_spec_v0.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ pub(crate) enum ApiSpecError {
4444
#[error("network backend {backend} not found for device {device}")]
4545
NetworkBackendNotFound { backend: SpecKey, device: SpecKey },
4646

47+
#[error("USB host controller {xhc} not found for device {device}")]
48+
HostControllerNotFound { xhc: SpecKey, device: SpecKey },
49+
4750
#[allow(dead_code)]
4851
#[error("support for component {component} compiled out via {feature}")]
4952
FeatureCompiledOut { component: SpecKey, feature: &'static str },
@@ -77,6 +80,11 @@ impl From<Spec> for v1::instance_spec::InstanceSpec {
7780
// Not part of `v1::instance_spec::InstanceSpec`. Added in
7881
// `InstanceSpec` in API Version 3.0.0.
7982
vsock: _,
83+
84+
// Not part of `v1::instance_spec::InstanceSpec`. Added in
85+
// `InstanceSpec` in API Version 4.0.0.
86+
xhcs: _,
87+
usbdevs: _,
8088
} = val;
8189

8290
// Inserts a component entry into the supplied map, asserting first that

0 commit comments

Comments
 (0)