blob: 8389184d008f05b976b710c7e9084af27de33eb0 [file] [log] [blame]
// Copyright 2017 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 <cpuid.h>
#include <errno.h>
#include <fcntl.h>
#include <fidl/fuchsia.hardware.thermal/cpp/wire.h>
#include <fidl/fuchsia.kernel/cpp/wire.h>
#include <inttypes.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/fdio/watcher.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace-provider/provider.h>
#include <lib/trace/event.h>
#include <lib/zx/channel.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <zircon/syscalls.h>
#include <zircon/syscalls/system.h>
#include <future>
#include <fbl/unique_fd.h>
// degrees Celsius below threshold before we adjust PL value
constexpr float kCoolThresholdCelsius = 5.0f;
class PlatformConfiguration {
public:
static std::unique_ptr<PlatformConfiguration> Create(zx::resource);
zx_status_t SetMinPL1() { return SetPL1Mw(pl1_min_mw_); }
zx_status_t SetMaxPL1() { return SetPL1Mw(pl1_max_mw_); }
bool IsAtMax() { return current_pl1_mw_ == pl1_max_mw_; }
bool IsAtMin() { return current_pl1_mw_ == pl1_min_mw_; }
private:
PlatformConfiguration(uint32_t pl1_min_mw, uint32_t pl1_max_mw, zx::resource power_resource)
: pl1_min_mw_(pl1_min_mw),
pl1_max_mw_(pl1_max_mw),
power_resource_(std::move(power_resource)) {}
zx_status_t SetPL1Mw(uint32_t target_mw);
const uint32_t pl1_min_mw_;
const uint32_t pl1_max_mw_;
const zx::resource power_resource_;
static constexpr uint32_t kEvePL1MinMw = 2500;
static constexpr uint32_t kEvePL1MaxMw = 7000;
static constexpr uint32_t kAtlasPL1MinMw = 3000;
static constexpr uint32_t kAtlasPL1MaxMw = 7000;
uint32_t current_pl1_mw_;
};
zx::result<zx::resource> get_power_resource() {
zx::result client_end = component::Connect<fuchsia_kernel::PowerResource>();
if (client_end.is_error()) {
FX_PLOGS(ERROR, client_end.status_value()) << "Failed to open fuchsia.kernel.PowerResource";
return client_end.take_error();
}
fidl::WireSyncClient client{std::move(client_end.value())};
fidl::WireResult result = client->Get();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "FIDL error while trying to get power resource";
return zx::error(result.status());
}
return zx::ok(std::move(result.value().resource));
}
std::unique_ptr<PlatformConfiguration> PlatformConfiguration::Create(zx::resource power_resource) {
unsigned int a, b, c, d;
unsigned int leaf_num = 0x80000002;
char brand_string[50];
memset(brand_string, 0, sizeof(brand_string));
for (int i = 0; i < 3; i++) {
if (!__get_cpuid(leaf_num + i, &a, &b, &c, &d)) {
return nullptr;
}
memcpy(brand_string + (i * 16), &a, sizeof(uint32_t));
memcpy(brand_string + (i * 16) + 4, &b, sizeof(uint32_t));
memcpy(brand_string + (i * 16) + 8, &c, sizeof(uint32_t));
memcpy(brand_string + (i * 16) + 12, &d, sizeof(uint32_t));
}
// Only run thermd for processors used in Pixelbooks. The PL1 min/max settings are specified by
// the chipset.
if (strstr(brand_string, "i5-7Y57") || strstr(brand_string, "i7-7Y75")) {
return std::unique_ptr<PlatformConfiguration>(
new PlatformConfiguration(kEvePL1MinMw, kEvePL1MaxMw, std::move(power_resource)));
} else if (strstr(brand_string, "i5-8200Y") || strstr(brand_string, "i7-8500Y") ||
strstr(brand_string, "m3-8100Y")) {
return std::unique_ptr<PlatformConfiguration>(
new PlatformConfiguration(kAtlasPL1MinMw, kAtlasPL1MaxMw, std::move(power_resource)));
}
return nullptr;
}
zx_status_t PlatformConfiguration::SetPL1Mw(uint32_t target_mw) {
zx_system_powerctl_arg_t arg = {
.x86_power_limit =
{
.power_limit = target_mw,
.time_window = 0,
.clamp = 1,
.enable = 1,
},
};
zx_status_t status =
zx_system_powerctl(power_resource_.get(), ZX_SYSTEM_POWERCTL_X86_SET_PKG_PL1, &arg);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to set PL1 to " << target_mw;
return status;
}
current_pl1_mw_ = target_mw;
TRACE_COUNTER("thermal", "throttle", 0, "pl1", target_mw);
return ZX_OK;
}
void start_trace() {
// Create a message loop
static async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
static trace::TraceProviderWithFdio trace_provider(loop.dispatcher());
static bool started = false;
if (!started) {
loop.StartThread();
started = true;
}
}
// TODO(https://fxbug.dev/42059997): This code here needs an update, it's using some very old patterns.
zx_status_t RunThermd() {
zx::result power_resource = get_power_resource();
if (power_resource.is_error()) {
FX_PLOGS(ERROR, power_resource.status_value()) << "Failed to get power resource";
return power_resource.status_value();
}
auto config = PlatformConfiguration::Create(std::move(power_resource.value()));
if (!config) {
// If there is no platform configuration then we should warn since thermd should only be
// included on devices where we expect it to run.
FX_LOGS(WARNING) << "no platform configuration found";
return ZX_ERR_NOT_FOUND;
}
FX_LOGS(INFO) << "started";
start_trace();
zx_nanosleep(zx_deadline_after(ZX_SEC(3)));
fbl::unique_fd dirfd;
if (dirfd.reset(open("/dev/class/thermal", O_DIRECTORY | O_RDONLY)) < 0) {
FX_LOGS(ERROR) << "Failed to open /dev/class/thermal: " << strerror(errno);
return ZX_ERR_IO;
}
std::optional<zx::result<fidl::ClientEnd<fuchsia_hardware_thermal::Device>>> device;
if (zx_status_t status = fdio_watch_directory(
dirfd.get(),
[](int dirfd, int event, const char* name, void* cookie) {
if (event != WATCH_EVENT_ADD_FILE) {
return ZX_OK;
}
if (std::string_view{name} == ".") {
return ZX_OK;
}
// first sensor is ambient sensor
// TODO(https://fxbug.dev/42059997): come up with a way to detect this is the ambient sensor
auto& device = *static_cast<
std::optional<zx::result<fidl::ClientEnd<fuchsia_hardware_thermal::Device>>>*>(
cookie);
fdio_cpp::UnownedFdioCaller caller(dirfd);
device.emplace(
component::ConnectAt<fuchsia_hardware_thermal::Device>(caller.directory(), name));
return ZX_ERR_STOP;
},
ZX_TIME_INFINITE, &device);
status != ZX_ERR_STOP) {
FX_PLOGS(ERROR, status) << "watcher terminating without finding sensors, terminating thermd...";
return status;
}
if (!device.has_value()) {
FX_LOGS(ERROR) << "watcher did not find sensors, terminating thermd...";
return ZX_ERR_NOT_FOUND;
}
if (device.value().is_error()) {
FX_PLOGS(ERROR, device.value().status_value()) << "failed to connect to sensor";
return device.value().status_value();
}
fidl::WireSyncClient client{std::move(device.value().value())};
float temp;
{
const fidl::WireResult result = client->GetTemperatureCelsius();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get temperature";
return result.status();
}
const fidl::WireResponse response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get temperature";
return status;
}
temp = response.temp;
TRACE_COUNTER("thermal", "temp", 0, "ambient-c", temp);
}
fuchsia_hardware_thermal::wire::ThermalInfo info;
{
const fidl::WireResult result = client->GetInfo();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get info";
return result.status();
}
const fidl::WireResponse response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get info";
return status;
}
info = *response.info;
TRACE_COUNTER("thermal", "trip-point", 0, "passive-c", info.passive_temp_celsius, "critical-c",
info.critical_temp_celsius);
if (info.max_trip_count == 0) {
FX_LOGS(ERROR) << "Trip points not supported, exiting";
return ZX_ERR_NOT_SUPPORTED;
}
}
zx::event h;
{
fidl::WireResult result = client->GetStateChangeEvent();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get state change event";
return result.status();
}
auto& response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get state change event";
return status;
}
h = std::move(response.handle);
}
// Set a trip point.
{
const fidl::WireResult result = client->SetTripCelsius(0, info.passive_temp_celsius);
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to set trip point";
return result.status();
}
const auto& response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to set trip point";
return status;
}
}
// Update info.
{
const fidl::WireResult result = client->GetInfo();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get info";
return result.status();
}
const fidl::WireResponse response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get info";
return status;
}
info = *response.info;
TRACE_COUNTER("thermal", "trip-point", 0, "passive-c", info.passive_temp_celsius, "critical-c",
info.critical_temp_celsius, "active0-c", info.active_trip[0]);
}
// Set PL1 to the platform maximum.
config->SetMaxPL1();
for (;;) {
zx_signals_t observed = 0;
zx_status_t status = h.wait_one(ZX_USER_SIGNAL_0, zx::deadline_after(zx::sec(1)), &observed);
if ((status != ZX_OK) && (status != ZX_ERR_TIMED_OUT)) {
FX_PLOGS(ERROR, status) << "Failed to wait on event";
return status;
}
if (observed & ZX_USER_SIGNAL_0) {
const fidl::WireResult result = client->GetInfo();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get info";
return result.status();
}
const fidl::WireResponse response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get info";
return status;
}
info = *response.info;
if (info.state) {
// Decrease power limit
config->SetMinPL1();
const fidl::WireResult result = client->GetTemperatureCelsius();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get temperature";
return result.status();
}
const fidl::WireResponse response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get temperature";
return status;
}
temp = response.temp;
} else {
TRACE_COUNTER("thermal", "event", 0, "spurious", temp);
}
}
if (status == ZX_ERR_TIMED_OUT) {
const fidl::WireResult result = client->GetTemperatureCelsius();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get temperature";
return result.status();
}
const fidl::WireResponse response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get temperature";
return status;
}
temp = response.temp;
TRACE_COUNTER("thermal", "temp", 0, "ambient-c", temp);
// Increase power limit if the temperature dropped enough
if (temp < info.active_trip[0] - kCoolThresholdCelsius && !config->IsAtMax()) {
// Make sure the state is clear
const fidl::WireResult result = client->GetInfo();
if (!result.ok()) {
FX_PLOGS(ERROR, result.status()) << "Failed to get info";
return result.status();
}
const fidl::WireResponse response = result.value();
if (zx_status_t status = response.status; status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failed to get info";
return status;
}
info = *response.info;
if (!info.state) {
config->SetMaxPL1();
}
}
if (temp > info.active_trip[0] && !config->IsAtMin()) {
// Decrease power limit
config->SetMinPL1();
}
}
}
// Do not return so that the compiler will catch it if this becomes reachable.
}
int main(int argc, char** argv) {
zx_status_t status = RunThermd();
// RunThermd never returns successfully, so always treat this as an error path.
FX_LOGS(ERROR) << "Exited with status: " << zx_status_get_string(status);
// TODO(https://fxbug.dev/42179909): Hang around. If we exit before archivist has started, our logs
// will be lost, and it's important that we know that thermd is failing and why.
std::promise<void>().get_future().wait();
return -1;
}