ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
power_mgmt.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file power_mgmt.h
6 * @brief SoC power governor: frequency scaling, thermal throttle, brownout recovery, gating
7 * (PROTOCORE_ENABLE_POWER_MGMT).
8 *
9 * network_drivers/physical/radio_power owns the radio and server/sleep_sched decides how *long* to sleep. Neither
10 * owns the SoC itself, which is where the rest of the power budget goes: the CPU clock, the die
11 * temperature, and the peripherals nobody is using.
12 *
13 * The governor answers one question - given the current load, die temperature, and how the board
14 * last reset, what should the CPU clock be right now:
15 *
16 * - **Scaling.** Busy work runs at the ceiling; an idle server drops to the floor. Running a
17 * 240 MHz core to poll an idle socket is the single easiest power win on this part.
18 * - **Thermal throttle.** Hot parts clock down, and the restore threshold is *lower* than the
19 * throttle threshold. Without that gap a device sitting exactly at the limit oscillates between
20 * full speed and floor forever, which is worse than either.
21 * - **Brownout recovery.** A board that just browned out is on a supply that could not hold up the
22 * last load it saw, so slamming straight back to full speed invites the same collapse and a boot
23 * loop. After a brownout reset it comes up at the floor and stays there for a settle window.
24 * - **Gating.** Blocks the firmware never uses still burn current; Bluetooth is the big one, since
25 * the controller draws power whether or not anything is connected.
26 *
27 * The decision is pure and takes every input explicitly - load, temperature, the brownout flag, the
28 * time since boot, and the previous throttle state for the hysteresis - so the whole governor is
29 * host-testable with no hardware. The binding only reads the sensors and applies the result.
30 *
31 * @author Douglas Quigg (dstroy0)
32 * @date 2026
33 */
34
35#ifndef PROTOCORE_POWER_MGMT_H
36#define PROTOCORE_POWER_MGMT_H
37
38#include "protocore_config.h" // the entry point: protocore_types.h for the widths
39
40#if PROTOCORE_ENABLE_POWER_MGMT
41
43
44/** @brief Governor limits. Temperatures in whole degrees C; frequencies in MHz. */
45typedef struct
46{
47 uint16_t mhz_max; ///< clock when there is work to do.
48 uint16_t mhz_min; ///< clock when idle, throttled, or recovering.
49 uint8_t busy_pct; ///< load at/above which the ceiling is used.
50 int16_t temp_hot_c; ///< throttle at/above this die temperature.
51 int16_t temp_cool_c; ///< release the throttle at/below this one (must be < temp_hot_c).
52 uint32_t recover_ms; ///< how long to stay at the floor after a brownout reset.
53} PowerCfg;
54
55/** @brief What the governor decided this tick. */
56typedef struct
57{
58 uint16_t cpu_mhz; ///< clock to apply.
59 proto_bool throttled; ///< the thermal limit is holding the clock down.
60 proto_bool recovering; ///< still inside the post-brownout settle window.
61} PowerPlan;
62
63/** @brief What the pure plan reads. */
64typedef struct
65{
66 const PowerCfg *cfg; ///< the thresholds
67 uint8_t load_pct; ///< how busy the loop has been
68 int16_t temp_c; ///< the die temperature, or INT16_MIN when there is no sensor
69 proto_bool brownout_boot; ///< this boot followed a brownout
70 uint32_t since_boot_ms; ///< how long it has been running
71 proto_bool was_throttled; ///< the plan's own previous output, which is what gives it hysteresis
72} PowerPlanArgs;
73
74/** @brief The plan a call acts on, and where a report is written. */
75typedef struct
76{
77 const PowerPlan *plan; ///< the plan an apply carries out, or a report describes
78 int16_t temp_c; ///< the temperature that report carries
79 char *out; ///< where the JSON lands
80 size_t cap; ///< how much room it has
81} PowerOutArgs;
82
83/**
84 * @brief The CPU clock governor.
85 *
86 * A caller sets the members a call takes, invokes it through ::Power, and reads the outcome off the
87 * same handle. The latched boot cause and the released-domain flag are behind @ref internal.
88 *
89 * @var PowerMgmtNs::plan_args what the pure plan reads
90 * @var PowerMgmtNs::out_args the plan a call acts on, and where a report is written
91 * @var PowerMgmtNs::cfg_out where defaults are written
92 * @var PowerMgmtNs::plan the plan a decide produced
93 * @var PowerMgmtNs::ok a call's true/false outcome
94 * @var PowerMgmtNs::n bytes a report wrote, or 0 when it did not fit
95 * @var PowerMgmtNs::temp_c the die temperature a read reports, INT16_MIN for no sensor
96 * @var PowerMgmtNs::mhz the CPU clock a read reports
97 * @var PowerMgmtNs::defaults fill a config from the build flags
98 * @var PowerMgmtNs::decide choose a clock, reading nothing outside plan_args
99 * @var PowerMgmtNs::json serialize a plan
100 * @var PowerMgmtNs::brownout this boot followed a brownout; latched, so it stays true
101 * @var PowerMgmtNs::die_temp the die temperature, from the platform seam
102 * @var PowerMgmtNs::cpu_mhz the clock the part is running at
103 * @var PowerMgmtNs::apply set the clock a plan asks for, when it is not already there
104 * @var PowerMgmtNs::gate_bt release the radio controller's power domain
105 *
106 * decide takes its own previous output back in as @c plan_args.was_throttled: with one threshold a
107 * part sitting at the limit would flap between ceiling and floor every tick, so once throttled it
108 * holds until the die drops to the cool threshold.
109 */
110typedef struct
111{
112 PowerPlanArgs plan_args;
113 PowerOutArgs out_args;
114 PowerCfg *cfg_out;
115 PowerPlan plan;
116 proto_bool ok;
117 size_t n;
118 int16_t temp_c;
119 uint16_t mhz;
120#if PROTOCORE_HAS_VENDOR_PM
121#endif
122#if PROTOCORE_HAS_VENDOR_BT
123#endif
124} PowerVars;
125
126/** @brief The operands and the outcome. */
127extern PowerVars PowerV;
128
129/** @brief The entries. */
130typedef struct
131{
132 void (*const defaults)(uint8_t *work);
133 void (*const decide)(uint8_t *work);
134 void (*const json)(uint8_t *work);
135 void (*const brownout)(uint8_t *work);
136 void (*const die_temp)(uint8_t *work);
137 void (*const cpu_mhz)(uint8_t *work);
138 void (*const apply)(uint8_t *work);
139 void (*const gate_bt)(uint8_t *work);
140} PowerMgmtNs;
141
142// What the table binds, defined once in the .c and taking one parameter each: everything
143// else an entry needs is an operand in PowerV or a region of the borrow at a fixed offset.
144void protocore_power_defaults(uint8_t *work);
145void protocore_power_decide(uint8_t *work);
146void protocore_power_json(uint8_t *work);
147#if PROTOCORE_HAS_VENDOR_PM
148void protocore_power_brownout(uint8_t *work);
149void protocore_power_die_temp(uint8_t *work);
150void protocore_power_cpu_mhz(uint8_t *work);
151void protocore_power_apply(uint8_t *work);
152#endif
153#if PROTOCORE_HAS_VENDOR_BT
154void protocore_power_gate_bt(uint8_t *work);
155#endif
156
157// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
158// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
159// `Power.defaults(work)` resolves to a named function and becomes a DIRECT call. An extern table
160// leaves the call indirect and the symbol live at every level, -O2 -flto included.
161static const PowerMgmtNs Power __attribute__((unused)) = {
162 .defaults = protocore_power_defaults,
163 .decide = protocore_power_decide,
164 .json = protocore_power_json,
165#if PROTOCORE_HAS_VENDOR_PM
166 .brownout = protocore_power_brownout,
167 .die_temp = protocore_power_die_temp,
168 .cpu_mhz = protocore_power_cpu_mhz,
169 .apply = protocore_power_apply,
170#endif
171#if PROTOCORE_HAS_VENDOR_BT
172 .gate_bt = protocore_power_gate_bt,
173#endif
174};
175
176/**
177 * @brief The PROTOCORE_POWER_MGMT_BORROW bytes this module's state lives in.
178 *
179 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
180 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
181 * walks, so the state lasts the life of the program.
182 *
183 * @return the span.
184 */
185uint8_t *protocore_power_mgmt_span(void);
186
188
189#endif // PROTOCORE_ENABLE_POWER_MGMT
190
191#endif // PROTOCORE_POWER_MGMT_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97