Files
RedBear-OS/drivers/i2c/intel-lpss-i2cd/src/main.rs
T
Red Bear OS 6db0f48170 i2c: review-fix round — hardware-correct DesignWare engine + LPSS bring-up
Post-implementation review returned FAIL with two verified-CRITICAL
findings; all legitimate findings fixed in this round:

CRITICAL — engine wrote IC_CON/IC_TAR while enabled (DesignWare
databook forbids; Linux i2c_dw_xfer_init disables first). Engine
restructured: wait-idle -> disable -> program -> enable with
IC_ENABLE_STATUS polling on every transfer.

CRITICAL — Intel LPSS PCI bring-up skipped parent-device init.
intel-lpss-i2cd now claims functions via pcid_interface
(connect_by_path + enable_device), validates the real BAR size, and
performs intel_lpss_init_dev's sequence (reset-deassert + 64-bit
remap address at BAR0+0x200), ported from Linux drivers/mfd/intel-lpss.c.

MAJOR fixes:
- wait_for checks TX_ABRT before success predicates — a NACKed write
  no longer reports success via post-abort idle state
- SCL timing corrected to Linux's formulas: HCNT uses sda_fall_ns,
  round-to-nearest division (FS 100/200, SS 552/652 for bxt 133 MHz)
- stop=false rejected honestly instead of hanging on the idle wait
- recover(): ABORT-bit cycle + disable + state flush after any failed
  transfer
- validate_request: segment/byte/address/10-bit limits before any MMIO
- i2cd + endpoint: exact-first adapter resolution; last-component
  matching accepted only when unambiguous
- i2cd registration validates provider_scheme as a single safe
  scheme-name component
- I2cTransferResponse gains typed status (I2cTransferStatus,
  serde-defaulted, wire-compatible)
- endpoint::serve takes a Result-returning on_ready callback;
  setrens failure is fatal (fail-closed namespace reduction)
- unexpected i2cd registration responses are fatal for that controller

Tests: dw-i2c 5 (timing vectors, validation, resolution),
intel-lpss-i2cd 1 (PCI ID table coverage), full workspace cargo check
clean, base cooks for x86_64-unknown-redox.

Refs: local/docs/LG-GRAM-16Z90TP-COMPATIBILITY-PLAN.md review-fix round
2026-07-21 12:03:00 +09:00

636 lines
21 KiB
Rust

//! 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};
use std::path::Path;
use std::process;
use acpi_resource::{
AddressResourceType, ExtendedIrqDescriptor, FixedMemory32Descriptor, I2cSerialBusDescriptor,
IrqDescriptor, Memory32RangeDescriptor, ResourceDescriptor,
};
use anyhow::{Context, Result};
use common::{MemoryType, PhysBorrowed, Prot};
use i2c_interface::{I2cAdapterInfo, I2cControlRequest, I2cControlResponse};
use serde::Deserialize;
use dw_i2c::endpoint::EndpointController;
use dw_i2c::{DwI2c, BXT_I2C_TIMING};
use pcid_interface::PciFunctionHandle;
// Intel LPSS private registers at BAR0 + 0x200 (Linux drivers/mfd/intel-lpss.c
// intel_lpss_init_dev): reset cycle + 64-bit remap address programming.
const LPSS_PRIV_OFFSET: usize = 0x200;
const LPSS_PRIV_RESETS: usize = 0x04;
const LPSS_PRIV_RESETS_FUNC_IDMA: u32 = 0x3 | (1 << 2);
const LPSS_PRIV_REMAP_ADDR_LO: usize = 0x40;
const LPSS_PRIV_REMAP_ADDR_HI: usize = 0x44;
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;
/// Keeps the hardware claim and MMIO mapping alive. The PCI variant owns
/// the pcid claim (released on drop); the ACPI variant owns the raw mapping.
/// The fields are intentionally never read — ownership is the mechanism.
#[allow(dead_code)]
enum MmioGuard {
Pci(PciFunctionHandle),
Acpi(PhysBorrowed),
}
struct Controller {
name: String,
aliases: Vec<String>,
engine: DwI2c,
supports_10bit_addr: bool,
registration: File,
guard: MmioGuard,
}
#[derive(Debug, Deserialize)]
struct AmlSymbol {
name: String,
value: AmlValue,
}
#[derive(Debug, Deserialize)]
enum AmlValue {
Integer(u64),
String(String),
}
#[derive(Clone, Debug)]
struct ControllerResources {
mmio_base: usize,
mmio_len: usize,
irq: Option<u32>,
serial_bus: Option<I2cSerialBusDescriptor>,
}
#[derive(Debug)]
struct DiscoveredController {
device: String,
hid: String,
resources: ControllerResources,
}
fn main() {
common::setup_logging(
"bus",
"i2c",
"intel-lpss-i2cd",
common::output_level(),
common::file_level(),
);
daemon::Daemon::new(daemon_runner);
}
fn daemon_runner(daemon: daemon::Daemon) -> ! {
if let Err(err) = daemon_main(daemon) {
log::error!("intel-lpss-i2cd: {err:#}");
process::exit(1);
}
process::exit(0);
}
fn daemon_main(daemon: daemon::Daemon) -> Result<()> {
common::init();
let mut controllers = Vec::new();
for candidate in discover_pci_controllers() {
match bring_up_pci_controller(candidate) {
Ok(controller) => controllers.push(controller),
Err(err) => log::warn!("intel-lpss-i2cd: PCI controller bring-up skipped: {err:#}"),
}
}
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,
supports_10bit_addr,
registration,
guard,
} = controller;
guards.push((registration, guard));
EndpointController {
name,
aliases,
engine,
supports_10bit_addr,
}
})
.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. Namespace reduction is fail-closed: if it
// fails the daemon exits instead of running unsandboxed.
dw_i2c::endpoint::serve("i2c-lpss", endpoint_controllers, || {
daemon.ready();
libredox::call::setrens(0, 0).context("failed to enter null namespace")?;
Ok(())
})
}
// ---------------------------------------------------------------------------
// 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 supports_10bit_addr = discovered
.resources
.serial_bus
.as_ref()
.map(|bus| bus.access_mode_10bit)
.unwrap_or(true);
let registration = register_adapter(&I2cAdapterInfo {
id: 0,
name: name.clone(),
max_transaction_size: 0,
supports_10bit_addr,
provider_scheme: "i2c-lpss".to_string(),
aliases: aliases.clone(),
})
.and_then(|mut file| read_registration_response(&mut file).map(|response| (file, response)))
.with_context(|| format!("failed to register {name} with i2cd"))?;
match registration.1 {
I2cControlResponse::AdapterRegistered { id } => {
log::info!("intel-lpss-i2cd: adapter {name} registered with i2cd as {id}");
}
other => anyhow::bail!("unexpected i2cd registration response for {name}: {other:?}"),
}
Ok(Controller {
name,
aliases,
engine,
supports_10bit_addr,
registration: registration.0,
guard: MmioGuard::Acpi(mmio),
})
}
// ---------------------------------------------------------------------------
// PCI bring-up (Linux intel-lpss-pci.c model)
// ---------------------------------------------------------------------------
struct PciCandidate {
path: std::path::PathBuf,
location: String,
device_id: u16,
}
fn bring_up_pci_controller(candidate: PciCandidate) -> Result<Controller> {
let mut handle = PciFunctionHandle::connect_by_path(&candidate.path)
.with_context(|| format!("failed to claim {}", candidate.location))?;
handle.enable_device();
let config = handle.config();
let (bar_phys, bar_len) = config.func.bars[0].expect_mem();
let bar_phys = usize::try_from(bar_phys).context("BAR0 address does not fit in usize")?;
anyhow::ensure!(
bar_len >= LPSS_PRIV_OFFSET + LPSS_PRIV_REMAP_ADDR_HI + 4,
"{}: BAR0 too small for LPSS private registers ({bar_len:#x})",
candidate.location,
);
let engine = unsafe {
let mapped = handle.map_bar(0);
DwI2c::new(mapped.ptr.as_ptr())
};
// intel_lpss_init_dev: reset cycle, then program the 64-bit remap address.
unsafe {
let priv_base = engine.mmio_base().add(LPSS_PRIV_OFFSET);
(priv_base.add(LPSS_PRIV_RESETS) as *mut u32).write_volatile(0);
(priv_base.add(LPSS_PRIV_RESETS) as *mut u32)
.write_volatile(LPSS_PRIV_RESETS_FUNC_IDMA);
(priv_base.add(LPSS_PRIV_REMAP_ADDR_LO) as *mut u32)
.write_volatile(bar_phys as u32);
(priv_base.add(LPSS_PRIV_REMAP_ADDR_HI) as *mut u32)
.write_volatile((bar_phys >> 32) as u32);
}
engine.init(&BXT_I2C_TIMING);
let name = format!("intel-lpss:{}", candidate.location);
let aliases = discover_acpi_aliases(bar_phys);
log::info!(
"intel-lpss-i2cd: controller {} pci=8086:{:04x} bar={:#x}+{:#x} aliases={:?}",
name,
candidate.device_id,
bar_phys,
bar_len,
aliases,
);
let registration = register_adapter(&I2cAdapterInfo {
id: 0,
name: name.clone(),
max_transaction_size: 0,
supports_10bit_addr: true,
provider_scheme: "i2c-lpss".to_string(),
aliases: aliases.clone(),
})
.and_then(|mut file| read_registration_response(&mut file).map(|response| (file, response)))
.with_context(|| format!("failed to register {name} with i2cd"))?;
match registration.1 {
I2cControlResponse::AdapterRegistered { id } => {
log::info!("intel-lpss-i2cd: adapter {name} registered with i2cd as {id}");
}
other => anyhow::bail!("unexpected i2cd registration response for {name}: {other:?}"),
}
Ok(Controller {
name,
aliases,
engine,
supports_10bit_addr: true,
registration: registration.0,
guard: MmioGuard::Pci(handle),
})
}
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<PciCandidate> {
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 location = entry
.file_name()
.to_str()
.map(str::to_owned)
.unwrap_or_else(|| format!("pci-{device:04x}"));
out.push(PciCandidate {
path: entry.path(),
location,
device_id: device,
});
}
out
}
// ---------------------------------------------------------------------------
// 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") {
Ok(entries) => entries,
Err(err)
if err.kind() == std::io::ErrorKind::WouldBlock || err.raw_os_error() == Some(11) =>
{
log::debug!("intel-lpss-i2cd: ACPI symbols are not ready yet");
return Ok(Vec::new());
}
Err(err) => return Err(err).context("failed to read /scheme/acpi/symbols"),
};
for entry in entries {
let entry = entry.context("failed to read ACPI symbol directory entry")?;
let Some(file_name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if !file_name.ends_with("_HID") && !file_name.ends_with("_CID") {
continue;
}
let Some(id) = read_symbol_id(&entry.path())? else {
continue;
};
if !supported_ids.iter().any(|candidate| *candidate == id) {
continue;
}
let device = file_name
.strip_suffix("_HID")
.or_else(|| file_name.strip_suffix("_CID"))
.map(str::to_owned);
if let Some(device) = device {
matched.entry(device).or_insert(id);
}
}
let mut controllers = Vec::new();
for (device, hid) in matched {
let resources = read_controller_resources(&device)
.with_context(|| format!("failed to read resources for {device}"))?;
controllers.push(DiscoveredController {
device,
hid,
resources,
});
}
Ok(controllers)
}
fn read_symbol_id(path: &Path) -> Result<Option<String>> {
let contents = fs::read_to_string(path)
.with_context(|| format!("failed to read ACPI symbol {}", path.display()))?;
let symbol = match ron::from_str::<AmlSymbol>(&contents) {
Ok(symbol) => symbol,
Err(err) => {
log::debug!(
"intel-lpss-i2cd: skipping {} because the symbol payload was not a scalar ID: {err}",
path.display(),
);
return Ok(None);
}
};
let id = match symbol.value {
AmlValue::Integer(integer) => eisa_id_from_integer(integer),
AmlValue::String(string) => string,
};
log::debug!("intel-lpss-i2cd: {} -> {id}", symbol.name);
Ok(Some(id))
}
fn read_controller_resources(device: &str) -> Result<ControllerResources> {
let contents = fs::read_to_string(format!("/scheme/acpi/resources/{device}"))
.with_context(|| format!("failed to read /scheme/acpi/resources/{device}"))?;
let resources = ron::from_str::<Vec<ResourceDescriptor>>(&contents)
.with_context(|| format!("failed to decode RON resources for {device}"))?;
let mut mmio = None;
let mut irq = None;
let mut serial_bus = None;
for resource in &resources {
match resource {
ResourceDescriptor::FixedMemory32(FixedMemory32Descriptor {
address,
address_length,
..
}) if mmio.is_none() => {
mmio = Some((*address as usize, (*address_length as usize).max(0x100)));
}
ResourceDescriptor::Memory32Range(Memory32RangeDescriptor {
minimum,
maximum,
address_length,
..
}) if mmio.is_none() && maximum >= minimum => {
let span = maximum.saturating_sub(*minimum).saturating_add(1) as usize;
mmio = Some((*minimum as usize, span.max((*address_length as usize).max(0x100))));
}
ResourceDescriptor::Address32(descriptor)
if mmio.is_none()
&& matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
{
mmio = Some((
descriptor.minimum as usize,
(descriptor.address_length as usize).max(0x100),
));
}
ResourceDescriptor::Address64(descriptor)
if mmio.is_none()
&& matches!(descriptor.resource_type, AddressResourceType::MemoryRange) =>
{
let base = usize::try_from(descriptor.minimum)
.context("64-bit MMIO base does not fit in usize")?;
let len = usize::try_from(descriptor.address_length)
.context("64-bit MMIO length does not fit in usize")?;
mmio = Some((base, len.max(0x100)));
}
ResourceDescriptor::Irq(IrqDescriptor { interrupts, .. }) if irq.is_none() => {
irq = interrupts.first().copied().map(u32::from);
}
ResourceDescriptor::ExtendedIrq(ExtendedIrqDescriptor { interrupts, .. })
if irq.is_none() =>
{
irq = interrupts.first().copied();
}
ResourceDescriptor::I2cSerialBus(descriptor) if serial_bus.is_none() => {
serial_bus = Some(descriptor.clone());
}
_ => {}
}
}
let (mmio_base, mmio_len) = mmio.context("no MMIO resource was found")?;
Ok(ControllerResources {
mmio_base,
mmio_len,
irq,
serial_bus,
})
}
/// 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();
};
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());
}
}
}
aliases
}
fn eisa_id_from_integer(integer: u64) -> String {
let vendor = integer & 0xFFFF;
let device = (integer >> 16) & 0xFFFF;
let vendor_rev = ((vendor & 0xFF) << 8) | (vendor >> 8);
let vendor_1 = (((vendor_rev >> 10) & 0x1F) as u8 + 64) as char;
let vendor_2 = (((vendor_rev >> 5) & 0x1F) as u8 + 64) as char;
let vendor_3 = (((vendor_rev >> 0) & 0x1F) as u8 + 64) as char;
let device_1 = (device >> 4) & 0xF;
let device_2 = (device >> 0) & 0xF;
let device_3 = (device >> 12) & 0xF;
let device_4 = (device >> 8) & 0xF;
format!(
"{vendor_1}{vendor_2}{vendor_3}{device_1:01X}{device_2:01X}{device_3:01X}{device_4:01X}"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lpss_pci_id_table_covers_arl_h_and_mtl_p() {
// Host-critical controllers (LG Gram 16Z90TP: 8086:7778, 8086:7779).
for id in [0x7778u16, 0x7779, 0x777a, 0x777b, 0x7750, 0x7751] {
assert!(LPSS_I2C_PCI_IDS.contains(&id), "ARL-H id {id:#06x} missing");
}
for id in [0x7e50u16, 0x7e51, 0x7e78, 0x7e79, 0x7e7a, 0x7e7b] {
assert!(LPSS_I2C_PCI_IDS.contains(&id), "MTL-P id {id:#06x} missing");
}
}
}