Extracted from local/reference/linux-7.1/drivers/gpu/drm/i915/: - Panel backlight: BLC_PWM_CTL/CTL2 register layouts, PWM frequency formulas for all platforms (Gen2 through BXT/CNP), enable/disable sequences - Panel power sequencing: PP_STATUS/PP_CONTROL/PP_*_DELAYS/PP_DIVISOR register offsets and bit layouts, power-on/off/VDD sequences, delay computation - GPU hang detection: ACTHD comparison, ring head/tail tracking, hangcheck state machine, timeout thresholds - GPU engine reset: GEN6_GDRST/GEN8_GDRST/RING_RESET_CTL register definitions, per-engine reset sequences for Gen8+, global reset flows, platform variations (Gen2 through MTL+) Intended as technical reference for Intel driver implementation in local/recipes/gpu/redox-drm/source/src/drivers/intel/.
16 KiB
Intel Display Subsystem Reference — Linux i915 Patterns
Extracted: 2026-05-30
Source: local/reference/linux-7.1/drivers/gpu/drm/i915/
Purpose: Exact register layouts, bit definitions, and initialization sequences for four Intel display subsystems — extracted for Rust implementation in local/recipes/gpu/redox-drm/source/src/drivers/intel/
1. PANEL BACKLIGHT CONTROL
Register Map (intel_backlight_regs.h)
| Register | Offset | Platform | Purpose |
|---|---|---|---|
BLC_PWM_CTL |
0x61254 | Gen2–4 | Legacy PWM duty cycle control |
BLC_PWM_CTL2 |
0x61250 | Gen4+ | Legacy PWM enable + pipe select |
BLC_PWM_CPU_CTL |
0x48254 | ILK+ | CPU PWM duty cycle |
BLC_PWM_CPU_CTL2 |
0x48250 | ILK+ | CPU PWM enable |
BLC_PWM_PCH_CTL1 |
0xc8250 | PCH-split | PCH backlight enable/polarity |
BLC_PWM_PCH_CTL2 |
0xc8254 | PCH-split | PCH max duty cycle (upper 16 bits) |
BXT_BLC_PWM_CTL |
per-controller | BXT+ | PWM enable + polarity |
BXT_BLC_PWM_FREQ |
per-controller | BXT+ | PWM frequency divisor |
BXT_BLC_PWM_DUTY |
per-controller | BXT+ | PWM duty cycle |
Key Bit Definitions
// BLC_PWM_CTL2 (legacy gen4)
BLM_PWM_ENABLE = (1 << 31)
BLM_COMBINATION_MODE = (1 << 30) // gen4 only
BLM_PIPE_SELECT = (1 << 29)
BLM_PIPE(pipe) = ((pipe) << 29)
BLM_POLARITY_I965 = (1 << 28)
// BLC_PWM_CTL (gen2-4)
BACKLIGHT_MODULATION_FREQ_SHIFT = 17
BACKLIGHT_MODULATION_FREQ_MASK = (0x7fff << 17)
BACKLIGHT_DUTY_CYCLE_SHIFT = 0
BACKLIGHT_DUTY_CYCLE_MASK = 0xffff
BACKLIGHT_DUTY_CYCLE_MASK_PNV = 0xfffe // Pineview 15-bit
BLM_LEGACY_MODE = (1 << 16) // gen2 only
// BLC_PWM_PCH_CTL1 (PCH-split)
BLM_PCH_PWM_ENABLE = (1 << 31)
BLM_PCH_OVERRIDE_ENABLE = (1 << 30) // LPT+ override mode
BLM_PCH_POLARITY = (1 << 29)
// BXT_BLC_PWM_CTL
BXT_BLC_PWM_ENABLE = (1 << 31)
BXT_BLC_PWM_POLARITY = (1 << 29)
// UTIL_PIN_CTL (BXT controller 1 aux pin)
UTIL_PIN_ENABLE = (1 << 31)
UTIL_PIN_PIPE(x) = ((x) << 29)
UTIL_PIN_MODE_PWM = (1 << 24)
UTIL_PIN_POLARITY = (1 << 22)
PWM Frequency Formulas by Platform
| Platform | Clock | Formula | Notes |
|---|---|---|---|
| Gen2 | HRAWCLK/32 | clock / (freq * 32) |
|
| Gen3 (Pineview) | rawclk/32 | clock / (freq * 32) |
|
| Gen4 | CDCLK or rawclk | clock / (freq * 128) |
G4X uses rawclk, otherwise CDCLK |
| ILK/SNB/IVB | rawclk * 128 | rawclk / (freq * 128) |
|
| VLV/CHV | 25MHz/19.2MHz/rawclk | clock / (freq * 16 or 128) |
Clock mux via CBR1_VLV |
| LPT | 135MHz (H) or 24MHz (LP) | clock / (freq * 128 or 16) |
Alt increment via SOUTH_CHICKEN2 |
| SPT | 24MHz | clock / (freq * 16 or 128) |
Alt increment via SOUTH_CHICKEN1 |
| BXT/CNP | 19.2MHz | 19200 / freq |
|
| CNP | rawclk | rawclk / freq |
Enable Sequence (LPT and later PCH-split)
// lpt_enable_backlight()
1. Read existing BLC_PWM_PCH_CTL1; if BLM_PCH_PWM_ENABLE set, clear it first
2. For LPT: Set SOUTH_CHICKEN2 LPT_PWM_GRANULARITY based on panel->backlight.alternate_pwm_increment
For SPT: Set SOUTH_CHICKEN1 SPT_PWM_GRANULARITY
3. Write BLC_PWM_PCH_CTL2 = (pwm_level_max << 16) // Upper 16 bits = max duty
4. Build pch_ctl1 = 0
- If active_low_pwm: pch_ctl1 |= BLM_PCH_POLARITY
- If LPT+: pch_ctl1 |= BLM_PCH_OVERRIDE_ENABLE
5. Write BLC_PWM_PCH_CTL1 = pch_ctl1
6. Posting read BLC_PWM_PCH_CTL1
7. Write BLC_PWM_PCH_CTL1 = pch_ctl1 | BLM_PCH_PWM_ENABLE
8. Call intel_backlight_set_pwm_level(conn_state, level) // Writes duty cycle to CTL2
Disable Sequence (LPT)
// lpt_disable_backlight()
1. intel_backlight_set_pwm_level(old_conn_state, level) // Set final level
2. If BLC_PWM_CPU_CTL2 has BLM_PWM_ENABLE:
Write BLC_PWM_CPU_CTL2 = tmp& ~BLM_PWM_ENABLE
3. Write BLC_PWM_PCH_CTL1 = BLM_PCH_PWM_ENABLE -> 0
2. PANEL POWER SEQUENCING
Register Map (intel_pps_regs.h)
| Register | Offset | Purpose |
|---|---|---|
PP_STATUS |
0x61200 | Panel power status (read-only) |
PP_CONTROL |
0x61204 | Panel power on/off + VDD force |
PP_ON_DELAYS |
0x61208 | Power-up delay + light-on delay |
PP_OFF_DELAYS |
0x6120C | Power-down delay + light-off delay |
PP_DIVISOR |
0x61210 | Clock divider + power cycle delay |
PP_STATUS Bit Definitions
PP_ON = REG_BIT(31)
PP_READY = REG_BIT(30)
PP_SEQUENCE_MASK = REG_GENMASK(29, 28)
PP_SEQUENCE_NONE = 0
PP_SEQUENCE_POWER_UP = 1
PP_SEQUENCE_POWER_DOWN = 2
PP_CYCLE_DELAY_ACTIVE = REG_BIT(27)
PP_SEQUENCE_STATE_MASK = REG_GENMASK(3, 0)
PP_SEQUENCE_STATE_OFF_IDLE = 0x0
PP_SEQUENCE_STATE_ON_IDLE = 0x8
PP_SEQUENCE_STATE_RESET = 0xf
PP_CONTROL Bit Definitions
PANEL_UNLOCK_MASK = REG_GENMASK(31, 16)
PANEL_UNLOCK_REGS = 0xabcd
BXT_POWER_CYCLE_DELAY_MASK = REG_GENMASK(8, 4) // CNP+/BXT+ moved to PP_CONTROL
EDP_FORCE_VDD = REG_BIT(3)
EDP_BLC_ENABLE = REG_BIT(2)
PANEL_POWER_RESET = REG_BIT(1)
PANEL_POWER_ON = REG_BIT(0)
PP_ON_DELAYS Bit Layout
PANEL_PORT_SELECT_MASK = REG_GENMASK(31, 30)
PANEL_PORT_SELECT_VLV(port) = REG_FIELD_PREP(PANEL_PORT_SELECT_MASK, port)
PANEL_POWER_UP_DELAY_MASK = REG_GENMASK(28, 16) // T1+T3 in 100µs units
PANEL_LIGHT_ON_DELAY_MASK = REG_GENMASK(12, 0) // T8 in 100µs units
PP_OFF_DELAYS Bit Layout
PANEL_POWER_DOWN_DELAY_MASK = REG_GENMASK(28, 16) // T10 in 100µs units
PANEL_LIGHT_OFF_DELAY_MASK = REG_GENMASK(12, 0) // T9 in 100µs units
PP_DIVISOR Bit Layout
PP_REFERENCE_DIVIDER_MASK = REG_GENMASK(31, 8)
PANEL_POWER_CYCLE_DELAY_MASK = REG_GENMASK(4, 0) // T11+T12 in 100ms units
Power-On Sequence
// intel_pps_on_unlocked()
1. If panel already on, return early
2. wait_panel_power_cycle() // Ensure T12 delay elapsed
3. For ILK: temporarily disable PANEL_POWER_RESET
4. For DG2: disable SOUTH_DSPCLK_GATE_D PCH_DPLSUNIT_CLOCK_GATE_DISABLE
5. Set PANEL_POWER_ON + (if !ILK) PANEL_POWER_RESET
6. Write PP_CONTROL
7. Posting read PP_CONTROL
8. wait_panel_on() // Poll PP_STATUS until PP_ON + sequence idle
9. Record last_power_on = jiffies
10. Restore DPLS clock gate
11. For ILK: restore PANEL_POWER_RESET bit
Power-Off Sequence
// intel_pps_off_unlocked()
1. Clear: PANEL_POWER_ON | PANEL_POWER_RESET | EDP_FORCE_VDD | EDP_BLC_ENABLE
2. Write PP_CONTROL
3. Posting read PP_CONTROL
4. wait_panel_off() // Poll until PP_ON == 0, sequence idle
5. Record panel_power_off_time = ktime_get_boottime()
VDD Force Sequence
// intel_pps_vdd_on_unlocked()
1. If panel_power_off_time recorded, call wait_panel_power_cycle() if needed
2. Read PP_CONTROL, set EDP_FORCE_VDD
3. Write PP_CONTROL, posting read
4. If panel was not on, sleep panel_power_up_delay ms
// intel_pps_vdd_off_sync_unlocked()
1. Read PP_CONTROL, clear EDP_FORCE_VDD
2. Write PP_CONTROL, posting read
3. If PANEL_POWER_ON was also cleared, record panel_power_off_time
Delay Computation
- All delays in PPS registers use 100µs units
- Power up delay (T1+T3): 10–200ms → write value =
delay_ms * 10 - Power down delay (T10): 0–500ms → write value =
delay_ms * 10 - Power cycle delay (T11+T12): minimum 100ms granularity
- Written as
(value / 100ms) + 1for PP_DIVISOR, or+ 1for BXT+ PP_CONTROL
- Written as
- Final delay =
max(bios_delay, vbt_delay, spec_min) - power_cycle rounds up to nearest 100ms
3. GPU HANG DETECTION
Hangcheck Timer Period
From intel_engine_heartbeat.c: the hangcheck timer fires based on I915_PARAM_HANGCHECK_PERIOD (default 60 seconds). The actual check interval is 30 seconds (half the period).
ACTHD Register Comparison
// check_active_request() — each engine tracks its own ACTHD
// For RCS: ACTHD at RING_ACTHD(RENDER_RING_BASE) = 0xc874
// For VCS: ACTHD at RING_ACTHD(GEN6_BSD_RING_BASE) = engine-specific
// For BCS: ACTHD at RING_ACTHD(BLT_RING_BASE)
// For VECS: ACTHD at RING_ACTHD(VEBOX_RING_BASE)
// Check sequence:
// 1. Read active request's last known ACTHD from context
// 2. Read current ACTHD from hardware
// 3. If current > last by more than 2 DWORDS, engine is making progress
// 4. If equal for two consecutive checks, engine is hung
Ring Head/Tail Comparison
// Per-engine ring state tracked via:
RING_HEAD(base) = base + 0x34 // HEAD_WRAP_COUNT | HEAD_ADDR
RING_TAIL(base) = base + 0x30 // TAIL_ADDR
RING_START(base) = base + 0x38 // Ring base address
RING_CTL(base) = base + 0x3c // Size and valid bits
// Hang detection compares:
// - Ring head pointer vs breadcrumb seqno position
// - ACTHD vs last submitted request's tail
// - Request timeout vs jiffies
// Timeout thresholds from i915_params:
// I915_DEFAULT_HANG_PERIOD = 120 seconds (guilty)
// I915_ACTIVE_HANG_PERIOD = 60 seconds (active request)
Hangcheck State Machine
// Per-engine hang state:
I915_RESET_ENGINE + engine_id // Engine-specific reset in progress
// Global reset state:
I915_RESET_BACKOFF // Another reset in progress
I915_WEDGED // GPU wedged, recovery impossible
// Hangcheck comparison:
// 1. For each active request: compare elapsed time vs I915_ACTIVE_HANG_PERIOD
// 2. If exceeded: mark request as hung, increment gpu_error.reset_engine_count
// 3. If reset fails and retry >= 1, attempt global reset
// 4. Global reset failures set I915_WEDGED
4. GPU ENGINE RESET
Reset Register Definitions (intel_gt_regs.h, intel_engine_regs.h)
// Gen6+ global reset (PCI config space)
I915_GDRST = 0xc04
GRDOM_RESET_ENABLE = (1 << 0)
GRDOM_RESET_STATUS = (1 << 1)
GRDOM_MEDIA = (1 << 2)
GRDOM_RENDER = (1 << 1)
// Gen6+ engine-specific reset (MMIO)
GEN6_GDRST = 0x941c
GEN6_GRDOM_FULL = (1 << 0)
GEN6_GRDOM_RENDER = (1 << 1)
GEN6_GRDOM_MEDIA = (1 << 2)
GEN6_GRDOM_BLT = (1 << 3)
GEN6_GRDOM_VECS = (1 << 4)
GEN9_GRDOM_GUC = (1 << 5)
GEN8_GRDOM_MEDIA2 = (1 << 7)
// Gen11+ expanded domains
GEN11_GRDOM_FULL = GEN6_GRDOM_FULL
GEN11_GRDOM_RENDER = GEN6_GRDOM_RENDER
GEN11_GRDOM_SFC0-3 = BIT(17-20)
GEN11_GRDOM_VECS4 = BIT(16)
// ... more media engines up to GEN11_GRDOM_MEDIA8 = BIT(12)
// Per-engine ring reset control (engine-specific MMIO)
RING_RESET_CTL(base) = base + 0xd0
RESET_CTL_CAT_ERROR = REG_BIT(2) // Catastrophic error, skip ready-to-reset
RESET_CTL_READY_TO_RESET = REG_BIT(1) // Engine ready for reset
RESET_CTL_REQUEST_RESET = REG_BIT(0) // Request reset
Per-Engine Reset Sequence (Gen8+)
// gen8_engine_reset_prepare()
1. Read RING_RESET_CTL(engine->mmio_base)
2. If RESET_CTL_CAT_ERROR set:
request = RESET_CTL_CAT_ERROR
mask = RESET_CTL_CAT_ERROR
ack = 0 (HW clears cat error)
Else if RESET_CTL_READY_TO_RESET not set:
request = RESET_CTL_REQUEST_RESET
mask = RESET_CTL_READY_TO_RESET
ack = RESET_CTL_READY_TO_RESET
Else:
return 0 (already ready)
3. Write RING_RESET_CTL = REG_MASKED_FIELD_ENABLE(request)
4. Wait for RING_RESET_CTL & mask == ack, timeout 700µs
5. If timeout, log error and return -ETIMEDOUT
// gen8_engine_reset_cancel()
1. Write RING_RESET_CTL = REG_MASKED_FIELD_DISABLE(RESET_CTL_REQUEST_RESET)
Global Reset Sequences
Gen3 (PCI config):
pci_write_config_byte(pdev, I915_GDRST, GRDOM_RESET_ENABLE)
udelay(50)
_wait_for_atomic(i915_in_reset(pdev), 50000)
pci_write_config_byte(pdev, I915_GDRST, 0)
udelay(50)
_wait_for_atomic(!i915_in_reset(pdev), 50000)
Gen4 (PCI config, separate media/render):
pci_write_config_byte(pdev, I915_GDRST, GRDOM_MEDIA | GRDOM_RESET_ENABLE)
_wait_for_atomic(g4x_reset_complete(), 50000)
pci_write_config_byte(pdev, I915_GDRST, GRDOM_RENDER | GRDOM_RESET_ENABLE)
_wait_for_atomic(g4x_reset_complete(), 50000)
pci_write_config_byte(pdev, I915_GDRST, 0)
Gen6+ (MMIO domain reset):
loops = (GRAPHICS_VER_FULL < IP_VER(12, 70)) ? 2 : 1 // Jasperlake needs 2x
do {
intel_uncore_write_fw(uncore, GEN6_GDRST, hw_domain_mask)
err = __intel_wait_for_register_fw(uncore, GEN6_GDRST,
hw_domain_mask, 0,
2000, 0, NULL)
} while (err == 0 && --loops)
udelay(50) // Settle delay after reset ack
Gen11+ (with SFC lock handling):
// __gen11_reset_engines()
1. For each engine in mask: call gen11_lock_sfc()
- Read GEN11_VCS_SFC_FORCED_LOCK, set lock bit
- Wait 1000µs for GEN11_VCS_SFC_LOCK_ACK
- If lock obtained to different engine, add paired engine to unlock_mask
2. Call gen6_hw_domain_reset(gt, reset_mask)
3. For each engine in unlock_mask: call gen11_unlock_sfc()
- Clear GEN11_VCS_SFC_FORCED_LOCK_BIT
Reset Flow (Top-Level)
// __intel_gt_reset()
reset = intel_get_gpu_reset(gt)
intel_uncore_forcewake_get(gt->uncore, FORCEWAKE_ALL)
for retry in 0..RESET_MAX_RETRIES:
reset_mask = wa_14015076503_start(gt, engine_mask, !retry)
ret = reset(gt, reset_mask, retry)
wa_14015076503_end(gt, reset_mask)
if ret == 0 break
intel_uncore_forcewake_put(gt->uncore, FORCEWAKE_ALL)
return ret
// intel_gt_reset()
1. gt_revoke() // Revoke mmaps with userfault
2. mutex_lock(>->reset.mutex)
3. __intel_gt_unset_wedged()
4. reset_prepare() // Stop engines, prevent RC6 entry
5. do_reset() // Call __intel_gt_reset()
6. gt_reset() // Re-init GGTT, reset each engine, restore GuC
7. resume() // Resume each engine
8. reset_finish() // Signal breadcrumbs, put forcewake
Platform Variations
| Platform | Reset Mechanism | Notes |
|---|---|---|
| Gen2–3 | PCI config GDRST | Simple pulse reset |
| G33/G4x | PCI config GDRST | Separate media/render domains |
| ILK | ILK_GDSR MMIO | Separate render/media domains |
| Gen6+ | GEN6_GDRST MMIO | Per-domain reset bits |
| Gen8+ | gen8_reset_engines() | Per-engine RING_RESET_CTL sequence |
| Gen11+ | gen11_lock_sfc() + __gen11_reset_engines() | SFC forced lock before reset |
| DG2 | __gen11_reset_engines() first, then full reset | Wa_22011100796 |
| MTL+ | Wa_14015076503 for GSC engine | 200ms wait for GSC FW |
Implementation Notes for Red Bear Rust Driver
-
Backlight: The PWM duty cycle register is the lower 16 bits of
BLC_PWM_CTL2on legacy platforms. On PCH-split, the max duty goes in the upper 16 bits ofBLC_PWM_PCH_CTL2. The enable bit is inBLC_PWM_PCH_CTL1. Track the platform variant and use the correct register pair. -
PPS: The power sequencing state machine relies on polling
PP_STATUSbits with timeout. The sequence state machine inPP_STATUStracks 16 states. VDD must be forced before powering on panel. T12 (power cycle delay) must be observed before re-powering. -
Hangcheck: The ACTHD comparison is between the hardware register and the last known position of the active request. If they match across two check intervals, the engine is hung.
-
Reset: The
RING_RESET_CTLsequence on Gen8+ requires waiting forREADY_TO_RESETbefore issuingREQUEST_RESET. Catastrophic errors (CAT_ERROR) bypass the ready check. The reset settle delay (udelay(50)) is required before restoring engine state.
Key Source Files
| File | Subsystem | Key content |
|---|---|---|
display/intel_backlight.c |
Backlight | PWM enable/disable sequences, level setting |
display/intel_backlight_regs.h |
Backlight | Register offsets + bit definitions |
display/intel_pps.c |
PPS | Power sequencing state machine, VDD control |
display/intel_pps_regs.h |
PPS | PP_STATUS/PP_CONTROL/PP_*_DELAYS/PP_DIVISOR |
gt/intel_reset.c |
Reset | Global + engine reset logic, RESET_MAX_RETRIES=3 |
gt/intel_engine_heartbeat.c |
Hangcheck | Heartbeat timer, hang detection |
gt/gen6_engine_cs.c |
Hangcheck | ACTHD comparison, ring state |
gt/intel_gt_regs.h |
Reset | GEN6_GDRST, GEN8_GDRST, domain bit definitions |
gt/intel_engine_regs.h |
Reset | RING_RESET_CTL, RING_HEAD/TAIL/CTL offsets |
gt/selftest_hangcheck.c |
Hangcheck | Hang injection + detection test patterns |