ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
power_mgmt.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file power_mgmt.cpp
6 * @brief The power governor's decision + its device binding (see power_mgmt.h).
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_POWER_MGMT
13
14#include <stdio.h>
15
16// ---------------------------------------------------------------------------
17// Pure decision
18// ---------------------------------------------------------------------------
19
20#if defined(ARDUINO)
21#include <Arduino.h>
22#include <esp_system.h>
23#endif
24#if defined(ARDUINO) && defined(CONFIG_BT_ENABLED)
25#include <esp_bt.h>
26#endif
27void pc_power_cfg_defaults(PowerCfg *cfg)
28{
29 if (!cfg)
30 {
31 return;
32 }
33 cfg->mhz_max = PC_POWER_MHZ_MAX;
34 cfg->mhz_min = PC_POWER_MHZ_MIN;
35 cfg->busy_pct = PC_POWER_BUSY_PCT;
36 cfg->temp_hot_c = PC_POWER_TEMP_HOT_C;
37 cfg->temp_cool_c = PC_POWER_TEMP_COOL_C;
38 cfg->recover_ms = PC_POWER_RECOVER_MS;
39}
40
41PowerPlan pc_power_plan(const PowerCfg *cfg, uint8_t load_pct, int16_t temp_c, bool brownout_boot,
42 uint32_t since_boot_ms, bool was_throttled)
43{
44 PowerPlan p;
45 p.cpu_mhz = 0;
46 p.throttled = false;
47 p.recovering = false;
48 if (!cfg)
49 {
50 return p;
51 }
52
53 // Hysteresis: once throttled, hold it until the die drops to the *cool* threshold. With one
54 // threshold a part sitting at the limit would flap between ceiling and floor every tick.
55 // INT16_MIN means "no sensor on this part", which must not read as ice-cold and un-throttle.
56 bool have_temp = temp_c != INT16_MIN;
57 if (have_temp)
58 {
59 p.throttled = was_throttled ? (temp_c > cfg->temp_cool_c) : (temp_c >= cfg->temp_hot_c);
60 }
61 else
62 {
63 p.throttled = false;
64 }
65
66 // A supply that just failed under load gets a gentle restart rather than an immediate return to
67 // the clock that browned it out.
68 p.recovering = brownout_boot && since_boot_ms < cfg->recover_ms;
69
70 if (p.recovering || p.throttled)
71 {
72 p.cpu_mhz = cfg->mhz_min;
73 return p;
74 }
75 if (load_pct > 100)
76 {
77 load_pct = 100;
78 }
79 p.cpu_mhz = (load_pct >= cfg->busy_pct) ? cfg->mhz_max : cfg->mhz_min;
80 return p;
81}
82
83size_t pc_power_json(const PowerPlan *plan, int16_t temp_c, char *out, size_t cap)
84{
85 if (!plan || !out || cap == 0)
86 {
87 return 0;
88 }
89 // The two forms differ by one field, so one builder emits both rather than two copies that can
90 // drift apart.
91 pc_sb sb = {out, cap, 0, true};
92 pc_sb_put(&sb, "{\"cpu_mhz\":");
93 pc_sb_u32(&sb, (uint32_t)plan->cpu_mhz);
94 pc_sb_put(&sb, ",\"throttled\":");
95 pc_sb_put(&sb, plan->throttled ? "true" : "false");
96 pc_sb_put(&sb, ",\"recovering\":");
97 pc_sb_put(&sb, plan->recovering ? "true" : "false");
98 pc_sb_put(&sb, ",\"temp_c\":");
99 if (temp_c == INT16_MIN) // no sensor: report null rather than a sentinel that reads as a reading
100 {
101 pc_sb_put(&sb, "null");
102 }
103 else
104 {
105 pc_sb_i64(&sb, (int64_t)temp_c);
106 }
107 pc_sb_put(&sb, "}");
108 size_t n = pc_sb_finish(&sb);
109 if (n == 0)
110 {
111 // Fail closed: the builder writes the pieces that fit before it latches, so without this
112 // the caller would be handed a partial object like `{"cpu_mhz":` and no way to know.
113 out[0] = '\0';
114 return 0;
115 }
116 return n;
117}
118
119// ---------------------------------------------------------------------------
120// Device binding
121// ---------------------------------------------------------------------------
122
123#if defined(ARDUINO)
124
125/** @brief Owned state: the latched boot reason and whether BT has already been released. */
126struct PowerCtx
127{
128 bool brownout_latched;
129 bool boot_checked;
130 bool bt_released;
131};
132static PowerCtx s_pwr = {false, false, false};
133
134bool pc_power_brownout_boot(void)
135{
136 // Read once and latch: the reset reason describes this boot, so it must not change under a
137 // caller polling it every tick through the recovery window.
138 if (!s_pwr.boot_checked)
139 {
140 s_pwr.brownout_latched = (esp_reset_reason() == ESP_RST_BROWNOUT);
141 s_pwr.boot_checked = true;
142 }
143 return s_pwr.brownout_latched;
144}
145
146int16_t pc_power_temp_c(void)
147{
148#if defined(SOC_TEMP_SENSOR_SUPPORTED) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || \
149 defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)
150 float t = temperatureRead();
151 // The driver reports a sentinel far outside any real die temperature when the sensor is not up.
152 if (t < -60.0f || t > 200.0f)
153 {
154 return INT16_MIN;
155 }
156 return (int16_t)(t + (t < 0 ? -0.5f : 0.5f));
157#else
158 return INT16_MIN; // classic ESP32 has no usable internal sensor
159#endif
160}
161
162uint16_t pc_power_cpu_mhz(void)
163{
164 return (uint16_t)getCpuFrequencyMhz();
165}
166
167bool pc_power_apply(const PowerPlan *plan)
168{
169 if (!plan || plan->cpu_mhz == 0)
170 {
171 return false;
172 }
173 if (pc_power_cpu_mhz() == plan->cpu_mhz)
174 {
175 return false; // already there; re-setting the clock is not free
176 }
177 return setCpuFrequencyMhz((uint32_t)plan->cpu_mhz);
178}
179
180bool pc_power_gate_bt(void)
181{
182#if defined(CONFIG_BT_ENABLED)
183 if (s_pwr.bt_released)
184 {
185 return false;
186 }
187 // Disable before release: releasing an enabled controller's memory is rejected, and the
188 // whole point is to actually drop the domain rather than report a success that did not happen.
189 if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED)
190 {
191 esp_bt_controller_disable();
192 }
193 bool ok = esp_bt_controller_mem_release(ESP_BT_MODE_BTDM) == ESP_OK;
194 s_pwr.bt_released = ok;
195 return ok;
196#else
197 return false; // BT not built in, so there is nothing holding the domain
198#endif
199}
200
201#endif // ARDUINO
202
203#endif // PC_ENABLE_POWER_MGMT
SoC power governor: frequency scaling, thermal throttle, brownout recovery, gating (PC_ENABLE_POWER_M...
#define PC_POWER_RECOVER_MS
How long (ms) to hold the floor clock after a brownout reset before ramping back up.
#define PC_POWER_TEMP_COOL_C
Die temperature (C) at/below which the throttle is released.
#define PC_POWER_TEMP_HOT_C
Die temperature (C) at/above which the clock is throttled.
#define PC_POWER_MHZ_MIN
CPU clock (MHz) when idle, thermally throttled, or recovering from a brownout.
#define PC_POWER_BUSY_PCT
Load percentage at/above which the ceiling clock is used.
#define PC_POWER_MHZ_MAX
CPU clock (MHz) when there is work to do.
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
Definition strbuf.h:315
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30