android13/u-boot/drivers/power/dvfs/rockchip_wtemp_dvfs.c

640 lines
16 KiB
C

// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2018 Fuzhou Rockchip Electronics Co., Ltd
*/
#include <common.h>
#include <dm.h>
#include <clk.h>
#include <dvfs.h>
#include <thermal.h>
#include <linux/list.h>
#include <asm/arch/clock.h>
#include <power/regulator.h>
#ifdef CONFIG_ROCKCHIP_DMC
#include <asm/arch/rockchip_dmc.h>
#endif
/*
* # This is a simple wide temperature(ie. wtemp) dvfs driver, the policy is:
*
* 1. U-Boot parse cpu/dmc opp table from kernel dtb, anyone of
* "rockchip,low-temp = <...>" and "rockchip,high-temp = <...>" present in
* cpu/dmc nodes means wtemp is enabled.
*
* 1.1. When temperature trigger "rockchip,low-temp", increase 50mv voltage
* as target voltage. If target voltage is over "rockchip,max-volt",
* just set "rockchip,max-volt" as target voltage and lower 2 level freq,
*
* 1.2. When temperature trigger "rockchip,high-temp", just apply opp table[0]
* voltage and freq.
*
* 2. U-Boot parse cpu/dmc thermal zone "trip-point-0" temperature from kernel
* dtb, and apply the same rules as above [1.2] policy.
*
*
* # The dvfs policy apply moment is:
*
* 1. Appy it after clk and regulator drivers setup;
* 2. Repeat apply it by CONFIG_PREBOOT command until achieve the target
* temperature. user should add: #define CONFIG_PREBOOT "dvfs repeat" and
* assign repeat property in dts:
*
* uboot-wide-temperature {
* status = "okay";
* compatible = "rockchip,uboot-wide-temperature";
*
* cpu,low-temp-repeat;
* cpu,high-temp-repeat;
* dmc,low-temp-repeat;
* dmc,high-temp-repeat;
* };
*/
#define FDT_PATH_CPUS "/cpus"
#define FDT_PATH_DMC "/dmc"
#define FDT_PATH_THREMAL_TRIP_POINT0 \
"/thermal-zones/soc-thermal/trips/trip-point-0"
#define FDT_PATH_THREMAL_COOLING_MAPS \
"/thermal-zones/soc-thermal/cooling-maps"
#define OPP_TABLE_MAX 20
#define RATE_LOWER_LEVEL_N 2
#define DIFF_VOLTAGE_UV 50000
#define TEMP_STRING_LEN 12
#define REPEAT_PERIOD_US 1000000
static LIST_HEAD(pm_e_head);
enum pm_id {
PM_CPU,
PM_DMC,
};
enum pm_event {
PM_EVT_NONE = 0x0,
PM_EVT_LOW = 0x1,
PM_EVT_HIGH = 0x2,
PM_EVT_BOTH = PM_EVT_LOW | PM_EVT_HIGH,
};
struct opp_table {
u64 hz;
u32 uv;
};
struct lmt_param {
int low_temp; /* milli degree */
int high_temp; /* milli degree */
int tz_temp; /* milli degree */
int max_volt; /* uV */
bool htemp_repeat;
bool ltemp_repeat;
bool ltemp_limit;
bool htemp_limit;
bool tztemp_limit;
};
struct pm_element {
int id;
const char *name;
const char *supply_name;
int volt_diff;
u32 opp_nr;
struct opp_table opp[OPP_TABLE_MAX];
struct lmt_param lmt;
struct udevice *supply;
struct clk clk;
struct list_head node;
};
struct wtemp_dvfs_priv {
struct udevice *thermal;
struct pm_element *cpu;
struct pm_element *dmc;
};
static struct pm_element pm_cpu = {
.id = PM_CPU,
.name = "cpu",
.supply_name = "cpu-supply",
.volt_diff = DIFF_VOLTAGE_UV,
};
static struct pm_element pm_dmc = {
.id = PM_DMC,
.name = "dmc",
.supply_name = "center-supply",
.volt_diff = DIFF_VOLTAGE_UV,
};
static void temp2string(int temp, char *data, int len)
{
int decimal_point;
int integer;
integer = abs(temp) / 1000;
decimal_point = abs(temp) % 1000;
snprintf(data, len, "%s%d.%d",
temp < 0 ? "-" : "", integer, decimal_point);
}
static ulong wtemp_get_lowlevel_rate(ulong rate, u32 level,
struct pm_element *e)
{
struct opp_table *opp;
int i, count, idx = 0;
opp = e->opp;
count = e->opp_nr;
for (i = 0; i < count; i++) {
if (opp[i].hz >= rate) {
idx = (i <= level) ? 0 : i - level;
break;
}
}
return opp[idx].hz;
}
static ulong __wtemp_clk_get_rate(struct pm_element *e)
{
#ifdef CONFIG_ROCKCHIP_DMC
if (e->id == PM_DMC)
return rockchip_ddrclk_sip_recalc_rate_v2();
#endif
return clk_get_rate(&e->clk);
}
static ulong __wtemp_clk_set_rate(struct pm_element *e, ulong rate)
{
#ifdef CONFIG_ROCKCHIP_DMC
if (e->id == PM_DMC) {
rate = rockchip_ddrclk_sip_round_rate_v2(rate);
rockchip_ddrclk_sip_set_rate_v2(rate);
} else
#endif
clk_set_rate(&e->clk, rate);
return rate;
}
static int __wtemp_regulator_get_value(struct pm_element *e)
{
return regulator_get_value(e->supply);
}
static int __wtemp_regulator_set_value(struct pm_element *e, int value)
{
return regulator_set_value(e->supply, value);
}
/*
* Policy: Increase voltage
*
* 1. target volt = original volt + diff volt;
* 2. If target volt is not over max_volt, just set it;
* 3. Otherwise set max_volt as target volt and lower the rate(front N level).
*/
static void wtemp_dvfs_low_temp_adjust(struct udevice *dev, struct pm_element *e)
{
struct wtemp_dvfs_priv *priv = dev_get_priv(dev);
ulong org_rate, tgt_rate, rb_rate;
int org_volt, tgt_volt, rb_volt;
org_rate = __wtemp_clk_get_rate(e);
org_volt = __wtemp_regulator_get_value(e);
tgt_volt = org_volt + e->volt_diff;
if ((e->lmt.max_volt != -ENODATA) && (tgt_volt > e->lmt.max_volt)) {
tgt_volt = e->lmt.max_volt;
__wtemp_regulator_set_value(e, tgt_volt);
tgt_rate = wtemp_get_lowlevel_rate(org_rate,
RATE_LOWER_LEVEL_N, priv->cpu);
tgt_rate = __wtemp_clk_set_rate(e, tgt_rate);
} else {
__wtemp_regulator_set_value(e, tgt_volt);
tgt_rate = org_rate;
}
/* Check */
rb_rate = __wtemp_clk_get_rate(e);
rb_volt = __wtemp_regulator_get_value(e);
if (tgt_rate != rb_rate)
printf("DVFS: %s: target rate=%ld, readback rate=%ld !\n",
e->name, tgt_rate, rb_rate);
if (tgt_volt != rb_volt)
printf("DVFS: %s: target volt=%d, readback volt=%d !\n",
e->name, tgt_volt, rb_volt);
printf("DVFS: %s(low): %ld->%ld Hz, %d->%d uV\n",
e->name, org_rate, rb_rate, org_volt, rb_volt);
}
/*
* Policy:
*
* Just set opp table[0] volt and rate, i.e. the lowest performance.
*/
static void wtemp_dvfs_high_temp_adjust(struct udevice *dev, struct pm_element *e)
{
ulong org_rate, tgt_rate, rb_rate;
int org_volt, tgt_volt, rb_volt;
/* Apply opp[0] */
org_rate = __wtemp_clk_get_rate(e);
tgt_rate = e->opp[0].hz;
tgt_rate = __wtemp_clk_set_rate(e, tgt_rate);
org_volt = __wtemp_regulator_get_value(e);
tgt_volt = e->opp[0].uv;
__wtemp_regulator_set_value(e, tgt_volt);
/* Check */
rb_rate = __wtemp_clk_get_rate(e);
rb_volt = __wtemp_regulator_get_value(e);
if (tgt_rate != rb_rate)
printf("DVFS: %s: target rate=%ld, readback rate=%ld !\n",
e->name, tgt_rate, rb_rate);
if (tgt_volt != rb_volt)
printf("DVFS: %s: target volt=%d, readback volt=%d !\n",
e->name, tgt_volt, rb_volt);
printf("DVFS: %s(high): %ld->%ld Hz, %d->%d uV\n",
e->name, org_rate, tgt_rate, org_volt, tgt_volt);
}
static bool wtemp_dvfs_is_effect(struct pm_element *e,
int temp, enum pm_event evt)
{
if (evt & PM_EVT_LOW) {
if (e->lmt.ltemp_limit && temp <= e->lmt.low_temp)
return false;
}
if (evt & PM_EVT_HIGH) {
if (e->lmt.tztemp_limit && temp >= e->lmt.tz_temp)
return false;
else if (e->lmt.htemp_limit && temp >= e->lmt.high_temp)
return false;
}
return true;
}
static int __wtemp_dvfs_apply(struct udevice *dev, struct pm_element *e,
int temp, enum pm_event evt)
{
enum pm_event ret = PM_EVT_NONE;
if (evt & PM_EVT_LOW) {
/* Over lowest temperature: increase voltage */
if (e->lmt.ltemp_limit && temp <= e->lmt.low_temp) {
ret |= PM_EVT_LOW;
wtemp_dvfs_low_temp_adjust(dev, e);
}
}
if (evt & PM_EVT_HIGH) {
/* Over highest/thermal_zone temperature: decrease rate and voltage */
if (e->lmt.tztemp_limit && temp >= e->lmt.tz_temp) {
ret |= PM_EVT_HIGH;
wtemp_dvfs_high_temp_adjust(dev, e);
} else if (e->lmt.htemp_limit && temp >= e->lmt.high_temp) {
ret |= PM_EVT_HIGH;
wtemp_dvfs_high_temp_adjust(dev, e);
}
}
return ret;
}
static int __wtemp_common_ofdata_to_platdata(ofnode node, struct pm_element *e)
{
ofnode supply, opp_node;
u32 phandle, uv, clock[2];
uint64_t hz;
int ret;
/* Get regulator and clk */
if (!ofnode_read_u32(node, e->supply_name, &phandle)) {
supply = ofnode_get_by_phandle(phandle);
ret = regulator_get_by_devname(supply.np->name, &e->supply);
if (ret) {
printf("DVFS: %s: Get supply(%s) failed, ret=%d",
e->name, supply.np->full_name, ret);
return ret;
}
debug("DVFS: supply: %s\n", supply.np->full_name);
}
if (!ofnode_read_u32_array(node, "clocks", clock, ARRAY_SIZE(clock))) {
e->clk.id = clock[1];
ret = rockchip_get_clk(&e->clk.dev);
if (ret) {
printf("DVFS: %s: Get clk failed, ret=%d\n", e->name, ret);
return ret;
}
}
/* Get opp-table & limit param */
if (!ofnode_read_u32(node, "operating-points-v2", &phandle)) {
opp_node = ofnode_get_by_phandle(phandle);
e->lmt.low_temp = ofnode_read_s32_default(opp_node,
"rockchip,low-temp", -ENODATA);
e->lmt.high_temp = ofnode_read_s32_default(opp_node,
"rockchip,high-temp", -ENODATA);
e->lmt.max_volt = ofnode_read_u32_default(opp_node,
"rockchip,max-volt", -ENODATA);
debug("DVFS: %s: low-temp=%d, high-temp=%d, max-volt=%d\n",
e->name, e->lmt.low_temp, e->lmt.high_temp,
e->lmt.max_volt);
ofnode_for_each_subnode(node, opp_node) {
if (e->opp_nr >= OPP_TABLE_MAX) {
printf("DVFS: over max(%d) opp table items\n",
OPP_TABLE_MAX);
break;
}
ofnode_read_u64(node, "opp-hz", &hz);
ofnode_read_u32_array(node, "opp-microvolt", &uv, 1);
e->opp[e->opp_nr].hz = hz;
e->opp[e->opp_nr].uv = uv;
e->opp_nr++;
debug("DVFS: %s: opp[%d]: hz=%lld, uv=%d, %s\n",
e->name, e->opp_nr - 1,
hz, uv, ofnode_get_name(node));
}
}
if (!e->opp_nr) {
printf("DVFS: %s: Can't find opp table\n", e->name);
return -EINVAL;
}
if (e->lmt.max_volt == -ENODATA)
e->lmt.max_volt = e->opp[e->opp_nr - 1].uv;
if (e->lmt.low_temp != -ENODATA)
e->lmt.ltemp_limit = true;
if (e->lmt.high_temp != -ENODATA)
e->lmt.htemp_limit = true;
return 0;
}
static int wtemp_dvfs_apply(struct udevice *dev)
{
struct wtemp_dvfs_priv *priv = dev_get_priv(dev);
struct list_head *node;
struct pm_element *e;
char s_temp[TEMP_STRING_LEN];
int temp, ret;
ret = thermal_get_temp(priv->thermal, &temp);
if (ret) {
printf("DVFS: Get temperature failed, ret=%d\n", ret);
return ret;
}
temp2string(temp, s_temp, TEMP_STRING_LEN);
printf("DVFS: %s'c\n", s_temp);
/* Apply dvfs policy for all pm element */
list_for_each(node, &pm_e_head) {
e = list_entry(node, struct pm_element, node);
__wtemp_dvfs_apply(dev, e, temp, PM_EVT_BOTH);
}
return 0;
}
static int wtemp_dvfs_repeat_apply(struct udevice *dev)
{
struct wtemp_dvfs_priv *priv = dev_get_priv(dev);
struct list_head *node;
struct pm_element *e;
enum pm_event applied;
char s_temp[TEMP_STRING_LEN];
int temp, ret;
repeat:
ret = thermal_get_temp(priv->thermal, &temp);
if (ret) {
printf("DVFS: Get thermal temperature failed, ret=%d\n", ret);
return false;
}
/* Apply dvfs policy for all pm element if there is repeat request */
applied = PM_EVT_NONE;
list_for_each(node, &pm_e_head) {
e = list_entry(node, struct pm_element, node);
if (e->lmt.ltemp_repeat)
applied |= __wtemp_dvfs_apply(dev, e, temp, PM_EVT_LOW);
if (e->lmt.htemp_repeat)
applied |= __wtemp_dvfs_apply(dev, e, temp, PM_EVT_HIGH);
}
/* Everything is fine, exit */
if (applied == PM_EVT_NONE)
goto finish;
/* Check repeat result */
udelay(REPEAT_PERIOD_US);
list_for_each(node, &pm_e_head) {
e = list_entry(node, struct pm_element, node);
if (e->lmt.ltemp_repeat &&
!wtemp_dvfs_is_effect(e, temp, PM_EVT_LOW))
goto repeat;
if (e->lmt.htemp_repeat &&
!wtemp_dvfs_is_effect(e, temp, PM_EVT_HIGH))
goto repeat;
}
finish:
list_for_each(node, &pm_e_head) {
e = list_entry(node, struct pm_element, node);
temp2string(temp, s_temp, TEMP_STRING_LEN);
printf("DVFS: %s %s'c, %ld Hz, %d uV\n", e->name,
s_temp, __wtemp_clk_get_rate(e),
__wtemp_regulator_get_value(e));
}
return 0;
}
static void print_e_state(void)
{
struct pm_element *e;
struct list_head *node;
char s_low[TEMP_STRING_LEN];
char s_high[TEMP_STRING_LEN];
char s_tz[TEMP_STRING_LEN];
list_for_each(node, &pm_e_head) {
e = list_entry(node, struct pm_element, node);
if (!e->lmt.ltemp_limit &&
!e->lmt.htemp_limit && !e->lmt.tztemp_limit)
return;
temp2string(e->lmt.tz_temp, s_tz, TEMP_STRING_LEN);
temp2string(e->lmt.low_temp, s_low, TEMP_STRING_LEN);
temp2string(e->lmt.high_temp, s_high, TEMP_STRING_LEN);
printf("DVFS: %s: low=%s'c, high=%s'c, Vmax=%duV, tz_temp=%s'c, "
"h_repeat=%d, l_repeat=%d\n",
e->name, e->lmt.ltemp_limit ? s_low : NULL,
e->lmt.htemp_limit ? s_high : NULL,
e->lmt.max_volt,
e->lmt.tztemp_limit ? s_tz : NULL,
e->lmt.htemp_repeat, e->lmt.ltemp_repeat);
}
}
static int wtemp_dvfs_ofdata_to_platdata(struct udevice *dev)
{
struct wtemp_dvfs_priv *priv = dev_get_priv(dev);
ofnode tz_trip0, cooling_maps, node;
ofnode cpus, cpu, dmc;
const char *name;
int ret, tz_temp;
u32 phandle;
INIT_LIST_HEAD(&pm_e_head);
/* 1. Parse cpu node */
priv->cpu = &pm_cpu;
cpus = ofnode_path(FDT_PATH_CPUS);
if (!ofnode_valid(cpus)) {
debug("DVFS: Can't find %s\n", FDT_PATH_CPUS);
goto parse_dmc;
}
ofnode_for_each_subnode(cpu, cpus) {
name = ofnode_get_property(cpu, "device_type", NULL);
if (!name)
continue;
if (!strcmp(name, "cpu")) {
ret = __wtemp_common_ofdata_to_platdata(cpu, priv->cpu);
if (ret)
return ret;
break;
}
}
priv->cpu->lmt.ltemp_repeat =
dev_read_bool(dev, "cpu,low-temp-repeat");
priv->cpu->lmt.htemp_repeat =
dev_read_bool(dev, "cpu,high-temp-repeat");
list_add_tail(&priv->cpu->node, &pm_e_head);
/* 2. Parse dmc node */
parse_dmc:
priv->dmc = &pm_dmc;
dmc = ofnode_path(FDT_PATH_DMC);
if (!ofnode_valid(dmc)) {
debug("DVFS: Can't find %s\n", FDT_PATH_CPUS);
goto parse_tz;
}
if (!IS_ENABLED(CONFIG_ROCKCHIP_DMC)) {
debug("DVFS: CONFIG_ROCKCHIP_DMC is disabled\n");
goto parse_tz;
}
ret = __wtemp_common_ofdata_to_platdata(dmc, priv->dmc);
if (ret)
return ret;
priv->dmc->lmt.ltemp_repeat =
dev_read_bool(dev, "dmc,low-temp-repeat");
priv->dmc->lmt.htemp_repeat =
dev_read_bool(dev, "dmc,high-temp-repeat");
list_add_tail(&priv->dmc->node, &pm_e_head);
/* 3. Parse thermal zone node */
parse_tz:
tz_trip0 = ofnode_path(FDT_PATH_THREMAL_TRIP_POINT0);
if (!ofnode_valid(tz_trip0)) {
debug("DVFS: Can't find %s\n", FDT_PATH_THREMAL_TRIP_POINT0);
goto finish;
}
tz_temp = ofnode_read_s32_default(tz_trip0, "temperature", -ENODATA);
if (tz_temp == -ENODATA) {
debug("DVFS: Can't get thermal zone trip0 temperature\n");
goto finish;
}
cooling_maps = ofnode_path(FDT_PATH_THREMAL_COOLING_MAPS);
if (!ofnode_valid(cooling_maps)) {
debug("DVFS: Can't find %s\n", FDT_PATH_THREMAL_COOLING_MAPS);
goto finish;
}
ofnode_for_each_subnode(node, cooling_maps) {
ofnode_read_u32_array(node, "cooling-device", &phandle, 1);
name = ofnode_get_name(ofnode_get_by_phandle(phandle));
if (!name)
continue;
if (strstr(name, "cpu")) {
priv->cpu->lmt.tztemp_limit = true;
priv->cpu->lmt.tz_temp = tz_temp;
} else if (strstr(name, "dmc")) {
priv->dmc->lmt.tztemp_limit = true;
priv->dmc->lmt.tz_temp = tz_temp;
}
}
finish:
print_e_state();
return 0;
}
static const struct dm_dvfs_ops wtemp_dvfs_ops = {
.apply = wtemp_dvfs_apply,
.repeat_apply = wtemp_dvfs_repeat_apply,
};
static int wtemp_dvfs_probe(struct udevice *dev)
{
struct wtemp_dvfs_priv *priv = dev_get_priv(dev);
int ret;
#ifdef CONFIG_ROCKCHIP_DMC
struct udevice *ram_dev;
/* Init dmc */
ret = uclass_get_device(UCLASS_RAM, 0, &ram_dev);
if (ret) {
printf("DVFS: Get dmc device failed, ret=%d\n", ret);
return ret;
}
#endif
/* Init thermal */
ret = uclass_get_device(UCLASS_THERMAL, 0, &priv->thermal);
if (ret) {
printf("DVFS: Get thermal device failed, ret=%d\n", ret);
return ret;
}
return 0;
}
static const struct udevice_id wtemp_dvfs_match[] = {
{ .compatible = "rockchip,uboot-wide-temperature", },
{},
};
U_BOOT_DRIVER(rockchip_wide_temp_dvfs) = {
.name = "rockchip_wide_temp_dvfs",
.id = UCLASS_DVFS,
.ops = &wtemp_dvfs_ops,
.of_match = wtemp_dvfs_match,
.probe = wtemp_dvfs_probe,
.ofdata_to_platdata = wtemp_dvfs_ofdata_to_platdata,
.priv_auto_alloc_size = sizeof(struct wtemp_dvfs_priv),
};