| // Copyright 2019 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 "aml-cpu.h" |
| |
| #include <lib/ddk/platform-defs.h> |
| #include <lib/driver/component/cpp/driver_base.h> |
| #include <lib/mmio/mmio.h> |
| #include <zircon/syscalls/smc.h> |
| |
| #include <vector> |
| |
| namespace amlogic_cpu { |
| |
| namespace { |
| constexpr uint32_t kCpuGetDvfsTableIndexFuncId = 0x82000088; |
| constexpr uint64_t kDefaultClusterId = 0; |
| |
| constexpr uint32_t kInitialOpp = fuchsia_hardware_cpu_ctrl::wire::kDeviceOperatingPointP0; |
| |
| } // namespace |
| |
| zx_status_t GetPopularVoltageTable(const zx::resource& smc_resource, uint32_t* metadata_type) { |
| if (smc_resource.is_valid()) { |
| zx_smc_parameters_t smc_params = {}; |
| smc_params.func_id = kCpuGetDvfsTableIndexFuncId; |
| smc_params.arg1 = kDefaultClusterId; |
| |
| zx_smc_result_t smc_result; |
| zx_status_t status = zx_smc_call(smc_resource.get(), &smc_params, &smc_result); |
| if (status != ZX_OK) { |
| FDF_LOG(ERROR, "zx_smc_call failed: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| switch (smc_result.arg0) { |
| case amlogic_cpu::OppTable1: |
| *metadata_type = DEVICE_METADATA_AML_OP_1_POINTS; |
| break; |
| case amlogic_cpu::OppTable2: |
| *metadata_type = DEVICE_METADATA_AML_OP_2_POINTS; |
| break; |
| case amlogic_cpu::OppTable3: |
| *metadata_type = DEVICE_METADATA_AML_OP_3_POINTS; |
| break; |
| default: |
| *metadata_type = DEVICE_METADATA_AML_OP_POINTS; |
| break; |
| } |
| FDF_LOG(INFO, "Dvfs using table%ld.\n", smc_result.arg0); |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx::result<AmlCpuConfiguration> LoadConfiguration(ddk::PDevFidl& pdev) { |
| zx_status_t st; |
| AmlCpuConfiguration config; |
| |
| std::optional<fdf::MmioBuffer> mmio_buffer; |
| st = pdev.MapMmio(0, &mmio_buffer); |
| if (st != ZX_OK) { |
| FDF_LOG(ERROR, "aml-cpu: Failed to map mmio: %s", zx_status_get_string(st)); |
| return zx::error(st); |
| } |
| |
| config.info = {}; |
| st = pdev.GetDeviceInfo(&config.info); |
| if (st != ZX_OK) { |
| FDF_LOG(ERROR, "Failed to get DeviceInfo: %s", zx_status_get_string(st)); |
| return zx::error(st); |
| } |
| |
| zx::resource smc_resource = {}; |
| config.metadata_type = DEVICE_METADATA_AML_OP_POINTS; |
| config.fragments_per_pf_domain = kFragmentsPerPfDomain; |
| zx_off_t cpu_version_offset = kCpuVersionOffset; |
| if (config.info.pid == PDEV_PID_AMLOGIC_A5) { |
| st = pdev.GetSmc(0, &smc_resource); |
| if (st != ZX_OK) { |
| FDF_LOG(ERROR, "Failed to get smc: %s", zx_status_get_string(st)); |
| return zx::error(st); |
| } |
| st = GetPopularVoltageTable(smc_resource, &config.metadata_type); |
| if (st != ZX_OK) { |
| FDF_LOG(ERROR, "Failed to get popular voltage table: %s", zx_status_get_string(st)); |
| return zx::error(st); |
| } |
| config.fragments_per_pf_domain = kFragmentsPerPfDomainA5; |
| cpu_version_offset = kCpuVersionOffsetA5; |
| } else if (config.info.pid == PDEV_PID_AMLOGIC_A1) { |
| config.fragments_per_pf_domain = kFragmentsPerPfDomainA1; |
| cpu_version_offset = kCpuVersionOffsetA1; |
| } |
| |
| config.cpu_version_packed = mmio_buffer->Read32(cpu_version_offset); |
| |
| config.has_div16_clients = config.fragments_per_pf_domain == kFragmentsPerPfDomain; |
| |
| // For A1, the CPU power is VDD_CORE, which share with other module. |
| // The fixed voltage is 0.8v, we can't adjust it dynamically. |
| config.has_power_client = config.info.pid != PDEV_PID_AMLOGIC_A1; |
| |
| return zx::ok(config); |
| } |
| |
| std::vector<operating_point_t> PerformanceDomainOpPoints(const perf_domain_t& perf_domain, |
| std::vector<operating_point>& op_points) { |
| std::vector<operating_point_t> pd_op_points; |
| std::copy_if(op_points.begin(), op_points.end(), std::back_inserter(pd_op_points), |
| [&perf_domain](const operating_point_t& op) { return op.pd_id == perf_domain.id; }); |
| |
| // Order operating points from highest frequency to lowest because Operating Point 0 is the |
| // fastest. |
| std::sort(pd_op_points.begin(), pd_op_points.end(), |
| [](const operating_point_t& a, const operating_point_t& b) { |
| // Use voltage as a secondary sorting key. |
| if (a.freq_hz == b.freq_hz) { |
| return a.volt_uv > b.volt_uv; |
| } |
| return a.freq_hz > b.freq_hz; |
| }); |
| |
| return pd_op_points; |
| } |
| |
| zx_status_t AmlCpu::SetCurrentOperatingPointInternal(uint32_t requested_opp, uint32_t* out_opp) { |
| std::scoped_lock lock(lock_); |
| |
| if (requested_opp >= operating_points_.size()) { |
| FDF_LOG(ERROR, "Requested opp is out of bounds, opp = %u\n", requested_opp); |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| |
| if (!out_opp) { |
| FDF_LOG(ERROR, "out_opp may not be null"); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| // There is no condition under which this function will return ZX_OK but out_opp will not |
| // be requested_opp so we're going to go ahead and set that up front. |
| *out_opp = requested_opp; |
| |
| const operating_point_t& target_opp = operating_points_[requested_opp]; |
| const operating_point_t& initial_opp = operating_points_[current_operating_point_]; |
| |
| FDF_LOG(INFO, "Scaling from %u MHz %u mV to %u MHz %u mV", initial_opp.freq_hz / 1000000, |
| initial_opp.volt_uv / 1000, target_opp.freq_hz / 1000000, target_opp.volt_uv / 1000); |
| |
| if (initial_opp.freq_hz == target_opp.freq_hz && initial_opp.volt_uv == target_opp.volt_uv) { |
| // Nothing to be done. |
| return ZX_OK; |
| } |
| |
| if (target_opp.volt_uv > initial_opp.volt_uv) { |
| // If we're increasing the voltage we need to do it before setting the |
| // frequency. |
| ZX_ASSERT(pwr_.is_valid()); |
| fidl::WireResult result = pwr_->RequestVoltage(target_opp.volt_uv); |
| if (!result.ok()) { |
| FDF_LOG(ERROR, "Failed to send RequestVoltage request: %s", result.status_string()); |
| return result.error().status(); |
| } |
| |
| if (result->is_error()) { |
| FDF_LOG(ERROR, "RequestVoltage call returned error: %s", |
| zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| |
| uint32_t actual_voltage = result->value()->actual_voltage; |
| if (actual_voltage != target_opp.volt_uv) { |
| FDF_LOG(ERROR, "Actual voltage does not match, requested = %u, got = %u", target_opp.volt_uv, |
| actual_voltage); |
| return ZX_OK; |
| } |
| } |
| |
| // Set the frequency next. |
| fidl::WireResult result = cpuscaler_->SetRate(target_opp.freq_hz); |
| if (!result.ok() || result->is_error()) { |
| FDF_LOG(ERROR, "Could not set CPU frequency: %s", result.FormatDescription().c_str()); |
| |
| // Put the voltage back if frequency scaling fails. |
| if (pwr_.is_valid()) { |
| fidl::WireResult result = pwr_->RequestVoltage(initial_opp.volt_uv); |
| if (!result.ok()) { |
| FDF_LOG(ERROR, "Failed to send RequestVoltage request: %s", result.status_string()); |
| return result.error().status(); |
| } |
| |
| if (result->is_error()) { |
| FDF_LOG(ERROR, "Failed to reset CPU voltage, st = %s, Voltage and frequency mismatch!", |
| zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| } |
| |
| if (!result.ok()) { |
| return result.status(); |
| } |
| return result->error_value(); |
| } |
| |
| // If we're decreasing the voltage, then we do it after the frequency has been |
| // reduced to avoid undervolt conditions. |
| if (target_opp.volt_uv < initial_opp.volt_uv) { |
| ZX_ASSERT(pwr_.is_valid()); |
| fidl::WireResult result = pwr_->RequestVoltage(target_opp.volt_uv); |
| if (!result.ok()) { |
| FDF_LOG(ERROR, "Failed to send RequestVoltage request: %s", result.status_string()); |
| return result.error().status(); |
| } |
| |
| if (result->is_error()) { |
| FDF_LOG(ERROR, "RequestVoltage call returned error: %s", |
| zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| |
| uint32_t actual_voltage = result->value()->actual_voltage; |
| if (actual_voltage != target_opp.volt_uv) { |
| FDF_LOG(ERROR, |
| "Failed to set cpu voltage, requested = %u, got = %u. " |
| "Voltage and frequency mismatch!", |
| target_opp.volt_uv, actual_voltage); |
| return ZX_OK; |
| } |
| } |
| |
| FDF_LOG(INFO, "Success\n"); |
| |
| current_operating_point_ = requested_opp; |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t AmlCpu::Init(fidl::ClientEnd<fuchsia_hardware_clock::Clock> plldiv16, |
| fidl::ClientEnd<fuchsia_hardware_clock::Clock> cpudiv16, |
| fidl::ClientEnd<fuchsia_hardware_clock::Clock> cpuscaler, |
| fidl::ClientEnd<fuchsia_hardware_power::Device> pwr) { |
| cpuscaler_.Bind(std::move(cpuscaler)); |
| |
| if (plldiv16.is_valid()) { |
| plldiv16_.Bind(std::move(plldiv16)); |
| |
| fidl::WireResult result = plldiv16_->Enable(); |
| if (!result.ok()) { |
| FDF_LOG(ERROR, "Failed to send request to enable plldiv16: %s", result.status_string()); |
| return result.status(); |
| } |
| if (result->is_error()) { |
| FDF_LOG(ERROR, "Failed to enable plldiv16: %s", zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| } |
| |
| if (cpudiv16.is_valid()) { |
| cpudiv16_.Bind(std::move(cpudiv16)); |
| |
| fidl::WireResult result = cpudiv16_->Enable(); |
| if (!result.ok()) { |
| FDF_LOG(ERROR, "Failed to send request to enable cpudiv16: %s", result.status_string()); |
| return result.status(); |
| } |
| if (result->is_error()) { |
| FDF_LOG(ERROR, "Failed to enable cpudiv16: %s", zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| } |
| |
| if (pwr.is_valid()) { |
| pwr_.Bind(std::move(pwr)); |
| |
| fidl::WireResult voltage_range_result = pwr_->GetSupportedVoltageRange(); |
| if (!voltage_range_result.ok()) { |
| FDF_LOG(ERROR, "Failed to send GetSupportedVoltageRange request: %s", |
| voltage_range_result.status_string()); |
| return voltage_range_result.status(); |
| } |
| |
| if (voltage_range_result->is_error()) { |
| FDF_LOG(ERROR, "GetSupportedVoltageRange returned error: %s", |
| zx_status_get_string(voltage_range_result->error_value())); |
| return voltage_range_result->error_value(); |
| } |
| |
| uint32_t max_voltage = voltage_range_result->value()->max; |
| uint32_t min_voltage = voltage_range_result->value()->min; |
| |
| fidl::WireResult register_result = pwr_->RegisterPowerDomain(min_voltage, max_voltage); |
| if (!register_result.ok()) { |
| FDF_LOG(ERROR, "Failed to send RegisterPowerDomain request: %s", |
| register_result.status_string()); |
| return voltage_range_result.status(); |
| } |
| |
| if (register_result->is_error()) { |
| FDF_LOG(ERROR, "RegisterPowerDomain returned error: %s", |
| zx_status_get_string(register_result->error_value())); |
| return register_result->error_value(); |
| } |
| } |
| |
| uint32_t actual; |
| // Returns ZX_ERR_OUT_OF_RANGE if `operating_points_` is empty. |
| zx_status_t result = SetCurrentOperatingPointInternal(kInitialOpp, &actual); |
| |
| if (result != ZX_OK) { |
| FDF_LOG(ERROR, "Failed to set initial opp, st = %d", result); |
| return result; |
| } |
| |
| if (actual != kInitialOpp) { |
| FDF_LOG(ERROR, "Failed to set initial opp, requested = %u, actual = %u", kInitialOpp, actual); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| return ZX_OK; |
| } |
| |
| void AmlCpu::SetCpuInfo(uint32_t cpu_version_packed) { |
| const uint8_t major_revision = (cpu_version_packed >> 24) & 0xff; |
| const uint8_t minor_revision = (cpu_version_packed >> 8) & 0xff; |
| const uint8_t cpu_package_id = (cpu_version_packed >> 20) & 0x0f; |
| FDF_LOG(INFO, "major revision number: 0x%x", major_revision); |
| FDF_LOG(INFO, "minor revision number: 0x%x", minor_revision); |
| FDF_LOG(INFO, "cpu package id number: 0x%x", cpu_package_id); |
| |
| cpu_info_.CreateUint("cpu_major_revision", major_revision, &inspector_); |
| cpu_info_.CreateUint("cpu_minor_revision", minor_revision, &inspector_); |
| cpu_info_.CreateUint("cpu_package_id", cpu_package_id, &inspector_); |
| } |
| |
| void AmlCpu::GetOperatingPointInfo(GetOperatingPointInfoRequestView request, |
| GetOperatingPointInfoCompleter::Sync& completer) { |
| auto operating_points = GetOperatingPoints(); |
| if (request->opp >= operating_points.size()) { |
| FDF_LOG(INFO, "Requested an operating point that's out of bounds, %u\n", request->opp); |
| completer.ReplyError(ZX_ERR_OUT_OF_RANGE); |
| return; |
| } |
| |
| fuchsia_hardware_cpu_ctrl::wire::CpuOperatingPointInfo result; |
| result.frequency_hz = operating_points[request->opp].freq_hz; |
| result.voltage_uv = operating_points[request->opp].volt_uv; |
| |
| completer.ReplySuccess(result); |
| } |
| |
| void AmlCpu::SetCurrentOperatingPoint(SetCurrentOperatingPointRequestView request, |
| SetCurrentOperatingPointCompleter::Sync& completer) { |
| uint32_t out_opp = 0; |
| zx_status_t status = SetCurrentOperatingPointInternal(request->requested_opp, &out_opp); |
| if (status != ZX_OK) { |
| completer.ReplyError(status); |
| } else { |
| completer.ReplySuccess(out_opp); |
| } |
| } |
| |
| void AmlCpu::GetCurrentOperatingPoint(GetCurrentOperatingPointCompleter::Sync& completer) { |
| completer.Reply(GetCurrentOperatingPoint()); |
| } |
| |
| void AmlCpu::GetOperatingPointCount(GetOperatingPointCountCompleter::Sync& completer) { |
| completer.ReplySuccess(GetOperatingPointCount()); |
| } |
| |
| void AmlCpu::GetNumLogicalCores(GetNumLogicalCoresCompleter::Sync& completer) { |
| completer.Reply(GetCoreCount()); |
| } |
| |
| void AmlCpu::GetLogicalCoreId(GetLogicalCoreIdRequestView request, |
| GetLogicalCoreIdCompleter::Sync& completer) { |
| // Placeholder. |
| completer.Reply(0); |
| } |
| |
| } // namespace amlogic_cpu |