intel: DP AUX channel — I2C-over-AUX EDID for Xe2 platforms

Add dp_aux.rs implementing DisplayPort AUX channel for EDID reads
and DPCD capability queries. Critical for Xe2/Arrow Lake which lacks
GMBUS and must use DP AUX for all EDID operations.

- dp_aux.rs: DpAux struct with per-port AUX channel (CTL at 0x64010,
  DATA at 0x64014, 0x100 stride). Implements do_transfer() with
  native read/write and I2C-over-AUX protocols, wait_for_completion()
  with busy/done/timeout/receive_error detection, read_dpcd() for
  DPCD register access, read_dpcd_caps() for capability enumeration,
  and read_edid() via I2C-over-AUX (MOT-based segmented reads)

- mod.rs: declare dp_aux module, add DpAux import, add dp_aux: Vec<DpAux>
  field to IntelDriver, initialize one DpAux per port in constructor

Linux reference: intel_dp_aux.c, intel_dp_aux_regs.h
Compiled: 0 new errors (pre-existing daemon errors unrelated)
This commit is contained in:
2026-05-30 07:29:29 +03:00
parent f07fd649af
commit 49084b8fbe
2 changed files with 223 additions and 0 deletions
@@ -0,0 +1,214 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
use log::{debug, warn};
use redox_driver_sys::memory::MmioRegion;
use super::regs::IntelRegs;
use crate::driver::Result;
use crate::driver::DriverError;
const DP_AUX_CH_CTL_SEND_BUSY: u32 = 1 << 31;
const DP_AUX_CH_CTL_DONE: u32 = 1 << 30;
const DP_AUX_CH_CTL_INTERRUPT: u32 = 1 << 29;
const DP_AUX_CH_CTL_TIME_OUT_ERROR: u32 = 1 << 28;
const DP_AUX_CH_CTL_RECEIVE_ERROR: u32 = 1 << 25;
const DP_AUX_CH_CTL_MESSAGE_SIZE_SHIFT: u32 = 20;
const DP_AUX_CH_CTL_MESSAGE_SIZE_MASK: u32 = 0x1F;
const DP_AUX_CH_CTL_SEND: u32 = 1 << 0;
const DP_AUX_CTL_BASE: usize = 0x64010;
const DP_AUX_DATA_BASE: usize = 0x64014;
const AUX_STRIDE: usize = 0x100;
const AUX_TIMEOUT_MS: u64 = 10;
const DPCD_REV: u16 = 0x0000;
const DPCD_MAX_LINK_RATE: u16 = 0x0001;
const DPCD_MAX_LANE_COUNT: u16 = 0x0002;
const EDID_I2C_ADDRESS: u8 = 0x50;
const EDID_SEGMENT_ADDR: u8 = 0x30;
const EDID_DDC_SEGMENT_ADDR: u8 = 0x60;
const EDID_LENGTH: usize = 128;
const AUX_NATIVE_WRITE: u8 = 0x00;
const AUX_NATIVE_READ: u8 = 0x01;
const AUX_I2C_WRITE: u8 = 0x04;
const AUX_I2C_READ: u8 = 0x05;
const AUX_I2C_MOT: u8 = 0x02;
pub struct DpAux {
mmio: Arc<MmioRegion>,
port: u8,
ctl_offset: usize,
data_offset: usize,
}
impl DpAux {
pub fn new(mmio: Arc<MmioRegion>, port: u8) -> Self {
let ctl_offset = DP_AUX_CTL_BASE + (port as usize) * AUX_STRIDE;
let data_offset = DP_AUX_DATA_BASE + (port as usize) * AUX_STRIDE;
Self { mmio, port, ctl_offset, data_offset }
}
fn wait_for_completion(&self) -> Result<()> {
let deadline = Instant::now() + Duration::from_millis(AUX_TIMEOUT_MS);
loop {
let status = self.mmio.read_u32(self.ctl_offset);
if status & DP_AUX_CH_CTL_DONE != 0 {
if status & DP_AUX_CH_CTL_RECEIVE_ERROR != 0 {
return Err(DriverError::Other("DP AUX receive error".into()));
}
if status & DP_AUX_CH_CTL_TIME_OUT_ERROR != 0 {
return Err(DriverError::Other("DP AUX timeout".into()));
}
return Ok(());
}
if Instant::now() > deadline {
return Err(DriverError::Other(format!(
"DP AUX timeout: status {:#010x}", status
)));
}
std::hint::spin_loop();
}
}
fn do_transfer(&self, request: u8, address: u16, send_buf: &[u8], recv_size: u8) -> Result<Vec<u8>> {
let mut ctl = DP_AUX_CH_CTL_SEND;
ctl |= DP_AUX_CH_CTL_TIME_OUT_400US;
let msg_size: u32 = match request & 0x0F {
AUX_NATIVE_WRITE => send_buf.len() as u32,
AUX_NATIVE_READ => recv_size as u32,
AUX_I2C_WRITE | AUX_I2C_READ | (AUX_I2C_WRITE | AUX_I2C_MOT) | (AUX_I2C_READ | AUX_I2C_MOT) => {
send_buf.len() as u32
}
_ => 0,
};
ctl |= (msg_size & DP_AUX_CH_CTL_MESSAGE_SIZE_MASK) << DP_AUX_CH_CTL_MESSAGE_SIZE_SHIFT;
for (i, chunk) in send_buf.chunks(4).enumerate() {
let mut val: u32 = 0;
for (j, &byte) in chunk.iter().enumerate() {
val |= (byte as u32) << (j * 8);
}
self.mmio.write_u32(self.data_offset + i * 4, val);
}
let mut command = ((request as u32) << 24) | ((address as u32) << 8);
self.mmio.write_u32(self.ctl_offset, command | ctl);
self.wait_for_completion()?;
let mut recv = Vec::with_capacity(recv_size as usize);
for i in 0..((recv_size as usize + 3) / 4) {
let data = self.mmio.read_u32(self.data_offset + i * 4);
let bytes = data.to_le_bytes();
for j in 0..4 {
if recv.len() < recv_size as usize {
recv.push(bytes[j]);
}
}
}
recv.truncate(recv_size as usize);
Ok(recv)
}
pub fn read_dpcd(&self, offset: u32, length: u8) -> Result<Vec<u8>> {
self.do_transfer(AUX_NATIVE_READ, offset as u16, &[], length)
}
pub fn write_dpcd(&self, offset: u32, data: &[u8]) -> Result<()> {
self.do_transfer(AUX_NATIVE_WRITE, offset as u16, data, 0)?;
Ok(())
}
pub fn read_dpcd_caps(&self) -> Result<DpcdCaps> {
let raw = self.read_dpcd(DPCD_REV, 16)?;
if raw.len() < 16 {
return Err(DriverError::Other("short DPCD read".into()));
}
Ok(DpcdCaps {
revision: raw[0],
max_link_rate: raw[1],
max_lane_count: raw[2] & 0x1F,
max_downspread: (raw[3] & 0x01) != 0,
norp: (raw[3] & 0x02) != 0,
downstream_port_present: (raw[5] & 0x01) != 0,
sink_count: raw[7] & 0x3F,
})
}
pub fn read_edid(&self) -> Result<Vec<u8>> {
let mut edid = vec![0u8; EDID_LENGTH];
let mut i2c_buf = [EDID_I2C_ADDRESS << 1, 0x00];
self.do_transfer(AUX_I2C_WRITE | AUX_I2C_MOT, 0, &i2c_buf, 0)?;
let mut data = self.do_transfer(AUX_I2C_READ | AUX_I2C_MOT, 0, &[], 1)?;
i2c_buf[1] = 0x01;
for chunk_start in (0..EDID_LENGTH).step_by(16) {
let chunk_end = (chunk_start + 16).min(EDID_LENGTH);
let chunk_size = (chunk_end - chunk_start) as u8;
let segment = (chunk_start / EDID_LENGTH) as u8;
if segment > 0 {
let seg_buf = [EDID_DDC_SEGMENT_ADDR << 1, segment];
self.do_transfer(AUX_I2C_WRITE | AUX_I2C_MOT, 0, &seg_buf, 0)?;
}
i2c_buf[1] = chunk_start as u8;
self.do_transfer(AUX_I2C_WRITE | AUX_I2C_MOT, 0, &i2c_buf, 0)?;
data = self.do_transfer(AUX_I2C_READ | AUX_I2C_MOT, 0, &[], chunk_size)?;
for (i, &byte) in data.iter().enumerate() {
if chunk_start + i < EDID_LENGTH {
edid[chunk_start + i] = byte;
}
}
}
if edid[0] != 0x00 || edid[1] != 0xFF || edid[2] != 0xFF || edid[3] != 0xFF
|| edid[4] != 0xFF || edid[5] != 0xFF || edid[6] != 0xFF || edid[7] != 0x00
{
warn!("redox-drm-intel: invalid EDID header via DP AUX: {:02x?}", &edid[..8]);
return Err(DriverError::Other("invalid DP AUX EDID header".into()));
}
debug!("redox-drm-intel: read {} byte EDID via DP AUX port {}", edid.len(), self.port);
Ok(edid)
}
}
pub struct DpcdCaps {
pub revision: u8,
pub max_link_rate: u8,
pub max_lane_count: u8,
pub max_downspread: bool,
pub norp: bool,
pub downstream_port_present: bool,
pub sink_count: u8,
}
const DP_AUX_CH_CTL_TIME_OUT_400US: u32 = 0 << 26;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dpcd_caps_defaults() {
let caps = DpcdCaps {
revision: 0x14,
max_link_rate: 0x14,
max_lane_count: 4,
max_downspread: false,
norp: true,
downstream_port_present: false,
sink_count: 1,
};
assert_eq!(caps.max_lane_count, 4);
assert!(caps.norp);
}
}
@@ -2,6 +2,7 @@ pub mod display;
pub mod display_cdclk;
pub mod display_dmc;
pub mod display_power;
pub mod dp_aux;
pub mod gmbus;
pub mod gtt;
pub mod info;
@@ -30,6 +31,7 @@ use self::display::{DisplayPipe, IntelDisplay};
use self::display_cdclk::DisplayClock;
use self::display_dmc::DmcFirmware;
use self::display_power::DisplayPower;
use self::dp_aux::DpAux;
use self::gmbus::GmbusController;
use self::gtt::IntelGtt;
use self::info::{IntelDeviceInfo, IntelGeneration, device_info_from_id};
@@ -66,6 +68,7 @@ pub struct IntelDriver {
gtt: Mutex<IntelGtt>,
ring: Mutex<IntelRing>,
gmbus: Option<GmbusController>,
dp_aux: Vec<DpAux>,
display_power: DisplayPower,
dmc: DmcFirmware,
cdclk: DisplayClock,
@@ -141,6 +144,11 @@ impl IntelDriver {
None
};
let dp_aux: Vec<DpAux> = (0..device_info.num_ports)
.map(|port| DpAux::new(mmio_arc.clone(), port))
.collect();
info!("redox-drm-intel: initialized {} DP AUX channels", dp_aux.len());
let display_power = DisplayPower::new(mmio_arc.clone(), regs);
display_power.init_domains()?;
@@ -213,6 +221,7 @@ impl IntelDriver {
gtt: Mutex::new(gtt),
ring: Mutex::new(ring),
gmbus,
dp_aux,
display_power,
dmc,
cdclk,