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:
Red Bear OS
2026-07-21 06:02:31 +09:00
parent d78fd44a07
commit 452452e453
11 changed files with 1254 additions and 230 deletions
Generated
+18
View File
@@ -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",
]
+1
View File
@@ -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",
+2
View File
@@ -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")?;
+19
View File
@@ -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
+671
View File
@@ -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"));
}
}
}
+1
View File
@@ -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]
+78 -56
View File
@@ -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())
+11
View File
@@ -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
View File
@@ -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())
}
}
}
+3
View File
@@ -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
+338 -124
View File
@@ -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);
}
}