blob: b71c6b85d76e4423a85695f4f21232288c8b4c53 [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 <ddk/device.h>
#include <fbl/auto_call.h>
#include <fbl/unique_ptr.h>
#include <soc/aml-common/aml-thermal.h>
#include <string.h>
#include <zircon/device/thermal.h>
#include <zircon/syscalls/port.h>
namespace thermal {
namespace {
// Worker-thread's internal loop deadline in seconds.
constexpr int kDeadline = 5;
} // namespace
zx_status_t AmlThermal::Create(zx_device_t* device) {
zxlogf(INFO, "aml_thermal: driver begin...\n");
zx_status_t status;
ddk::PDevProtocolClient pdev(device);
if (!pdev.is_valid()) {
THERMAL_ERROR("could not get platform device protocol\n");
return ZX_ERR_NO_RESOURCES;
}
size_t actual;
gpio_protocol_t fan0_gpio_proto;
status = pdev.GetProtocol(ZX_PROTOCOL_GPIO, FAN_CTL0, &fan0_gpio_proto, sizeof(fan0_gpio_proto),
&actual);
if (status != ZX_OK) {
THERMAL_ERROR("could not get fan0 gpio protocol: %d\n", status);
return status;
}
gpio_protocol_t fan1_gpio_proto;
status = pdev.GetProtocol(ZX_PROTOCOL_GPIO, FAN_CTL1, &fan1_gpio_proto, sizeof(fan1_gpio_proto),
&actual);
if (status != ZX_OK) {
THERMAL_ERROR("could not get fan1 gpio protocol: %d\n", status);
return status;
}
scpi_protocol_t scpi_proto;
status = pdev.GetProtocol(ZX_PROTOCOL_SCPI, 0, &scpi_proto, sizeof(scpi_proto), &actual);
if (status != ZX_OK) {
THERMAL_ERROR("could not get scpi protocol: %d\n", status);
return status;
}
ddk::ScpiProtocolClient scpi(&scpi_proto);
uint32_t sensor_id;
status = scpi.GetSensor("aml_thermal", &sensor_id);
if (status != ZX_OK) {
THERMAL_ERROR("could not thermal get sensor: %d\n", status);
return status;
}
zx::port port;
status = zx::port::create(0, &port);
if (status != ZX_OK) {
THERMAL_ERROR("could not configure port: %d\n", status);
return status;
}
auto thermal = fbl::make_unique<AmlThermal>(device, pdev, fan0_gpio_proto,
fan1_gpio_proto, scpi_proto, sensor_id, port);
status = thermal->DdkAdd("vim-thermal", DEVICE_ADD_INVISIBLE);
if (status != ZX_OK) {
THERMAL_ERROR("could not add driver: %d\n", status);
return status;
}
// Perform post-construction initialization before device is made visible.
status = thermal->Init();
if (status != ZX_OK) {
THERMAL_ERROR("could not initialize thermal driver: %d\n", status);
thermal->DdkRemove();
return status;
}
thermal->DdkMakeVisible();
// devmgr is now in charge of this device.
__UNUSED auto _ = thermal.release();
return ZX_OK;
}
zx_status_t AmlThermal::DdkIoctl(uint32_t op, const void* in_buf, size_t in_len, void* out_buf,
size_t out_len, size_t* actual) {
switch (op) {
// Input: None, Output: thermal_device_info_t.
case IOCTL_THERMAL_GET_DEVICE_INFO: {
if (out_len != sizeof(thermal_device_info_t)) {
return ZX_ERR_INVALID_ARGS;
}
memcpy(static_cast<thermal_device_info_t*>(out_buf), &info_,
sizeof(thermal_device_info_t));
*actual = sizeof(thermal_device_info_t);
return ZX_OK;
}
// Input: None, Output: zx_handle_t.
case IOCTL_THERMAL_GET_STATE_CHANGE_PORT: {
if (out_len != sizeof(zx_handle_t)) {
return ZX_ERR_INVALID_ARGS;
}
zx::port dup;
port_.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup);
*static_cast<zx_handle_t*>(out_buf) = dup.release();
*actual = sizeof(zx_handle_t);
return ZX_OK;
}
// Input: uint32_t, Output: None.
case IOCTL_THERMAL_SET_FAN_LEVEL: {
if (in_len != sizeof(uint32_t)) {
return ZX_ERR_INVALID_ARGS;
}
auto status = SetFanLevel(*static_cast<const FanLevel*>(in_buf));
if (status != ZX_OK) {
return status;
}
return ZX_OK;
}
// Input: None, Output: uint32_t.
case IOCTL_THERMAL_GET_FAN_LEVEL: {
if (out_len != sizeof(uint32_t)) {
return ZX_ERR_INVALID_ARGS;
}
*static_cast<uint32_t*>(out_buf) = fan_level_;
*actual = sizeof(uint32_t);
return ZX_OK;
}
// Input: uint32_t, Output: scpi_opp_t.
case IOCTL_THERMAL_GET_DVFS_INFO: {
if (in_len != sizeof(uint32_t) || out_len != sizeof(scpi_opp_t)) {
return ZX_ERR_INVALID_ARGS;
}
auto in = *static_cast<const uint8_t*>(in_buf);
if (in > MAX_DVFS_DOMAINS) {
return ZX_ERR_INVALID_ARGS;
}
scpi_opp_t opps;
auto status = scpi_.GetDvfsInfo(in, &opps);
if (status != ZX_OK) {
return status;
}
memcpy(static_cast<scpi_opp_t*>(out_buf), &opps,
sizeof(scpi_opp_t));
*actual = sizeof(scpi_opp_t);
return ZX_OK;
}
// Input: uint32_t, Output: uint32_t.
case IOCTL_THERMAL_GET_DVFS_OPP: {
if (in_len != sizeof(uint32_t) || out_len != sizeof(uint32_t)) {
return ZX_ERR_INVALID_ARGS;
}
auto in = *static_cast<const uint8_t*>(in_buf);
if (in == BIG_CLUSTER_POWER_DOMAIN) {
*static_cast<uint32_t*>(out_buf) = cur_bigcluster_opp_idx_;
} else if (in == LITTLE_CLUSTER_POWER_DOMAIN) {
*static_cast<uint32_t*>(out_buf) = cur_littlecluster_opp_idx_;
} else {
return ZX_ERR_INVALID_ARGS;
}
*actual = sizeof(uint32_t);
return ZX_OK;
}
// Input: dvfs_info_t, Output: None.
case IOCTL_THERMAL_SET_DVFS_OPP: {
if (in_len != sizeof(dvfs_info_t)) {
return ZX_ERR_INVALID_ARGS;
}
auto in = static_cast<const dvfs_info_t*>(in_buf);
bool set_new_opp = false;
if (in->power_domain == BIG_CLUSTER_POWER_DOMAIN) {
if (cur_bigcluster_opp_idx_ != in->op_idx) {
set_new_opp = true;
cur_bigcluster_opp_idx_ = in->op_idx;
}
} else {
if (cur_littlecluster_opp_idx_ != in->op_idx) {
set_new_opp = true;
cur_littlecluster_opp_idx_ = in->op_idx;
}
}
if (set_new_opp) {
return scpi_.SetDvfsIdx(static_cast<uint8_t>(in->power_domain), in->op_idx);
} else {
return ZX_OK;
}
}
// Input: None, Output: uint32_t.
case IOCTL_THERMAL_GET_TEMPERATURE: {
if (out_len != sizeof(uint32_t)) {
return ZX_ERR_INVALID_ARGS;
}
*static_cast<uint32_t*>(out_buf) = temperature_;
*actual = sizeof(uint32_t);
return ZX_OK;
}
default: {
return ZX_ERR_NOT_SUPPORTED;
}
}
return ZX_OK;
}
void AmlThermal::DdkRelease() {
if (worker_) {
const auto status = thrd_join(worker_, nullptr);
if (status != thrd_success) {
THERMAL_ERROR("worker thread failed: %d\n", status);
}
}
}
void AmlThermal::DdkUnbind() {
sync_completion_signal(&quit_);
}
zx_status_t AmlThermal::Init() {
auto status = fan0_gpio_.ConfigOut(0);
if (status != ZX_OK) {
THERMAL_ERROR("could not configure FAN_CTL0 gpio: %d\n", status);
return status;
}
status = fan1_gpio_.ConfigOut(0);
if (status != ZX_OK) {
THERMAL_ERROR("could not configure FAN_CTL1 gpio: %d\n", status);
return status;
}
size_t read;
status = DdkGetMetadata(DEVICE_METADATA_PRIVATE, &info_, sizeof(thermal_device_info_t), &read);
if (status != ZX_OK) {
THERMAL_ERROR("could not read device metadata: %d\n", status);
return status;
} else if (read != sizeof(thermal_device_info_t)) {
THERMAL_ERROR("could not read device metadata\n");
return ZX_ERR_NO_MEMORY;
}
status = scpi_.GetDvfsInfo(BIG_CLUSTER_POWER_DOMAIN, &info_.opps[0]);
if (status != ZX_OK) {
THERMAL_ERROR("could not get bigcluster dvfs opps: %d\n", status);
return status;
}
status = scpi_.GetDvfsInfo(LITTLE_CLUSTER_POWER_DOMAIN, &info_.opps[1]);
if (status != ZX_OK) {
THERMAL_ERROR("could not get littlecluster dvfs opps: %d\n", status);
return status;
}
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\n", 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\n", 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\n", status);
return status;
}
status = fan1_gpio_.Write(fan1_level);
if (status != ZX_OK) {
THERMAL_ERROR("could not set FAN_CTL1 level: %d\n", 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\n", status);
return status;
}
do {
status = scpi_.GetSensorValue(sensor_id_, &temperature_);
if (status != ZX_OK) {
THERMAL_ERROR("could not read temperature: %d\n", status);
return status;
}
signal = true;
if (trip_pt != trip_limit && temperature_ >= info_.trip_point_info[trip_pt + 1].up_temp) {
trip_pt++; // Triggered next trip point.
} else if (trip_pt && temperature_ < info_.trip_point_info[trip_pt].down_temp) {
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 && !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(BIG_CLUSTER_POWER_DOMAIN, 0);
if (status != ZX_OK) {
THERMAL_ERROR("unable to set DVFS OPP for Big cluster\n");
return status;
}
status = scpi_.SetDvfsIdx(LITTLE_CLUSTER_POWER_DOMAIN, 0);
if (status != ZX_OK) {
THERMAL_ERROR("unable to set DVFS OPP for Little cluster\n");
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\n", status);
return status;
}
}
} while (sync_completion_wait(&quit_, ZX_SEC(kDeadline)) == ZX_ERR_TIMED_OUT);
return ZX_OK;
}
} // namespace thermal
extern "C" zx_status_t aml_thermal_bind(void* ctx, zx_device_t* device) {
return thermal::AmlThermal::Create(device);
}