i2c: real DesignWare transfer chain — engine, PCI discovery, provider routing
The I2C transfer chain was stubbed end-to-end: - i2cd's transfer path returned 'not implemented yet' for every transfer, and its wire format didn't match the only in-tree consumer (i2c-hidd sends a bare I2cTransferRequest, i2cd expected I2cControlRequest::Transfer). - intel-lpss-i2cd / dw-acpi-i2cd / amd-mp2-i2cd registered an adapter name and parked forever without initializing the controller or executing a transfer. - intel-lpss-i2cd matched only legacy ACPI HIDs, which do not exist on Meteor Lake / Arrow Lake platforms (LPSS I2C binds by PCI ID there, per Linux drivers/mfd/intel-lpss-pci.c). Replace with real implementations: - New shared crate drivers/i2c/designware (dw-i2c): DesignWare I2C master engine ported from Linux 7.1 i2c-designware-master.c and i2c-designware-common.c — IC_CON + SCL timing computed from ic_clk with Linux's exact hcnt/lcnt formulas (bxt_i2c_info 133 MHz for LPSS, 100 MHz for ACPI-designated blocks), SDA hold with RX-hold workaround, polling transfer engine with RESTART/STOP sequencing, TX_ABRT decode (Nack/arbitration-lost/abort/timeout), 7/10-bit addressing, bounded timeouts. Also provides the shared /scheme/<name> transfer endpoint with register-then-ready ordering. - intel-lpss-i2cd: PCI discovery for ARL-H (0x7750/0x7751, 0x7778-0x777b) and MTL-P (0x7e50/0x7e51, 0x7e78-0x7e7b) with 32/64-bit BAR0 decode, ACPI alias resolution via FixedMemory32 == BAR0 across /scheme/acpi/resources, legacy ACPI-HID path kept, i2cd registration with bounded retry, serves /scheme/i2c-lpss. - dw-acpi-i2cd: converted from register-and-park to the shared engine, serves /scheme/dw-acpi-i2c. - i2cd: real transfer routing — adapter resolution by name/alias (exact, normalized, last-component) and forwarding to the provider daemon's /scheme/<provider>/transfer endpoint. - i2c-interface: I2cAdapterInfo gains provider_scheme + aliases (serde-default, wire-compatible). Tests: dw-i2c 3, intel-lpss-i2cd 3; full workspace cargo check clean; base cooks for x86_64-unknown-redox. Refs: local/docs/LG-GRAM-16Z90TP-COMPATIBILITY-PLAN.md Phase 4
This commit is contained in:
Generated
+18
@@ -678,6 +678,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"daemon",
|
||||
"dw-i2c",
|
||||
"i2c-interface",
|
||||
"libredox",
|
||||
"log",
|
||||
@@ -686,6 +687,20 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dw-i2c"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"i2c-interface",
|
||||
"log",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.9.0+rb0.3.1",
|
||||
"ron",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "e1000d"
|
||||
version = "0.1.0"
|
||||
@@ -1250,11 +1265,14 @@ dependencies = [
|
||||
"anyhow",
|
||||
"common",
|
||||
"daemon",
|
||||
"dw-i2c",
|
||||
"i2c-interface",
|
||||
"libredox",
|
||||
"log",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.9.0+rb0.3.1",
|
||||
"ron",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ members = [
|
||||
"drivers/i2c/i2c-interface",
|
||||
"drivers/i2c/i2cd",
|
||||
"drivers/i2c/amd-mp2-i2cd",
|
||||
"drivers/i2c/designware",
|
||||
"drivers/i2c/dw-acpi-i2cd",
|
||||
"drivers/i2c/intel-lpss-i2cd",
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ fn daemon_main(daemon: daemon::Daemon, pcid_handle: &mut PciFunctionHandle) -> R
|
||||
name: format!("amd-mp2:{}", pci_config.func.name()),
|
||||
max_transaction_size: 0,
|
||||
supports_10bit_addr: false,
|
||||
provider_scheme: String::new(),
|
||||
aliases: Vec::new(),
|
||||
};
|
||||
let mut registration = register_adapter(&info)
|
||||
.context("failed to register AMD MP2 controller with i2cd")?;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "dw-i2c"
|
||||
description = "Synopsys DesignWare I2C master controller engine (Linux i2c-designware port)"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
log.workspace = true
|
||||
redox_syscall = { workspace = true, features = ["std"] }
|
||||
redox-scheme.workspace = true
|
||||
ron.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
i2c-interface = { path = "../i2c-interface" }
|
||||
scheme-utils = { path = "../../../scheme-utils" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,671 @@
|
||||
//! Synopsys DesignWare I2C master controller engine.
|
||||
//!
|
||||
//! Ported from Linux 7.1 `drivers/i2c/busses/i2c-designware-master.c` and
|
||||
//! `i2c-designware-common.c` (register model in `i2c-designware-core.h`).
|
||||
//! Polling variant: transfers complete by polling IC_STATUS with bounded
|
||||
//! timeouts instead of interrupt-driven completion, the same approach used
|
||||
//! by the U-Boot DesignWare driver. Transfer semantics (RESTART/STOP
|
||||
//! sequencing, TX_ABRT error decode, 7/10-bit addressing) follow the Linux
|
||||
//! master driver.
|
||||
|
||||
use std::fmt;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// Register offsets (i2c-designware-core.h).
|
||||
const DW_IC_CON: usize = 0x00;
|
||||
const DW_IC_TAR: usize = 0x04;
|
||||
const DW_IC_DATA_CMD: usize = 0x10;
|
||||
const DW_IC_SS_SCL_HCNT: usize = 0x14;
|
||||
const DW_IC_SS_SCL_LCNT: usize = 0x18;
|
||||
const DW_IC_FS_SCL_HCNT: usize = 0x1c;
|
||||
const DW_IC_FS_SCL_LCNT: usize = 0x20;
|
||||
const DW_IC_INTR_MASK: usize = 0x30;
|
||||
const DW_IC_RAW_INTR_STAT: usize = 0x34;
|
||||
const DW_IC_CLR_INTR: usize = 0x40;
|
||||
const DW_IC_CLR_TX_ABRT: usize = 0x54;
|
||||
const DW_IC_ENABLE: usize = 0x6c;
|
||||
const DW_IC_STATUS: usize = 0x70;
|
||||
const DW_IC_SDA_HOLD: usize = 0x7c;
|
||||
const DW_IC_TX_ABRT_SOURCE: usize = 0x80;
|
||||
const DW_IC_COMP_VERSION: usize = 0xf8;
|
||||
|
||||
// IC_CON bits (i2c-designware-core.h).
|
||||
const IC_CON_MASTER: u32 = 1 << 0;
|
||||
const IC_CON_SPEED_FAST: u32 = 2 << 1;
|
||||
const IC_CON_10BITADDR_MASTER: u32 = 1 << 4;
|
||||
const IC_CON_RESTART_EN: u32 = 1 << 5;
|
||||
const IC_CON_SLAVE_DISABLE: u32 = 1 << 6;
|
||||
|
||||
// IC_DATA_CMD bits: [7:0] data, [8] read, [9] stop, [10] restart.
|
||||
const IC_DATA_CMD_READ: u32 = 1 << 8;
|
||||
const IC_DATA_CMD_STOP: u32 = 1 << 9;
|
||||
const IC_DATA_CMD_RESTART: u32 = 1 << 10;
|
||||
|
||||
// IC_STATUS bits (i2c-designware-core.h).
|
||||
const IC_STATUS_ACTIVITY: u32 = 1 << 0;
|
||||
const IC_STATUS_TFNF: u32 = 1 << 1;
|
||||
const IC_STATUS_TFE: u32 = 1 << 2;
|
||||
const IC_STATUS_RFNE: u32 = 1 << 3;
|
||||
const IC_STATUS_MASTER_ACTIVITY: u32 = 1 << 5;
|
||||
|
||||
// IC_RAW_INTR_STAT bit for transfer abort.
|
||||
const IC_RAW_TX_ABRT: u32 = 1 << 6;
|
||||
|
||||
// IC_TX_ABRT_SOURCE codes (i2c-designware-core.h).
|
||||
const ABRT_7B_ADDR_NOACK: u32 = 1 << 0;
|
||||
const ABRT_10ADDR1_NOACK: u32 = 1 << 1;
|
||||
const ABRT_10ADDR2_NOACK: u32 = 1 << 2;
|
||||
const ABRT_TXDATA_NOACK: u32 = 1 << 3;
|
||||
const ABRT_GCALL_NOACK: u32 = 1 << 4;
|
||||
const ABRT_MASTER_DIS: u32 = 1 << 11;
|
||||
const ABRT_ARB_LOST: u32 = 1 << 12;
|
||||
const ABRT_NOACK_MASK: u32 = ABRT_7B_ADDR_NOACK
|
||||
| ABRT_10ADDR1_NOACK
|
||||
| ABRT_10ADDR2_NOACK
|
||||
| ABRT_TXDATA_NOACK
|
||||
| ABRT_GCALL_NOACK;
|
||||
|
||||
const IC_SDA_HOLD_MIN_VERS: u32 = 0x3131_312a; // "111*"
|
||||
const IC_SDA_HOLD_RX_SHIFT: u32 = 16;
|
||||
|
||||
const DW_IC_TAR_10BITADDR_MASTER: u32 = 1 << 12;
|
||||
|
||||
/// Polling budget for a single byte-level wait. HID-over-I2C workloads are
|
||||
/// small; 50 ms is generous even for a heavily loaded 100 kHz bus.
|
||||
const BYTE_WAIT_TIMEOUT: Duration = Duration::from_millis(50);
|
||||
/// Polling budget for whole-transfer completion (FIFO drain + bus idle).
|
||||
const COMPLETION_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DwError {
|
||||
/// Slave addressed NACKed (address or data phase).
|
||||
Nack,
|
||||
/// Arbitration lost (multi-master contention).
|
||||
ArbitrationLost,
|
||||
/// Hardware abort that is not a simple NACK.
|
||||
Abort(u32),
|
||||
/// Polling timeout waiting for FIFO space, data, or bus idle.
|
||||
Timeout,
|
||||
/// Controller reported MASTER_DIS abort (master mode disabled).
|
||||
MasterDisabled,
|
||||
}
|
||||
|
||||
impl fmt::Display for DwError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DwError::Nack => write!(f, "slave NACK"),
|
||||
DwError::ArbitrationLost => write!(f, "arbitration lost"),
|
||||
DwError::Abort(src) => write!(f, "transfer abort (IC_TX_ABRT_SOURCE={src:#x})"),
|
||||
DwError::Timeout => write!(f, "transfer timeout"),
|
||||
DwError::MasterDisabled => write!(f, "master mode disabled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DwError {}
|
||||
|
||||
/// SCL timing parameters for one bus speed, derived from the controller
|
||||
/// clock exactly as Linux's `i2c_dw_scl_hcnt`/`i2c_dw_scl_lcnt` do.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SclTiming {
|
||||
pub hcnt: u32,
|
||||
pub lcnt: u32,
|
||||
pub sda_hold_tx: u32,
|
||||
pub sda_fall_ns: u32,
|
||||
pub scl_fall_ns: u32,
|
||||
}
|
||||
|
||||
/// Bus timing inputs in the same shape Linux's intel-lpss-pci.c carries them
|
||||
/// (`bxt_i2c_info`: 133 MHz clock, sda-hold 42 ns, sda-fall 171 ns,
|
||||
/// scl-fall 208 ns).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BusTiming {
|
||||
pub ic_clk_hz: u32,
|
||||
pub sda_hold_ns: u32,
|
||||
pub sda_fall_ns: u32,
|
||||
pub scl_fall_ns: u32,
|
||||
}
|
||||
|
||||
/// Broxton-derived LPSS timing used by every modern Intel LPSS I2C block
|
||||
/// (ARL-H 0x7778/0x7779 and MTL-P 0x7e50/0x7e78 families per
|
||||
/// `intel-lpss-pci.c`: `.clk_rate = 133000000`, `bxt_i2c_properties`).
|
||||
pub const BXT_I2C_TIMING: BusTiming = BusTiming {
|
||||
ic_clk_hz: 133_000_000,
|
||||
sda_hold_ns: 42,
|
||||
sda_fall_ns: 171,
|
||||
scl_fall_ns: 208,
|
||||
};
|
||||
|
||||
impl BusTiming {
|
||||
/// Fast-mode (400 kHz) SCL counts. Formulas from
|
||||
/// `i2c-designware-common.c:i2c_dw_scl_hcnt/lcnt`:
|
||||
/// hcnt = IC_CLK * (tHD;STA + tf) - 3
|
||||
/// lcnt = IC_CLK * (tLOW + tf) - 1
|
||||
/// with tHD;STA = 0.6 us and tLOW = 1.3 us (I2C fast-mode spec).
|
||||
pub fn fast_mode(&self) -> SclTiming {
|
||||
let clk_khz = self.ic_clk_hz / 1000;
|
||||
let hcnt = (clk_khz as u64 * (600 + self.scl_fall_ns as u64) / 1_000_000)
|
||||
.saturating_sub(3) as u32;
|
||||
let lcnt = (clk_khz as u64 * (1300 + self.scl_fall_ns as u64) / 1_000_000)
|
||||
.saturating_sub(1) as u32;
|
||||
// IC_SDA_HOLD TX value is in IC_CLK cycles (datasheet §5.11).
|
||||
let sda_hold_tx = (clk_khz as u64 * self.sda_hold_ns as u64).div_ceil(1_000_000) as u32;
|
||||
SclTiming {
|
||||
hcnt,
|
||||
lcnt,
|
||||
sda_hold_tx,
|
||||
sda_fall_ns: self.sda_fall_ns,
|
||||
scl_fall_ns: self.scl_fall_ns,
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard-mode (100 kHz) SCL counts: tHD;STA = 4.0 us, tLOW = 4.7 us.
|
||||
pub fn standard_mode(&self) -> SclTiming {
|
||||
let clk_khz = self.ic_clk_hz / 1000;
|
||||
let hcnt = (clk_khz as u64 * (4000 + self.scl_fall_ns as u64) / 1_000_000)
|
||||
.saturating_sub(3) as u32;
|
||||
let lcnt = (clk_khz as u64 * (4700 + self.scl_fall_ns as u64) / 1_000_000)
|
||||
.saturating_sub(1) as u32;
|
||||
let sda_hold_tx = (clk_khz as u64 * self.sda_hold_ns as u64).div_ceil(1_000_000) as u32;
|
||||
SclTiming {
|
||||
hcnt,
|
||||
lcnt,
|
||||
sda_hold_tx,
|
||||
sda_fall_ns: self.sda_fall_ns,
|
||||
scl_fall_ns: self.scl_fall_ns,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One transfer segment: either a write of `data` or a read of `len` bytes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Segment {
|
||||
Write(Vec<u8>),
|
||||
Read(usize),
|
||||
}
|
||||
|
||||
/// DesignWare master engine over a mapped MMIO register block.
|
||||
pub struct DwI2c {
|
||||
base: *mut u8,
|
||||
}
|
||||
|
||||
// The MMIO block is process-local and the driver serves transfers from a
|
||||
// single scheme handler thread.
|
||||
unsafe impl Send for DwI2c {}
|
||||
|
||||
impl DwI2c {
|
||||
/// # Safety
|
||||
/// `base` must point at the start of a mapped DesignWare register block
|
||||
/// of at least 0x100 bytes, exclusively owned by the caller.
|
||||
pub unsafe fn new(base: *mut u8) -> Self {
|
||||
Self { base }
|
||||
}
|
||||
|
||||
fn read32(&self, reg: usize) -> u32 {
|
||||
unsafe { (self.base.add(reg) as *const u32).read_volatile() }
|
||||
}
|
||||
|
||||
fn write32(&self, reg: usize, value: u32) {
|
||||
unsafe { (self.base.add(reg) as *mut u32).write_volatile(value) }
|
||||
}
|
||||
|
||||
fn enabled(&self) -> bool {
|
||||
self.read32(DW_IC_ENABLE) & 1 != 0
|
||||
}
|
||||
|
||||
fn set_enabled(&self, enable: bool) {
|
||||
self.write32(DW_IC_ENABLE, enable as u32);
|
||||
}
|
||||
|
||||
/// Initialize the controller for master-mode operation.
|
||||
///
|
||||
/// Mirrors `i2c_dw_init_master` (i2c-designware-master.c): disable,
|
||||
/// program IC_CON and SCL counts for both speed grades, apply SDA hold
|
||||
/// times with the RX-hold workaround, mask all interrupts (polling
|
||||
/// driver), flush stale state, then enable.
|
||||
pub fn init(&self, timing: &BusTiming) {
|
||||
let fast = timing.fast_mode();
|
||||
let standard = timing.standard_mode();
|
||||
|
||||
self.set_enabled(false);
|
||||
|
||||
let con = IC_CON_MASTER
|
||||
| IC_CON_SPEED_FAST
|
||||
| IC_CON_RESTART_EN
|
||||
| IC_CON_SLAVE_DISABLE;
|
||||
self.write32(DW_IC_CON, con);
|
||||
|
||||
self.write32(DW_IC_SS_SCL_HCNT, standard.hcnt);
|
||||
self.write32(DW_IC_SS_SCL_LCNT, standard.lcnt);
|
||||
self.write32(DW_IC_FS_SCL_HCNT, fast.hcnt);
|
||||
self.write32(DW_IC_FS_SCL_LCNT, fast.lcnt);
|
||||
|
||||
// i2c_dw_set_sda_hold: only controllers at version >= 1.11 have a
|
||||
// writable IC_SDA_HOLD; apply the TX hold and force a 1-cycle RX
|
||||
// hold to avoid TX arbitration loss against fast slaves.
|
||||
if self.read32(DW_IC_COMP_VERSION) >= IC_SDA_HOLD_MIN_VERS {
|
||||
let mut hold = fast.sda_hold_tx;
|
||||
if self.read32(DW_IC_SDA_HOLD) >> IC_SDA_HOLD_RX_SHIFT == 0 {
|
||||
hold |= 1 << IC_SDA_HOLD_RX_SHIFT;
|
||||
}
|
||||
self.write32(DW_IC_SDA_HOLD, hold);
|
||||
}
|
||||
|
||||
// Polling driver: no interrupt sources, clear any pending state.
|
||||
self.write32(DW_IC_INTR_MASK, 0);
|
||||
let _ = self.read32(DW_IC_CLR_INTR);
|
||||
let _ = self.read32(DW_IC_CLR_TX_ABRT);
|
||||
|
||||
self.set_enabled(true);
|
||||
}
|
||||
|
||||
fn wait_for(&self, mut condition: impl FnMut(u32) -> bool, timeout: Duration) -> Result<(), DwError> {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
let status = self.read32(DW_IC_STATUS);
|
||||
if condition(status) {
|
||||
return Ok(());
|
||||
}
|
||||
if self.read32(DW_IC_RAW_INTR_STAT) & IC_RAW_TX_ABRT != 0 {
|
||||
return Err(self.decode_abort());
|
||||
}
|
||||
if start.elapsed() > timeout {
|
||||
return Err(DwError::Timeout);
|
||||
}
|
||||
std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_abort(&self) -> DwError {
|
||||
let source = self.read32(DW_IC_TX_ABRT_SOURCE);
|
||||
let _ = self.read32(DW_IC_CLR_TX_ABRT);
|
||||
if source & ABRT_NOACK_MASK != 0 {
|
||||
DwError::Nack
|
||||
} else if source & ABRT_ARB_LOST != 0 {
|
||||
DwError::ArbitrationLost
|
||||
} else if source & ABRT_MASTER_DIS != 0 {
|
||||
DwError::MasterDisabled
|
||||
} else {
|
||||
DwError::Abort(source)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_target_address(&self, address: u16, ten_bit: bool) {
|
||||
let mut con = self.read32(DW_IC_CON);
|
||||
if ten_bit {
|
||||
con |= IC_CON_10BITADDR_MASTER;
|
||||
} else {
|
||||
con &= !IC_CON_10BITADDR_MASTER;
|
||||
}
|
||||
self.write32(DW_IC_CON, con);
|
||||
|
||||
let mut tar = u32::from(address & 0x3ff);
|
||||
if ten_bit {
|
||||
tar |= DW_IC_TAR_10BITADDR_MASTER;
|
||||
}
|
||||
self.write32(DW_IC_TAR, tar);
|
||||
}
|
||||
|
||||
/// Execute a sequence of write/read segments against one slave address,
|
||||
/// issuing a repeated START between segments and a STOP after the last
|
||||
/// one (or leaving the bus held when `stop` is false).
|
||||
///
|
||||
/// Mirrors `i2c_dw_xfer` segment handling (i2c-designware-master.c):
|
||||
/// writes feed IC_DATA_CMD with STOP on the final byte of the final
|
||||
/// segment, reads issue IC_DATA_CMD_READ commands (RESTART on the first
|
||||
/// read command after a direction change) and drain RX FIFO as data
|
||||
/// arrives.
|
||||
pub fn xfer(
|
||||
&self,
|
||||
address: u16,
|
||||
ten_bit: bool,
|
||||
segments: &[Segment],
|
||||
stop: bool,
|
||||
) -> Result<Vec<Vec<u8>>, DwError> {
|
||||
if segments.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if !self.enabled() {
|
||||
return Err(DwError::MasterDisabled);
|
||||
}
|
||||
|
||||
self.set_target_address(address, ten_bit);
|
||||
let _ = self.read32(DW_IC_CLR_TX_ABRT);
|
||||
|
||||
let mut reads: Vec<Vec<u8>> = Vec::new();
|
||||
let last = segments.len() - 1;
|
||||
|
||||
for (index, segment) in segments.iter().enumerate() {
|
||||
let is_last_segment = index == last;
|
||||
match segment {
|
||||
Segment::Write(data) => {
|
||||
for (byte_index, byte) in data.iter().enumerate() {
|
||||
let mut cmd = u32::from(*byte);
|
||||
if is_last_segment && byte_index == data.len() - 1 && stop {
|
||||
cmd |= IC_DATA_CMD_STOP;
|
||||
}
|
||||
self.wait_for(
|
||||
|status| status & IC_STATUS_TFNF != 0,
|
||||
BYTE_WAIT_TIMEOUT,
|
||||
)?;
|
||||
self.write32(DW_IC_DATA_CMD, cmd);
|
||||
}
|
||||
}
|
||||
Segment::Read(len) => {
|
||||
let mut buf = Vec::with_capacity(*len);
|
||||
for byte_index in 0..*len {
|
||||
let mut cmd = IC_DATA_CMD_READ;
|
||||
if byte_index == 0 {
|
||||
cmd |= IC_DATA_CMD_RESTART;
|
||||
}
|
||||
if is_last_segment && byte_index == len - 1 && stop {
|
||||
cmd |= IC_DATA_CMD_STOP;
|
||||
}
|
||||
self.wait_for(
|
||||
|status| status & IC_STATUS_TFNF != 0,
|
||||
BYTE_WAIT_TIMEOUT,
|
||||
)?;
|
||||
self.write32(DW_IC_DATA_CMD, cmd);
|
||||
|
||||
self.wait_for(
|
||||
|status| status & IC_STATUS_RFNE != 0,
|
||||
BYTE_WAIT_TIMEOUT,
|
||||
)?;
|
||||
buf.push((self.read32(DW_IC_DATA_CMD) & 0xff) as u8);
|
||||
}
|
||||
reads.push(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the transfer to drain and the bus to go idle
|
||||
// (i2c_dw_wait_bus_not_busy).
|
||||
self.wait_for(
|
||||
|status| {
|
||||
status & IC_STATUS_TFE != 0
|
||||
&& status & (IC_STATUS_ACTIVITY | IC_STATUS_MASTER_ACTIVITY) == 0
|
||||
},
|
||||
COMPLETION_TIMEOUT,
|
||||
)?;
|
||||
|
||||
Ok(reads)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bxt_fast_mode_matches_linux_formula() {
|
||||
// 133 MHz, tHD;STA=0.6us, tLOW=1.3us, scl_fall=208ns:
|
||||
// hcnt = 133000*808/1e6 - 3 = 104, lcnt = 133000*1508/1e6 - 1 = 199.
|
||||
let timing = BXT_I2C_TIMING.fast_mode();
|
||||
assert_eq!(timing.hcnt, 104);
|
||||
assert_eq!(timing.lcnt, 199);
|
||||
// sda_hold_tx = ceil(133000 * 42 / 1e6) = ceil(5.586) = 6.
|
||||
assert_eq!(timing.sda_hold_tx, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bxt_standard_mode_matches_linux_formula() {
|
||||
// hcnt = 133000*4208/1e6 - 3 = 556, lcnt = 133000*4908/1e6 - 1 = 651.
|
||||
let timing = BXT_I2C_TIMING.standard_mode();
|
||||
assert_eq!(timing.hcnt, 556);
|
||||
assert_eq!(timing.lcnt, 651);
|
||||
assert_eq!(timing.sda_hold_tx, 6);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod endpoint {
|
||||
//! Shared `/scheme/<name>` transfer endpoint for DesignWare-based
|
||||
//! controller daemons. Accepts bare `I2cTransferRequest` payloads and
|
||||
//! executes them on the matching controller's engine.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use i2c_interface::{I2cTransferRequest, I2cTransferResponse};
|
||||
use redox_scheme::scheme::{register_sync_scheme, SchemeSync};
|
||||
use redox_scheme::{CallerCtx, OpenResult, Socket};
|
||||
use scheme_utils::{Blocking, HandleMap};
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
use syscall::{Error as SysError, EBADF, EINVAL, ENOENT};
|
||||
|
||||
use super::{DwI2c, Segment};
|
||||
|
||||
/// A brought-up controller: initialized engine plus the names consumers
|
||||
/// use to address it (driver name and ACPI aliases).
|
||||
pub struct EndpointController {
|
||||
pub name: String,
|
||||
pub aliases: Vec<String>,
|
||||
pub engine: DwI2c,
|
||||
}
|
||||
|
||||
enum Handle {
|
||||
SchemeRoot,
|
||||
Transfer { pending: Vec<u8> },
|
||||
}
|
||||
|
||||
struct Endpoint {
|
||||
handles: HandleMap<Handle>,
|
||||
controllers: Vec<EndpointController>,
|
||||
}
|
||||
|
||||
impl Endpoint {
|
||||
fn execute(&self, request: &I2cTransferRequest) -> I2cTransferResponse {
|
||||
if request.segments.is_empty() {
|
||||
return I2cTransferResponse {
|
||||
ok: true,
|
||||
read_data: Vec::new(),
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
|
||||
let Some(controller) = self
|
||||
.controllers
|
||||
.iter()
|
||||
.find(|controller| adapter_matches(&controller.name, &controller.aliases, &request.adapter))
|
||||
else {
|
||||
return I2cTransferResponse {
|
||||
ok: false,
|
||||
read_data: Vec::new(),
|
||||
error: Some(format!(
|
||||
"no controller matches adapter '{}'",
|
||||
request.adapter
|
||||
)),
|
||||
};
|
||||
};
|
||||
|
||||
let first = &request.segments[0];
|
||||
let (address, ten_bit) = (first.address, first.ten_bit_address);
|
||||
if request
|
||||
.segments
|
||||
.iter()
|
||||
.any(|segment| segment.address != address || segment.ten_bit_address != ten_bit)
|
||||
{
|
||||
return I2cTransferResponse {
|
||||
ok: false,
|
||||
read_data: Vec::new(),
|
||||
error: Some("mixed-address segment sequences are not supported".to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let ops: Vec<Segment> = request
|
||||
.segments
|
||||
.iter()
|
||||
.map(|segment| match &segment.op {
|
||||
i2c_interface::I2cTransferOp::Write(data) => Segment::Write(data.clone()),
|
||||
i2c_interface::I2cTransferOp::Read(len) => Segment::Read(*len),
|
||||
})
|
||||
.collect();
|
||||
|
||||
match controller.engine.xfer(address, ten_bit, &ops, request.stop) {
|
||||
Ok(reads) => I2cTransferResponse {
|
||||
ok: true,
|
||||
read_data: reads,
|
||||
error: None,
|
||||
},
|
||||
Err(err) => I2cTransferResponse {
|
||||
ok: false,
|
||||
read_data: Vec::new(),
|
||||
error: Some(format!("{}: {err}", controller.name)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter name matching tolerant to ACPI path normalization forms:
|
||||
/// exact match, then last-dot-component match
|
||||
/// ("I2C0" ≡ "PC00.I2C0" ≡ "\_SB_.PC00.I2C0").
|
||||
fn adapter_matches(name: &str, aliases: &[String], requested: &str) -> bool {
|
||||
fn last_component(value: &str) -> &str {
|
||||
value.rsplit('.').next().unwrap_or(value)
|
||||
}
|
||||
|
||||
let normalized = requested
|
||||
.trim_start_matches("\\_SB_.")
|
||||
.trim_start_matches('_');
|
||||
if name == requested || aliases.iter().any(|alias| alias == requested || alias == &normalized) {
|
||||
return true;
|
||||
}
|
||||
if last_component(name) == last_component(normalized) {
|
||||
return true;
|
||||
}
|
||||
aliases
|
||||
.iter()
|
||||
.any(|alias| last_component(alias) == last_component(normalized))
|
||||
}
|
||||
|
||||
impl SchemeSync for Endpoint {
|
||||
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 => match segments {
|
||||
"transfer" => Handle::Transfer {
|
||||
pending: Vec::new(),
|
||||
},
|
||||
_ => return Err(SysError::new(ENOENT)),
|
||||
},
|
||||
_ => return Err(SysError::new(EBADF)),
|
||||
};
|
||||
|
||||
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 handle = self.handles.get_mut(id)?;
|
||||
let pending = match handle {
|
||||
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)
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &[u8],
|
||||
_offset: u64,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> syscall::Result<usize> {
|
||||
if !matches!(self.handles.get(id)?, Handle::Transfer { .. }) {
|
||||
return Err(SysError::new(EBADF));
|
||||
}
|
||||
|
||||
let text = std::str::from_utf8(buf).map_err(|_| SysError::new(EINVAL))?;
|
||||
let request: I2cTransferRequest =
|
||||
ron::from_str(text).map_err(|_| SysError::new(EINVAL))?;
|
||||
|
||||
let response = self.execute(&request);
|
||||
let pending = ron::ser::to_string(&response)
|
||||
.map(|text| text.into_bytes())
|
||||
.map_err(|_| SysError::new(EINVAL))?;
|
||||
|
||||
let handle = self.handles.get_mut(id)?;
|
||||
let Handle::Transfer { pending: slot } = handle else {
|
||||
return Err(SysError::new(EBADF));
|
||||
};
|
||||
*slot = pending;
|
||||
Ok(buf.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Register `/scheme/<scheme_name>` and serve transfer requests forever.
|
||||
///
|
||||
/// `on_ready` runs after the scheme is registered but before requests
|
||||
/// are processed — daemons use it for their init-system readiness
|
||||
/// notification (matching the xhcid ordering: register, then ready).
|
||||
pub fn serve(
|
||||
scheme_name: &str,
|
||||
controllers: Vec<EndpointController>,
|
||||
on_ready: impl FnOnce(),
|
||||
) -> Result<()> {
|
||||
let socket = Socket::create().context("failed to create scheme socket")?;
|
||||
let mut endpoint = Endpoint {
|
||||
handles: HandleMap::new(),
|
||||
controllers,
|
||||
};
|
||||
register_sync_scheme(&socket, scheme_name, &mut endpoint)
|
||||
.with_context(|| format!("failed to register {scheme_name} scheme"))?;
|
||||
|
||||
log::info!(
|
||||
"dw-i2c: serving /scheme/{scheme_name} with {} controller(s)",
|
||||
endpoint.controllers.len()
|
||||
);
|
||||
|
||||
on_ready();
|
||||
|
||||
let blocking = Blocking::new(&socket, 16);
|
||||
blocking
|
||||
.process_requests_blocking(endpoint)
|
||||
.context("scheme handler exited")?;
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adapter_name_matching_forms() {
|
||||
let aliases = vec!["PC00.I2C0".to_string()];
|
||||
assert!(adapter_matches("intel-lpss:00--15.0", &aliases, "PC00.I2C0"));
|
||||
assert!(adapter_matches("intel-lpss:00--15.0", &aliases, "\\_SB_.PC00.I2C0"));
|
||||
assert!(adapter_matches("intel-lpss:00--15.0", &aliases, "I2C0"));
|
||||
assert!(!adapter_matches("intel-lpss:00--15.0", &aliases, "I2C3"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ ron.workspace = true
|
||||
acpi-resource = { path = "../../acpi-resource" }
|
||||
common = { path = "../../common" }
|
||||
daemon = { path = "../../../daemon" }
|
||||
dw-i2c = { path = "../designware" }
|
||||
i2c-interface = { path = "../i2c-interface" }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -13,18 +13,22 @@ use common::{MemoryType, PhysBorrowed, Prot};
|
||||
use i2c_interface::{I2cAdapterInfo, I2cControlRequest, I2cControlResponse};
|
||||
use serde::Deserialize;
|
||||
|
||||
use dw_i2c::endpoint::EndpointController;
|
||||
use dw_i2c::{BusTiming, DwI2c};
|
||||
|
||||
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>();
|
||||
// Baytrail/Cherrytrail/AMD DesignWare blocks clock the controller at 100 MHz
|
||||
// (Linux i2c-designware-pcidrv.c: ehl_get_clk_rate_khz and
|
||||
// navi_amd_get_clk_rate_khz both return 100000). The fall-time constants
|
||||
// reuse the Broxton values; the formula produces slower-than-spec timings
|
||||
// on faster blocks, which is the safe direction.
|
||||
const DW_100MHZ_TIMING: BusTiming = BusTiming {
|
||||
ic_clk_hz: 100_000_000,
|
||||
sda_hold_ns: 42,
|
||||
sda_fall_ns: 171,
|
||||
scl_fall_ns: 208,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AmlSymbol {
|
||||
@@ -53,11 +57,6 @@ struct ControllerDescriptor {
|
||||
resources: ControllerResources,
|
||||
}
|
||||
|
||||
struct RegisteredController {
|
||||
_mmio: Option<PhysBorrowed>,
|
||||
_registration: File,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
common::setup_logging(
|
||||
"bus",
|
||||
@@ -88,22 +87,26 @@ fn daemon_main(daemon: daemon::Daemon) -> Result<()> {
|
||||
log::info!("dw-acpi-i2cd: no supported ACPI controllers found");
|
||||
}
|
||||
|
||||
let mut registered = Vec::new();
|
||||
let mut guards = Vec::new();
|
||||
let mut endpoint_controllers = 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:#}"),
|
||||
match bring_up_controller("dw-acpi", controller) {
|
||||
Ok((endpoint, guard)) => {
|
||||
endpoint_controllers.push(endpoint);
|
||||
guards.push(guard);
|
||||
}
|
||||
Err(err) => log::warn!("dw-acpi-i2cd: controller bring-up 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();
|
||||
}
|
||||
// Registrations and MMIO mappings must outlive the scheme endpoint.
|
||||
let _guards = guards;
|
||||
dw_i2c::endpoint::serve("dw-acpi-i2c", endpoint_controllers, || {
|
||||
daemon.ready();
|
||||
if let Err(err) = libredox::call::setrens(0, 0) {
|
||||
log::error!("dw-acpi-i2cd: failed to enter null namespace: {err}");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn discover_controllers(supported_ids: &[&str]) -> Result<Vec<ControllerDescriptor>> {
|
||||
@@ -199,7 +202,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
address_length,
|
||||
..
|
||||
}) if mmio.is_none() => {
|
||||
mmio = Some((*address as usize, (*address_length as usize).max(DW_MMIO_WINDOW)));
|
||||
mmio = Some((*address as usize, (*address_length as usize).max(0x100)));
|
||||
}
|
||||
ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
|
||||
minimum,
|
||||
@@ -210,7 +213,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
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)),
|
||||
span.max((*address_length as usize).max(0x100)),
|
||||
));
|
||||
}
|
||||
ResourceDescriptor::Address32(descriptor)
|
||||
@@ -219,7 +222,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
{
|
||||
mmio = Some((
|
||||
descriptor.minimum as usize,
|
||||
(descriptor.address_length as usize).max(DW_MMIO_WINDOW),
|
||||
(descriptor.address_length as usize).max(0x100),
|
||||
));
|
||||
}
|
||||
ResourceDescriptor::Address64(descriptor)
|
||||
@@ -230,7 +233,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
.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)));
|
||||
mmio = Some((base, len.max(0x100)));
|
||||
}
|
||||
ResourceDescriptor::Irq(IrqDescriptor { interrupts, .. }) if irq.is_none() => {
|
||||
irq = interrupts.first().copied().map(u32::from);
|
||||
@@ -256,29 +259,31 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
})
|
||||
}
|
||||
|
||||
fn register_controller(prefix: &str, controller: ControllerDescriptor) -> Result<RegisteredController> {
|
||||
fn bring_up_controller(
|
||||
prefix: &str,
|
||||
controller: ControllerDescriptor,
|
||||
) -> Result<(EndpointController, (PhysBorrowed, File))> {
|
||||
let ControllerDescriptor {
|
||||
device,
|
||||
hid,
|
||||
resources,
|
||||
} = controller;
|
||||
|
||||
let mmio = match PhysBorrowed::map(
|
||||
let mmio = 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
|
||||
}
|
||||
};
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to map MMIO for {device} ({:#x}, len {:#x})",
|
||||
resources.mmio_base, resources.mmio_len
|
||||
)
|
||||
})?;
|
||||
|
||||
let engine = unsafe { DwI2c::new(mmio.as_ptr() as *mut u8) };
|
||||
engine.init(&DW_100MHZ_TIMING);
|
||||
|
||||
log::info!(
|
||||
"dw-acpi-i2cd: discovered {device} hid={hid} mmio={:#x}+{:#x} irq={:?}",
|
||||
@@ -286,9 +291,6 @@ fn register_controller(prefix: &str, controller: ControllerDescriptor) -> Result
|
||||
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,
|
||||
@@ -299,6 +301,8 @@ fn register_controller(prefix: &str, controller: ControllerDescriptor) -> Result
|
||||
.as_ref()
|
||||
.map(|bus| bus.access_mode_10bit)
|
||||
.unwrap_or(false),
|
||||
provider_scheme: "dw-acpi-i2c".to_string(),
|
||||
aliases: vec![device.clone()],
|
||||
};
|
||||
let mut registration = register_adapter(&info)
|
||||
.with_context(|| format!("failed to register {device} with i2cd"))?;
|
||||
@@ -314,18 +318,36 @@ fn register_controller(prefix: &str, controller: ControllerDescriptor) -> Result
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RegisteredController {
|
||||
_mmio: mmio,
|
||||
_registration: registration,
|
||||
})
|
||||
let endpoint = EndpointController {
|
||||
name: format!("{prefix}:{device}"),
|
||||
aliases: vec![device],
|
||||
engine,
|
||||
};
|
||||
Ok((endpoint, (mmio, 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")?;
|
||||
// i2cd is ordered before us via init requires_weak, but scheme
|
||||
// registration is asynchronous — tolerate a short startup skew instead
|
||||
// of permanently losing the controller.
|
||||
let mut file = None;
|
||||
for attempt in 0..100 {
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/scheme/i2c/register")
|
||||
{
|
||||
Ok(opened) => {
|
||||
file = Some(opened);
|
||||
break;
|
||||
}
|
||||
Err(err) if attempt == 99 => {
|
||||
return Err(err).context("failed to open /scheme/i2c/register");
|
||||
}
|
||||
Err(_) => std::thread::sleep(std::time::Duration::from_millis(50)),
|
||||
}
|
||||
}
|
||||
let mut file = file.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())
|
||||
|
||||
@@ -61,6 +61,17 @@ pub struct I2cAdapterInfo {
|
||||
pub name: String,
|
||||
pub max_transaction_size: usize,
|
||||
pub supports_10bit_addr: bool,
|
||||
/// Scheme name of the provider daemon that executes transfers for this
|
||||
/// adapter (e.g. "i2c-lpss" → `/scheme/i2c-lpss/transfer`). Empty when the
|
||||
/// adapter is registered by a daemon that cannot execute transfers.
|
||||
#[serde(default)]
|
||||
pub provider_scheme: String,
|
||||
/// Alternative names that identify this adapter in transfer requests —
|
||||
/// typically the ACPI path of the controller (e.g. "PC00.I2C0"), which is
|
||||
/// what HID-over-I2C consumers obtain from the device's `_CRS`
|
||||
/// `I2cSerialBus` resource source.
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
+112
-50
@@ -23,7 +23,6 @@ enum Handle {
|
||||
|
||||
struct AdapterEntry {
|
||||
info: I2cAdapterInfo,
|
||||
provider_handle: usize,
|
||||
}
|
||||
|
||||
struct I2cDaemon {
|
||||
@@ -85,44 +84,77 @@ impl I2cDaemon {
|
||||
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,
|
||||
/// Resolve an adapter by request name. Consumers identify adapters by the
|
||||
/// ACPI resource-source string from the device's `_CRS` (e.g.
|
||||
/// "\_SB_.PC00.I2C0"), so matching is tolerant to normalization forms:
|
||||
/// exact name/alias match first, then last-dot-component match.
|
||||
fn resolve_adapter(&self, requested: &str) -> Option<&I2cAdapterInfo> {
|
||||
fn last_component(value: &str) -> &str {
|
||||
value.rsplit('.').next().unwrap_or(value)
|
||||
}
|
||||
|
||||
let normalized = requested
|
||||
.trim_start_matches("\\_SB_.")
|
||||
.trim_start_matches('_');
|
||||
|
||||
self.adapters
|
||||
.values()
|
||||
.map(|entry| &entry.info)
|
||||
.find(|info| {
|
||||
info.name == requested
|
||||
|| info
|
||||
.aliases
|
||||
.iter()
|
||||
.any(|alias| alias == requested || alias == &normalized)
|
||||
|| last_component(&info.name) == last_component(normalized)
|
||||
|| info
|
||||
.aliases
|
||||
.iter()
|
||||
.any(|alias| last_component(alias) == last_component(normalized))
|
||||
})
|
||||
.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,
|
||||
);
|
||||
/// Forward a transfer to the provider daemon that owns the adapter's
|
||||
/// hardware: write the request to `/scheme/<provider>/transfer` and read
|
||||
/// back the response. The wire format is the bare `I2cTransferRequest` /
|
||||
/// `I2cTransferResponse` pair that in-tree consumers (i2c-hidd) speak.
|
||||
fn forward_transfer(request: &I2cTransferRequest, info: &I2cAdapterInfo) -> I2cTransferResponse {
|
||||
let fail = |message: String| I2cTransferResponse {
|
||||
ok: false,
|
||||
read_data: Vec::new(),
|
||||
error: Some(message),
|
||||
};
|
||||
|
||||
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")),
|
||||
}),
|
||||
)
|
||||
if info.provider_scheme.is_empty() {
|
||||
return fail(format!(
|
||||
"adapter '{}' has no transfer provider",
|
||||
info.name
|
||||
));
|
||||
}
|
||||
|
||||
let path = format!("/scheme/{}/transfer", info.provider_scheme);
|
||||
let mut file = match std::fs::OpenOptions::new().read(true).write(true).open(&path) {
|
||||
Ok(file) => file,
|
||||
Err(err) => return fail(format!("cannot open {path}: {err}")),
|
||||
};
|
||||
|
||||
let payload = match ron::ser::to_string(request) {
|
||||
Ok(payload) => payload,
|
||||
Err(err) => return fail(format!("cannot encode transfer request: {err}")),
|
||||
};
|
||||
if let Err(err) = std::io::Write::write_all(&mut file, payload.as_bytes()) {
|
||||
return fail(format!("cannot write transfer to {path}: {err}"));
|
||||
}
|
||||
|
||||
let mut text = String::new();
|
||||
if let Err(err) = std::io::Read::read_to_string(&mut file, &mut text) {
|
||||
return fail(format!("cannot read transfer response from {path}: {err}"));
|
||||
}
|
||||
|
||||
match ron::from_str::<I2cTransferResponse>(&text) {
|
||||
Ok(response) => response,
|
||||
Err(err) => fail(format!("invalid transfer response from {path}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_pending(handle: &mut Handle, buf: &mut [u8], offset: u64) -> syscall::Result<usize> {
|
||||
@@ -250,6 +282,46 @@ impl SchemeSync for I2cDaemon {
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> syscall::Result<usize> {
|
||||
// The transfer endpoint speaks the bare I2cTransferRequest /
|
||||
// I2cTransferResponse wire format (what i2c-hidd and other in-tree
|
||||
// consumers use); every other endpoint speaks I2cControlRequest.
|
||||
if matches!(self.handles.get(id)?, Handle::Transfer { .. }) {
|
||||
let text = std::str::from_utf8(buf).map_err(|_| SysError::new(EINVAL))?;
|
||||
let transfer: I2cTransferRequest =
|
||||
ron::from_str(text).map_err(|_| SysError::new(EINVAL))?;
|
||||
|
||||
let response = match self.resolve_adapter(&transfer.adapter) {
|
||||
Some(info) => {
|
||||
let info = info.clone();
|
||||
log::debug!(
|
||||
"i2cd: routing transfer for '{}' to provider '{}' ({})",
|
||||
transfer.adapter,
|
||||
info.provider_scheme,
|
||||
info.name,
|
||||
);
|
||||
Self::forward_transfer(&transfer, &info)
|
||||
}
|
||||
None => I2cTransferResponse {
|
||||
ok: false,
|
||||
read_data: Vec::new(),
|
||||
error: Some(format!(
|
||||
"no registered adapter matches '{}'",
|
||||
transfer.adapter
|
||||
)),
|
||||
},
|
||||
};
|
||||
|
||||
let pending = ron::ser::to_string(&response)
|
||||
.map(|text| text.into_bytes())
|
||||
.map_err(|_| SysError::new(EINVAL))?;
|
||||
let handle = self.handles.get_mut(id)?;
|
||||
let Handle::Transfer { pending: slot } = handle else {
|
||||
return Err(SysError::new(EBADF));
|
||||
};
|
||||
*slot = pending;
|
||||
return Ok(buf.len());
|
||||
}
|
||||
|
||||
let request = Self::deserialize_request(buf)?;
|
||||
|
||||
match request {
|
||||
@@ -262,7 +334,6 @@ impl SchemeSync for I2cDaemon {
|
||||
adapter_id,
|
||||
AdapterEntry {
|
||||
info: info.clone(),
|
||||
provider_handle: id,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -308,21 +379,12 @@ impl SchemeSync for I2cDaemon {
|
||||
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 info = entry.info.clone();
|
||||
let response = Self::forward_transfer(&request, &info);
|
||||
|
||||
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)),
|
||||
}
|
||||
Self::set_pending_response(handle, I2cControlResponse::TransferResult(response))?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ edition = "2021"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
redox_syscall = { workspace = true, features = ["std"] }
|
||||
redox-scheme.workspace = true
|
||||
libredox.workspace = true
|
||||
serde.workspace = true
|
||||
ron.workspace = true
|
||||
@@ -15,7 +16,9 @@ ron.workspace = true
|
||||
acpi-resource = { path = "../../acpi-resource" }
|
||||
common = { path = "../../common" }
|
||||
daemon = { path = "../../../daemon" }
|
||||
dw-i2c = { path = "../designware" }
|
||||
i2c-interface = { path = "../i2c-interface" }
|
||||
scheme-utils = { path = "../../../scheme-utils" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
//! Intel LPSS I2C controller daemon.
|
||||
//!
|
||||
//! Discovery follows Linux 7.1 `drivers/mfd/intel-lpss-pci.c`: modern
|
||||
//! platforms (Meteor Lake, Arrow Lake) bind the LPSS I2C blocks by PCI ID,
|
||||
//! while older platforms (Haswell through some CNL boards) expose them as
|
||||
//! ACPI devices with LPSS HIDs. Both paths converge on the same DesignWare
|
||||
//! engine (`dw.rs`, ported from Linux's i2c-designware master driver).
|
||||
//!
|
||||
//! The daemon registers each controller with i2cd and serves
|
||||
//! `/scheme/i2c-lpss/transfer`, executing real I2C transactions on hardware.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
@@ -13,18 +24,32 @@ use common::{MemoryType, PhysBorrowed, Prot};
|
||||
use i2c_interface::{I2cAdapterInfo, I2cControlRequest, I2cControlResponse};
|
||||
use serde::Deserialize;
|
||||
|
||||
const SUPPORTED_IDS: &[&str] = &["INT33C2", "INT33C3", "INT3432", "INT3433", "INTC10EF"];
|
||||
use dw_i2c::endpoint::EndpointController;
|
||||
use dw_i2c::{DwI2c, BXT_I2C_TIMING};
|
||||
|
||||
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>();
|
||||
const SUPPORTED_ACPI_IDS: &[&str] = &["INT33C2", "INT33C3", "INT3432", "INT3433", "INTC10EF"];
|
||||
|
||||
/// Intel LPSS I2C PCI IDs, ported from Linux 7.1
|
||||
/// `drivers/mfd/intel-lpss-pci.c` (entries carrying `*_i2c_info`).
|
||||
/// All of these use the Broxton-derived timing (`bxt_i2c_info`, 133 MHz).
|
||||
const LPSS_I2C_PCI_IDS: &[u16] = &[
|
||||
// ARL-H (0x7750/0x7751, 0x7778-0x777b)
|
||||
0x7750, 0x7751, 0x7778, 0x7779, 0x777a, 0x777b,
|
||||
// MTL-P (0x7e50/0x7e51, 0x7e78-0x7e7b)
|
||||
0x7e50, 0x7e51, 0x7e78, 0x7e79, 0x7e7a, 0x7e7b,
|
||||
];
|
||||
|
||||
const PCI_VENDOR_INTEL: u16 = 0x8086;
|
||||
const PCI_CLASS_SERIAL_BUS: u8 = 0x0c;
|
||||
const PCI_SUBCLASS_I2C: u8 = 0x80;
|
||||
|
||||
struct Controller {
|
||||
name: String,
|
||||
aliases: Vec<String>,
|
||||
engine: DwI2c,
|
||||
registration: File,
|
||||
mmio: PhysBorrowed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AmlSymbol {
|
||||
@@ -47,17 +72,12 @@ struct ControllerResources {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ControllerDescriptor {
|
||||
struct DiscoveredController {
|
||||
device: String,
|
||||
hid: String,
|
||||
resources: ControllerResources,
|
||||
}
|
||||
|
||||
struct RegisteredController {
|
||||
_mmio: Option<PhysBorrowed>,
|
||||
_registration: File,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
common::setup_logging(
|
||||
"bus",
|
||||
@@ -82,31 +102,256 @@ fn daemon_runner(daemon: daemon::Daemon) -> ! {
|
||||
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 controllers = Vec::new();
|
||||
|
||||
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:#}"),
|
||||
for discovered in discover_pci_controllers() {
|
||||
match bring_up_controller(discovered) {
|
||||
Ok(controller) => controllers.push(controller),
|
||||
Err(err) => log::warn!("intel-lpss-i2cd: PCI controller bring-up 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();
|
||||
for discovered in discover_acpi_controllers(SUPPORTED_ACPI_IDS)
|
||||
.context("failed to discover Intel LPSS ACPI I2C controllers")?
|
||||
{
|
||||
match bring_up_controller(discovered) {
|
||||
Ok(controller) => controllers.push(controller),
|
||||
Err(err) => log::warn!("intel-lpss-i2cd: ACPI controller bring-up skipped: {err:#}"),
|
||||
}
|
||||
}
|
||||
|
||||
if controllers.is_empty() {
|
||||
log::info!("intel-lpss-i2cd: no supported controllers found (PCI or ACPI)");
|
||||
}
|
||||
|
||||
// i2cd registrations and MMIO mappings must outlive the scheme endpoint.
|
||||
let mut guards = Vec::new();
|
||||
let endpoint_controllers: Vec<EndpointController> = controllers
|
||||
.into_iter()
|
||||
.map(|controller| {
|
||||
let Controller {
|
||||
name,
|
||||
aliases,
|
||||
engine,
|
||||
registration,
|
||||
mmio,
|
||||
} = controller;
|
||||
guards.push((registration, mmio));
|
||||
EndpointController {
|
||||
name,
|
||||
aliases,
|
||||
engine,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Register the scheme first, then notify init (xhcid ordering) so a
|
||||
// `type = { scheme = "i2c-lpss" }` service only reports ready once the
|
||||
// scheme path actually exists.
|
||||
dw_i2c::endpoint::serve("i2c-lpss", endpoint_controllers, || {
|
||||
daemon.ready();
|
||||
if let Err(err) = libredox::call::setrens(0, 0) {
|
||||
log::error!("intel-lpss-i2cd: failed to enter null namespace: {err}");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn discover_controllers(supported_ids: &[&str]) -> Result<Vec<ControllerDescriptor>> {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller bring-up: MMIO map, engine init, i2cd registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bring_up_controller(discovered: DiscoveredController) -> Result<Controller> {
|
||||
let mmio = PhysBorrowed::map(
|
||||
discovered.resources.mmio_base,
|
||||
discovered.resources.mmio_len,
|
||||
Prot::RW,
|
||||
MemoryType::Uncacheable,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to map MMIO for {} ({:#x}, len {:#x})",
|
||||
discovered.device, discovered.resources.mmio_base, discovered.resources.mmio_len
|
||||
)
|
||||
})?;
|
||||
|
||||
let engine = unsafe { DwI2c::new(mmio.as_ptr() as *mut u8) };
|
||||
engine.init(&BXT_I2C_TIMING);
|
||||
|
||||
let name = format!("intel-lpss:{}", discovered.device);
|
||||
let aliases = discover_acpi_aliases(discovered.resources.mmio_base);
|
||||
|
||||
log::info!(
|
||||
"intel-lpss-i2cd: controller {} hid={} mmio={:#x}+{:#x} irq={:?} aliases={:?}",
|
||||
name,
|
||||
discovered.hid,
|
||||
discovered.resources.mmio_base,
|
||||
discovered.resources.mmio_len,
|
||||
discovered.resources.irq,
|
||||
aliases,
|
||||
);
|
||||
|
||||
let info = I2cAdapterInfo {
|
||||
id: 0,
|
||||
name: name.clone(),
|
||||
max_transaction_size: 0,
|
||||
supports_10bit_addr: discovered
|
||||
.resources
|
||||
.serial_bus
|
||||
.as_ref()
|
||||
.map(|bus| bus.access_mode_10bit)
|
||||
.unwrap_or(true),
|
||||
provider_scheme: "i2c-lpss".to_string(),
|
||||
aliases: aliases.clone(),
|
||||
};
|
||||
let mut registration = register_adapter(&info)
|
||||
.with_context(|| format!("failed to register {name} with i2cd"))?;
|
||||
let response = read_registration_response(&mut registration)
|
||||
.with_context(|| format!("failed to read i2cd registration response for {name}"))?;
|
||||
|
||||
match response {
|
||||
I2cControlResponse::AdapterRegistered { id } => {
|
||||
log::info!("intel-lpss-i2cd: adapter {name} registered with i2cd as {id}");
|
||||
}
|
||||
other => {
|
||||
log::warn!("intel-lpss-i2cd: unexpected i2cd registration response for {name}: {other:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Controller {
|
||||
name,
|
||||
aliases,
|
||||
engine,
|
||||
registration,
|
||||
mmio,
|
||||
})
|
||||
}
|
||||
|
||||
fn register_adapter(info: &I2cAdapterInfo) -> Result<File> {
|
||||
// i2cd is ordered before us via init requires_weak, but scheme
|
||||
// registration is asynchronous — tolerate a short startup skew instead
|
||||
// of permanently losing the controller.
|
||||
let mut file = None;
|
||||
for attempt in 0..100 {
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/scheme/i2c/register")
|
||||
{
|
||||
Ok(opened) => {
|
||||
file = Some(opened);
|
||||
break;
|
||||
}
|
||||
Err(err) if attempt == 99 => {
|
||||
return Err(err).context("failed to open /scheme/i2c/register");
|
||||
}
|
||||
Err(_) => std::thread::sleep(std::time::Duration::from_millis(50)),
|
||||
}
|
||||
}
|
||||
let mut file = file.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")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PCI discovery (Linux intel-lpss-pci.c model)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn discover_pci_controllers() -> Vec<DiscoveredController> {
|
||||
let entries = match fs::read_dir("/scheme/pci") {
|
||||
Ok(entries) => entries,
|
||||
Err(err) => {
|
||||
log::warn!("intel-lpss-i2cd: cannot read /scheme/pci: {err}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut out = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let config_path = entry.path().join("config");
|
||||
let Ok(config) = fs::read(&config_path) else {
|
||||
continue;
|
||||
};
|
||||
if config.len() < 0x40 {
|
||||
continue;
|
||||
}
|
||||
let vendor = u16::from_le_bytes([config[0x00], config[0x01]]);
|
||||
let device = u16::from_le_bytes([config[0x02], config[0x03]]);
|
||||
let class_code = config[0x0b];
|
||||
let subclass = config[0x0a];
|
||||
if vendor != PCI_VENDOR_INTEL
|
||||
|| class_code != PCI_CLASS_SERIAL_BUS
|
||||
|| subclass != PCI_SUBCLASS_I2C
|
||||
|| !LPSS_I2C_PCI_IDS.contains(&device)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mmio_base) = pci_bar0_address(&config) else {
|
||||
log::warn!("intel-lpss-i2cd: {device:04x} has no usable BAR0, skipped");
|
||||
continue;
|
||||
};
|
||||
|
||||
let location = entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| format!("pci-{device:04x}"));
|
||||
let irq = config[0x3c];
|
||||
let irq = (irq != 0 && irq != 0xff).then_some(u32::from(irq));
|
||||
|
||||
out.push(DiscoveredController {
|
||||
device: location,
|
||||
hid: format!("pci:8086:{device:04x}"),
|
||||
resources: ControllerResources {
|
||||
mmio_base,
|
||||
mmio_len: 0x1000,
|
||||
irq,
|
||||
serial_bus: None,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Extract a usable memory BAR0 from raw PCI config space. Handles both
|
||||
/// 32-bit and 64-bit memory BARs; returns the CPU-physical base address.
|
||||
fn pci_bar0_address(config: &[u8]) -> Option<usize> {
|
||||
let bar_lo = u32::from_le_bytes([config[0x10], config[0x11], config[0x12], config[0x13]]);
|
||||
if bar_lo == 0 || bar_lo & 1 != 0 {
|
||||
// Not present or an I/O-space BAR; LPSS I2C is always memory-mapped.
|
||||
return None;
|
||||
}
|
||||
let is_64bit = (bar_lo >> 1) & 0x3 == 0x2;
|
||||
let base_lo = u64::from(bar_lo & !0xf);
|
||||
let base = if is_64bit {
|
||||
let bar_hi = u32::from_le_bytes([config[0x14], config[0x15], config[0x16], config[0x17]]);
|
||||
base_lo | (u64::from(bar_hi) << 32)
|
||||
} else {
|
||||
base_lo
|
||||
};
|
||||
(base != 0).then_some(base as usize)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACPI discovery (legacy LPSS HID model)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn discover_acpi_controllers(supported_ids: &[&str]) -> Result<Vec<DiscoveredController>> {
|
||||
let mut matched = BTreeMap::new();
|
||||
|
||||
let entries = match fs::read_dir("/scheme/acpi/symbols") {
|
||||
@@ -149,7 +394,7 @@ fn discover_controllers(supported_ids: &[&str]) -> Result<Vec<ControllerDescript
|
||||
for (device, hid) in matched {
|
||||
let resources = read_controller_resources(&device)
|
||||
.with_context(|| format!("failed to read resources for {device}"))?;
|
||||
controllers.push(ControllerDescriptor {
|
||||
controllers.push(DiscoveredController {
|
||||
device,
|
||||
hid,
|
||||
resources,
|
||||
@@ -199,7 +444,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
address_length,
|
||||
..
|
||||
}) if mmio.is_none() => {
|
||||
mmio = Some((*address as usize, (*address_length as usize).max(DW_MMIO_WINDOW)));
|
||||
mmio = Some((*address as usize, (*address_length as usize).max(0x100)));
|
||||
}
|
||||
ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
|
||||
minimum,
|
||||
@@ -208,10 +453,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
..
|
||||
}) 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)),
|
||||
));
|
||||
mmio = Some((*minimum as usize, span.max((*address_length as usize).max(0x100))));
|
||||
}
|
||||
ResourceDescriptor::Address32(descriptor)
|
||||
if mmio.is_none()
|
||||
@@ -219,7 +461,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
{
|
||||
mmio = Some((
|
||||
descriptor.minimum as usize,
|
||||
(descriptor.address_length as usize).max(DW_MMIO_WINDOW),
|
||||
(descriptor.address_length as usize).max(0x100),
|
||||
));
|
||||
}
|
||||
ResourceDescriptor::Address64(descriptor)
|
||||
@@ -230,7 +472,7 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
.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)));
|
||||
mmio = Some((base, len.max(0x100)));
|
||||
}
|
||||
ResourceDescriptor::Irq(IrqDescriptor { interrupts, .. }) if irq.is_none() => {
|
||||
irq = interrupts.first().copied().map(u32::from);
|
||||
@@ -256,91 +498,34 @@ fn read_controller_resources(device: &str) -> Result<ControllerResources> {
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
/// Find the ACPI device path whose `_CRS` FixedMemory32 window matches this
|
||||
/// controller's MMIO base. That path ("PC00.I2C0") is what HID-over-I2C
|
||||
/// devices name in their `I2cSerialBus` resource source, so it is the alias
|
||||
/// consumers will use to address this adapter.
|
||||
fn discover_acpi_aliases(mmio_base: usize) -> Vec<String> {
|
||||
let Ok(entries) = fs::read_dir("/scheme/acpi/resources") else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
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:?}");
|
||||
let mut aliases = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let Ok(contents) = fs::read_to_string(entry.path()) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(resources) = ron::from_str::<Vec<ResourceDescriptor>>(&contents) else {
|
||||
continue;
|
||||
};
|
||||
let matches_base = resources.iter().any(|resource| match resource {
|
||||
ResourceDescriptor::FixedMemory32(descriptor) => descriptor.address as usize == mmio_base,
|
||||
_ => false,
|
||||
});
|
||||
if matches_base {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
aliases.push(name.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
aliases
|
||||
}
|
||||
|
||||
fn eisa_id_from_integer(integer: u64) -> String {
|
||||
@@ -359,3 +544,32 @@ fn eisa_id_from_integer(integer: u64) -> String {
|
||||
"{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bar0_32bit_memory() {
|
||||
let mut config = [0u8; 0x40];
|
||||
config[0x10..0x14].copy_from_slice(&0x5019_2de0u32.to_le_bytes());
|
||||
assert_eq!(pci_bar0_address(&config), Some(0x5019_2de0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar0_64bit_memory() {
|
||||
let mut config = [0u8; 0x40];
|
||||
// 64-bit BAR: flags bits [2:1] == 0b10, base 0x5019_92de_0000.
|
||||
config[0x10..0x14].copy_from_slice(&0x92de_0004u32.to_le_bytes());
|
||||
config[0x14..0x18].copy_from_slice(&0x5019u32.to_le_bytes());
|
||||
assert_eq!(pci_bar0_address(&config), Some(0x5019_92de_0000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar0_io_space_rejected() {
|
||||
let mut config = [0u8; 0x40];
|
||||
config[0x10..0x14].copy_from_slice(&0xefa1u32.to_le_bytes());
|
||||
assert_eq!(pci_bar0_address(&config), None);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user