From b5c5e6509602253f41079f45878e818e70bfe824 Mon Sep 17 00:00:00 2001 From: Kurtis Dinelle Date: Tue, 29 Sep 2026 12:12:36 -0700 Subject: [PATCH] ec: Add PCC test commands --- ec/test-cli/README.md | 29 +++- ec/test-cli/src/cli.rs | 15 +++ ec/test-cli/src/commands/mod.rs | 2 + ec/test-cli/src/commands/pcc.rs | 81 ++++++++++++ ec/test-cli/src/main.rs | 10 ++ ec/test-lib/src/lib.rs | 3 + ec/test-lib/src/pcc.rs | 225 ++++++++++++++++++++++++++++++++ ec/test-lib/src/windows.rs | 6 +- 8 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 ec/test-cli/src/commands/pcc.rs create mode 100644 ec/test-lib/src/pcc.rs diff --git a/ec/test-cli/README.md b/ec/test-cli/README.md index 999c358..4183e0a 100644 --- a/ec/test-cli/README.md +++ b/ec/test-cli/README.md @@ -1,7 +1,7 @@ # ec-test-cli ## Overview -Command-line tool for testing EC features (thermal, battery, RTC). Each command maps directly to an EC data source trait method — it executes the request, prints the result, and exits. +Command-line tool for testing EC features (thermal, battery, RTC) and native PCC connectivity. Commands execute the request, print the result, and exit. See [ODP Documentation](https://opendevicepartnership.github.io/documentation/guide/overview.html) for details on EC specification. @@ -37,6 +37,33 @@ Use `ec-test-cli --help` and `ec-test-cli --help` to see available com Setter commands print nothing on success — exit code 0 indicates success. +## Native PCC (Windows ARM64) + +Use `--source acpi` with the native-PCC-enabled `ectest` driver and the QEMU +platform's Type 3 subspace 0. The driver's native interface is experimental +and currently gated to Windows ARM64 10.0.28000.2605 and the matching PCCT. + +```text +ec-test-cli --source acpi pcc probe +ec-test-cli --source acpi pcc ping --sequence 42 +``` + +`probe` prints the native interface metadata and query status without sending +an EC command. A successful probe does not prove that the EC can respond. + +`ping` requires dev-qemu's PCC responder running over the host/EC eSPI link. +It sends command `1` with an eight-byte `PING` marker and little-endian `u32` +sequence, then checks the returned status, `PONG` marker, and matching sequence. +Omit `--sequence` to generate a value. Successful output includes: + +```text +PCC execute: status=0x00000000 +PCC response: PONG sequence=42 +``` + +Failed queries, executions, and malformed or mismatched replies exit nonzero. +The `pcc` commands are Windows-only and do not use the serial or mock sources. + ## Raw ACPI evaluation (Windows only) The `eval` command evaluates an arbitrary ACPI method by name and prints its return diff --git a/ec/test-cli/src/cli.rs b/ec/test-cli/src/cli.rs index b295aa8..ac1557c 100644 --- a/ec/test-cli/src/cli.rs +++ b/ec/test-cli/src/cli.rs @@ -88,6 +88,21 @@ pub enum Command { /// Evaluate a raw ACPI method (Windows `--source acpi` only). #[cfg(target_os = "windows")] Eval(EvalCommand), + /// Query or exercise native PCC through ectest (Windows `--source acpi` only). + #[cfg(target_os = "windows")] + #[command(subcommand)] + Pcc(PccCommand), +} + +#[cfg(target_os = "windows")] +#[derive(Subcommand)] +pub enum PccCommand { + Probe, + Ping { + /// Sequence to echo; generated when omitted. + #[arg(long)] + sequence: Option, + }, } /// Raw ACPI method evaluation arguments. diff --git a/ec/test-cli/src/commands/mod.rs b/ec/test-cli/src/commands/mod.rs index 057879d..63a8362 100644 --- a/ec/test-cli/src/commands/mod.rs +++ b/ec/test-cli/src/commands/mod.rs @@ -6,6 +6,8 @@ pub mod battery; #[cfg(target_os = "windows")] pub mod eval; +#[cfg(any(test, target_os = "windows"))] +pub mod pcc; pub mod rtc; pub mod script; pub mod thermal; diff --git a/ec/test-cli/src/commands/pcc.rs b/ec/test-cli/src/commands/pcc.rs new file mode 100644 index 0000000..fe7e492 --- /dev/null +++ b/ec/test-cli/src/commands/pcc.rs @@ -0,0 +1,81 @@ +#[cfg(target_os = "windows")] +use crate::cli::PccCommand; +#[cfg(target_os = "windows")] +use ec_test_lib::pcc::Pcc; + +fn ping_request(sequence: u32) -> [u8; 8] { + let mut request = *b"PING\0\0\0\0"; + request[4..].copy_from_slice(&sequence.to_le_bytes()); + request +} + +fn pong_sequence(message: &[u8], expected: u32) -> Result { + if message.len() < 8 || &message[..4] != b"PONG" { + return Err("PCC reply is not PONG"); + } + let sequence = u32::from_le_bytes([message[4], message[5], message[6], message[7]]); + if sequence != expected { + return Err("PCC PONG sequence mismatch"); + } + Ok(sequence) +} + +#[cfg(target_os = "windows")] +pub fn run(command: &PccCommand) -> Result<(), Box> { + let pcc = Pcc::new()?; + match command { + PccCommand::Probe => { + let response = pcc.probe()?; + println!( + "PCC probe: status=0x{:08x} interface={} subspace={} type={} size={} flags=0x{:x} latency_us={} max_rate={} acpi_timestamp=0x{:08x} acpi_image_size=0x{:x}", + response.query_status as u32, + response.interface_version, + response.subspace_id, + response.subspace_type, + response.subspace_size, + response.flags, + response.nominal_latency, + response.maximum_periodic_rate, + response.acpi_timestamp, + response.acpi_image_size + ); + if response.query_status < 0 { + return Err(format!("PCC probe failed: NTSTATUS 0x{:08x}", response.query_status as u32).into()); + } + } + PccCommand::Ping { sequence } => { + let sequence = match *sequence { + Some(sequence) => sequence, + None => std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .subsec_nanos(), + }; + let response = pcc.execute(1, &ping_request(sequence))?; + println!("PCC execute: status=0x{:08x}", response.status as u32); + if response.status < 0 { + return Err(format!("PCC execute failed: NTSTATUS 0x{:08x}", response.status as u32).into()); + } + let reply_sequence = pong_sequence(&response.message, sequence)?; + println!("PCC response: PONG sequence={reply_sequence}"); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ping_contains_marker_and_little_endian_sequence() { + assert_eq!(ping_request(0x1234_5678), *b"PING\x78\x56\x34\x12"); + } + + #[test] + fn pong_requires_matching_marker_and_sequence() { + assert_eq!(pong_sequence(b"PONG\x78\x56\x34\x12", 0x1234_5678), Ok(0x1234_5678)); + assert!(pong_sequence(b"PING\x78\x56\x34\x12", 0x1234_5678).is_err()); + assert!(pong_sequence(b"PONG\x78\x56\x34\x12", 0x1234_5679).is_err()); + assert!(pong_sequence(b"PONG", 0x1234_5678).is_err()); + } +} diff --git a/ec/test-cli/src/main.rs b/ec/test-cli/src/main.rs index 1766a1d..5f83f71 100644 --- a/ec/test-cli/src/main.rs +++ b/ec/test-cli/src/main.rs @@ -20,6 +20,8 @@ fn dispatch(source: S, command: Command) -> Result<(), Box unreachable!("eval is dispatched before reaching a generic source"), + #[cfg(target_os = "windows")] + Command::Pcc(_) => unreachable!("pcc is dispatched before reaching a generic source"), } } @@ -35,6 +37,14 @@ fn main() -> Result<(), Box> { }; } + #[cfg(target_os = "windows")] + if let Command::Pcc(cmd) = &cli.command { + return match cli.source { + SourceKind::Acpi => commands::pcc::run(cmd), + _ => Err("`pcc` is only supported with `--source acpi`".into()), + }; + } + match cli.source { SourceKind::Mock => dispatch(ec_test_lib::mock::Mock::default(), cli.command), diff --git a/ec/test-lib/src/lib.rs b/ec/test-lib/src/lib.rs index 46f471b..6f3da05 100644 --- a/ec/test-lib/src/lib.rs +++ b/ec/test-lib/src/lib.rs @@ -15,6 +15,9 @@ pub mod acpi; #[cfg(target_os = "windows")] pub mod windows; +#[cfg(target_os = "windows")] +pub mod pcc; + pub mod mock; pub mod serial; pub mod ucsi; diff --git a/ec/test-lib/src/pcc.rs b/ec/test-lib/src/pcc.rs new file mode 100644 index 0000000..32e5ccb --- /dev/null +++ b/ec/test-lib/src/pcc.rs @@ -0,0 +1,225 @@ +//! Experimental, build-specific native PCC interface client. + +const PROTOCOL_VERSION: u32 = 1; +const RESPONSE_SIZE: usize = 48; +/// Largest message exchanged through the native PCC bridge. +pub const MESSAGE_SIZE: usize = 256; +const EXECUTE_REQUEST_SIZE: usize = 16 + MESSAGE_SIZE; +const EXECUTE_RESPONSE_SIZE: usize = 8 + MESSAGE_SIZE; + +/// Encode an execute request for PCC subspace 0. +pub fn encode_execute(command: u8, message: &[u8]) -> Result<[u8; EXECUTE_REQUEST_SIZE], &'static str> { + if message.len() > MESSAGE_SIZE { + return Err("PCC message exceeds the bridge capacity"); + } + let mut bytes = [0u8; EXECUTE_REQUEST_SIZE]; + bytes[0..4].copy_from_slice(&PROTOCOL_VERSION.to_le_bytes()); + bytes[8..12].copy_from_slice(&u32::from(command).to_le_bytes()); + bytes[12..16].copy_from_slice(&(message.len() as u32).to_le_bytes()); + bytes[16..16 + message.len()].copy_from_slice(message); + Ok(bytes) +} + +/// Result of one native PCC transaction. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecuteResponse { + /// NTSTATUS from validation, acquisition, execution or release. + pub status: i32, + /// Communication area contents after completion; valid only on success. + pub message: [u8; MESSAGE_SIZE], +} + +impl TryFrom<&[u8]> for ExecuteResponse { + type Error = &'static str; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != EXECUTE_RESPONSE_SIZE { + return Err("invalid PCC execute response size"); + } + let word = |offset: usize| { + u32::from_le_bytes([bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]]) + }; + if word(0) != PROTOCOL_VERSION { + return Err("invalid PCC execute protocol version"); + } + let mut message = [0u8; MESSAGE_SIZE]; + message.copy_from_slice(&bytes[8..]); + Ok(Self { + status: word(4) as i32, + message, + }) + } +} + +/// Metadata returned by the opt-in native PCC bridge probe. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProbeResponse { + /// NTSTATUS returned by build validation or the native interface query. + pub query_status: i32, + /// Native PCC interface version, not the IOCTL protocol version. + pub interface_version: u32, + /// Selected PCCT subspace index. + pub subspace_id: u32, + /// Selected PCCT subspace type. + pub subspace_type: u32, + /// Size of the returned communication area, excluding the Type 3 header. + pub subspace_size: u32, + /// Native interface capability flags. + pub flags: u32, + /// Nominal command latency in microseconds. + pub nominal_latency: u32, + /// Maximum periodic command rate returned by the native engine. + pub maximum_periodic_rate: u32, + /// Loaded ACPI image timestamp used by the experimental build gate. + pub acpi_timestamp: u32, + /// Loaded ACPI image size used by the experimental build gate. + pub acpi_image_size: u32, +} + +impl TryFrom<&[u8]> for ProbeResponse { + type Error = &'static str; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != RESPONSE_SIZE { + return Err("invalid PCC probe response size"); + } + let mut words = [0u32; RESPONSE_SIZE / 4]; + let (fields, _) = bytes.as_chunks::<4>(); + for (word, field) in words.iter_mut().zip(fields) { + *word = u32::from_le_bytes(*field); + } + if words[0] != PROTOCOL_VERSION || words[11] != 0 { + return Err("invalid PCC probe protocol version or reserved field"); + } + let response = Self { + query_status: words[1] as i32, + interface_version: words[2], + subspace_id: words[3], + subspace_type: words[4], + subspace_size: words[5], + flags: words[6], + nominal_latency: words[7], + maximum_periodic_rate: words[8], + acpi_timestamp: words[9], + acpi_image_size: words[10], + }; + if response.query_status >= 0 + && (response.query_status != 0 + || response.interface_version != 1 + || response.subspace_id != 0 + || response.subspace_type != 3 + || response.subspace_size != 4080 + || response.flags & !1 != 0) + { + return Err("unexpected native PCC interface metadata"); + } + Ok(response) + } +} + +/// Client of the existing test bridge's experimental native PCC IOCTLs. +pub struct Pcc { + device: crate::windows::WindowsDevice, +} + +impl Pcc { + /// Open the test bridge's device interface. + pub fn new() -> Result { + let interface = windows::core::GUID::from_u128(0xcdc35b6e_0be4_4936_bf5f_5537380a7c1a); + Ok(Self { + device: crate::windows::WindowsDevice::new(&interface)?, + }) + } + + /// Query native PCC subspace 0 without reading or writing the mailbox. + pub fn probe(&self) -> Result { + const IOCTL_ECTEST_PCC_PROBE: u32 = 0x0022_E000; + let mut input = [0u8; 8]; + input[..4].copy_from_slice(&PROTOCOL_VERSION.to_le_bytes()); + let mut output = [0u8; RESPONSE_SIZE]; + self.device.ioctl(IOCTL_ECTEST_PCC_PROBE, &input, &mut output)?; + ProbeResponse::try_from(output.as_slice()).map_err(|_| crate::windows::Error::InvalidData) + } + + /// Send one command through Windows' native PCC transport on subspace 0. + pub fn execute(&self, command: u8, message: &[u8]) -> Result { + const IOCTL_ECTEST_PCC_EXECUTE: u32 = 0x0022_E004; + let input = encode_execute(command, message).map_err(|_| crate::windows::Error::InvalidData)?; + let mut output = [0u8; EXECUTE_RESPONSE_SIZE]; + self.device.ioctl(IOCTL_ECTEST_PCC_EXECUTE, &input, &mut output)?; + ExecuteResponse::try_from(output.as_slice()).map_err(|_| crate::windows::Error::InvalidData) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn valid_response() -> Vec { + [1u32, 0, 1, 0, 3, 4080, 1, 100_000, 0, 0xF61F_B868, 0xE1000, 0] + .into_iter() + .flat_map(u32::to_le_bytes) + .collect() + } + + #[test] + fn decodes_native_interface_metadata() { + let bytes = valid_response(); + let response = ProbeResponse::try_from(bytes.as_slice()).unwrap(); + assert_eq!(response.query_status, 0); + assert_eq!(response.interface_version, 1); + assert_eq!(response.subspace_id, 0); + assert_eq!(response.subspace_type, 3); + assert_eq!(response.subspace_size, 4080); + assert_eq!(response.acpi_timestamp, 0xF61F_B868); + } + + #[test] + fn preserves_native_query_failure() { + let mut bytes = [0u8; RESPONSE_SIZE]; + bytes[..4].copy_from_slice(&PROTOCOL_VERSION.to_le_bytes()); + bytes[4..8].copy_from_slice(&0xC000_00BBu32.to_le_bytes()); + let response = ProbeResponse::try_from(bytes.as_slice()).unwrap(); + assert_eq!(response.query_status as u32, 0xC000_00BB); + } + + #[test] + fn rejects_malformed_probe_responses() { + let bytes = valid_response(); + for length in 0..bytes.len() { + assert!(ProbeResponse::try_from(&bytes[..length]).is_err()); + } + for offset in [0, 4, 8, 12, 16, 20, 24, 44] { + let mut invalid = bytes.clone(); + invalid[offset..offset + 4].copy_from_slice(&2u32.to_le_bytes()); + assert!(ProbeResponse::try_from(invalid.as_slice()).is_err()); + } + let mut oversized = bytes; + oversized.push(0); + assert!(ProbeResponse::try_from(oversized.as_slice()).is_err()); + } + + #[test] + fn encodes_execute_request_layout() { + let bytes = encode_execute(1, b"abc").unwrap(); + assert_eq!(&bytes[..16], &[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0]); + assert_eq!(&bytes[16..19], b"abc"); + assert!(bytes[19..].iter().all(|byte| *byte == 0)); + assert!(encode_execute(1, &[0; MESSAGE_SIZE]).is_ok()); + assert!(encode_execute(1, &[0; MESSAGE_SIZE + 1]).is_err()); + } + + #[test] + fn decodes_execute_response_and_rejects_malformed_buffers() { + let mut bytes = vec![0u8; EXECUTE_RESPONSE_SIZE]; + bytes[0..4].copy_from_slice(&PROTOCOL_VERSION.to_le_bytes()); + bytes[4..8].copy_from_slice(&0xC000_00B5u32.to_le_bytes()); + bytes[8] = 0x5A; + let response = ExecuteResponse::try_from(bytes.as_slice()).unwrap(); + assert_eq!(response.status as u32, 0xC000_00B5); + assert_eq!(response.message[0], 0x5A); + assert!(ExecuteResponse::try_from(&bytes[..EXECUTE_RESPONSE_SIZE - 1]).is_err()); + bytes[0] = 2; + assert!(ExecuteResponse::try_from(bytes.as_slice()).is_err()); + } +} diff --git a/ec/test-lib/src/windows.rs b/ec/test-lib/src/windows.rs index 86cd7c1..84df919 100644 --- a/ec/test-lib/src/windows.rs +++ b/ec/test-lib/src/windows.rs @@ -119,20 +119,20 @@ impl From for Error { } /// A resolved handle to one Windows class-driver device interface. -struct WindowsDevice { +pub(crate) struct WindowsDevice { device_path: String, } impl WindowsDevice { /// Create a device from given `interface_guid`. - fn new(interface_guid: &GUID) -> Result { + pub(crate) fn new(interface_guid: &GUID) -> Result { Ok(Self { device_path: get_device_path(interface_guid)?, }) } /// Issue a single buffered IOCTL, requiring the driver to fill `output` completely. - fn ioctl(&self, code: u32, input: &[u8], output: &mut [u8]) -> Result<(), Error> { + pub(crate) fn ioctl(&self, code: u32, input: &[u8], output: &mut [u8]) -> Result<(), Error> { let wide: Vec = self.device_path.encode_utf16().chain(std::iter::once(0)).collect(); let handle = unsafe { CreateFileW(