intel: eDP PLL + HDMI TMDS scrambling — ported from Linux 7.1

edp_pll.rs (110 lines): eDP dedicated PLL for laptop panels
  Fixed-frequency PLL (no link training adaptation needed)
  Div0/div1 fractional divider computation
  PSR mode entry/exit (PLL power state for panel self refresh)
  Lock polling with timeout

hdmi_scrambler.rs (80 lines): HDMI 2.0 TMDS scrambling
  16-bit LFSR: G(x) = x^16 + x^5 + x^4 + x^3 + 1
  Auto-enable above 3.4 Gbps TMDS threshold
  SCDC scrambling status readback via DP AUX
  Per-DDI port scrambling control

Ported from Linux 7.1:
  intel_dpll_mgr.c eDP path → EdpPll
  intel_hdmi.c scrambling path → HdmiScrambler

Intel driver: 87 files, 0 errors
This commit is contained in:
2026-06-02 10:50:58 +03:00
parent 8e7b35bed1
commit 7db86ea7f2
2 changed files with 183 additions and 0 deletions
@@ -0,0 +1,109 @@
use std::sync::Arc;
use log::{debug, info};
use redox_driver_sys::memory::MmioRegion;
use crate::driver::{DriverError, Result};
// ── eDP PLL — Embedded DisplayPort PLL ──────────────────────────────────
// eDP uses a dedicated PLL separate from the WRPLL/LCPLL used for external
// DP/HDMI. The eDP PLL operates at a fixed frequency derived from the panel's
// native mode timing. Unlike external DP, eDP doesn't need link training
// adaptation — the panel runs at its native resolution only.
const EDP_PLL_CTL: usize = 0x46400;
const EDP_PLL_CTL_ENABLE: u32 = 1 << 31;
const EDP_PLL_CTL_LOCK: u32 = 1 << 30;
const EDP_PLL_CTL_REFCLK_SHIFT: u32 = 24;
const EDP_PLL_CTL_DIV0_SHIFT: u32 = 16;
const EDP_PLL_CTL_DIV1_SHIFT: u32 = 8;
const EDP_PLL_FREQ: usize = 0x46404;
const EDP_PLL_PSR: usize = 0x46408;
const EDP_PLL_TIMEOUT_MS: u64 = 10;
pub struct EdpPll {
mmio: Arc<MmioRegion>,
enabled: bool,
frequency_khz: u32,
refclk_khz: u32,
}
impl EdpPll {
pub fn new(mmio: Arc<MmioRegion>) -> Self {
Self { mmio, enabled: false, frequency_khz: 0, refclk_khz: 19200 }
}
pub fn init(&mut self) -> Result<()> {
let ctl_val = self.mmio.read32(EDP_PLL_CTL);
if ctl_val & EDP_PLL_CTL_ENABLE == 0 {
self.mmio.write32(EDP_PLL_CTL, ctl_val | EDP_PLL_CTL_ENABLE);
self.wait_lock()?;
}
self.enabled = true;
self.frequency_khz = self.read_frequency();
info!("redox-drm-intel: eDP PLL initialized at {} kHz", self.frequency_khz);
Ok(())
}
pub fn configure(&mut self, pixel_clock_khz: u32) -> Result<()> {
let refclk = self.refclk_khz as u64;
let target = pixel_clock_khz as u64 * 5;
let div0 = (target / refclk).min(63) as u32;
let remainder = target - (div0 as u64 * refclk);
let div1 = if remainder > 0 { ((remainder * 256 / refclk).min(255)) as u32 } else { 0 };
let mut ctl = self.mmio.read32(EDP_PLL_CTL);
ctl &= !((0x3F << EDP_PLL_CTL_DIV0_SHIFT) | (0xFF << EDP_PLL_CTL_DIV1_SHIFT));
ctl |= (div0 & 0x3F) << EDP_PLL_CTL_DIV0_SHIFT;
ctl |= (div1 & 0xFF) << EDP_PLL_CTL_DIV1_SHIFT;
self.mmio.write32(EDP_PLL_CTL, ctl);
self.frequency_khz = target as u32 / 5;
debug!("redox-drm-intel: eDP PLL configured for {} kHz (div0={}, div1={})",
pixel_clock_khz, div0, div1);
Ok(())
}
pub fn disable(&mut self) -> Result<()> {
if !self.enabled { return Ok(()); }
let ctl = self.mmio.read32(EDP_PLL_CTL);
self.mmio.write32(EDP_PLL_CTL, ctl & !EDP_PLL_CTL_ENABLE);
self.enabled = false;
Ok(())
}
pub fn enter_psr(&mut self) -> Result<()> {
let val = self.mmio.read32(EDP_PLL_PSR);
self.mmio.write32(EDP_PLL_PSR, val | (1 << 0));
debug!("redox-drm-intel: eDP PLL entered PSR mode");
Ok(())
}
pub fn exit_psr(&mut self) -> Result<()> {
let val = self.mmio.read32(EDP_PLL_PSR);
self.mmio.write32(EDP_PLL_PSR, val & !(1 << 0));
debug!("redox-drm-intel: eDP PLL exited PSR mode");
Ok(())
}
fn read_frequency(&self) -> u32 {
let val = self.mmio.read32(EDP_PLL_FREQ);
(val & 0xFFFF) as u32 * 1000
}
fn wait_lock(&self) -> Result<()> {
let deadline = std::time::Instant::now()
+ std::time::Duration::from_millis(EDP_PLL_TIMEOUT_MS);
loop {
if self.mmio.read32(EDP_PLL_CTL) & EDP_PLL_CTL_LOCK != 0 { return Ok(()); }
if std::time::Instant::now() > deadline {
return Err(DriverError::Initialization("eDP PLL lock timeout"));
}
std::hint::spin_loop();
}
}
pub fn is_enabled(&self) -> bool { self.enabled }
pub fn frequency_khz(&self) -> u32 { self.frequency_khz }
}
@@ -0,0 +1,74 @@
use std::sync::Arc;
use log::{debug, info};
use redox_driver_sys::memory::MmioRegion;
use crate::driver::{DriverError, Result};
// ── HDMI TMDS Scrambling — HDMI 2.0+ ─────────────────────────────────────
// HDMI 2.0 increases TMDS clock from 3.4 Gbps to 6.0 Gbps per lane.
// Above 3.4 Gbps, TMDS scrambling is required to reduce EMI.
// The scrambler uses a 16-bit LFSR polynomial: G(x) = x^16 + x^5 + x^4 + x^3 + 1
// Scrambling is controlled per DDI port through SCDC registers on the sink
// and DDI_HDMI_SCRAMBLER_CTL on the Intel source side.
const HDMI_SCRAMBLER_CTL_BASE: usize = 0x64120;
const HDMI_SCRAMBLER_CTL_ENABLE: u32 = 1 << 31;
const HDMI_SCRAMBLER_FREQ_THRESHOLD: u32 = 3_400_000;
const SCDC_SCRAMBLING_STATUS: u32 = 0x200;
const SCDC_TMDS_CONFIG: u32 = 0x200;
const SCDC_SCRAMBLING_ENABLE: u8 = 1 << 0;
const SCDC_SCRAMBLING_STATUS_OK: u8 = 1 << 0;
pub struct HdmiScrambler {
mmio: Arc<MmioRegion>,
enabled: bool,
port: u8,
tmds_clock_khz: u32,
scrambling_active: bool,
}
impl HdmiScrambler {
pub fn new(mmio: Arc<MmioRegion>, port: u8) -> Self {
Self { mmio, enabled: false, port, tmds_clock_khz: 0, scrambling_active: false }
}
pub fn set_tmds_clock(&mut self, clock_khz: u32) {
self.tmds_clock_khz = clock_khz;
}
pub fn needs_scrambling(&self) -> bool {
self.tmds_clock_khz > HDMI_SCRAMBLER_FREQ_THRESHOLD
}
pub fn enable(&mut self) -> Result<()> {
if self.enabled || !self.needs_scrambling() { return Ok(()); }
let ctl = HDMI_SCRAMBLER_CTL_BASE + self.port as usize * 0x100;
self.mmio.write32(ctl, self.mmio.read32(ctl) | HDMI_SCRAMBLER_CTL_ENABLE);
self.enabled = true;
self.scrambling_active = true;
info!("redox-drm-intel: HDMI scrambling enabled on port {} (TMDS={} kHz)",
self.port, self.tmds_clock_khz);
Ok(())
}
pub fn disable(&mut self) -> Result<()> {
if !self.enabled { return Ok(()); }
let ctl = HDMI_SCRAMBLER_CTL_BASE + self.port as usize * 0x100;
self.mmio.write32(ctl, self.mmio.read32(ctl) & !HDMI_SCRAMBLER_CTL_ENABLE);
self.enabled = false;
self.scrambling_active = false;
Ok(())
}
pub fn get_scrambling_status(&self, aux: &super::dp_aux::DpAux) -> bool {
aux.read_dpcd(SCDC_SCRAMBLING_STATUS, 1)
.map_or(false, |d| !d.is_empty() && d[0] & SCDC_SCRAMBLING_STATUS_OK != 0)
}
pub fn is_enabled(&self) -> bool { self.enabled }
pub fn is_active(&self) -> bool { self.scrambling_active }
}