| // 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/dpll.h" |
| |
| #include <lib/ddk/debug.h> |
| #include <lib/zx/time.h> |
| #include <zircon/assert.h> |
| |
| #include <memory> |
| #include <optional> |
| #include <tuple> |
| |
| #include "src/graphics/display/drivers/intel-i915/dpll-config.h" |
| #include "src/graphics/display/drivers/intel-i915/hardware-common.h" |
| #include "src/graphics/display/drivers/intel-i915/poll-until.h" |
| #include "src/graphics/display/drivers/intel-i915/registers-ddi.h" |
| #include "src/graphics/display/drivers/intel-i915/registers-dpll.h" |
| #include "src/graphics/display/drivers/intel-i915/registers-typec.h" |
| #include "src/graphics/display/drivers/intel-i915/registers.h" |
| |
| namespace i915 { |
| |
| bool DdiPllConfig::IsValid() const { |
| if (ddi_clock_khz <= 0) { |
| return false; |
| } |
| if (!admits_display_port && !admits_hdmi) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool operator==(const DdiPllConfig& lhs, const DdiPllConfig& rhs) noexcept { |
| return std::tie(lhs.ddi_clock_khz, lhs.spread_spectrum_clocking, lhs.admits_display_port, |
| lhs.admits_hdmi) == std::tie(rhs.ddi_clock_khz, rhs.spread_spectrum_clocking, |
| rhs.admits_display_port, rhs.admits_hdmi); |
| } |
| |
| bool operator!=(const DdiPllConfig& lhs, const DdiPllConfig& rhs) noexcept { return !(lhs == rhs); } |
| |
| namespace { |
| |
| std::string GetDpllName(PllId pll_id) { |
| switch (pll_id) { |
| case PllId::DPLL_0: |
| return "DPLL 0"; |
| case PllId::DPLL_1: |
| return "DPLL 1"; |
| case PllId::DPLL_2: |
| return "DPLL 2"; |
| case PllId::DPLL_3: |
| return "DPLL 3"; |
| case PllId::DPLL_TC_1: |
| return "DPLL TC 1"; |
| case PllId::DPLL_TC_2: |
| return "DPLL TC 2"; |
| case PllId::DPLL_TC_3: |
| return "DPLL TC 3"; |
| case PllId::DPLL_TC_4: |
| return "DPLL TC 4"; |
| case PllId::DPLL_TC_5: |
| return "DPLL TC 5"; |
| case PllId::DPLL_TC_6: |
| return "DPLL TC 6"; |
| default: |
| return "DPLL Invalid"; |
| } |
| } |
| |
| } // namespace |
| |
| DisplayPll::DisplayPll(PllId pll_id) : pll_id_(pll_id), name_(GetDpllName(pll_id)) {} |
| |
| DisplayPll* DisplayPllManager::SetDdiPllConfig(DdiId ddi_id, bool is_edp, |
| const DdiPllConfig& desired_config) { |
| zxlogf(TRACE, "Configuring PLL for DDI %d - SSC %s, DDI clock %d kHz, DisplayPort %s, HDMI %s", |
| ddi_id, desired_config.spread_spectrum_clocking ? "yes" : "no", |
| desired_config.ddi_clock_khz, desired_config.admits_display_port ? "yes" : "no", |
| desired_config.admits_hdmi ? "yes" : "no"); |
| |
| // Asserting after zxlogf() facilitates debugging, because the invalid |
| // configuration will be captured in the log. |
| ZX_ASSERT(desired_config.IsValid()); |
| |
| const auto ddi_to_dpll_it = ddi_to_dpll_.find(ddi_id); |
| if (ddi_to_dpll_it != ddi_to_dpll_.end()) { |
| DisplayPll* pll = ddi_to_dpll_it->second; |
| if (pll->config() == desired_config) { |
| zxlogf(WARNING, "SetDdiPllConfig() will unnecessarily reset the PLL for DDI %d", ddi_id); |
| } |
| ResetDdiPll(ddi_id); |
| } |
| |
| DisplayPll* best_dpll = FindPllFor(ddi_id, is_edp, desired_config); |
| if (!best_dpll) { |
| zxlogf(ERROR, "Failed to allocate DPLL to DDI %d - %d kHz %s DisplayPort: %s HDMI: %s", ddi_id, |
| desired_config.ddi_clock_khz, desired_config.spread_spectrum_clocking ? "SSC" : "no SSC", |
| desired_config.admits_display_port ? "yes" : "no", |
| desired_config.admits_hdmi ? "yes" : "no"); |
| return nullptr; |
| } |
| zxlogf(DEBUG, "Assigning DPLL %s to DDI %d - %d kHz %s DisplayPort: %s HDMI: %s", |
| best_dpll->name().c_str(), ddi_id, desired_config.ddi_clock_khz, |
| desired_config.spread_spectrum_clocking ? "SSC" : "no SSC", |
| desired_config.admits_display_port ? "yes" : "no", |
| desired_config.admits_hdmi ? "yes" : "no"); |
| |
| if (ref_count_[best_dpll] > 0 || best_dpll->Enable(desired_config)) { |
| if (!SetDdiClockSource(ddi_id, best_dpll->pll_id())) { |
| zxlogf(ERROR, "Failed to map DDI %d to DPLL (%s)", ddi_id, best_dpll->name().c_str()); |
| return nullptr; |
| } |
| ref_count_[best_dpll]++; |
| ddi_to_dpll_[ddi_id] = best_dpll; |
| return best_dpll; |
| } |
| return nullptr; |
| } |
| |
| bool DisplayPll::Enable(const DdiPllConfig& pll_config) { |
| zxlogf(TRACE, "Configuring PLL %d: SSC %s, DDI clock %d kHz, DisplayPort %s, HDMI %s", pll_id(), |
| pll_config.spread_spectrum_clocking ? "yes" : "no", pll_config.ddi_clock_khz, |
| pll_config.admits_display_port ? "yes" : "no", pll_config.admits_hdmi ? "yes" : "no"); |
| |
| // Asserting after zxlogf() facilitates debugging, because the invalid |
| // configuration will be captured in the log. |
| ZX_ASSERT(pll_config.IsValid()); |
| |
| if (!config_.IsEmpty()) { |
| zxlogf(ERROR, "Enable(): PLL %s already enabled!", name().c_str()); |
| return false; |
| } |
| |
| const bool success = DoEnable(pll_config); |
| if (success) { |
| config_ = pll_config; |
| zxlogf(TRACE, "Enabled DPLL %d: SSC %s, DDI clock %d kHz, DisplayPort %s, HDMI %s", pll_id(), |
| pll_config.spread_spectrum_clocking ? "yes" : "no", pll_config.ddi_clock_khz, |
| pll_config.admits_display_port ? "yes" : "no", pll_config.admits_hdmi ? "yes" : "no"); |
| |
| } else { |
| zxlogf(ERROR, "Failed to enable DPLL %d: SSC %s, DDI clock %d kHz, DisplayPort %s, HDMI %s", |
| pll_id(), pll_config.spread_spectrum_clocking ? "yes" : "no", pll_config.ddi_clock_khz, |
| pll_config.admits_display_port ? "yes" : "no", pll_config.admits_hdmi ? "yes" : "no"); |
| } |
| return success; |
| } |
| |
| bool DisplayPll::Disable() { |
| zxlogf(TRACE, "Disabling PLL %d", pll_id()); |
| if (config_.IsEmpty()) { |
| zxlogf(INFO, "DoDisable(): PLL %s already disabled", name().c_str()); |
| return true; |
| } |
| const bool success = DoDisable(); |
| |
| if (success) { |
| config_ = {}; |
| zxlogf(TRACE, "Disabled PLL %d", pll_id()); |
| } else { |
| zxlogf(ERROR, "Failed to disable PLL %d", pll_id()); |
| } |
| return success; |
| } |
| |
| bool DisplayPllManager::ResetDdiPll(DdiId ddi_id) { |
| if (ddi_to_dpll_.find(ddi_id) == ddi_to_dpll_.end()) { |
| return true; |
| } |
| |
| DisplayPll* dpll = ddi_to_dpll_[ddi_id]; |
| if (!ResetDdiClockSource(ddi_id)) { |
| zxlogf(ERROR, "Failed to unmap DPLL (%s) for DDI %d", dpll->name().c_str(), ddi_id); |
| return false; |
| } |
| |
| ZX_DEBUG_ASSERT(ref_count_[dpll] > 0); |
| if (--ref_count_[dpll] == 0) { |
| ddi_to_dpll_.erase(ddi_id); |
| return dpll->Disable(); |
| } |
| return true; |
| } |
| |
| bool DisplayPllManager::DdiPllMatchesConfig(DdiId ddi_id, const DdiPllConfig& desired_config) { |
| const auto ddi_to_dpll_it = ddi_to_dpll_.find(ddi_id); |
| if (ddi_to_dpll_it == ddi_to_dpll_.end()) { |
| return true; |
| } |
| return ddi_to_dpll_it->second->config() != desired_config; |
| } |
| |
| DpllSkylake::DpllSkylake(fdf::MmioBuffer* mmio_space, PllId pll_id) |
| : DisplayPll(pll_id), mmio_space_(mmio_space) {} |
| |
| bool DpllSkylake::DoEnable(const DdiPllConfig& pll_config) { |
| // This implements the common steps in the sections "DisplayPort Programming" |
| // > "DisplayPort PLL Enable Sequence" and "HDMI and DVI PLL Enable |
| // Sequence" in the display engine PRMs. |
| // |
| // The specifics of each section are implemented in ConfigureForHdmi() and |
| // ConfigureForDisplayPort(), which contain full references to the PRMs. |
| |
| bool configure_success; |
| if (pll_config.admits_hdmi) { |
| configure_success = ConfigureForHdmi(pll_config); |
| } else { |
| ZX_DEBUG_ASSERT(pll_config.admits_display_port); |
| configure_success = ConfigureForDisplayPort(pll_config); |
| } |
| if (!configure_success) { |
| return false; |
| } |
| |
| auto dpll_enable = registers::PllEnable::GetForSkylakeDpll(pll_id()).ReadFrom(mmio_space_); |
| dpll_enable.set_pll_enabled(true).WriteTo(mmio_space_); |
| if (!PollUntil( |
| [&] { |
| return registers::DisplayPllStatus::Get().ReadFrom(mmio_space_).pll_locked(pll_id()); |
| }, |
| zx::msec(1), 5)) { |
| zxlogf(ERROR, "Skylake DPLL %d failed to lock after 5ms!", pll_id()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool DpllSkylake::ConfigureForDisplayPort(const DdiPllConfig& pll_config) { |
| // This implements the "DisplayPort Programming" > "DisplayPort PLL Enable |
| // Sequence" section in the display engine PRMs. |
| // |
| // Kaby Lake: IHD-OS-KBL-Vol 12-1.17 page 133 |
| // Skylake: IHD-OS-SKL-Vol 12-05.16 page 130 |
| |
| const int32_t display_port_link_rate_mhz = pll_config.ddi_clock_khz / 500; |
| zxlogf(TRACE, "Configuring Skylake DPLL %d: DisplayPort, link rate %d Mbps", pll_id(), |
| display_port_link_rate_mhz); |
| |
| auto dpll_control1 = registers::DisplayPllControl1::Get().ReadFrom(mmio_space_); |
| const int16_t ddi_clock_mhz = static_cast<int16_t>(pll_config.ddi_clock_khz / 1'000); |
| dpll_control1.set_pll_uses_hdmi_configuration_mode(pll_id(), false) |
| .set_pll_spread_spectrum_clocking_enabled(pll_id(), false) |
| .set_pll_display_port_ddi_frequency_mhz(pll_id(), ddi_clock_mhz) |
| .set_pll_programming_enabled(pll_id(), true) |
| .WriteTo(mmio_space_); |
| |
| // The PRM instructs us to read back the configuration register in order to |
| // ensure that the writes completed. This must happen before enabling the PLL. |
| dpll_control1.ReadFrom(mmio_space_); |
| |
| return true; |
| } |
| |
| bool DpllSkylake::ConfigureForHdmi(const DdiPllConfig& pll_config) { |
| ZX_ASSERT_MSG(pll_id() != PllId::DPLL_0, "DPLL 0 only supports DisplayPort DDIs"); |
| |
| // This implements the "HDMI and DVI Programming" > "HDMI and DVI PLL Enable |
| // Sequence" section in the display engine PRMs. |
| // |
| // Kaby Lake: IHD-OS-KBL-Vol 12-1.17 page 134 |
| // Skylake: IHD-OS-SKL-Vol 12-05.16 page 131 |
| |
| const DpllOscillatorConfig dco_config = |
| CreateDpllOscillatorConfigKabyLake(pll_config.ddi_clock_khz); |
| if (dco_config.frequency_divider == 0) { |
| return false; |
| } |
| |
| const DpllFrequencyDividerConfig divider_config = |
| CreateDpllFrequencyDividerConfigKabyLake(dco_config.frequency_divider); |
| |
| zxlogf(TRACE, "Configuring DPLL %d: HDMI DCO frequency=%d dividers P*Q*K=%u*%u*%u Center=%u Mhz", |
| pll_id(), dco_config.frequency_khz, divider_config.p0_p_divider, |
| divider_config.p1_q_divider, divider_config.p2_k_divider, dco_config.center_frequency_khz); |
| |
| auto dpll_control1 = registers::DisplayPllControl1::Get().ReadFrom(mmio_space_); |
| dpll_control1.set_pll_uses_hdmi_configuration_mode(pll_id(), true) |
| .set_pll_spread_spectrum_clocking_enabled(pll_id(), false) |
| .set_pll_programming_enabled(pll_id(), true) |
| .WriteTo(mmio_space_); |
| |
| auto dpll_config1 = registers::DisplayPllDcoFrequencyKabyLake::GetForDpll(pll_id()).FromValue(0); |
| dpll_config1.set_frequency_programming_enabled(true) |
| .set_dco_frequency_khz(dco_config.frequency_khz) |
| .WriteTo(mmio_space_); |
| |
| auto dpll_config2 = registers::DisplayPllDcoDividersKabyLake::GetForDpll(pll_id()).FromValue(0); |
| dpll_config2.set_q_p1_divider(divider_config.p1_q_divider) |
| .set_k_p2_divider(divider_config.p2_k_divider) |
| .set_p_p0_divider(divider_config.p0_p_divider) |
| .set_center_frequency_mhz(static_cast<int16_t>(dco_config.center_frequency_khz / 1'000)) |
| .WriteTo(mmio_space_); |
| |
| // The PRM instructs us to read back the configuration registers in order to |
| // ensure that the writes completed. This must happen before enabling the PLL. |
| dpll_control1.ReadFrom(mmio_space_); |
| dpll_config1.ReadFrom(mmio_space_); |
| dpll_config2.ReadFrom(mmio_space_); |
| return true; |
| } |
| |
| bool DpllSkylake::DoDisable() { |
| // We must not disable DPLL0 here, because it also drives the core display |
| // clocks (CDCLK, CD2XCLK). DPLL0 must only get disabled during display engine |
| // un-initialization. |
| if (pll_id() != PllId::DPLL_0) { |
| auto dpll_enable = registers::PllEnable::GetForSkylakeDpll(pll_id()).ReadFrom(mmio_space_); |
| dpll_enable.set_pll_enabled(false).WriteTo(mmio_space_); |
| } |
| return true; |
| } |
| |
| DpllManagerSkylake::DpllManagerSkylake(fdf::MmioBuffer* mmio_space) : mmio_space_(mmio_space) { |
| for (const auto dpll : PllIds<registers::Platform::kSkylake>()) { |
| plls_[dpll] = std::make_unique<DpllSkylake>(mmio_space, dpll); |
| ref_count_[plls_[dpll].get()] = 0; |
| } |
| } |
| |
| bool DpllManagerSkylake::SetDdiClockSource(DdiId ddi_id, PllId pll_id) { |
| auto dpll_ddi_map = registers::DisplayPllDdiMapKabyLake::Get().ReadFrom(mmio_space_); |
| dpll_ddi_map.set_ddi_clock_programming_enabled(ddi_id, true) |
| .set_ddi_clock_disabled(ddi_id, false) |
| .set_ddi_clock_display_pll(ddi_id, pll_id) |
| .WriteTo(mmio_space_); |
| |
| return true; |
| } |
| |
| bool DpllManagerSkylake::ResetDdiClockSource(DdiId ddi_id) { |
| auto dpll_ddi_map = registers::DisplayPllDdiMapKabyLake::Get().ReadFrom(mmio_space_); |
| dpll_ddi_map.set_ddi_clock_disabled(ddi_id, true).WriteTo(mmio_space_); |
| |
| return true; |
| } |
| |
| DdiPllConfig DpllManagerSkylake::LoadState(DdiId ddi_id) { |
| auto dpll_ddi_map = registers::DisplayPllDdiMapKabyLake::Get().ReadFrom(mmio_space_); |
| if (dpll_ddi_map.ddi_clock_disabled(ddi_id)) { |
| zxlogf(TRACE, "Loaded DDI %d DPLL state: DDI clock disabled", ddi_id); |
| return DdiPllConfig{}; |
| } |
| |
| const PllId pll_id = dpll_ddi_map.ddi_clock_display_pll(ddi_id); |
| auto dpll_enable = registers::PllEnable::GetForSkylakeDpll(pll_id).ReadFrom(mmio_space_); |
| if (!dpll_enable.pll_enabled()) { |
| zxlogf(TRACE, "Loaded DDI %d DPLL %d state: DPLL disabled", ddi_id, pll_id); |
| return DdiPllConfig{}; |
| } |
| |
| // Remove stale mappings first. |
| if (ddi_to_dpll_.find(ddi_id) != ddi_to_dpll_.end()) { |
| ZX_DEBUG_ASSERT(ref_count_.find(ddi_to_dpll_[ddi_id]) != ref_count_.end()); |
| ZX_DEBUG_ASSERT(ref_count_.at(ddi_to_dpll_[ddi_id]) > 0); |
| --ref_count_[ddi_to_dpll_[ddi_id]]; |
| ddi_to_dpll_.erase(ddi_id); |
| } |
| |
| ddi_to_dpll_[ddi_id] = plls_[pll_id].get(); |
| ++ref_count_[ddi_to_dpll_[ddi_id]]; |
| |
| auto dpll_control1 = registers::DisplayPllControl1::Get().ReadFrom(mmio_space_); |
| const bool uses_hdmi_mode = dpll_control1.pll_uses_hdmi_configuration_mode(pll_id); |
| if (uses_hdmi_mode) { |
| auto dpll_dco_frequency = |
| registers::DisplayPllDcoFrequencyKabyLake::GetForDpll(pll_id).ReadFrom(mmio_space_); |
| auto dpll_dco_dividers = |
| registers::DisplayPllDcoDividersKabyLake::GetForDpll(pll_id).ReadFrom(mmio_space_); |
| |
| // P (P0) and K (P2) are <= 7, so their product fits in int8_t. |
| const int16_t dco_frequency_divider = |
| (dpll_dco_dividers.p_p0_divider() * dpll_dco_dividers.k_p2_divider()) * |
| int16_t{dpll_dco_dividers.q_p1_divider()}; |
| |
| const int32_t ddi_clock_khz = |
| static_cast<int32_t>(dpll_dco_frequency.dco_frequency_khz() / dco_frequency_divider); |
| |
| zxlogf(TRACE, |
| "Loaded DDI %d DPLL %d state: HDMI no SSC DCO frequency=%d kHz divider P*Q*K=%u*%u*%u " |
| "Center=%u Mhz", |
| ddi_id, pll_id, dpll_dco_frequency.dco_frequency_khz(), dpll_dco_dividers.p_p0_divider(), |
| dpll_dco_dividers.q_p1_divider(), dpll_dco_dividers.k_p2_divider(), |
| dpll_dco_dividers.center_frequency_mhz()); |
| |
| // TODO(fxbug.com/112752): The DpllSkylake instance is not updated to |
| // reflect the state in the registers. |
| return DdiPllConfig{ |
| .ddi_clock_khz = ddi_clock_khz, |
| .spread_spectrum_clocking = false, |
| .admits_display_port = false, |
| .admits_hdmi = true, |
| }; |
| } |
| |
| const int16_t ddi_frequency_mhz = dpll_control1.pll_display_port_ddi_frequency_mhz(pll_id); |
| if (ddi_frequency_mhz == 0) { |
| zxlogf(ERROR, "DPLL %d has invalid DisplayPort DDI clock. DPLL_CTRL1 value: %x", pll_id, |
| dpll_control1.reg_value()); |
| return DdiPllConfig{}; |
| } |
| |
| const int32_t ddi_clock_khz = ddi_frequency_mhz * 1'000; |
| const bool spread_spectrum_clocking = dpll_control1.pll_spread_spectrum_clocking_enabled(pll_id); |
| |
| zxlogf(TRACE, "Loaded DDI %d DPLL %d state: DisplayPort %s %d kHz (link rate %d Mbps)", ddi_id, |
| pll_id, spread_spectrum_clocking ? "SSC" : "no SSC", ddi_clock_khz, ddi_frequency_mhz * 2); |
| |
| // TODO(fxbug.com/112752): The DpllSkylake instance is not updated to reflect |
| // the state in the registers. |
| return DdiPllConfig{ |
| .ddi_clock_khz = ddi_clock_khz, |
| .spread_spectrum_clocking = spread_spectrum_clocking, |
| .admits_display_port = true, |
| .admits_hdmi = false, |
| }; |
| } |
| |
| DisplayPll* DpllManagerSkylake::FindPllFor(DdiId ddi_id, bool is_edp, |
| const DdiPllConfig& desired_config) { |
| if (is_edp) { |
| ZX_DEBUG_ASSERT(desired_config.admits_display_port); |
| |
| DisplayPll* pll0 = plls_[PllId::DPLL_0].get(); |
| if (ref_count_[pll0] == 0 || pll0->config() == desired_config) { |
| return pll0; |
| } |
| } else { |
| DisplayPll* const kCandidates[] = { |
| plls_[PllId::DPLL_1].get(), |
| plls_[PllId::DPLL_3].get(), |
| plls_[PllId::DPLL_2].get(), |
| }; |
| for (DisplayPll* candidate : kCandidates) { |
| if (ref_count_[candidate] == 0 || candidate->config() == desired_config) { |
| return candidate; |
| } |
| } |
| } |
| return nullptr; |
| } |
| |
| namespace { |
| |
| // Timeout for the DisplayPllTigerLake to wait for PLL lock status. |
| // Overridden in tests via |
| // DisplayPllTigerLake::OverrideLockWaitTimeoutUsForTesting(). |
| int g_display_pll_tiger_lake_lock_wait_timeout_us = 500; |
| |
| // Timeout for the DisplayPllTigerLake to wait for PLL power on status. |
| // Overridden in tests via |
| // DisplayPllTigerLake::OverridePowerOnWaitTimeoutMsForTesting(). |
| int g_display_pll_tiger_lake_power_on_wait_timeout_ms = 5; |
| |
| } // namespace |
| |
| DisplayPllTigerLake::DisplayPllTigerLake(fdf::MmioBuffer* mmio_space, PllId pll_id) |
| : DisplayPll(pll_id), mmio_space_(mmio_space) {} |
| |
| bool DisplayPllTigerLake::DoEnable(const DdiPllConfig& pll_config) { |
| // This implements the "DisplayPort Combo PHY Programming" > "DisplayPort Mode |
| // PLL Enable Sequence" section in the display engine PRMs. The "HDMI Mode PLL |
| // Enable Sequence" is documented to be identical, modulo SSC ability. |
| // |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0 pages 177-178 |
| |
| DpllOscillatorConfig dco_config = |
| pll_config.admits_hdmi |
| ? CreateDpllOscillatorConfigForHdmiTigerLake(pll_config.ddi_clock_khz) |
| : CreateDpllOscillatorConfigForDisplayPortTigerLake(pll_config.ddi_clock_khz); |
| if (dco_config.frequency_divider == 0) { |
| return false; |
| } |
| |
| const DpllFrequencyDividerConfig divider_config = |
| CreateDpllFrequencyDividerConfigTigerLake(dco_config.frequency_divider); |
| zxlogf(TRACE, "Configuring PLL %d: DCO frequency=%d dividers P*Q*K=%u*%u*%u Center=%u Mhz", |
| pll_id(), dco_config.frequency_khz, divider_config.p0_p_divider, |
| divider_config.p1_q_divider, divider_config.p2_k_divider, dco_config.center_frequency_khz); |
| |
| auto dpll_enable = registers::PllEnable::GetForTigerLakeDpll(pll_id()).ReadFrom(mmio_space_); |
| dpll_enable.set_power_on_request_tiger_lake(true); |
| dpll_enable.WriteTo(mmio_space_); |
| if (!PollUntil([&] { return dpll_enable.ReadFrom(mmio_space_).powered_on_tiger_lake(); }, |
| zx::msec(1), g_display_pll_tiger_lake_power_on_wait_timeout_ms)) { |
| zxlogf(ERROR, "DPLL %d power up failure!", pll_id()); |
| return false; |
| } |
| |
| auto display_straps = registers::DisplayStraps::Get().ReadFrom(mmio_space_); |
| const int32_t reference_clock_khz = display_straps.reference_frequency_khz_tiger_lake(); |
| |
| auto pll_dco_frequency = |
| registers::DisplayPllDcoFrequencyTigerLake::GetForDpll(pll_id()).FromValue(0); |
| pll_dco_frequency.set_dco_frequency_khz(dco_config.frequency_khz, reference_clock_khz) |
| .WriteTo(mmio_space_); |
| |
| auto pll_dco_dividers = |
| registers::DisplayPllDcoDividersTigerLake::GetForDpll(pll_id()).FromValue(0); |
| pll_dco_dividers.set_q_p1_divider(divider_config.p1_q_divider) |
| .set_k_p2_divider(divider_config.p2_k_divider) |
| .set_p_p0_divider(divider_config.p0_p_divider) |
| .WriteTo(mmio_space_); |
| |
| // The PRM instructs us to read back any configuration register in order to |
| // ensure that the writes completed. This must happen before enabling the PLL. |
| pll_dco_dividers.ReadFrom(mmio_space_); |
| |
| dpll_enable.set_pll_enabled(true); |
| dpll_enable.WriteTo(mmio_space_); |
| if (!PollUntil( |
| [&] { return dpll_enable.ReadFrom(mmio_space_).pll_locked_tiger_lake_and_lcpll1(); }, |
| zx::usec(1), g_display_pll_tiger_lake_lock_wait_timeout_us)) { |
| zxlogf(ERROR, "DPLL %d lock failure! Failed to lock after 500us", pll_id()); |
| return false; |
| } |
| |
| set_config(pll_config); |
| return true; |
| } |
| |
| bool DisplayPllTigerLake::DoDisable() { |
| auto pll_enable = registers::PllEnable::GetForTigerLakeDpll(pll_id()).ReadFrom(mmio_space_); |
| pll_enable.set_pll_enabled(false).WriteTo(mmio_space_); |
| return true; |
| } |
| |
| // static |
| ScopedValueChange<int> DisplayPllTigerLake::OverrideLockWaitTimeoutUsForTesting(int timeout_us) { |
| return ScopedValueChange(g_display_pll_tiger_lake_lock_wait_timeout_us, timeout_us); |
| } |
| |
| // static |
| ScopedValueChange<int> DisplayPllTigerLake::OverridePowerOnWaitTimeoutMsForTesting(int timeout_ms) { |
| return ScopedValueChange(g_display_pll_tiger_lake_power_on_wait_timeout_ms, timeout_ms); |
| } |
| |
| namespace { |
| |
| PllId TypeCDdiToDekelPll(DdiId type_c_ddi) { |
| switch (type_c_ddi) { |
| case DdiId::DDI_TC_1: |
| return PllId::DPLL_TC_1; |
| case DdiId::DDI_TC_2: |
| return PllId::DPLL_TC_2; |
| case DdiId::DDI_TC_3: |
| return PllId::DPLL_TC_3; |
| case DdiId::DDI_TC_4: |
| return PllId::DPLL_TC_4; |
| case DdiId::DDI_TC_5: |
| return PllId::DPLL_TC_5; |
| case DdiId::DDI_TC_6: |
| return PllId::DPLL_TC_6; |
| default: |
| ZX_ASSERT_MSG(false, "Not a Type-C DDI"); |
| } |
| } |
| |
| DdiId DekelPllToTypeCDdi(PllId dekel_pll) { |
| switch (dekel_pll) { |
| case PllId::DPLL_TC_1: |
| return DdiId::DDI_TC_1; |
| case PllId::DPLL_TC_2: |
| return DdiId::DDI_TC_2; |
| case PllId::DPLL_TC_3: |
| return DdiId::DDI_TC_3; |
| case PllId::DPLL_TC_4: |
| return DdiId::DDI_TC_4; |
| case PllId::DPLL_TC_5: |
| return DdiId::DDI_TC_5; |
| case PllId::DPLL_TC_6: |
| return DdiId::DDI_TC_6; |
| default: |
| ZX_ASSERT_MSG(false, "Not a Dekel PLL"); |
| } |
| } |
| |
| } // namespace |
| |
| DekelPllTigerLake::DekelPllTigerLake(fdf::MmioBuffer* mmio_space, PllId pll_id) |
| : DisplayPll(pll_id), mmio_space_(mmio_space) {} |
| |
| DdiId DekelPllTigerLake::ddi_id() const { |
| ZX_DEBUG_ASSERT(pll_id() >= PllId::DPLL_TC_1); |
| ZX_DEBUG_ASSERT(pll_id() <= PllId::DPLL_TC_6); |
| return DekelPllToTypeCDdi(pll_id()); |
| } |
| |
| bool DekelPllTigerLake::DoEnable(const DdiPllConfig& pll_config) { |
| if (pll_config.admits_hdmi) { |
| return EnableHdmi(pll_config); |
| } |
| return EnableDp(pll_config); |
| } |
| |
| bool DekelPllTigerLake::DoDisable() { |
| // Follow the "DKL PLL Disable Sequence" to disable the PLL. |
| // |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev2.0, Pages 188-189 |
| // "DKL PLL Disable Sequence" |
| |
| // Step 1. Configure DPCLKA_CFGCR0 to turn off the clock for the port. |
| auto ddi_clock_config = registers::DdiClockConfig::Get().ReadFrom(mmio_space_); |
| ddi_clock_config.set_ddi_clock_disabled(ddi_id(), true).WriteTo(mmio_space_); |
| ddi_clock_config.ReadFrom(mmio_space_); // Posting read |
| |
| // Step 2. If the frequency will result in a change to the voltage requirement, |
| // follow the "Display Voltage Frequency Switching - Sequence Before Frequency |
| // Change". |
| // |
| // TODO(https://fxbug.dev/42180877): Currently it is okay to ignore this, unless we need |
| // to support 5K+ display where we need to change display voltage and Core |
| // Display Clock. |
| |
| // 3. Disable PLL through MGPLL_ENABLE. |
| auto pll_enable = registers::PllEnable::GetForTigerLakeDpll(pll_id()).ReadFrom(mmio_space_); |
| pll_enable.ReadFrom(mmio_space_).set_pll_enabled(false).WriteTo(mmio_space_); |
| |
| // Step 4. Wait for PLL not locked status in MGPLL_ENABLE. |
| // Should complete within 50us. |
| if (!PollUntil( |
| [&] { return !pll_enable.ReadFrom(mmio_space_).pll_locked_tiger_lake_and_lcpll1(); }, |
| zx::usec(1), 50)) { |
| zxlogf(ERROR, "Dekel PLL %s: Cannot disable PLL", name().c_str()); |
| } |
| |
| // Step 5. If the frequency will result in a change to the voltage |
| // requirement, follow the "Display Voltage Frequency Switching - Sequence |
| // After Frequency Change". |
| // |
| // TODO(https://fxbug.dev/42180877): Currently it is okay to ignore this, unless we need |
| // to support 5K+ display where we need to change display voltage and Core |
| // Display Clock. |
| |
| // 6. Disable PLL power in MGPLL_ENABLE. |
| pll_enable.set_power_on_request_tiger_lake(false).WriteTo(mmio_space_); |
| |
| // 7. Wait for PLL power state disabled in MGPLL_ENABLE. |
| // - Should complete immediately. |
| if (!PollUntil([&] { return !pll_enable.ReadFrom(mmio_space_).powered_on_tiger_lake(); }, |
| zx::usec(1), 10)) { |
| zxlogf(ERROR, "Dekel PLL %s: Cannot disable PLL power", name().c_str()); |
| } |
| |
| return true; |
| } |
| |
| bool DekelPllTigerLake::EnableHdmi(const DdiPllConfig& pll_config) { |
| // TODO(https://fxbug.dev/42060757): Support HDMI on Type-C. |
| zxlogf(ERROR, "Dekel PLL %s: EnableHdmi: Not implemented", name().c_str()); |
| return false; |
| } |
| |
| bool DekelPllTigerLake::EnableDp(const DdiPllConfig& pll_config) { |
| // This method contains the procedure to enable DisplayPort Mode Dekel PLL. |
| // Reference: |
| // Tiger Lake: Section "DKL PLL Enable Sequence", |
| // IHD-OS-TGL-Vol 12-1.22-Rev 2.0, Pages 177-178 |
| |
| auto pll_enable = registers::PllEnable::GetForTigerLakeDpll(pll_id()).ReadFrom(mmio_space_); |
| pll_enable.set_power_on_request_tiger_lake(true).WriteTo(mmio_space_); |
| if (!PollUntil([&] { return pll_enable.ReadFrom(mmio_space_).powered_on_tiger_lake(); }, |
| zx::usec(1), 10)) { |
| zxlogf(ERROR, "Dekel PLL %s: Cannot enable PLL power", name().c_str()); |
| return false; |
| } |
| |
| // Step 3-4. Program PLL registers as given in tables. Read back PHY PLL |
| // register after writing to ensure writes completed. |
| // |
| // Step 3.1. Program rate independent registers for Native and Alt DP mode. |
| // |
| // Register value table: |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0, Pages 190-191 |
| |
| // Program DKL_PLL_DIV0. |
| auto divisor0 = registers::DekelPllDivisor0::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| divisor0.set_reg_value(0x70272269).WriteTo(mmio_space_).ReadFrom(mmio_space_); // Posting read |
| |
| // Program DKL_PLL_DIV1. |
| auto divisor1 = registers::DekelPllDivisor1::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| divisor1.set_reg_value(0x0CDCC527).WriteTo(mmio_space_).ReadFrom(mmio_space_); // Posting read |
| |
| // Program DKL_PLL_LF. |
| auto lf = registers::DekelPllLf::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| lf.set_reg_value(0x00401300).WriteTo(mmio_space_).ReadFrom(mmio_space_); // Posting read |
| |
| // Program DKL_PLL_FRAC_LOCK. |
| auto frac_lock = registers::DekelPllFractionalLock::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| frac_lock.set_reg_value(0x8044B56A).WriteTo(mmio_space_).ReadFrom(mmio_space_); // Posting read |
| |
| // Program DKL_SSC. |
| auto ssc_config = registers::DekelPllSsc::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| ssc_config.set_reg_value(0x401322FF).WriteTo(mmio_space_).ReadFrom(mmio_space_); // Posting read |
| |
| // Program DKL_CMN_DIG_PLL_MISC. |
| auto common_config_digital_pll_misc = |
| registers::DekelCommonConfigDigitalPllMisc::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| common_config_digital_pll_misc.set_reg_value(0x00000000) |
| .WriteTo(mmio_space_) |
| .ReadFrom(mmio_space_); // Posting read |
| |
| // Program DKL_REFCLKIN_CTL. |
| auto reference_clock_input_control = |
| registers::DekelPllReferenceClockInputControl::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| reference_clock_input_control.set_reg_value(0x00000101) |
| .WriteTo(mmio_space_) |
| .ReadFrom(mmio_space_); // Posting read |
| |
| // Program DKL_CMN_ANA_DWORD28. |
| auto common_config_analog_dword_28 = |
| registers::DekelCommonConfigAnalogDword28::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| common_config_analog_dword_28.set_reg_value(0x14158888) |
| .WriteTo(mmio_space_) |
| .ReadFrom(mmio_space_); // Posting read |
| |
| // Step 3.2. Program rate dependent registers for Native and Alt DP mode. |
| // |
| // Register value table: |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0, Pages 191 |
| auto high_speed_clock_control = |
| registers::DekelPllClktop2HighSpeedClockControl::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| auto core_clock_control = |
| registers::DekelPllClktop2CoreClockControl1::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| |
| const int display_port_link_rate_mbps = pll_config.ddi_clock_khz / 500; |
| switch (display_port_link_rate_mbps) { |
| case 8'100: { |
| high_speed_clock_control.set_reg_value(0x0000011D); |
| core_clock_control.set_reg_value(0x10080510); |
| break; |
| } |
| case 5'400: { |
| high_speed_clock_control.set_reg_value(0x0000121D); |
| core_clock_control.set_reg_value(0x10080510); |
| break; |
| } |
| case 2'700: { |
| high_speed_clock_control.set_reg_value(0x0000521D); |
| core_clock_control.set_reg_value(0x10080A12); |
| break; |
| } |
| case 1'620: { |
| high_speed_clock_control.set_reg_value(0x0000621D); |
| core_clock_control.set_reg_value(0x10080A12); |
| break; |
| } |
| default: { |
| zxlogf(ERROR, "Unsupported DP link rate: %d Mbps", display_port_link_rate_mbps); |
| return false; |
| } |
| } |
| |
| // Program CLKTOP2_HSCLKCTL. |
| high_speed_clock_control.WriteTo(mmio_space_).ReadFrom(mmio_space_); // Posting read |
| |
| // Program CLKTOP2_CORECLKCTL1. |
| core_clock_control.WriteTo(mmio_space_).ReadFrom(mmio_space_); // Posting read |
| |
| // Step 5. If the frequency will result in a change to the voltage |
| // requirement, follow the "Display Voltage Frequency Switching - Sequence |
| // Before Frequency Change." |
| // |
| // TODO(https://fxbug.dev/42180877): Currently it is okay to ignore this, unless we need |
| // to support 5K+ display where we need to change display voltage and Core |
| // Display Clock. |
| |
| // Step 6. Enable PLL in MGPLL_ENABLE. |
| pll_enable.ReadFrom(mmio_space_).set_pll_enabled(true).WriteTo(mmio_space_); |
| |
| // Step 7. Wait for PLL lock status in MGPLL_ENABLE. |
| // - Timeout and fail after 900us. |
| if (!PollUntil( |
| [&] { return pll_enable.ReadFrom(mmio_space_).pll_locked_tiger_lake_and_lcpll1(); }, |
| zx::usec(1), 900)) { |
| zxlogf(ERROR, "Dekel PLL (%s): Cannot enable PLL", name().c_str()); |
| return false; |
| } |
| |
| // Step 8. If the frequency will result in a change to the voltage |
| // requirement, follow the "Display Voltage Frequency Switching - Sequence |
| // After Frequency Change". |
| // |
| // TODO(https://fxbug.dev/42180877): Currently it is okay to ignore this, unless we need |
| // to support 5K+ display where we need to change display voltage and Core |
| // Display Clock. |
| |
| // 9. Program DDI_CLK_SEL to map the Type-C PLL clock to the port. |
| auto ddi_clk_sel = registers::TypeCDdiClockSelect::GetForDdi(ddi_id()).ReadFrom(mmio_space_); |
| ddi_clk_sel.set_clock_select(registers::TypeCDdiClockSelect::ClockSelect::kTypeCPll) |
| .WriteTo(mmio_space_); |
| |
| // 10. Configure DPCLKA_CFGCR0 to turn on the clock for the port. |
| auto ddi_clock_config = registers::DdiClockConfig::Get().ReadFrom(mmio_space_); |
| ddi_clock_config.set_ddi_clock_disabled(ddi_id(), false).WriteTo(mmio_space_); |
| ddi_clock_config.ReadFrom(mmio_space_); // Posting read |
| return true; |
| } |
| |
| DpllManagerTigerLake::DpllManagerTigerLake(fdf::MmioBuffer* mmio_space) : mmio_space_(mmio_space) { |
| constexpr std::array kDekelDplls = { |
| PllId::DPLL_TC_1, PllId::DPLL_TC_2, PllId::DPLL_TC_3, |
| PllId::DPLL_TC_4, PllId::DPLL_TC_5, PllId::DPLL_TC_6, |
| }; |
| for (const auto dpll : kDekelDplls) { |
| plls_[dpll] = std::make_unique<DekelPllTigerLake>(mmio_space_, dpll); |
| ref_count_[plls_[dpll].get()] = 0; |
| } |
| |
| constexpr std::array kDisplayPllIds = { |
| PllId::DPLL_0, PllId::DPLL_1, |
| // TODO(https://fxbug.dev/42061706): Add support for DPLL4. |
| }; |
| for (const PllId display_pll_id : kDisplayPllIds) { |
| const auto [plls_it, success] = plls_.try_emplace( |
| display_pll_id, std::make_unique<DisplayPllTigerLake>(mmio_space_, display_pll_id)); |
| ZX_DEBUG_ASSERT_MSG(success, "Display PLL %d already inserted in map", display_pll_id); |
| } |
| |
| // TODO(https://fxbug.dev/42182480): Add Thunderbolt PLL (DPLL 2) to the `plls_` map. |
| |
| auto display_straps = registers::DisplayStraps::Get().ReadFrom(mmio_space_); |
| reference_clock_khz_ = display_straps.reference_frequency_khz_tiger_lake(); |
| } |
| |
| bool DpllManagerTigerLake::SetDdiClockSource(DdiId ddi_id, PllId pll_id) { |
| switch (pll_id) { |
| case PllId::DPLL_TC_1: |
| case PllId::DPLL_TC_2: |
| case PllId::DPLL_TC_3: |
| case PllId::DPLL_TC_4: |
| case PllId::DPLL_TC_5: |
| case PllId::DPLL_TC_6: { |
| ZX_ASSERT(ddi_id >= DdiId::DDI_TC_1); |
| ZX_ASSERT(ddi_id <= DdiId::DDI_TC_6); |
| ZX_ASSERT(ddi_id - DdiId::DDI_TC_1 == pll_id - PllId::DPLL_TC_1); |
| return true; |
| } |
| |
| case PllId::DPLL_0: |
| case PllId::DPLL_1: { |
| if (ddi_id < DdiId::DDI_A || ddi_id > DdiId::DDI_C) { |
| return false; |
| } |
| auto dpll_clock_config = registers::DdiClockConfig::Get().ReadFrom(mmio_space_); |
| dpll_clock_config.set_ddi_clock_disabled(ddi_id, false) |
| .set_ddi_clock_display_pll(ddi_id, pll_id) |
| .WriteTo(mmio_space_); |
| return true; |
| } |
| |
| case PllId::DPLL_2: |
| // TODO(https://fxbug.dev/42182480): Thunderbolt support. |
| zxlogf(ERROR, "SetDdiClockSource() does not support DPLL 2 (for Thunderbolt) yet"); |
| return false; |
| |
| default: |
| zxlogf(ERROR, "SetDdiClockSource() does not support DPLL %d yet", pll_id); |
| return false; |
| } |
| } |
| |
| bool DpllManagerTigerLake::ResetDdiClockSource(DdiId ddi_id) { |
| if (ddi_id >= DdiId::DDI_TC_1 && ddi_id <= DdiId::DDI_TC_6) { |
| // TODO(https://fxbug.dev/42182480): Any configuration needed if the DDI uses DPLL 2 |
| // (Display PLL 2, dedicated to Thunderbolt frequencies)? |
| |
| return true; |
| } |
| |
| ZX_DEBUG_ASSERT(ddi_id >= DdiId::DDI_A); |
| ZX_DEBUG_ASSERT(ddi_id <= DdiId::DDI_C); |
| auto dpll_clock_config = registers::DdiClockConfig::Get().ReadFrom(mmio_space_); |
| dpll_clock_config.set_ddi_clock_disabled(ddi_id, true).WriteTo(mmio_space_); |
| return true; |
| } |
| |
| DdiPllConfig DpllManagerTigerLake::LoadStateForComboDdi(DdiId ddi_id) { |
| ZX_ASSERT(ddi_id >= DdiId::DDI_A); |
| ZX_ASSERT(ddi_id <= DdiId::DDI_C); |
| |
| auto ddi_clock_config = registers::DdiClockConfig::Get().ReadFrom(mmio_space_); |
| if (ddi_clock_config.ddi_clock_disabled(ddi_id)) { |
| zxlogf(TRACE, "Loaded DDI %d DPLL config: DDI clock disabled", ddi_id); |
| return DdiPllConfig{}; |
| } |
| |
| const PllId pll_id = ddi_clock_config.ddi_clock_display_pll(ddi_id); |
| if (pll_id == PllId::DPLL_INVALID) { |
| zxlogf(WARNING, |
| "Invalid DDI %d DPLL config: Invalid clock source DPLL! DDI Clock Config register: %x", |
| ddi_id, ddi_clock_config.reg_value()); |
| return DdiPllConfig{}; |
| } |
| if (pll_id == PllId::DPLL_2) { |
| zxlogf(WARNING, |
| "Invalid DDI %d DPLL config: clock source is DPLL 2, but DPLL2 reserved for Thunderbot.", |
| ddi_id); |
| return DdiPllConfig{}; |
| } |
| |
| auto dpll_enable = registers::PllEnable::GetForTigerLakeDpll(pll_id).ReadFrom(mmio_space_); |
| if (!dpll_enable.pll_enabled()) { |
| zxlogf(TRACE, "Loaded DDI %d DPLL %d config: DPLL disabled", ddi_id, pll_id); |
| return DdiPllConfig{}; |
| } |
| |
| // We don't currently have enough documentation to configure the DPLL divider |
| // register. However, since the field breakdown is documented, we can log it, |
| // in case it helps any future investigation. |
| auto dpll_divider = registers::DisplayPllDivider::GetForDpll(pll_id).ReadFrom(mmio_space_); |
| zxlogf( |
| TRACE, |
| "Loaded DDI %d DPLL %d dividers: early lock %d, true lock %d, AFC start point %d, " |
| "feedback clock retiming %s, loop filter - integral 2^(-%d) proportional 2^(1-%d) gain 2^%d " |
| "pre-divider %d, post-divider (M2) %d", |
| ddi_id, pll_id, dpll_divider.early_lock_criteria_cycles(), |
| dpll_divider.true_lock_criteria_cycles(), |
| dpll_divider.automatic_frequency_calibration_start_point(), |
| dpll_divider.feedback_clock_retiming_enabled() ? "yes" : "no", |
| dpll_divider.loop_filter_integral_coefficient_exponent(), |
| dpll_divider.loop_filter_proportional_coefficient_exponent(), |
| dpll_divider.loop_filter_gain_control(), dpll_divider.feedback_pre_divider(), |
| dpll_divider.feedback_post_divider()); |
| |
| auto dpll_dco_frequency = |
| registers::DisplayPllDcoFrequencyTigerLake::GetForDpll(pll_id).ReadFrom(mmio_space_); |
| auto dpll_dco_dividers = |
| registers::DisplayPllDcoDividersTigerLake::GetForDpll(pll_id).ReadFrom(mmio_space_); |
| auto dpll_spread_spectrum_clocking = |
| registers::DisplayPllSpreadSpectrumClocking::GetForDpll(pll_id).ReadFrom(mmio_space_); |
| |
| if (dpll_dco_dividers.reference_clock_select() != |
| registers::DisplayPllDcoDividersTigerLake::ReferenceClockSelect::kDisplayReference) { |
| zxlogf( |
| ERROR, |
| "Loaded DDI %d DPLL %d config: DPLL uses genlock clock reference %d. Genlock not supported!", |
| ddi_id, pll_id, static_cast<int>(dpll_dco_dividers.reference_clock_select())); |
| return DdiPllConfig{}; |
| } |
| |
| // Remove stale mappings first. |
| if (ddi_to_dpll_.find(ddi_id) != ddi_to_dpll_.end()) { |
| ZX_DEBUG_ASSERT(ref_count_.count(ddi_to_dpll_[ddi_id]) > 0); |
| ZX_DEBUG_ASSERT(ref_count_[ddi_to_dpll_[ddi_id]] > 0); |
| --ref_count_[ddi_to_dpll_[ddi_id]]; |
| ddi_to_dpll_.erase(ddi_id); |
| } |
| |
| ddi_to_dpll_[ddi_id] = plls_[pll_id].get(); |
| ++ref_count_[ddi_to_dpll_[ddi_id]]; |
| |
| const int32_t dco_frequency_khz = |
| dpll_dco_frequency.dco_frequency_khz(static_cast<int32_t>(reference_clock_khz_)); |
| |
| // P (P0) and K (P2) are <= 7, so their product fits in int8_t. |
| const int16_t dco_frequency_divider = |
| (dpll_dco_dividers.p_p0_divider() * dpll_dco_dividers.k_p2_divider()) * |
| int16_t{dpll_dco_dividers.q_p1_divider()}; |
| |
| const int32_t ddi_clock_khz = static_cast<int32_t>(dco_frequency_khz / dco_frequency_divider); |
| |
| zxlogf( |
| TRACE, |
| "Loaded DDI %d DPLL %d config: %s DDI clock %d kHz DCO frequency=%d kHz divider P*Q*K=%u*%u*%u", |
| ddi_id, pll_id, dpll_spread_spectrum_clocking.enabled() ? "SSC" : "no SSC", ddi_clock_khz, |
| dco_frequency_khz, dpll_dco_dividers.p_p0_divider(), dpll_dco_dividers.q_p1_divider(), |
| dpll_dco_dividers.k_p2_divider()); |
| |
| return DdiPllConfig{ |
| .ddi_clock_khz = ddi_clock_khz, |
| .spread_spectrum_clocking = (dpll_spread_spectrum_clocking.enabled() != 0), |
| .admits_display_port = true, |
| .admits_hdmi = true, |
| }; |
| } |
| |
| DdiPllConfig DpllManagerTigerLake::LoadStateForTypeCDdi(DdiId ddi_id) { |
| ZX_ASSERT(ddi_id >= DdiId::DDI_TC_1); |
| ZX_ASSERT(ddi_id <= DdiId::DDI_TC_6); |
| |
| // TODO(https://fxbug.dev/42182480): Currently this method assume all Type-C PHYs use |
| // USB-C (Dekel PLL) instead of Thunderbolt. This needs to be changed once |
| // we support Thunderbolt. |
| |
| // Follow the "Calculating PLL Frequency from Divider Values" algorithm |
| // to calculate the output frequency of the PLL. |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0, Page 193 |
| // Section "Calculating PLL Frequency from Divider Values" |
| auto pll_divisor0 = registers::DekelPllDivisor0::GetForDdi(ddi_id).ReadFrom(mmio_space_); |
| auto pll_bias = registers::DekelPllBias::GetForDdi(ddi_id).ReadFrom(mmio_space_); |
| auto high_speed_clock_control = |
| registers::DekelPllClktop2HighSpeedClockControl::GetForDdi(ddi_id).ReadFrom(mmio_space_); |
| |
| // M1 (feedback predivider) = DKL_PLL_DIV0[i_fbprediv_3_0] |
| const int64_t m1_feedback_predivider = pll_divisor0.feedback_predivider_ratio(); |
| |
| // M2 (feedback divider) = m2_integer_part + m2_fractional_part_bits / 2^22. |
| const int64_t m2_feedback_divider_integer_part = pll_divisor0.feedback_divider_integer_part(); |
| const int64_t m2_feedback_divider_fractional_part_bits = |
| pll_bias.fractional_modulator_enabled() ? pll_bias.feedback_divider_fractional_part_22_bits() |
| : 0; |
| |
| // DIV1 (high speed divisor) = DKL_CLKTOP2_HSCLKCTL[od_clktop_hsdiv_divratio] |
| const int64_t div1_high_speed_divisor = high_speed_clock_control.high_speed_divider_ratio(); |
| |
| // DIV2 (programmable divisor) = DKL_CLKTOP2_HSCLKCTL[od_clktop_dsdiv_divratio] |
| const int64_t div2_programmable_divisor = high_speed_clock_control.programmable_divider_ratio(); |
| |
| // Symbol clock frequency |
| // = M1 * M2 * reference frequency / ( 5 * div1 * div2 ) |
| // = M1 * (m2_integer_part + m2_fractional_part_bits / 2^22) * reference frequency / ( 5 * div1 * |
| // div2 ) |
| // = (M1 * m2_integer_part * reference frequency + M1 * m2_fractional_part_bits * reference |
| // frequency / 2^22) / (5 * div1 * div2); |
| const int64_t symbol_rate_khz = |
| (m1_feedback_predivider * m2_feedback_divider_integer_part * reference_clock_khz_ + |
| ((m1_feedback_predivider * m2_feedback_divider_fractional_part_bits * |
| reference_clock_khz_) >> |
| 22)) / |
| (5ul * div1_high_speed_divisor * div2_programmable_divisor); |
| |
| // PLL output frequency (rate) is 5x the symbol rate. |
| // |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0 "Type-C PLLs", Page 171 |
| const int64_t pll_out_rate_khz = symbol_rate_khz * 5; |
| |
| // Bit rate is 2x PLL output rate. |
| // |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0 "Type-C PLLs", Page 171 |
| const int64_t bit_rate_khz = pll_out_rate_khz * 2; |
| |
| // Match calculated bit rate to valid DisplayPort bit rates. |
| // |
| // Valid DisplayPort link bit rates are: |
| // - 1.62 GHz |
| // - 2.7 GHz |
| // - 5.4 GHz |
| // - 8.1 GHz |
| // Tiger Lake: IHD-OS-TGL-Vol 12-1.22-Rev 2.0 "Type-C PLLs", Page 171 |
| |
| // TODO(https://fxbug.dev/42060757): Currently we just assume all Type-C PHYs use DP Alt |
| // mode, and only match the calculated bit rate to DisplayPort bit rates. |
| // It could also be configured to use legacy HDMI / DVI, in which case the |
| // symbol rate will fail to match any of the candidates and fail. |
| static constexpr int64_t kEpsilonKhz = 50'000; |
| static constexpr int64_t kValidDisplayPortBitRatesKhz[] = {1'620'000, 2'700'000, 5'400'000, |
| 8'100'000}; |
| |
| for (const auto valid_dp_bit_rate_khz : kValidDisplayPortBitRatesKhz) { |
| if (abs(bit_rate_khz - valid_dp_bit_rate_khz) < kEpsilonKhz) { |
| // TODO(fxbug.com/112752): The DekelPllTigerLake instance is not updated |
| // to reflect the state in the registers. |
| return DdiPllConfig{ |
| .ddi_clock_khz = static_cast<int16_t>(valid_dp_bit_rate_khz / 2), |
| .spread_spectrum_clocking = false, |
| .admits_display_port = true, |
| .admits_hdmi = false, |
| }; |
| } |
| } |
| |
| zxlogf(WARNING, "LoadTypeCPllState: DDI %d has invalid DisplayPort bit rate: %ld KHz", ddi_id, |
| bit_rate_khz); |
| return DdiPllConfig{}; |
| } |
| |
| DdiPllConfig DpllManagerTigerLake::LoadState(DdiId ddi_id) { |
| switch (ddi_id) { |
| case DdiId::DDI_TC_1: |
| case DdiId::DDI_TC_2: |
| case DdiId::DDI_TC_3: |
| case DdiId::DDI_TC_4: |
| case DdiId::DDI_TC_5: |
| case DdiId::DDI_TC_6: |
| return LoadStateForTypeCDdi(ddi_id); |
| |
| case DdiId::DDI_A: |
| case DdiId::DDI_B: |
| case DdiId::DDI_C: |
| return LoadStateForComboDdi(ddi_id); |
| } |
| } |
| |
| DisplayPll* DpllManagerTigerLake::FindPllFor(DdiId ddi_id, bool is_edp, |
| const DdiPllConfig& desired_config) { |
| // TODO(https://fxbug.dev/42182480): Currently we assume `ddi` is always in DisplayPort |
| // Alt mode. We need to map `ddi` to Thunderbolt DPLL once we support |
| // Thunderbolt. |
| if (ddi_id >= DdiId::DDI_TC_1 && ddi_id <= DdiId::DDI_TC_6) { |
| auto dpll = TypeCDdiToDekelPll(ddi_id); |
| return plls_[dpll].get(); |
| } |
| |
| constexpr std::array kDisplayPllIds = { |
| PllId::DPLL_0, PllId::DPLL_1, |
| // TODO(https://fxbug.dev/42061706): Add support for DPLL4. |
| }; |
| for (const PllId display_pll_id : kDisplayPllIds) { |
| const auto plls_it = plls_.find(display_pll_id); |
| ZX_DEBUG_ASSERT_MSG(plls_it != plls_.end(), "PLL %d not in map", display_pll_id); |
| |
| DisplayPll* const display_pll = plls_it->second.get(); |
| if (ref_count_[display_pll] == 0 || display_pll->config() == desired_config) { |
| return display_pll; |
| } |
| } |
| return nullptr; |
| } |
| |
| } // namespace i915 |