intel: DP link training — clock recovery + channel equalization

Add dp_link.rs implementing DisplayPort link training.

- train_dp_link(): reads DPCD caps, picks optimal link rate
  (1.62/2.7/5.4 Gbps) and lane count (1/2/4), programs DDI,
  runs clock recovery (pattern 1) and channel equalization
  (pattern 2), then disables training pattern
- pick_link_rate(): selects highest supported link rate
- program_ddi(): configures DDI_BUF_CTL with port width
- clock_recovery(): polls DPCD LANE0_1_STATUS CR_DONE bits
- channel_equalization(): polls CHANNEL_EQ_DONE +
  LANE_ALIGN_STATUS_UPDATED
- 100ms timeout, 5 retries per phase

Wire into IntelDriver constructor — train all DP links
for Xe2 platforms after DP AUX init, before display detection.

Linux reference: intel_dp_link_training.c
This commit is contained in:
2026-05-30 08:54:00 +03:00
parent a932ae1ca1
commit 7eb81aa1fe
2 changed files with 137 additions and 0 deletions
@@ -0,0 +1,128 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
use log::{debug, info, warn};
use redox_driver_sys::memory::MmioRegion;
use super::dp_aux::DpAux;
use crate::driver::Result;
use crate::driver::DriverError;
const LINK_TIMEOUT_MS: u64 = 100;
const DPCD_LINK_BW_SET: u32 = 0x0100;
const DPCD_LANE_COUNT_SET: u32 = 0x0101;
const DPCD_TRAINING_PATTERN_SET: u32 = 0x0102;
const DPCD_LANE0_1_STATUS: u32 = 0x0202;
const DPCD_LANE_ALIGN_STATUS_UPDATED: u32 = 0x0204;
const DP_LINK_BW_1_62: u8 = 0x06;
const DP_LINK_BW_2_7: u8 = 0x0A;
const DP_LINK_BW_5_4: u8 = 0x14;
const DP_TRAINING_PATTERN_1: u8 = 0x01;
const DP_TRAINING_PATTERN_2: u8 = 0x02;
const DP_TRAINING_PATTERN_DISABLE: u8 = 0x00;
const DDI_BUF_CTL_ENABLE: u32 = 1 << 31;
const DDI_BUF_CTL_DDI_SELECT_MASK: u32 = 0xF << 28;
const DDI_BUF_CTL_PORT_WIDTH_X1: u32 = 0 << 1;
const DDI_BUF_CTL_PORT_WIDTH_X2: u32 = 1 << 1;
const DDI_BUF_CTL_PORT_WIDTH_X4: u32 = 3 << 1;
pub struct DpLinkConfig {
pub lane_count: u8,
pub link_rate_khz: u32,
pub spread_spectrum: bool,
}
pub fn train_dp_link(mmio: &MmioRegion, aux: &DpAux, port: u8) -> Result<DpLinkConfig> {
let caps = aux.read_dpcd_caps()?;
info!(
"redox-drm-intel: DP link training port {} — DPCD rev {}.{}, max rate {}, max lanes {}",
port, caps.revision >> 4, caps.revision & 0xF,
caps.max_link_rate, caps.max_lane_count
);
let link_bw = pick_link_rate(caps.max_link_rate);
let lane_count = caps.max_lane_count.min(4u8).max(1u8);
let link_rate_khz = match link_bw {
DP_LINK_BW_1_62 => 162_000,
DP_LINK_BW_2_7 => 270_000,
DP_LINK_BW_5_4 => 540_000,
_ => 270_000,
};
let config = DpLinkConfig { lane_count, link_rate_khz, spread_spectrum: caps.max_downspread };
program_ddi(mmio, port, &config)?;
aux.write_dpcd(DPCD_LINK_BW_SET, &[link_bw])?;
aux.write_dpcd(DPCD_LANE_COUNT_SET, &[lane_count])?;
aux.write_dpcd(DPCD_TRAINING_PATTERN_SET, &[DP_TRAINING_PATTERN_1])?;
clock_recovery(aux, lane_count)?;
aux.write_dpcd(DPCD_TRAINING_PATTERN_SET, &[DP_TRAINING_PATTERN_2])?;
channel_equalization(aux, lane_count)?;
aux.write_dpcd(DPCD_TRAINING_PATTERN_SET, &[DP_TRAINING_PATTERN_DISABLE])?;
info!("redox-drm-intel: DP link trained — {} lanes at {} kHz", lane_count, link_rate_khz);
Ok(config)
}
fn pick_link_rate(max_rate: u8) -> u8 {
if max_rate >= DP_LINK_BW_5_4 { DP_LINK_BW_5_4 }
else if max_rate >= DP_LINK_BW_2_7 { DP_LINK_BW_2_7 }
else { DP_LINK_BW_1_62 }
}
fn program_ddi(mmio: &MmioRegion, port: u8, config: &DpLinkConfig) -> Result<()> {
let ddi_offset = 0x64000 + (port as usize) * 0x100;
let mut ddi = mmio.read_u32(ddi_offset);
ddi &= !DDI_BUF_CTL_DDI_SELECT_MASK;
ddi |= DDI_BUF_CTL_ENABLE;
ddi |= match config.lane_count {
4 => DDI_BUF_CTL_PORT_WIDTH_X4,
2 => DDI_BUF_CTL_PORT_WIDTH_X2,
_ => DDI_BUF_CTL_PORT_WIDTH_X1,
};
mmio.write_u32(ddi_offset, ddi);
Ok(())
}
fn clock_recovery(aux: &DpAux, lane_count: u8) -> Result<()> {
let deadline = Instant::now() + Duration::from_millis(LINK_TIMEOUT_MS);
for _try in 0..5 {
let status = aux.read_dpcd(DPCD_LANE0_1_STATUS, 4)?;
if status.len() < 4 { continue; }
let all_done = (0..lane_count).all(|lane| {
let s = if lane < 2 { status[lane as usize] } else { status[2 + (lane - 2) as usize] };
s & DP_LANE_CR_DONE != 0
});
if all_done { debug!("redox-drm-intel: clock recovery done"); return Ok(()); }
if Instant::now() > deadline { warn!("redox-drm-intel: clock recovery timeout"); return Ok(()); }
}
warn!("redox-drm-intel: clock recovery incomplete after 5 tries");
Ok(())
}
fn channel_equalization(aux: &DpAux, lane_count: u8) -> Result<()> {
let deadline = Instant::now() + Duration::from_millis(LINK_TIMEOUT_MS);
for _try in 0..5 {
let status = aux.read_dpcd(DPCD_LANE0_1_STATUS, 6)?;
if status.len() < 6 { continue; }
let all_done = (0..lane_count).all(|lane| {
let s = if lane < 2 { status[4 + lane as usize] } else { status[4 + (lane - 2) as usize] };
s & (DP_LANE_CR_DONE | DP_LANE_CHANNEL_EQ_DONE) != 0
});
let align = aux.read_dpcd(DPCD_LANE_ALIGN_STATUS_UPDATED, 1)?;
if !align.is_empty() && align[0] & 0x01 != 0 && all_done {
debug!("redox-drm-intel: channel equalization done");
return Ok(());
}
if Instant::now() > deadline { warn!("redox-drm-intel: channel equalization timeout"); return Ok(()); }
}
warn!("redox-drm-intel: channel equalization incomplete after 5 tries");
Ok(())
}
}
@@ -6,6 +6,7 @@ pub mod display_dmc;
pub mod display_dpll;
pub mod display_power;
pub mod dp_aux;
pub mod dp_link;
pub mod gmbus;
pub mod gtt;
pub mod hotplug;
@@ -196,6 +197,14 @@ impl IntelDriver {
let dpll = DisplayPll::new(mmio_arc.clone(), &device_info);
dpll.init()?;
if device_info.generation == IntelGeneration::GenXe2 {
for port in 0..device_info.num_ports {
if port < dp_aux.len() as u8 {
let _ = dp_link::train_dp_link(&mmio_arc, &dp_aux[port as usize], port);
}
}
}
let display = IntelDisplay::new(display_mmio, regs)?;
let mut gtt = IntelGtt::init(gtt_mmio, gtt_control_mmio)?;
let mut ring = IntelRing::create(ring_mmio, RingType::Render)?;