Skip to content

Commit ac3e3cc

Browse files
luqmanalif
authored and
lif
committed
Emulated eXtensible Host Controller Interface (xHCI) device
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*. https://www.intel.com/content/dam/www/public/us/en/documents/technical-specifications/extensible-host-controler-interface-usb-xhci.pdf At present, the only USB device supported is a USB 2.0 `NullUsbDevice` with no actual functionality, which exists as a proof-of-concept and as a means to show that USB `DeviceDescriptor`s are successfully communicated to the guest in phd-tests. ``` +---------+ | 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) | +------------------+ ``` Conventions =========== Wherever possible, the framework 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 indeces defined by the spec (slot ID, port ID), we put placeholder values at position 0 of any arrays in which the ID is used as an index, such that we aspire to categorically avoid off-by-one errors of omission (of `- 1`). Implementation ============== `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. 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 `Instant`s and `Duration`s rather than by repeated incrementation. 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 e398841 commit ac3e3cc

File tree

43 files changed

+9763
-9
lines changed

Some content is hidden

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

43 files changed

+9763
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use propolis::hw::qemu::{
4242
ramfb,
4343
};
4444
use propolis::hw::uart::LpcUart;
45+
use propolis::hw::usb::xhci;
4546
use propolis::hw::{nvme, virtio};
4647
use propolis::intr_pins;
4748
use propolis::vmm::{self, Builder, Machine};
@@ -98,6 +99,9 @@ pub enum MachineInitError {
9899
#[error("failed to specialize CPUID for vcpu {0}")]
99100
CpuidSpecializationFailed(i32, #[source] propolis::cpuid::SpecializeError),
100101

102+
#[error("xHC USB root hub port number invalid: {0}")]
103+
UsbRootHubPortNumberInvalid(String),
104+
101105
#[cfg(feature = "falcon")]
102106
#[error("softnpu p9 device missing")]
103107
SoftNpuP9Missing,
@@ -814,6 +818,44 @@ impl MachineInitializer<'_> {
814818
Ok(())
815819
}
816820

821+
/// Initialize xHCI controllers, connect any USB devices given in the spec,
822+
/// add them to the device map, and attach them to the chipset.
823+
pub fn initialize_xhc_usb(
824+
&mut self,
825+
chipset: &RegisteredChipset,
826+
) -> Result<(), MachineInitError> {
827+
for (xhc_id, xhc_spec) in &self.spec.xhcs {
828+
info!(
829+
self.log,
830+
"Creating xHCI controller";
831+
"pci_path" => %xhc_spec.pci_path,
832+
);
833+
834+
let log = self.log.new(slog::o!("dev" => "xhci"));
835+
let bdf: pci::Bdf = xhc_spec.pci_path.into();
836+
let xhc = xhci::PciXhci::create(log);
837+
838+
for (_usb_id, usb) in &self.spec.usbdevs {
839+
if *xhc_id == usb.xhc_device {
840+
info!(
841+
self.log,
842+
"Attaching USB device";
843+
"xhc_pci_path" => %xhc_spec.pci_path,
844+
"usb_port" => %usb.root_hub_port_num,
845+
);
846+
xhc.add_usb_device(usb.root_hub_port_num).map_err(
847+
MachineInitError::UsbRootHubPortNumberInvalid,
848+
)?;
849+
}
850+
}
851+
852+
self.devices.insert(xhc_id.clone(), xhc.clone());
853+
chipset.pci_attach(bdf, xhc);
854+
}
855+
856+
Ok(())
857+
}
858+
817859
#[cfg(feature = "failure-injection")]
818860
pub fn initialize_test_devices(&mut self) {
819861
use propolis::hw::testdev::{

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! Conversions from version-0 instance specs in the [`propolis_api_types`]
66
//! crate to the internal [`super::Spec`] representation.
77
8-
use std::collections::BTreeMap;
8+
use std::collections::{BTreeMap, BTreeSet};
99

1010
use propolis_api_types::instance_spec::{
1111
components::{
@@ -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 },
@@ -61,6 +64,8 @@ impl From<Spec> for InstanceSpecV0 {
6164
cpuid,
6265
disks,
6366
nics,
67+
xhcs,
68+
usbdevs,
6469
boot_settings,
6570
serial,
6671
pci_pci_bridges,
@@ -121,6 +126,14 @@ impl From<Spec> for InstanceSpecV0 {
121126
);
122127
}
123128

129+
for (id, xhc) in xhcs {
130+
insert_component(&mut spec, id, ComponentV0::Xhci(xhc));
131+
}
132+
133+
for (id, usb) in usbdevs {
134+
insert_component(&mut spec, id, ComponentV0::UsbPlaceholder(usb));
135+
}
136+
124137
for (name, desc) in serial {
125138
if desc.device == SerialPortDevice::Uart {
126139
insert_component(
@@ -230,6 +243,7 @@ impl TryFrom<InstanceSpecV0> for Spec {
230243
BTreeMap::new();
231244
let mut dlpi_backends: BTreeMap<SpecKey, DlpiNetworkBackend> =
232245
BTreeMap::new();
246+
let mut xhci_controllers: BTreeSet<SpecKey> = BTreeSet::new();
233247

234248
for (id, component) in value.components.into_iter() {
235249
match component {
@@ -249,6 +263,10 @@ impl TryFrom<InstanceSpecV0> for Spec {
249263
ComponentV0::DlpiNetworkBackend(dlpi) => {
250264
dlpi_backends.insert(id, dlpi);
251265
}
266+
ComponentV0::Xhci(xhc) => {
267+
xhci_controllers.insert(id.to_owned());
268+
builder.add_xhci_controller(id, xhc)?;
269+
}
252270
device => {
253271
devices.push((id, device));
254272
}
@@ -365,6 +383,19 @@ impl TryFrom<InstanceSpecV0> for Spec {
365383
ComponentV0::P9fs(p9fs) => {
366384
builder.set_p9fs(p9fs)?;
367385
}
386+
ComponentV0::Xhci(xhci) => {
387+
builder.add_xhci_controller(device_id, xhci)?;
388+
}
389+
ComponentV0::UsbPlaceholder(usbdev) => {
390+
if xhci_controllers.contains(&usbdev.xhc_device) {
391+
builder.add_usb_device(device_id, usbdev)?;
392+
} else {
393+
return Err(ApiSpecError::HostControllerNotFound {
394+
xhc: usbdev.xhc_device.to_owned(),
395+
device: device_id,
396+
});
397+
}
398+
}
368399
ComponentV0::CrucibleStorageBackend(_)
369400
| ComponentV0::FileStorageBackend(_)
370401
| ComponentV0::BlobStorageBackend(_)

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::collections::{BTreeSet, HashSet};
99
use propolis_api_types::instance_spec::{
1010
components::{
1111
board::Board as InstanceSpecBoard,
12-
devices::{PciPciBridge, SerialPortNumber},
12+
devices::{PciPciBridge, SerialPortNumber, UsbDevice, XhciController},
1313
},
1414
PciPath, SpecKey,
1515
};
@@ -65,13 +65,17 @@ pub(crate) enum SpecBuilderError {
6565

6666
#[error("failed to read default CPUID settings from the host")]
6767
DefaultCpuidReadFailed(#[from] cpuid_utils::host::GetHostCpuidError),
68+
69+
#[error("a USB device is already attached to xHC {0} root hub port {1}")]
70+
UsbPortInUse(SpecKey, u8),
6871
}
6972

7073
#[derive(Debug, Default)]
7174
pub(crate) struct SpecBuilder {
7275
spec: super::Spec,
7376
pci_paths: BTreeSet<PciPath>,
7477
serial_ports: HashSet<SerialPortNumber>,
78+
xhc_usb_ports: BTreeSet<(SpecKey, u8)>,
7579
component_names: BTreeSet<SpecKey>,
7680
}
7781

@@ -154,6 +158,23 @@ impl SpecBuilder {
154158
}
155159
}
156160

161+
fn register_usb_device(
162+
&mut self,
163+
usbdev: &UsbDevice,
164+
) -> Result<(), SpecBuilderError> {
165+
// slightly awkward: we have to take a ref of an owned tuple for
166+
// .contains() below, and in either case we need an owned SpecKey,
167+
// so we'll just clone it once here
168+
let xhc_and_port =
169+
(usbdev.xhc_device.to_owned(), usbdev.root_hub_port_num);
170+
if self.xhc_usb_ports.contains(&xhc_and_port) {
171+
Err(SpecBuilderError::UsbPortInUse(xhc_and_port.0, xhc_and_port.1))
172+
} else {
173+
self.xhc_usb_ports.insert(xhc_and_port);
174+
Ok(())
175+
}
176+
}
177+
157178
/// Adds a storage device with an associated backend.
158179
pub(super) fn add_storage_device(
159180
&mut self,
@@ -355,6 +376,26 @@ impl SpecBuilder {
355376
Ok(self)
356377
}
357378

379+
pub fn add_xhci_controller(
380+
&mut self,
381+
device_id: SpecKey,
382+
xhc: XhciController,
383+
) -> Result<&Self, SpecBuilderError> {
384+
self.register_pci_device(xhc.pci_path)?;
385+
self.spec.xhcs.insert(device_id, xhc);
386+
Ok(self)
387+
}
388+
389+
pub fn add_usb_device(
390+
&mut self,
391+
device_id: SpecKey,
392+
usbdev: UsbDevice,
393+
) -> Result<&Self, SpecBuilderError> {
394+
self.register_usb_device(&usbdev)?;
395+
self.spec.usbdevs.insert(device_id, usbdev);
396+
Ok(self)
397+
}
398+
358399
/// Yields the completed spec, consuming the builder.
359400
pub fn finish(self) -> super::Spec {
360401
self.spec

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use propolis_api_types::instance_spec::{
2626
board::{Chipset, GuestHypervisorInterface, I440Fx},
2727
devices::{
2828
NvmeDisk, PciPciBridge, QemuPvpanic as QemuPvpanicDesc,
29-
SerialPortNumber, VirtioDisk, VirtioNic,
29+
SerialPortNumber, UsbDevice, VirtioDisk, VirtioNic, XhciController,
3030
},
3131
},
3232
v0::ComponentV0,
@@ -66,6 +66,8 @@ pub(crate) struct Spec {
6666
pub cpuid: CpuidSet,
6767
pub disks: BTreeMap<SpecKey, Disk>,
6868
pub nics: BTreeMap<SpecKey, Nic>,
69+
pub xhcs: BTreeMap<SpecKey, XhciController>,
70+
pub usbdevs: BTreeMap<SpecKey, UsbDevice>,
6971
pub boot_settings: Option<BootSettings>,
7072

7173
pub serial: BTreeMap<SpecKey, SerialPort>,

bin/propolis-server/src/lib/vm/ensure.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ async fn initialize_vm_objects(
563563
&properties,
564564
))?;
565565
init.initialize_network_devices(&chipset).await?;
566+
init.initialize_xhc_usb(&chipset)?;
566567

567568
#[cfg(feature = "failure-injection")]
568569
init.initialize_test_devices();

bin/propolis-standalone/src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,13 @@ fn setup_instance(
12481248
block::attach(nvme.clone(), backend).unwrap();
12491249
chipset_pci_attach(bdf, nvme);
12501250
}
1251+
"pci-xhci" => {
1252+
let log = log.new(slog::o!("dev" => "xhci"));
1253+
let bdf = bdf.unwrap();
1254+
let xhci = hw::usb::xhci::PciXhci::create(log);
1255+
guard.inventory.register_instance(&xhci, &bdf.to_string());
1256+
chipset_pci_attach(bdf, xhci);
1257+
}
12511258
qemu::pvpanic::DEVICE_NAME => {
12521259
let enable_isa = dev
12531260
.options

crates/propolis-api-types/src/instance_spec/components/devices.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,33 @@ pub struct P9fs {
190190
pub pci_path: PciPath,
191191
}
192192

193+
/// Describes a PCI device implementing the eXtensible Host Controller Interface
194+
/// for the purpose of attaching USB devices.
195+
///
196+
/// (Note that at present no functional USB devices have yet been implemented.)
197+
#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)]
198+
#[serde(deny_unknown_fields)]
199+
pub struct XhciController {
200+
/// The PCI path at which to attach the guest to this xHC.
201+
pub pci_path: PciPath,
202+
}
203+
204+
/// Describes a USB device, requires the presence of an XhciController.
205+
///
206+
/// (Note that at present no USB devices have yet been implemented
207+
/// outside of a null device for testing purposes.)
208+
#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)]
209+
#[serde(deny_unknown_fields)]
210+
pub struct UsbDevice {
211+
/// The name of the xHC to which this USB device shall be attached.
212+
pub xhc_device: SpecKey,
213+
/// The root hub port number to which this USB device shall be attached.
214+
/// For USB 2.0 devices, valid values are 1-4, inclusive.
215+
/// For USB 3.0 devices, valid values are 5-8, inclusive.
216+
pub root_hub_port_num: u8,
217+
// TODO(lif): a field for device type (e.g. HID tablet, mass storage...)
218+
}
219+
193220
/// Describes a synthetic device that registers for VM lifecycle notifications
194221
/// and returns errors during attempts to migrate.
195222
///

crates/propolis-api-types/src/instance_spec/v0.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub enum ComponentV0 {
2727
SoftNpuPort(components::devices::SoftNpuPort),
2828
SoftNpuP9(components::devices::SoftNpuP9),
2929
P9fs(components::devices::P9fs),
30+
Xhci(components::devices::XhciController),
31+
UsbPlaceholder(components::devices::UsbDevice),
3032
MigrationFailureInjector(components::devices::MigrationFailureInjector),
3133
CrucibleStorageBackend(components::backends::CrucibleStorageBackend),
3234
FileStorageBackend(components::backends::FileStorageBackend),

crates/propolis-config-toml/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ impl Device {
9797
pub fn get<T: FromStr, S: AsRef<str>>(&self, key: S) -> Option<T> {
9898
self.get_string(key)?.parse().ok()
9999
}
100+
101+
pub fn get_integer<S: AsRef<str>>(&self, key: S) -> Option<i64> {
102+
self.options.get(key.as_ref())?.as_integer()
103+
}
100104
}
101105

102106
#[derive(Debug, Deserialize, Serialize, PartialEq)]

crates/propolis-config-toml/src/spec.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use propolis_client::{
1313
instance_spec::{
1414
ComponentV0, DlpiNetworkBackend, FileStorageBackend,
1515
MigrationFailureInjector, NvmeDisk, P9fs, PciPath, PciPciBridge,
16-
SoftNpuP9, SoftNpuPciPort, SoftNpuPort, SpecKey, VirtioDisk,
17-
VirtioNetworkBackend, VirtioNic,
16+
SoftNpuP9, SoftNpuPciPort, SoftNpuPort, SpecKey, UsbDevice, VirtioDisk,
17+
VirtioNetworkBackend, VirtioNic, XhciController,
1818
},
1919
support::nvme_serial_from_str,
2020
};
@@ -33,6 +33,12 @@ pub enum TomlToSpecError {
3333
#[error("failed to get PCI path for device {0:?}")]
3434
InvalidPciPath(String),
3535

36+
#[error("failed to get USB root hub port for device {0:?}")]
37+
InvalidUsbPort(String),
38+
39+
#[error("no xHC name for USB device {0:?}")]
40+
NoHostControllerNameForUsbDevice(String),
41+
3642
#[error("failed to parse PCI path string {0:?}")]
3743
PciPathParseFailed(String, #[source] std::io::Error),
3844

@@ -249,6 +255,44 @@ impl TryFrom<&super::Config> for SpecConfig {
249255
)?),
250256
)?;
251257
}
258+
"pci-xhci" => {
259+
let pci_path: PciPath =
260+
device.get("pci-path").ok_or_else(|| {
261+
TomlToSpecError::InvalidPciPath(
262+
device_name.to_owned(),
263+
)
264+
})?;
265+
266+
spec.components.insert(
267+
device_id,
268+
ComponentV0::Xhci(XhciController { pci_path }),
269+
);
270+
}
271+
"usb-dummy" => {
272+
let root_hub_port_num = device
273+
.get_integer("root-hub-port")
274+
.filter(|x| (1..=8).contains(x))
275+
.ok_or_else(|| {
276+
TomlToSpecError::InvalidUsbPort(
277+
device_name.to_owned(),
278+
)
279+
})? as u8;
280+
281+
let xhc_device: SpecKey =
282+
device.get("xhc-device").ok_or_else(|| {
283+
TomlToSpecError::NoHostControllerNameForUsbDevice(
284+
device_name.to_owned(),
285+
)
286+
})?;
287+
288+
spec.components.insert(
289+
device_id,
290+
ComponentV0::UsbPlaceholder(UsbDevice {
291+
root_hub_port_num,
292+
xhc_device,
293+
}),
294+
);
295+
}
252296
_ => {
253297
return Err(TomlToSpecError::UnrecognizedDeviceType(
254298
driver.to_owned(),

0 commit comments

Comments
 (0)