c0587f9a2d
The 556MB monolithic redox.patch was impossible to manage, unreviewable, blocked GitHub pushes, and could only grow. This commit: - Moves all 64 absorbed patches from absorbed/ to active use in base/ - Removes the absorbed/ directory (consolidation history is now PATCH-HISTORY.md) - Removes the redox.patch symlink from recipes/core/base/ - Fixes all recipe symlinks to point to active patches (not absorbed/) - Patches are now individually wired, reviewable, and independently rebasable The redox.patch mega-file is no longer needed — individual patches are applied directly from the recipe.toml patches list.
5926 lines
191 KiB
Diff
5926 lines
191 KiB
Diff
diff --git a/Cargo.toml b/Cargo.toml
|
|
index 9e776232..380f8d85 100644
|
|
--- a/Cargo.toml
|
|
+++ b/Cargo.toml
|
|
@@ -67,6 +67,22 @@ members = [
|
|
"drivers/usb/xhcid",
|
|
"drivers/usb/usbctl",
|
|
"drivers/usb/usbhubd",
|
|
+ "drivers/usb/ucsid",
|
|
+
|
|
+ "drivers/i2c/i2c-interface",
|
|
+ "drivers/i2c/i2cd",
|
|
+ "drivers/i2c/amd-mp2-i2cd",
|
|
+ "drivers/i2c/dw-acpi-i2cd",
|
|
+ "drivers/i2c/intel-lpss-i2cd",
|
|
+
|
|
+ "drivers/gpio/gpiod",
|
|
+ "drivers/gpio/intel-gpiod",
|
|
+ "drivers/gpio/i2c-gpio-expanderd",
|
|
+
|
|
+ "drivers/input/i2c-hidd",
|
|
+ "drivers/input/intel-thc-hidd",
|
|
+
|
|
+ "drivers/acpi-resource",
|
|
]
|
|
|
|
# Bootstrap needs it's own profile configuration
|
|
diff --git a/drivers/acpi-resource/Cargo.toml b/drivers/acpi-resource/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..f30c6d02
|
|
--- /dev/null
|
|
+++ b/drivers/acpi-resource/Cargo.toml
|
|
@@ -0,0 +1,13 @@
|
|
+[package]
|
|
+name = "acpi-resource"
|
|
+description = "Shared ACPI resource template decoder"
|
|
+version = "0.0.1"
|
|
+authors = ["Red Bear OS"]
|
|
+repository = "https://gitlab.redox-os.org/redox-os/drivers"
|
|
+categories = ["hardware-support"]
|
|
+license = "MIT/Apache-2.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+serde.workspace = true
|
|
+thiserror.workspace = true
|
|
diff --git a/drivers/acpi-resource/src/lib.rs b/drivers/acpi-resource/src/lib.rs
|
|
new file mode 100644
|
|
index 00000000..57ae4b4b
|
|
--- /dev/null
|
|
+++ b/drivers/acpi-resource/src/lib.rs
|
|
@@ -0,0 +1,688 @@
|
|
+use serde::{Deserialize, Serialize};
|
|
+use thiserror::Error;
|
|
+
|
|
+const SMALL_IRQ: u8 = 0x20;
|
|
+const SMALL_END_TAG: u8 = 0x78;
|
|
+
|
|
+const LARGE_MEMORY32: u8 = 0x85;
|
|
+const LARGE_FIXED_MEMORY32: u8 = 0x86;
|
|
+const LARGE_ADDRESS32: u8 = 0x87;
|
|
+const LARGE_EXTENDED_IRQ: u8 = 0x89;
|
|
+const LARGE_ADDRESS64: u8 = 0x8A;
|
|
+const LARGE_GPIO: u8 = 0x8C;
|
|
+const LARGE_SERIAL_BUS: u8 = 0x8E;
|
|
+
|
|
+const SERIAL_BUS_I2C: u8 = 1;
|
|
+const I2C_TYPE_DATA_LEN: usize = 6;
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum InterruptTrigger {
|
|
+ Edge,
|
|
+ Level,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum InterruptPolarity {
|
|
+ ActiveHigh,
|
|
+ ActiveLow,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum AddressResourceType {
|
|
+ MemoryRange,
|
|
+ IoRange,
|
|
+ BusNumberRange,
|
|
+ Unknown(u8),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct ResourceSource {
|
|
+ pub index: u8,
|
|
+ pub source: String,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct IrqDescriptor {
|
|
+ pub interrupts: Vec<u8>,
|
|
+ pub triggering: InterruptTrigger,
|
|
+ pub polarity: InterruptPolarity,
|
|
+ pub shareable: bool,
|
|
+ pub wake_capable: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct ExtendedIrqDescriptor {
|
|
+ pub producer_consumer: bool,
|
|
+ pub interrupts: Vec<u32>,
|
|
+ pub triggering: InterruptTrigger,
|
|
+ pub polarity: InterruptPolarity,
|
|
+ pub shareable: bool,
|
|
+ pub wake_capable: bool,
|
|
+ pub resource_source: Option<ResourceSource>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct GpioDescriptor {
|
|
+ pub revision_id: u8,
|
|
+ pub producer_consumer: bool,
|
|
+ pub pin_config: u8,
|
|
+ pub shareable: bool,
|
|
+ pub wake_capable: bool,
|
|
+ pub io_restriction: u8,
|
|
+ pub triggering: InterruptTrigger,
|
|
+ pub polarity: InterruptPolarity,
|
|
+ pub drive_strength: u16,
|
|
+ pub debounce_timeout: u16,
|
|
+ pub pins: Vec<u16>,
|
|
+ pub resource_source: Option<ResourceSource>,
|
|
+ pub vendor_data: Vec<u8>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct I2cSerialBusDescriptor {
|
|
+ pub revision_id: u8,
|
|
+ pub producer_consumer: bool,
|
|
+ pub slave_mode: bool,
|
|
+ pub connection_sharing: bool,
|
|
+ pub type_revision_id: u8,
|
|
+ pub access_mode_10bit: bool,
|
|
+ pub slave_address: u16,
|
|
+ pub connection_speed: u32,
|
|
+ pub resource_source: Option<ResourceSource>,
|
|
+ pub vendor_data: Vec<u8>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct Memory32RangeDescriptor {
|
|
+ pub write_protect: bool,
|
|
+ pub minimum: u32,
|
|
+ pub maximum: u32,
|
|
+ pub alignment: u32,
|
|
+ pub address_length: u32,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct FixedMemory32Descriptor {
|
|
+ pub write_protect: bool,
|
|
+ pub address: u32,
|
|
+ pub address_length: u32,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct Address32Descriptor {
|
|
+ pub resource_type: AddressResourceType,
|
|
+ pub producer_consumer: bool,
|
|
+ pub decode: bool,
|
|
+ pub min_address_fixed: bool,
|
|
+ pub max_address_fixed: bool,
|
|
+ pub specific_flags: u8,
|
|
+ pub granularity: u32,
|
|
+ pub minimum: u32,
|
|
+ pub maximum: u32,
|
|
+ pub translation_offset: u32,
|
|
+ pub address_length: u32,
|
|
+ pub resource_source: Option<ResourceSource>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct Address64Descriptor {
|
|
+ pub resource_type: AddressResourceType,
|
|
+ pub producer_consumer: bool,
|
|
+ pub decode: bool,
|
|
+ pub min_address_fixed: bool,
|
|
+ pub max_address_fixed: bool,
|
|
+ pub specific_flags: u8,
|
|
+ pub granularity: u64,
|
|
+ pub minimum: u64,
|
|
+ pub maximum: u64,
|
|
+ pub translation_offset: u64,
|
|
+ pub address_length: u64,
|
|
+ pub resource_source: Option<ResourceSource>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum ResourceDescriptor {
|
|
+ Irq(IrqDescriptor),
|
|
+ ExtendedIrq(ExtendedIrqDescriptor),
|
|
+ GpioInt(GpioDescriptor),
|
|
+ GpioIo(GpioDescriptor),
|
|
+ I2cSerialBus(I2cSerialBusDescriptor),
|
|
+ Memory32Range(Memory32RangeDescriptor),
|
|
+ FixedMemory32(FixedMemory32Descriptor),
|
|
+ Address32(Address32Descriptor),
|
|
+ Address64(Address64Descriptor),
|
|
+}
|
|
+
|
|
+#[derive(Debug, Error, PartialEq, Eq)]
|
|
+pub enum ResourceDecodeError {
|
|
+ #[error("descriptor at offset {offset} overruns the resource template")]
|
|
+ TruncatedDescriptor { offset: usize },
|
|
+
|
|
+ #[error("unsupported small descriptor length {length} for tag {tag:#04x} at offset {offset}")]
|
|
+ InvalidSmallLength {
|
|
+ offset: usize,
|
|
+ tag: u8,
|
|
+ length: usize,
|
|
+ },
|
|
+
|
|
+ #[error("descriptor {descriptor} at offset {offset} is shorter than {minimum} bytes")]
|
|
+ InvalidLargeLength {
|
|
+ offset: usize,
|
|
+ descriptor: &'static str,
|
|
+ minimum: usize,
|
|
+ },
|
|
+
|
|
+ #[error("descriptor {descriptor} at offset {offset} has an invalid internal offset")]
|
|
+ InvalidInternalOffset {
|
|
+ offset: usize,
|
|
+ descriptor: &'static str,
|
|
+ },
|
|
+}
|
|
+
|
|
+pub fn decode_resource_template(
|
|
+ bytes: &[u8],
|
|
+) -> Result<Vec<ResourceDescriptor>, ResourceDecodeError> {
|
|
+ let mut resources = Vec::new();
|
|
+ let mut offset = 0usize;
|
|
+
|
|
+ while offset < bytes.len() {
|
|
+ let descriptor = *bytes
|
|
+ .get(offset)
|
|
+ .ok_or(ResourceDecodeError::TruncatedDescriptor { offset })?;
|
|
+
|
|
+ if descriptor & 0x80 == 0 {
|
|
+ let length = usize::from(descriptor & 0x07);
|
|
+ let end = offset + 1 + length;
|
|
+ let desc = bytes
|
|
+ .get(offset..end)
|
|
+ .ok_or(ResourceDecodeError::TruncatedDescriptor { offset })?;
|
|
+ let body = &desc[1..];
|
|
+
|
|
+ match descriptor & 0x78 {
|
|
+ SMALL_IRQ => resources.push(ResourceDescriptor::Irq(parse_irq(body, offset)?)),
|
|
+ SMALL_END_TAG => break,
|
|
+ _ => {}
|
|
+ }
|
|
+
|
|
+ offset = end;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let length = usize::from(read_u16(bytes, offset + 1)?);
|
|
+ let end = offset + 3 + length;
|
|
+ let desc = bytes
|
|
+ .get(offset..end)
|
|
+ .ok_or(ResourceDecodeError::TruncatedDescriptor { offset })?;
|
|
+ let body = &desc[3..];
|
|
+
|
|
+ match descriptor {
|
|
+ LARGE_MEMORY32 => resources.push(ResourceDescriptor::Memory32Range(parse_memory32(
|
|
+ body, offset,
|
|
+ )?)),
|
|
+ LARGE_FIXED_MEMORY32 => resources.push(ResourceDescriptor::FixedMemory32(
|
|
+ parse_fixed_memory32(body, offset)?,
|
|
+ )),
|
|
+ LARGE_ADDRESS32 => {
|
|
+ resources.push(ResourceDescriptor::Address32(parse_address32(
|
|
+ desc, body, offset,
|
|
+ )?));
|
|
+ }
|
|
+ LARGE_ADDRESS64 => {
|
|
+ resources.push(ResourceDescriptor::Address64(parse_address64(
|
|
+ desc, body, offset,
|
|
+ )?));
|
|
+ }
|
|
+ LARGE_EXTENDED_IRQ => resources.push(ResourceDescriptor::ExtendedIrq(
|
|
+ parse_extended_irq(desc, body, offset)?,
|
|
+ )),
|
|
+ LARGE_GPIO => {
|
|
+ let (is_interrupt, descriptor) = parse_gpio(desc, body, offset)?;
|
|
+ resources.push(if is_interrupt {
|
|
+ ResourceDescriptor::GpioInt(descriptor)
|
|
+ } else {
|
|
+ ResourceDescriptor::GpioIo(descriptor)
|
|
+ });
|
|
+ }
|
|
+ LARGE_SERIAL_BUS => {
|
|
+ if let Some(descriptor) = parse_i2c_serial_bus(desc, body, offset)? {
|
|
+ resources.push(ResourceDescriptor::I2cSerialBus(descriptor));
|
|
+ }
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+
|
|
+ offset = end;
|
|
+ }
|
|
+
|
|
+ Ok(resources)
|
|
+}
|
|
+
|
|
+fn parse_irq(body: &[u8], offset: usize) -> Result<IrqDescriptor, ResourceDecodeError> {
|
|
+ if body.len() != 2 && body.len() != 3 {
|
|
+ return Err(ResourceDecodeError::InvalidSmallLength {
|
|
+ offset,
|
|
+ tag: SMALL_IRQ,
|
|
+ length: body.len(),
|
|
+ });
|
|
+ }
|
|
+
|
|
+ let mask = u16::from_le_bytes([body[0], body[1]]);
|
|
+ let flags = body.get(2).copied().unwrap_or(0);
|
|
+ let interrupts = (0..16)
|
|
+ .filter(|irq| mask & (1 << irq) != 0)
|
|
+ .map(|irq| irq as u8)
|
|
+ .collect();
|
|
+
|
|
+ Ok(IrqDescriptor {
|
|
+ interrupts,
|
|
+ triggering: if flags & 0x01 != 0 {
|
|
+ InterruptTrigger::Level
|
|
+ } else {
|
|
+ InterruptTrigger::Edge
|
|
+ },
|
|
+ polarity: if flags & 0x08 != 0 {
|
|
+ InterruptPolarity::ActiveLow
|
|
+ } else {
|
|
+ InterruptPolarity::ActiveHigh
|
|
+ },
|
|
+ shareable: flags & 0x10 != 0,
|
|
+ wake_capable: flags & 0x20 != 0,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn parse_extended_irq(
|
|
+ desc: &[u8],
|
|
+ body: &[u8],
|
|
+ offset: usize,
|
|
+) -> Result<ExtendedIrqDescriptor, ResourceDecodeError> {
|
|
+ ensure_length(body, 2, offset, "ExtendedIrq")?;
|
|
+
|
|
+ let flags = body[0];
|
|
+ let count = usize::from(body[1]);
|
|
+ let ints_len = count * 4;
|
|
+ ensure_length(body, 2 + ints_len, offset, "ExtendedIrq")?;
|
|
+
|
|
+ let interrupts = (0..count)
|
|
+ .map(|index| read_u32(body, 2 + index * 4))
|
|
+ .collect::<Result<Vec<_>, _>>()?;
|
|
+ let resource_source = if body.len() > 2 + ints_len {
|
|
+ Some(parse_source_inline(&body[2 + ints_len..]))
|
|
+ } else {
|
|
+ None
|
|
+ };
|
|
+
|
|
+ let _ = desc;
|
|
+
|
|
+ Ok(ExtendedIrqDescriptor {
|
|
+ producer_consumer: flags & 0x01 != 0,
|
|
+ triggering: if flags & 0x02 != 0 {
|
|
+ InterruptTrigger::Level
|
|
+ } else {
|
|
+ InterruptTrigger::Edge
|
|
+ },
|
|
+ polarity: if flags & 0x04 != 0 {
|
|
+ InterruptPolarity::ActiveLow
|
|
+ } else {
|
|
+ InterruptPolarity::ActiveHigh
|
|
+ },
|
|
+ shareable: flags & 0x08 != 0,
|
|
+ wake_capable: flags & 0x10 != 0,
|
|
+ interrupts,
|
|
+ resource_source,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn parse_gpio(
|
|
+ desc: &[u8],
|
|
+ body: &[u8],
|
|
+ offset: usize,
|
|
+) -> Result<(bool, GpioDescriptor), ResourceDecodeError> {
|
|
+ ensure_length(body, 20, offset, "Gpio")?;
|
|
+
|
|
+ let connection_type = body[1];
|
|
+ let flags = read_u16(body, 2)?;
|
|
+ let int_flags = read_u16(body, 4)?;
|
|
+ let pin_table_offset = usize::from(read_u16(body, 11)?);
|
|
+ let resource_source_index = body[13];
|
|
+ let resource_source_offset = usize::from(read_u16(body, 14)?);
|
|
+ let vendor_offset = usize::from(read_u16(body, 16)?);
|
|
+ let vendor_length = usize::from(read_u16(body, 18)?);
|
|
+
|
|
+ let pins_end = min_nonzero([resource_source_offset, vendor_offset, desc.len()]);
|
|
+ let pins = parse_u16_list(desc, pin_table_offset, pins_end, offset, "Gpio")?;
|
|
+ let resource_source = parse_source_absolute(
|
|
+ desc,
|
|
+ resource_source_offset,
|
|
+ min_nonzero([vendor_offset, desc.len()]),
|
|
+ resource_source_index,
|
|
+ offset,
|
|
+ "Gpio",
|
|
+ )?;
|
|
+ let vendor_data = parse_blob_absolute(desc, vendor_offset, vendor_length, offset, "Gpio")?;
|
|
+
|
|
+ Ok((
|
|
+ connection_type == 0,
|
|
+ GpioDescriptor {
|
|
+ revision_id: body[0],
|
|
+ producer_consumer: flags & 0x0001 != 0,
|
|
+ pin_config: body[6],
|
|
+ shareable: int_flags & 0x0008 != 0,
|
|
+ wake_capable: int_flags & 0x0010 != 0,
|
|
+ io_restriction: (int_flags & 0x0003) as u8,
|
|
+ triggering: if int_flags & 0x0001 != 0 {
|
|
+ InterruptTrigger::Level
|
|
+ } else {
|
|
+ InterruptTrigger::Edge
|
|
+ },
|
|
+ polarity: if int_flags & 0x0002 != 0 {
|
|
+ InterruptPolarity::ActiveLow
|
|
+ } else {
|
|
+ InterruptPolarity::ActiveHigh
|
|
+ },
|
|
+ drive_strength: read_u16(body, 7)?,
|
|
+ debounce_timeout: read_u16(body, 9)?,
|
|
+ pins,
|
|
+ resource_source,
|
|
+ vendor_data,
|
|
+ },
|
|
+ ))
|
|
+}
|
|
+
|
|
+fn parse_i2c_serial_bus(
|
|
+ desc: &[u8],
|
|
+ body: &[u8],
|
|
+ offset: usize,
|
|
+) -> Result<Option<I2cSerialBusDescriptor>, ResourceDecodeError> {
|
|
+ ensure_length(body, 15, offset, "SerialBus")?;
|
|
+ if body[2] != SERIAL_BUS_I2C {
|
|
+ return Ok(None);
|
|
+ }
|
|
+
|
|
+ let type_data_length = usize::from(read_u16(body, 7)?);
|
|
+ if type_data_length < I2C_TYPE_DATA_LEN {
|
|
+ return Err(ResourceDecodeError::InvalidLargeLength {
|
|
+ offset,
|
|
+ descriptor: "I2cSerialBus",
|
|
+ minimum: 15,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ let vendor_length = type_data_length - I2C_TYPE_DATA_LEN;
|
|
+ let vendor_data = parse_blob_absolute(desc, 18, vendor_length, offset, "I2cSerialBus")?;
|
|
+ let resource_source = parse_source_absolute(
|
|
+ desc,
|
|
+ 12 + type_data_length,
|
|
+ desc.len(),
|
|
+ body[1],
|
|
+ offset,
|
|
+ "I2cSerialBus",
|
|
+ )?;
|
|
+
|
|
+ Ok(Some(I2cSerialBusDescriptor {
|
|
+ revision_id: body[0],
|
|
+ producer_consumer: body[3] & 0x02 != 0,
|
|
+ slave_mode: body[3] & 0x01 != 0,
|
|
+ connection_sharing: body[3] & 0x04 != 0,
|
|
+ type_revision_id: body[6],
|
|
+ access_mode_10bit: read_u16(body, 4)? & 0x0001 != 0,
|
|
+ connection_speed: read_u32(body, 9)?,
|
|
+ slave_address: read_u16(body, 13)?,
|
|
+ resource_source,
|
|
+ vendor_data,
|
|
+ }))
|
|
+}
|
|
+
|
|
+fn parse_memory32(
|
|
+ body: &[u8],
|
|
+ offset: usize,
|
|
+) -> Result<Memory32RangeDescriptor, ResourceDecodeError> {
|
|
+ ensure_length(body, 17, offset, "Memory32Range")?;
|
|
+ Ok(Memory32RangeDescriptor {
|
|
+ write_protect: body[0] & 0x01 != 0,
|
|
+ minimum: read_u32(body, 1)?,
|
|
+ maximum: read_u32(body, 5)?,
|
|
+ alignment: read_u32(body, 9)?,
|
|
+ address_length: read_u32(body, 13)?,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn parse_fixed_memory32(
|
|
+ body: &[u8],
|
|
+ offset: usize,
|
|
+) -> Result<FixedMemory32Descriptor, ResourceDecodeError> {
|
|
+ ensure_length(body, 9, offset, "FixedMemory32")?;
|
|
+ Ok(FixedMemory32Descriptor {
|
|
+ write_protect: body[0] & 0x01 != 0,
|
|
+ address: read_u32(body, 1)?,
|
|
+ address_length: read_u32(body, 5)?,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn parse_address32(
|
|
+ desc: &[u8],
|
|
+ body: &[u8],
|
|
+ offset: usize,
|
|
+) -> Result<Address32Descriptor, ResourceDecodeError> {
|
|
+ ensure_length(body, 23, offset, "Address32")?;
|
|
+ Ok(Address32Descriptor {
|
|
+ resource_type: parse_address_type(body[0]),
|
|
+ producer_consumer: body[1] & 0x01 != 0,
|
|
+ decode: body[1] & 0x02 != 0,
|
|
+ min_address_fixed: body[1] & 0x04 != 0,
|
|
+ max_address_fixed: body[1] & 0x08 != 0,
|
|
+ specific_flags: body[2],
|
|
+ granularity: read_u32(body, 3)?,
|
|
+ minimum: read_u32(body, 7)?,
|
|
+ maximum: read_u32(body, 11)?,
|
|
+ translation_offset: read_u32(body, 15)?,
|
|
+ address_length: read_u32(body, 19)?,
|
|
+ resource_source: if desc.len() > 26 {
|
|
+ parse_source_absolute(desc, 26, desc.len(), desc[26], offset, "Address32")?
|
|
+ } else {
|
|
+ None
|
|
+ },
|
|
+ })
|
|
+}
|
|
+
|
|
+fn parse_address64(
|
|
+ desc: &[u8],
|
|
+ body: &[u8],
|
|
+ offset: usize,
|
|
+) -> Result<Address64Descriptor, ResourceDecodeError> {
|
|
+ ensure_length(body, 43, offset, "Address64")?;
|
|
+ Ok(Address64Descriptor {
|
|
+ resource_type: parse_address_type(body[0]),
|
|
+ producer_consumer: body[1] & 0x01 != 0,
|
|
+ decode: body[1] & 0x02 != 0,
|
|
+ min_address_fixed: body[1] & 0x04 != 0,
|
|
+ max_address_fixed: body[1] & 0x08 != 0,
|
|
+ specific_flags: body[2],
|
|
+ granularity: read_u64(body, 3)?,
|
|
+ minimum: read_u64(body, 11)?,
|
|
+ maximum: read_u64(body, 19)?,
|
|
+ translation_offset: read_u64(body, 27)?,
|
|
+ address_length: read_u64(body, 35)?,
|
|
+ resource_source: if desc.len() > 46 {
|
|
+ parse_source_absolute(desc, 46, desc.len(), desc[46], offset, "Address64")?
|
|
+ } else {
|
|
+ None
|
|
+ },
|
|
+ })
|
|
+}
|
|
+
|
|
+fn ensure_length(
|
|
+ body: &[u8],
|
|
+ minimum: usize,
|
|
+ offset: usize,
|
|
+ descriptor: &'static str,
|
|
+) -> Result<(), ResourceDecodeError> {
|
|
+ if body.len() < minimum {
|
|
+ return Err(ResourceDecodeError::InvalidLargeLength {
|
|
+ offset,
|
|
+ descriptor,
|
|
+ minimum,
|
|
+ });
|
|
+ }
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+fn parse_source_inline(bytes: &[u8]) -> ResourceSource {
|
|
+ let index = bytes.first().copied().unwrap_or(0);
|
|
+ let source = bytes.get(1..).map(parse_nul_string).unwrap_or_default();
|
|
+ ResourceSource { index, source }
|
|
+}
|
|
+
|
|
+fn parse_source_absolute(
|
|
+ desc: &[u8],
|
|
+ start: usize,
|
|
+ end: usize,
|
|
+ index: u8,
|
|
+ offset: usize,
|
|
+ descriptor: &'static str,
|
|
+) -> Result<Option<ResourceSource>, ResourceDecodeError> {
|
|
+ if start == 0 || start >= end || start > desc.len() {
|
|
+ return Ok(None);
|
|
+ }
|
|
+ let slice = desc
|
|
+ .get(start..end)
|
|
+ .ok_or(ResourceDecodeError::InvalidInternalOffset { offset, descriptor })?;
|
|
+ Ok(Some(ResourceSource {
|
|
+ index,
|
|
+ source: parse_nul_string(slice),
|
|
+ }))
|
|
+}
|
|
+
|
|
+fn parse_blob_absolute(
|
|
+ desc: &[u8],
|
|
+ start: usize,
|
|
+ length: usize,
|
|
+ offset: usize,
|
|
+ descriptor: &'static str,
|
|
+) -> Result<Vec<u8>, ResourceDecodeError> {
|
|
+ if start == 0 || length == 0 {
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ let end = start + length;
|
|
+ Ok(desc
|
|
+ .get(start..end)
|
|
+ .ok_or(ResourceDecodeError::InvalidInternalOffset { offset, descriptor })?
|
|
+ .to_vec())
|
|
+}
|
|
+
|
|
+fn parse_u16_list(
|
|
+ desc: &[u8],
|
|
+ start: usize,
|
|
+ end: usize,
|
|
+ offset: usize,
|
|
+ descriptor: &'static str,
|
|
+) -> Result<Vec<u16>, ResourceDecodeError> {
|
|
+ if start == 0 || start >= end || start > desc.len() {
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ let slice = desc
|
|
+ .get(start..end)
|
|
+ .ok_or(ResourceDecodeError::InvalidInternalOffset { offset, descriptor })?;
|
|
+ if slice.len() % 2 != 0 {
|
|
+ return Err(ResourceDecodeError::InvalidInternalOffset { offset, descriptor });
|
|
+ }
|
|
+ slice
|
|
+ .chunks_exact(2)
|
|
+ .map(|chunk| Ok(u16::from_le_bytes([chunk[0], chunk[1]])))
|
|
+ .collect()
|
|
+}
|
|
+
|
|
+fn parse_nul_string(bytes: &[u8]) -> String {
|
|
+ let end = bytes
|
|
+ .iter()
|
|
+ .position(|byte| *byte == 0)
|
|
+ .unwrap_or(bytes.len());
|
|
+ String::from_utf8_lossy(&bytes[..end]).to_string()
|
|
+}
|
|
+
|
|
+fn parse_address_type(value: u8) -> AddressResourceType {
|
|
+ match value {
|
|
+ 0 => AddressResourceType::MemoryRange,
|
|
+ 1 => AddressResourceType::IoRange,
|
|
+ 2 => AddressResourceType::BusNumberRange,
|
|
+ other => AddressResourceType::Unknown(other),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, ResourceDecodeError> {
|
|
+ let slice = bytes
|
|
+ .get(offset..offset + 2)
|
|
+ .ok_or(ResourceDecodeError::TruncatedDescriptor { offset })?;
|
|
+ Ok(u16::from_le_bytes([slice[0], slice[1]]))
|
|
+}
|
|
+
|
|
+fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, ResourceDecodeError> {
|
|
+ let slice = bytes
|
|
+ .get(offset..offset + 4)
|
|
+ .ok_or(ResourceDecodeError::TruncatedDescriptor { offset })?;
|
|
+ Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
|
|
+}
|
|
+
|
|
+fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, ResourceDecodeError> {
|
|
+ let slice = bytes
|
|
+ .get(offset..offset + 8)
|
|
+ .ok_or(ResourceDecodeError::TruncatedDescriptor { offset })?;
|
|
+ Ok(u64::from_le_bytes([
|
|
+ slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
|
|
+ ]))
|
|
+}
|
|
+
|
|
+fn min_nonzero<const N: usize>(values: [usize; N]) -> usize {
|
|
+ values
|
|
+ .into_iter()
|
|
+ .filter(|value| *value != 0)
|
|
+ .min()
|
|
+ .unwrap_or(0)
|
|
+}
|
|
+
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::{decode_resource_template, ResourceDescriptor};
|
|
+
|
|
+ #[test]
|
|
+ fn decodes_small_irq_descriptor() {
|
|
+ let resources = decode_resource_template(&[0x23, 0x0A, 0x00, 0x19, 0x79, 0x00]).unwrap();
|
|
+
|
|
+ assert!(matches!(
|
|
+ &resources[0],
|
|
+ ResourceDescriptor::Irq(descriptor)
|
|
+ if descriptor.interrupts == vec![1, 3]
|
|
+ && descriptor.shareable
|
|
+ && descriptor.wake_capable == false
|
|
+ ));
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn decodes_i2c_serial_bus_descriptor() {
|
|
+ let template = [
|
|
+ 0x8E, 0x14, 0x00, 0x01, 0x02, 0x01, 0x02, 0x00, 0x00, 0x01, 0x06, 0x00, 0x80, 0x1A,
|
|
+ 0x06, 0x00, 0x15, 0x00, b'I', b'2', b'C', b'0', 0x00, 0x79, 0x00,
|
|
+ ];
|
|
+ let resources = decode_resource_template(&template).unwrap();
|
|
+
|
|
+ assert!(matches!(
|
|
+ &resources[0],
|
|
+ ResourceDescriptor::I2cSerialBus(descriptor)
|
|
+ if descriptor.connection_speed == 400_000
|
|
+ && descriptor.slave_address == 0x15
|
|
+ && descriptor.resource_source.as_ref().map(|source| source.source.as_str())
|
|
+ == Some("I2C0")
|
|
+ ));
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn decodes_gpio_interrupt_descriptor() {
|
|
+ let template = [
|
|
+ 0x8C, 0x1B, 0x00, 0x01, 0x00, 0x01, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17,
|
|
+ 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x34, 0x12, b'\\', b'_', b'S', b'B',
|
|
+ 0x00, 0x79, 0x00,
|
|
+ ];
|
|
+ let resources = decode_resource_template(&template).unwrap();
|
|
+
|
|
+ assert!(matches!(&resources[0], ResourceDescriptor::GpioInt(_)));
|
|
+ }
|
|
+}
|
|
diff --git a/drivers/gpio/gpiod/Cargo.toml b/drivers/gpio/gpiod/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..7e087bd7
|
|
--- /dev/null
|
|
+++ b/drivers/gpio/gpiod/Cargo.toml
|
|
@@ -0,0 +1,21 @@
|
|
+[package]
|
|
+name = "gpiod"
|
|
+description = "GPIO controller registry daemon"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+redox-scheme.workspace = true
|
|
+ron.workspace = true
|
|
+serde.workspace = true
|
|
+
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+scheme-utils = { path = "../../../scheme-utils" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/gpio/gpiod/src/main.rs b/drivers/gpio/gpiod/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..41618ba1
|
|
--- /dev/null
|
|
+++ b/drivers/gpio/gpiod/src/main.rs
|
|
@@ -0,0 +1,496 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::process;
|
|
+
|
|
+use anyhow::{Context, Result};
|
|
+use redox_scheme::scheme::SchemeSync;
|
|
+use redox_scheme::{CallerCtx, OpenResult, Socket};
|
|
+use scheme_utils::{Blocking, HandleMap};
|
|
+use serde::{Deserialize, Serialize};
|
|
+use syscall::schemev2::NewFdFlags;
|
|
+use syscall::{Error as SysError, EACCES, EBADF, EINVAL, ENOENT};
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct GpioControllerInfo {
|
|
+ pub id: u32,
|
|
+ pub name: String,
|
|
+ pub pin_count: usize,
|
|
+ pub supports_interrupt: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum GpioControlRequest {
|
|
+ RegisterController { info: GpioControllerInfo },
|
|
+ ReadPin { controller_id: u32, pin: u32 },
|
|
+ WritePin { controller_id: u32, pin: u32, value: bool },
|
|
+ ConfigurePin { controller_id: u32, pin: u32, config: PinConfig },
|
|
+ ListControllers,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct PinConfig {
|
|
+ pub direction: PinDirection,
|
|
+ pub pull: PullMode,
|
|
+ pub interrupt_mode: Option<InterruptMode>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum PinDirection {
|
|
+ Input,
|
|
+ Output,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum PullMode {
|
|
+ None,
|
|
+ Up,
|
|
+ Down,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum InterruptMode {
|
|
+ EdgeRising,
|
|
+ EdgeFalling,
|
|
+ EdgeBoth,
|
|
+ LevelHigh,
|
|
+ LevelLow,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+enum GpioControlResponse {
|
|
+ ControllerRegistered { id: u32 },
|
|
+ Controllers(Vec<GpioControllerInfo>),
|
|
+ Controller(GpioControllerInfo),
|
|
+ PinValue(bool),
|
|
+ Ack,
|
|
+ Error(String),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
+enum PinOpKind {
|
|
+ Read,
|
|
+ Write,
|
|
+ Configure,
|
|
+}
|
|
+
|
|
+enum Handle {
|
|
+ SchemeRoot,
|
|
+ Register { pending: Vec<u8> },
|
|
+ Provider { controller_id: u32, pending: Vec<u8> },
|
|
+ ControllersDir { pending: Vec<u8> },
|
|
+ ControllerDetail { id: u32, pending: Vec<u8> },
|
|
+ PinOp { kind: PinOpKind, pending: Vec<u8> },
|
|
+}
|
|
+
|
|
+struct ControllerEntry {
|
|
+ info: GpioControllerInfo,
|
|
+ provider_handle: usize,
|
|
+}
|
|
+
|
|
+struct GpioDaemon {
|
|
+ handles: HandleMap<Handle>,
|
|
+ controllers: BTreeMap<u32, ControllerEntry>,
|
|
+ next_id: u32,
|
|
+}
|
|
+
|
|
+impl GpioDaemon {
|
|
+ fn new() -> Self {
|
|
+ Self {
|
|
+ handles: HandleMap::new(),
|
|
+ controllers: BTreeMap::new(),
|
|
+ next_id: 0,
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn controller_list(&self) -> Vec<GpioControllerInfo> {
|
|
+ self.controllers
|
|
+ .values()
|
|
+ .map(|entry| entry.info.clone())
|
|
+ .collect()
|
|
+ }
|
|
+
|
|
+ fn serialize_response(response: &GpioControlResponse) -> syscall::Result<Vec<u8>> {
|
|
+ ron::ser::to_string(response)
|
|
+ .map(|text| text.into_bytes())
|
|
+ .map_err(|err| {
|
|
+ log::error!("gpiod: failed to serialize control response: {err}");
|
|
+ SysError::new(EINVAL)
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn deserialize_request(buf: &[u8]) -> syscall::Result<GpioControlRequest> {
|
|
+ let text = std::str::from_utf8(buf).map_err(|err| {
|
|
+ log::warn!("gpiod: invalid UTF-8 request payload: {err}");
|
|
+ SysError::new(EINVAL)
|
|
+ })?;
|
|
+
|
|
+ ron::from_str(text).map_err(|err| {
|
|
+ log::warn!("gpiod: failed to decode control request: {err}");
|
|
+ SysError::new(EINVAL)
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn set_pending_response(handle: &mut Handle, response: GpioControlResponse) -> syscall::Result<()> {
|
|
+ let pending = Self::serialize_response(&response)?;
|
|
+ Self::set_pending_bytes(handle, pending)
|
|
+ }
|
|
+
|
|
+ fn set_pending_bytes(handle: &mut Handle, pending: Vec<u8>) -> syscall::Result<()> {
|
|
+ match handle {
|
|
+ Handle::Register { pending: slot }
|
|
+ | Handle::Provider { pending: slot, .. }
|
|
+ | Handle::ControllersDir { pending: slot }
|
|
+ | Handle::ControllerDetail { pending: slot, .. }
|
|
+ | Handle::PinOp { pending: slot, .. } => {
|
|
+ *slot = pending;
|
|
+ Ok(())
|
|
+ }
|
|
+ Handle::SchemeRoot => Err(SysError::new(EBADF)),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn copy_pending(handle: &mut Handle, buf: &mut [u8], offset: u64) -> syscall::Result<usize> {
|
|
+ let pending = match handle {
|
|
+ Handle::Register { pending }
|
|
+ | Handle::Provider { pending, .. }
|
|
+ | Handle::ControllersDir { pending }
|
|
+ | Handle::ControllerDetail { pending, .. }
|
|
+ | Handle::PinOp { pending, .. } => pending,
|
|
+ Handle::SchemeRoot => return Err(SysError::new(EBADF)),
|
|
+ };
|
|
+
|
|
+ let offset = usize::try_from(offset).map_err(|_| SysError::new(EINVAL))?;
|
|
+ if offset >= pending.len() {
|
|
+ return Ok(0);
|
|
+ }
|
|
+
|
|
+ let copy_len = buf.len().min(pending.len() - offset);
|
|
+ buf[..copy_len].copy_from_slice(&pending[offset..offset + copy_len]);
|
|
+ Ok(copy_len)
|
|
+ }
|
|
+
|
|
+ fn validate_pin_target(
|
|
+ &self,
|
|
+ controller_id: u32,
|
|
+ pin: u32,
|
|
+ ) -> std::result::Result<GpioControllerInfo, String> {
|
|
+ let entry = self
|
|
+ .controllers
|
|
+ .get(&controller_id)
|
|
+ .ok_or_else(|| format!("unknown controller {controller_id}"))?;
|
|
+ if usize::try_from(pin)
|
|
+ .ok()
|
|
+ .filter(|pin| *pin < entry.info.pin_count)
|
|
+ .is_none()
|
|
+ {
|
|
+ return Err(format!(
|
|
+ "pin {pin} is out of range for controller {} (pin_count={})",
|
|
+ entry.info.name, entry.info.pin_count
|
|
+ ));
|
|
+ }
|
|
+ Ok(entry.info.clone())
|
|
+ }
|
|
+}
|
|
+
|
|
+impl SchemeSync for GpioDaemon {
|
|
+ fn scheme_root(&mut self) -> syscall::Result<usize> {
|
|
+ Ok(self.handles.insert(Handle::SchemeRoot))
|
|
+ }
|
|
+
|
|
+ fn openat(
|
|
+ &mut self,
|
|
+ dirfd: usize,
|
|
+ path: &str,
|
|
+ _flags: usize,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<OpenResult> {
|
|
+ let handle = self.handles.get(dirfd)?;
|
|
+ let segments = path.trim_matches('/');
|
|
+
|
|
+ let new_handle = match handle {
|
|
+ Handle::SchemeRoot => {
|
|
+ if segments.is_empty() {
|
|
+ return Err(SysError::new(EINVAL));
|
|
+ }
|
|
+
|
|
+ let mut parts = segments.split('/');
|
|
+ match parts.next() {
|
|
+ Some("register") if parts.next().is_none() => Handle::Register {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ Some("controllers") => match parts.next() {
|
|
+ None => Handle::ControllersDir {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ Some(id) if parts.next().is_none() => Handle::ControllerDetail {
|
|
+ id: id.parse::<u32>().map_err(|_| SysError::new(EINVAL))?,
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ _ => return Err(SysError::new(EINVAL)),
|
|
+ },
|
|
+ Some("read_pin") if parts.next().is_none() => Handle::PinOp {
|
|
+ kind: PinOpKind::Read,
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ Some("write_pin") if parts.next().is_none() => Handle::PinOp {
|
|
+ kind: PinOpKind::Write,
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ Some("configure_pin") if parts.next().is_none() => Handle::PinOp {
|
|
+ kind: PinOpKind::Configure,
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ _ => return Err(SysError::new(ENOENT)),
|
|
+ }
|
|
+ }
|
|
+ Handle::ControllersDir { .. } => {
|
|
+ if segments.is_empty() {
|
|
+ return Err(SysError::new(EINVAL));
|
|
+ }
|
|
+
|
|
+ Handle::ControllerDetail {
|
|
+ id: segments.parse::<u32>().map_err(|_| SysError::new(EINVAL))?,
|
|
+ pending: Vec::new(),
|
|
+ }
|
|
+ }
|
|
+ _ => return Err(SysError::new(EACCES)),
|
|
+ };
|
|
+
|
|
+ let fd = self.handles.insert(new_handle);
|
|
+ Ok(OpenResult::ThisScheme {
|
|
+ number: fd,
|
|
+ flags: NewFdFlags::empty(),
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn read(
|
|
+ &mut self,
|
|
+ id: usize,
|
|
+ buf: &mut [u8],
|
|
+ offset: u64,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<usize> {
|
|
+ let controllers = self.controller_list();
|
|
+ let detail = match self.handles.get(id)? {
|
|
+ Handle::ControllerDetail { id, .. } => self.controllers.get(id).map(|entry| entry.info.clone()),
|
|
+ _ => None,
|
|
+ };
|
|
+
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ match handle {
|
|
+ Handle::ControllersDir { pending } if pending.is_empty() => {
|
|
+ *pending = Self::serialize_response(&GpioControlResponse::Controllers(controllers))?;
|
|
+ }
|
|
+ Handle::ControllerDetail { id, pending } if pending.is_empty() => {
|
|
+ let info = detail.ok_or(SysError::new(ENOENT))?;
|
|
+ *pending = Self::serialize_response(&GpioControlResponse::Controller(info))?;
|
|
+ log::debug!("gpiod: served controller detail for id={id}");
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+
|
|
+ Self::copy_pending(handle, buf, offset)
|
|
+ }
|
|
+
|
|
+ fn write(
|
|
+ &mut self,
|
|
+ id: usize,
|
|
+ buf: &[u8],
|
|
+ _offset: u64,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<usize> {
|
|
+ let request = Self::deserialize_request(buf)?;
|
|
+
|
|
+ match request {
|
|
+ GpioControlRequest::RegisterController { mut info } => {
|
|
+ if !matches!(self.handles.get(id)?, Handle::Register { .. }) {
|
|
+ return Err(SysError::new(EINVAL));
|
|
+ }
|
|
+
|
|
+ let controller_id = self.next_id;
|
|
+ self.next_id = self.next_id.checked_add(1).ok_or(SysError::new(EINVAL))?;
|
|
+ info.id = controller_id;
|
|
+ self.controllers.insert(
|
|
+ controller_id,
|
|
+ ControllerEntry {
|
|
+ info: info.clone(),
|
|
+ provider_handle: id,
|
|
+ },
|
|
+ );
|
|
+
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ *handle = Handle::Provider {
|
|
+ controller_id,
|
|
+ pending: Self::serialize_response(&GpioControlResponse::ControllerRegistered {
|
|
+ id: controller_id,
|
|
+ })?,
|
|
+ };
|
|
+
|
|
+ log::info!(
|
|
+ "RB_GPIOD_CONTROLLER_REGISTERED id={} name={} pin_count={} supports_interrupt={}",
|
|
+ info.id,
|
|
+ info.name,
|
|
+ info.pin_count,
|
|
+ info.supports_interrupt,
|
|
+ );
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ GpioControlRequest::ListControllers => {
|
|
+ let controllers = self.controller_list();
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ Self::set_pending_response(handle, GpioControlResponse::Controllers(controllers))?;
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ GpioControlRequest::ReadPin { controller_id, pin } => {
|
|
+ let validation = self.validate_pin_target(controller_id, pin);
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ match handle {
|
|
+ Handle::PinOp {
|
|
+ kind: PinOpKind::Read,
|
|
+ ..
|
|
+ } => {
|
|
+ match validation {
|
|
+ Ok(info) => {
|
|
+ log::info!(
|
|
+ "RB_GPIOD_PIN_READ controller_id={} name={} pin={} routed=stub",
|
|
+ controller_id,
|
|
+ info.name,
|
|
+ pin,
|
|
+ );
|
|
+ Self::set_pending_response(handle, GpioControlResponse::PinValue(false))?;
|
|
+ }
|
|
+ Err(message) => {
|
|
+ Self::set_pending_response(handle, GpioControlResponse::Error(message))?;
|
|
+ }
|
|
+ }
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ _ => Err(SysError::new(EINVAL)),
|
|
+ }
|
|
+ }
|
|
+ GpioControlRequest::WritePin {
|
|
+ controller_id,
|
|
+ pin,
|
|
+ value,
|
|
+ } => {
|
|
+ let validation = self.validate_pin_target(controller_id, pin);
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ match handle {
|
|
+ Handle::PinOp {
|
|
+ kind: PinOpKind::Write,
|
|
+ ..
|
|
+ } => {
|
|
+ match validation {
|
|
+ Ok(info) => {
|
|
+ log::info!(
|
|
+ "RB_GPIOD_PIN_WRITE controller_id={} name={} pin={} value={} routed=stub",
|
|
+ controller_id,
|
|
+ info.name,
|
|
+ pin,
|
|
+ value,
|
|
+ );
|
|
+ Self::set_pending_response(handle, GpioControlResponse::Ack)?;
|
|
+ }
|
|
+ Err(message) => {
|
|
+ Self::set_pending_response(handle, GpioControlResponse::Error(message))?;
|
|
+ }
|
|
+ }
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ _ => Err(SysError::new(EINVAL)),
|
|
+ }
|
|
+ }
|
|
+ GpioControlRequest::ConfigurePin {
|
|
+ controller_id,
|
|
+ pin,
|
|
+ config,
|
|
+ } => {
|
|
+ let validation = self.validate_pin_target(controller_id, pin);
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ match handle {
|
|
+ Handle::PinOp {
|
|
+ kind: PinOpKind::Configure,
|
|
+ ..
|
|
+ } => {
|
|
+ match validation {
|
|
+ Ok(info) => {
|
|
+ log::info!(
|
|
+ "RB_GPIOD_PIN_CONFIG controller_id={} name={} pin={} direction={:?} pull={:?} interrupt={:?} routed=stub",
|
|
+ controller_id,
|
|
+ info.name,
|
|
+ pin,
|
|
+ config.direction,
|
|
+ config.pull,
|
|
+ config.interrupt_mode,
|
|
+ );
|
|
+ Self::set_pending_response(handle, GpioControlResponse::Ack)?;
|
|
+ }
|
|
+ Err(message) => {
|
|
+ Self::set_pending_response(handle, GpioControlResponse::Error(message))?;
|
|
+ }
|
|
+ }
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ _ => Err(SysError::new(EINVAL)),
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn on_close(&mut self, id: usize) {
|
|
+ let Some(handle) = self.handles.remove(id) else {
|
|
+ return;
|
|
+ };
|
|
+ if let Handle::Provider { controller_id, .. } = handle {
|
|
+ if let Some(entry) = self.controllers.remove(&controller_id) {
|
|
+ log::info!(
|
|
+ "RB_GPIOD_CONTROLLER_REMOVED id={} name={} provider_handle={}",
|
|
+ controller_id,
|
|
+ entry.info.name,
|
|
+ entry.provider_handle,
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+fn run_daemon(daemon: daemon::SchemeDaemon) -> Result<()> {
|
|
+ let socket = Socket::create().context("failed to create gpio scheme socket")?;
|
|
+ let mut scheme = GpioDaemon::new();
|
|
+ let handler = Blocking::new(&socket, 16);
|
|
+
|
|
+ daemon
|
|
+ .ready_sync_scheme(&socket, &mut scheme)
|
|
+ .context("failed to publish gpio scheme root")?;
|
|
+
|
|
+ log::info!("RB_GPIOD_SCHEMA version=1");
|
|
+
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ handler
|
|
+ .process_requests_blocking(scheme)
|
|
+ .context("failed to process gpiod requests")?;
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::SchemeDaemon) -> ! {
|
|
+ if let Err(err) = run_daemon(daemon) {
|
|
+ log::error!("gpiod: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn main() {
|
|
+ common::setup_logging(
|
|
+ "gpio",
|
|
+ "gpio",
|
|
+ "gpiod",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ daemon::SchemeDaemon::new(daemon_runner);
|
|
+}
|
|
diff --git a/drivers/gpio/i2c-gpio-expanderd/Cargo.toml b/drivers/gpio/i2c-gpio-expanderd/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..3e168e96
|
|
--- /dev/null
|
|
+++ b/drivers/gpio/i2c-gpio-expanderd/Cargo.toml
|
|
@@ -0,0 +1,21 @@
|
|
+[package]
|
|
+name = "i2c-gpio-expanderd"
|
|
+description = "I2C GPIO expander bridge daemon"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+serde.workspace = true
|
|
+ron.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+i2c-interface = { path = "../../i2c/i2c-interface" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/gpio/i2c-gpio-expanderd/src/main.rs b/drivers/gpio/i2c-gpio-expanderd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..223ebc01
|
|
--- /dev/null
|
|
+++ b/drivers/gpio/i2c-gpio-expanderd/src/main.rs
|
|
@@ -0,0 +1,450 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::fs::{self, File, OpenOptions};
|
|
+use std::io::{Read, Write};
|
|
+use std::path::Path;
|
|
+use std::process;
|
|
+
|
|
+use acpi_resource::{GpioDescriptor, I2cSerialBusDescriptor, ResourceDescriptor};
|
|
+use anyhow::{Context, Result};
|
|
+use i2c_interface::{
|
|
+ I2cAdapterInfo, I2cControlRequest, I2cControlResponse, I2cTransferRequest,
|
|
+ I2cTransferResponse, I2cTransferSegment,
|
|
+};
|
|
+use serde::{Deserialize, Serialize};
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+struct AmlSymbol {
|
|
+ name: String,
|
|
+ value: AmlValue,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+enum AmlValue {
|
|
+ Integer(u64),
|
|
+ String(String),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+struct ExpanderResources {
|
|
+ i2c: I2cSerialBusDescriptor,
|
|
+ pin_count: usize,
|
|
+ gpio_int_count: usize,
|
|
+ gpio_io_count: usize,
|
|
+}
|
|
+
|
|
+#[derive(Debug)]
|
|
+struct ExpanderDescriptor {
|
|
+ device: String,
|
|
+ hid: String,
|
|
+ resources: ExpanderResources,
|
|
+}
|
|
+
|
|
+struct RegisteredExpander {
|
|
+ _registration: File,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+struct GpioControllerInfo {
|
|
+ id: u32,
|
|
+ name: String,
|
|
+ pin_count: usize,
|
|
+ supports_interrupt: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+enum GpioControlRequest {
|
|
+ RegisterController { info: GpioControllerInfo },
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+enum GpioControlResponse {
|
|
+ ControllerRegistered { id: u32 },
|
|
+ Error(String),
|
|
+}
|
|
+
|
|
+fn main() {
|
|
+ common::setup_logging(
|
|
+ "gpio",
|
|
+ "i2c-gpio-expander",
|
|
+ "i2c-gpio-expanderd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ daemon::Daemon::new(daemon_runner);
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::Daemon) -> ! {
|
|
+ if let Err(err) = daemon_main(daemon) {
|
|
+ log::error!("i2c-gpio-expanderd: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn daemon_main(daemon: daemon::Daemon) -> Result<()> {
|
|
+ let expanders = discover_expanders().context("failed to discover ACPI I2C GPIO expanders")?;
|
|
+ if expanders.is_empty() {
|
|
+ log::info!("i2c-gpio-expanderd: no probable ACPI I2C GPIO expanders found");
|
|
+ }
|
|
+
|
|
+ let adapters = list_i2c_adapters().unwrap_or_else(|err| {
|
|
+ log::warn!("i2c-gpio-expanderd: unable to query i2cd adapters: {err:#}");
|
|
+ Vec::new()
|
|
+ });
|
|
+
|
|
+ let mut registered = Vec::new();
|
|
+ for expander in expanders {
|
|
+ match register_expander(expander, &adapters) {
|
|
+ Ok(expander) => registered.push(expander),
|
|
+ Err(err) => log::warn!("i2c-gpio-expanderd: expander registration skipped: {err:#}"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ daemon.ready();
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ log::info!("i2c-gpio-expanderd: registered {} expander(s)", registered.len());
|
|
+
|
|
+ loop {
|
|
+ std::thread::park();
|
|
+ }
|
|
+}
|
|
+
|
|
+fn discover_expanders() -> Result<Vec<ExpanderDescriptor>> {
|
|
+ let mut matched = BTreeMap::new();
|
|
+
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => {
|
|
+ log::debug!("i2c-gpio-expanderd: ACPI symbols are not ready yet");
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to read ACPI symbol directory entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("_HID") && !file_name.ends_with("_CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let Some(id) = read_symbol_id(&entry.path())? else {
|
|
+ continue;
|
|
+ };
|
|
+ if is_excluded_device_id(&id) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let Some(device) = file_name
|
|
+ .strip_suffix("_HID")
|
|
+ .or_else(|| file_name.strip_suffix("_CID"))
|
|
+ .map(str::to_owned)
|
|
+ else {
|
|
+ continue;
|
|
+ };
|
|
+
|
|
+ let resources = match read_expander_resources(&device) {
|
|
+ Ok(resources) => resources,
|
|
+ Err(err) => {
|
|
+ log::debug!("i2c-gpio-expanderd: skipping {device}: {err:#}");
|
|
+ continue;
|
|
+ }
|
|
+ };
|
|
+ if resources.gpio_int_count == 0 && resources.gpio_io_count == 0 {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ matched.entry(device).or_insert((id, resources));
|
|
+ }
|
|
+
|
|
+ let mut expanders = Vec::new();
|
|
+ for (device, (hid, resources)) in matched {
|
|
+ expanders.push(ExpanderDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ });
|
|
+ }
|
|
+ Ok(expanders)
|
|
+}
|
|
+
|
|
+fn read_symbol_id(path: &Path) -> Result<Option<String>> {
|
|
+ let contents = fs::read_to_string(path)
|
|
+ .with_context(|| format!("failed to read ACPI symbol {}", path.display()))?;
|
|
+ let symbol = match ron::from_str::<AmlSymbol>(&contents) {
|
|
+ Ok(symbol) => symbol,
|
|
+ Err(err) => {
|
|
+ log::debug!(
|
|
+ "i2c-gpio-expanderd: skipping {} because the symbol payload was not a scalar ID: {err}",
|
|
+ path.display(),
|
|
+ );
|
|
+ return Ok(None);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let id = match symbol.value {
|
|
+ AmlValue::Integer(integer) => eisa_id_from_integer(integer),
|
|
+ AmlValue::String(string) => string,
|
|
+ };
|
|
+
|
|
+ log::debug!("i2c-gpio-expanderd: {} -> {id}", symbol.name);
|
|
+ Ok(Some(id))
|
|
+}
|
|
+
|
|
+fn read_expander_resources(device: &str) -> Result<ExpanderResources> {
|
|
+ let contents = fs::read_to_string(format!("/scheme/acpi/resources/{device}"))
|
|
+ .with_context(|| format!("failed to read /scheme/acpi/resources/{device}"))?;
|
|
+ let resources = ron::from_str::<Vec<ResourceDescriptor>>(&contents)
|
|
+ .with_context(|| format!("failed to decode RON resources for {device}"))?;
|
|
+
|
|
+ let mut i2c = None;
|
|
+ let mut pin_count = 0usize;
|
|
+ let mut gpio_int_count = 0usize;
|
|
+ let mut gpio_io_count = 0usize;
|
|
+
|
|
+ for resource in resources {
|
|
+ match resource {
|
|
+ ResourceDescriptor::I2cSerialBus(bus) if i2c.is_none() => i2c = Some(bus),
|
|
+ ResourceDescriptor::GpioInt(descriptor) => {
|
|
+ gpio_int_count += 1;
|
|
+ pin_count = pin_count.max(pin_count_from_descriptor(&descriptor));
|
|
+ }
|
|
+ ResourceDescriptor::GpioIo(descriptor) => {
|
|
+ gpio_io_count += 1;
|
|
+ pin_count = pin_count.max(pin_count_from_descriptor(&descriptor));
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(ExpanderResources {
|
|
+ i2c: i2c.context("no I2cSerialBus resource was found")?,
|
|
+ pin_count,
|
|
+ gpio_int_count,
|
|
+ gpio_io_count,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn pin_count_from_descriptor(descriptor: &GpioDescriptor) -> usize {
|
|
+ descriptor
|
|
+ .pins
|
|
+ .iter()
|
|
+ .copied()
|
|
+ .max()
|
|
+ .map(|pin| usize::from(pin).saturating_add(1))
|
|
+ .unwrap_or(0)
|
|
+}
|
|
+
|
|
+fn is_excluded_device_id(id: &str) -> bool {
|
|
+ matches!(
|
|
+ id,
|
|
+ "PNP0C50"
|
|
+ | "ACPI0C50"
|
|
+ | "INT34C5"
|
|
+ | "INTC1055"
|
|
+ | "INT33C2"
|
|
+ | "INT33C3"
|
|
+ | "INT3432"
|
|
+ | "INT3433"
|
|
+ | "INTC10EF"
|
|
+ | "AMDI0010"
|
|
+ | "AMDI0019"
|
|
+ | "AMDI0510"
|
|
+ | "PNP0CA0"
|
|
+ | "AMDI0042"
|
|
+ ) || id.starts_with("ELAN")
|
|
+ || id.starts_with("CYAP")
|
|
+ || id.starts_with("SYNA")
|
|
+}
|
|
+
|
|
+fn register_expander(expander: ExpanderDescriptor, adapters: &[I2cAdapterInfo]) -> Result<RegisteredExpander> {
|
|
+ let ExpanderDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ } = expander;
|
|
+
|
|
+ let adapter_name = resources
|
|
+ .i2c
|
|
+ .resource_source
|
|
+ .as_ref()
|
|
+ .map(|source| source.source.clone())
|
|
+ .filter(|source| !source.is_empty())
|
|
+ .unwrap_or_else(|| String::from("ACPI-I2C"));
|
|
+ let adapter = match match_i2c_adapter(adapters, &adapter_name) {
|
|
+ Some(adapter) => Some(adapter.clone()),
|
|
+ None => {
|
|
+ log::warn!(
|
|
+ "i2c-gpio-expanderd: unable to resolve I2C adapter {} for {}",
|
|
+ adapter_name,
|
|
+ device,
|
|
+ );
|
|
+ None
|
|
+ }
|
|
+ };
|
|
+
|
|
+ if let Some(adapter) = adapter.as_ref() {
|
|
+ if let Err(err) = probe_expander(adapter, &adapter_name, resources.i2c.slave_address) {
|
|
+ log::warn!(
|
|
+ "i2c-gpio-expanderd: expander {} probe on {}@{:04x} failed: {err:#}",
|
|
+ device,
|
|
+ adapter_name,
|
|
+ resources.i2c.slave_address,
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let info = GpioControllerInfo {
|
|
+ id: 0,
|
|
+ name: format!("i2c-gpio-expander:{device}"),
|
|
+ pin_count: resources.pin_count,
|
|
+ supports_interrupt: resources.gpio_int_count > 0,
|
|
+ };
|
|
+ let mut registration = register_with_gpiod(&info)
|
|
+ .with_context(|| format!("failed to register {device} with gpiod"))?;
|
|
+ let response = read_gpio_registration_response(&mut registration)
|
|
+ .with_context(|| format!("failed to read gpiod registration response for {device}"))?;
|
|
+
|
|
+ match response {
|
|
+ GpioControlResponse::ControllerRegistered { id } => {
|
|
+ log::info!(
|
|
+ "RB_I2C_GPIO_EXPANDERD_DEVICE device={} hid={} controller_id={} adapter={} addr={:04x} pin_count={} gpio_int={} gpio_io={}",
|
|
+ device,
|
|
+ hid,
|
|
+ id,
|
|
+ adapter_name,
|
|
+ resources.i2c.slave_address,
|
|
+ info.pin_count,
|
|
+ resources.gpio_int_count,
|
|
+ resources.gpio_io_count,
|
|
+ );
|
|
+ }
|
|
+ GpioControlResponse::Error(message) => {
|
|
+ anyhow::bail!("gpiod rejected expander {device}: {message}");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(RegisteredExpander {
|
|
+ _registration: registration,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn list_i2c_adapters() -> Result<Vec<I2cAdapterInfo>> {
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/adapters")
|
|
+ .context("failed to open /scheme/i2c/adapters")?;
|
|
+
|
|
+ let payload = ron::ser::to_string(&I2cControlRequest::ListAdapters)
|
|
+ .context("failed to encode I2C list-adapters request")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to request I2C adapter list")?;
|
|
+
|
|
+ let response = read_i2c_control_response(&mut file)?;
|
|
+ match response {
|
|
+ I2cControlResponse::AdapterList(adapters) => Ok(adapters),
|
|
+ I2cControlResponse::Error(message) => anyhow::bail!("i2cd returned an error: {message}"),
|
|
+ other => anyhow::bail!("unexpected i2cd list-adapters response: {other:?}"),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn match_i2c_adapter<'a>(adapters: &'a [I2cAdapterInfo], wanted: &str) -> Option<&'a I2cAdapterInfo> {
|
|
+ adapters
|
|
+ .iter()
|
|
+ .find(|adapter| adapter.name == wanted)
|
|
+ .or_else(|| adapters.iter().find(|adapter| adapter.name.ends_with(wanted)))
|
|
+ .or_else(|| adapters.iter().find(|adapter| wanted.ends_with(&adapter.name)))
|
|
+}
|
|
+
|
|
+fn probe_expander(adapter: &I2cAdapterInfo, adapter_name: &str, address: u16) -> Result<I2cTransferResponse> {
|
|
+ let request = I2cTransferRequest {
|
|
+ adapter: adapter_name.to_string(),
|
|
+ segments: vec![I2cTransferSegment::read(address, 1)],
|
|
+ stop: true,
|
|
+ };
|
|
+
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/transfer")
|
|
+ .context("failed to open /scheme/i2c/transfer")?;
|
|
+ let payload = ron::ser::to_string(&I2cControlRequest::Transfer {
|
|
+ adapter_id: adapter.id,
|
|
+ request,
|
|
+ })
|
|
+ .context("failed to encode I2C expander probe request")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to send I2C expander probe request")?;
|
|
+
|
|
+ let response = read_i2c_control_response(&mut file)?;
|
|
+ match response {
|
|
+ I2cControlResponse::TransferResult(result) => {
|
|
+ if !result.ok {
|
|
+ let detail = result
|
|
+ .error
|
|
+ .clone()
|
|
+ .unwrap_or_else(|| String::from("unknown I2C transfer failure"));
|
|
+ anyhow::bail!("I2C probe failed: {detail}");
|
|
+ }
|
|
+ Ok(result)
|
|
+ }
|
|
+ I2cControlResponse::Error(message) => anyhow::bail!("i2cd returned an error: {message}"),
|
|
+ other => anyhow::bail!("unexpected I2C transfer response: {other:?}"),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn register_with_gpiod(info: &GpioControllerInfo) -> Result<File> {
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/gpio/register")
|
|
+ .context("failed to open /scheme/gpio/register")?;
|
|
+ let payload = ron::ser::to_string(&GpioControlRequest::RegisterController { info: info.clone() })
|
|
+ .context("failed to encode GPIO controller registration")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to send GPIO controller registration")?;
|
|
+ Ok(file)
|
|
+}
|
|
+
|
|
+fn read_gpio_registration_response(file: &mut File) -> Result<GpioControlResponse> {
|
|
+ let mut buffer = vec![0_u8; 4096];
|
|
+ let count = file
|
|
+ .read(&mut buffer)
|
|
+ .context("failed to read GPIO registration response")?;
|
|
+ buffer.truncate(count);
|
|
+ let text = std::str::from_utf8(&buffer).context("GPIO registration response was not UTF-8")?;
|
|
+ ron::from_str(text).context("failed to decode GPIO registration response")
|
|
+}
|
|
+
|
|
+fn read_i2c_control_response(file: &mut File) -> Result<I2cControlResponse> {
|
|
+ let mut buffer = vec![0_u8; 4096];
|
|
+ let count = file
|
|
+ .read(&mut buffer)
|
|
+ .context("failed to read I2C control response")?;
|
|
+ buffer.truncate(count);
|
|
+ let text = std::str::from_utf8(&buffer).context("I2C control response was not UTF-8")?;
|
|
+ ron::from_str(text).context("failed to decode I2C control response")
|
|
+}
|
|
+
|
|
+fn eisa_id_from_integer(integer: u64) -> String {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1F) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+
|
|
+ format!(
|
|
+ "{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
|
|
+ )
|
|
+}
|
|
diff --git a/drivers/gpio/intel-gpiod/Cargo.toml b/drivers/gpio/intel-gpiod/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..f5ac9355
|
|
--- /dev/null
|
|
+++ b/drivers/gpio/intel-gpiod/Cargo.toml
|
|
@@ -0,0 +1,20 @@
|
|
+[package]
|
|
+name = "intel-gpiod"
|
|
+description = "Intel ACPI GPIO registrar daemon"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+serde.workspace = true
|
|
+ron.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/gpio/intel-gpiod/src/main.rs b/drivers/gpio/intel-gpiod/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..78e60990
|
|
--- /dev/null
|
|
+++ b/drivers/gpio/intel-gpiod/src/main.rs
|
|
@@ -0,0 +1,401 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::fs::{self, File, OpenOptions};
|
|
+use std::io::{Read, Write};
|
|
+use std::path::Path;
|
|
+use std::process;
|
|
+
|
|
+use acpi_resource::{
|
|
+ AddressResourceType, ExtendedIrqDescriptor, FixedMemory32Descriptor, GpioDescriptor,
|
|
+ IrqDescriptor, Memory32RangeDescriptor, ResourceDescriptor,
|
|
+};
|
|
+use anyhow::{Context, Result};
|
|
+use common::{MemoryType, PhysBorrowed, Prot};
|
|
+use serde::{Deserialize, Serialize};
|
|
+
|
|
+const SUPPORTED_IDS: &[&str] = &["INT34C5", "INTC1055"];
|
|
+
|
|
+const PADNFGPIO_OWN_BASE: usize = 0x20;
|
|
+const PADNFGPIO_PADCFG_BASE: usize = 0x700;
|
|
+const GPI_INT_STATUS: usize = 0x100;
|
|
+const GPI_INT_EN: usize = 0x120;
|
|
+const INTEL_GPIO_MMIO_WINDOW: usize = PADNFGPIO_PADCFG_BASE + core::mem::size_of::<u32>();
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+struct AmlSymbol {
|
|
+ name: String,
|
|
+ value: AmlValue,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+enum AmlValue {
|
|
+ Integer(u64),
|
|
+ String(String),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+struct ControllerResources {
|
|
+ mmio_base: usize,
|
|
+ mmio_len: usize,
|
|
+ pin_count: usize,
|
|
+ supports_interrupt: bool,
|
|
+ gpio_int_count: usize,
|
|
+ gpio_io_count: usize,
|
|
+}
|
|
+
|
|
+#[derive(Debug)]
|
|
+struct ControllerDescriptor {
|
|
+ device: String,
|
|
+ hid: String,
|
|
+ resources: ControllerResources,
|
|
+}
|
|
+
|
|
+struct RegisteredController {
|
|
+ _mmio: Option<PhysBorrowed>,
|
|
+ _registration: File,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+struct GpioControllerInfo {
|
|
+ id: u32,
|
|
+ name: String,
|
|
+ pin_count: usize,
|
|
+ supports_interrupt: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+enum GpioControlRequest {
|
|
+ RegisterController { info: GpioControllerInfo },
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+enum GpioControlResponse {
|
|
+ ControllerRegistered { id: u32 },
|
|
+ Error(String),
|
|
+}
|
|
+
|
|
+fn main() {
|
|
+ common::setup_logging(
|
|
+ "gpio",
|
|
+ "intel-gpio",
|
|
+ "intel-gpiod",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ daemon::Daemon::new(daemon_runner);
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::Daemon) -> ! {
|
|
+ if let Err(err) = daemon_main(daemon) {
|
|
+ log::error!("intel-gpiod: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn daemon_main(daemon: daemon::Daemon) -> Result<()> {
|
|
+ common::init();
|
|
+
|
|
+ let controllers =
|
|
+ discover_controllers(SUPPORTED_IDS).context("failed to discover Intel GPIO controllers")?;
|
|
+ if controllers.is_empty() {
|
|
+ log::info!("intel-gpiod: no supported Intel GPIO ACPI controllers found");
|
|
+ }
|
|
+
|
|
+ let mut registered = Vec::new();
|
|
+ for controller in controllers {
|
|
+ match register_controller(controller) {
|
|
+ Ok(controller) => registered.push(controller),
|
|
+ Err(err) => log::warn!("intel-gpiod: controller registration skipped: {err:#}"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ daemon.ready();
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ log::info!("intel-gpiod: registered {} controller(s)", registered.len());
|
|
+
|
|
+ loop {
|
|
+ std::thread::park();
|
|
+ }
|
|
+}
|
|
+
|
|
+fn discover_controllers(supported_ids: &[&str]) -> Result<Vec<ControllerDescriptor>> {
|
|
+ let mut matched = BTreeMap::new();
|
|
+
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => {
|
|
+ log::debug!("intel-gpiod: ACPI symbols are not ready yet");
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to read ACPI symbol directory entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("_HID") && !file_name.ends_with("_CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let Some(id) = read_symbol_id(&entry.path())? else {
|
|
+ continue;
|
|
+ };
|
|
+ if !supported_ids.iter().any(|candidate| *candidate == id) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let device = file_name
|
|
+ .strip_suffix("_HID")
|
|
+ .or_else(|| file_name.strip_suffix("_CID"))
|
|
+ .map(str::to_owned);
|
|
+ if let Some(device) = device {
|
|
+ matched.entry(device).or_insert(id);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let mut controllers = Vec::new();
|
|
+ for (device, hid) in matched {
|
|
+ let resources = read_controller_resources(&device)
|
|
+ .with_context(|| format!("failed to read resources for {device}"))?;
|
|
+ controllers.push(ControllerDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ Ok(controllers)
|
|
+}
|
|
+
|
|
+fn read_symbol_id(path: &Path) -> Result<Option<String>> {
|
|
+ let contents = fs::read_to_string(path)
|
|
+ .with_context(|| format!("failed to read ACPI symbol {}", path.display()))?;
|
|
+ let symbol = match ron::from_str::<AmlSymbol>(&contents) {
|
|
+ Ok(symbol) => symbol,
|
|
+ Err(err) => {
|
|
+ log::debug!(
|
|
+ "intel-gpiod: skipping {} because the symbol payload was not a scalar ID: {err}",
|
|
+ path.display(),
|
|
+ );
|
|
+ return Ok(None);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let id = match symbol.value {
|
|
+ AmlValue::Integer(integer) => eisa_id_from_integer(integer),
|
|
+ AmlValue::String(string) => string,
|
|
+ };
|
|
+
|
|
+ log::debug!("intel-gpiod: {} -> {id}", symbol.name);
|
|
+ Ok(Some(id))
|
|
+}
|
|
+
|
|
+fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
|
+ let contents = fs::read_to_string(format!("/scheme/acpi/resources/{device}"))
|
|
+ .with_context(|| format!("failed to read /scheme/acpi/resources/{device}"))?;
|
|
+ let resources = ron::from_str::<Vec<ResourceDescriptor>>(&contents)
|
|
+ .with_context(|| format!("failed to decode RON resources for {device}"))?;
|
|
+
|
|
+ let mut mmio = None;
|
|
+ let mut supports_interrupt = false;
|
|
+ let mut gpio_int_count = 0usize;
|
|
+ let mut gpio_io_count = 0usize;
|
|
+ let mut pin_count = 0usize;
|
|
+
|
|
+ for resource in &resources {
|
|
+ match resource {
|
|
+ ResourceDescriptor::FixedMemory32(FixedMemory32Descriptor {
|
|
+ address,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() => {
|
|
+ mmio = Some((
|
|
+ *address as usize,
|
|
+ (*address_length as usize).max(INTEL_GPIO_MMIO_WINDOW),
|
|
+ ));
|
|
+ }
|
|
+ ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
|
|
+ minimum,
|
|
+ maximum,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() && maximum >= minimum => {
|
|
+ let span = maximum.saturating_sub(*minimum).saturating_add(1) as usize;
|
|
+ mmio = Some((
|
|
+ *minimum as usize,
|
|
+ span.max((*address_length as usize).max(INTEL_GPIO_MMIO_WINDOW)),
|
|
+ ));
|
|
+ }
|
|
+ ResourceDescriptor::Address32(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ mmio = Some((
|
|
+ descriptor.minimum as usize,
|
|
+ (descriptor.address_length as usize).max(INTEL_GPIO_MMIO_WINDOW),
|
|
+ ));
|
|
+ }
|
|
+ ResourceDescriptor::Address64(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ let base = usize::try_from(descriptor.minimum)
|
|
+ .context("64-bit MMIO base does not fit in usize")?;
|
|
+ let len = usize::try_from(descriptor.address_length)
|
|
+ .context("64-bit MMIO length does not fit in usize")?;
|
|
+ mmio = Some((base, len.max(INTEL_GPIO_MMIO_WINDOW)));
|
|
+ }
|
|
+ ResourceDescriptor::Irq(IrqDescriptor { interrupts, .. }) => {
|
|
+ supports_interrupt |= !interrupts.is_empty();
|
|
+ }
|
|
+ ResourceDescriptor::ExtendedIrq(ExtendedIrqDescriptor { interrupts, .. }) => {
|
|
+ supports_interrupt |= !interrupts.is_empty();
|
|
+ }
|
|
+ ResourceDescriptor::GpioInt(descriptor) => {
|
|
+ gpio_int_count += 1;
|
|
+ supports_interrupt = true;
|
|
+ pin_count = pin_count.max(pin_count_from_descriptor(descriptor));
|
|
+ }
|
|
+ ResourceDescriptor::GpioIo(descriptor) => {
|
|
+ gpio_io_count += 1;
|
|
+ pin_count = pin_count.max(pin_count_from_descriptor(descriptor));
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let (mmio_base, mmio_len) = mmio.context("no MMIO resource was found")?;
|
|
+ Ok(ControllerResources {
|
|
+ mmio_base,
|
|
+ mmio_len,
|
|
+ pin_count,
|
|
+ supports_interrupt,
|
|
+ gpio_int_count,
|
|
+ gpio_io_count,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn pin_count_from_descriptor(descriptor: &GpioDescriptor) -> usize {
|
|
+ descriptor
|
|
+ .pins
|
|
+ .iter()
|
|
+ .copied()
|
|
+ .max()
|
|
+ .map(|pin| usize::from(pin).saturating_add(1))
|
|
+ .unwrap_or(0)
|
|
+}
|
|
+
|
|
+fn register_controller(controller: ControllerDescriptor) -> Result<RegisteredController> {
|
|
+ let ControllerDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ } = controller;
|
|
+
|
|
+ let mmio = match PhysBorrowed::map(
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ Prot::RW,
|
|
+ MemoryType::Uncacheable,
|
|
+ ) {
|
|
+ Ok(mapping) => Some(mapping),
|
|
+ Err(err) => {
|
|
+ log::warn!(
|
|
+ "intel-gpiod: failed to map MMIO for {device} ({:#x}, len {:#x}): {err}",
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ );
|
|
+ None
|
|
+ }
|
|
+ };
|
|
+
|
|
+ log::info!(
|
|
+ "intel-gpiod: discovered {device} hid={hid} mmio={:#x}+{:#x} pin_count={} gpio_int={} gpio_io={} supports_interrupt={}",
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ resources.pin_count,
|
|
+ resources.gpio_int_count,
|
|
+ resources.gpio_io_count,
|
|
+ resources.supports_interrupt,
|
|
+ );
|
|
+ log::debug!(
|
|
+ "intel-gpiod: register model own={PADNFGPIO_OWN_BASE:#x} padcfg={PADNFGPIO_PADCFG_BASE:#x} gpi_int_status={GPI_INT_STATUS:#x} gpi_int_en={GPI_INT_EN:#x}",
|
|
+ );
|
|
+
|
|
+ let info = GpioControllerInfo {
|
|
+ id: 0,
|
|
+ name: format!("intel-gpio:{device}"),
|
|
+ pin_count: resources.pin_count,
|
|
+ supports_interrupt: resources.supports_interrupt,
|
|
+ };
|
|
+ let mut registration = register_with_gpiod(&info)
|
|
+ .with_context(|| format!("failed to register {device} with gpiod"))?;
|
|
+ let response = read_registration_response(&mut registration)
|
|
+ .with_context(|| format!("failed to read gpiod registration response for {device}"))?;
|
|
+
|
|
+ match response {
|
|
+ GpioControlResponse::ControllerRegistered { id } => {
|
|
+ log::info!(
|
|
+ "RB_INTEL_GPIOD_DEVICE device={} hid={} controller_id={} pin_count={} supports_interrupt={}",
|
|
+ device,
|
|
+ hid,
|
|
+ id,
|
|
+ info.pin_count,
|
|
+ info.supports_interrupt,
|
|
+ );
|
|
+ }
|
|
+ GpioControlResponse::Error(message) => {
|
|
+ anyhow::bail!("gpiod rejected Intel GPIO controller {device}: {message}");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(RegisteredController {
|
|
+ _mmio: mmio,
|
|
+ _registration: registration,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn register_with_gpiod(info: &GpioControllerInfo) -> Result<File> {
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/gpio/register")
|
|
+ .context("failed to open /scheme/gpio/register")?;
|
|
+ let payload = ron::ser::to_string(&GpioControlRequest::RegisterController { info: info.clone() })
|
|
+ .context("failed to encode GPIO controller registration")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to send GPIO controller registration")?;
|
|
+ Ok(file)
|
|
+}
|
|
+
|
|
+fn read_registration_response(file: &mut File) -> Result<GpioControlResponse> {
|
|
+ let mut buffer = vec![0_u8; 4096];
|
|
+ let count = file
|
|
+ .read(&mut buffer)
|
|
+ .context("failed to read GPIO registration response")?;
|
|
+ buffer.truncate(count);
|
|
+ let text = std::str::from_utf8(&buffer).context("GPIO registration response was not UTF-8")?;
|
|
+ ron::from_str(text).context("failed to decode GPIO registration response")
|
|
+}
|
|
+
|
|
+fn eisa_id_from_integer(integer: u64) -> String {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1F) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+
|
|
+ format!(
|
|
+ "{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
|
|
+ )
|
|
+}
|
|
diff --git a/drivers/i2c/amd-mp2-i2cd/Cargo.toml b/drivers/i2c/amd-mp2-i2cd/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..357ca948
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/amd-mp2-i2cd/Cargo.toml
|
|
@@ -0,0 +1,22 @@
|
|
+[package]
|
|
+name = "amd-mp2-i2cd"
|
|
+description = "AMD MP2 PCI I2C controller driver"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+serde.workspace = true
|
|
+ron.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+i2c-interface = { path = "../i2c-interface" }
|
|
+pcid = { path = "../../pcid" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/i2c/amd-mp2-i2cd/src/main.rs b/drivers/i2c/amd-mp2-i2cd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..925b45e7
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/amd-mp2-i2cd/src/main.rs
|
|
@@ -0,0 +1,106 @@
|
|
+use std::fs::{File, OpenOptions};
|
|
+use std::io::{Read, Write};
|
|
+use std::process;
|
|
+
|
|
+use anyhow::{Context, Result};
|
|
+use i2c_interface::{I2cAdapterInfo, I2cControlRequest, I2cControlResponse};
|
|
+use pcid_interface::PciFunctionHandle;
|
|
+
|
|
+const MP2_MAILBOX_STATUS: usize = 0x00;
|
|
+const MP2_MAILBOX_COMMAND: usize = 0x04;
|
|
+const MP2_MAILBOX_ARGUMENT0: usize = 0x08;
|
|
+const MP2_MAILBOX_ARGUMENT1: usize = 0x0C;
|
|
+
|
|
+fn main() {
|
|
+ pcid_interface::pci_daemon(daemon_runner);
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
|
|
+ if let Err(err) = daemon_main(daemon, &mut pcid_handle) {
|
|
+ log::error!("amd-mp2-i2cd: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn daemon_main(daemon: daemon::Daemon, pcid_handle: &mut PciFunctionHandle) -> Result<()> {
|
|
+ let pci_config = pcid_handle.config();
|
|
+ let log_name = format!("{}_amd-mp2-i2c", pci_config.func.name());
|
|
+
|
|
+ common::setup_logging(
|
|
+ "bus",
|
|
+ "i2c",
|
|
+ &log_name,
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ let mapped_bar = unsafe { pcid_handle.map_bar(0) };
|
|
+ let bar_addr = mapped_bar.ptr.as_ptr() as usize;
|
|
+ let bar_size = mapped_bar.bar_size;
|
|
+
|
|
+ log::info!(
|
|
+ "amd-mp2-i2cd: {} BAR0={:#x}+{:#x} mapped={:p}+{:#x}",
|
|
+ pci_config.func.display(),
|
|
+ bar_addr,
|
|
+ bar_size,
|
|
+ mapped_bar.ptr.as_ptr(),
|
|
+ mapped_bar.bar_size,
|
|
+ );
|
|
+ log::debug!(
|
|
+ "amd-mp2-i2cd: MP2 mailbox regs status={MP2_MAILBOX_STATUS:#x} cmd={MP2_MAILBOX_COMMAND:#x} arg0={MP2_MAILBOX_ARGUMENT0:#x} arg1={MP2_MAILBOX_ARGUMENT1:#x}",
|
|
+ );
|
|
+
|
|
+ let info = I2cAdapterInfo {
|
|
+ id: 0,
|
|
+ name: format!("amd-mp2:{}", pci_config.func.name()),
|
|
+ max_transaction_size: 0,
|
|
+ supports_10bit_addr: false,
|
|
+ };
|
|
+ let mut registration = register_adapter(&info)
|
|
+ .context("failed to register AMD MP2 controller with i2cd")?;
|
|
+ let response = read_registration_response(&mut registration)
|
|
+ .context("failed to read AMD MP2 i2cd registration response")?;
|
|
+
|
|
+ match response {
|
|
+ I2cControlResponse::AdapterRegistered { id } => {
|
|
+ log::info!("amd-mp2-i2cd: controller registered with i2cd as adapter {id}");
|
|
+ }
|
|
+ other => anyhow::bail!("unexpected i2cd registration response: {other:?}"),
|
|
+ }
|
|
+
|
|
+ daemon.ready();
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ let _keep_registration = registration;
|
|
+ let _keep_pcid = pcid_handle;
|
|
+
|
|
+ loop {
|
|
+ std::thread::park();
|
|
+ }
|
|
+}
|
|
+
|
|
+fn register_adapter(info: &I2cAdapterInfo) -> Result<File> {
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/register")
|
|
+ .context("failed to open /scheme/i2c/register")?;
|
|
+ let payload = ron::ser::to_string(&I2cControlRequest::RegisterAdapter { info: info.clone() })
|
|
+ .context("failed to encode AMD MP2 I2C registration payload")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to send AMD MP2 I2C registration payload")?;
|
|
+ Ok(file)
|
|
+}
|
|
+
|
|
+fn read_registration_response(file: &mut File) -> Result<I2cControlResponse> {
|
|
+ let mut buffer = vec![0_u8; 4096];
|
|
+ let count = file
|
|
+ .read(&mut buffer)
|
|
+ .context("failed to read AMD MP2 I2C registration response")?;
|
|
+ buffer.truncate(count);
|
|
+ let text = std::str::from_utf8(&buffer)
|
|
+ .context("AMD MP2 I2C registration response was not UTF-8")?;
|
|
+ ron::from_str(text).context("failed to decode AMD MP2 I2C registration response")
|
|
+}
|
|
diff --git a/drivers/i2c/dw-acpi-i2cd/Cargo.toml b/drivers/i2c/dw-acpi-i2cd/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..a90b48cc
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/dw-acpi-i2cd/Cargo.toml
|
|
@@ -0,0 +1,21 @@
|
|
+[package]
|
|
+name = "dw-acpi-i2cd"
|
|
+description = "Generic DesignWare ACPI I2C controller driver"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+serde.workspace = true
|
|
+ron.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+i2c-interface = { path = "../i2c-interface" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/i2c/dw-acpi-i2cd/src/main.rs b/drivers/i2c/dw-acpi-i2cd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..b22a2773
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/dw-acpi-i2cd/src/main.rs
|
|
@@ -0,0 +1,361 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::fs::{self, File, OpenOptions};
|
|
+use std::io::{Read, Write};
|
|
+use std::path::Path;
|
|
+use std::process;
|
|
+
|
|
+use acpi_resource::{
|
|
+ AddressResourceType, ExtendedIrqDescriptor, FixedMemory32Descriptor, I2cSerialBusDescriptor,
|
|
+ IrqDescriptor, Memory32RangeDescriptor, ResourceDescriptor,
|
|
+};
|
|
+use anyhow::{Context, Result};
|
|
+use common::{MemoryType, PhysBorrowed, Prot};
|
|
+use i2c_interface::{I2cAdapterInfo, I2cControlRequest, I2cControlResponse};
|
|
+use serde::Deserialize;
|
|
+
|
|
+const SUPPORTED_IDS: &[&str] = &["80860F41", "808622C1", "AMDI0010", "AMDI0019", "AMDI0510"];
|
|
+
|
|
+const DW_IC_CON: usize = 0x00;
|
|
+const DW_IC_TAR: usize = 0x04;
|
|
+const DW_IC_SS_SCL_HCNT: usize = 0x14;
|
|
+const DW_IC_SS_SCL_LCNT: usize = 0x18;
|
|
+const DW_IC_DATA_CMD: usize = 0x10;
|
|
+const DW_IC_INTR_MASK: usize = 0x30;
|
|
+const DW_IC_CLR_INTR: usize = 0x40;
|
|
+const DW_IC_ENABLE: usize = 0x6C;
|
|
+const DW_IC_STATUS: usize = 0x70;
|
|
+const DW_MMIO_WINDOW: usize = DW_IC_STATUS + core::mem::size_of::<u32>();
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+struct AmlSymbol {
|
|
+ name: String,
|
|
+ value: AmlValue,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+enum AmlValue {
|
|
+ Integer(u64),
|
|
+ String(String),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+struct ControllerResources {
|
|
+ mmio_base: usize,
|
|
+ mmio_len: usize,
|
|
+ irq: Option<u32>,
|
|
+ serial_bus: Option<I2cSerialBusDescriptor>,
|
|
+}
|
|
+
|
|
+#[derive(Debug)]
|
|
+struct ControllerDescriptor {
|
|
+ device: String,
|
|
+ hid: String,
|
|
+ resources: ControllerResources,
|
|
+}
|
|
+
|
|
+struct RegisteredController {
|
|
+ _mmio: Option<PhysBorrowed>,
|
|
+ _registration: File,
|
|
+}
|
|
+
|
|
+fn main() {
|
|
+ common::setup_logging(
|
|
+ "bus",
|
|
+ "i2c",
|
|
+ "dw-acpi-i2cd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ daemon::Daemon::new(daemon_runner);
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::Daemon) -> ! {
|
|
+ if let Err(err) = daemon_main(daemon) {
|
|
+ log::error!("dw-acpi-i2cd: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn daemon_main(daemon: daemon::Daemon) -> Result<()> {
|
|
+ common::init();
|
|
+
|
|
+ let controllers = discover_controllers(SUPPORTED_IDS)
|
|
+ .context("failed to discover DesignWare ACPI I2C controllers")?;
|
|
+ if controllers.is_empty() {
|
|
+ log::info!("dw-acpi-i2cd: no supported ACPI controllers found");
|
|
+ }
|
|
+
|
|
+ let mut registered = Vec::new();
|
|
+ for controller in controllers {
|
|
+ match register_controller("dw-acpi", controller) {
|
|
+ Ok(controller) => registered.push(controller),
|
|
+ Err(err) => log::warn!("dw-acpi-i2cd: controller registration skipped: {err:#}"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ daemon.ready();
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ log::info!("dw-acpi-i2cd: registered {} controller(s)", registered.len());
|
|
+
|
|
+ loop {
|
|
+ std::thread::park();
|
|
+ }
|
|
+}
|
|
+
|
|
+fn discover_controllers(supported_ids: &[&str]) -> Result<Vec<ControllerDescriptor>> {
|
|
+ let mut matched = BTreeMap::new();
|
|
+
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err)
|
|
+ if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) =>
|
|
+ {
|
|
+ log::debug!("dw-acpi-i2cd: ACPI symbols are not ready yet");
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to read ACPI symbol directory entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("_HID") && !file_name.ends_with("_CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let Some(id) = read_symbol_id(&entry.path())? else {
|
|
+ continue;
|
|
+ };
|
|
+ if !supported_ids.iter().any(|candidate| *candidate == id) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let device = file_name
|
|
+ .strip_suffix("_HID")
|
|
+ .or_else(|| file_name.strip_suffix("_CID"))
|
|
+ .map(str::to_owned);
|
|
+ if let Some(device) = device {
|
|
+ matched.entry(device).or_insert(id);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let mut controllers = Vec::new();
|
|
+ for (device, hid) in matched {
|
|
+ let resources = read_controller_resources(&device)
|
|
+ .with_context(|| format!("failed to read resources for {device}"))?;
|
|
+ controllers.push(ControllerDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ Ok(controllers)
|
|
+}
|
|
+
|
|
+fn read_symbol_id(path: &Path) -> Result<Option<String>> {
|
|
+ let contents = fs::read_to_string(path)
|
|
+ .with_context(|| format!("failed to read ACPI symbol {}", path.display()))?;
|
|
+ let symbol = match ron::from_str::<AmlSymbol>(&contents) {
|
|
+ Ok(symbol) => symbol,
|
|
+ Err(err) => {
|
|
+ log::debug!(
|
|
+ "dw-acpi-i2cd: skipping {} because the symbol payload was not a scalar ID: {err}",
|
|
+ path.display(),
|
|
+ );
|
|
+ return Ok(None);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let id = match symbol.value {
|
|
+ AmlValue::Integer(integer) => eisa_id_from_integer(integer),
|
|
+ AmlValue::String(string) => string,
|
|
+ };
|
|
+
|
|
+ log::debug!("dw-acpi-i2cd: {} -> {id}", symbol.name);
|
|
+ Ok(Some(id))
|
|
+}
|
|
+
|
|
+fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
|
+ let contents = fs::read_to_string(format!("/scheme/acpi/resources/{device}"))
|
|
+ .with_context(|| format!("failed to read /scheme/acpi/resources/{device}"))?;
|
|
+ let resources = ron::from_str::<Vec<ResourceDescriptor>>(&contents)
|
|
+ .with_context(|| format!("failed to decode RON resources for {device}"))?;
|
|
+
|
|
+ let mut mmio = None;
|
|
+ let mut irq = None;
|
|
+ let mut serial_bus = None;
|
|
+
|
|
+ for resource in &resources {
|
|
+ match resource {
|
|
+ ResourceDescriptor::FixedMemory32(FixedMemory32Descriptor {
|
|
+ address,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() => {
|
|
+ mmio = Some((*address as usize, (*address_length as usize).max(DW_MMIO_WINDOW)));
|
|
+ }
|
|
+ ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
|
|
+ minimum,
|
|
+ maximum,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() && maximum >= minimum => {
|
|
+ let span = maximum.saturating_sub(*minimum).saturating_add(1) as usize;
|
|
+ mmio = Some((
|
|
+ *minimum as usize,
|
|
+ span.max((*address_length as usize).max(DW_MMIO_WINDOW)),
|
|
+ ));
|
|
+ }
|
|
+ ResourceDescriptor::Address32(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ mmio = Some((
|
|
+ descriptor.minimum as usize,
|
|
+ (descriptor.address_length as usize).max(DW_MMIO_WINDOW),
|
|
+ ));
|
|
+ }
|
|
+ ResourceDescriptor::Address64(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ let base = usize::try_from(descriptor.minimum)
|
|
+ .context("64-bit MMIO base does not fit in usize")?;
|
|
+ let len = usize::try_from(descriptor.address_length)
|
|
+ .context("64-bit MMIO length does not fit in usize")?;
|
|
+ mmio = Some((base, len.max(DW_MMIO_WINDOW)));
|
|
+ }
|
|
+ ResourceDescriptor::Irq(IrqDescriptor { interrupts, .. }) if irq.is_none() => {
|
|
+ irq = interrupts.first().copied().map(u32::from);
|
|
+ }
|
|
+ ResourceDescriptor::ExtendedIrq(ExtendedIrqDescriptor { interrupts, .. })
|
|
+ if irq.is_none() =>
|
|
+ {
|
|
+ irq = interrupts.first().copied();
|
|
+ }
|
|
+ ResourceDescriptor::I2cSerialBus(descriptor) if serial_bus.is_none() => {
|
|
+ serial_bus = Some(descriptor.clone());
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let (mmio_base, mmio_len) = mmio.context("no MMIO resource was found")?;
|
|
+ Ok(ControllerResources {
|
|
+ mmio_base,
|
|
+ mmio_len,
|
|
+ irq,
|
|
+ serial_bus,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn register_controller(prefix: &str, controller: ControllerDescriptor) -> Result<RegisteredController> {
|
|
+ let ControllerDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ } = controller;
|
|
+
|
|
+ let mmio = match PhysBorrowed::map(
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ Prot::RW,
|
|
+ MemoryType::Uncacheable,
|
|
+ ) {
|
|
+ Ok(mapping) => Some(mapping),
|
|
+ Err(err) => {
|
|
+ log::warn!(
|
|
+ "dw-acpi-i2cd: failed to map MMIO for {device} ({:#x}, len {:#x}): {err}",
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ );
|
|
+ None
|
|
+ }
|
|
+ };
|
|
+
|
|
+ log::info!(
|
|
+ "dw-acpi-i2cd: discovered {device} hid={hid} mmio={:#x}+{:#x} irq={:?}",
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ resources.irq,
|
|
+ );
|
|
+ log::debug!(
|
|
+ "dw-acpi-i2cd: DesignWare regs con={DW_IC_CON:#x} tar={DW_IC_TAR:#x} data_cmd={DW_IC_DATA_CMD:#x} intr_mask={DW_IC_INTR_MASK:#x} clr_intr={DW_IC_CLR_INTR:#x} enable={DW_IC_ENABLE:#x} ss_hcnt={DW_IC_SS_SCL_HCNT:#x} ss_lcnt={DW_IC_SS_SCL_LCNT:#x}",
|
|
+ );
|
|
+
|
|
+ let info = I2cAdapterInfo {
|
|
+ id: 0,
|
|
+ name: format!("{prefix}:{device}"),
|
|
+ max_transaction_size: 0,
|
|
+ supports_10bit_addr: resources
|
|
+ .serial_bus
|
|
+ .as_ref()
|
|
+ .map(|bus| bus.access_mode_10bit)
|
|
+ .unwrap_or(false),
|
|
+ };
|
|
+ let mut registration = register_adapter(&info)
|
|
+ .with_context(|| format!("failed to register {device} with i2cd"))?;
|
|
+ let response = read_registration_response(&mut registration)
|
|
+ .with_context(|| format!("failed to read i2cd registration response for {device}"))?;
|
|
+
|
|
+ match response {
|
|
+ I2cControlResponse::AdapterRegistered { id } => {
|
|
+ log::info!("dw-acpi-i2cd: adapter {device} registered with i2cd as {id}");
|
|
+ }
|
|
+ other => {
|
|
+ anyhow::bail!("unexpected i2cd registration response for {device}: {other:?}");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(RegisteredController {
|
|
+ _mmio: mmio,
|
|
+ _registration: registration,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn register_adapter(info: &I2cAdapterInfo) -> Result<File> {
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/register")
|
|
+ .context("failed to open /scheme/i2c/register")?;
|
|
+ let payload = ron::ser::to_string(&I2cControlRequest::RegisterAdapter { info: info.clone() })
|
|
+ .context("failed to encode I2C adapter registration")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to send I2C adapter registration")?;
|
|
+ Ok(file)
|
|
+}
|
|
+
|
|
+fn read_registration_response(file: &mut File) -> Result<I2cControlResponse> {
|
|
+ let mut buffer = vec![0_u8; 4096];
|
|
+ let count = file
|
|
+ .read(&mut buffer)
|
|
+ .context("failed to read I2C registration response")?;
|
|
+ buffer.truncate(count);
|
|
+ let text = std::str::from_utf8(&buffer).context("I2C registration response was not UTF-8")?;
|
|
+ ron::from_str(text).context("failed to decode I2C registration response")
|
|
+}
|
|
+
|
|
+fn eisa_id_from_integer(integer: u64) -> String {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1F) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+
|
|
+ format!(
|
|
+ "{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
|
|
+ )
|
|
+}
|
|
diff --git a/drivers/i2c/i2c-interface/Cargo.toml b/drivers/i2c/i2c-interface/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..fa9bbe4f
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/i2c-interface/Cargo.toml
|
|
@@ -0,0 +1,12 @@
|
|
+[package]
|
|
+name = "i2c-interface"
|
|
+description = "Shared I2C transfer and registry types"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+serde.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/i2c/i2c-interface/src/lib.rs b/drivers/i2c/i2c-interface/src/lib.rs
|
|
new file mode 100644
|
|
index 00000000..9e5ab444
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/i2c-interface/src/lib.rs
|
|
@@ -0,0 +1,92 @@
|
|
+use serde::{Deserialize, Serialize};
|
|
+
|
|
+pub use syscall;
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum I2cTransferOp {
|
|
+ Write(Vec<u8>),
|
|
+ Read(usize),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct I2cTransferSegment {
|
|
+ pub address: u16,
|
|
+ pub ten_bit_address: bool,
|
|
+ pub op: I2cTransferOp,
|
|
+}
|
|
+
|
|
+impl I2cTransferSegment {
|
|
+ pub fn write(address: u16, payload: impl Into<Vec<u8>>) -> Self {
|
|
+ Self {
|
|
+ address,
|
|
+ ten_bit_address: false,
|
|
+ op: I2cTransferOp::Write(payload.into()),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ pub fn read(address: u16, len: usize) -> Self {
|
|
+ Self {
|
|
+ address,
|
|
+ ten_bit_address: false,
|
|
+ op: I2cTransferOp::Read(len),
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct I2cTransferRequest {
|
|
+ pub adapter: String,
|
|
+ pub segments: Vec<I2cTransferSegment>,
|
|
+ pub stop: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct I2cTransferResponse {
|
|
+ pub ok: bool,
|
|
+ pub read_data: Vec<Vec<u8>>,
|
|
+ pub error: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
+pub struct I2cAdapterRegistration {
|
|
+ pub name: String,
|
|
+ pub description: String,
|
|
+ pub acpi_companion: Option<String>,
|
|
+ pub slave_address_override: Option<u16>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub struct I2cAdapterInfo {
|
|
+ pub id: u32,
|
|
+ pub name: String,
|
|
+ pub max_transaction_size: usize,
|
|
+ pub supports_10bit_addr: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum I2cTransferStatus {
|
|
+ Ok,
|
|
+ Nack,
|
|
+ Timeout,
|
|
+ Error,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum I2cControlRequest {
|
|
+ RegisterAdapter { info: I2cAdapterInfo },
|
|
+ OpenAdapter { id: u32 },
|
|
+ Transfer {
|
|
+ adapter_id: u32,
|
|
+ request: I2cTransferRequest,
|
|
+ },
|
|
+ ListAdapters,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
+pub enum I2cControlResponse {
|
|
+ AdapterRegistered { id: u32 },
|
|
+ AdapterOpened,
|
|
+ TransferResult(I2cTransferResponse),
|
|
+ AdapterList(Vec<I2cAdapterInfo>),
|
|
+ Error(String),
|
|
+}
|
|
diff --git a/drivers/i2c/i2cd/Cargo.toml b/drivers/i2c/i2cd/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..0fdd6911
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/i2cd/Cargo.toml
|
|
@@ -0,0 +1,22 @@
|
|
+[package]
|
|
+name = "i2cd"
|
|
+description = "I2C adapter registry scheme daemon"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+redox-scheme.workspace = true
|
|
+serde.workspace = true
|
|
+ron.workspace = true
|
|
+
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+scheme-utils = { path = "../../../scheme-utils" }
|
|
+i2c-interface = { path = "../i2c-interface" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/i2c/i2cd/src/main.rs b/drivers/i2c/i2cd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..b7b7d89a
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/i2cd/src/main.rs
|
|
@@ -0,0 +1,377 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::process;
|
|
+
|
|
+use anyhow::{Context, Result};
|
|
+use i2c_interface::{
|
|
+ I2cAdapterInfo, I2cControlRequest, I2cControlResponse, I2cTransferRequest,
|
|
+ I2cTransferResponse,
|
|
+};
|
|
+use redox_scheme::scheme::SchemeSync;
|
|
+use redox_scheme::{CallerCtx, OpenResult, Socket};
|
|
+use scheme_utils::{Blocking, HandleMap};
|
|
+use syscall::schemev2::NewFdFlags;
|
|
+use syscall::{Error as SysError, EACCES, EBADF, EINVAL, ENOENT};
|
|
+
|
|
+enum Handle {
|
|
+ SchemeRoot,
|
|
+ Register { pending: Vec<u8> },
|
|
+ Provider { adapter_id: u32, pending: Vec<u8> },
|
|
+ Adapters { pending: Vec<u8> },
|
|
+ AdapterDetail { id: u32, pending: Vec<u8> },
|
|
+ Transfer { pending: Vec<u8> },
|
|
+}
|
|
+
|
|
+struct AdapterEntry {
|
|
+ info: I2cAdapterInfo,
|
|
+ provider_handle: usize,
|
|
+}
|
|
+
|
|
+struct I2cDaemon {
|
|
+ handles: HandleMap<Handle>,
|
|
+ adapters: BTreeMap<u32, AdapterEntry>,
|
|
+ next_id: u32,
|
|
+}
|
|
+
|
|
+impl I2cDaemon {
|
|
+ fn new() -> Self {
|
|
+ Self {
|
|
+ handles: HandleMap::new(),
|
|
+ adapters: BTreeMap::new(),
|
|
+ next_id: 0,
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn adapter_list(&self) -> Vec<I2cAdapterInfo> {
|
|
+ self.adapters.values().map(|entry| entry.info.clone()).collect()
|
|
+ }
|
|
+
|
|
+ fn serialize_response(response: &I2cControlResponse) -> syscall::Result<Vec<u8>> {
|
|
+ ron::ser::to_string(response)
|
|
+ .map(|text| text.into_bytes())
|
|
+ .map_err(|err| {
|
|
+ log::error!("i2cd: failed to serialize control response: {err}");
|
|
+ SysError::new(EINVAL)
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn deserialize_request(buf: &[u8]) -> syscall::Result<I2cControlRequest> {
|
|
+ let text = std::str::from_utf8(buf).map_err(|err| {
|
|
+ log::warn!("i2cd: invalid UTF-8 control payload: {err}");
|
|
+ SysError::new(EINVAL)
|
|
+ })?;
|
|
+
|
|
+ ron::from_str(text).map_err(|err| {
|
|
+ log::warn!("i2cd: failed to decode control request: {err}");
|
|
+ SysError::new(EINVAL)
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn set_pending_response(handle: &mut Handle, response: I2cControlResponse) -> syscall::Result<()> {
|
|
+ let pending = Self::serialize_response(&response)?;
|
|
+ match handle {
|
|
+ Handle::Register { pending: slot }
|
|
+ | Handle::Provider { pending: slot, .. }
|
|
+ | Handle::Adapters { pending: slot }
|
|
+ | Handle::AdapterDetail { pending: slot, .. }
|
|
+ | Handle::Transfer { pending: slot } => {
|
|
+ *slot = pending;
|
|
+ Ok(())
|
|
+ }
|
|
+ Handle::SchemeRoot => Err(SysError::new(EBADF)),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn queue_adapter_list(handle: &mut Handle, adapters: Vec<I2cAdapterInfo>) -> syscall::Result<()> {
|
|
+ Self::set_pending_response(handle, I2cControlResponse::AdapterList(adapters))
|
|
+ }
|
|
+
|
|
+ fn queue_transfer_stub(
|
|
+ handle: &mut Handle,
|
|
+ adapter: &I2cAdapterInfo,
|
|
+ request: &I2cTransferRequest,
|
|
+ ) -> syscall::Result<()> {
|
|
+ let write_bytes = request
|
|
+ .segments
|
|
+ .iter()
|
|
+ .filter_map(|segment| match &segment.op {
|
|
+ i2c_interface::I2cTransferOp::Write(bytes) => Some(bytes.len()),
|
|
+ i2c_interface::I2cTransferOp::Read(_) => None,
|
|
+ })
|
|
+ .sum::<usize>();
|
|
+ let read_segments = request
|
|
+ .segments
|
|
+ .iter()
|
|
+ .filter(|segment| matches!(segment.op, i2c_interface::I2cTransferOp::Read(_)))
|
|
+ .count();
|
|
+
|
|
+ log::info!(
|
|
+ "i2cd: routing transfer to adapter {} ({}) name={} segments={} write_bytes={} read_segments={} stop={} (stubbed)",
|
|
+ adapter.id,
|
|
+ adapter.name,
|
|
+ request.adapter,
|
|
+ request.segments.len(),
|
|
+ write_bytes,
|
|
+ read_segments,
|
|
+ request.stop,
|
|
+ );
|
|
+
|
|
+ Self::set_pending_response(
|
|
+ handle,
|
|
+ I2cControlResponse::TransferResult(I2cTransferResponse {
|
|
+ ok: false,
|
|
+ read_data: Vec::new(),
|
|
+ error: Some(String::from("I2C controller transfer path is not implemented yet")),
|
|
+ }),
|
|
+ )
|
|
+ }
|
|
+
|
|
+ fn copy_pending(handle: &mut Handle, buf: &mut [u8], offset: u64) -> syscall::Result<usize> {
|
|
+ let pending = match handle {
|
|
+ Handle::Register { pending }
|
|
+ | Handle::Provider { pending, .. }
|
|
+ | Handle::Adapters { pending }
|
|
+ | Handle::AdapterDetail { pending, .. }
|
|
+ | Handle::Transfer { pending } => pending,
|
|
+ Handle::SchemeRoot => return Err(SysError::new(EBADF)),
|
|
+ };
|
|
+
|
|
+ let offset = usize::try_from(offset).map_err(|_| SysError::new(EINVAL))?;
|
|
+ if offset >= pending.len() {
|
|
+ return Ok(0);
|
|
+ }
|
|
+
|
|
+ let copy_len = buf.len().min(pending.len() - offset);
|
|
+ buf[..copy_len].copy_from_slice(&pending[offset..offset + copy_len]);
|
|
+ Ok(copy_len)
|
|
+ }
|
|
+}
|
|
+
|
|
+impl SchemeSync for I2cDaemon {
|
|
+ fn scheme_root(&mut self) -> syscall::Result<usize> {
|
|
+ Ok(self.handles.insert(Handle::SchemeRoot))
|
|
+ }
|
|
+
|
|
+ fn openat(
|
|
+ &mut self,
|
|
+ dirfd: usize,
|
|
+ path: &str,
|
|
+ _flags: usize,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<OpenResult> {
|
|
+ let handle = self.handles.get(dirfd)?;
|
|
+ let segments = path.trim_matches('/');
|
|
+
|
|
+ let new_handle = match handle {
|
|
+ Handle::SchemeRoot => {
|
|
+ if segments.is_empty() {
|
|
+ return Err(SysError::new(EINVAL));
|
|
+ }
|
|
+
|
|
+ let mut parts = segments.split('/');
|
|
+ match parts.next() {
|
|
+ Some("register") if parts.next().is_none() => Handle::Register {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ Some("adapters") => match parts.next() {
|
|
+ None => Handle::Adapters {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ Some(id) if parts.next().is_none() => {
|
|
+ let id = id.parse::<u32>().map_err(|_| SysError::new(EINVAL))?;
|
|
+ Handle::AdapterDetail {
|
|
+ id,
|
|
+ pending: Vec::new(),
|
|
+ }
|
|
+ }
|
|
+ _ => return Err(SysError::new(EINVAL)),
|
|
+ },
|
|
+ Some("transfer") if parts.next().is_none() => Handle::Transfer {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ _ => return Err(SysError::new(ENOENT)),
|
|
+ }
|
|
+ }
|
|
+ Handle::Adapters { .. } => {
|
|
+ if segments.is_empty() {
|
|
+ return Err(SysError::new(EINVAL));
|
|
+ }
|
|
+
|
|
+ let id = segments.parse::<u32>().map_err(|_| SysError::new(EINVAL))?;
|
|
+ Handle::AdapterDetail {
|
|
+ id,
|
|
+ pending: Vec::new(),
|
|
+ }
|
|
+ }
|
|
+ _ => return Err(SysError::new(EACCES)),
|
|
+ };
|
|
+
|
|
+ let fd = self.handles.insert(new_handle);
|
|
+ Ok(OpenResult::ThisScheme {
|
|
+ number: fd,
|
|
+ flags: NewFdFlags::empty(),
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn read(
|
|
+ &mut self,
|
|
+ id: usize,
|
|
+ buf: &mut [u8],
|
|
+ offset: u64,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<usize> {
|
|
+ let adapters = self.adapter_list();
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+
|
|
+ match handle {
|
|
+ Handle::Adapters { pending } if pending.is_empty() => {
|
|
+ *pending = Self::serialize_response(&I2cControlResponse::AdapterList(adapters))?;
|
|
+ }
|
|
+ Handle::AdapterDetail { id, pending } if pending.is_empty() => {
|
|
+ let info = self
|
|
+ .adapters
|
|
+ .get(id)
|
|
+ .map(|entry| entry.info.clone())
|
|
+ .ok_or(SysError::new(ENOENT))?;
|
|
+ *pending = Self::serialize_response(&I2cControlResponse::AdapterList(vec![info]))?;
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+
|
|
+ Self::copy_pending(handle, buf, offset)
|
|
+ }
|
|
+
|
|
+ fn write(
|
|
+ &mut self,
|
|
+ id: usize,
|
|
+ buf: &[u8],
|
|
+ _offset: u64,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<usize> {
|
|
+ let request = Self::deserialize_request(buf)?;
|
|
+
|
|
+ match request {
|
|
+ I2cControlRequest::RegisterAdapter { mut info } => {
|
|
+ let adapter_id = self.next_id;
|
|
+ self.next_id = self.next_id.checked_add(1).ok_or(SysError::new(EINVAL))?;
|
|
+ info.id = adapter_id;
|
|
+
|
|
+ self.adapters.insert(
|
|
+ adapter_id,
|
|
+ AdapterEntry {
|
|
+ info: info.clone(),
|
|
+ provider_handle: id,
|
|
+ },
|
|
+ );
|
|
+
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ *handle = Handle::Provider {
|
|
+ adapter_id,
|
|
+ pending: Self::serialize_response(&I2cControlResponse::AdapterRegistered {
|
|
+ id: adapter_id,
|
|
+ })?,
|
|
+ };
|
|
+
|
|
+ log::info!(
|
|
+ "RB_I2CD_ADAPTER_REGISTERED id={} name={} max_transaction_size={} supports_10bit_addr={}",
|
|
+ info.id,
|
|
+ info.name,
|
|
+ info.max_transaction_size,
|
|
+ info.supports_10bit_addr,
|
|
+ );
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ I2cControlRequest::ListAdapters => {
|
|
+ let adapters = self.adapter_list();
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ Self::queue_adapter_list(handle, adapters)?;
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ I2cControlRequest::OpenAdapter { id: adapter_id } => {
|
|
+ if !self.adapters.contains_key(&adapter_id) {
|
|
+ return Err(SysError::new(ENOENT));
|
|
+ }
|
|
+
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ match handle {
|
|
+ Handle::Adapters { .. } | Handle::AdapterDetail { .. } => {
|
|
+ Self::set_pending_response(handle, I2cControlResponse::AdapterOpened)?;
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ _ => Err(SysError::new(EINVAL)),
|
|
+ }
|
|
+ }
|
|
+ I2cControlRequest::Transfer {
|
|
+ adapter_id,
|
|
+ request,
|
|
+ } => {
|
|
+ let entry = self.adapters.get(&adapter_id).ok_or(SysError::new(ENOENT))?;
|
|
+ log::debug!(
|
|
+ "i2cd: transfer requested for adapter {} via provider fd {}",
|
|
+ adapter_id,
|
|
+ entry.provider_handle,
|
|
+ );
|
|
+
|
|
+ let adapter_info = entry.info.clone();
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ match handle {
|
|
+ Handle::Transfer { .. } => {
|
|
+ Self::queue_transfer_stub(handle, &adapter_info, &request)?;
|
|
+ Ok(buf.len())
|
|
+ }
|
|
+ _ => Err(SysError::new(EINVAL)),
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn on_close(&mut self, id: usize) {
|
|
+ let Some(handle) = self.handles.remove(id) else {
|
|
+ return;
|
|
+ };
|
|
+ if let Handle::Provider { adapter_id, .. } = handle {
|
|
+ self.adapters.remove(&adapter_id);
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+fn run_daemon(daemon: daemon::SchemeDaemon) -> Result<()> {
|
|
+ let socket = Socket::create().context("failed to create i2c scheme socket")?;
|
|
+ let mut scheme = I2cDaemon::new();
|
|
+ let handler = Blocking::new(&socket, 16);
|
|
+
|
|
+ daemon
|
|
+ .ready_sync_scheme(&socket, &mut scheme)
|
|
+ .context("failed to publish i2c scheme root")?;
|
|
+
|
|
+ log::info!("RB_I2CD_SCHEMA");
|
|
+
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ handler
|
|
+ .process_requests_blocking(scheme)
|
|
+ .context("failed to process i2cd requests")?;
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::SchemeDaemon) -> ! {
|
|
+ if let Err(err) = run_daemon(daemon) {
|
|
+ log::error!("i2cd: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn main() {
|
|
+ common::setup_logging(
|
|
+ "bus",
|
|
+ "i2c",
|
|
+ "i2cd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ daemon::SchemeDaemon::new(daemon_runner);
|
|
+}
|
|
diff --git a/drivers/i2c/intel-lpss-i2cd/Cargo.toml b/drivers/i2c/intel-lpss-i2cd/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..0e74cf94
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/intel-lpss-i2cd/Cargo.toml
|
|
@@ -0,0 +1,21 @@
|
|
+[package]
|
|
+name = "intel-lpss-i2cd"
|
|
+description = "Intel LPSS ACPI I2C controller driver"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+serde.workspace = true
|
|
+ron.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+i2c-interface = { path = "../i2c-interface" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/i2c/intel-lpss-i2cd/src/main.rs b/drivers/i2c/intel-lpss-i2cd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..ca3ead43
|
|
--- /dev/null
|
|
+++ b/drivers/i2c/intel-lpss-i2cd/src/main.rs
|
|
@@ -0,0 +1,361 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::fs::{self, File, OpenOptions};
|
|
+use std::io::{Read, Write};
|
|
+use std::path::Path;
|
|
+use std::process;
|
|
+
|
|
+use acpi_resource::{
|
|
+ AddressResourceType, ExtendedIrqDescriptor, FixedMemory32Descriptor, I2cSerialBusDescriptor,
|
|
+ IrqDescriptor, Memory32RangeDescriptor, ResourceDescriptor,
|
|
+};
|
|
+use anyhow::{Context, Result};
|
|
+use common::{MemoryType, PhysBorrowed, Prot};
|
|
+use i2c_interface::{I2cAdapterInfo, I2cControlRequest, I2cControlResponse};
|
|
+use serde::Deserialize;
|
|
+
|
|
+const SUPPORTED_IDS: &[&str] = &["INT33C2", "INT33C3", "INT3432", "INT3433", "INTC10EF"];
|
|
+
|
|
+const DW_IC_CON: usize = 0x00;
|
|
+const DW_IC_TAR: usize = 0x04;
|
|
+const DW_IC_SS_SCL_HCNT: usize = 0x14;
|
|
+const DW_IC_SS_SCL_LCNT: usize = 0x18;
|
|
+const DW_IC_DATA_CMD: usize = 0x10;
|
|
+const DW_IC_INTR_MASK: usize = 0x30;
|
|
+const DW_IC_CLR_INTR: usize = 0x40;
|
|
+const DW_IC_ENABLE: usize = 0x6C;
|
|
+const DW_IC_STATUS: usize = 0x70;
|
|
+const DW_MMIO_WINDOW: usize = DW_IC_STATUS + core::mem::size_of::<u32>();
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+struct AmlSymbol {
|
|
+ name: String,
|
|
+ value: AmlValue,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+enum AmlValue {
|
|
+ Integer(u64),
|
|
+ String(String),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+struct ControllerResources {
|
|
+ mmio_base: usize,
|
|
+ mmio_len: usize,
|
|
+ irq: Option<u32>,
|
|
+ serial_bus: Option<I2cSerialBusDescriptor>,
|
|
+}
|
|
+
|
|
+#[derive(Debug)]
|
|
+struct ControllerDescriptor {
|
|
+ device: String,
|
|
+ hid: String,
|
|
+ resources: ControllerResources,
|
|
+}
|
|
+
|
|
+struct RegisteredController {
|
|
+ _mmio: Option<PhysBorrowed>,
|
|
+ _registration: File,
|
|
+}
|
|
+
|
|
+fn main() {
|
|
+ common::setup_logging(
|
|
+ "bus",
|
|
+ "i2c",
|
|
+ "intel-lpss-i2cd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ daemon::Daemon::new(daemon_runner);
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::Daemon) -> ! {
|
|
+ if let Err(err) = daemon_main(daemon) {
|
|
+ log::error!("intel-lpss-i2cd: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn daemon_main(daemon: daemon::Daemon) -> Result<()> {
|
|
+ common::init();
|
|
+
|
|
+ let controllers = discover_controllers(SUPPORTED_IDS)
|
|
+ .context("failed to discover Intel LPSS ACPI I2C controllers")?;
|
|
+ if controllers.is_empty() {
|
|
+ log::info!("intel-lpss-i2cd: no supported ACPI controllers found");
|
|
+ }
|
|
+
|
|
+ let mut registered = Vec::new();
|
|
+ for controller in controllers {
|
|
+ match register_controller("intel-lpss", controller) {
|
|
+ Ok(controller) => registered.push(controller),
|
|
+ Err(err) => log::warn!("intel-lpss-i2cd: controller registration skipped: {err:#}"),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ daemon.ready();
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ log::info!("intel-lpss-i2cd: registered {} controller(s)", registered.len());
|
|
+
|
|
+ loop {
|
|
+ std::thread::park();
|
|
+ }
|
|
+}
|
|
+
|
|
+fn discover_controllers(supported_ids: &[&str]) -> Result<Vec<ControllerDescriptor>> {
|
|
+ let mut matched = BTreeMap::new();
|
|
+
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err)
|
|
+ if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) =>
|
|
+ {
|
|
+ log::debug!("intel-lpss-i2cd: ACPI symbols are not ready yet");
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to read ACPI symbol directory entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("_HID") && !file_name.ends_with("_CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let Some(id) = read_symbol_id(&entry.path())? else {
|
|
+ continue;
|
|
+ };
|
|
+ if !supported_ids.iter().any(|candidate| *candidate == id) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let device = file_name
|
|
+ .strip_suffix("_HID")
|
|
+ .or_else(|| file_name.strip_suffix("_CID"))
|
|
+ .map(str::to_owned);
|
|
+ if let Some(device) = device {
|
|
+ matched.entry(device).or_insert(id);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let mut controllers = Vec::new();
|
|
+ for (device, hid) in matched {
|
|
+ let resources = read_controller_resources(&device)
|
|
+ .with_context(|| format!("failed to read resources for {device}"))?;
|
|
+ controllers.push(ControllerDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ Ok(controllers)
|
|
+}
|
|
+
|
|
+fn read_symbol_id(path: &Path) -> Result<Option<String>> {
|
|
+ let contents = fs::read_to_string(path)
|
|
+ .with_context(|| format!("failed to read ACPI symbol {}", path.display()))?;
|
|
+ let symbol = match ron::from_str::<AmlSymbol>(&contents) {
|
|
+ Ok(symbol) => symbol,
|
|
+ Err(err) => {
|
|
+ log::debug!(
|
|
+ "intel-lpss-i2cd: skipping {} because the symbol payload was not a scalar ID: {err}",
|
|
+ path.display(),
|
|
+ );
|
|
+ return Ok(None);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let id = match symbol.value {
|
|
+ AmlValue::Integer(integer) => eisa_id_from_integer(integer),
|
|
+ AmlValue::String(string) => string,
|
|
+ };
|
|
+
|
|
+ log::debug!("intel-lpss-i2cd: {} -> {id}", symbol.name);
|
|
+ Ok(Some(id))
|
|
+}
|
|
+
|
|
+fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
|
+ let contents = fs::read_to_string(format!("/scheme/acpi/resources/{device}"))
|
|
+ .with_context(|| format!("failed to read /scheme/acpi/resources/{device}"))?;
|
|
+ let resources = ron::from_str::<Vec<ResourceDescriptor>>(&contents)
|
|
+ .with_context(|| format!("failed to decode RON resources for {device}"))?;
|
|
+
|
|
+ let mut mmio = None;
|
|
+ let mut irq = None;
|
|
+ let mut serial_bus = None;
|
|
+
|
|
+ for resource in &resources {
|
|
+ match resource {
|
|
+ ResourceDescriptor::FixedMemory32(FixedMemory32Descriptor {
|
|
+ address,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() => {
|
|
+ mmio = Some((*address as usize, (*address_length as usize).max(DW_MMIO_WINDOW)));
|
|
+ }
|
|
+ ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
|
|
+ minimum,
|
|
+ maximum,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() && maximum >= minimum => {
|
|
+ let span = maximum.saturating_sub(*minimum).saturating_add(1) as usize;
|
|
+ mmio = Some((
|
|
+ *minimum as usize,
|
|
+ span.max((*address_length as usize).max(DW_MMIO_WINDOW)),
|
|
+ ));
|
|
+ }
|
|
+ ResourceDescriptor::Address32(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ mmio = Some((
|
|
+ descriptor.minimum as usize,
|
|
+ (descriptor.address_length as usize).max(DW_MMIO_WINDOW),
|
|
+ ));
|
|
+ }
|
|
+ ResourceDescriptor::Address64(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ let base = usize::try_from(descriptor.minimum)
|
|
+ .context("64-bit MMIO base does not fit in usize")?;
|
|
+ let len = usize::try_from(descriptor.address_length)
|
|
+ .context("64-bit MMIO length does not fit in usize")?;
|
|
+ mmio = Some((base, len.max(DW_MMIO_WINDOW)));
|
|
+ }
|
|
+ ResourceDescriptor::Irq(IrqDescriptor { interrupts, .. }) if irq.is_none() => {
|
|
+ irq = interrupts.first().copied().map(u32::from);
|
|
+ }
|
|
+ ResourceDescriptor::ExtendedIrq(ExtendedIrqDescriptor { interrupts, .. })
|
|
+ if irq.is_none() =>
|
|
+ {
|
|
+ irq = interrupts.first().copied();
|
|
+ }
|
|
+ ResourceDescriptor::I2cSerialBus(descriptor) if serial_bus.is_none() => {
|
|
+ serial_bus = Some(descriptor.clone());
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let (mmio_base, mmio_len) = mmio.context("no MMIO resource was found")?;
|
|
+ Ok(ControllerResources {
|
|
+ mmio_base,
|
|
+ mmio_len,
|
|
+ irq,
|
|
+ serial_bus,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn register_controller(prefix: &str, controller: ControllerDescriptor) -> Result<RegisteredController> {
|
|
+ let ControllerDescriptor {
|
|
+ device,
|
|
+ hid,
|
|
+ resources,
|
|
+ } = controller;
|
|
+
|
|
+ let mmio = match PhysBorrowed::map(
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ Prot::RW,
|
|
+ MemoryType::Uncacheable,
|
|
+ ) {
|
|
+ Ok(mapping) => Some(mapping),
|
|
+ Err(err) => {
|
|
+ log::warn!(
|
|
+ "intel-lpss-i2cd: failed to map MMIO for {device} ({:#x}, len {:#x}): {err}",
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ );
|
|
+ None
|
|
+ }
|
|
+ };
|
|
+
|
|
+ log::info!(
|
|
+ "intel-lpss-i2cd: discovered {device} hid={hid} mmio={:#x}+{:#x} irq={:?}",
|
|
+ resources.mmio_base,
|
|
+ resources.mmio_len,
|
|
+ resources.irq,
|
|
+ );
|
|
+ log::debug!(
|
|
+ "intel-lpss-i2cd: DesignWare regs con={DW_IC_CON:#x} tar={DW_IC_TAR:#x} data_cmd={DW_IC_DATA_CMD:#x} intr_mask={DW_IC_INTR_MASK:#x} clr_intr={DW_IC_CLR_INTR:#x} enable={DW_IC_ENABLE:#x} ss_hcnt={DW_IC_SS_SCL_HCNT:#x} ss_lcnt={DW_IC_SS_SCL_LCNT:#x}",
|
|
+ );
|
|
+
|
|
+ let info = I2cAdapterInfo {
|
|
+ id: 0,
|
|
+ name: format!("{prefix}:{device}"),
|
|
+ max_transaction_size: 0,
|
|
+ supports_10bit_addr: resources
|
|
+ .serial_bus
|
|
+ .as_ref()
|
|
+ .map(|bus| bus.access_mode_10bit)
|
|
+ .unwrap_or(false),
|
|
+ };
|
|
+ let mut registration = register_adapter(&info)
|
|
+ .with_context(|| format!("failed to register {device} with i2cd"))?;
|
|
+ let response = read_registration_response(&mut registration)
|
|
+ .with_context(|| format!("failed to read i2cd registration response for {device}"))?;
|
|
+
|
|
+ match response {
|
|
+ I2cControlResponse::AdapterRegistered { id } => {
|
|
+ log::info!("intel-lpss-i2cd: adapter {device} registered with i2cd as {id}");
|
|
+ }
|
|
+ other => {
|
|
+ anyhow::bail!("unexpected i2cd registration response for {device}: {other:?}");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(RegisteredController {
|
|
+ _mmio: mmio,
|
|
+ _registration: registration,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn register_adapter(info: &I2cAdapterInfo) -> Result<File> {
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/register")
|
|
+ .context("failed to open /scheme/i2c/register")?;
|
|
+ let payload = ron::ser::to_string(&I2cControlRequest::RegisterAdapter { info: info.clone() })
|
|
+ .context("failed to encode I2C adapter registration")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to send I2C adapter registration")?;
|
|
+ Ok(file)
|
|
+}
|
|
+
|
|
+fn read_registration_response(file: &mut File) -> Result<I2cControlResponse> {
|
|
+ let mut buffer = vec![0_u8; 4096];
|
|
+ let count = file
|
|
+ .read(&mut buffer)
|
|
+ .context("failed to read I2C registration response")?;
|
|
+ buffer.truncate(count);
|
|
+ let text = std::str::from_utf8(&buffer).context("I2C registration response was not UTF-8")?;
|
|
+ ron::from_str(text).context("failed to decode I2C registration response")
|
|
+}
|
|
+
|
|
+fn eisa_id_from_integer(integer: u64) -> String {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1F) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+
|
|
+ format!(
|
|
+ "{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
|
|
+ )
|
|
+}
|
|
diff --git a/drivers/input/i2c-hidd/Cargo.toml b/drivers/input/i2c-hidd/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..db7b3f03
|
|
--- /dev/null
|
|
+++ b/drivers/input/i2c-hidd/Cargo.toml
|
|
@@ -0,0 +1,26 @@
|
|
+[package]
|
|
+name = "i2c-hidd"
|
|
+description = "I2C HID client daemon"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+orbclient.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+redox-scheme.workspace = true
|
|
+ron.workspace = true
|
|
+serde.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+amlserde = { path = "../../amlserde" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+i2c-interface = { path = "../../i2c/i2c-interface" }
|
|
+inputd = { path = "../../inputd" }
|
|
+scheme-utils = { path = "../../../scheme-utils" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/input/i2c-hidd/src/acpi.rs b/drivers/input/i2c-hidd/src/acpi.rs
|
|
new file mode 100644
|
|
index 00000000..1132154c
|
|
--- /dev/null
|
|
+++ b/drivers/input/i2c-hidd/src/acpi.rs
|
|
@@ -0,0 +1,307 @@
|
|
+use acpi_resource::{GpioDescriptor, ResourceDescriptor};
|
|
+use amlserde::{AmlSerde, AmlSerdeValue};
|
|
+use anyhow::{anyhow, bail, Context, Result};
|
|
+use libredox::flag::{O_CLOEXEC, O_RDWR};
|
|
+use std::collections::BTreeSet;
|
|
+use std::fs::{self, OpenOptions};
|
|
+use std::io::{ErrorKind, Read};
|
|
+
|
|
+use crate::quirks::ProbeFailureQuirk;
|
|
+
|
|
+const I2C_HID_DSM_GUID: [u8; 16] = [
|
|
+ 0xF7, 0xF6, 0xDF, 0x3C, 0x67, 0x42, 0x55, 0x45, 0xAD, 0x05, 0xB3, 0x0A, 0x3D, 0x89, 0x41, 0x76,
|
|
+];
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+pub struct I2cBinding {
|
|
+ pub adapter: String,
|
|
+ pub address: u16,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+pub struct AcpiDeviceResources {
|
|
+ pub i2c: I2cBinding,
|
|
+ pub irq: Option<u32>,
|
|
+ pub gpio_int: Vec<GpioDescriptor>,
|
|
+ pub gpio_io: Vec<GpioDescriptor>,
|
|
+}
|
|
+
|
|
+pub fn scan_acpi_i2c_hid_devices() -> Result<Vec<String>> {
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err) if err.kind() == ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => {
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+
|
|
+ let mut devices = BTreeSet::new();
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to enumerate ACPI symbol entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("._HID") && !file_name.ends_with("._CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let symbol = read_aml_symbol(&file_name)
|
|
+ .with_context(|| format!("failed to read ACPI symbol {file_name}"))?;
|
|
+ let Some(id) = decode_hardware_id(&symbol.value) else {
|
|
+ continue;
|
|
+ };
|
|
+
|
|
+ if matches!(id.as_str(), "PNP0C50" | "ACPI0C50") {
|
|
+ let device = symbol
|
|
+ .name
|
|
+ .strip_suffix("._HID")
|
|
+ .or_else(|| symbol.name.strip_suffix("._CID"))
|
|
+ .unwrap_or(&symbol.name)
|
|
+ .trim_start_matches('\\')
|
|
+ .trim_matches('/')
|
|
+ .replace('/', ".");
|
|
+ if !device.is_empty() {
|
|
+ devices.insert(device);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(devices.into_iter().collect())
|
|
+}
|
|
+
|
|
+pub fn read_decoded_resources(path: &str) -> Result<AcpiDeviceResources> {
|
|
+ let resource_path = format!("/scheme/acpi/resources/{}", normalize_device_path(path));
|
|
+ let serialized = fs::read_to_string(&resource_path)
|
|
+ .with_context(|| format!("failed to read {resource_path}"))?;
|
|
+ let descriptors: Vec<ResourceDescriptor> = ron::from_str(&serialized)
|
|
+ .with_context(|| format!("invalid ACPI resources in {resource_path}"))?;
|
|
+
|
|
+ let mut i2c = None;
|
|
+ let mut irq = None;
|
|
+ let mut gpio_int = Vec::new();
|
|
+ let mut gpio_io = Vec::new();
|
|
+
|
|
+ for descriptor in descriptors {
|
|
+ match descriptor {
|
|
+ ResourceDescriptor::I2cSerialBus(bus) => {
|
|
+ if i2c.is_none() {
|
|
+ let adapter = bus
|
|
+ .resource_source
|
|
+ .as_ref()
|
|
+ .map(|source| source.source.clone())
|
|
+ .filter(|source| !source.is_empty())
|
|
+ .unwrap_or_else(|| "ACPI-I2C".to_string());
|
|
+ i2c = Some(I2cBinding {
|
|
+ adapter,
|
|
+ address: bus.slave_address,
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+ ResourceDescriptor::Irq(descriptor) => {
|
|
+ if irq.is_none() {
|
|
+ irq = descriptor.interrupts.first().copied().map(u32::from);
|
|
+ }
|
|
+ }
|
|
+ ResourceDescriptor::ExtendedIrq(descriptor) => {
|
|
+ if irq.is_none() {
|
|
+ irq = descriptor.interrupts.first().copied();
|
|
+ }
|
|
+ }
|
|
+ ResourceDescriptor::GpioInt(descriptor) => gpio_int.push(descriptor),
|
|
+ ResourceDescriptor::GpioIo(descriptor) => gpio_io.push(descriptor),
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let mut resources = AcpiDeviceResources {
|
|
+ i2c: i2c.ok_or_else(|| anyhow!("no I2cSerialBus resource in _CRS"))?,
|
|
+ irq,
|
|
+ gpio_int,
|
|
+ gpio_io,
|
|
+ };
|
|
+
|
|
+ if let Some(override_address) = companion_icrs_override(path)? {
|
|
+ log::info!(
|
|
+ "{}: applying THC companion ICRS override {:04x} -> {:04x}",
|
|
+ path,
|
|
+ resources.i2c.address,
|
|
+ override_address
|
|
+ );
|
|
+ resources.i2c.address = override_address;
|
|
+ }
|
|
+
|
|
+ Ok(resources)
|
|
+}
|
|
+
|
|
+pub fn prepare_acpi_device(path: &str) -> Result<()> {
|
|
+ let sta = evaluate_integer_method(path, "_STA").ok();
|
|
+ if let Some(sta) = sta {
|
|
+ if sta & 0x01 == 0 {
|
|
+ bail!("ACPI device is not present according to _STA={sta:#x}");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let _ = evaluate_method(path, "_PS0", &[]);
|
|
+ let _ = evaluate_method(path, "_INI", &[]);
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+pub fn recover_acpi_device(
|
|
+ path: &str,
|
|
+ resources: &AcpiDeviceResources,
|
|
+ quirk: Option<&ProbeFailureQuirk>,
|
|
+) -> Result<()> {
|
|
+ let _ = evaluate_method(path, "_PS3", &[]);
|
|
+
|
|
+ if let Some(quirk) = quirk {
|
|
+ if !resources.gpio_io.is_empty() {
|
|
+ log::warn!(
|
|
+ "{}: applying GPIO probe-failure recovery quirk {} vendor={:?} product={:?} board={:?} across {} GPIO IO resources",
|
|
+ path,
|
|
+ quirk.name,
|
|
+ quirk.system_vendor,
|
|
+ quirk.product_name,
|
|
+ quirk.board_name,
|
|
+ resources.gpio_io.len()
|
|
+ );
|
|
+ } else {
|
|
+ log::warn!(
|
|
+ "{}: quirk {} vendor={:?} product={:?} board={:?} matched but no GPIO IO resource was exposed",
|
|
+ path,
|
|
+ quirk.name
|
|
+ ,
|
|
+ quirk.system_vendor,
|
|
+ quirk.product_name,
|
|
+ quirk.board_name
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let _ = evaluate_method(path, "_PS0", &[]);
|
|
+ let _ = evaluate_method(path, "_INI", &[]);
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+pub fn hid_descriptor_address(path: &str) -> Result<u16> {
|
|
+ let args = [
|
|
+ AmlSerdeValue::Buffer(I2C_HID_DSM_GUID.to_vec()),
|
|
+ AmlSerdeValue::Integer(1),
|
|
+ AmlSerdeValue::Integer(1),
|
|
+ AmlSerdeValue::Package {
|
|
+ contents: Vec::new(),
|
|
+ },
|
|
+ ];
|
|
+
|
|
+ match evaluate_method(path, "_DSM", &args) {
|
|
+ Ok(AmlSerdeValue::Integer(value)) => {
|
|
+ return u16::try_from(value).context("_DSM descriptor address out of range")
|
|
+ }
|
|
+ Ok(other) => log::warn!(
|
|
+ "{}._DSM returned unexpected value {:?}; retrying fallback index",
|
|
+ path,
|
|
+ other
|
|
+ ),
|
|
+ Err(err) => log::warn!(
|
|
+ "{}._DSM function 1 failed: {err}; retrying function 0",
|
|
+ path
|
|
+ ),
|
|
+ }
|
|
+
|
|
+ let fallback = [
|
|
+ AmlSerdeValue::Buffer(I2C_HID_DSM_GUID.to_vec()),
|
|
+ AmlSerdeValue::Integer(1),
|
|
+ AmlSerdeValue::Integer(0),
|
|
+ AmlSerdeValue::Package {
|
|
+ contents: Vec::new(),
|
|
+ },
|
|
+ ];
|
|
+
|
|
+ match evaluate_method(path, "_DSM", &fallback)? {
|
|
+ AmlSerdeValue::Integer(value) => {
|
|
+ u16::try_from(value).context("fallback _DSM descriptor address out of range")
|
|
+ }
|
|
+ other => bail!("fallback _DSM returned unexpected value {other:?}"),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn companion_icrs_override(path: &str) -> Result<Option<u16>> {
|
|
+ let value = match evaluate_integer_method(path, "ICRS") {
|
|
+ Ok(value) => value,
|
|
+ Err(_) => return Ok(None),
|
|
+ };
|
|
+ Ok(Some(
|
|
+ u16::try_from(value).context("ICRS override out of range")?,
|
|
+ ))
|
|
+}
|
|
+
|
|
+pub fn evaluate_integer_method(path: &str, method: &str) -> Result<u64> {
|
|
+ match evaluate_method(path, method, &[])? {
|
|
+ AmlSerdeValue::Integer(value) => Ok(value),
|
|
+ other => bail!(
|
|
+ "{}.{} returned non-integer AML value {other:?}",
|
|
+ path,
|
|
+ method
|
|
+ ),
|
|
+ }
|
|
+}
|
|
+
|
|
+pub fn evaluate_method(path: &str, method: &str, args: &[AmlSerdeValue]) -> Result<AmlSerdeValue> {
|
|
+ let symbol_name = format!("{}.{}", normalize_device_path(path), method);
|
|
+ let symbol_path = format!("/scheme/acpi/symbols/{symbol_name}");
|
|
+ let fd = libredox::Fd::open(&symbol_path, O_RDWR | O_CLOEXEC, 0)
|
|
+ .with_context(|| format!("failed to open {symbol_path} for ACPI evaluation"))?;
|
|
+
|
|
+ let serialized = ron::to_string(args)
|
|
+ .with_context(|| format!("failed to serialize ACPI arguments for {symbol_name}"))?;
|
|
+ let mut payload = serialized.into_bytes();
|
|
+ payload.resize(payload.len() + 4096, 0);
|
|
+
|
|
+ let used = libredox::call::call_ro(fd.raw(), &mut payload, syscall::CallFlags::empty(), &[])
|
|
+ .with_context(|| format!("ACPI evaluation failed for {symbol_name}"))?;
|
|
+ let response = std::str::from_utf8(&payload[..used])
|
|
+ .with_context(|| format!("invalid UTF-8 ACPI response for {symbol_name}"))?;
|
|
+ ron::from_str(response)
|
|
+ .with_context(|| format!("failed to decode ACPI response for {symbol_name}"))
|
|
+}
|
|
+
|
|
+fn read_aml_symbol(file_name: &str) -> Result<AmlSerde> {
|
|
+ let path = format!("/scheme/acpi/symbols/{file_name}");
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .open(&path)
|
|
+ .with_context(|| format!("failed to open {path}"))?;
|
|
+ let mut ron_text = String::new();
|
|
+ file.read_to_string(&mut ron_text)
|
|
+ .with_context(|| format!("failed to read {path}"))?;
|
|
+ ron::from_str(&ron_text).with_context(|| format!("failed to decode {path}"))
|
|
+}
|
|
+
|
|
+fn decode_hardware_id(value: &AmlSerdeValue) -> Option<String> {
|
|
+ match value {
|
|
+ AmlSerdeValue::String(value) => Some(value.clone()),
|
|
+ AmlSerdeValue::Integer(integer) => {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1f) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1f) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1f) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+
|
|
+ Some(format!(
|
|
+ "{}{}{}{:01X}{:01X}{:01X}{:01X}",
|
|
+ vendor_1, vendor_2, vendor_3, device_1, device_2, device_3, device_4
|
|
+ ))
|
|
+ }
|
|
+ _ => None,
|
|
+ }
|
|
+}
|
|
+
|
|
+pub fn normalize_device_path(path: &str) -> String {
|
|
+ path.trim_start_matches('\\')
|
|
+ .trim_matches('/')
|
|
+ .replace('/', ".")
|
|
+}
|
|
diff --git a/drivers/input/i2c-hidd/src/hid.rs b/drivers/input/i2c-hidd/src/hid.rs
|
|
new file mode 100644
|
|
index 00000000..dd18e36c
|
|
--- /dev/null
|
|
+++ b/drivers/input/i2c-hidd/src/hid.rs
|
|
@@ -0,0 +1,195 @@
|
|
+use std::fs::OpenOptions;
|
|
+use std::io::{Read, Write};
|
|
+
|
|
+use anyhow::{bail, Context, Result};
|
|
+use i2c_interface::{I2cTransferRequest, I2cTransferResponse, I2cTransferSegment};
|
|
+use serde::{Deserialize, Serialize};
|
|
+
|
|
+use crate::acpi::I2cBinding;
|
|
+
|
|
+#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
+pub struct HidDescriptor {
|
|
+ pub hid_desc_length: u16,
|
|
+ pub bcd_version: u16,
|
|
+ pub report_desc_length: u16,
|
|
+ pub report_desc_register: u16,
|
|
+ pub input_register: u16,
|
|
+ pub max_input_length: u16,
|
|
+ pub output_register: u16,
|
|
+ pub max_output_length: u16,
|
|
+ pub command_register: u16,
|
|
+ pub data_register: u16,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Default)]
|
|
+pub struct ReportDescriptorSummary {
|
|
+ pub has_keyboard_page: bool,
|
|
+ pub has_pointer_page: bool,
|
|
+ pub report_ids: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+pub struct I2cAdapterClient {
|
|
+ binding: I2cBinding,
|
|
+}
|
|
+
|
|
+impl I2cAdapterClient {
|
|
+ pub fn new(binding: I2cBinding) -> Self {
|
|
+ Self { binding }
|
|
+ }
|
|
+ pub fn transfer(&self, segments: Vec<I2cTransferSegment>) -> Result<I2cTransferResponse> {
|
|
+ let request = I2cTransferRequest {
|
|
+ adapter: self.binding.adapter.clone(),
|
|
+ segments,
|
|
+ stop: true,
|
|
+ };
|
|
+
|
|
+ let serialized = ron::to_string(&request).context("failed to serialize I2C request")?;
|
|
+ let mut handle = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/transfer")
|
|
+ .context("failed to open /scheme/i2c/transfer")?;
|
|
+ handle
|
|
+ .write_all(serialized.as_bytes())
|
|
+ .context("failed to write I2C transfer request")?;
|
|
+
|
|
+ let mut response = String::new();
|
|
+ handle
|
|
+ .read_to_string(&mut response)
|
|
+ .context("failed to read I2C transfer response")?;
|
|
+
|
|
+ let transfer: I2cTransferResponse =
|
|
+ ron::from_str(&response).context("failed to decode I2C transfer response")?;
|
|
+ if !transfer.ok {
|
|
+ bail!(
|
|
+ "I2C transfer failed: {}",
|
|
+ transfer
|
|
+ .error
|
|
+ .unwrap_or_else(|| "unspecified transfer error".to_string())
|
|
+ );
|
|
+ }
|
|
+ Ok(transfer)
|
|
+ }
|
|
+
|
|
+ pub fn write_read(&self, address: u16, write_data: &[u8], read_len: usize) -> Result<Vec<u8>> {
|
|
+ let response = self.transfer(vec![
|
|
+ I2cTransferSegment::write(address, write_data.to_vec()),
|
|
+ I2cTransferSegment::read(address, read_len),
|
|
+ ])?;
|
|
+
|
|
+ response
|
|
+ .read_data
|
|
+ .last()
|
|
+ .cloned()
|
|
+ .ok_or_else(|| anyhow::anyhow!("I2C transfer returned no readable segment payload"))
|
|
+ }
|
|
+}
|
|
+
|
|
+pub fn fetch_hid_descriptor(
|
|
+ adapter: &I2cAdapterClient,
|
|
+ address: u16,
|
|
+ hid_desc_addr: u16,
|
|
+) -> Result<HidDescriptor> {
|
|
+ let prefix = adapter
|
|
+ .write_read(address, &hid_desc_addr.to_le_bytes(), 2)
|
|
+ .context("failed to read HID descriptor length prefix")?;
|
|
+ if prefix.len() < 2 {
|
|
+ bail!("short HID descriptor prefix: {} bytes", prefix.len());
|
|
+ }
|
|
+
|
|
+ let hid_desc_length = u16::from_le_bytes([prefix[0], prefix[1]]);
|
|
+ if hid_desc_length < 18 {
|
|
+ bail!("invalid HID descriptor length {hid_desc_length}");
|
|
+ }
|
|
+
|
|
+ let raw = adapter
|
|
+ .write_read(
|
|
+ address,
|
|
+ &hid_desc_addr.to_le_bytes(),
|
|
+ usize::from(hid_desc_length),
|
|
+ )
|
|
+ .context("failed to read full HID descriptor")?;
|
|
+ parse_hid_descriptor(&raw)
|
|
+}
|
|
+
|
|
+pub fn fetch_report_descriptor(
|
|
+ adapter: &I2cAdapterClient,
|
|
+ address: u16,
|
|
+ desc: &HidDescriptor,
|
|
+) -> Result<Vec<u8>> {
|
|
+ adapter
|
|
+ .write_read(
|
|
+ address,
|
|
+ &desc.report_desc_register.to_le_bytes(),
|
|
+ usize::from(desc.report_desc_length),
|
|
+ )
|
|
+ .context("failed to read HID report descriptor")
|
|
+}
|
|
+
|
|
+pub fn stream_input_reports(
|
|
+ adapter: &I2cAdapterClient,
|
|
+ address: u16,
|
|
+ desc: &HidDescriptor,
|
|
+ report_desc: &[u8],
|
|
+ sink: &mut crate::input::InputForwarder,
|
|
+) -> Result<()> {
|
|
+ let summary = summarize_report_descriptor(report_desc);
|
|
+ let input_len = usize::from(desc.max_input_length.max(4));
|
|
+
|
|
+ loop {
|
|
+ let report = adapter
|
|
+ .write_read(address, &desc.input_register.to_le_bytes(), input_len)
|
|
+ .context("failed to fetch I2C HID input report")?;
|
|
+ sink.forward_report(&summary, &report)?;
|
|
+ }
|
|
+}
|
|
+
|
|
+fn parse_hid_descriptor(bytes: &[u8]) -> Result<HidDescriptor> {
|
|
+ if bytes.len() < 18 {
|
|
+ bail!("short HID descriptor: {} bytes", bytes.len());
|
|
+ }
|
|
+
|
|
+ Ok(HidDescriptor {
|
|
+ hid_desc_length: le16(bytes, 0)?,
|
|
+ bcd_version: le16(bytes, 2)?,
|
|
+ report_desc_length: le16(bytes, 4)?,
|
|
+ report_desc_register: le16(bytes, 6)?,
|
|
+ input_register: le16(bytes, 8)?,
|
|
+ max_input_length: le16(bytes, 10)?,
|
|
+ output_register: le16(bytes, 12)?,
|
|
+ max_output_length: le16(bytes, 14)?,
|
|
+ command_register: le16(bytes, 16)?,
|
|
+ data_register: if bytes.len() >= 20 {
|
|
+ le16(bytes, 18)?
|
|
+ } else {
|
|
+ 0
|
|
+ },
|
|
+ })
|
|
+}
|
|
+
|
|
+fn summarize_report_descriptor(report_desc: &[u8]) -> ReportDescriptorSummary {
|
|
+ let mut summary = ReportDescriptorSummary::default();
|
|
+
|
|
+ for window in report_desc.windows(2) {
|
|
+ match window {
|
|
+ [0x05, 0x01] => summary.has_pointer_page = true,
|
|
+ [0x05, 0x07] => summary.has_keyboard_page = true,
|
|
+ [0x85, _] => summary.report_ids = true,
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if !summary.has_keyboard_page && !summary.has_pointer_page {
|
|
+ summary.has_pointer_page = true;
|
|
+ }
|
|
+
|
|
+ summary
|
|
+}
|
|
+
|
|
+fn le16(bytes: &[u8], offset: usize) -> Result<u16> {
|
|
+ let slice = bytes
|
|
+ .get(offset..offset + 2)
|
|
+ .ok_or_else(|| anyhow::anyhow!("short LE16 field at offset {offset}"))?;
|
|
+ Ok(u16::from_le_bytes([slice[0], slice[1]]))
|
|
+}
|
|
diff --git a/drivers/input/i2c-hidd/src/input.rs b/drivers/input/i2c-hidd/src/input.rs
|
|
new file mode 100644
|
|
index 00000000..432a0782
|
|
--- /dev/null
|
|
+++ b/drivers/input/i2c-hidd/src/input.rs
|
|
@@ -0,0 +1,175 @@
|
|
+use std::collections::BTreeSet;
|
|
+
|
|
+use anyhow::Result;
|
|
+use inputd::ProducerHandle;
|
|
+use orbclient::{
|
|
+ ButtonEvent, KeyEvent, MouseRelativeEvent, ScrollEvent, K_ALT, K_ALT_GR, K_BKSP, K_BRACE_CLOSE,
|
|
+ K_BRACE_OPEN, K_CAPS, K_COMMA, K_ENTER, K_EQUALS, K_ESC, K_LEFT_CTRL, K_LEFT_SHIFT,
|
|
+ K_LEFT_SUPER, K_MINUS, K_PERIOD, K_QUOTE, K_RIGHT_CTRL, K_RIGHT_SHIFT, K_RIGHT_SUPER,
|
|
+ K_SEMICOLON, K_SLASH, K_SPACE, K_TAB, K_TICK,
|
|
+};
|
|
+
|
|
+use crate::hid::ReportDescriptorSummary;
|
|
+
|
|
+pub struct InputForwarder {
|
|
+ producer: ProducerHandle,
|
|
+ keyboard_state: BTreeSet<u8>,
|
|
+ last_buttons: u8,
|
|
+}
|
|
+
|
|
+impl InputForwarder {
|
|
+ pub fn new() -> Result<Self> {
|
|
+ Ok(Self {
|
|
+ producer: ProducerHandle::new()?,
|
|
+ keyboard_state: BTreeSet::new(),
|
|
+ last_buttons: 0,
|
|
+ })
|
|
+ }
|
|
+
|
|
+ pub fn forward_report(
|
|
+ &mut self,
|
|
+ summary: &ReportDescriptorSummary,
|
|
+ report: &[u8],
|
|
+ ) -> Result<()> {
|
|
+ if report.is_empty() {
|
|
+ return Ok(());
|
|
+ }
|
|
+
|
|
+ if summary.has_keyboard_page && report.len() >= 8 {
|
|
+ self.forward_boot_keyboard(report)?;
|
|
+ return Ok(());
|
|
+ }
|
|
+
|
|
+ if summary.has_pointer_page && report.len() >= 3 {
|
|
+ self.forward_boot_pointer(report)?;
|
|
+ return Ok(());
|
|
+ }
|
|
+
|
|
+ Ok(())
|
|
+ }
|
|
+
|
|
+ fn forward_boot_keyboard(&mut self, report: &[u8]) -> Result<()> {
|
|
+ let modifiers = report[0];
|
|
+ for (bit, scancode) in [
|
|
+ (0_u8, K_LEFT_CTRL),
|
|
+ (1, K_LEFT_SHIFT),
|
|
+ (2, K_ALT),
|
|
+ (3, K_LEFT_SUPER),
|
|
+ (4, K_RIGHT_CTRL),
|
|
+ (5, K_RIGHT_SHIFT),
|
|
+ (6, K_ALT_GR),
|
|
+ (7, K_RIGHT_SUPER),
|
|
+ ] {
|
|
+ self.producer.write_event(
|
|
+ KeyEvent {
|
|
+ character: '\0',
|
|
+ scancode,
|
|
+ pressed: modifiers & (1 << bit) != 0,
|
|
+ }
|
|
+ .to_event(),
|
|
+ )?;
|
|
+ }
|
|
+
|
|
+ let current = report[2..8]
|
|
+ .iter()
|
|
+ .copied()
|
|
+ .filter(|code| *code != 0)
|
|
+ .collect::<BTreeSet<_>>();
|
|
+
|
|
+ for code in current.difference(&self.keyboard_state) {
|
|
+ if let Some(scancode) = map_boot_keyboard_usage(*code) {
|
|
+ self.producer.write_event(
|
|
+ KeyEvent {
|
|
+ character: '\0',
|
|
+ scancode,
|
|
+ pressed: true,
|
|
+ }
|
|
+ .to_event(),
|
|
+ )?;
|
|
+ }
|
|
+ }
|
|
+ for code in self.keyboard_state.difference(¤t) {
|
|
+ if let Some(scancode) = map_boot_keyboard_usage(*code) {
|
|
+ self.producer.write_event(
|
|
+ KeyEvent {
|
|
+ character: '\0',
|
|
+ scancode,
|
|
+ pressed: false,
|
|
+ }
|
|
+ .to_event(),
|
|
+ )?;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ self.keyboard_state = current;
|
|
+ Ok(())
|
|
+ }
|
|
+
|
|
+ fn forward_boot_pointer(&mut self, report: &[u8]) -> Result<()> {
|
|
+ let dx = i8::from_ne_bytes([report[1]]) as i32;
|
|
+ let dy = i8::from_ne_bytes([report[2]]) as i32;
|
|
+ if dx != 0 || dy != 0 {
|
|
+ self.producer
|
|
+ .write_event(MouseRelativeEvent { dx, dy }.to_event())?;
|
|
+ }
|
|
+
|
|
+ if let Some(scroll) = report.get(3).copied() {
|
|
+ let scroll = i8::from_ne_bytes([scroll]) as i32;
|
|
+ if scroll != 0 {
|
|
+ self.producer
|
|
+ .write_event(ScrollEvent { x: 0, y: scroll }.to_event())?;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ let buttons = report[0] & 0x07;
|
|
+ for index in 0..3 {
|
|
+ let mask = 1 << index;
|
|
+ if (buttons & mask) != (self.last_buttons & mask) {
|
|
+ self.producer.write_event(
|
|
+ ButtonEvent {
|
|
+ left: buttons & 0x01 != 0,
|
|
+ middle: buttons & 0x04 != 0,
|
|
+ right: buttons & 0x02 != 0,
|
|
+ }
|
|
+ .to_event(),
|
|
+ )?;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ self.last_buttons = buttons;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+fn map_boot_keyboard_usage(usage: u8) -> Option<u8> {
|
|
+ Some(match usage {
|
|
+ 0x04..=0x1D => b'a' + (usage - 0x04),
|
|
+ 0x1E => b'1',
|
|
+ 0x1F => b'2',
|
|
+ 0x20 => b'3',
|
|
+ 0x21 => b'4',
|
|
+ 0x22 => b'5',
|
|
+ 0x23 => b'6',
|
|
+ 0x24 => b'7',
|
|
+ 0x25 => b'8',
|
|
+ 0x26 => b'9',
|
|
+ 0x27 => b'0',
|
|
+ 0x28 => K_ENTER,
|
|
+ 0x29 => K_ESC,
|
|
+ 0x2A => K_BKSP,
|
|
+ 0x2B => K_TAB,
|
|
+ 0x2C => K_SPACE,
|
|
+ 0x2D => K_MINUS,
|
|
+ 0x2E => K_EQUALS,
|
|
+ 0x2F => K_BRACE_OPEN,
|
|
+ 0x30 => K_BRACE_CLOSE,
|
|
+ 0x33 => K_SEMICOLON,
|
|
+ 0x34 => K_QUOTE,
|
|
+ 0x35 => K_TICK,
|
|
+ 0x36 => K_COMMA,
|
|
+ 0x37 => K_PERIOD,
|
|
+ 0x38 => K_SLASH,
|
|
+ 0x39 => K_CAPS,
|
|
+ _ => return None,
|
|
+ })
|
|
+}
|
|
diff --git a/drivers/input/i2c-hidd/src/main.rs b/drivers/input/i2c-hidd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..88270e37
|
|
--- /dev/null
|
|
+++ b/drivers/input/i2c-hidd/src/main.rs
|
|
@@ -0,0 +1,114 @@
|
|
+use std::process;
|
|
+use std::thread;
|
|
+use std::time::Duration;
|
|
+
|
|
+use anyhow::{Context, Result};
|
|
+
|
|
+mod acpi;
|
|
+mod hid;
|
|
+mod input;
|
|
+mod quirks;
|
|
+
|
|
+use acpi::{
|
|
+ hid_descriptor_address, prepare_acpi_device, read_decoded_resources, recover_acpi_device,
|
|
+ scan_acpi_i2c_hid_devices,
|
|
+};
|
|
+use hid::{fetch_hid_descriptor, fetch_report_descriptor, stream_input_reports, I2cAdapterClient};
|
|
+use input::InputForwarder;
|
|
+use quirks::match_probe_failure_quirk;
|
|
+
|
|
+fn main() {
|
|
+ daemon::Daemon::new(daemon);
|
|
+}
|
|
+
|
|
+fn daemon(daemon: daemon::Daemon) -> ! {
|
|
+ common::setup_logging(
|
|
+ "input",
|
|
+ "i2c-hid",
|
|
+ "i2c-hidd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ if let Err(err) = run(daemon) {
|
|
+ log::error!("RB_I2C_HIDD_BLOCKER stage=startup error={err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn run(daemon: daemon::Daemon) -> Result<()> {
|
|
+ log::info!("RB_I2C_HIDD_SCHEMA version=1");
|
|
+
|
|
+ let devices = scan_acpi_i2c_hid_devices().context("failed to scan ACPI I2C HID devices")?;
|
|
+ if devices.is_empty() {
|
|
+ log::warn!("RB_I2C_HIDD_BLOCKER stage=scan error=no PNP0C50/ACPI0C50 devices found");
|
|
+ }
|
|
+
|
|
+ let mut workers = Vec::new();
|
|
+ for device in devices {
|
|
+ log::info!("RB_I2C_HIDD_SNAPSHOT device={device}");
|
|
+ workers.push(thread::spawn(move || {
|
|
+ if let Err(err) = bind_device(&device) {
|
|
+ log::error!("RB_I2C_HIDD_BLOCKER device={} error={:#}", device, err);
|
|
+ }
|
|
+ }));
|
|
+ }
|
|
+
|
|
+ daemon.ready();
|
|
+
|
|
+ if workers.is_empty() {
|
|
+ loop {
|
|
+ thread::sleep(Duration::from_secs(5));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for worker in workers {
|
|
+ let _ = worker.join();
|
|
+ }
|
|
+ Ok(())
|
|
+}
|
|
+
|
|
+pub fn bind_device(device_path: &str) -> Result<()> {
|
|
+ prepare_acpi_device(device_path)
|
|
+ .with_context(|| format!("failed to prepare ACPI device {device_path}"))?;
|
|
+
|
|
+ let resources = read_decoded_resources(device_path)
|
|
+ .with_context(|| format!("failed to decode _CRS for {device_path}"))?;
|
|
+ log::info!(
|
|
+ "RB_I2C_HIDD_SNAPSHOT device={} adapter={} addr={:04x} irq={:?} gpio_int={} gpio_io={}",
|
|
+ device_path,
|
|
+ resources.i2c.adapter,
|
|
+ resources.i2c.address,
|
|
+ resources.irq,
|
|
+ resources.gpio_int.len(),
|
|
+ resources.gpio_io.len()
|
|
+ );
|
|
+
|
|
+ let hid_desc_addr = hid_descriptor_address(device_path)
|
|
+ .with_context(|| format!("failed to evaluate _DSM for {device_path}"))?;
|
|
+ let adapter = I2cAdapterClient::new(resources.i2c.clone());
|
|
+ let hid_desc = fetch_hid_descriptor(&adapter, resources.i2c.address, hid_desc_addr)
|
|
+ .with_context(|| format!("failed to fetch HID descriptor for {device_path}"))?;
|
|
+ let report_desc = fetch_report_descriptor(&adapter, resources.i2c.address, &hid_desc)
|
|
+ .with_context(|| format!("failed to fetch report descriptor for {device_path}"))?;
|
|
+ let mut forwarder = InputForwarder::new().context("failed to connect to inputd producer")?;
|
|
+
|
|
+ match stream_input_reports(
|
|
+ &adapter,
|
|
+ resources.i2c.address,
|
|
+ &hid_desc,
|
|
+ &report_desc,
|
|
+ &mut forwarder,
|
|
+ ) {
|
|
+ Ok(()) => Ok(()),
|
|
+ Err(err) => {
|
|
+ let quirk =
|
|
+ match_probe_failure_quirk().context("failed to evaluate DMI recovery quirks")?;
|
|
+ recover_acpi_device(device_path, &resources, quirk.as_ref())
|
|
+ .with_context(|| format!("failed ACPI recovery for {device_path}"))?;
|
|
+ Err(err).with_context(|| format!("streaming input reports failed for {device_path}"))
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/drivers/input/i2c-hidd/src/quirks.rs b/drivers/input/i2c-hidd/src/quirks.rs
|
|
new file mode 100644
|
|
index 00000000..450cb19b
|
|
--- /dev/null
|
|
+++ b/drivers/input/i2c-hidd/src/quirks.rs
|
|
@@ -0,0 +1,88 @@
|
|
+use std::fs;
|
|
+
|
|
+use anyhow::{Context, Result};
|
|
+use serde::Deserialize;
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+pub struct ProbeFailureQuirk {
|
|
+ pub name: String,
|
|
+ pub system_vendor: Option<String>,
|
|
+ pub product_name: Option<String>,
|
|
+ pub board_name: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Default, Deserialize)]
|
|
+struct ProbeFailureQuirkFile {
|
|
+ quirks: Vec<ProbeFailureQuirkEntry>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Deserialize)]
|
|
+struct ProbeFailureQuirkEntry {
|
|
+ name: String,
|
|
+ system_vendor: Option<String>,
|
|
+ product_name: Option<String>,
|
|
+ board_name: Option<String>,
|
|
+}
|
|
+
|
|
+#[derive(Default)]
|
|
+struct DmiSnapshot {
|
|
+ system_vendor: String,
|
|
+ product_name: String,
|
|
+ board_name: String,
|
|
+}
|
|
+
|
|
+pub fn match_probe_failure_quirk() -> Result<Option<ProbeFailureQuirk>> {
|
|
+ let snapshot = read_dmi_snapshot()?;
|
|
+ for entry in load_quirks()? {
|
|
+ if field_matches(&entry.system_vendor, &snapshot.system_vendor)
|
|
+ && field_matches(&entry.product_name, &snapshot.product_name)
|
|
+ && field_matches(&entry.board_name, &snapshot.board_name)
|
|
+ {
|
|
+ return Ok(Some(ProbeFailureQuirk {
|
|
+ name: entry.name,
|
|
+ system_vendor: entry.system_vendor,
|
|
+ product_name: entry.product_name,
|
|
+ board_name: entry.board_name,
|
|
+ }));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(None)
|
|
+}
|
|
+
|
|
+fn load_quirks() -> Result<Vec<ProbeFailureQuirkEntry>> {
|
|
+ let path = "/etc/i2c-hidd-quirks.ron";
|
|
+ let text = match fs::read_to_string(path) {
|
|
+ Ok(text) => text,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
|
+ Err(err) => return Err(err).with_context(|| format!("failed to read {path}")),
|
|
+ };
|
|
+
|
|
+ let file: ProbeFailureQuirkFile =
|
|
+ ron::from_str(&text).with_context(|| format!("failed to decode {path}"))?;
|
|
+ Ok(file.quirks)
|
|
+}
|
|
+
|
|
+fn read_dmi_snapshot() -> Result<DmiSnapshot> {
|
|
+ Ok(DmiSnapshot {
|
|
+ system_vendor: read_dmi_field("system_vendor")?,
|
|
+ product_name: read_dmi_field("product_name")?,
|
|
+ board_name: read_dmi_field("board_name")?,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn read_dmi_field(field: &str) -> Result<String> {
|
|
+ let path = format!("/scheme/acpi/dmi/{field}");
|
|
+ match fs::read_to_string(&path) {
|
|
+ Ok(value) => Ok(value.trim().to_string()),
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
|
|
+ Err(err) => Err(err).with_context(|| format!("failed to read {path}")),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn field_matches(expected: &Option<String>, actual: &str) -> bool {
|
|
+ expected
|
|
+ .as_deref()
|
|
+ .map(|expected| actual.eq_ignore_ascii_case(expected))
|
|
+ .unwrap_or(true)
|
|
+}
|
|
diff --git a/drivers/input/intel-thc-hidd/Cargo.toml b/drivers/input/intel-thc-hidd/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..f6aa2248
|
|
--- /dev/null
|
|
+++ b/drivers/input/intel-thc-hidd/Cargo.toml
|
|
@@ -0,0 +1,26 @@
|
|
+[package]
|
|
+name = "intel-thc-hidd"
|
|
+description = "Intel THC QuickI2C HID transport daemon"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+pci_types = "0.10.1"
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+redox-scheme.workspace = true
|
|
+ron.workspace = true
|
|
+serde.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+amlserde = { path = "../../amlserde" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+i2c-interface = { path = "../../i2c/i2c-interface" }
|
|
+pcid = { path = "../../pcid" }
|
|
+scheme-utils = { path = "../../../scheme-utils" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/input/intel-thc-hidd/src/main.rs b/drivers/input/intel-thc-hidd/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..423977e0
|
|
--- /dev/null
|
|
+++ b/drivers/input/intel-thc-hidd/src/main.rs
|
|
@@ -0,0 +1,260 @@
|
|
+use std::collections::BTreeSet;
|
|
+use std::fs::{self, OpenOptions};
|
|
+use std::io::Read;
|
|
+use std::process;
|
|
+use std::thread;
|
|
+use std::time::Duration;
|
|
+
|
|
+use acpi_resource::ResourceDescriptor;
|
|
+use amlserde::{AmlSerde, AmlSerdeValue};
|
|
+use anyhow::{bail, Context, Result};
|
|
+use libredox::flag::{O_CLOEXEC, O_RDWR};
|
|
+use pcid_interface::PciFunctionHandle;
|
|
+
|
|
+mod quicki2c;
|
|
+mod thc;
|
|
+
|
|
+use quicki2c::QuickI2cTransport;
|
|
+use thc::{ThcController, SUPPORTED_PCI_IDS};
|
|
+
|
|
+fn main() {
|
|
+ pcid_interface::pci_daemon(daemon);
|
|
+}
|
|
+
|
|
+fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
|
|
+ common::setup_logging(
|
|
+ "input",
|
|
+ "intel-thc",
|
|
+ "intel-thc-hidd",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ if let Err(err) = run(daemon, &mut pcid_handle) {
|
|
+ log::error!("RB_THC_HIDD_FATAL error={err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn run(daemon: daemon::Daemon, pcid_handle: &mut PciFunctionHandle) -> Result<()> {
|
|
+ log::info!("RB_THC_HIDD_SCHEMA version=1");
|
|
+
|
|
+ let pci_config = pcid_handle.config();
|
|
+ let id = (
|
|
+ pci_config.func.full_device_id.vendor_id,
|
|
+ pci_config.func.full_device_id.device_id,
|
|
+ );
|
|
+ if !SUPPORTED_PCI_IDS.contains(&id) {
|
|
+ bail!("unsupported Intel THC PCI device {:04x}:{:04x}", id.0, id.1);
|
|
+ }
|
|
+
|
|
+ pcid_handle.enable_device();
|
|
+ let bar = unsafe { pcid_handle.map_bar(0) };
|
|
+ let controller = ThcController::new(bar.ptr.as_ptr(), bar.bar_size)
|
|
+ .context("failed to create THC controller")?;
|
|
+
|
|
+ let companion = resolve_acpi_companion(&pci_config.func.addr)
|
|
+ .context("failed to resolve ACPI companion for THC device")?;
|
|
+ let override_address = companion
|
|
+ .as_deref()
|
|
+ .map(companion_slave_address_override)
|
|
+ .transpose()
|
|
+ .context("failed to evaluate THC slave-address override")?
|
|
+ .flatten();
|
|
+ let hid_devices = scan_bound_i2c_hid_devices(companion.as_deref())
|
|
+ .context("failed to scan PNP0C50 devices for THC controller")?;
|
|
+
|
|
+ let effective_address = override_address.unwrap_or(0x0015);
|
|
+ let transport = QuickI2cTransport::new(controller, effective_address);
|
|
+ transport.prime_controller();
|
|
+ transport.emulate_transfer(&[]);
|
|
+ log::debug!("RB_THC_HIDD status={:#x}", transport.status());
|
|
+
|
|
+ match transport.register_with_i2cd(companion.as_deref(), override_address) {
|
|
+ Ok(()) => {}
|
|
+ Err(err) => {
|
|
+ log::warn!("RB_THC_HIDD registration error={err:#}");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ log::info!(
|
|
+ "RB_THC_HIDD pci={} companion={:?} override={:?} hid_devices={}",
|
|
+ pci_config.func.name(),
|
|
+ companion,
|
|
+ override_address,
|
|
+ hid_devices.len()
|
|
+ );
|
|
+
|
|
+ daemon.ready();
|
|
+
|
|
+ loop {
|
|
+ thread::sleep(Duration::from_secs(5));
|
|
+ }
|
|
+}
|
|
+
|
|
+fn resolve_acpi_companion(addr: &pci_types::PciAddress) -> Result<Option<String>> {
|
|
+ let entries =
|
|
+ fs::read_dir("/scheme/acpi/symbols").context("failed to read /scheme/acpi/symbols")?;
|
|
+ let expected_adr = (u64::from(addr.device()) << 16) | u64::from(addr.function());
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to enumerate ACPI symbol entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("._ADR") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let symbol = read_aml_symbol(&file_name)?;
|
|
+ if !matches!(symbol.value, AmlSerdeValue::Integer(value) if value == expected_adr) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let device = symbol
|
|
+ .name
|
|
+ .strip_suffix("._ADR")
|
|
+ .unwrap_or(&symbol.name)
|
|
+ .trim_start_matches('\\')
|
|
+ .replace('/', ".");
|
|
+ return Ok(Some(device));
|
|
+ }
|
|
+
|
|
+ Ok(None)
|
|
+}
|
|
+
|
|
+fn companion_slave_address_override(path: &str) -> Result<Option<u16>> {
|
|
+ let icrs = evaluate_integer_method(path, "ICRS").ok();
|
|
+ let isub = evaluate_integer_method(path, "ISUB").ok();
|
|
+ Ok(icrs
|
|
+ .or(isub)
|
|
+ .map(|value| u16::try_from(value))
|
|
+ .transpose()
|
|
+ .context("THC ACPI override out of range")?)
|
|
+}
|
|
+
|
|
+fn scan_bound_i2c_hid_devices(companion: Option<&str>) -> Result<Vec<String>> {
|
|
+ let entries =
|
|
+ fs::read_dir("/scheme/acpi/symbols").context("failed to read /scheme/acpi/symbols")?;
|
|
+ let mut devices = BTreeSet::new();
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to enumerate ACPI HID entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("._HID") && !file_name.ends_with("._CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let symbol = read_aml_symbol(&file_name)?;
|
|
+ let is_hid = matches!(
|
|
+ decode_hardware_id(&symbol.value).as_deref(),
|
|
+ Some("PNP0C50" | "ACPI0C50")
|
|
+ );
|
|
+ if !is_hid {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let device = symbol
|
|
+ .name
|
|
+ .strip_suffix("._HID")
|
|
+ .or_else(|| symbol.name.strip_suffix("._CID"))
|
|
+ .unwrap_or(&symbol.name)
|
|
+ .trim_start_matches('\\')
|
|
+ .replace('/', ".");
|
|
+ if let Some(companion) = companion {
|
|
+ if !is_bound_to_companion(&device, companion)? {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ devices.insert(device);
|
|
+ }
|
|
+
|
|
+ Ok(devices.into_iter().collect())
|
|
+}
|
|
+
|
|
+fn is_bound_to_companion(device: &str, companion: &str) -> Result<bool> {
|
|
+ let resource_path = format!("/scheme/acpi/resources/{device}");
|
|
+ let serialized = match fs::read_to_string(&resource_path) {
|
|
+ Ok(serialized) => serialized,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
|
+ Err(err) => return Err(err).with_context(|| format!("failed to read {resource_path}")),
|
|
+ };
|
|
+
|
|
+ let resources: Vec<ResourceDescriptor> =
|
|
+ ron::from_str(&serialized).with_context(|| format!("failed to decode {resource_path}"))?;
|
|
+ Ok(resources.into_iter().any(|resource| match resource {
|
|
+ ResourceDescriptor::I2cSerialBus(bus) => bus
|
|
+ .resource_source
|
|
+ .as_ref()
|
|
+ .map(|source| source.source == companion)
|
|
+ .unwrap_or(false),
|
|
+ _ => false,
|
|
+ }))
|
|
+}
|
|
+
|
|
+fn evaluate_integer_method(path: &str, method: &str) -> Result<u64> {
|
|
+ let symbol_name = format!("{}.{}", normalize_device_path(path), method);
|
|
+ let symbol_path = format!("/scheme/acpi/symbols/{symbol_name}");
|
|
+ let fd = libredox::Fd::open(&symbol_path, O_RDWR | O_CLOEXEC, 0)
|
|
+ .with_context(|| format!("failed to open {symbol_path}"))?;
|
|
+
|
|
+ let mut payload = ron::to_string(&Vec::<AmlSerdeValue>::new())
|
|
+ .context("failed to serialize ACPI call arguments")?
|
|
+ .into_bytes();
|
|
+ payload.resize(payload.len() + 2048, 0);
|
|
+ let used = libredox::call::call_ro(fd.raw(), &mut payload, syscall::CallFlags::empty(), &[])
|
|
+ .with_context(|| format!("ACPI evaluation failed for {symbol_name}"))?;
|
|
+ let response = std::str::from_utf8(&payload[..used])
|
|
+ .with_context(|| format!("invalid UTF-8 ACPI response for {symbol_name}"))?;
|
|
+ match ron::from_str::<AmlSerdeValue>(response)
|
|
+ .with_context(|| format!("failed to decode ACPI response for {symbol_name}"))?
|
|
+ {
|
|
+ AmlSerdeValue::Integer(value) => Ok(value),
|
|
+ other => bail!("{}.{} returned non-integer value {other:?}", path, method),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn read_aml_symbol(file_name: &str) -> Result<AmlSerde> {
|
|
+ let path = format!("/scheme/acpi/symbols/{file_name}");
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .open(&path)
|
|
+ .with_context(|| format!("failed to open {path}"))?;
|
|
+ let mut ron_text = String::new();
|
|
+ file.read_to_string(&mut ron_text)
|
|
+ .with_context(|| format!("failed to read {path}"))?;
|
|
+ ron::from_str(&ron_text).with_context(|| format!("failed to decode {path}"))
|
|
+}
|
|
+
|
|
+fn decode_hardware_id(value: &AmlSerdeValue) -> Option<String> {
|
|
+ match value {
|
|
+ AmlSerdeValue::String(value) => Some(value.clone()),
|
|
+ AmlSerdeValue::Integer(integer) => {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1f) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1f) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1f) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+ Some(format!(
|
|
+ "{}{}{}{:01X}{:01X}{:01X}{:01X}",
|
|
+ vendor_1, vendor_2, vendor_3, device_1, device_2, device_3, device_4
|
|
+ ))
|
|
+ }
|
|
+ _ => None,
|
|
+ }
|
|
+}
|
|
+
|
|
+fn normalize_device_path(path: &str) -> String {
|
|
+ path.trim_start_matches('\\')
|
|
+ .trim_matches('/')
|
|
+ .replace('/', ".")
|
|
+}
|
|
diff --git a/drivers/input/intel-thc-hidd/src/quicki2c.rs b/drivers/input/intel-thc-hidd/src/quicki2c.rs
|
|
new file mode 100644
|
|
index 00000000..721f0be3
|
|
--- /dev/null
|
|
+++ b/drivers/input/intel-thc-hidd/src/quicki2c.rs
|
|
@@ -0,0 +1,86 @@
|
|
+use std::fs::OpenOptions;
|
|
+use std::io::Write;
|
|
+
|
|
+use anyhow::{Context, Result};
|
|
+use i2c_interface::{I2cAdapterRegistration, I2cTransferSegment};
|
|
+
|
|
+use crate::thc::ThcController;
|
|
+
|
|
+const QUICKI2C_OPCODE_WRITE: u32 = 0x1;
|
|
+const QUICKI2C_OPCODE_READ: u32 = 0x2;
|
|
+
|
|
+pub struct QuickI2cTransport {
|
|
+ controller: ThcController,
|
|
+ slave_address: u16,
|
|
+}
|
|
+
|
|
+impl QuickI2cTransport {
|
|
+ pub fn new(controller: ThcController, slave_address: u16) -> Self {
|
|
+ Self {
|
|
+ controller,
|
|
+ slave_address,
|
|
+ }
|
|
+ }
|
|
+
|
|
+ pub fn prime_controller(&self) {
|
|
+ self.controller.initialize_quicki2c_mode();
|
|
+ }
|
|
+
|
|
+ pub fn emulate_transfer(&self, segments: &[I2cTransferSegment]) {
|
|
+ for segment in segments {
|
|
+ match &segment.op {
|
|
+ i2c_interface::I2cTransferOp::Write(data) => {
|
|
+ self.controller.program_subip_transaction(
|
|
+ QUICKI2C_OPCODE_WRITE,
|
|
+ segment.address,
|
|
+ data.len(),
|
|
+ );
|
|
+ for (index, chunk) in data.chunks(4).enumerate() {
|
|
+ let mut word = [0_u8; 4];
|
|
+ word[..chunk.len()].copy_from_slice(chunk);
|
|
+ self.controller
|
|
+ .write_subip_data(index * 4, u32::from_le_bytes(word));
|
|
+ }
|
|
+ }
|
|
+ i2c_interface::I2cTransferOp::Read(len) => {
|
|
+ self.controller.program_subip_transaction(
|
|
+ QUICKI2C_OPCODE_READ,
|
|
+ segment.address,
|
|
+ *len,
|
|
+ );
|
|
+ let _ = self.controller.read_subip_data(0);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ pub fn status(&self) -> u32 {
|
|
+ self.controller.status()
|
|
+ }
|
|
+
|
|
+ pub fn register_with_i2cd(
|
|
+ &self,
|
|
+ acpi_companion: Option<&str>,
|
|
+ override_address: Option<u16>,
|
|
+ ) -> Result<()> {
|
|
+ let registration = I2cAdapterRegistration {
|
|
+ name: "intel-thc-quicki2c".to_string(),
|
|
+ description: format!(
|
|
+ "Intel THC QuickI2C adapter for slave {:04x}",
|
|
+ self.slave_address
|
|
+ ),
|
|
+ acpi_companion: acpi_companion.map(str::to_owned),
|
|
+ slave_address_override: override_address,
|
|
+ };
|
|
+ let payload =
|
|
+ ron::to_string(®istration).context("failed to serialize i2cd registration")?;
|
|
+
|
|
+ let mut file = OpenOptions::new()
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/register")
|
|
+ .context("failed to open /scheme/i2c/register")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to write i2cd adapter registration")?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
diff --git a/drivers/input/intel-thc-hidd/src/thc.rs b/drivers/input/intel-thc-hidd/src/thc.rs
|
|
new file mode 100644
|
|
index 00000000..e06a6f8a
|
|
--- /dev/null
|
|
+++ b/drivers/input/intel-thc-hidd/src/thc.rs
|
|
@@ -0,0 +1,78 @@
|
|
+use std::ptr::NonNull;
|
|
+
|
|
+use anyhow::{bail, Result};
|
|
+
|
|
+pub const SUPPORTED_PCI_IDS: &[(u16, u16)] = &[
|
|
+ (0x8086, 0x7eb8),
|
|
+ (0x8086, 0x7eb9),
|
|
+ (0x8086, 0x7ebd),
|
|
+ (0x8086, 0x7ebe),
|
|
+ (0x8086, 0xa8b8),
|
|
+ (0x8086, 0xa8b9),
|
|
+];
|
|
+
|
|
+pub const REG_CONTROL: usize = 0x0000;
|
|
+pub const REG_STATUS: usize = 0x0004;
|
|
+pub const REG_MODE: usize = 0x0010;
|
|
+pub const REG_SUBIP_OPCODE: usize = 0x0800;
|
|
+pub const REG_SUBIP_ADDRESS: usize = 0x0804;
|
|
+pub const REG_SUBIP_LENGTH: usize = 0x0808;
|
|
+pub const REG_SUBIP_DOORBELL: usize = 0x080C;
|
|
+pub const REG_SUBIP_DATA: usize = 0x0810;
|
|
+
|
|
+#[derive(Clone, Copy)]
|
|
+pub struct ThcController {
|
|
+ base: NonNull<u8>,
|
|
+ len: usize,
|
|
+}
|
|
+
|
|
+impl ThcController {
|
|
+ pub fn new(base: *mut u8, len: usize) -> Result<Self> {
|
|
+ let Some(base) = NonNull::new(base) else {
|
|
+ bail!("THC BAR mapping returned null base pointer");
|
|
+ };
|
|
+ Ok(Self { base, len })
|
|
+ }
|
|
+
|
|
+ pub fn initialize_quicki2c_mode(&self) {
|
|
+ self.write32(REG_MODE, 0x1);
|
|
+ self.write32(REG_CONTROL, 0x1);
|
|
+ }
|
|
+
|
|
+ pub fn status(&self) -> u32 {
|
|
+ self.read32(REG_STATUS)
|
|
+ }
|
|
+
|
|
+ pub fn program_subip_transaction(&self, opcode: u32, address: u16, len: usize) {
|
|
+ self.write32(REG_SUBIP_OPCODE, opcode);
|
|
+ self.write32(REG_SUBIP_ADDRESS, u32::from(address));
|
|
+ self.write32(REG_SUBIP_LENGTH, len as u32);
|
|
+ self.write32(REG_SUBIP_DOORBELL, 1);
|
|
+ }
|
|
+
|
|
+ pub fn write_subip_data(&self, offset: usize, value: u32) {
|
|
+ self.write32(REG_SUBIP_DATA + offset, value);
|
|
+ }
|
|
+
|
|
+ pub fn read_subip_data(&self, offset: usize) -> u32 {
|
|
+ self.read32(REG_SUBIP_DATA + offset)
|
|
+ }
|
|
+
|
|
+ fn read32(&self, offset: usize) -> u32 {
|
|
+ if offset + 4 > self.len {
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ let ptr = unsafe { self.base.as_ptr().add(offset).cast::<u32>() };
|
|
+ unsafe { ptr.read_volatile() }
|
|
+ }
|
|
+
|
|
+ fn write32(&self, offset: usize, value: u32) {
|
|
+ if offset + 4 > self.len {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ let ptr = unsafe { self.base.as_ptr().add(offset).cast::<u32>() };
|
|
+ unsafe { ptr.write_volatile(value) };
|
|
+ }
|
|
+}
|
|
diff --git a/drivers/usb/ucsid/Cargo.toml b/drivers/usb/ucsid/Cargo.toml
|
|
new file mode 100644
|
|
index 00000000..1a6833e5
|
|
--- /dev/null
|
|
+++ b/drivers/usb/ucsid/Cargo.toml
|
|
@@ -0,0 +1,23 @@
|
|
+[package]
|
|
+name = "ucsid"
|
|
+description = "USB-C UCSI topology daemon"
|
|
+version = "0.1.0"
|
|
+edition = "2021"
|
|
+
|
|
+[dependencies]
|
|
+anyhow.workspace = true
|
|
+log.workspace = true
|
|
+redox_syscall = { workspace = true, features = ["std"] }
|
|
+libredox.workspace = true
|
|
+redox-scheme.workspace = true
|
|
+serde.workspace = true
|
|
+ron.workspace = true
|
|
+
|
|
+acpi-resource = { path = "../../acpi-resource" }
|
|
+common = { path = "../../common" }
|
|
+daemon = { path = "../../../daemon" }
|
|
+i2c-interface = { path = "../../i2c/i2c-interface" }
|
|
+scheme-utils = { path = "../../../scheme-utils" }
|
|
+
|
|
+[lints]
|
|
+workspace = true
|
|
diff --git a/drivers/usb/ucsid/src/main.rs b/drivers/usb/ucsid/src/main.rs
|
|
new file mode 100644
|
|
index 00000000..612f5ce2
|
|
--- /dev/null
|
|
+++ b/drivers/usb/ucsid/src/main.rs
|
|
@@ -0,0 +1,835 @@
|
|
+use std::collections::BTreeMap;
|
|
+use std::fs::{self, File, OpenOptions};
|
|
+use std::io::{Read, Write};
|
|
+use std::path::Path;
|
|
+use std::process;
|
|
+
|
|
+use acpi_resource::{
|
|
+ AddressResourceType, FixedMemory32Descriptor, I2cSerialBusDescriptor, Memory32RangeDescriptor,
|
|
+ ResourceDescriptor,
|
|
+};
|
|
+use anyhow::{bail, Context, Result};
|
|
+use i2c_interface::{
|
|
+ I2cAdapterInfo, I2cControlRequest, I2cControlResponse, I2cTransferRequest,
|
|
+ I2cTransferSegment,
|
|
+};
|
|
+use libredox::flag::{O_CLOEXEC, O_RDWR};
|
|
+use redox_scheme::scheme::SchemeSync;
|
|
+use redox_scheme::{CallerCtx, OpenResult, Socket};
|
|
+use scheme_utils::{Blocking, HandleMap};
|
|
+use serde::{Deserialize, Serialize};
|
|
+use syscall::schemev2::NewFdFlags;
|
|
+use syscall::{Error as SysError, EACCES, EBADF, EINVAL, ENOENT};
|
|
+
|
|
+const SUPPORTED_IDS: &[&str] = &["PNP0CA0", "AMDI0042"];
|
|
+const GET_CAPABILITY: u8 = 0x01;
|
|
+const GET_CONNECTOR_STATUS: u8 = 0x10;
|
|
+const UCSI_RESPONSE_HEADER_LEN: usize = 4;
|
|
+const UCSI_CAPABILITY_READ_LEN: usize = 20;
|
|
+const UCSI_CONNECTOR_STATUS_READ_LEN: usize = 20;
|
|
+const MAX_CONNECTOR_PROBE: u8 = 8;
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+struct AmlSymbol {
|
|
+ name: String,
|
|
+ value: AmlValue,
|
|
+}
|
|
+
|
|
+#[derive(Debug, Deserialize)]
|
|
+enum AmlValue {
|
|
+ Integer(u64),
|
|
+ String(String),
|
|
+}
|
|
+
|
|
+#[derive(Clone, Copy, Debug)]
|
|
+struct UcsiCommand {
|
|
+ command: u8,
|
|
+ data_length: u8,
|
|
+ specific_data: [u8; 6],
|
|
+}
|
|
+
|
|
+impl UcsiCommand {
|
|
+ fn new(command: u8, data_length: u8, specific_data: [u8; 6]) -> Self {
|
|
+ Self {
|
|
+ command,
|
|
+ data_length,
|
|
+ specific_data,
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn as_bytes(self) -> [u8; 8] {
|
|
+ let mut bytes = [0_u8; 8];
|
|
+ bytes[0] = self.command;
|
|
+ bytes[1] = self.data_length;
|
|
+ bytes[2..].copy_from_slice(&self.specific_data);
|
|
+ bytes
|
|
+ }
|
|
+}
|
|
+
|
|
+#[derive(Clone, Copy, Debug)]
|
|
+struct UcsiResponseHeader {
|
|
+ _status: u16,
|
|
+ data_length: u16,
|
|
+}
|
|
+
|
|
+impl UcsiResponseHeader {
|
|
+ fn parse(bytes: &[u8]) -> Option<Self> {
|
|
+ let header = bytes.get(..UCSI_RESPONSE_HEADER_LEN)?;
|
|
+ Some(Self {
|
|
+ _status: u16::from_le_bytes([header[0], header[1]]),
|
|
+ data_length: u16::from_le_bytes([header[2], header[3]]),
|
|
+ })
|
|
+ }
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug)]
|
|
+struct DiscoveredUcsiDevice {
|
|
+ name: String,
|
|
+ hid: String,
|
|
+ transport: UcsiTransport,
|
|
+ dsm_probe: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
+enum UcsiTransport {
|
|
+ I2c {
|
|
+ adapter: String,
|
|
+ address: u16,
|
|
+ ten_bit_address: bool,
|
|
+ },
|
|
+ Mmio {
|
|
+ base: usize,
|
|
+ len: usize,
|
|
+ },
|
|
+ Unknown,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
+struct UcsiCapability {
|
|
+ connector_count: u8,
|
|
+ supports_usb_pd: bool,
|
|
+ supports_alt_modes: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
+struct UcsiConnectorSummary {
|
|
+ device: String,
|
|
+ connector_number: u8,
|
|
+ connected: bool,
|
|
+ data_role: String,
|
|
+ power_direction: String,
|
|
+ input_critical: bool,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
+struct UcsiDeviceSummary {
|
|
+ name: String,
|
|
+ hid: String,
|
|
+ transport: UcsiTransport,
|
|
+ capability: Option<UcsiCapability>,
|
|
+ connectors: Vec<UcsiConnectorSummary>,
|
|
+ dsm_probe: bool,
|
|
+ issues: Vec<String>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
+struct UcsiSummary {
|
|
+ schema_version: u32,
|
|
+ device_count: usize,
|
|
+ connector_count: usize,
|
|
+ input_critical_ports: usize,
|
|
+ devices: Vec<UcsiDeviceSummary>,
|
|
+}
|
|
+
|
|
+#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
+struct UcsiHealth {
|
|
+ healthy: bool,
|
|
+ scanned_devices: usize,
|
|
+ responsive_devices: usize,
|
|
+ issues: Vec<String>,
|
|
+}
|
|
+
|
|
+struct UcsiState {
|
|
+ summary: UcsiSummary,
|
|
+ connectors: Vec<UcsiConnectorSummary>,
|
|
+ health: UcsiHealth,
|
|
+}
|
|
+
|
|
+enum Handle {
|
|
+ SchemeRoot,
|
|
+ Summary { pending: Vec<u8> },
|
|
+ Connectors { pending: Vec<u8> },
|
|
+ Health { pending: Vec<u8> },
|
|
+}
|
|
+
|
|
+struct UcsiScheme {
|
|
+ handles: HandleMap<Handle>,
|
|
+ state: UcsiState,
|
|
+}
|
|
+
|
|
+impl UcsiScheme {
|
|
+ fn new(state: UcsiState) -> Self {
|
|
+ Self {
|
|
+ handles: HandleMap::new(),
|
|
+ state,
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn serialize_payload<T: Serialize>(value: &T) -> syscall::Result<Vec<u8>> {
|
|
+ ron::ser::to_string(value)
|
|
+ .map(|text| text.into_bytes())
|
|
+ .map_err(|err| {
|
|
+ log::error!("ucsid: failed to serialize scheme payload: {err}");
|
|
+ SysError::new(EINVAL)
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn set_pending(handle: &mut Handle, pending: Vec<u8>) -> syscall::Result<()> {
|
|
+ match handle {
|
|
+ Handle::Summary { pending: slot }
|
|
+ | Handle::Connectors { pending: slot }
|
|
+ | Handle::Health { pending: slot } => {
|
|
+ *slot = pending;
|
|
+ Ok(())
|
|
+ }
|
|
+ Handle::SchemeRoot => Err(SysError::new(EBADF)),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ fn copy_pending(handle: &mut Handle, buf: &mut [u8], offset: u64) -> syscall::Result<usize> {
|
|
+ let pending = match handle {
|
|
+ Handle::Summary { pending }
|
|
+ | Handle::Connectors { pending }
|
|
+ | Handle::Health { pending } => pending,
|
|
+ Handle::SchemeRoot => return Err(SysError::new(EBADF)),
|
|
+ };
|
|
+
|
|
+ let offset = usize::try_from(offset).map_err(|_| SysError::new(EINVAL))?;
|
|
+ if offset >= pending.len() {
|
|
+ return Ok(0);
|
|
+ }
|
|
+
|
|
+ let copy_len = buf.len().min(pending.len() - offset);
|
|
+ buf[..copy_len].copy_from_slice(&pending[offset..offset + copy_len]);
|
|
+ Ok(copy_len)
|
|
+ }
|
|
+}
|
|
+
|
|
+impl SchemeSync for UcsiScheme {
|
|
+ fn scheme_root(&mut self) -> syscall::Result<usize> {
|
|
+ Ok(self.handles.insert(Handle::SchemeRoot))
|
|
+ }
|
|
+
|
|
+ fn openat(
|
|
+ &mut self,
|
|
+ dirfd: usize,
|
|
+ path: &str,
|
|
+ _flags: usize,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<OpenResult> {
|
|
+ if !matches!(self.handles.get(dirfd)?, Handle::SchemeRoot) {
|
|
+ return Err(SysError::new(EACCES));
|
|
+ }
|
|
+
|
|
+ let handle = match path.trim_matches('/') {
|
|
+ "summary" => Handle::Summary {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ "connectors" => Handle::Connectors {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ "health" => Handle::Health {
|
|
+ pending: Vec::new(),
|
|
+ },
|
|
+ "" => return Err(SysError::new(EINVAL)),
|
|
+ _ => return Err(SysError::new(ENOENT)),
|
|
+ };
|
|
+
|
|
+ let fd = self.handles.insert(handle);
|
|
+ Ok(OpenResult::ThisScheme {
|
|
+ number: fd,
|
|
+ flags: NewFdFlags::empty(),
|
|
+ })
|
|
+ }
|
|
+
|
|
+ fn read(
|
|
+ &mut self,
|
|
+ id: usize,
|
|
+ buf: &mut [u8],
|
|
+ offset: u64,
|
|
+ _fcntl_flags: u32,
|
|
+ _ctx: &CallerCtx,
|
|
+ ) -> syscall::Result<usize> {
|
|
+ let payload = match self.handles.get(id)? {
|
|
+ Handle::Summary { pending } if pending.is_empty() => {
|
|
+ Some(Self::serialize_payload(&self.state.summary)?)
|
|
+ }
|
|
+ Handle::Connectors { pending } if pending.is_empty() => {
|
|
+ Some(Self::serialize_payload(&self.state.connectors)?)
|
|
+ }
|
|
+ Handle::Health { pending } if pending.is_empty() => {
|
|
+ log::info!(
|
|
+ "RB_UCSID_HEALTH healthy={} scanned_devices={} responsive_devices={} issues={}",
|
|
+ self.state.health.healthy,
|
|
+ self.state.health.scanned_devices,
|
|
+ self.state.health.responsive_devices,
|
|
+ self.state.health.issues.len(),
|
|
+ );
|
|
+ Some(Self::serialize_payload(&self.state.health)?)
|
|
+ }
|
|
+ _ => None,
|
|
+ };
|
|
+
|
|
+ let handle = self.handles.get_mut(id)?;
|
|
+ if let Some(payload) = payload {
|
|
+ Self::set_pending(handle, payload)?;
|
|
+ }
|
|
+ Self::copy_pending(handle, buf, offset)
|
|
+ }
|
|
+}
|
|
+
|
|
+fn main() {
|
|
+ common::setup_logging(
|
|
+ "usb",
|
|
+ "ucsi",
|
|
+ "ucsid",
|
|
+ common::output_level(),
|
|
+ common::file_level(),
|
|
+ );
|
|
+
|
|
+ daemon::SchemeDaemon::new(daemon_runner);
|
|
+}
|
|
+
|
|
+fn daemon_runner(daemon: daemon::SchemeDaemon) -> ! {
|
|
+ if let Err(err) = run_daemon(daemon) {
|
|
+ log::error!("ucsid: {err:#}");
|
|
+ process::exit(1);
|
|
+ }
|
|
+
|
|
+ process::exit(0);
|
|
+}
|
|
+
|
|
+fn run_daemon(daemon: daemon::SchemeDaemon) -> Result<()> {
|
|
+ log::info!("RB_UCSID_SCHEMA version=1");
|
|
+
|
|
+ let state = build_state().context("failed to build UCSI device snapshot")?;
|
|
+ let socket = Socket::create().context("failed to create ucsi scheme socket")?;
|
|
+ let mut scheme = UcsiScheme::new(state);
|
|
+ let handler = Blocking::new(&socket, 16);
|
|
+
|
|
+ daemon
|
|
+ .ready_sync_scheme(&socket, &mut scheme)
|
|
+ .context("failed to publish ucsi scheme root")?;
|
|
+
|
|
+ libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
|
|
+
|
|
+ handler
|
|
+ .process_requests_blocking(scheme)
|
|
+ .context("failed to process ucsid requests")?;
|
|
+}
|
|
+
|
|
+fn build_state() -> Result<UcsiState> {
|
|
+ let adapters = list_i2c_adapters().unwrap_or_else(|err| {
|
|
+ log::warn!("ucsid: failed to query i2cd adapters: {err:#}");
|
|
+ Vec::new()
|
|
+ });
|
|
+ let devices = discover_ucsi_devices().context("failed to discover ACPI UCSI devices")?;
|
|
+
|
|
+ let mut summaries = Vec::new();
|
|
+ let mut connectors = Vec::new();
|
|
+ let mut issues = Vec::new();
|
|
+ let mut responsive_devices = 0usize;
|
|
+
|
|
+ for device in devices {
|
|
+ log::info!(
|
|
+ "RB_UCSID_DEVICE name={} hid={} transport={:?} dsm_probe={}",
|
|
+ device.name,
|
|
+ device.hid,
|
|
+ device.transport,
|
|
+ device.dsm_probe,
|
|
+ );
|
|
+ let summary = summarize_device(device, &adapters)
|
|
+ .context("failed to summarize discovered UCSI device")?;
|
|
+ if summary.capability.is_some() {
|
|
+ responsive_devices += 1;
|
|
+ }
|
|
+ issues.extend(summary.issues.iter().cloned());
|
|
+ connectors.extend(summary.connectors.iter().cloned());
|
|
+ summaries.push(summary);
|
|
+ }
|
|
+
|
|
+ let summary = UcsiSummary {
|
|
+ schema_version: 1,
|
|
+ device_count: summaries.len(),
|
|
+ connector_count: connectors.len(),
|
|
+ input_critical_ports: connectors.iter().filter(|connector| connector.input_critical).count(),
|
|
+ devices: summaries,
|
|
+ };
|
|
+ let health = UcsiHealth {
|
|
+ healthy: issues.is_empty(),
|
|
+ scanned_devices: summary.device_count,
|
|
+ responsive_devices,
|
|
+ issues,
|
|
+ };
|
|
+
|
|
+ log::info!(
|
|
+ "RB_UCSID_SUMMARY devices={} connectors={} input_critical_ports={} healthy={}",
|
|
+ summary.device_count,
|
|
+ summary.connector_count,
|
|
+ summary.input_critical_ports,
|
|
+ health.healthy,
|
|
+ );
|
|
+
|
|
+ Ok(UcsiState {
|
|
+ summary,
|
|
+ connectors,
|
|
+ health,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn discover_ucsi_devices() -> Result<Vec<DiscoveredUcsiDevice>> {
|
|
+ let mut matched = BTreeMap::new();
|
|
+
|
|
+ let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
|
+ Ok(entries) => entries,
|
|
+ Err(err) if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) => {
|
|
+ log::debug!("ucsid: ACPI symbols are not ready yet");
|
|
+ return Ok(Vec::new());
|
|
+ }
|
|
+ Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
|
|
+ };
|
|
+
|
|
+ for entry in entries {
|
|
+ let entry = entry.context("failed to read ACPI symbol directory entry")?;
|
|
+ let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
|
|
+ continue;
|
|
+ };
|
|
+ if !file_name.ends_with("_HID") && !file_name.ends_with("_CID") {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let Some(id) = read_symbol_id(&entry.path())? else {
|
|
+ continue;
|
|
+ };
|
|
+ if !SUPPORTED_IDS.iter().any(|candidate| *candidate == id) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ let Some(device) = file_name
|
|
+ .strip_suffix("_HID")
|
|
+ .or_else(|| file_name.strip_suffix("_CID"))
|
|
+ .map(str::to_owned)
|
|
+ else {
|
|
+ continue;
|
|
+ };
|
|
+ matched.entry(device).or_insert(id);
|
|
+ }
|
|
+
|
|
+ let mut devices = Vec::new();
|
|
+ for (device, hid) in matched {
|
|
+ let transport = read_ucsi_transport(&device)
|
|
+ .with_context(|| format!("failed to decode transport resources for {device}"))?;
|
|
+ let dsm_probe = bounded_dsm_probe(&device).unwrap_or_else(|err| {
|
|
+ log::debug!("ucsid: bounded _DSM probe failed for {device}: {err:#}");
|
|
+ false
|
|
+ });
|
|
+ devices.push(DiscoveredUcsiDevice {
|
|
+ name: device,
|
|
+ hid,
|
|
+ transport,
|
|
+ dsm_probe,
|
|
+ });
|
|
+ }
|
|
+
|
|
+ Ok(devices)
|
|
+}
|
|
+
|
|
+fn summarize_device(device: DiscoveredUcsiDevice, adapters: &[I2cAdapterInfo]) -> Result<UcsiDeviceSummary> {
|
|
+ let mut issues = Vec::new();
|
|
+ let capability = match &device.transport {
|
|
+ UcsiTransport::I2c {
|
|
+ adapter,
|
|
+ address,
|
|
+ ten_bit_address,
|
|
+ } => match match_i2c_adapter(adapters, adapter) {
|
|
+ Some(adapter_info) => match execute_ucsi_i2c_command(
|
|
+ adapter_info,
|
|
+ adapter,
|
|
+ *address,
|
|
+ *ten_bit_address,
|
|
+ UcsiCommand::new(GET_CAPABILITY, 0, [0; 6]),
|
|
+ UCSI_CAPABILITY_READ_LEN,
|
|
+ ) {
|
|
+ Ok(bytes) => parse_ucsi_payload(&bytes)
|
|
+ .and_then(|(_header, payload)| parse_capability(payload))
|
|
+ .or_else(|| {
|
|
+ issues.push(format!(
|
|
+ "{}: GET_CAPABILITY returned an unexpected payload",
|
|
+ device.name
|
|
+ ));
|
|
+ None
|
|
+ }),
|
|
+ Err(err) => {
|
|
+ issues.push(format!("{}: GET_CAPABILITY failed: {err:#}", device.name));
|
|
+ None
|
|
+ }
|
|
+ },
|
|
+ None => {
|
|
+ issues.push(format!(
|
|
+ "{}: no i2cd adapter matched ACPI source {}",
|
|
+ device.name, adapter
|
|
+ ));
|
|
+ None
|
|
+ }
|
|
+ },
|
|
+ UcsiTransport::Mmio { base, len } => {
|
|
+ issues.push(format!(
|
|
+ "{}: MMIO UCSI transport discovered at {base:#x}+{len:#x} but command execution is not implemented yet",
|
|
+ device.name,
|
|
+ ));
|
|
+ None
|
|
+ }
|
|
+ UcsiTransport::Unknown => {
|
|
+ issues.push(format!(
|
|
+ "{}: no supported UCSI transport was decoded from ACPI resources",
|
|
+ device.name,
|
|
+ ));
|
|
+ None
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let connector_count = capability
|
|
+ .as_ref()
|
|
+ .map(|capability| capability.connector_count.min(MAX_CONNECTOR_PROBE))
|
|
+ .unwrap_or(0);
|
|
+ let mut connectors = Vec::new();
|
|
+ for connector in 1..=connector_count {
|
|
+ match query_connector_status(&device, adapters, connector) {
|
|
+ Ok(connector_summary) => connectors.push(connector_summary),
|
|
+ Err(err) => issues.push(format!(
|
|
+ "{}: GET_CONNECTOR_STATUS({connector}) failed: {err:#}",
|
|
+ device.name,
|
|
+ )),
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Ok(UcsiDeviceSummary {
|
|
+ name: device.name,
|
|
+ hid: device.hid,
|
|
+ transport: device.transport,
|
|
+ capability,
|
|
+ connectors,
|
|
+ dsm_probe: device.dsm_probe,
|
|
+ issues,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn read_ucsi_transport(device: &str) -> Result<UcsiTransport> {
|
|
+ let contents = fs::read_to_string(format!("/scheme/acpi/resources/{device}"))
|
|
+ .with_context(|| format!("failed to read /scheme/acpi/resources/{device}"))?;
|
|
+ let resources = ron::from_str::<Vec<ResourceDescriptor>>(&contents)
|
|
+ .with_context(|| format!("failed to decode RON resources for {device}"))?;
|
|
+
|
|
+ let mut i2c = None::<I2cSerialBusDescriptor>;
|
|
+ let mut mmio = None::<(usize, usize)>;
|
|
+
|
|
+ for resource in resources {
|
|
+ match resource {
|
|
+ ResourceDescriptor::I2cSerialBus(bus) if i2c.is_none() => i2c = Some(bus),
|
|
+ ResourceDescriptor::FixedMemory32(FixedMemory32Descriptor {
|
|
+ address,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() => {
|
|
+ mmio = Some((address as usize, address_length as usize));
|
|
+ }
|
|
+ ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
|
|
+ minimum,
|
|
+ maximum,
|
|
+ address_length,
|
|
+ ..
|
|
+ }) if mmio.is_none() && maximum >= minimum => {
|
|
+ let span = maximum.saturating_sub(minimum).saturating_add(1) as usize;
|
|
+ mmio = Some((minimum as usize, span.max(address_length as usize)));
|
|
+ }
|
|
+ ResourceDescriptor::Address32(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ mmio = Some((descriptor.minimum as usize, descriptor.address_length as usize));
|
|
+ }
|
|
+ ResourceDescriptor::Address64(descriptor)
|
|
+ if mmio.is_none()
|
|
+ && matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
|
|
+ {
|
|
+ let base = usize::try_from(descriptor.minimum)
|
|
+ .context("64-bit MMIO base does not fit in usize")?;
|
|
+ let len = usize::try_from(descriptor.address_length)
|
|
+ .context("64-bit MMIO length does not fit in usize")?;
|
|
+ mmio = Some((base, len));
|
|
+ }
|
|
+ _ => {}
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if let Some(bus) = i2c {
|
|
+ let adapter = bus
|
|
+ .resource_source
|
|
+ .as_ref()
|
|
+ .map(|source| source.source.clone())
|
|
+ .filter(|source| !source.is_empty())
|
|
+ .unwrap_or_else(|| String::from("ACPI-I2C"));
|
|
+ return Ok(UcsiTransport::I2c {
|
|
+ adapter,
|
|
+ address: bus.slave_address,
|
|
+ ten_bit_address: bus.access_mode_10bit,
|
|
+ });
|
|
+ }
|
|
+ if let Some((base, len)) = mmio {
|
|
+ return Ok(UcsiTransport::Mmio { base, len });
|
|
+ }
|
|
+ Ok(UcsiTransport::Unknown)
|
|
+}
|
|
+
|
|
+fn bounded_dsm_probe(device: &str) -> Result<bool> {
|
|
+ let symbol_name = format!("{}.{}", normalize_device_path(device), "_DSM");
|
|
+ let symbol_path = format!("/scheme/acpi/symbols/{symbol_name}");
|
|
+ let fd = match libredox::Fd::open(&symbol_path, O_RDWR | O_CLOEXEC, 0) {
|
|
+ Ok(fd) => fd,
|
|
+ Err(err) => {
|
|
+ log::debug!("ucsid: {} has no callable _DSM: {err}", device);
|
|
+ return Ok(false);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let mut payload = ron::to_string(&Vec::<u8>::new())
|
|
+ .context("failed to serialize bounded _DSM probe arguments")?
|
|
+ .into_bytes();
|
|
+ payload.resize(payload.len() + 1024, 0);
|
|
+ match libredox::call::call_ro(fd.raw(), &mut payload, syscall::CallFlags::empty(), &[]) {
|
|
+ Ok(_) => Ok(true),
|
|
+ Err(err) => {
|
|
+ log::debug!("ucsid: bounded _DSM probe for {} failed: {err}", device);
|
|
+ Ok(false)
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+fn parse_capability(payload: &[u8]) -> Option<UcsiCapability> {
|
|
+ let connector_count = *payload.first()?;
|
|
+ let flags = payload.get(1).copied().unwrap_or(0);
|
|
+ Some(UcsiCapability {
|
|
+ connector_count,
|
|
+ supports_usb_pd: flags & 0x01 != 0,
|
|
+ supports_alt_modes: flags & 0x02 != 0,
|
|
+ })
|
|
+}
|
|
+
|
|
+fn query_connector_status(
|
|
+ device: &DiscoveredUcsiDevice,
|
|
+ adapters: &[I2cAdapterInfo],
|
|
+ connector: u8,
|
|
+) -> Result<UcsiConnectorSummary> {
|
|
+ match &device.transport {
|
|
+ UcsiTransport::I2c {
|
|
+ adapter,
|
|
+ address,
|
|
+ ten_bit_address,
|
|
+ } => {
|
|
+ let adapter_info = match_i2c_adapter(adapters, adapter).with_context(|| {
|
|
+ format!("no i2cd adapter matched ACPI source {} for {}", adapter, device.name)
|
|
+ })?;
|
|
+ let bytes = execute_ucsi_i2c_command(
|
|
+ adapter_info,
|
|
+ adapter,
|
|
+ *address,
|
|
+ *ten_bit_address,
|
|
+ UcsiCommand::new(GET_CONNECTOR_STATUS, 1, [connector, 0, 0, 0, 0, 0]),
|
|
+ UCSI_CONNECTOR_STATUS_READ_LEN,
|
|
+ )?;
|
|
+ let (_header, payload) = parse_ucsi_payload(&bytes)
|
|
+ .with_context(|| format!("{}: malformed connector-status response", device.name))?;
|
|
+ Ok(parse_connector_summary(&device.name, connector, payload))
|
|
+ }
|
|
+ UcsiTransport::Mmio { base, len } => bail!(
|
|
+ "MMIO connector-status transport is not implemented yet for {:#x}+{:#x}",
|
|
+ base,
|
|
+ len,
|
|
+ ),
|
|
+ UcsiTransport::Unknown => bail!("unknown UCSI transport"),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn parse_connector_summary(device_name: &str, connector: u8, payload: &[u8]) -> UcsiConnectorSummary {
|
|
+ let state = payload.first().copied().unwrap_or(0);
|
|
+ let connected = state & 0x01 != 0;
|
|
+ let power_direction = if state & 0x02 != 0 { "source" } else { "sink" };
|
|
+ let data_role = if state & 0x04 != 0 { "dfp" } else { "ufp" };
|
|
+ UcsiConnectorSummary {
|
|
+ device: device_name.to_string(),
|
|
+ connector_number: connector,
|
|
+ connected,
|
|
+ data_role: data_role.to_string(),
|
|
+ power_direction: power_direction.to_string(),
|
|
+ input_critical: classify_input_critical(device_name),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn classify_input_critical(device_name: &str) -> bool {
|
|
+ let normalized = device_name.to_ascii_lowercase();
|
|
+ normalized.contains("kbd")
|
|
+ || normalized.contains("key")
|
|
+ || normalized.contains("touch")
|
|
+ || normalized.contains("thc")
|
|
+}
|
|
+
|
|
+fn parse_ucsi_payload(bytes: &[u8]) -> Option<(UcsiResponseHeader, &[u8])> {
|
|
+ let header = UcsiResponseHeader::parse(bytes)?;
|
|
+ let body = bytes.get(UCSI_RESPONSE_HEADER_LEN..)?;
|
|
+ let body_len = usize::from(header.data_length).min(body.len());
|
|
+ Some((header, &body[..body_len]))
|
|
+}
|
|
+
|
|
+fn execute_ucsi_i2c_command(
|
|
+ adapter: &I2cAdapterInfo,
|
|
+ adapter_name: &str,
|
|
+ address: u16,
|
|
+ ten_bit_address: bool,
|
|
+ command: UcsiCommand,
|
|
+ read_len: usize,
|
|
+) -> Result<Vec<u8>> {
|
|
+ let request = I2cTransferRequest {
|
|
+ adapter: adapter_name.to_string(),
|
|
+ segments: vec![
|
|
+ I2cTransferSegment {
|
|
+ address,
|
|
+ ten_bit_address,
|
|
+ op: i2c_interface::I2cTransferOp::Write(command.as_bytes().to_vec()),
|
|
+ },
|
|
+ I2cTransferSegment {
|
|
+ address,
|
|
+ ten_bit_address,
|
|
+ op: i2c_interface::I2cTransferOp::Read(read_len),
|
|
+ },
|
|
+ ],
|
|
+ stop: true,
|
|
+ };
|
|
+
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/transfer")
|
|
+ .context("failed to open /scheme/i2c/transfer")?;
|
|
+ let payload = ron::ser::to_string(&I2cControlRequest::Transfer {
|
|
+ adapter_id: adapter.id,
|
|
+ request,
|
|
+ })
|
|
+ .context("failed to encode UCSI I2C transfer request")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to send UCSI I2C transfer request")?;
|
|
+
|
|
+ let response = read_i2c_control_response(&mut file)?;
|
|
+ match response {
|
|
+ I2cControlResponse::TransferResult(result) => {
|
|
+ if !result.ok {
|
|
+ let detail = result
|
|
+ .error
|
|
+ .clone()
|
|
+ .unwrap_or_else(|| String::from("unknown I2C transfer failure"));
|
|
+ bail!("UCSI I2C transfer failed: {detail}");
|
|
+ }
|
|
+ result
|
|
+ .read_data
|
|
+ .into_iter()
|
|
+ .next()
|
|
+ .context("UCSI I2C transfer returned no response payload")
|
|
+ }
|
|
+ I2cControlResponse::Error(message) => bail!("i2cd returned an error: {message}"),
|
|
+ other => bail!("unexpected i2cd transfer response: {other:?}"),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn list_i2c_adapters() -> Result<Vec<I2cAdapterInfo>> {
|
|
+ let mut file = OpenOptions::new()
|
|
+ .read(true)
|
|
+ .write(true)
|
|
+ .open("/scheme/i2c/adapters")
|
|
+ .context("failed to open /scheme/i2c/adapters")?;
|
|
+
|
|
+ let payload = ron::ser::to_string(&I2cControlRequest::ListAdapters)
|
|
+ .context("failed to encode I2C list-adapters request")?;
|
|
+ file.write_all(payload.as_bytes())
|
|
+ .context("failed to request I2C adapter list")?;
|
|
+
|
|
+ let response = read_i2c_control_response(&mut file)?;
|
|
+ match response {
|
|
+ I2cControlResponse::AdapterList(adapters) => Ok(adapters),
|
|
+ I2cControlResponse::Error(message) => bail!("i2cd returned an error: {message}"),
|
|
+ other => bail!("unexpected i2cd list-adapters response: {other:?}"),
|
|
+ }
|
|
+}
|
|
+
|
|
+fn match_i2c_adapter<'a>(adapters: &'a [I2cAdapterInfo], wanted: &str) -> Option<&'a I2cAdapterInfo> {
|
|
+ adapters
|
|
+ .iter()
|
|
+ .find(|adapter| adapter.name == wanted)
|
|
+ .or_else(|| adapters.iter().find(|adapter| adapter.name.ends_with(wanted)))
|
|
+ .or_else(|| adapters.iter().find(|adapter| wanted.ends_with(&adapter.name)))
|
|
+}
|
|
+
|
|
+fn read_i2c_control_response(file: &mut File) -> Result<I2cControlResponse> {
|
|
+ let mut buffer = vec![0_u8; 4096];
|
|
+ let count = file
|
|
+ .read(&mut buffer)
|
|
+ .context("failed to read I2C control response")?;
|
|
+ buffer.truncate(count);
|
|
+ let text = std::str::from_utf8(&buffer).context("I2C control response was not UTF-8")?;
|
|
+ ron::from_str(text).context("failed to decode I2C control response")
|
|
+}
|
|
+
|
|
+fn read_symbol_id(path: &Path) -> Result<Option<String>> {
|
|
+ let contents = fs::read_to_string(path)
|
|
+ .with_context(|| format!("failed to read ACPI symbol {}", path.display()))?;
|
|
+ let symbol = match ron::from_str::<AmlSymbol>(&contents) {
|
|
+ Ok(symbol) => symbol,
|
|
+ Err(err) => {
|
|
+ log::debug!(
|
|
+ "ucsid: skipping {} because the symbol payload was not a scalar ID: {err}",
|
|
+ path.display(),
|
|
+ );
|
|
+ return Ok(None);
|
|
+ }
|
|
+ };
|
|
+
|
|
+ let id = match symbol.value {
|
|
+ AmlValue::Integer(integer) => eisa_id_from_integer(integer),
|
|
+ AmlValue::String(string) => string,
|
|
+ };
|
|
+
|
|
+ log::debug!("ucsid: {} -> {id}", symbol.name);
|
|
+ Ok(Some(id))
|
|
+}
|
|
+
|
|
+fn normalize_device_path(path: &str) -> String {
|
|
+ path.trim_start_matches('\\')
|
|
+ .trim_matches('/')
|
|
+ .replace('/', ".")
|
|
+}
|
|
+
|
|
+fn eisa_id_from_integer(integer: u64) -> String {
|
|
+ let vendor = integer & 0xFFFF;
|
|
+ let device = (integer >> 16) & 0xFFFF;
|
|
+ let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
|
|
+ let vendor_1 = (((vendor_rev >> 10) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_2 = (((vendor_rev >> 5) & 0x1F) as u8 + 64) as char;
|
|
+ let vendor_3 = (((vendor_rev >> 0) & 0x1F) as u8 + 64) as char;
|
|
+ let device_1 = (device >> 4) & 0xF;
|
|
+ let device_2 = (device >> 0) & 0xF;
|
|
+ let device_3 = (device >> 12) & 0xF;
|
|
+ let device_4 = (device >> 8) & 0xF;
|
|
+
|
|
+ format!(
|
|
+ "{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
|
|
+ )
|
|
+}
|