blob: 6d6f11b3f69b487b4e7ab18247ad74a1e84868cd [file] [log] [blame]
// Copyright 2018 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-thermal.h"
#include <string.h>
#include <zircon/syscalls/port.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/metadata.h>
#include <ddk/platform-defs.h>
#include <ddktl/protocol/composite.h>
#include <fbl/auto_call.h>
#include <soc/aml-common/aml-thermal.h>
#include "src/devices/thermal/drivers/aml-thermal-s912/aml-thermal-s912-bind.h"
#define THERMAL_ERROR(fmt, ...) zxlogf(ERROR, "[%s %d]" fmt, __func__, __LINE__, ##__VA_ARGS__)
namespace thermal {
namespace {
enum {
FRAGMENT_SCPI,
FRAGMENT_GPIO_FAN_0,
FRAGMENT_GPIO_FAN_1,
FRAGMENT_COUNT,
};
fuchsia_hardware_thermal_OperatingPoint ScpiToThermalOpps(const scpi_opp_t& opps) {
fuchsia_hardware_thermal_OperatingPoint thermal_opps = {
.opp = {},
.latency = opps.latency,
.count = opps.count,
};
for (uint32_t i = 0; i < opps.count; i++) {
thermal_opps.opp[i] = {
.freq_hz = opps.opp[i].freq_hz,
.volt_uv = opps.opp[i].volt_uv,
};
}
return thermal_opps;
}
} // namespace
zx_status_t AmlThermal::Create(void* ctx, zx_device_t* device) {
zxlogf(INFO, "aml_thermal: driver begin...");
ddk::CompositeProtocolClient composite(device);
if (!composite.is_valid()) {
THERMAL_ERROR("could not get composite protocol");
return ZX_ERR_NOT_SUPPORTED;
}
zx_device_t* scpi_dev = nullptr;
bool found = composite.GetFragment("scpi", &scpi_dev);
if (!found) {
THERMAL_ERROR("could not get scpi fragment");
return ZX_ERR_NO_RESOURCES;
}
ddk::ScpiProtocolClient scpi(scpi_dev);
if (!scpi.is_valid()) {
THERMAL_ERROR("could not get scpi protocol");
return ZX_ERR_NO_RESOURCES;
}
ddk::GpioProtocolClient fan0_gpio(composite, "gpio-fan0");
if (!fan0_gpio.is_valid()) {
THERMAL_ERROR("could not get fan0 gpio protocol");
return ZX_ERR_NO_RESOURCES;
}
ddk::GpioProtocolClient fan1_gpio(composite, "gpio-fan1");
if (!fan1_gpio.is_valid()) {
THERMAL_ERROR("could not get fan0 gpio protocol");
return ZX_ERR_NO_RESOURCES;
}
uint32_t sensor_id;
zx_status_t status = scpi.GetSensor("aml_thermal", &sensor_id);
if (status != ZX_OK) {
THERMAL_ERROR("could not thermal get sensor: %d", status);
return status;
}
zx::port port;
status = zx::port::create(0, &port);
if (status != ZX_OK) {
THERMAL_ERROR("could not configure port: %d", status);
return status;
}
auto thermal = std::make_unique<AmlThermal>(device, fan0_gpio, fan1_gpio, scpi,
sensor_id, std::move(port), scpi_dev);
status = thermal->DdkAdd("vim-thermal");
if (status != ZX_OK) {
THERMAL_ERROR("could not add driver: %d", status);
return status;
}
// devmgr is now in charge of this device.
__UNUSED auto _ = thermal.release();
return ZX_OK;
}
zx_status_t AmlThermal::ThermalConnect(zx::channel chan) { return ZX_ERR_NOT_SUPPORTED; }
zx_status_t AmlThermal::DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn) {
return fuchsia_hardware_thermal_Device_dispatch(this, txn, msg, &fidl_ops);
}
zx_status_t AmlThermal::GetInfo(fidl_txn_t* txn) {
return fuchsia_hardware_thermal_DeviceGetInfo_reply(txn, ZX_ERR_NOT_SUPPORTED, nullptr);
}
zx_status_t AmlThermal::GetDeviceInfo(fidl_txn_t* txn) {
return fuchsia_hardware_thermal_DeviceGetDeviceInfo_reply(txn, ZX_OK, &info_);
}
zx_status_t AmlThermal::GetDvfsInfo(fuchsia_hardware_thermal_PowerDomain power_domain,
fidl_txn_t* txn) {
if (power_domain >= fuchsia_hardware_thermal_MAX_DVFS_DOMAINS) {
return fuchsia_hardware_thermal_DeviceGetDvfsInfo_reply(txn, ZX_ERR_INVALID_ARGS, nullptr);
}
scpi_opp_t opps = {};
auto status = scpi_.GetDvfsInfo(static_cast<uint8_t>(power_domain), &opps);
const fuchsia_hardware_thermal_OperatingPoint thermal_opps = ScpiToThermalOpps(opps);
return fuchsia_hardware_thermal_DeviceGetDvfsInfo_reply(txn, status, &thermal_opps);
}
zx_status_t AmlThermal::GetTemperatureCelsius(fidl_txn_t* txn) {
return fuchsia_hardware_thermal_DeviceGetTemperatureCelsius_reply(txn, ZX_OK, temperature_);
}
zx_status_t AmlThermal::GetStateChangeEvent(fidl_txn_t* txn) {
return fuchsia_hardware_thermal_DeviceGetStateChangeEvent_reply(txn, ZX_ERR_NOT_SUPPORTED,
ZX_HANDLE_INVALID);
}
zx_status_t AmlThermal::GetStateChangePort(fidl_txn_t* txn) {
zx::port dup;
zx_status_t status = port_.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup);
return fuchsia_hardware_thermal_DeviceGetStateChangePort_reply(txn, status, dup.release());
}
zx_status_t AmlThermal::SetTripCelsius(uint32_t id, float temp, fidl_txn_t* txn) {
return fuchsia_hardware_thermal_DeviceSetTripCelsius_reply(txn, ZX_ERR_NOT_SUPPORTED);
}
zx_status_t AmlThermal::GetDvfsOperatingPoint(fuchsia_hardware_thermal_PowerDomain power_domain,
fidl_txn_t* txn) {
if (power_domain == fuchsia_hardware_thermal_PowerDomain_BIG_CLUSTER_POWER_DOMAIN) {
return fuchsia_hardware_thermal_DeviceGetDvfsOperatingPoint_reply(
txn, ZX_OK, static_cast<uint16_t>(cur_bigcluster_opp_idx_));
} else if (power_domain == fuchsia_hardware_thermal_PowerDomain_LITTLE_CLUSTER_POWER_DOMAIN) {
return fuchsia_hardware_thermal_DeviceGetDvfsOperatingPoint_reply(
txn, ZX_OK, static_cast<uint16_t>(cur_littlecluster_opp_idx_));
}
return fuchsia_hardware_thermal_DeviceGetDvfsOperatingPoint_reply(txn, ZX_ERR_INVALID_ARGS, 0);
}
zx_status_t AmlThermal::SetDvfsOperatingPoint(uint16_t op_idx,
fuchsia_hardware_thermal_PowerDomain power_domain,
fidl_txn_t* txn) {
zx_status_t status = ZX_OK;
if (power_domain == fuchsia_hardware_thermal_PowerDomain_BIG_CLUSTER_POWER_DOMAIN) {
if (op_idx != cur_bigcluster_opp_idx_) {
status = scpi_.SetDvfsIdx(static_cast<uint8_t>(power_domain), op_idx);
}
cur_bigcluster_opp_idx_ = op_idx;
} else if (power_domain == fuchsia_hardware_thermal_PowerDomain_LITTLE_CLUSTER_POWER_DOMAIN) {
if (op_idx != cur_littlecluster_opp_idx_) {
status = scpi_.SetDvfsIdx(static_cast<uint8_t>(power_domain), op_idx);
}
cur_littlecluster_opp_idx_ = op_idx;
} else {
status = ZX_ERR_INVALID_ARGS;
}
return fuchsia_hardware_thermal_DeviceSetDvfsOperatingPoint_reply(txn, status);
}
zx_status_t AmlThermal::GetFanLevel(fidl_txn_t* txn) {
return fuchsia_hardware_thermal_DeviceGetFanLevel_reply(txn, ZX_OK, fan_level_);
}
zx_status_t AmlThermal::SetFanLevel(uint32_t fan_level, fidl_txn_t* txn) {
return fuchsia_hardware_thermal_DeviceSetFanLevel_reply(
txn, SetFanLevel(static_cast<FanLevel>(fan_level)));
}
void AmlThermal::JoinWorkerThread() {
const auto status = thrd_join(worker_, nullptr);
if (status != thrd_success) {
THERMAL_ERROR("worker thread failed: %d", status);
}
}
void AmlThermal::DdkRelease() {
if (worker_) {
JoinWorkerThread();
}
delete this;
}
void AmlThermal::DdkUnbind(ddk::UnbindTxn txn) {
sync_completion_signal(&quit_);
txn.Reply();
}
void AmlThermal::DdkInit(ddk::InitTxn txn) {
zx_status_t status = Init(scpi_dev_);
txn.Reply(status);
}
zx_status_t AmlThermal::Init(zx_device_t* dev) {
auto status = fan0_gpio_.ConfigOut(0);
if (status != ZX_OK) {
THERMAL_ERROR("could not configure FAN_CTL0 gpio: %d", status);
return status;
}
status = fan1_gpio_.ConfigOut(0);
if (status != ZX_OK) {
THERMAL_ERROR("could not configure FAN_CTL1 gpio: %d", status);
return status;
}
size_t read;
status = device_get_metadata(dev, DEVICE_METADATA_THERMAL_CONFIG, &info_,
sizeof(fuchsia_hardware_thermal_ThermalDeviceInfo), &read);
if (status != ZX_OK) {
THERMAL_ERROR("could not read device metadata: %d", status);
return status;
} else if (read != sizeof(fuchsia_hardware_thermal_ThermalDeviceInfo)) {
THERMAL_ERROR("could not read device metadata");
return ZX_ERR_NO_MEMORY;
}
scpi_opp_t opps;
status = scpi_.GetDvfsInfo(fuchsia_hardware_thermal_PowerDomain_BIG_CLUSTER_POWER_DOMAIN, &opps);
if (status != ZX_OK) {
THERMAL_ERROR("could not get bigcluster dvfs opps: %d", status);
return status;
}
info_.opps[0] = ScpiToThermalOpps(opps);
status =
scpi_.GetDvfsInfo(fuchsia_hardware_thermal_PowerDomain_LITTLE_CLUSTER_POWER_DOMAIN, &opps);
if (status != ZX_OK) {
THERMAL_ERROR("could not get littlecluster dvfs opps: %d", status);
return status;
}
info_.opps[1] = ScpiToThermalOpps(opps);
auto start_thread = [](void* arg) { return static_cast<AmlThermal*>(arg)->Worker(); };
status = thrd_create_with_name(&worker_, start_thread, this, "aml_thermal_notify_thread");
if (status != ZX_OK) {
THERMAL_ERROR("could not start worker thread: %d", status);
return status;
}
return ZX_OK;
}
zx_status_t AmlThermal::NotifyThermalDaemon(uint32_t trip_index) const {
zx_port_packet_t pkt;
pkt.key = trip_index;
pkt.type = ZX_PKT_TYPE_USER;
return port_.queue(&pkt);
}
zx_status_t AmlThermal::SetFanLevel(FanLevel level) {
// Levels per individual system fan.
uint8_t fan0_level;
uint8_t fan1_level;
switch (level) {
case FAN_L0:
fan0_level = 0;
fan1_level = 0;
break;
case FAN_L1:
fan0_level = 1;
fan1_level = 0;
break;
case FAN_L2:
fan0_level = 0;
fan1_level = 1;
break;
case FAN_L3:
fan0_level = 1;
fan1_level = 1;
break;
default:
THERMAL_ERROR("unknown fan level: %d", level);
return ZX_ERR_INVALID_ARGS;
}
auto status = fan0_gpio_.Write(fan0_level);
if (status != ZX_OK) {
THERMAL_ERROR("could not set FAN_CTL0 level: %d", status);
return status;
}
status = fan1_gpio_.Write(fan1_level);
if (status != ZX_OK) {
THERMAL_ERROR("could not set FAN_CTL1 level: %d", status);
return status;
}
fan_level_ = level;
return ZX_OK;
}
int AmlThermal::Worker() {
zx_status_t status;
uint32_t trip_pt = 0;
const uint32_t trip_limit = info_.num_trip_points - 1;
bool crit = false;
bool signal = false;
// Notify thermal daemon of initial settings.
status = NotifyThermalDaemon(trip_pt);
if (status != ZX_OK) {
THERMAL_ERROR("could not notify thermal daemon: %d", status);
return status;
}
do {
uint32_t temp_integer = 0;
status = scpi_.GetSensorValue(sensor_id_, &temp_integer);
if (status != ZX_OK) {
THERMAL_ERROR("could not read temperature: %d", status);
return status;
}
temperature_ = static_cast<float>(temp_integer);
signal = true;
if (trip_pt != trip_limit &&
temperature_ >= info_.trip_point_info[trip_pt + 1].up_temp_celsius) {
trip_pt++; // Triggered next trip point.
} else if (trip_pt && temperature_ < info_.trip_point_info[trip_pt].down_temp_celsius) {
if (trip_pt == trip_limit) {
// A prev trip point triggered, so the temperature is falling
// down below the critical temperature. Make a note of that.
crit = false;
}
trip_pt--; // Triggered prev trip point.
} else if (trip_pt == trip_limit && temperature_ >= info_.critical_temp_celsius && !crit) {
// The device temperature is crossing the critical temperature, set
// the CPU freq to the lowest possible setting to ensure the
// temperature doesn't rise any further.
crit = true;
status = scpi_.SetDvfsIdx(fuchsia_hardware_thermal_PowerDomain_BIG_CLUSTER_POWER_DOMAIN, 0);
if (status != ZX_OK) {
THERMAL_ERROR("unable to set DVFS OPP for Big cluster");
return status;
}
status =
scpi_.SetDvfsIdx(fuchsia_hardware_thermal_PowerDomain_LITTLE_CLUSTER_POWER_DOMAIN, 0);
if (status != ZX_OK) {
THERMAL_ERROR("unable to set DVFS OPP for Little cluster");
return status;
}
} else {
signal = false;
}
if (signal) {
// Notify the thermal daemon about which trip point triggered.
status = NotifyThermalDaemon(trip_pt);
if (status != ZX_OK) {
THERMAL_ERROR("could not notify thermal daemon: %d", status);
return status;
}
}
} while (sync_completion_wait(&quit_, duration_.get()) == ZX_ERR_TIMED_OUT);
return ZX_OK;
}
static constexpr zx_driver_ops_t driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = AmlThermal::Create;
return ops;
}();
} // namespace thermal
ZIRCON_DRIVER(aml_thermal, thermal::driver_ops, "zircon", "0.1");