| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/graphics/display/drivers/intel-i915/ddi-physical-layer.h" |
| |
| #include <lib/ddk/debug.h> |
| #include <lib/fit/defer.h> |
| #include <lib/mmio/mmio-buffer.h> |
| #include <lib/zx/result.h> |
| #include <zircon/assert.h> |
| |
| #include <cinttypes> |
| #include <cstdint> |
| |
| #include <fbl/string_printf.h> |
| |
| #include "src/graphics/display/drivers/intel-i915/ddi-physical-layer-internal.h" |
| #include "src/graphics/display/drivers/intel-i915/hardware-common.h" |
| #include "src/graphics/display/drivers/intel-i915/intel-i915.h" |
| #include "src/graphics/display/drivers/intel-i915/poll-until.h" |
| #include "src/graphics/display/drivers/intel-i915/power-controller.h" |
| #include "src/graphics/display/drivers/intel-i915/registers-ddi-phy-tiger-lake.h" |
| #include "src/graphics/display/drivers/intel-i915/registers-typec.h" |
| |
| namespace i915 { |
| |
| namespace { |
| |
| const char* DdiTypeToString(DdiPhysicalLayer::DdiType type) { |
| switch (type) { |
| case DdiPhysicalLayer::DdiType::kCombo: |
| return "COMBO"; |
| case DdiPhysicalLayer::DdiType::kTypeC: |
| return "Type-C"; |
| } |
| } |
| |
| const char* PortTypeToString(DdiPhysicalLayer::ConnectionType type) { |
| switch (type) { |
| case DdiPhysicalLayer::ConnectionType::kNone: |
| return "None"; |
| case DdiPhysicalLayer::ConnectionType::kBuiltIn: |
| return "Built In"; |
| case DdiPhysicalLayer::ConnectionType::kTypeCDisplayPortAltMode: |
| return "Type-C DisplayPort Alt Mode"; |
| case DdiPhysicalLayer::ConnectionType::kTypeCThunderbolt: |
| return "Type-C Thunderbolt Mode"; |
| break; |
| } |
| } |
| |
| } // namespace |
| |
| void DdiPhysicalLayer::AddRef() { |
| ZX_DEBUG_ASSERT(IsEnabled()); |
| ++ref_count_; |
| zxlogf(TRACE, "DdiPhysicalLayer: Reference count of DDI %d increased to %d", ddi_id(), |
| ref_count_); |
| } |
| |
| void DdiPhysicalLayer::Release() { |
| ZX_DEBUG_ASSERT(ref_count_ > 0); |
| if (--ref_count_ == 0) { |
| zxlogf(TRACE, "DdiPhysicalLayer: Reference count of DDI %d decreased to %d", ddi_id(), |
| ref_count_); |
| if (!Disable()) { |
| zxlogf(ERROR, "DdiPhysicalLayer: Failed to disable unused DDI %d", ddi_id()); |
| } |
| } |
| } |
| |
| fbl::String DdiPhysicalLayer::PhysicalLayerInfo::DebugString() const { |
| return fbl::StringPrintf("PhysicalLayerInfo { type: %s, port: %s, max_allowed_dp_lane: %u }", |
| DdiTypeToString(ddi_type), PortTypeToString(connection_type), |
| max_allowed_dp_lane_count); |
| } |
| |
| bool DdiSkylake::Enable() { |
| if (enabled_) { |
| zxlogf(WARNING, "DDI %d: Enable: PHY already enabled", ddi_id()); |
| } |
| enabled_ = true; |
| return true; |
| } |
| |
| bool DdiSkylake::Disable() { |
| if (!enabled_) { |
| zxlogf(WARNING, "DDI %d: Disable: PHY already disabled", ddi_id()); |
| } |
| enabled_ = false; |
| return true; |
| } |
| |
| DdiPhysicalLayer::PhysicalLayerInfo DdiSkylake::GetPhysicalLayerInfo() const { |
| return { |
| .ddi_type = DdiPhysicalLayer::DdiType::kCombo, |
| .connection_type = DdiPhysicalLayer::ConnectionType::kBuiltIn, |
| .max_allowed_dp_lane_count = 4u, |
| }; |
| } |
| |
| ComboDdiTigerLake::ComboDdiTigerLake(DdiId ddi_id, fdf::MmioBuffer* mmio_space) |
| : DdiPhysicalLayer(ddi_id), mmio_space_(mmio_space) { |
| ZX_DEBUG_ASSERT(mmio_space); |
| } |
| |
| bool ComboDdiTigerLake::Enable() { |
| if (enabled_) { |
| zxlogf(WARNING, "DDI %d: Enable: PHY already enabled", ddi_id()); |
| } |
| enabled_ = true; |
| return true; |
| } |
| |
| bool ComboDdiTigerLake::Disable() { |
| if (!enabled_) { |
| zxlogf(WARNING, "DDI %d: Enable: PHY already disabled", ddi_id()); |
| } |
| enabled_ = false; |
| return true; |
| } |
| |
| namespace { |
| |
| struct TigerLakeProcessCompensationConfig { |
| struct VoltageReferences { |
| struct Pair { |
| uint16_t low = 0; |
| uint16_t high = 0; |
| |
| // True for default-constructed values. |
| bool IsEmpty() const { return low == 0 && high == 0; } |
| }; |
| Pair negative, positive; |
| |
| // True for default-constructed values. |
| bool IsEmpty() const { return negative.IsEmpty() && positive.IsEmpty(); } |
| }; |
| VoltageReferences nominal, low; |
| |
| // True for default-constructed values. |
| bool IsEmpty() const { return nominal.IsEmpty() && low.IsEmpty(); } |
| }; |
| |
| TigerLakeProcessCompensationConfig ReadTigerLakeProcessCompensationConfig( |
| DdiId ddi_id, fdf::MmioBuffer* mmio_space) { |
| auto compensation1 = registers::PortCompensation1::GetForDdi(ddi_id).ReadFrom(mmio_space); |
| auto compensation_nominal = |
| registers::PortCompensationNominalVoltageReferences::GetForDdi(ddi_id).ReadFrom(mmio_space); |
| auto compensation_low = |
| registers::PortCompensationLowVoltageReferences::GetForDdi(ddi_id).ReadFrom(mmio_space); |
| |
| zxlogf(TRACE, "DDI %d PORT_COMP_DW1: %08x PORT_COMP_DW_9: %08x PORT_COMP_DW10: %08x", ddi_id, |
| compensation1.reg_value(), compensation_nominal.reg_value(), compensation_low.reg_value()); |
| |
| return TigerLakeProcessCompensationConfig{ |
| .nominal = |
| { |
| .negative = |
| { |
| .low = static_cast<uint16_t>( |
| compensation_nominal |
| .negative_nominal_voltage_reference_low_value_bits70() | |
| (compensation1.negative_nominal_voltage_reference_low_value_bits98() |
| << 8)), |
| .high = static_cast<uint16_t>( |
| compensation_nominal |
| .negative_nominal_voltage_reference_high_value_bits70() | |
| (compensation1.negative_nominal_voltage_reference_high_value_bits98() |
| << 8)), |
| }, |
| .positive = |
| { |
| .low = static_cast<uint16_t>( |
| compensation_nominal |
| .positive_nominal_voltage_reference_low_value_bits70() | |
| (compensation1.positive_nominal_voltage_reference_low_value_bits98() |
| << 8)), |
| .high = static_cast<uint16_t>( |
| compensation_nominal |
| .positive_nominal_voltage_reference_high_value_bits70() | |
| (compensation1.positive_nominal_voltage_reference_high_value_bits98() |
| << 8)), |
| }, |
| }, |
| .low = |
| { |
| .negative{ |
| .low = static_cast<uint16_t>( |
| compensation_low.negative_low_voltage_reference_low_value_bits70() | |
| (compensation1.negative_low_voltage_reference_low_value_bits98() << 8)), |
| .high = static_cast<uint16_t>( |
| compensation_low.negative_low_voltage_reference_high_value_bits70() | |
| (compensation1.negative_low_voltage_reference_high_value_bits98() << 8)), |
| }, |
| .positive{ |
| .low = static_cast<uint16_t>( |
| compensation_low.positive_low_voltage_reference_low_value_bits70() | |
| (compensation1.positive_low_voltage_reference_low_value_bits98() << 8)), |
| .high = static_cast<uint16_t>( |
| compensation_low.positive_low_voltage_reference_high_value_bits70() | |
| (compensation1.positive_low_voltage_reference_high_value_bits98() << 8)), |
| }, |
| }, |
| }; |
| } |
| |
| void WriteTigerLakeProcessCompensationConfig(const TigerLakeProcessCompensationConfig& config, |
| DdiId ddi_id, fdf::MmioBuffer* mmio_space) { |
| auto compensation1 = registers::PortCompensation1::GetForDdi(ddi_id).ReadFrom(mmio_space); |
| compensation1.set_negative_low_voltage_reference_low_value_bits98(config.low.negative.low >> 8) |
| .set_negative_low_voltage_reference_high_value_bits98(config.low.negative.high >> 8) |
| .set_positive_low_voltage_reference_low_value_bits98(config.low.positive.low >> 8) |
| .set_positive_low_voltage_reference_high_value_bits98(config.low.positive.high >> 8) |
| .set_negative_nominal_voltage_reference_low_value_bits98(config.nominal.negative.low >> 8) |
| .set_negative_nominal_voltage_reference_high_value_bits98(config.nominal.negative.high >> 8) |
| .set_positive_nominal_voltage_reference_low_value_bits98(config.nominal.positive.low >> 8) |
| .set_positive_nominal_voltage_reference_high_value_bits98(config.nominal.positive.high >> 8) |
| .WriteTo(mmio_space); |
| |
| auto compensation_nominal = |
| registers::PortCompensationNominalVoltageReferences::GetForDdi(ddi_id).FromValue(0); |
| compensation_nominal |
| .set_negative_nominal_voltage_reference_low_value_bits70(config.nominal.negative.low & 0xff) |
| .set_negative_nominal_voltage_reference_high_value_bits70(config.nominal.negative.high & 0xff) |
| .set_positive_nominal_voltage_reference_low_value_bits70(config.nominal.positive.low & 0xff) |
| .set_positive_nominal_voltage_reference_high_value_bits70(config.nominal.positive.high & 0xff) |
| .WriteTo(mmio_space); |
| |
| auto compensation_low = |
| registers::PortCompensationLowVoltageReferences::GetForDdi(ddi_id).FromValue(0); |
| compensation_low |
| .set_negative_low_voltage_reference_low_value_bits70(config.low.negative.low & 0xff) |
| .set_negative_low_voltage_reference_high_value_bits70(config.low.negative.high & 0xff) |
| .set_positive_low_voltage_reference_low_value_bits70(config.low.positive.low & 0xff) |
| .set_positive_low_voltage_reference_high_value_bits70(config.low.positive.high & 0xff) |
| .WriteTo(mmio_space); |
| } |
| |
| // Returns an empty configuration for unsupported process monitor values. |
| TigerLakeProcessCompensationConfig ProcessCompensationConfigFor( |
| registers::PortCompensationStatus::ProcessSelect process, |
| registers::PortCompensationStatus::VoltageSelect voltage) { |
| switch (voltage) { |
| case registers::PortCompensationStatus::VoltageSelect::k850mv: |
| switch (process) { |
| case registers::PortCompensationStatus::ProcessSelect::kDot0: |
| return TigerLakeProcessCompensationConfig{ |
| .nominal = {.negative = {.low = 0x62, .high = 0xab}, |
| .positive = {.low = 0x67, .high = 0xbb}}, |
| .low = {.negative = {.low = 0x51, .high = 0x91}, |
| .positive = {.low = 0x4f, .high = 0x96}}}; |
| case registers::PortCompensationStatus::ProcessSelect::kDot1: |
| case registers::PortCompensationStatus::ProcessSelect::kDot4: |
| break; |
| }; |
| break; |
| case registers::PortCompensationStatus::VoltageSelect::k950mv: |
| switch (process) { |
| case registers::PortCompensationStatus::ProcessSelect::kDot0: |
| return TigerLakeProcessCompensationConfig{ |
| .nominal = {.negative = {.low = 0x86, .high = 0xe1}, |
| .positive = {.low = 0x72, .high = 0xc7}}, |
| .low = {.negative = {.low = 0x77, .high = 0xca}, |
| .positive = {.low = 0x5e, .high = 0xab}}}; |
| case registers::PortCompensationStatus::ProcessSelect::kDot1: |
| return TigerLakeProcessCompensationConfig{ |
| .nominal = {.negative = {.low = 0x93, .high = 0xf8}, |
| .positive = {.low = 0x7e, .high = 0xf1}}, |
| .low = {.negative = {.low = 0x8a, .high = 0xe8}, |
| .positive = {.low = 0x71, .high = 0xc5}}}; |
| case registers::PortCompensationStatus::ProcessSelect::kDot4: |
| break; |
| }; |
| break; |
| case registers::PortCompensationStatus::VoltageSelect::k1050mv: |
| switch (process) { |
| case registers::PortCompensationStatus::ProcessSelect::kDot0: |
| return TigerLakeProcessCompensationConfig{ |
| .nominal = {.negative = {.low = 0x98, .high = 0xfa}, |
| .positive = {.low = 0x82, .high = 0xdd}}, |
| .low = {.negative = {.low = 0x89, .high = 0xe4}, |
| .positive = {.low = 0x6d, .high = 0xc1}}}; |
| case registers::PortCompensationStatus::ProcessSelect::kDot1: |
| return TigerLakeProcessCompensationConfig{ |
| .nominal = {.negative = {.low = 0x9a, .high = 0x100}, |
| .positive = {.low = 0xab, .high = 0x125}}, |
| .low = {.negative = {.low = 0x8a, .high = 0xe3}, |
| .positive = {.low = 0x8f, .high = 0xf1}}}; |
| case registers::PortCompensationStatus::ProcessSelect::kDot4: |
| break; |
| }; |
| }; |
| |
| zxlogf(ERROR, "Undocumented process/voltage combination"); |
| return {}; |
| } |
| |
| } // namespace |
| |
| bool ComboDdiTigerLake::Initialize() { |
| // This implements the section "Digital Display Interface" > "Combo PHY |
| // Initialization Sequence" in display engine PRMs. |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev2.0 pages 391-392 |
| // DG1: IHD-OS-DG1-Vol 12-2.21 pages 337-338 |
| // Ice Lake: IHD-OS-ICLLP-Vol 12-1.22-Rev2.0 pages 334-335 |
| |
| // TODO(https://fxbug.dev/42065111): Implement the compensation source dependency |
| // between DDI A and DDIs B-C. |
| |
| auto procmon_status = |
| registers::PortCompensationStatus::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| { |
| const char* process_name; |
| switch (procmon_status.process_select()) { |
| case registers::PortCompensationStatus::ProcessSelect::kDot0: |
| process_name = "dot-0"; |
| break; |
| case registers::PortCompensationStatus::ProcessSelect::kDot1: |
| process_name = "dot-1"; |
| break; |
| case registers::PortCompensationStatus::ProcessSelect::kDot4: |
| process_name = "dot-4"; |
| break; |
| default: |
| zxlogf(WARNING, "DDI %d process monitor reports undocumented process variation %" PRIu32, |
| ddi_id(), static_cast<unsigned int>(procmon_status.process_select())); |
| process_name = "dot-undocumented"; |
| }; |
| |
| const char* voltage_name; |
| switch (procmon_status.voltage_select()) { |
| case registers::PortCompensationStatus::VoltageSelect::k850mv: |
| voltage_name = "0.85v"; |
| break; |
| case registers::PortCompensationStatus::VoltageSelect::k950mv: |
| voltage_name = "0.95v"; |
| break; |
| case registers::PortCompensationStatus::VoltageSelect::k1050mv: |
| voltage_name = "1.05v"; |
| break; |
| default: |
| zxlogf(WARNING, "DDI %d process monitor reports undocumented voltage variation %" PRIu32, |
| ddi_id(), static_cast<unsigned int>(procmon_status.voltage_select())); |
| voltage_name = "undocumented-v"; |
| }; |
| |
| zxlogf(TRACE, "DDI %d Process variation: %s %s, Process monitor done: %s ", ddi_id(), |
| process_name, voltage_name, procmon_status.process_monitor_done() ? "yes" : "no"); |
| zxlogf(TRACE, |
| "DDI %d Current comp: %u%s%s, MIPI LPDn code: %u%s%s, First compensation done: %s", |
| ddi_id(), procmon_status.current_compensation_code(), |
| procmon_status.current_compensation_code_maxout() ? " maxout" : "", |
| procmon_status.current_compensation_code_minout() ? " minout" : "", |
| procmon_status.mipi_low_power_data_negative_code(), |
| procmon_status.mipi_low_power_data_negative_code_maxout() ? " maxout" : "", |
| procmon_status.mipi_low_power_data_negative_code_minout() ? " minout" : "", |
| procmon_status.first_compensation_done() ? "yes" : "no"); |
| } |
| |
| { |
| TigerLakeProcessCompensationConfig process_compensation = |
| ReadTigerLakeProcessCompensationConfig(ddi_id(), mmio_space_); |
| zxlogf( |
| TRACE, |
| "DDI %d Process monitor nominal voltage references: -ve low %x high %x, +ve low %x high %x", |
| ddi_id(), process_compensation.nominal.negative.low, |
| process_compensation.nominal.negative.high, process_compensation.nominal.positive.low, |
| process_compensation.nominal.positive.high); |
| zxlogf(TRACE, |
| "DDI %d Process monitor low voltage references: -ve low %x high %x, +ve low %x high %x", |
| ddi_id(), process_compensation.low.negative.low, process_compensation.low.negative.high, |
| process_compensation.low.positive.low, process_compensation.low.positive.high); |
| } |
| |
| auto common_lane5 = registers::PortCommonLane5::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| zxlogf(TRACE, |
| "DDI %d PORT_CL_DW5: %08x, common lane power down %s, suspend clock config %d, " |
| "downlink broadcast %s, force %02x, CRI clock: count max %d select %d, " |
| "IOSF PD: count %d divider select %d, PHY power ack override %s, " |
| "staggering: port %s power gate %s, fuse flags: %s %s %s", |
| ddi_id(), common_lane5.reg_value(), |
| common_lane5.common_lane_power_down_enabled() ? "enabled" : "disabled", |
| common_lane5.suspend_clock_config(), |
| common_lane5.downlink_broadcast_enable() ? "enabled" : "disabled", common_lane5.force(), |
| common_lane5.common_register_interface_clock_count_max(), |
| common_lane5.common_register_interface_clock_select(), |
| common_lane5.onchip_system_fabric_presence_detection_count(), |
| common_lane5.onchip_system_fabric_clock_divider_select(), |
| common_lane5.phy_power_ack_override() ? "enabled" : "disabled", |
| common_lane5.port_staggering_enabled() ? "enabled" : "disabled", |
| common_lane5.port_staggering_enabled() ? "enabled" : "disabled", |
| common_lane5.fuse_valid_override() ? "valid override" : "-", |
| common_lane5.fuse_valid_reset() ? "valid reset" : "-", |
| common_lane5.fuse_repull() ? "repull" : "-"); |
| |
| static constexpr registers::PortLane kAllLanes[] = { |
| registers::PortLane::kAux, registers::PortLane::kMainLinkLane0, |
| registers::PortLane::kMainLinkLane1, registers::PortLane::kMainLinkLane2, |
| registers::PortLane::kMainLinkLane3}; |
| for (registers::PortLane lane : kAllLanes) { |
| auto transmitter_dcc = |
| registers::PortTransmitterDutyCycleCorrection::GetForDdiLane(ddi_id(), lane) |
| .ReadFrom(mmio_space_); |
| zxlogf(TRACE, |
| "DDI %d Lane %d PORT_TX_DW8: %08x, output DCC clock: select %d divider select %d, " |
| "output DCC code: override %s %d limits %d - %d, output DCC fuse %s, " |
| "input DCC code: %d thermal %d", |
| ddi_id(), static_cast<int>(lane), transmitter_dcc.reg_value(), |
| transmitter_dcc.output_duty_cycle_correction_clock_select(), |
| static_cast<int>(transmitter_dcc.output_duty_cycle_correction_clock_divider_select()), |
| transmitter_dcc.output_duty_cycle_correction_code_override_valid() ? "valid" : "invalid", |
| transmitter_dcc.output_duty_cycle_correction_code_override(), |
| transmitter_dcc.output_duty_cycle_correction_lower_limit(), |
| transmitter_dcc.output_duty_cycle_correction_upper_limit(), |
| transmitter_dcc.output_duty_cycle_correction_fuse_enabled() ? "enabled" : "disabled", |
| transmitter_dcc.input_duty_cycle_correction_code(), |
| (transmitter_dcc.input_duty_cycle_correction_thermal_bits43() << 2) | |
| transmitter_dcc.input_duty_cycle_correction_thermal_bits20()); |
| |
| auto physical_coding1 = |
| registers::PortPhysicalCoding1::GetForDdiLane(ddi_id(), lane).ReadFrom(mmio_space_); |
| zxlogf(TRACE, |
| "DDI %d Lane %d PORT_PCS_DW1: %08x, power-gated %s, DCC schedule %d, " |
| "DCC calibration: force %s bypass %s on wake %s, clock request %d, " |
| "commmon keeper: %s / %s while power-gated / bias control %d, latency optimization %d, " |
| "soft lane reset: %s %s, transmitter fifo reset override: %s %s, " |
| "transmiter de-emphasis %d, TBC as symbol clock %s", |
| ddi_id(), static_cast<int>(lane), physical_coding1.reg_value(), |
| physical_coding1.power_gate_powered_down() ? "yes" : "no", |
| static_cast<int>(physical_coding1.duty_cycle_correction_schedule_select()), |
| physical_coding1.force_transmitter_duty_cycle_correction_calibration() ? "yes" : "no", |
| physical_coding1.duty_cycle_correction_calibration_bypassed() ? "enabled" : "disabled", |
| physical_coding1.duty_cycle_correction_calibration_on_wake() ? "yes" : "no", |
| physical_coding1.clock_request(), |
| physical_coding1.common_mode_keeper_enabled() ? "enabled" : "disabled", |
| physical_coding1.common_mode_keeper_enabled_while_power_gated() ? "enabled" : "disabled", |
| physical_coding1.common_mode_keeper_bias_control(), |
| physical_coding1.latency_optimization_value(), |
| physical_coding1.soft_lane_reset() ? "on" : "off", |
| physical_coding1.soft_lane_reset_valid() ? "valid" : "invalid", |
| physical_coding1.transmitter_fifo_reset_main_override() ? "on" : "off", |
| physical_coding1.transmitter_fifo_reset_main_override_valid() ? "valid" : "invalid", |
| physical_coding1.transmitter_deemphasis_value(), |
| physical_coding1.use_transmitter_buffer_clock_as_symbol_clock() ? "yes" : "no"); |
| } |
| |
| auto phy_misc = registers::PhyMisc::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| zxlogf(TRACE, "DDI %d PHY_MISC %08x, DE to IO: %x, IO to DE: %x, Comp power down: %s", ddi_id(), |
| phy_misc.reg_value(), phy_misc.display_engine_to_io(), phy_misc.io_to_display_engine(), |
| phy_misc.compensation_resistors_powered_down() ? "enabled" : "disabled"); |
| |
| auto compensation_source = |
| registers::PortCompensationSource::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| zxlogf(TRACE, |
| "DDI %d PORT_COMP_DW8 %08x, internal reference generation %s, periodic compensation %s", |
| ddi_id(), compensation_source.reg_value(), |
| compensation_source.generate_internal_references() ? "enabled" : "disabled", |
| compensation_source.periodic_current_compensation_disabled() ? "disabled" : "enabled"); |
| |
| auto compensation_initialized = |
| registers::PortCompensation0::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| zxlogf(TRACE, "DDI %d PORT_COMP_DW0: %08x PORT_COMP_DW3: %08x ", ddi_id(), |
| compensation_initialized.reg_value(), procmon_status.reg_value()); |
| if (compensation_initialized.initialized()) { |
| // The PRMs advise that we consider the PHY initialized if this bit is set, |
| // and skip the entire initialize process. A more robust approach would be |
| // to reset (de-initialize, initialize) the PHY if its current configuration |
| // doesn't match what we expect. |
| zxlogf(TRACE, "DDI %d PHY already initialized. Assuming everything is correct.", ddi_id()); |
| return true; |
| } |
| |
| for (registers::PortLane lane : kAllLanes) { |
| auto transmitter_dcc = |
| registers::PortTransmitterDutyCycleCorrection::GetForDdiLane(ddi_id(), lane) |
| .ReadFrom(mmio_space_); |
| transmitter_dcc.set_output_duty_cycle_correction_clock_select(1) |
| .set_output_duty_cycle_correction_clock_divider_select( |
| registers::PortTransmitterDutyCycleCorrection::ClockDividerSelect::k2) |
| .WriteTo(mmio_space_); |
| |
| auto physical_coding1 = |
| registers::PortPhysicalCoding1::GetForDdiLane(ddi_id(), lane).ReadFrom(mmio_space_); |
| physical_coding1 |
| .set_duty_cycle_correction_schedule_select( |
| registers::PortPhysicalCoding1::DutyCycleCorrectionScheduleSelect::kContinuously) |
| .WriteTo(mmio_space_); |
| } |
| |
| phy_misc.set_compensation_resistors_powered_down(false).WriteTo(mmio_space_); |
| |
| TigerLakeProcessCompensationConfig process_compensation = ProcessCompensationConfigFor( |
| procmon_status.process_select(), procmon_status.voltage_select()); |
| if (process_compensation.IsEmpty()) { |
| return false; |
| } |
| WriteTigerLakeProcessCompensationConfig(process_compensation, ddi_id(), mmio_space_); |
| |
| bool is_compensation_source = (ddi_id() == DdiId::DDI_A); |
| compensation_source.set_generate_internal_references(is_compensation_source).WriteTo(mmio_space_); |
| |
| compensation_initialized.set_initialized(true).WriteTo(mmio_space_); |
| |
| common_lane5.set_common_lane_power_down_enabled(true).WriteTo(mmio_space_); |
| return true; |
| } |
| |
| bool ComboDdiTigerLake::Deinitialize() { |
| // This implements the section "Digital Display Interface" > "Combo PHY |
| // Un-Initialization Sequence" in display engine PRMs. |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev2.0 page 392 |
| // DG1: IHD-OS-DG1-Vol 12-2.21 page 338 |
| // Ice Lake: IHD-OS-ICLLP-Vol 12-1.22-Rev2.0 page 335 |
| |
| // TODO(https://fxbug.dev/42065111): Implement the compensation source dependency |
| // between DDI A and DDIs B-C. |
| |
| auto phy_misc = registers::PhyMisc::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| phy_misc.set_compensation_resistors_powered_down(true).WriteTo(mmio_space_); |
| |
| auto port_compensation0 = registers::PortCompensation0::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| port_compensation0.set_initialized(false).WriteTo(mmio_space_); |
| |
| return true; |
| } |
| |
| DdiPhysicalLayer::PhysicalLayerInfo ComboDdiTigerLake::GetPhysicalLayerInfo() const { |
| return { |
| .ddi_type = DdiPhysicalLayer::DdiType::kCombo, |
| .connection_type = DdiPhysicalLayer::ConnectionType::kBuiltIn, |
| .max_allowed_dp_lane_count = 4u, |
| }; |
| } |
| |
| TypeCDdiTigerLake::TypeCDdiTigerLake(DdiId ddi_id, Power* power, fdf::MmioBuffer* mmio_space, |
| bool is_static_port) |
| : DdiPhysicalLayer(ddi_id), |
| power_(power), |
| mmio_space_(mmio_space), |
| initialization_phase_(InitializationPhase::kUninitialized), |
| is_static_port_(is_static_port), |
| physical_layer_info_(DefaultPhysicalLayerInfo()) { |
| ZX_ASSERT(power_); |
| ZX_ASSERT(mmio_space_); |
| ZX_ASSERT(ddi_id >= DdiId::DDI_TC_1); |
| ZX_ASSERT(ddi_id <= DdiId::DDI_TC_6); |
| } |
| |
| TypeCDdiTigerLake::~TypeCDdiTigerLake() { |
| if (initialization_phase_ != InitializationPhase::kUninitialized) { |
| zxlogf(WARNING, "DDI %d: not fully disabled on port teardown", ddi_id()); |
| } |
| } |
| |
| bool TypeCDdiTigerLake::IsEnabled() const { |
| return initialization_phase_ == InitializationPhase::kInitialized; |
| } |
| |
| bool TypeCDdiTigerLake::IsHealthy() const { |
| // All the other states indicate that the DDI PHY is not fully initialized |
| // or not fully deinitialized and thus in a limbo state. |
| return initialization_phase_ == InitializationPhase::kInitialized || |
| initialization_phase_ == InitializationPhase::kUninitialized; |
| } |
| |
| DdiPhysicalLayer::PhysicalLayerInfo TypeCDdiTigerLake::ReadPhysicalLayerInfo() const { |
| PhysicalLayerInfo physical_layer_info = { |
| .ddi_type = DdiType::kTypeC, |
| }; |
| |
| auto dp_sp = registers::DynamicFlexIoScratchPad::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| auto type_c_live_state = dp_sp.type_c_live_state(ddi_id()); |
| switch (type_c_live_state) { |
| using TypeCLiveState = registers::DynamicFlexIoScratchPad::TypeCLiveState; |
| case TypeCLiveState::kNoHotplugDisplay: |
| if (is_static_port_) { |
| physical_layer_info.connection_type = ConnectionType::kBuiltIn; |
| physical_layer_info.max_allowed_dp_lane_count = 4u; |
| } else { |
| physical_layer_info.connection_type = ConnectionType::kNone; |
| physical_layer_info.max_allowed_dp_lane_count = 0u; |
| } |
| break; |
| case TypeCLiveState::kTypeCHotplugDisplay: |
| physical_layer_info.connection_type = ConnectionType::kTypeCDisplayPortAltMode; |
| physical_layer_info.max_allowed_dp_lane_count = [&]() { |
| size_t count = dp_sp.display_port_assigned_tx_lane_count(ddi_id()); |
| ZX_DEBUG_ASSERT_MSG(count <= std::numeric_limits<uint8_t>::max(), "%lu overflows uint8_t", |
| count); |
| return static_cast<uint8_t>(count); |
| }(); |
| break; |
| case TypeCLiveState::kThunderboltHotplugDisplay: |
| physical_layer_info.connection_type = ConnectionType::kTypeCThunderbolt; |
| physical_layer_info.max_allowed_dp_lane_count = 4u; |
| break; |
| default: |
| ZX_ASSERT_MSG(false, "DDI %d: unsupported type C live state (0x%x)", ddi_id(), |
| static_cast<unsigned int>(type_c_live_state)); |
| } |
| |
| return physical_layer_info; |
| } |
| |
| bool TypeCDdiTigerLake::AdvanceEnableFsm() { |
| switch (initialization_phase_) { |
| case InitializationPhase::kUninitialized: |
| initialization_phase_ = InitializationPhase::kTypeCColdBlocked; |
| return BlockTypeCColdPowerState(); |
| case InitializationPhase::kTypeCColdBlocked: |
| initialization_phase_ = InitializationPhase::kSafeModeSet; |
| if (!SetPhySafeModeDisabled(/*target_disabled=*/true)) { |
| return false; |
| } |
| physical_layer_info_ = ReadPhysicalLayerInfo(); |
| return physical_layer_info_.connection_type != ConnectionType::kNone; |
| case InitializationPhase::kSafeModeSet: |
| initialization_phase_ = InitializationPhase::kAuxPoweredOn; |
| return SetAuxIoPower(/*target_enabled=*/true); |
| case InitializationPhase::kAuxPoweredOn: |
| initialization_phase_ = InitializationPhase::kInitialized; |
| return true; |
| case InitializationPhase::kInitialized: |
| return false; |
| } |
| } |
| |
| bool TypeCDdiTigerLake::AdvanceDisableFsm() { |
| switch (initialization_phase_) { |
| case InitializationPhase::kUninitialized: |
| return false; |
| case InitializationPhase::kTypeCColdBlocked: |
| if (UnblockTypeCColdPowerState()) { |
| physical_layer_info_ = DefaultPhysicalLayerInfo(); |
| initialization_phase_ = InitializationPhase::kUninitialized; |
| return true; |
| } |
| return false; |
| case InitializationPhase::kSafeModeSet: |
| if (SetPhySafeModeDisabled(/*target_disabled=*/false)) { |
| initialization_phase_ = InitializationPhase::kTypeCColdBlocked; |
| return true; |
| } |
| return false; |
| case InitializationPhase::kAuxPoweredOn: |
| if (SetAuxIoPower(/*target_enabled=*/false)) { |
| initialization_phase_ = InitializationPhase::kSafeModeSet; |
| return true; |
| } |
| return false; |
| case InitializationPhase::kInitialized: |
| initialization_phase_ = InitializationPhase::kAuxPoweredOn; |
| return true; |
| } |
| } |
| |
| bool TypeCDdiTigerLake::Enable() { |
| ZX_ASSERT(IsHealthy()); |
| |
| // `IsHealthy()` returns true entails that the device is either in |
| // `kInitialized` state where it needs to do nothing because of the function's |
| // idempotency, or in `kUninitialized` state where it needs to start the |
| // finite state machine. |
| if (initialization_phase_ == InitializationPhase::kInitialized) { |
| return true; |
| } |
| ZX_DEBUG_ASSERT(initialization_phase_ == InitializationPhase::kUninitialized); |
| |
| while (AdvanceEnableFsm()) { |
| } |
| if (initialization_phase_ == InitializationPhase::kInitialized) { |
| zxlogf(TRACE, "DDI %d: Enabled. New physical layer info: %s", ddi_id(), |
| physical_layer_info_.DebugString().c_str()); |
| return true; |
| } |
| while (AdvanceDisableFsm()) { |
| } |
| return false; |
| } |
| |
| bool TypeCDdiTigerLake::Disable() { |
| switch (initialization_phase_) { |
| case InitializationPhase::kUninitialized: |
| // Do nothing because of the function's idempotency. |
| return true; |
| case InitializationPhase::kInitialized: |
| // Start the finite state machine of disable process. |
| while (AdvanceDisableFsm()) { |
| } |
| if (initialization_phase_ == InitializationPhase::kUninitialized) { |
| zxlogf(TRACE, "DDI %d: Disabled successfully.", ddi_id()); |
| return true; |
| } |
| [[fallthrough]]; |
| default: |
| ZX_ASSERT(!IsHealthy()); |
| zxlogf(ERROR, "DDI %d: Failed to disable.", ddi_id()); |
| return false; |
| } |
| } |
| |
| bool TypeCDdiTigerLake::SetAuxIoPower(bool target_enabled) const { |
| power_->SetAuxIoPowerState(ddi_id(), /* enable */ target_enabled); |
| |
| if (target_enabled) { |
| if (!PollUntil([&] { return power_->GetAuxIoPowerState(ddi_id()); }, zx::usec(1), 1500)) { |
| zxlogf(ERROR, "DDI %d: failed to enable AUX power for ddi", ddi_id()); |
| return false; |
| } |
| |
| const bool is_thunderbolt = |
| physical_layer_info_.connection_type == DdiPhysicalLayer::ConnectionType::kTypeCThunderbolt; |
| if (!is_thunderbolt) { |
| // For every Type-C port (static and DP Alternate but not thunderbolt), |
| // the driver need to wait for the microcontroller health bit on |
| // DKL_CMN_UC_DW27 register after enabling AUX power. |
| // |
| // TODO(https://fxbug.dev/42182480): Currently Thunderbolt is not supported, so we |
| // always check health bit of the IO subsystem microcontroller. |
| // |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0, Page 417, "Type-C PHY |
| // Microcontroller health" |
| if (!PollUntil( |
| [&] { |
| return registers::DekelCommonConfigMicroControllerDword27::GetForDdi(ddi_id()) |
| .ReadFrom(mmio_space_) |
| .microcontroller_firmware_is_ready(); |
| }, |
| zx::usec(1), 10)) { |
| zxlogf(ERROR, "DDI %d: microcontroller health bit is not set", ddi_id()); |
| return false; |
| } |
| } |
| |
| auto ddi_aux_ctl = registers::DdiAuxControl::GetForTigerLakeDdi(ddi_id()).ReadFrom(mmio_space_); |
| ddi_aux_ctl.set_use_thunderbolt(is_thunderbolt); |
| ddi_aux_ctl.WriteTo(mmio_space_); |
| |
| zxlogf(TRACE, "DDI %d: AUX IO power enabled", ddi_id()); |
| } else { |
| zx::nanosleep(zx::deadline_after(zx::usec(10))); |
| zxlogf(TRACE, "DDI %d: AUX IO power %sdisabled", ddi_id(), |
| power_->GetAuxIoPowerState(ddi_id()) ? "not " : ""); |
| } |
| |
| return true; |
| } |
| |
| bool TypeCDdiTigerLake::SetPhySafeModeDisabled(bool target_disabled) const { |
| if (target_disabled && !registers::DynamicFlexIoDisplayPortPhyModeStatus::GetForDdi(ddi_id()) |
| .ReadFrom(mmio_space_) |
| .phy_is_ready_for_ddi(ddi_id())) { |
| zxlogf(ERROR, "DDI %d: lane not in DP mode", ddi_id()); |
| return false; |
| } |
| |
| auto dp_csss = |
| registers::DynamicFlexIoDisplayPortControllerSafeStateSettings::GetForDdi(ddi_id()).ReadFrom( |
| mmio_space_); |
| dp_csss.set_safe_mode_disabled_for_ddi(ddi_id(), /*disabled=*/target_disabled); |
| dp_csss.WriteTo(mmio_space_); |
| dp_csss.ReadFrom(mmio_space_); |
| zxlogf(TRACE, "DDI %d: %s DP safe mode", ddi_id(), target_disabled ? "disabled" : "enabled"); |
| return true; |
| } |
| |
| bool TypeCDdiTigerLake::BlockTypeCColdPowerState() { |
| // TODO(https://fxbug.dev/42062380): TCCOLD (Type C cold power state) blocking should |
| // be decided at the display engine level. We may have already blocked TCCOLD |
| // while bringing up another Type C DDI. |
| zxlogf(TRACE, "Asking PCU firmware to block Type C cold power state"); |
| PowerController power_controller(mmio_space_); |
| const zx::result<> power_status = power_controller.SetDisplayTypeCColdBlockingTigerLake( |
| /*blocked=*/true, PowerController::RetryBehavior::kRetryUntilStateChanges); |
| switch (power_status.status_value()) { |
| case ZX_OK: |
| zxlogf(TRACE, "PCU firmware blocked Type C cold power state"); |
| return true; |
| default: |
| zxlogf(ERROR, "Type C ports unusable. PCU firmware didn't block Type C cold power state: %s", |
| power_status.status_string()); |
| return false; |
| } |
| } |
| |
| bool TypeCDdiTigerLake::UnblockTypeCColdPowerState() { |
| // TODO(https://fxbug.dev/42062380): TCCOLD (Type C cold power state) blocking should |
| // be decided at the display engine level. We may have already blocked TCCOLD |
| // while bringing up another Type C DDI. |
| zxlogf(TRACE, "Asking PCU firmware to unblock Type C cold power state"); |
| PowerController power_controller(mmio_space_); |
| const zx::result<> power_status = power_controller.SetDisplayTypeCColdBlockingTigerLake( |
| /*blocked=*/false, PowerController::RetryBehavior::kNoRetry); |
| switch (power_status.status_value()) { |
| case ZX_OK: |
| zxlogf(TRACE, "PCU firmware unblocked and entered Type C cold power state"); |
| return true; |
| case ZX_ERR_IO_REFUSED: |
| zxlogf(INFO, |
| "PCU firmware did not enter Type C cold power state. " |
| "Type C ports in use elsewhere."); |
| return true; |
| default: |
| zxlogf(ERROR, |
| "PCU firmware failed to unblock Type C cold power state. " |
| "Type C ports unusable."); |
| return false; |
| } |
| } |
| |
| } // namespace i915 |