-/* $OpenBSD: cpu.c,v 1.14 2023/06/15 22:18:08 cheloha Exp $ */
+/* $OpenBSD: cpu.c,v 1.15 2023/09/19 19:20:33 kettenis Exp $ */
/*
* Copyright (c) 2016 Dale Rahn <drahn@dalerahn.com>
#include <sys/malloc.h>
#include <sys/device.h>
#include <sys/sysctl.h>
+#include <sys/task.h>
#include <uvm/uvm.h>
#include <dev/ofw/openfirm.h>
#include <dev/ofw/ofw_clock.h>
+#include <dev/ofw/ofw_regulator.h>
+#include <dev/ofw/ofw_thermal.h>
#include <dev/ofw/fdt.h>
/* CPU Identification */
int cpu_errata_sifive_cip_1200;
+void cpu_opp_init(struct cpu_info *, uint32_t);
+
void
cpu_identify(struct cpu_info *ci)
{
struct fdt_attach_args *faa = aux;
struct cpu_info *ci;
int node, level;
+ uint32_t opp;
KASSERT(faa->fa_nreg > 0);
}
#endif
+ opp = OF_getpropint(ci->ci_node, "operating-points-v2", 0);
+ if (opp)
+ cpu_opp_init(ci, opp);
+
node = faa->fa_node;
level = 1;
}
#endif
+
+/*
+ * Dynamic voltage and frequency scaling implementation.
+ */
+
+extern int perflevel;
+
+struct opp {
+ uint64_t opp_hz;
+ uint32_t opp_microvolt;
+};
+
+struct opp_table {
+ LIST_ENTRY(opp_table) ot_list;
+ uint32_t ot_phandle;
+
+ struct opp *ot_opp;
+ u_int ot_nopp;
+ uint64_t ot_opp_hz_min;
+ uint64_t ot_opp_hz_max;
+
+ struct cpu_info *ot_master;
+};
+
+LIST_HEAD(, opp_table) opp_tables = LIST_HEAD_INITIALIZER(opp_tables);
+struct task cpu_opp_task;
+
+void cpu_opp_mountroot(struct device *);
+void cpu_opp_dotask(void *);
+void cpu_opp_setperf(int);
+
+uint32_t cpu_opp_get_cooling_level(void *, uint32_t *);
+void cpu_opp_set_cooling_level(void *, uint32_t *, uint32_t);
+
+void
+cpu_opp_init(struct cpu_info *ci, uint32_t phandle)
+{
+ struct opp_table *ot;
+ struct cooling_device *cd;
+ int count, node, child;
+ uint32_t opp_hz, opp_microvolt;
+ uint32_t values[3];
+ int i, j, len;
+
+ LIST_FOREACH(ot, &opp_tables, ot_list) {
+ if (ot->ot_phandle == phandle) {
+ ci->ci_opp_table = ot;
+ return;
+ }
+ }
+
+ node = OF_getnodebyphandle(phandle);
+ if (node == 0)
+ return;
+
+ if (!OF_is_compatible(node, "operating-points-v2"))
+ return;
+
+ count = 0;
+ for (child = OF_child(node); child != 0; child = OF_peer(child)) {
+ if (OF_getproplen(child, "turbo-mode") == 0)
+ continue;
+ count++;
+ }
+ if (count == 0)
+ return;
+
+ ot = malloc(sizeof(struct opp_table), M_DEVBUF, M_ZERO | M_WAITOK);
+ ot->ot_phandle = phandle;
+ ot->ot_opp = mallocarray(count, sizeof(struct opp),
+ M_DEVBUF, M_ZERO | M_WAITOK);
+ ot->ot_nopp = count;
+
+ count = 0;
+ for (child = OF_child(node); child != 0; child = OF_peer(child)) {
+ if (OF_getproplen(child, "turbo-mode") == 0)
+ continue;
+ opp_hz = OF_getpropint64(child, "opp-hz", 0);
+ len = OF_getpropintarray(child, "opp-microvolt",
+ values, sizeof(values));
+ opp_microvolt = 0;
+ if (len == sizeof(uint32_t) || len == 3 * sizeof(uint32_t))
+ opp_microvolt = values[0];
+
+ /* Insert into the array, keeping things sorted. */
+ for (i = 0; i < count; i++) {
+ if (opp_hz < ot->ot_opp[i].opp_hz)
+ break;
+ }
+ for (j = count; j > i; j--)
+ ot->ot_opp[j] = ot->ot_opp[j - 1];
+ ot->ot_opp[i].opp_hz = opp_hz;
+ ot->ot_opp[i].opp_microvolt = opp_microvolt;
+ count++;
+ }
+
+ ot->ot_opp_hz_min = ot->ot_opp[0].opp_hz;
+ ot->ot_opp_hz_max = ot->ot_opp[count - 1].opp_hz;
+
+ if (OF_getproplen(node, "opp-shared") == 0)
+ ot->ot_master = ci;
+
+ LIST_INSERT_HEAD(&opp_tables, ot, ot_list);
+
+ ci->ci_opp_table = ot;
+ ci->ci_opp_idx = -1;
+ ci->ci_opp_max = ot->ot_nopp - 1;
+ ci->ci_cpu_supply = OF_getpropint(ci->ci_node, "cpu-supply", 0);
+
+ cd = malloc(sizeof(struct cooling_device), M_DEVBUF, M_ZERO | M_WAITOK);
+ cd->cd_node = ci->ci_node;
+ cd->cd_cookie = ci;
+ cd->cd_get_level = cpu_opp_get_cooling_level;
+ cd->cd_set_level = cpu_opp_set_cooling_level;
+ cooling_device_register(cd);
+
+ /*
+ * Do additional checks at mountroot when all the clocks and
+ * regulators are available.
+ */
+ config_mountroot(ci->ci_dev, cpu_opp_mountroot);
+}
+
+void
+cpu_opp_mountroot(struct device *self)
+{
+ struct cpu_info *ci;
+ CPU_INFO_ITERATOR cii;
+ int count = 0;
+ int level = 0;
+
+ if (cpu_setperf)
+ return;
+
+ CPU_INFO_FOREACH(cii, ci) {
+ struct opp_table *ot = ci->ci_opp_table;
+ uint64_t curr_hz;
+ uint32_t curr_microvolt;
+ int error;
+
+ if (ot == NULL)
+ continue;
+
+ /* Skip if this table is shared and we're not the master. */
+ if (ot->ot_master && ot->ot_master != ci)
+ continue;
+
+ /* PWM regulators may need to be explicitly enabled. */
+ regulator_enable(ci->ci_cpu_supply);
+
+ curr_hz = clock_get_frequency(ci->ci_node, NULL);
+ curr_microvolt = regulator_get_voltage(ci->ci_cpu_supply);
+
+ /* Disable if clock isn't implemented. */
+ error = ENODEV;
+ if (curr_hz != 0)
+ error = clock_set_frequency(ci->ci_node, NULL, curr_hz);
+ if (error) {
+ ci->ci_opp_table = NULL;
+ printf("%s: clock not implemented\n",
+ ci->ci_dev->dv_xname);
+ continue;
+ }
+
+ /* Disable if regulator isn't implemented. */
+ error = ci->ci_cpu_supply ? ENODEV : 0;
+ if (ci->ci_cpu_supply && curr_microvolt != 0)
+ error = regulator_set_voltage(ci->ci_cpu_supply,
+ curr_microvolt);
+ if (error) {
+ ci->ci_opp_table = NULL;
+ printf("%s: regulator not implemented\n",
+ ci->ci_dev->dv_xname);
+ continue;
+ }
+
+ /*
+ * Initialize performance level based on the current
+ * speed of the first CPU that supports DVFS.
+ */
+ if (level == 0) {
+ uint64_t min, max;
+ uint64_t level_hz;
+
+ min = ot->ot_opp_hz_min;
+ max = ot->ot_opp_hz_max;
+ level_hz = clock_get_frequency(ci->ci_node, NULL);
+ level = howmany(100 * (level_hz - min), (max - min));
+ }
+
+ count++;
+ }
+
+ if (count > 0) {
+ task_set(&cpu_opp_task, cpu_opp_dotask, NULL);
+ cpu_setperf = cpu_opp_setperf;
+
+ perflevel = (level > 0) ? level : 0;
+ cpu_setperf(perflevel);
+ }
+}
+
+void
+cpu_opp_dotask(void *arg)
+{
+ struct cpu_info *ci;
+ CPU_INFO_ITERATOR cii;
+
+ CPU_INFO_FOREACH(cii, ci) {
+ struct opp_table *ot = ci->ci_opp_table;
+ uint64_t curr_hz, opp_hz;
+ uint32_t curr_microvolt, opp_microvolt;
+ int opp_idx;
+ int error = 0;
+
+ if (ot == NULL)
+ continue;
+
+ /* Skip if this table is shared and we're not the master. */
+ if (ot->ot_master && ot->ot_master != ci)
+ continue;
+
+ opp_idx = MIN(ci->ci_opp_idx, ci->ci_opp_max);
+ opp_hz = ot->ot_opp[opp_idx].opp_hz;
+ opp_microvolt = ot->ot_opp[opp_idx].opp_microvolt;
+
+ curr_hz = clock_get_frequency(ci->ci_node, NULL);
+ curr_microvolt = regulator_get_voltage(ci->ci_cpu_supply);
+
+ if (error == 0 && opp_hz < curr_hz)
+ error = clock_set_frequency(ci->ci_node, NULL, opp_hz);
+ if (error == 0 && ci->ci_cpu_supply &&
+ opp_microvolt != 0 && opp_microvolt != curr_microvolt) {
+ error = regulator_set_voltage(ci->ci_cpu_supply,
+ opp_microvolt);
+ }
+ if (error == 0 && opp_hz > curr_hz)
+ error = clock_set_frequency(ci->ci_node, NULL, opp_hz);
+
+ if (error)
+ printf("%s: DVFS failed\n", ci->ci_dev->dv_xname);
+ }
+}
+
+void
+cpu_opp_setperf(int level)
+{
+ struct cpu_info *ci;
+ CPU_INFO_ITERATOR cii;
+ int update = 0;
+
+ CPU_INFO_FOREACH(cii, ci) {
+ struct opp_table *ot = ci->ci_opp_table;
+ uint64_t min, max;
+ uint64_t level_hz, opp_hz;
+ int opp_idx = -1;
+ int i;
+
+ if (ot == NULL)
+ continue;
+
+ /* Skip if this table is shared and we're not the master. */
+ if (ot->ot_master && ot->ot_master != ci)
+ continue;
+
+ min = ot->ot_opp_hz_min;
+ max = ot->ot_opp_hz_max;
+ level_hz = min + (level * (max - min)) / 100;
+ opp_hz = min;
+ for (i = 0; i < ot->ot_nopp; i++) {
+ if (ot->ot_opp[i].opp_hz <= level_hz &&
+ ot->ot_opp[i].opp_hz >= opp_hz)
+ opp_hz = ot->ot_opp[i].opp_hz;
+ }
+
+ /* Find index of selected operating point. */
+ for (i = 0; i < ot->ot_nopp; i++) {
+ if (ot->ot_opp[i].opp_hz == opp_hz) {
+ opp_idx = i;
+ break;
+ }
+ }
+ KASSERT(opp_idx >= 0);
+
+ if (ci->ci_opp_idx != opp_idx) {
+ ci->ci_opp_idx = opp_idx;
+ update = 1;
+ }
+ }
+
+ /*
+ * Update the hardware from a task since setting the
+ * regulators might need process context.
+ */
+ if (update)
+ task_add(systq, &cpu_opp_task);
+}
+
+uint32_t
+cpu_opp_get_cooling_level(void *cookie, uint32_t *cells)
+{
+ struct cpu_info *ci = cookie;
+ struct opp_table *ot = ci->ci_opp_table;
+
+ return ot->ot_nopp - ci->ci_opp_max - 1;
+}
+
+void
+cpu_opp_set_cooling_level(void *cookie, uint32_t *cells, uint32_t level)
+{
+ struct cpu_info *ci = cookie;
+ struct opp_table *ot = ci->ci_opp_table;
+ int opp_max;
+
+ if (level > (ot->ot_nopp - 1))
+ level = ot->ot_nopp - 1;
+
+ opp_max = (ot->ot_nopp - level - 1);
+ if (ci->ci_opp_max != opp_max) {
+ ci->ci_opp_max = opp_max;
+ task_add(systq, &cpu_opp_task);
+ }
+}