diff --git a/src/lib.rs b/src/lib.rs index ed8ff45..d85ef85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod pdinfo; pub mod pdo; pub mod type_c; pub mod ucsi; +pub mod usb; pub mod vdm; use core::hash::Hash; diff --git a/src/ucsi/lpm/get_alternate_modes.rs b/src/ucsi/lpm/get_alternate_modes.rs index 71fe74a..0f115a2 100644 --- a/src/ucsi/lpm/get_alternate_modes.rs +++ b/src/ucsi/lpm/get_alternate_modes.rs @@ -9,7 +9,8 @@ use bitfield::bitfield; use super::Recipient; use crate::ucsi::lpm::InvalidRecipient; use crate::ucsi::{CommandHeaderRaw, COMMAND_LEN}; -use crate::vdm::{AltModeId, Svid}; +use crate::vdm::structured::Svid; +use crate::vdm::AltModeId; /// Data length for the GET_ALTERNATE_MODES command response pub const RESPONSE_DATA_LEN: usize = 12; diff --git a/src/usb.rs b/src/usb.rs new file mode 100644 index 0000000..01dbd8d --- /dev/null +++ b/src/usb.rs @@ -0,0 +1,37 @@ +//! Universal Serial Bus (USB) related types and traits from the USB 2.0 and 3.2 +//! specifications. + +/// A Binary-Coded Decimal (BCD) format as defined by the USB 2.0 specification. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Bcd(pub u16); + +impl Bcd { + /// Parse the BCD value into its major, minor, and subminor components in the + /// format `jj.m.n` where + /// - `jj` is the major version (2 nibbles) + /// - `m` is the minor version (1 nibble) + /// - `n` is the subminor version (1 nibble) + pub const fn jjmn(&self) -> (u8, u8, u8) { + let jj = (self.0 >> 8) as u8; + let m = ((self.0 >> 4) & 0xF) as u8; + let n = (self.0 & 0xF) as u8; + (jj, m, n) + } +} + +/// The USB Product ID as assigned by the USB-IF. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProductId(pub u16); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bcd_jjmn() { + assert_eq!(Bcd(0x1234).jjmn(), (0x12, 0x3, 0x4)); + assert_eq!(Bcd(0xFEDC).jjmn(), (0xFE, 0xD, 0xC)); + } +} diff --git a/src/vdm.rs b/src/vdm/mod.rs similarity index 82% rename from src/vdm.rs rename to src/vdm/mod.rs index ea08f8b..0b3b91b 100644 --- a/src/vdm.rs +++ b/src/vdm/mod.rs @@ -1,3 +1,9 @@ +//! Vendor Defined Messages (VDM) allow vendors to exchange information outside of that defined by the PD spec. +//! +//! See PD spect 6.4.4 Vendor Defined Message. + +pub mod structured; + pub const DATA_OBJ_SIZE: usize = 4; pub const MAX_VDOS: usize = 6; pub const MAX_NUM_DATA_OBJECTS: usize = 7; @@ -25,11 +31,6 @@ pub enum Cmd { SvidCmdStart = 16, } -/// Standard or Vendor ID (SVID) newtype, see PD spec 6.4.4.2.1 -#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Svid(pub u16); - /// Altmode ID newtype for discover modes command and others, see PD spec 6.4.4.3.3 #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/vdm/structured/command/discover_identity/active_cable_vdo.rs b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs new file mode 100644 index 0000000..668f53d --- /dev/null +++ b/src/vdm/structured/command/discover_identity/active_cable_vdo.rs @@ -0,0 +1,1079 @@ +//! An Active Cable has a USB Plug on each end, at least one of which is a Cable +//! Plug supporting SOP' Communication. It incorporates data bus signal conditioning +//! circuits. +//! +//! See PD spec 6.4.4.3.1.7 Active Cable VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// An Active Cable has a USB Plug on each end, at least one of which is a Cable +/// Plug supporting SOP' Communication. It incorporates data bus signal conditioning +/// circuits. +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.7 Active Cable VDO, table 6.42 Active Cable VDO1. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ActiveCableVdo1 { + /// The highest rate the cable supports. + pub usb_highest_speed: UsbHighestSpeed, + + /// Whether one of the Cable Plugs is capable of SOP'' Communication in addition + /// to the normative SOP' Communication. + pub soppp_controller_present: bool, + + /// Whether the cable contains an end-to-end `VBUS` wire. + pub vbus_through_cable: bool, + + /// Whether the cable is capable of carrying 3A or 5A. + pub vbus_current_handling_capability: VbusCurrentHandlingCapability, + + /// Whether the SBUs are passive or active (e.g., digital). + pub sbu_type: SbuType, + + /// Whether the cable supports the SBUs in the cable. + pub sbu_supported: bool, + + /// The maximum voltage that shall be negotiated as part of an Explicit Contract. + pub maximum_vbus_voltage: MaximumVbusVoltage, + + /// Whether the Active Cable has one or two Cable Plugs requiring power from + /// `VCONN`. + pub cable_termination_type: CableTerminationType, + + /// The signal latency through the cable, which can be used as an approximation + /// for its length. + pub cable_latency: CableLatency, + + /// The cable is specifically designed for safe operation when carrying up to + /// 48 volts at 5 amps. + pub epr_capable: bool, + + /// Indicates whether the opposite end from the USB Type-C plug is another USB + /// Type-C plug or is a Captive Cable Assembly. + pub usb_type_c_or_captive: UsbTypeCOrCaptive, + + /// FW version assigned by the VID owner. + pub firmware_version: u8, + + /// HW version assigned by the VID owner. + pub hw_version: u8, +} + +/// An Active Cable has a USB Plug on each end, at least one of which is a Cable +/// Plug supporting SOP' Communication. It incorporates data bus signal conditioning +/// circuits. +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.7 Active Cable VDO, table 6.43 Active Cable VDO2. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ActiveCableVdo2 { + /// The signaling generation the cable supports. + pub usb_gen: UsbGen, + + /// Whether or not the cable supports asymmetric mode, as defined by the USB4 + /// and USB Type-C 2.4 specifications. + pub usb4_asymmetric_mode_supported: bool, + + /// Whether or not the cable is optically isolated, as defined by the USB Type-C + /// 2.4 specification. + pub optically_isolated_active_cable: bool, + + /// The number of lanes the cable supports. + pub usb_lanes_supported: UsbLanesSupported, + + /// Whether or not the cable supports USB 3.2 SuperSpeed signaling. + pub usb3p2_supported: bool, + + /// Whether or not the cable supports USB 2.0 only signaling. + pub usb2p0_supported: bool, + + /// The number of USB 2.0 "hub hops" that are lost due to the transmission time + /// of the cable. + pub usb2p0_hub_hops_consumed: u8, + + /// Whether or not the cable supports USB4 operation. + pub usb4_supported: bool, + + /// The cable's active element. + pub active_element: ActiveElement, + + /// The cable's construction between the active elements. + pub physical_connection: PhysicalConnection, + + /// Which U3 to U0 mode the cable supports. + pub u3_to_u0_transition_mode: U3ToU0TransitionMode, + + /// The power the cable consumes while in USB 3.2 U3 or USB4 CLd. + pub u3_cld_power: U3CldPower, + + /// The temperature, in degrees Celsius, at which the plug will shut down its + /// active signaling components. + pub shutdown_temperature: u8, + + /// The maximum allowable operating temperature inside the plug, in degrees Celsius. + pub maximum_operating_temperature: u8, +} + +/// Errors that can occur when parsing an [`ActiveCableVdo1`] from a raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseActiveCableVdo1Error { + /// [`ActiveCableVdo1::usb_highest_speed`] contains an invalid value. + InvalidUsbHighestSpeed, + + /// [`ActiveCableVdo1::vbus_current_handling_capability`] contains an invalid value. + InvalidVbusCurrentHandlingCapability, + + /// [`ActiveCableVdo1::sbu_type`] contains an invalid value. + InvalidSbuType, + + /// [`ActiveCableVdo1::maximum_vbus_voltage`] contains an invalid value. + InvalidMaximumVbusVoltage, + + /// [`ActiveCableVdo1::cable_termination_type`] contains an invalid value. + InvalidCableTerminationType, + + /// [`ActiveCableVdo1::cable_latency`] contains an invalid value. + InvalidCableLatency, + + /// [`ActiveCableVdo1::usb_type_c_or_captive`] contains an invalid value. + InvalidUsbTypeCOrCaptive, +} + +impl TryFrom for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(raw: Raw1) -> Result { + Ok(Self { + usb_highest_speed: raw + .usb_highest_speed() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidUsbHighestSpeed)?, + soppp_controller_present: raw.soppp_controller_present(), + vbus_through_cable: raw.vbus_through_cable(), + vbus_current_handling_capability: raw + .vbus_current_handling_capability() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidVbusCurrentHandlingCapability)?, + sbu_type: raw + .sbu_type() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidSbuType)?, + sbu_supported: !raw.sbu_supported_n(), + maximum_vbus_voltage: raw + .maximum_vbus_voltage() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidMaximumVbusVoltage)?, + cable_termination_type: raw + .cable_termination_type() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidCableTerminationType)?, + cable_latency: raw + .cable_latency() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidCableLatency)?, + epr_capable: raw.epr_capable(), + usb_type_c_or_captive: raw + .usb_type_c_or_captive() + .try_into() + .map_err(|()| ParseActiveCableVdo1Error::InvalidUsbTypeCOrCaptive)?, + firmware_version: raw.firmware_version(), + hw_version: raw.hw_version(), + }) + } +} + +impl TryFrom for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(value: u32) -> Result { + Raw1(value).try_into() + } +} + +impl TryFrom for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(value: ProductTypeVdo) -> Result { + Raw1(value.0).try_into() + } +} + +impl TryFrom<[u8; 4]> for ActiveCableVdo1 { + type Error = ParseActiveCableVdo1Error; + + fn try_from(bytes: [u8; 4]) -> Result { + let value = u32::from_le_bytes(bytes); + Raw1(value).try_into() + } +} + +/// Errors that can occur when parsing an [`ActiveCableVdo2`] from a raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseActiveCableVdo2Error { + /// [`ActiveCableVdo2::usb_gen`] contains an invalid value. + InvalidUsbGen, + + /// [`ActiveCableVdo2::usb4_asymmetric_mode_supported`] contains an invalid value. + InvalidUsb4AsymmetricModeSupported, + + /// [`ActiveCableVdo2::optically_isolated_active_cable`] contains an invalid value. + InvalidOpticallyIsolatedActiveCable, + + /// [`ActiveCableVdo2::usb_lanes_supported`] contains an invalid value. + InvalidUsbLanesSupported, + + /// [`ActiveCableVdo2::usb3p2_supported`] contains an invalid value. + InvalidUsb3p2Supported, + + /// [`ActiveCableVdo2::usb2p0_supported`] contains an invalid value. + InvalidUsb2p0Supported, + + /// [`ActiveCableVdo2::usb2p0_hub_hops_consumed`] contains an invalid value. + InvalidUsb2p0HubHopsConsumed, + + /// [`ActiveCableVdo2::usb4_supported`] contains an invalid value. + InvalidUsb4Supported, + + /// [`ActiveCableVdo2::active_element`] contains an invalid value. + InvalidActiveElement, + + /// [`ActiveCableVdo2::physical_connection`] contains an invalid value. + InvalidPhysicalConnection, + + /// [`ActiveCableVdo2::u3_to_u0_transition_mode`] contains an invalid value. + InvalidU3ToU0TransitionMode, + + /// [`ActiveCableVdo2::u3_cld_power`] contains an invalid value. + InvalidU3CldPower, +} + +impl TryFrom for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(raw: Raw2) -> Result { + Ok(Self { + usb_gen: raw + .usb_gen() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidUsbGen)?, + usb4_asymmetric_mode_supported: raw.usb4_asymmetric_mode_supported(), + optically_isolated_active_cable: raw.optically_isolated_active_cable(), + usb_lanes_supported: raw + .usb_lanes_supported() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidUsbLanesSupported)?, + usb3p2_supported: !raw.usb3p2_supported_n(), + usb2p0_supported: !raw.usb2p0_supported_n(), + usb2p0_hub_hops_consumed: raw.usb2p0_hub_hops_consumed(), + usb4_supported: !raw.usb4_supported_n(), + active_element: raw + .active_element() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidActiveElement)?, + physical_connection: raw + .physical_connection() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidPhysicalConnection)?, + u3_to_u0_transition_mode: raw + .u3_to_u0_transition_mode() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidU3ToU0TransitionMode)?, + u3_cld_power: raw + .u3_cld_power() + .try_into() + .map_err(|()| ParseActiveCableVdo2Error::InvalidU3CldPower)?, + shutdown_temperature: raw.shutdown_temperature(), + maximum_operating_temperature: raw.maximum_operating_temperature(), + }) + } +} + +impl TryFrom for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(value: u32) -> Result { + Raw2(value).try_into() + } +} + +impl TryFrom for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +impl TryFrom<[u8; 4]> for ActiveCableVdo2 { + type Error = ParseActiveCableVdo2Error; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +bitfield::bitfield! { + /// The raw value of an [`ActiveCableVdo1`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw1(u32); + impl Debug; + + /// See [`ActiveCableVdo1::usb_highest_speed`]. + pub u8, usb_highest_speed, set_usb_highest_speed: 2, 0; + + /// See [`ActiveCableVdo1::soppp_controller_present`]. + pub bool, soppp_controller_present, set_soppp_controller_present: 3; + + /// See [`ActiveCableVdo1::vbus_through_cable`]. + pub bool, vbus_through_cable, set_vbus_through_cable: 4; + + /// See [`ActiveCableVdo1::vbus_current_handling_capability`]. + pub u8, vbus_current_handling_capability, set_vbus_current_handling_capability: 6, 5; + + /// See [`ActiveCableVdo1::sbu_type`]. + pub u8, sbu_type, set_sbu_type: 7, 7; + + /// See [`ActiveCableVdo1::sbu_supported`]. + pub bool, sbu_supported_n, set_sbu_supported_n: 8; + + /// See [`ActiveCableVdo1::maximum_vbus_voltage`]. + pub u8, maximum_vbus_voltage, set_maximum_vbus_voltage: 10, 9; + + /// See [`ActiveCableVdo1::cable_termination_type`]. + pub u8, cable_termination_type, set_cable_termination_type: 12, 11; + + /// See [`ActiveCableVdo1::cable_latency`]. + pub u8, cable_latency, set_cable_latency: 16, 13; + + /// See [`ActiveCableVdo1::epr_capable`]. + pub bool, epr_capable, set_epr_capable: 17; + + /// See [`ActiveCableVdo1::usb_type_c_or_captive`]. + pub u8, usb_type_c_or_captive, set_usb_type_c_or_captive: 19, 18; + + /// See [`ActiveCableVdo1::firmware_version`]. + pub u8, firmware_version, set_firmware_version: 27, 24; + + /// See [`ActiveCableVdo1::hw_version`]. + pub u8, hw_version, set_hw_version: 31, 28; +} + +bitfield::bitfield! { + /// The raw value of an [`ActiveCableVdo2`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw2(u32); + impl Debug; + + /// See [`ActiveCableVdo2::usb_gen`]. + pub u8, usb_gen, set_usb_gen: 0, 0; + + /// See [`ActiveCableVdo2::usb4_asymmetric_mode_supported`]. + pub bool, usb4_asymmetric_mode_supported, set_usb4_asymmetric_mode_supported: 1; + + /// See [`ActiveCableVdo2::optically_isolated_active_cable`]. + pub bool, optically_isolated_active_cable, set_optically_isolated_active_cable: 2; + + /// See [`ActiveCableVdo2::usb_lanes_supported`]. + pub u8, usb_lanes_supported, set_usb_lanes_supported: 3, 3; + + /// See [`ActiveCableVdo2::usb3p2_supported`]. + pub bool, usb3p2_supported_n, set_usb3p2_supported_n: 4; + + /// See [`ActiveCableVdo2::usb2p0_supported`]. + pub bool, usb2p0_supported_n, set_usb2p0_supported_n: 5; + + /// See [`ActiveCableVdo2::usb2p0_hub_hops_consumed`]. + pub u8, usb2p0_hub_hops_consumed, set_usb2p0_hub_hops_consumed: 7, 6; + + /// See [`ActiveCableVdo2::usb4_supported`]. + pub bool, usb4_supported_n, set_usb4_supported_n: 8; + + /// See [`ActiveCableVdo2::active_element`]. + pub u8, active_element, set_active_element: 9, 9; + + /// See [`ActiveCableVdo2::physical_connection`]. + pub u8, physical_connection, set_physical_connection: 10, 10; + + /// See [`ActiveCableVdo2::u3_to_u0_transition_mode`]. + pub u8, u3_to_u0_transition_mode, set_u3_to_u0_transition_mode: 11, 11; + + /// See [`ActiveCableVdo2::u3_cld_power`]. + pub u8, u3_cld_power, set_u3_cld_power: 14, 12; + + /// See [`ActiveCableVdo2::shutdown_temperature`]. + pub u8, shutdown_temperature, set_shutdown_temperature: 23, 16; + + /// See [`ActiveCableVdo2::maximum_operating_temperature`]. + pub u8, maximum_operating_temperature, set_maximum_operating_temperature: 31, 24; +} + +/// The highest rate the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbHighestSpeed { + /// USB 2.0 only, no SuperSpeed support. + Usb2p0, + + /// USB 3.2 Gen1 + Usb3p2Gen1, + + /// USB 3.2 and USB4 Gen2 + Usb3p2, + + /// USB4 Gen3 + Usb4Gen3, + + /// USB4 Gen4 + Usb4Gen4, +} + +impl TryFrom for UsbHighestSpeed { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::Usb2p0), + 0b001 => Ok(Self::Usb3p2Gen1), + 0b010 => Ok(Self::Usb3p2), + 0b011 => Ok(Self::Usb4Gen3), + 0b100 => Ok(Self::Usb4Gen4), + _ => Err(()), + } + } +} + +/// Whether the cable is capable of carrying 3A or 5A. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VbusCurrentHandlingCapability { + /// 3A + ThreeAmps, + + /// 5A + FiveAmps, +} + +impl TryFrom for VbusCurrentHandlingCapability { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b01 => Ok(Self::ThreeAmps), + 0b10 => Ok(Self::FiveAmps), + _ => Err(()), + } + } +} + +/// Whether the SBUs are passive or active (e.g., digital). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SbuType { + /// SBU is passive. + Passive, + + /// SBU is active (e.g., digital). + Active, +} + +impl TryFrom for SbuType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Passive), + 1 => Ok(Self::Active), + _ => Err(()), + } + } +} + +/// The maximum voltage that shall be negotiated as part of an Explicit Contract. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MaximumVbusVoltage { + /// 20V + TwentyV, + + /// 30V + ThirtyV, + + /// 40V + FortyV, + + /// 50V + FiftyV, +} + +impl TryFrom for MaximumVbusVoltage { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::TwentyV), + 0b01 => Ok(Self::ThirtyV), + 0b10 => Ok(Self::FortyV), + 0b11 => Ok(Self::FiftyV), + _ => Err(()), + } + } +} + +/// Whether the Active Cable has one or two Cable Plugs requiring power from +/// `VCONN`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableTerminationType { + /// One end of the cable is active. + OneEndActive, + + /// Both ends of the cable are active. + BothEndsActive, +} + +impl TryFrom for CableTerminationType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::OneEndActive), + 0b11 => Ok(Self::BothEndsActive), + _ => Err(()), + } + } +} + +/// The signal latency through the cable, which can be used as an approximation +/// for its length. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableLatency { + /// <10ns (~1m) + LessThan10ns, + + /// 10ns to 20ns (~2m) + LessThan20ns, + + /// 20ns to 30ns (~3m) + LessThan30ns, + + /// 30ns to 40ns (~4m) + LessThan40ns, + + /// 40ns to 50ns (~5m) + LessThan50ns, + + /// 50ns to 60ns (~6m) + LessThan60ns, + + /// 60ns to 70ns (~7m) + LessThan70ns, + + /// 1000ns (~100m) + LessThan1000ns, + + /// 2000ns (~200m) + LessThan2000ns, + + /// 3000ns (~300m) + LessThan3000ns, +} + +impl TryFrom for CableLatency { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b0001 => Ok(Self::LessThan10ns), + 0b0010 => Ok(Self::LessThan20ns), + 0b0011 => Ok(Self::LessThan30ns), + 0b0100 => Ok(Self::LessThan40ns), + 0b0101 => Ok(Self::LessThan50ns), + 0b0110 => Ok(Self::LessThan60ns), + 0b0111 => Ok(Self::LessThan70ns), + 0b1000 => Ok(Self::LessThan1000ns), + 0b1001 => Ok(Self::LessThan2000ns), + 0b1010 => Ok(Self::LessThan3000ns), + _ => Err(()), + } + } +} + +/// Indicates whether the opposite end from the USB Type-C plug is another USB +/// Type-C plug or is a Captive Cable Assembly. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbTypeCOrCaptive { + /// The opposite end from the USB Type-C plug is another USB Type-C plug. + UsbTypeC, + + /// The opposite end from the USB Type-C plug is a Captive Cable Assembly. + Captive, +} + +impl TryFrom for UsbTypeCOrCaptive { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::UsbTypeC), + 0b11 => Ok(Self::Captive), + _ => Err(()), + } + } +} + +/// The signaling generation the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbGen { + /// Gen1 + Gen1, + + /// Gen2 or higher. + Gen2OrHigher, +} + +impl TryFrom for UsbGen { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Gen1), + 1 => Ok(Self::Gen2OrHigher), + _ => Err(()), + } + } +} + +/// The number of lanes the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbLanesSupported { + /// The cable supports one lane. + OneLane, + + /// The cable supports two lanes. + TwoLanes, +} + +impl TryFrom for UsbLanesSupported { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::OneLane), + 1 => Ok(Self::TwoLanes), + _ => Err(()), + } + } +} + +/// The cable's active element. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ActiveElement { + /// The active element is a re-driver. + Redriver, + + /// The active element is a re-timer. + Retimer, +} + +impl TryFrom for ActiveElement { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Redriver), + 1 => Ok(Self::Retimer), + _ => Err(()), + } + } +} + +/// The cable's construction between the active elements. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PhysicalConnection { + /// The connection between the active elements is made of copper. + Copper, + + /// The connection between the active elements is made of optical fiber. + Optical, +} + +impl TryFrom for PhysicalConnection { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Copper), + 1 => Ok(Self::Optical), + _ => Err(()), + } + } +} + +/// Which U3 to U0 mode the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum U3ToU0TransitionMode { + /// The cable supports a direct transition from U3 to U0. + Direct, + + /// The cable supports a transition from U3 to U0 through U3S. + ThroughU3S, +} + +impl TryFrom for U3ToU0TransitionMode { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Direct), + 1 => Ok(Self::ThroughU3S), + _ => Err(()), + } + } +} + +/// The power the cable consumes while in USB 3.2 U3 or USB4 CLd. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum U3CldPower { + /// >10mW + GreaterThan10Milliwatts, + + /// 5-10mW + FiveToTenMilliwatts, + + /// 1-5mW + OneToFiveMilliwatts, + + /// 0.5-1mW + P5To1Milliwatt, + + /// 0.2-0.5mW + P2ToP5Milliwatt, + + /// 50-200µW + FiftyToTwoHundredMicrowatts, + + /// <50µW + LessThanFiftyMicrowatts, +} + +impl TryFrom for U3CldPower { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::GreaterThan10Milliwatts), + 0b001 => Ok(Self::FiveToTenMilliwatts), + 0b010 => Ok(Self::OneToFiveMilliwatts), + 0b011 => Ok(Self::P5To1Milliwatt), + 0b100 => Ok(Self::P2ToP5Milliwatt), + 0b101 => Ok(Self::FiftyToTwoHundredMicrowatts), + 0b110 => Ok(Self::LessThanFiftyMicrowatts), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod usb_highest_speed { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbHighestSpeed); 5] = [ + (0b000, UsbHighestSpeed::Usb2p0), + (0b001, UsbHighestSpeed::Usb3p2Gen1), + (0b010, UsbHighestSpeed::Usb3p2), + (0b011, UsbHighestSpeed::Usb4Gen3), + (0b100, UsbHighestSpeed::Usb4Gen4), + ]; + for (raw, expected) in cases { + assert_eq!(UsbHighestSpeed::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 5..=255u8 { + assert!(UsbHighestSpeed::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod vbus_current_handling_capability { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, VbusCurrentHandlingCapability); 2] = [ + (0b01, VbusCurrentHandlingCapability::ThreeAmps), + (0b10, VbusCurrentHandlingCapability::FiveAmps), + ]; + for (raw, expected) in cases { + assert_eq!(VbusCurrentHandlingCapability::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 3] { + assert!( + VbusCurrentHandlingCapability::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod sbu_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, SbuType); 2] = [(0, SbuType::Passive), (1, SbuType::Active)]; + for (raw, expected) in cases { + assert_eq!(SbuType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(SbuType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod maximum_vbus_voltage { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, MaximumVbusVoltage); 4] = [ + (0b00, MaximumVbusVoltage::TwentyV), + (0b01, MaximumVbusVoltage::ThirtyV), + (0b10, MaximumVbusVoltage::FortyV), + (0b11, MaximumVbusVoltage::FiftyV), + ]; + for (raw, expected) in cases { + assert_eq!(MaximumVbusVoltage::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 4..=255u8 { + assert!(MaximumVbusVoltage::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod cable_termination_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableTerminationType); 2] = [ + (0b10, CableTerminationType::OneEndActive), + (0b11, CableTerminationType::BothEndsActive), + ]; + for (raw, expected) in cases { + assert_eq!(CableTerminationType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 1] { + assert!(CableTerminationType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod cable_latency { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableLatency); 10] = [ + (0b0001, CableLatency::LessThan10ns), + (0b0010, CableLatency::LessThan20ns), + (0b0011, CableLatency::LessThan30ns), + (0b0100, CableLatency::LessThan40ns), + (0b0101, CableLatency::LessThan50ns), + (0b0110, CableLatency::LessThan60ns), + (0b0111, CableLatency::LessThan70ns), + (0b1000, CableLatency::LessThan1000ns), + (0b1001, CableLatency::LessThan2000ns), + (0b1010, CableLatency::LessThan3000ns), + ]; + for (raw, expected) in cases { + assert_eq!(CableLatency::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + assert!(CableLatency::try_from(0u8).is_err()); + for v in 11..=255u8 { + assert!(CableLatency::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod usb_type_c_or_captive { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbTypeCOrCaptive); 2] = + [(0b10, UsbTypeCOrCaptive::UsbTypeC), (0b11, UsbTypeCOrCaptive::Captive)]; + for (raw, expected) in cases { + assert_eq!(UsbTypeCOrCaptive::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 1] { + assert!(UsbTypeCOrCaptive::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod usb_gen { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbGen); 2] = [(0, UsbGen::Gen1), (1, UsbGen::Gen2OrHigher)]; + for (raw, expected) in cases { + assert_eq!(UsbGen::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(UsbGen::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod usb_lanes_supported { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbLanesSupported); 2] = + [(0, UsbLanesSupported::OneLane), (1, UsbLanesSupported::TwoLanes)]; + for (raw, expected) in cases { + assert_eq!(UsbLanesSupported::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(UsbLanesSupported::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod active_element { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ActiveElement); 2] = [(0, ActiveElement::Redriver), (1, ActiveElement::Retimer)]; + for (raw, expected) in cases { + assert_eq!(ActiveElement::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(ActiveElement::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod physical_connection { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, PhysicalConnection); 2] = + [(0, PhysicalConnection::Copper), (1, PhysicalConnection::Optical)]; + for (raw, expected) in cases { + assert_eq!(PhysicalConnection::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(PhysicalConnection::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod u3_to_u0_transition_mode { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, U3ToU0TransitionMode); 2] = + [(0, U3ToU0TransitionMode::Direct), (1, U3ToU0TransitionMode::ThroughU3S)]; + for (raw, expected) in cases { + assert_eq!(U3ToU0TransitionMode::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(U3ToU0TransitionMode::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod u3_cld_power { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, U3CldPower); 7] = [ + (0b000, U3CldPower::GreaterThan10Milliwatts), + (0b001, U3CldPower::FiveToTenMilliwatts), + (0b010, U3CldPower::OneToFiveMilliwatts), + (0b011, U3CldPower::P5To1Milliwatt), + (0b100, U3CldPower::P2ToP5Milliwatt), + (0b101, U3CldPower::FiftyToTwoHundredMicrowatts), + (0b110, U3CldPower::LessThanFiftyMicrowatts), + ]; + for (raw, expected) in cases { + assert_eq!(U3CldPower::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 7..=255u8 { + assert!(U3CldPower::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/dfp_vdo.rs b/src/vdm/structured/command/discover_identity/dfp_vdo.rs new file mode 100644 index 0000000..ba7e0af --- /dev/null +++ b/src/vdm/structured/command/discover_identity/dfp_vdo.rs @@ -0,0 +1,91 @@ +//! A DFP (Downward Facing Port) is a Port that provides power and/or data to a +//! UFP. DFPs include Hosts, Hubs, and Power Bricks. +//! +//! See PD spec 6.4.4.3.1.5 DFP VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// Returned by Ports capable as operating as a DFP, including those implemented +/// by Hosts, Hubs, and Power Bricks. +/// +/// Sent based on the value of [`sop::IdHeaderVdo::product_type_dfp`][super::sop::IdHeaderVdo::product_type_dfp]. +/// +/// See PD spec 6.4.4.3.1.5 DFP VDO, +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DfpVdo { + /// A unique number that unambiguously identifies each USB Type-C 2.4 DFP, including + /// DRPs, on the device. + /// + /// This number is independent of the USB Port number. + pub port_number: u8, + + /// Whether the FDP can operate as a PDUSB Host, and its Capabilities when operating + /// as such. + pub host_capability: HostCapability, +} + +impl From for DfpVdo { + fn from(raw: Raw) -> Self { + Self { + port_number: raw.port_number(), + host_capability: raw.host_capability().into(), + } + } +} + +impl From for DfpVdo { + fn from(value: u32) -> Self { + Raw(value).into() + } +} + +impl From for DfpVdo { + fn from(value: ProductTypeVdo) -> Self { + value.0.into() + } +} + +impl From<[u8; 4]> for DfpVdo { + fn from(bytes: [u8; 4]) -> Self { + u32::from_le_bytes(bytes).into() + } +} + +bitfield::bitfield! { + /// The raw value of a [`DfpVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`DfpVdo::port_number`]. + pub u8, port_number, set_port_number: 4, 0; + + /// See [`DfpVdo::host_capability`]. + pub u8, host_capability, set_host_capability: 26, 24; +} + +/// A DFP's Capabilities when operating as a PDUSB Host. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct HostCapability { + /// USB 2.0 Host Capable. + pub usb2p0: bool, + + /// USB 3.2 Host Capable. + pub usb3p2: bool, + + /// USB4 Host Capable. + pub usb4: bool, +} + +impl From for HostCapability { + fn from(value: u8) -> Self { + Self { + usb2p0: value & 0b001 != 0, + usb3p2: value & 0b010 != 0, + usb4: value & 0b100 != 0, + } + } +} diff --git a/src/vdm/structured/command/discover_identity/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/id_header_vdo.rs new file mode 100644 index 0000000..b425ada --- /dev/null +++ b/src/vdm/structured/command/discover_identity/id_header_vdo.rs @@ -0,0 +1,42 @@ +use crate::vdm::structured::command::discover_identity::ConnectorType; + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +/// +/// This type differs from [`sop::IdHeaderVdo`] and [`sop_prime::IdHeaderVdo`] in +/// that it does not contain the product type fields. These fields are encoded into +/// the [`sop::ResponseVdos`] and [`sop_prime::ResponseVdos`] instead, in their +/// [`sop::ResponseVdos::dfp_product_type_vdos`], [`sop::ResponseVdos::ufp_product_type_vdos`], +/// and [`sop_prime::ResponseVdos::product_type_vdos`] fields, which provide a more +/// ergonomic API for accessing the Product Type VDOs. +/// +/// This type is not meant to be parsed directly; use the [`From`] implementations +/// on [`sop::IdHeaderVdo`] and [`sop_prime::IdHeaderVdo`] instead. +/// +/// [`sop::IdHeaderVdo`]: crate::vdm::structured::command::discover_identity::sop::IdHeaderVdo +/// [`sop_prime::IdHeaderVdo`]: crate::vdm::structured::command::discover_identity::sop_prime::IdHeaderVdo +/// [`sop::ResponseVdos`]: crate::vdm::structured::command::discover_identity::sop::ResponseVdos +/// [`sop_prime::ResponseVdos`]: crate::vdm::structured::command::discover_identity::sop_prime::ResponseVdos +/// [`sop::ResponseVdos::dfp_product_type_vdos`]: crate::vdm::structured::command::discover_identity::sop::ResponseVdos::dfp_product_type_vdos +/// [`sop::ResponseVdos::ufp_product_type_vdos`]: crate::vdm::structured::command::discover_identity::sop::ResponseVdos::ufp_product_type_vdos +/// [`sop_prime::ResponseVdos::product_type_vdos`]: crate::vdm::structured::command::discover_identity::sop_prime::ResponseVdos::product_type_vdos +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} diff --git a/src/vdm/structured/command/discover_identity/mod.rs b/src/vdm/structured/command/discover_identity/mod.rs new file mode 100644 index 0000000..8461ab0 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/mod.rs @@ -0,0 +1,62 @@ +//! The Discover Identity Command is used to identify a Port Partner and the Responder (Cable Plug or VPD). +//! +//! See PD spec 6.4.4.3.1 Discover Identity. + +pub mod active_cable_vdo; +pub mod dfp_vdo; +pub mod id_header_vdo; +pub mod passive_cable_vdo; +pub mod product_vdo; +pub mod sop; +pub mod sop_prime; +pub mod ufp_vdo; +pub mod vpd_vdo; + +pub use active_cable_vdo::{ActiveCableVdo1, ActiveCableVdo2}; +pub use dfp_vdo::DfpVdo; +pub use id_header_vdo::IdHeaderVdo; +pub use passive_cable_vdo::PassiveCableVdo; +pub use product_vdo::ProductVdo; +pub use ufp_vdo::UfpVdo; +pub use vpd_vdo::VpdVdo; + +/// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. +/// +/// See PD spec 6.4.4.3.1.1.7 Connector Type Field. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConnectorType { + /// The device is a USB Type-C receptacle. + Receptacle, + + /// The device is a USB Type-C plug. + Plug, +} + +impl TryFrom for ConnectorType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::Receptacle), + 0b11 => Ok(Self::Plug), + _ => Err(()), + } + } +} + +/// Contains the XID assigned by USB-IF to the product before certification in binary format. +/// +/// See PD spec 6.4.4.3.1.2 Cert Stat VDO, table 6.37 Cert Stat VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CertStatVdo(pub u32); + +/// An unspecified Product Type VDO in the Product Type VDO(s) of the Discover +/// Identity Command response. +/// +/// The type of this VDO is determined by the ID Header VDO and whether targeting +/// SOP or SOP'. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProductTypeVdo(pub u32); diff --git a/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs b/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs new file mode 100644 index 0000000..15b07de --- /dev/null +++ b/src/vdm/structured/command/discover_identity/passive_cable_vdo.rs @@ -0,0 +1,509 @@ +//! A Passive Cable has a USB Plug on each end, at least one of which is a Cable +//! Plug supporting SOP' Communication. +//! +//! See PD spec 6.4.4.3.1.6 Passive Cable VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// A Passive Cable has a USB Plug on each end, at least one of which is a Cable +/// Plug supporting SOP' Communication. +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.6 Passive Cable VDO, table 6.41 Passive Cable VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PassiveCableVdo { + /// The highest rate the cable supports. + pub usb_highest_speed: UsbHighestSpeed, + + /// Indicates whether the cable can carry 3A or 5A. + pub vbus_current_handling_capability: VbusCurrentHandlingCapability, + + /// The maximum voltage that shall be negotiated using a Fixed Supply over the + /// cable as part of an Explicit Contract. + pub maximum_vbus_voltage: MaximumVbusVoltage, + + /// Whether the cable needs `VCONN` only initially in order to support the Discover + /// Identity Command, after which it can be removed, or if it needs `VCONN` + /// to be continuously applied to power some feature of the Cable Plug. + pub cable_termination_type: CableTerminationType, + + /// The signal latency through the cable, which can be used as an approximation + /// for its length. + pub cable_latency: CableLatency, + + /// Whether or not the cable is designed for safe operation when carrying up + /// to 48 volts at 5 amps. + pub epr_capable: bool, + + /// Whether or not the opposite end from the USB Type-C plug is another USB + /// Type-C plug or a Captive Cable Assembly. + pub usb_type_c_or_captive: UsbTypeCPlugOrCaptive, + + /// The FW version assigned by the VID owner. + pub firmware_version: u8, + + /// The HW version assigned by the VID owner. + pub hw_version: u8, +} + +impl TryFrom for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_highest_speed: raw + .usb_highest_speed() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidUsbHighestSpeed)?, + vbus_current_handling_capability: raw + .vbus_current_handling_capability() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidVbusCurrentHandlingCapability)?, + maximum_vbus_voltage: raw + .maximum_vbus_voltage() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidMaximumVbusVoltage)?, + cable_termination_type: raw + .cable_termination_type() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidCableTerminationType)?, + cable_latency: raw + .cable_latency() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidCableLatency)?, + epr_capable: raw.epr_capable(), + usb_type_c_or_captive: raw + .usb_type_c_or_captive() + .try_into() + .map_err(|()| ParsePassiveCableVdoError::InvalidUsbTypeCOrCaptive)?, + firmware_version: raw.firmware_version(), + hw_version: raw.hw_version(), + }) + } +} + +impl TryFrom for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +impl TryFrom<[u8; 4]> for PassiveCableVdo { + type Error = ParsePassiveCableVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +/// Errors that can occur when parsing a [`PassiveCableVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParsePassiveCableVdoError { + /// [`PassiveCableVdo::usb_highest_speed`] contains an invalid value. + InvalidUsbHighestSpeed, + + /// [`PassiveCableVdo::vbus_current_handling_capability`] contains an invalid value. + InvalidVbusCurrentHandlingCapability, + + /// [`PassiveCableVdo::maximum_vbus_voltage`] contains an invalid value. + InvalidMaximumVbusVoltage, + + /// [`PassiveCableVdo::cable_termination_type`] contains an invalid value. + InvalidCableTerminationType, + + /// [`PassiveCableVdo::cable_latency`] contains an invalid value. + InvalidCableLatency, + + /// [`PassiveCableVdo::usb_type_c_or_captive`] contains an invalid value. + InvalidUsbTypeCOrCaptive, +} + +bitfield::bitfield! { + /// The raw value of a [`PassiveCableVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`PassiveCableVdo::usb_highest_speed`]. + pub u8, usb_highest_speed, set_usb_highest_speed: 2, 0; + + /// See [`PassiveCableVdo::vbus_current_handling_capability`]. + pub u8, vbus_current_handling_capability, set_vbus_current_handling_capability: 6, 5; + + /// See [`PassiveCableVdo::maximum_vbus_voltage`]. + pub u8, maximum_vbus_voltage, set_maximum_vbus_voltage: 10, 9; + + /// See [`PassiveCableVdo::cable_termination_type`]. + pub u8, cable_termination_type, set_cable_termination_type: 12, 11; + + /// See [`PassiveCableVdo::cable_latency`]. + pub u8, cable_latency, set_cable_latency: 16, 13; + + /// See [`PassiveCableVdo::epr_capable`]. + pub bool, epr_capable, set_epr_capable: 17; + + /// See [`PassiveCableVdo::usb_type_c_or_captive`]. + pub u8, usb_type_c_or_captive, set_usb_type_c_or_captive: 19, 18; + + /// See [`PassiveCableVdo::firmware_version`]. + pub u8, firmware_version, set_firmware_version: 27, 24; + + /// See [`PassiveCableVdo::hw_version`]. + pub u8, hw_version, set_hw_version: 31, 28; +} + +/// The highest rate the cable supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbHighestSpeed { + /// USB 2.0 only, no SuperSpeed support. + Usb2p0, + + /// USB 3.2 Gen1. + Usb3p2Gen1, + + /// USB 3.2 and USB4 Gen2. + Usb3p2, + + /// USB4 Gen3. + Usb4Gen3, + + /// USB4 Gen4. + Usb4Gen4, +} + +impl TryFrom for UsbHighestSpeed { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::Usb2p0), + 0b001 => Ok(Self::Usb3p2Gen1), + 0b010 => Ok(Self::Usb3p2), + 0b011 => Ok(Self::Usb4Gen3), + 0b100 => Ok(Self::Usb4Gen4), + _ => Err(()), + } + } +} + +/// Indicates whether the cable can carry 3A or 5A. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VbusCurrentHandlingCapability { + /// 3A + ThreeAmps, + + /// 5A + FiveAmps, +} + +impl TryFrom for VbusCurrentHandlingCapability { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b01 => Ok(Self::ThreeAmps), + 0b10 => Ok(Self::FiveAmps), + _ => Err(()), + } + } +} + +/// The maximum voltage that shall be negotiated using a Fixed Supply over the +/// cable as part of an Explicit Contract. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MaximumVbusVoltage { + /// 20V + TwentyVolt, + + /// 30V + ThirtyVolt, + + /// 40V + FortyVolt, + + /// 50V + FiftyVolt, +} + +impl TryFrom for MaximumVbusVoltage { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::TwentyVolt), + 0b01 => Ok(Self::ThirtyVolt), + 0b10 => Ok(Self::FortyVolt), + 0b11 => Ok(Self::FiftyVolt), + _ => Err(()), + } + } +} + +/// Whether the cable needs `VCONN` only initially in order to support the Discover +/// Identity Command, after which it can be removed, or if it needs `VCONN` +/// to be continuously applied to power some feature of the Cable Plug. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableTerminationType { + /// The cable only requires `VCONN` to support the Discover Identity Command, + /// after which it can be removed. + VconnNotRequired, + + /// The cable requires `VCONN` to be continuously applied to power some feature + /// of the Cable Plug, in addition to needing `VCONN` to support the Discover + /// Identity Command. + VconnRequired, +} + +impl TryFrom for CableTerminationType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::VconnNotRequired), + 0b01 => Ok(Self::VconnRequired), + _ => Err(()), + } + } +} + +/// The signal latency through the cable, which can be used as an approximation +/// for its length. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CableLatency { + /// <10ns (~1m) + LessThan10ns, + + /// 10ns to 20ns (~2m) + LessThan20ns, + + /// 20ns to 30ns (~3m) + LessThan30ns, + + /// 30ns to 40ns (~4m) + LessThan40ns, + + /// 40ns to 50ns (~5m) + LessThan50ns, + + /// 50ns to 60ns (~6m) + LessThan60ns, + + /// 60ns to 70ns (~7m) + LessThan70ns, + + /// >70ns (>~7m) + GreaterThan70ns, +} + +impl TryFrom for CableLatency { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b0001 => Ok(Self::LessThan10ns), + 0b0010 => Ok(Self::LessThan20ns), + 0b0011 => Ok(Self::LessThan30ns), + 0b0100 => Ok(Self::LessThan40ns), + 0b0101 => Ok(Self::LessThan50ns), + 0b0110 => Ok(Self::LessThan60ns), + 0b0111 => Ok(Self::LessThan70ns), + 0b1000 => Ok(Self::GreaterThan70ns), + _ => Err(()), + } + } +} + +/// Whether or not the opposite end from the USB Type-C plug is another USB +/// Type-C plug or a Captive Cable Assembly. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbTypeCPlugOrCaptive { + /// Opposite end from the plug is another USB Type-C plug. + UsbTypeC, + + /// Opposite end from the plug is a Captive Cable Assembly. + Captive, +} + +impl TryFrom for UsbTypeCPlugOrCaptive { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b10 => Ok(Self::UsbTypeC), + 0b11 => Ok(Self::Captive), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod usb_highest_speed { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbHighestSpeed); 5] = [ + (0b000, UsbHighestSpeed::Usb2p0), + (0b001, UsbHighestSpeed::Usb3p2Gen1), + (0b010, UsbHighestSpeed::Usb3p2), + (0b011, UsbHighestSpeed::Usb4Gen3), + (0b100, UsbHighestSpeed::Usb4Gen4), + ]; + for (raw, expected) in cases { + assert_eq!(UsbHighestSpeed::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 5..=255u8 { + assert!(UsbHighestSpeed::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod vbus_current_handling_capability { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, VbusCurrentHandlingCapability); 2] = [ + (0b01, VbusCurrentHandlingCapability::ThreeAmps), + (0b10, VbusCurrentHandlingCapability::FiveAmps), + ]; + for (raw, expected) in cases { + assert_eq!(VbusCurrentHandlingCapability::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 3] { + assert!( + VbusCurrentHandlingCapability::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod maximum_vbus_voltage { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, MaximumVbusVoltage); 4] = [ + (0b00, MaximumVbusVoltage::TwentyVolt), + (0b01, MaximumVbusVoltage::ThirtyVolt), + (0b10, MaximumVbusVoltage::FortyVolt), + (0b11, MaximumVbusVoltage::FiftyVolt), + ]; + for (raw, expected) in cases { + assert_eq!(MaximumVbusVoltage::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 4..=255u8 { + assert!(MaximumVbusVoltage::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod cable_termination_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableTerminationType); 2] = [ + (0b00, CableTerminationType::VconnNotRequired), + (0b01, CableTerminationType::VconnRequired), + ]; + for (raw, expected) in cases { + assert_eq!(CableTerminationType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!(CableTerminationType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod cable_latency { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CableLatency); 8] = [ + (0b0001, CableLatency::LessThan10ns), + (0b0010, CableLatency::LessThan20ns), + (0b0011, CableLatency::LessThan30ns), + (0b0100, CableLatency::LessThan40ns), + (0b0101, CableLatency::LessThan50ns), + (0b0110, CableLatency::LessThan60ns), + (0b0111, CableLatency::LessThan70ns), + (0b1000, CableLatency::GreaterThan70ns), + ]; + for (raw, expected) in cases { + assert_eq!(CableLatency::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + assert!(CableLatency::try_from(0u8).is_err()); + for v in 9..=255u8 { + assert!(CableLatency::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod usb_type_c_plug_or_captive { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbTypeCPlugOrCaptive); 2] = [ + (0b10, UsbTypeCPlugOrCaptive::UsbTypeC), + (0b11, UsbTypeCPlugOrCaptive::Captive), + ]; + for (raw, expected) in cases { + assert_eq!(UsbTypeCPlugOrCaptive::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0u8, 1] { + assert!(UsbTypeCPlugOrCaptive::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/product_vdo.rs b/src/vdm/structured/command/discover_identity/product_vdo.rs new file mode 100644 index 0000000..f955d6d --- /dev/null +++ b/src/vdm/structured/command/discover_identity/product_vdo.rs @@ -0,0 +1,53 @@ +//! [`ProductVdo`] contains identity information relating to the product. +//! +//! See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. + +use crate::usb::{Bcd, ProductId}; + +/// The Product VDO contains identity information relating to the product. +/// +/// See PD spec 6.4.4.3.1.3 Product VDO, table 6.38 Product VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProductVdo { + /// The USB Product ID, as defined by the USB 2.0 / USB 3.2 specifications. + pub usb_product_id: ProductId, + + /// The USB Device Release Number, as defined by the USB 2.0 / USB 3.2 specifications. + pub bcd_device: Bcd, +} + +bitfield::bitfield! { + /// The Raw value of a [`ProductVdo`], before parsing bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`ProductVdo::usb_product_id`]. + pub u16, usb_product_id, set_usb_product_id: 15, 0; + + /// See [`ProductVdo::bcd_device`]. + pub u16, bcd_device, set_bcd_device: 31, 16 +} + +impl From for ProductVdo { + fn from(raw: Raw) -> Self { + Self { + usb_product_id: ProductId(raw.usb_product_id()), + bcd_device: Bcd(raw.bcd_device()), + } + } +} + +impl From for ProductVdo { + fn from(value: u32) -> Self { + Raw(value).into() + } +} + +impl From<[u8; 4]> for ProductVdo { + fn from(bytes: [u8; 4]) -> Self { + u32::from_le_bytes(bytes).into() + } +} diff --git a/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs new file mode 100644 index 0000000..5352257 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop/id_header_vdo.rs @@ -0,0 +1,316 @@ +use super::{DfpProductTypeVdos, UfpProductTypeVdos}; +use crate::vdm::structured::command::discover_identity::ConnectorType; + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// This type differs from [`crate::vdm::structured::command::discover_identity::IdHeaderVdo`] +/// in that it contains the product type fields, which are encoded into the [`ResponseVdos::dfp_product_type_vdos`] +/// and [`ResponseVdos::ufp_product_type_vdos`] fields. This type is meant to be parsed directly from the raw VDO. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +/// +/// [`ResponseVdos::dfp_product_type_vdos`]: super::ResponseVdos::dfp_product_type_vdos +/// [`ResponseVdos::ufp_product_type_vdos`]: super::ResponseVdos::ufp_product_type_vdos +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates the type of Product when in DFP Data Role, whether a VDO will be + /// returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`ResponseVdos::dfp_product_type_vdos`] is interpreted. + /// + /// [`ResponseVdos::dfp_product_type_vdos`]: super::ResponseVdos::dfp_product_type_vdos + pub product_type_dfp: ProductTypeDfp, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Indicates the type of Product when in UFP Data Role, whether a VDO will be + /// returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`ResponseVdos::ufp_product_type_vdos`] is interpreted. + /// + /// [`ResponseVdos::ufp_product_type_vdos`]: super::ResponseVdos::ufp_product_type_vdos + pub product_type_ufp: ProductTypeUfp, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} + +bitfield::bitfield! { + /// The raw value of an [`IdHeaderVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`IdHeaderVdo::usb_vendor_id`]. + pub u16, usb_vendor_id, set_usb_vendor_id: 15, 0; + + /// See [`IdHeaderVdo::connector_type`]. + pub u8, connector_type, set_connector_type: 22, 21; + + /// See [`IdHeaderVdo::product_type_dfp`]. + pub u8, product_type_dfp, set_product_type_dfp: 25, 23; + + /// See [`IdHeaderVdo::modal_operation_supported`]. + pub bool, modal_operation_supported, set_modal_operation_supported: 26; + + /// See [`IdHeaderVdo::product_type_ufp`]. + pub u8, product_type_ufp, set_product_type_ufp: 29, 27; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_device`]. + pub bool, usb_communication_capable_as_usb_device, set_usb_communication_capable_as_usb_device: 30; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_host`]. + pub bool, usb_communication_capable_as_usb_host, set_usb_communication_capable_as_usb_host: 31; +} + +/// Errors that can occur when parsing an [`IdHeaderVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseIdHeaderVdoError { + InvalidConnectorType, + InvalidProductTypeDfp, + InvalidProductTypeUfp, +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_vendor_id: raw.usb_vendor_id(), + connector_type: raw + .connector_type() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidConnectorType)?, + product_type_dfp: raw + .product_type_dfp() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidProductTypeDfp)?, + modal_operation_supported: raw.modal_operation_supported(), + product_type_ufp: raw + .product_type_ufp() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidProductTypeUfp)?, + usb_communication_capable_as_usb_device: raw.usb_communication_capable_as_usb_device(), + usb_communication_capable_as_usb_host: raw.usb_communication_capable_as_usb_host(), + }) + } +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom<[u8; 4]> for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +impl From for crate::vdm::structured::command::discover_identity::IdHeaderVdo { + fn from(value: IdHeaderVdo) -> Self { + Self { + usb_vendor_id: value.usb_vendor_id, + connector_type: value.connector_type, + modal_operation_supported: value.modal_operation_supported, + usb_communication_capable_as_usb_device: value.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: value.usb_communication_capable_as_usb_host, + } + } +} + +/// The [`IdHeaderVdo::product_type_dfp`] field indicates the type of Product when +/// in DFP Data Role, whether a VDO will be returned, and if so, the type of VDO +/// to be returned. +/// +/// See PD spec 6.4.4.3.1.1.6 Product Type (DFP), table 6.36 Product Types (DFP). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeDfp { + /// This is not a DFP. + /// + /// The Product Type VDOs is empty. + NotADfp, + + /// The product is a PDUSB Hub. + /// + /// If the device is not a Dual-Role Device, the first item in the Product Type + /// VDOs is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in the Product Type VDOs + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Hub, + + /// The product is a PDUSB Host or a PDUSB host that supports one or more Alternate + /// Modes as an AMC. + /// + /// If the device is not a Dual-Role Device, the first item in the Product Type + /// VDOs is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in the Product Type VDOs + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Host, + + /// The product is a charger / power brick. + /// + /// If the device is not a Dual-Role Device, the first item in the Product Type + /// VDOs is a [`DfpVdo`][`super::DfpVdo`]. + /// + /// If the device is a Dual-Role Device, the first item in the Product Type VDOs + /// is defined by [`IdHeaderVdo::product_type_ufp`], the second is padding (all 0s), + /// and the third item is a [`DfpVdo`][`super::DfpVdo`]. + Charger, +} + +impl TryFrom for ProductTypeDfp { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::NotADfp), + 0b001 => Ok(Self::Hub), + 0b010 => Ok(Self::Host), + 0b011 => Ok(Self::Charger), + _ => Err(()), + } + } +} + +impl From for ProductTypeUfp { + fn from(ufp_product_type_vdos: UfpProductTypeVdos) -> Self { + match ufp_product_type_vdos { + UfpProductTypeVdos::NotAUfp => Self::NotAUfp, + UfpProductTypeVdos::Hub(_) => Self::Hub, + UfpProductTypeVdos::Peripheral(_) => Self::Peripheral, + UfpProductTypeVdos::Psd => Self::Psd, + } + } +} + +/// The [`IdHeaderVdo::product_type_ufp`] field indicates the type of Product when +/// in the UFP Data Role, whether a VDO will be returned, and if so, the type of +/// VDO to be returned. +/// +/// See PD spec 6.4.4.3.1.1.3 Product Type (UFP), table 6.34 Product Types (UFP). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeUfp { + /// This is not a UFP. + /// + /// The Product Type VDOs is empty. + NotAUfp, + + /// The product is a PDUSB Hub. + /// + /// The first item in the Product Type VDOs is a [`UfpVdo`][`super::UfpVdo`]. + Hub, + + /// The product is a PDUSB Device other than a Hub. + /// + /// The first item in the Product Type VDOs is a [`UfpVdo`][`super::UfpVdo`]. + Peripheral, + + /// The product is a PSD, e.g., power bank. + /// + /// The Product Type VDOs is empty. + Psd, +} + +impl TryFrom for ProductTypeUfp { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::NotAUfp), + 0b001 => Ok(Self::Hub), + 0b010 => Ok(Self::Peripheral), + 0b011 => Ok(Self::Psd), + _ => Err(()), + } + } +} + +impl From for ProductTypeDfp { + fn from(dfp_product_type_vdos: DfpProductTypeVdos) -> Self { + match dfp_product_type_vdos { + DfpProductTypeVdos::NotADfp => Self::NotADfp, + DfpProductTypeVdos::Hub(_) => Self::Hub, + DfpProductTypeVdos::Host(_) => Self::Host, + DfpProductTypeVdos::Charger(_) => Self::Charger, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod product_type_dfp { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ProductTypeDfp); 4] = [ + (0b000, ProductTypeDfp::NotADfp), + (0b001, ProductTypeDfp::Hub), + (0b010, ProductTypeDfp::Host), + (0b011, ProductTypeDfp::Charger), + ]; + for (raw, expected) in cases { + assert_eq!(ProductTypeDfp::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0b100, 0b101, 0b110, 0b111] { + assert!(ProductTypeDfp::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod product_type_ufp { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ProductTypeUfp); 4] = [ + (0b000, ProductTypeUfp::NotAUfp), + (0b001, ProductTypeUfp::Hub), + (0b010, ProductTypeUfp::Peripheral), + (0b011, ProductTypeUfp::Psd), + ]; + for (raw, expected) in cases { + assert_eq!(ProductTypeUfp::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0b100, 0b101, 0b110, 0b111] { + assert!(ProductTypeUfp::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/sop/mod.rs b/src/vdm/structured/command/discover_identity/sop/mod.rs new file mode 100644 index 0000000..dd5de9f --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop/mod.rs @@ -0,0 +1,87 @@ +//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targeting SOP. + +use crate::vdm::structured::command::discover_identity::{CertStatVdo, DfpVdo, ProductVdo, UfpVdo}; + +pub mod id_header_vdo; + +pub use id_header_vdo::IdHeaderVdo; + +/// The response VDOs to a Discover Identity Command using SOP. +/// +/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ResponseVdos { + /// Information corresponding to the Product. + /// + /// To get an SOP-specific ID Header VDO, use [`Self::id()`]. + pub id: crate::vdm::structured::command::discover_identity::IdHeaderVdo, + + /// The XID assigned by the USB-IF to the product. + pub cert_stat: CertStatVdo, + + /// Identity information relating to the product. + pub product: ProductVdo, + + /// The Product-specific DFP VDOs. + /// + /// These are determined by the [`IdHeaderVdo::product_type_dfp`] field during + /// parsing. + pub dfp_product_type_vdos: DfpProductTypeVdos, + + /// The Product-specific UFP VDOs. + /// + /// These are determined by the [`IdHeaderVdo::product_type_ufp`] field during + /// parsing. + pub ufp_product_type_vdos: UfpProductTypeVdos, +} + +impl ResponseVdos { + /// Gets the SOP-specific ID Header VDO from this response. + pub fn id(&self) -> IdHeaderVdo { + IdHeaderVdo { + usb_vendor_id: self.id.usb_vendor_id, + connector_type: self.id.connector_type, + product_type_dfp: self.dfp_product_type_vdos.into(), + modal_operation_supported: self.id.modal_operation_supported, + product_type_ufp: self.ufp_product_type_vdos.into(), + usb_communication_capable_as_usb_device: self.id.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: self.id.usb_communication_capable_as_usb_host, + } + } +} + +/// The Product Type DFP VDOs, parsed based on [`IdHeaderVdo::product_type_dfp`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum DfpProductTypeVdos { + /// This is not a DFP. + NotADfp, + + /// The product is a PDUSB Hub. + Hub(DfpVdo), + + /// The product is a PDUSB Host or a PDUSB host that supports one or more Alternate + /// Modes as an AMC. + Host(DfpVdo), + + /// The product is a charger / power brick. + Charger(DfpVdo), +} + +/// The Product Type UFP VDOs, parsed based on [`IdHeaderVdo::product_type_ufp`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UfpProductTypeVdos { + /// This is not a UFP. + NotAUfp, + + /// The product is a PDUSB Hub. + Hub(UfpVdo), + + /// The product is a PDUSB Device other than a Hub. + Peripheral(UfpVdo), + + /// The product is a PSD, e.g., power bank. + Psd, +} diff --git a/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs new file mode 100644 index 0000000..4ccf699 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop_prime/id_header_vdo.rs @@ -0,0 +1,214 @@ +use super::ProductTypeVdos; +use crate::vdm::structured::command::discover_identity::ConnectorType; + +/// The ID Header VDO contains information corresponding to the Power Delivery Product. +/// +/// This type differs from [`crate::vdm::structured::command::discover_identity::IdHeaderVdo`] +/// in that it contains the product type fields, which are encoded into the [`ResponseVdos::product_type_vdos`] +/// field. This type is meant to be parsed directly from the raw VDO. +/// +/// See PD spec 6.4.4.3.1.1 ID Header VDO, table 6.3.3 ID Header VDO. +/// +/// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct IdHeaderVdo { + /// The USB Vendor ID as assigned by the USB-IF. + pub usb_vendor_id: u16, + + /// Identifies the device as either a USB Type-C receptacle of a USB Type-C plug. + pub connector_type: ConnectorType, + + /// Indicates whether or not the Product (either a Cable Plug or a device that + /// can operate in the UFP role) is capable of supporting Modes. + pub modal_operation_supported: bool, + + /// Indicates the type of Product when the Product is a Cable Plug or VPD, whether + /// a VDO will be returned, and if so, the type of VDO to be returned. + /// + /// The value of this type changes how [`ResponseVdos::product_type_vdos`] is interpreted. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos + pub product_type: ProductType, + + /// Whether or not the Port has a USB Device Capability. + pub usb_communication_capable_as_usb_device: bool, + + /// Whether or not the Port has a USB Host Capability. + pub usb_communication_capable_as_usb_host: bool, +} + +bitfield::bitfield! { + /// The raw value of an [`IdHeaderVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`IdHeaderVdo::usb_vendor_id`]. + pub u16, usb_vendor_id, set_usb_vendor_id: 15, 0; + + /// See [`IdHeaderVdo::connector_type`]. + pub u8, connector_type, set_connector_type: 22, 21; + + /// See [`IdHeaderVdo::modal_operation_supported`]. + pub bool, modal_operation_supported, set_modal_operation_supported: 26; + + /// See [`IdHeaderVdo::product_type`]. + pub u8, product_type, set_product_type: 29, 27; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_device`]. + pub bool, usb_communication_capable_as_usb_device, set_usb_communication_capable_as_usb_device: 30; + + /// See [`IdHeaderVdo::usb_communication_capable_as_usb_host`]. + pub bool, usb_communication_capable_as_usb_host, set_usb_communication_capable_as_usb_host: 31; +} + +/// Errors that can occur when parsing an [`IdHeaderVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseIdHeaderVdoError { + InvalidConnectorType, + InvalidProductType, +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_vendor_id: raw.usb_vendor_id(), + connector_type: raw + .connector_type() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidConnectorType)?, + modal_operation_supported: raw.modal_operation_supported(), + product_type: raw + .product_type() + .try_into() + .map_err(|()| ParseIdHeaderVdoError::InvalidProductType)?, + usb_communication_capable_as_usb_device: raw.usb_communication_capable_as_usb_device(), + usb_communication_capable_as_usb_host: raw.usb_communication_capable_as_usb_host(), + }) + } +} + +impl TryFrom for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom<[u8; 4]> for IdHeaderVdo { + type Error = ParseIdHeaderVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +impl From for crate::vdm::structured::command::discover_identity::IdHeaderVdo { + fn from(id_header_vdo: IdHeaderVdo) -> Self { + Self { + usb_vendor_id: id_header_vdo.usb_vendor_id, + connector_type: id_header_vdo.connector_type, + modal_operation_supported: id_header_vdo.modal_operation_supported, + usb_communication_capable_as_usb_device: id_header_vdo.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: id_header_vdo.usb_communication_capable_as_usb_host, + } + } +} + +/// The `SOP'` Product Type (Cable Plug/VPD) field indicates the type of Product +/// when the Product is a Cable Plug or VPD, whether a VDO will be returned, and +/// if so, the type of VDO to be returned. +/// +/// See PD spec 6.4.4.3.1.1.4 Product Type (Cable Plug), table 6.35 Product Types (Cable Plug/VPD). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductType { + /// No other Product Type is appropriate. + /// + /// [`ResponseVdos::product_type_vdos`] is empty. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos + NotACablePlugVpd, + + /// The Product is a cable that does not incorporate signal conditioning circuits. + /// + /// The first item in [`ResponseVdos::product_type_vdos`] is a [`PassiveCableVdo`][`super::PassiveCableVdo`]. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos + PassiveCable, + + /// The Product is a cable that incorporates signal conditioning circuits. + /// + /// The first item in [`ResponseVdos::product_type_vdos`] is a [`ActiveCableVdo1`][`super::ActiveCableVdo1`]. + /// The second item is a [`ActiveCableVdo2`][`super::ActiveCableVdo2`]. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos + ActiveCable, + + /// The Product is a `VCONN`-powered USB device. + /// + /// The first item in [`ResponseVdos::product_type_vdos`] is a [`VpdVdo`][`super::VpdVdo`]. + /// + /// [`ResponseVdos::product_type_vdos`]: super::ResponseVdos::product_type_vdos + Vpd, +} + +impl TryFrom for ProductType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::NotACablePlugVpd), + 0b011 => Ok(Self::PassiveCable), + 0b100 => Ok(Self::ActiveCable), + 0b110 => Ok(Self::Vpd), + _ => Err(()), + } + } +} + +impl From for ProductType { + fn from(product_type_vdos: ProductTypeVdos) -> Self { + match product_type_vdos { + ProductTypeVdos::NotACablePlugVpd => Self::NotACablePlugVpd, + ProductTypeVdos::PassiveCable(_) => Self::PassiveCable, + ProductTypeVdos::ActiveCable(_, _) => Self::ActiveCable, + ProductTypeVdos::Vpd(_) => Self::Vpd, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod product_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ProductType); 4] = [ + (0b000, ProductType::NotACablePlugVpd), + (0b011, ProductType::PassiveCable), + (0b100, ProductType::ActiveCable), + (0b110, ProductType::Vpd), + ]; + for (raw, expected) in cases { + assert_eq!(ProductType::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in [0b001, 0b010, 0b101, 0b111] { + assert!(ProductType::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/sop_prime/mod.rs b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs new file mode 100644 index 0000000..9ede491 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/sop_prime/mod.rs @@ -0,0 +1,63 @@ +//! [`ResponseVdos`] contains the response VDOs to a Discover Identity Command targeting SOP'. + +use crate::vdm::structured::command::discover_identity::{ + ActiveCableVdo1, ActiveCableVdo2, CertStatVdo, PassiveCableVdo, ProductVdo, VpdVdo, +}; + +pub mod id_header_vdo; + +pub use id_header_vdo::IdHeaderVdo; + +/// The response VDOs to a Discover Identity Command using `SOP'`. +/// +/// See PD spec 6.4.4.3.1 Discover Identity, table 6.16 Discover Identity Command response. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ResponseVdos { + /// Information corresponding to the Product. + /// + /// To get an SOP'-specific ID Header VDO, use [`Self::id()`]. + pub id: crate::vdm::structured::command::discover_identity::IdHeaderVdo, + + /// The XID assigned by the USB-IF to the product. + pub cert_stat: CertStatVdo, + + /// Identity information relating to the product. + pub product: ProductVdo, + + /// The Product-specific VDOs. + /// + /// These are determined by [`IdHeaderVdo::product_type`] during parsing. + pub product_type_vdos: ProductTypeVdos, +} + +impl ResponseVdos { + /// Gets the SOP'-specific ID Header VDO from this response. + pub fn id(&self) -> IdHeaderVdo { + IdHeaderVdo { + usb_vendor_id: self.id.usb_vendor_id, + connector_type: self.id.connector_type, + modal_operation_supported: self.id.modal_operation_supported, + product_type: self.product_type_vdos.into(), + usb_communication_capable_as_usb_device: self.id.usb_communication_capable_as_usb_device, + usb_communication_capable_as_usb_host: self.id.usb_communication_capable_as_usb_host, + } + } +} + +/// The Product Type VDOs, parsed based on [`IdHeaderVdo::product_type`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProductTypeVdos { + /// No other Product Type is appropriate. + NotACablePlugVpd, + + /// The Product is a cable that does not incorporate signal conditioning circuits. + PassiveCable(PassiveCableVdo), + + /// The Product is a cable that incorporates signal conditioning circuits. + ActiveCable(ActiveCableVdo1, ActiveCableVdo2), + + /// The Product is a `VCONN`-powered USB device. + Vpd(VpdVdo), +} diff --git a/src/vdm/structured/command/discover_identity/ufp_vdo.rs b/src/vdm/structured/command/discover_identity/ufp_vdo.rs new file mode 100644 index 0000000..98be917 --- /dev/null +++ b/src/vdm/structured/command/discover_identity/ufp_vdo.rs @@ -0,0 +1,305 @@ +//! A UFP (Upward Facing Port) is a Port that consumes power and/or data from a +//! DFP. UFPs include traditional USB peripherals, USB Hub's upstream Port, and +//! DRD-capable host Ports. +//! +//! See PD spec 6.4.4.3.1.4 UFP VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// Returned by Ports capable of operating as a UFP, including traditional USB peripherals, +/// USB Hub's upstream Port, and DRD-capable host Ports. +/// +/// Sent based on the value of [`sop::IdHeaderVdo::product_type_ufp`][super::sop::IdHeaderVdo::product_type_ufp]. +/// +/// See PD spec 6.4.4.3.1.4 UFP VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct UfpVdo { + /// The port's highest speed capability. + pub usb_highest_speed: UsbHighestSpeed, + + /// All types of Alternate Modes, if any, the device supports. + pub alternate_modes: AlternateModes, + pub vbus_required: bool, + pub vconn_required: bool, + + /// Whether `VCONN` is needed for the AMA to operate. + pub vconn_power: VconnPower, + + /// The UFP's Capabilities when operating as either a PDUSB Device or Hub. + pub device_capability: DeviceCapability, +} + +bitfield::bitfield! { + /// The raw value of a [`UfpVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`UfpVdo::usb_highest_speed`]. + pub u8, usb_highest_speed, set_usb_highest_speed: 2, 0; + + /// See [`UfpVdo::alternate_modes`]. + pub u8, alternate_modes, set_alternate_modes: 5, 3; + + /// See [`UfpVdo::vbus_required`]. + pub bool, vbus_required_n, set_vbus_required_n: 6; + + /// See [`UfpVdo::vconn_required`]. + pub bool, vconn_required, set_vconn_required: 7; + + /// See [`UfpVdo::vconn_power`]. + pub u8, vconn_power, set_vconn_power: 10, 8; + + /// See [`UfpVdo::device_capability`]. + pub u8, device_capability, set_device_capability: 27, 24; +} + +/// Errors that can occur when parsing a [`UfpVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseUfpVdoError { + /// [`UfpVdo::usb_highest_speed`] contains an invalid value. + InvalidUsbHighestSpeed, + + /// [`UfpVdo::vconn_power`] contains an invalid value. + InvalidVconnPower, + + /// [`UfpVdo::device_capability`] contains an invalid value. + InvalidDeviceCapability, +} + +impl TryFrom for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + usb_highest_speed: raw + .usb_highest_speed() + .try_into() + .map_err(|()| ParseUfpVdoError::InvalidUsbHighestSpeed)?, + alternate_modes: raw.alternate_modes().into(), + vbus_required: !raw.vbus_required_n(), + vconn_required: raw.vconn_required(), + vconn_power: raw + .vconn_power() + .try_into() + .map_err(|()| ParseUfpVdoError::InvalidVconnPower)?, + device_capability: raw.device_capability().into(), + }) + } +} + +impl TryFrom for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +impl TryFrom<[u8; 4]> for UfpVdo { + type Error = ParseUfpVdoError; + + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +/// The port's highest speed capability. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum UsbHighestSpeed { + /// USB 2.0 only, no SuperSpeed support. + Usb2p0, + + /// USB 3.2 Gen1. + Usb3p2Gen1, + + /// USB 3.2 and USB4 Gen2. + Usb3p2Gen2, + + /// USB4 Gen3. + Usb4Gen3, + + /// USB4 Gen4. + Usb4Gen4, +} + +impl TryFrom for UsbHighestSpeed { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::Usb2p0), + 0b001 => Ok(Self::Usb3p2Gen1), + 0b010 => Ok(Self::Usb3p2Gen2), + 0b011 => Ok(Self::Usb4Gen3), + 0b100 => Ok(Self::Usb4Gen4), + _ => Err(()), + } + } +} + +/// All types of Alternate Modes, if any, the device supports. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AlternateModes { + /// Supports Thunderbolt 3 Alternate Mode. + pub tbt3: bool, + + /// Supports Alternate Modes that reconfigure the signals on the USB Type-C + /// 2.4 connector, except for Thunderbolt 3. + pub reconfigures_type_c: bool, + + /// Supports Alternate Modes that do not reconfigure the signals on the + /// USB Type-C 2.4 connector. + pub does_not_reconfigure_type_c: bool, +} + +impl From for AlternateModes { + fn from(value: u8) -> Self { + Self { + tbt3: value & 0b001 != 0, + reconfigures_type_c: value & 0b010 != 0, + does_not_reconfigure_type_c: value & 0b100 != 0, + } + } +} + +/// Whether `VCONN` is needed for the AMA to operate. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VconnPower { + /// 1W + OneW, + + /// 1.5W + OnePointFiveW, + + /// 2W + TwoW, + + /// 3W + ThreeW, + + /// 4W + FourW, + + /// 5W + FiveW, + + /// 6W + SixW, +} + +impl TryFrom for VconnPower { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b000 => Ok(Self::OneW), + 0b001 => Ok(Self::OnePointFiveW), + 0b010 => Ok(Self::TwoW), + 0b011 => Ok(Self::ThreeW), + 0b100 => Ok(Self::FourW), + 0b101 => Ok(Self::FiveW), + 0b110 => Ok(Self::SixW), + _ => Err(()), + } + } +} + +/// The UFP's Capabilities when operating as either a PDUSB Device or Hub. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DeviceCapability { + /// USB 2.0 Device Capable + pub usb2p0: bool, + + /// USB 2.0 Device Capable (Billboard only) + pub usb2p0_billboard_only: bool, + + /// USB 3.2 Device Capable + pub usb3p2: bool, + + /// USB4 Device Capable + pub usb4: bool, +} + +impl From for DeviceCapability { + fn from(value: u8) -> Self { + Self { + usb2p0: value & 0b0001 != 0, + usb2p0_billboard_only: value & 0b0010 != 0, + usb3p2: value & 0b0100 != 0, + usb4: value & 0b1000 != 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod usb_highest_speed { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, UsbHighestSpeed); 5] = [ + (0b000, UsbHighestSpeed::Usb2p0), + (0b001, UsbHighestSpeed::Usb3p2Gen1), + (0b010, UsbHighestSpeed::Usb3p2Gen2), + (0b011, UsbHighestSpeed::Usb4Gen3), + (0b100, UsbHighestSpeed::Usb4Gen4), + ]; + for (raw, expected) in cases { + assert_eq!(UsbHighestSpeed::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 5..=255u8 { + assert!(UsbHighestSpeed::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod vconn_power { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, VconnPower); 7] = [ + (0b000, VconnPower::OneW), + (0b001, VconnPower::OnePointFiveW), + (0b010, VconnPower::TwoW), + (0b011, VconnPower::ThreeW), + (0b100, VconnPower::FourW), + (0b101, VconnPower::FiveW), + (0b110, VconnPower::SixW), + ]; + for (raw, expected) in cases { + assert_eq!(VconnPower::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 7..=255u8 { + assert!(VconnPower::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/discover_identity/vpd_vdo.rs b/src/vdm/structured/command/discover_identity/vpd_vdo.rs new file mode 100644 index 0000000..36ceafd --- /dev/null +++ b/src/vdm/structured/command/discover_identity/vpd_vdo.rs @@ -0,0 +1,232 @@ +//! VCONN-Powered USB Devices (VPDs) are Ports that consume power from a DFP. +//! +//! See PD spec 6.4.4.3.1.9 VCONN Powered USB Device VDO. + +use crate::vdm::structured::command::discover_identity::ProductTypeVdo; + +/// A `VCONN`-Powered USB Device (VPD). +/// +/// Sent based on the value of [`sop_prime::IdHeaderVdo::product_type`][super::sop_prime::IdHeaderVdo::product_type]. +/// +/// See PD spec 6.4.4.3.1.9 VCONN Powered USB Device VDO, table 6.44 VPD VDO. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct VpdVdo { + /// Whether the VPD supports Charge Through. + pub charge_through_support: bool, + + /// The impedance through ground the VPD adds in series between the Source and Sink. + pub ground_impedance: GroundImpedance, + + /// The impedance through `VBUS` the VPD adds in series between the Source and Sink. + pub vbus_impedance: VbusImpedance, + + /// The level of current the VPD can pass through when Charge Through is active. + pub charge_through_current_support: ChargeThroughCurrentSupport, + + /// The maximum voltage that a Sink shall negotiate through the VPD Charge Through + /// Port as part of an Explicit Contract. + pub maximum_vbus_voltage: MaximumVbusVoltage, + + /// The FW version assigned by the VID owner. + pub fw_version: u8, + + /// The HW version assigned by the VID owner. + pub hw_version: u8, +} + +/// Errors that can occur when parsing a [`VpdVdo`] from its raw value. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseVpdVdoError { + /// [`VpdVdo::charge_through_current_support`] contains an invalid value. + InvalidChargeThroughCurrentSupport, + + /// [`VpdVdo::maximum_vbus_voltage`] contains an invalid value. + InvalidMaximumVbusVoltage, +} + +impl TryFrom for VpdVdo { + type Error = ParseVpdVdoError; + + fn try_from(raw: Raw) -> Result { + Ok(Self { + charge_through_support: raw.charge_through_support(), + ground_impedance: GroundImpedance(raw.ground_impedance()), + vbus_impedance: VbusImpedance(raw.vbus_impedance()), + charge_through_current_support: raw + .charge_through_current_support() + .try_into() + .map_err(|_| ParseVpdVdoError::InvalidChargeThroughCurrentSupport)?, + maximum_vbus_voltage: raw + .maximum_vbus_voltage() + .try_into() + .map_err(|_| ParseVpdVdoError::InvalidMaximumVbusVoltage)?, + fw_version: raw.fw_version(), + hw_version: raw.hw_version(), + }) + } +} + +impl TryFrom for VpdVdo { + type Error = ParseVpdVdoError; + + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom for VpdVdo { + type Error = ParseVpdVdoError; + + fn try_from(value: ProductTypeVdo) -> Result { + value.0.try_into() + } +} + +bitfield::bitfield! { + /// The raw value of a [`VpdVdo`], before parsing enumerations and bitfields. + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + /// See [`VpdVdo::charge_through_support`]. + pub bool, charge_through_support, set_charge_through_support: 0; + + /// See [`VpdVdo::ground_impedance`]. + pub u8, ground_impedance, set_ground_impedance: 6, 1; + + /// See [`VpdVdo::vbus_impedance`]. + pub u8, vbus_impedance, set_vbus_impedance: 12, 7; + + /// See [`VpdVdo::charge_through_current_support`]. + pub u8, charge_through_current_support, set_charge_through_current_support: 14, 14; + + /// See [`VpdVdo::maximum_vbus_voltage`]. + pub u8, maximum_vbus_voltage, set_maximum_vbus_voltage: 16, 15; + + /// See [`VpdVdo::fw_version`]. + pub u8, fw_version, set_fw_version: 27, 24; + + /// See [`VpdVdo::hw_version`]. + pub u8, hw_version, set_hw_version: 31, 28; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// Ground impedance through the VPD in 2mΩ increments. +pub struct GroundImpedance(pub u8); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// `VBUS` impedance through the VPD in 2mΩ increments. +pub struct VbusImpedance(pub u8); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ChargeThroughCurrentSupport { + /// 3A + ThreeAmps, + + /// 5A + FiveAmps, +} + +impl TryFrom for ChargeThroughCurrentSupport { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b0 => Ok(Self::ThreeAmps), + 0b1 => Ok(Self::FiveAmps), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// The maximum voltage that a Sink shall negotiate through the VPD Charge Through +/// Port as part of an Explicit Contract. +pub enum MaximumVbusVoltage { + /// 20V + TwentyVolts, + + /// 30V + ThirtyVolts, + + /// 40V + FortyVolts, + + /// 50V + FiftyVolts, +} + +impl TryFrom for MaximumVbusVoltage { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0b00 => Ok(Self::TwentyVolts), + 0b01 => Ok(Self::ThirtyVolts), + 0b10 => Ok(Self::FortyVolts), + 0b11 => Ok(Self::FiftyVolts), + _ => Err(()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod charge_through_current_support { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, ChargeThroughCurrentSupport); 2] = [ + (0, ChargeThroughCurrentSupport::ThreeAmps), + (1, ChargeThroughCurrentSupport::FiveAmps), + ]; + for (raw, expected) in cases { + assert_eq!(ChargeThroughCurrentSupport::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 2..=255u8 { + assert!( + ChargeThroughCurrentSupport::try_from(v).is_err(), + "raw={v} should be invalid" + ); + } + } + } + + mod maximum_vbus_voltage { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, MaximumVbusVoltage); 4] = [ + (0b00, MaximumVbusVoltage::TwentyVolts), + (0b01, MaximumVbusVoltage::ThirtyVolts), + (0b10, MaximumVbusVoltage::FortyVolts), + (0b11, MaximumVbusVoltage::FiftyVolts), + ]; + for (raw, expected) in cases { + assert_eq!(MaximumVbusVoltage::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + for v in 4..=255u8 { + assert!(MaximumVbusVoltage::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } +} diff --git a/src/vdm/structured/command/mod.rs b/src/vdm/structured/command/mod.rs new file mode 100644 index 0000000..f06f848 --- /dev/null +++ b/src/vdm/structured/command/mod.rs @@ -0,0 +1,5 @@ +//! The VDM Header for a Structured VDM Message defines Commands used to retrieve +//! information from the device. +//! +//! See PD spec 6.4.4.3 Use of Commands. +pub mod discover_identity; diff --git a/src/vdm/structured/header.rs b/src/vdm/structured/header.rs new file mode 100644 index 0000000..eda335b --- /dev/null +++ b/src/vdm/structured/header.rs @@ -0,0 +1,203 @@ +//! [`Header`] defines the VDM Header for a Structured VDM Message. + +use crate::vdm::structured::Svid; + +/// The VDM Header for a Structured VDM Message. +/// +/// See PD spec 6.4.4.2 Structured VDM, table 6.29 Structured VDM Header. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Header { + pub command: Command, + pub command_type: CommandType, + pub object_position: ObjectPosition, + pub structured_vdm_version: StructuredVdmVersion, + pub svid: Svid, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Command { + DiscoverIdentity, + DiscoverSvids, + DiscoverModes, + EnterMode, + ExitMode, + Attention, + + /// SVID-specific Commands as defined by the vendor in [`Header::svid`]. + SvidSpecific(u8), +} + +impl TryFrom for Command { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(Self::DiscoverIdentity), + 2 => Ok(Self::DiscoverSvids), + 3 => Ok(Self::DiscoverModes), + 4 => Ok(Self::EnterMode), + 5 => Ok(Self::ExitMode), + 6 => Ok(Self::Attention), + cmd if cmd >= 16 => Ok(Self::SvidSpecific(cmd)), + _ => Err(()), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CommandType { + Request = 0b00, + Ack = 0b01, + Nak = 0b10, + Busy = 0b11, +} + +impl From for CommandType { + fn from(value: u8) -> Self { + match value & 0b11 { + 0b00 => Self::Request, + 0b01 => Self::Ack, + 0b10 => Self::Nak, + // technically >0b11 is unreachable since we masked with 0b11, but we'll + // treat any cosmic rays as a Busy response to make this From, not TryFrom + // 0b11 | _ bothers clippy::wildcard_in_or_patterns, so we'll just use a wildcard arm + _ => Self::Busy, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ObjectPosition(pub u8); + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// Version number of the Structured VDM (not the specification). +pub struct StructuredVdmVersion(pub u8); + +bitfield::bitfield! { + #[derive(Copy, Clone, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct Raw(u32); + impl Debug; + + pub u8, command, set_command: 4, 0; + pub u8, command_type, set_command_type: 7, 6; + pub u8, object_position, set_object_position: 10, 8; + pub u8, structured_vdm_version, set_structured_vdm_version: 14, 11; + pub u16, svid, set_svid: 31, 16; +} + +/// Errors that can occur when parsing a [`Header`]. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ParseError { + InvalidCommand, +} + +impl TryFrom for Header { + type Error = ParseError; + fn try_from(raw: Raw) -> Result { + Ok(Self { + command: raw.command().try_into().map_err(|()| ParseError::InvalidCommand)?, + command_type: raw.command_type().into(), + object_position: ObjectPosition(raw.object_position()), + structured_vdm_version: StructuredVdmVersion(raw.structured_vdm_version()), + svid: Svid(raw.svid()), + }) + } +} + +impl TryFrom for Header { + type Error = ParseError; + fn try_from(value: u32) -> Result { + Raw(value).try_into() + } +} + +impl TryFrom<[u8; 4]> for Header { + type Error = ParseError; + fn try_from(bytes: [u8; 4]) -> Result { + u32::from_le_bytes(bytes).try_into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod command { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, Command); 7] = [ + (1, Command::DiscoverIdentity), + (2, Command::DiscoverSvids), + (3, Command::DiscoverModes), + (4, Command::EnterMode), + (5, Command::ExitMode), + (6, Command::Attention), + (16, Command::SvidSpecific(16)), + ]; + for (raw, expected) in cases { + assert_eq!(Command::try_from(raw), Ok(expected), "raw={raw}"); + } + } + + #[test] + fn svid_specific_upper_range() { + for v in [16u8, 17, 31, 127, 255] { + assert_eq!(Command::try_from(v), Ok(Command::SvidSpecific(v))); + } + } + + #[test] + fn invalid_values() { + // 0 and 7..=15 are invalid + assert!(Command::try_from(0u8).is_err()); + for v in 7..=15u8 { + assert!(Command::try_from(v).is_err(), "raw={v} should be invalid"); + } + } + } + + mod command_type { + use super::*; + + #[test] + fn all_valid_variants() { + let cases: [(u8, CommandType); 4] = [ + (0, CommandType::Request), + (1, CommandType::Ack), + (2, CommandType::Nak), + (3, CommandType::Busy), + ]; + for (raw, expected) in cases { + assert_eq!(CommandType::from(raw), expected, "raw={raw}"); + } + } + + #[test] + fn invalid_values() { + // since the value is masked with 0b11, all values >0b11 are technically invalid but actually unreachable + // assert that we loop in order rather than panic + let expected = [ + CommandType::Request, + CommandType::Ack, + CommandType::Nak, + CommandType::Busy, + ]; + + for (raw, expected) in (4..=255u8).zip(expected.iter().cycle().copied()) { + assert_eq!( + CommandType::from(raw), + expected, + "raw={raw} should parse as {expected:?}" + ); + } + } + } +} diff --git a/src/vdm/structured/mod.rs b/src/vdm/structured/mod.rs new file mode 100644 index 0000000..42e06a1 --- /dev/null +++ b/src/vdm/structured/mod.rs @@ -0,0 +1,10 @@ +//! Structured VDMs define the contents of bits `[0, 14]` in the VDM Header. +//! +//! See PD spec 6.4.4.2 Structured VDM. + +pub mod command; +pub mod header; +mod svid; + +pub use header::Header; +pub use svid::Svid; diff --git a/src/vdm/structured/svid.rs b/src/vdm/structured/svid.rs new file mode 100644 index 0000000..4dbc545 --- /dev/null +++ b/src/vdm/structured/svid.rs @@ -0,0 +1,18 @@ +/// The Standard or Vendor ID (SVID) field Shall contain either a 16-bit USB Standard +/// ID value (SID) or the 16-bit Vendor ID (VID) assigned to the vendor by the USB-IF. +/// +/// See PD spec 6.4.4.2.1 SVID. +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Svid(pub u16); + +impl Svid { + /// The Standard ID allocated to the PD specification by USB-IF. + pub const PD: Self = Self(0xFF00); + + /// The Standard ID allocated to DisplayPort Type-C (DPTC) by USB-IF. + pub const DISPLAY_PORT_TYPE_C: Self = Self(0xFF01); + + /// The Vendor ID assigned to Thunderbolt by USB-IF. + pub const THUNDERBOLT: Self = Self(0x8087); +}