Add Power Scheduler

This commit is contained in:
Phillip Kühne 2025-02-11 23:33:19 +01:00
parent 893234ed24
commit b44538b473
Signed by: phillip
GPG Key ID: E4C1C4D2F90902AA
5 changed files with 425 additions and 180 deletions

View File

@ -10,41 +10,106 @@
#include "Power.h"
static portMUX_TYPE mux;
void Power::begin()
{
// Check if another instance of us already initialized the power scheduler,
// if not, we will do it.
if (powerScheduler == nullptr)
{
powerScheduler = PowerScheduler::getPowerScheduler();
if (!(powerScheduler->tryAccquirePowerAllowance(CONSUMPTION_ESP_BASE)))
{
Serial.println("Alledgedly not enough power available to reserve the ESP32s base power consumption. Something is wrong.");
return;
}
void Power::begin() {
// Check if another instance of us already initialized the power scheduler,
// if not, we will do it.
if (powerScheduler == nullptr) {
powerScheduler = &PowerScheduler::getPowerScheduler();
if (!(powerScheduler->tryAccquireCurrentAllowance(
PowerParameters::PowerConsumers::ESP,
PowerParameters::CurrentConsumptions::CURRENT_ESP_AVG))) {
Serial.println("Alledgedly not enough power available to reserve the "
"ESP32s base power consumption. Something is wrong.");
return;
}
}
}
uint16_t Power::getFreePowerBudget(void)
{
return powerScheduler->getFreePowerBudget();
uint16_t Power::getFreeCurrentBudget(void) {
return powerScheduler->getFreeCurrentBudget();
}
bool Power::tryAccquirePowerAllowance(uint16_t neededPower)
{
return powerScheduler->tryAccquirePowerAllowance(neededPower);
bool Power::tryAccquireCurrentAllowance(
PowerParameters::PowerConsumers consumer, uint16_t neededCurrent,
uint16_t requestedDurationMs) {
return powerScheduler->tryAccquireCurrentAllowance(consumer, neededCurrent,
requestedDurationMs);
}
bool Power::waitForPowerAllowance(uint16_t neededPower, TickType_t TicksToWait)
{
return powerScheduler->waitForPowerAllowance(neededPower, TicksToWait);
bool Power::waitForCurrentAllowance(PowerParameters::PowerConsumers consumer,
uint16_t neededCurrent,
TickType_t TicksToWait,
uint16_t requestedDurationMs) {
return powerScheduler->waitForCurrentAllowance(
consumer, neededCurrent, TicksToWait, requestedDurationMs);
}
void Power::beginPermanentDeepSleep(void)
{
return powerScheduler->beginPermanentDeepSleep();
void Power::beginPermanentDeepSleep(void) {
return powerScheduler->beginPermanentDeepSleep();
}
void Power::releasePower(uint16_t power)
{
void Power::releaseCurrent(PowerParameters::PowerConsumers consumer) {
powerScheduler->releaseCurrent(consumer);
}
float Power::getBatteryVoltage() {
// Get the battery voltage from the ADC and convert it to a voltage
// using the voltage divider.
portENTER_CRITICAL(&mux);
// Enable voltage divider
digitalWrite(PowerParameters::PinConfig::BAT_ADC_EN, HIGH);
// Returns value between 0 and 4095 mapping to between 0 and 3.3V
uint16_t batteryAdcValue =
analogRead(PowerParameters::PinConfig::BAT_ADC) *
(PowerParameters::Battery::BAT_ADC::VOLTAGE_DIVIDER_FACTOR);
// Disable voltage divider
digitalWrite(PowerParameters::PinConfig::BAT_ADC_EN, LOW);
portEXIT_CRITICAL(&mux);
// Convert ADC value to voltage
float batteryVoltage =
(batteryAdcValue * 3.3 / 4095) *
PowerParameters::Battery::BAT_ADC::VOLTAGE_DIVIDER_FACTOR;
return batteryVoltage;
}
int Power::getBatteryChargePercent() {
// Get the battery voltage and calculate the charge state based on the
// discharge curve.
float batteryVoltage = getBatteryVoltage();
float chargeState = 0;
// Clamp edge cases
if (batteryVoltage >=
PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[0]) {
return PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES[0];
}
if (batteryVoltage <=
PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES
[PowerParameters::Battery::DISCHARGE_CURVE::NUM_POINTS - 1]) {
return PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES
[PowerParameters::Battery::DISCHARGE_CURVE::NUM_POINTS - 1];
}
float p1_x, p1_y, p2_x, p2_y;
for (int i = 0; i < PowerParameters::Battery::DISCHARGE_CURVE::NUM_POINTS;
i++) {
if (batteryVoltage >=
PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[i]) {
p1_y = PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES[i];
p1_x = PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[i];
p2_y = PowerParameters::Battery::DISCHARGE_CURVE::CHARGE_STATES[i + 1];
p2_x = PowerParameters::Battery::DISCHARGE_CURVE::VOLTAGES[i + 1];
chargeState =
((p2_y - p1_y) / (p2_x - p1_x)) * (batteryVoltage - p1_x) + p1_y;
return chargeState;
}
}
}
PowerScheduler* Power::powerScheduler = nullptr;
Power::Power() {
// Initialize the power scheduler
powerScheduler = &PowerScheduler::getPowerScheduler();
// Initialize the mutex
mux = portMUX_INITIALIZER_UNLOCKED;
}

View File

@ -13,38 +13,77 @@
#define TOTAL_POWER_MILLIWATTS POWER_BUDGET
enum TaskResumptionReason
{
POWER_AVAILABLE,
TIMEOUT
};
enum TaskResumptionReason { POWER_AVAILABLE, TIMEOUT };
class Power
{
class Power {
public:
static void begin();
Power();
uint16_t getFreePowerBudget(void);
/// @brief Request an allowance of a certain number of milliwatts from the power scheduler
/// @param neededPower the amount of power we want to be accounted for (in mW)
/// @return whether the power could be successfully allocated
bool tryAccquirePowerAllowance(uint16_t neededPower);
/// @brief "Return" a certain amount of power when it is no longer needed
/// @param neededPower the amount of power to return (in mW)
/// @return whether the power
void releasePower(uint16_t power);
static void begin(void);
Power();
uint16_t getFreeCurrentBudget(void);
/// @brief Request an allowance of a certain number of milliwatts from the
/// power scheduler
/// @param neededPower the amount of power we want to be accounted for (in mW)
/// @return whether the power could be successfully allocated
static bool
tryAccquireCurrentAllowance(PowerParameters::PowerConsumers consumer,
uint16_t neededCurrent,
uint16_t requestedDurationMs);
/// @brief "Return" the power consumed by an active consumer
/// @param neededPower the amount of power to return (in mW)
/// @return whether the power
static void releaseCurrent(PowerParameters::PowerConsumers consumer);
/// @brief Wait for a certain amount of power to be available
/// @param neededPower the amount of power we want to be accounted for (in mW)
/// @param TicksToWait the amount of time to wait for the power to become available
/// @return whether the power could be successfully allocatedy
bool waitForPowerAllowance(uint16_t neededPower, TickType_t TicksToWait);
/// @brief Put the ESP32 into deep sleep mode, without a method to wake up again. Basically this is a shutdown.
void beginPermanentDeepSleep(void);
/// @brief Wait for a certain amount of power to be available
/// @param neededPower the amount of power we want to be accounted for (in mW)
/// @param TicksToWait the amount of time to wait for the power to become
/// available
/// @return whether the power could be successfully allocatedy
static bool waitForCurrentAllowance(PowerParameters::PowerConsumers consumer,
uint16_t neededCurrent,
TickType_t TicksToWait,
uint16_t requestedDurationMs);
/// @brief Put the ESP32 into deep sleep mode, without a method to wake up
/// again. Basically this is a shutdown.
static void beginPermanentDeepSleep(void);
/// @brief Get battery voltage measurement.
/// @return Battery Terminal Voltage in Volts
static float getBatteryVoltage();
/// @brief Get estimated battery charge state as percentage
/// @return Battery charge state in percent
static int getBatteryChargePercent();
/// @brief Get estimated battery charge state as coulombs
/// @return Battery charge state in coulombs
static float getBatteryChargeCoulombs();
/// @brief get available current (after voltage conversion and efficiency
/// losses, referencing 1C discharge)
/// @return available current in milliamps
static float getAvailableCurrent();
protected:
static PowerScheduler *powerScheduler;
/// @brief PowerScheduler instance to manage power consumption
static PowerScheduler *powerScheduler;
/// @brief update Power State
static void updatePowerState();
/*
* Power State
*/
/// @brief remaining Charge in coulombs
static int coloumbsRemaining;
/// @brief remaining Charge in percent
static int percentRemaining;
friend class PowerScheduler;
};
extern Power power;
#endif // Power

View File

@ -99,7 +99,7 @@ namespace PowerParameters {
PT_DL_FRONT,
PT_DL_BOTTOM,
LED_UV,
DISPLAY,
DISPLAY_OLED,
MOTOR_LEFT,
MOTOR_RIGHT,
IMU

View File

@ -1,7 +1,8 @@
/**
* @file PowerScheduler.cpp
* @author Phillip Kühne
* @brief The actual power scheduler class, which keeps track of the power budget and allocates power to different components.
* @brief The actual power scheduler class, which keeps track of the power
* budget and allocates power to different components.
* @version 0.1
* @date 2024-12-21
*
@ -11,99 +12,186 @@
#include "PowerScheduler.h"
bool PowerScheduler::tryAccquirePowerAllowance(uint16_t neededPower)
{
if (this->freePowerBudget >= neededPower)
{
this->freePowerBudget -= neededPower;
return true;
}
else
{
return false;
}
bool PowerScheduler::tryAccquireCurrentAllowance(
PowerParameters::PowerConsumers consumer, uint16_t neededCurrent,
uint16_t requestedDurationMs) {
portENTER_CRITICAL(&mux);
if (this->freeLimitCurrentBudget > 0 &&
this->freeMaximumCurrentBudget >= neededCurrent) {
this->currentAllowances.push_back(
{.consumer = consumer,
.maxSlackTimeMs = 0,
.requestedDurationMs = requestedDurationMs,
.taskHandle = xTaskGetCurrentTaskHandle(),
.neededCurrent = neededCurrent,
.requestedAt = xTaskGetTickCount(),
.granted = false});
this->recalculateCurrentBudgets();
portEXIT_CRITICAL(&mux);
return true;
} else {
return false;
}
}
void PowerScheduler::releasePower(uint16_t power)
{
if (this->freePowerBudget + power <= this->totalPowerBudget)
{
this->freePowerBudget += power;
void PowerScheduler::releaseCurrent(PowerParameters::PowerConsumers consumer) {
portENTER_CRITICAL(&mux);
for (auto it = currentAllowances.begin(); it != currentAllowances.end();
++it) {
if (it->consumer == consumer && it->granted) {
currentAllowances.erase(it);
break;
}
else // TODO: Maybe we should actually throw an error here, since obviouslsy someone used us wrong.
{
this->freePowerBudget = this->totalPowerBudget;
}
// Check if there are tasks waiting for power
checkWaitingTasks();
}
recalculateCurrentBudgets();
portEXIT_CRITICAL(&mux);
// Check if there are tasks waiting for power
checkWaitingTasks();
}
bool PowerScheduler::waitForPowerAllowance(uint16_t neededPower, TickType_t ticksToWait)
{
if (tryAccquirePowerAllowance(neededPower))
{
return true;
bool PowerScheduler::waitForCurrentAllowance(
PowerParameters::PowerConsumers consumer, uint16_t neededCurrent,
uint16_t maxSlackTimeMs, uint16_t requestedDurationMs) {
if (tryAccquireCurrentAllowance(consumer, neededCurrent,
requestedDurationMs)) {
return true;
} else {
// Suspend the task while waiting for power to be available
TaskHandle_t currentTask = xTaskGetCurrentTaskHandle();
TickType_t initialTickCount = xTaskGetTickCount();
PowerScheduler::CurrentAllowance newAllowance = {
.consumer = consumer,
.maxSlackTimeMs = maxSlackTimeMs,
.requestedDurationMs = requestedDurationMs,
.taskHandle = currentTask,
.neededCurrent = neededCurrent,
.requestedAt = initialTickCount,
.granted = false};
this->currentAllowances.push_back(newAllowance);
uint32_t notificationValue;
BaseType_t notificationStatus = xTaskNotifyWait(
0, 0, &notificationValue, pdMS_TO_TICKS(maxSlackTimeMs));
// Code below will be executed after the task is woken up
while (notificationStatus == pdPASS) {
if (notificationValue ==
PowerScheduler::PowerWakeupReasons::POWER_AVAILABLE) {
// We were woken up because new power is available, check if it is
// enough
if (this->freeLimitCurrentBudget > 0 &&
this->freeMaximumCurrentBudget >= neededCurrent) {
return true;
} else {
// Still not enough power available for us. Wait the remaining ticks.
xTaskNotifyWait(0, 0, &notificationValue,
pdMS_TO_TICKS(maxSlackTimeMs) -
(xTaskGetTickCount() - initialTickCount));
}
}
}
else
{
// Suspend the task while waiting for power to be available
TaskHandle_t currentTask = xTaskGetCurrentTaskHandle();
TickType_t initialTickCount = xTaskGetTickCount();
waitingTasks.push(currentTask);
uint32_t notificationValue;
BaseType_t notificationStatus = xTaskNotifyWait(0, 0, &notificationValue, ticksToWait);
// Code below will be executed after the task is woken up
while (notificationStatus == pdPASS)
{
if (notificationValue == POWER_AVAILABLE)
{
// We were woken up because new power is available, check if it is enough
if (tryAccquirePowerAllowance(neededPower))
{
return true;
}
else
{
// Still not enough power available for us. Wait the remaining ticks.
xTaskNotifyWait(0, 0, &notificationValue, ticksToWait - (xTaskGetTickCount() - initialTickCount));
}
}
}
if (notificationStatus == pdFALSE)
{
// We waited long enough...
return false;
}
else
{
// Should be impossible to reach
throw "Reached impossible state";
if (notificationStatus == pdFALSE) {
// We waited long enough...
// Remove the task from the list of waiting tasks
for (auto it = currentAllowances.begin(); it != currentAllowances.end();
++it) {
if (it->consumer == consumer &&
it->requestedAt == initialTickCount) {
currentAllowances.erase(it);
break;
}
}
return false;
} else {
// Should be impossible to reach
throw "Reached impossible state";
}
}
}
void PowerScheduler::checkWaitingTasks(void)
{
// Check if there are tasks waiting for power
if (!waitingTasks.empty())
{
TaskHandle_t task = waitingTasks.front();
waitingTasks.pop();
xTaskNotify(task, POWER_AVAILABLE, eSetValueWithOverwrite);
void PowerScheduler::checkWaitingTasks(void) {
// If there are requested allowances, try to grant the one expiring next
if (this->currentAllowances.size() > 0) {
PowerScheduler::CurrentAllowance* nextAllowance =
getNextExpiringAllowance();
if (nextAllowance != nullptr) {
xTaskNotify(nextAllowance->taskHandle,
PowerScheduler::PowerWakeupReasons::POWER_AVAILABLE,
eSetValueWithOverwrite);
}
}
}
PowerScheduler *PowerScheduler::getPowerScheduler()
{
if (powerSchedulerInstance == nullptr)
{
// Double check locking https://www.aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf
taskENTER_CRITICAL(&mux);
if (powerSchedulerInstance == nullptr)
{
powerSchedulerInstance = new PowerScheduler();
}
taskEXIT_CRITICAL(&mux);
void PowerScheduler::recalculateCurrentBudgets(void) {
// TODO: replace with actual current modeling
this->freeLimitCurrentBudget = this->limitCurrent;
this->freeMaximumCurrentBudget = this->maximumCurrent * 2;
for (auto &allowance : currentAllowances) {
if (allowance.granted) {
this->freeLimitCurrentBudget -= allowance.neededCurrent;
this->freeMaximumCurrentBudget -= allowance.neededCurrent;
}
return powerSchedulerInstance;
}
}
PowerScheduler::CurrentAllowance*
PowerScheduler::getCurrentAllowance(PowerParameters::PowerConsumers consumer) {
for (auto &allowance : currentAllowances) {
if (allowance.consumer == consumer) {
return &allowance;
}
}
return nullptr;
}
PowerScheduler::CurrentAllowance*
PowerScheduler::getCurrentAllowance(TaskHandle_t taskHandle) {
for (auto &allowance : currentAllowances) {
if (allowance.taskHandle == taskHandle) {
return &allowance;
}
}
return nullptr;
}
PowerScheduler::CurrentAllowance*
PowerScheduler::getNextExpiringAllowance(void) {
TickType_t minTicks = UINT32_MAX;
CurrentAllowance *nextAllowance = nullptr;
for (auto &allowance : currentAllowances) {
if (!(allowance.granted)) {
TickType_t ticks =
allowance.requestedAt + pdMS_TO_TICKS(allowance.maxSlackTimeMs);
if (ticks < minTicks) {
minTicks = ticks;
nextAllowance = &allowance;
}
}
}
// Will be nullptr if no allowance was found
return nextAllowance;
}
PowerScheduler& PowerScheduler::getPowerScheduler(float i_limit_ma,
float i_max_ma) {
if (powerSchedulerInstance == nullptr) {
// Double check locking
// https://www.aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf
taskENTER_CRITICAL(&mux);
if (powerSchedulerInstance == nullptr) {
powerSchedulerInstance = new PowerScheduler(i_limit_ma, i_max_ma);
}
taskEXIT_CRITICAL(&mux);
}
return *powerSchedulerInstance;
}
PowerScheduler::PowerScheduler(float i_limit_ma, float i_max_ma) {
this->limitCurrent = i_limit_ma;
this->maximumCurrent = i_max_ma;
this->currentAllowances = std::vector<CurrentAllowance>();
mux = portMUX_INITIALIZER_UNLOCKED;
}
portMUX_TYPE PowerScheduler::mux = portMUX_INITIALIZER_UNLOCKED;
PowerScheduler::~PowerScheduler() {}

View File

@ -1,7 +1,8 @@
/**
* @file PowerScheduler.hpp
* @author Phillip Kühne
* @brief The actual power scheduler class, which keeps track of the power budget and allocates power to different components.
* @brief The actual power scheduler class, which keeps track of the power
* budget and allocates power to different components.
* @version 0.1
* @date 2024-12-21
*
@ -9,62 +10,114 @@
*
*/
#include <queue>
#include <Arduino.h>
#include "Consumptions.h"
#include "PowerParameters.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <Arduino.h>
#include <vector>
#ifndef PowerScheduler_h
#define PowerScheduler_h
#define TOTAL_POWER_MILLIWATTS POWER_BUDGET
class PowerScheduler
{
class PowerScheduler {
private:
/* data */
public:
PowerScheduler();
~PowerScheduler();
/// @brief Initialize the singleton instance of the power manager
/// @return reference to the power manager
static PowerScheduler *getPowerScheduler();
uint16_t getFreePowerBudget(void);
/// @brief Request an allowance of a certain number of milliwatts from the power scheduler
/// @param neededPower the amount of power we want to be accounted for (in mW)
/// @return whether the power could be successfully allocated
bool tryAccquirePowerAllowance(uint16_t neededPower);
/// @brief "Return" a certain amount of power when it is no longer needed
/// @param neededPower the amount of power to return (in mW)
/// @return whether the power
void releasePower(uint16_t power);
static constexpr uint16_t DEFAULT_SLACK_TIME_MS = 100;
PowerScheduler(float i_limit_ma, float i_max_ma);
/// @brief Wait for a certain amount of power to be available
/// @param neededPower the amount of power we want to be accounted for (in mW)
/// @param TicksToWait the amount of time to wait for the power to become available
/// @return whether the power could be successfully allocatedy
bool waitForPowerAllowance(uint16_t neededPower, TickType_t TicksToWait);
/// @brief Put the ESP32 into deep sleep mode, without a method to wake up again. Basically this is a shutdown.
void beginPermanentDeepSleep(void);
public:
~PowerScheduler();
/// @brief Initialize the singleton instance of the power manager
/// @return reference to the power manager
static PowerScheduler& getPowerScheduler(float i_limit_ma=0, float i_max_ma=0);
/// @brief Get the current free current budget (to C1 discharge)
/// @return the amount of power that is currently available (in mA)
uint16_t getFreeCurrentBudget(void);
/// @brief Get the current hard maximum free current (to C2 discharge)
/// @return the maximum amount of power that can be allocated (in mA)
uint16_t getFreeHardMaxCurrent(void);
/// @brief Request an allowance of a certain number of milliamperes from the
/// power scheduler without waiting for it (meaning it will not be scheduled
/// for future allocation). Only one can be active per consumer.
/// @param neededCurrent the amount of current we want to be accounted for (in
/// mA)
/// @return whether the current could be successfully allocated
bool tryAccquireCurrentAllowance(PowerParameters::PowerConsumers consumer,
uint16_t neededcurrent,
uint16_t requestedDurationMs = 0);
/// @brief "Return" the current currently allocated to a consumer
/// @param consumer the active consumer to release the current for
void releaseCurrent(PowerParameters::PowerConsumers consumer);
/// @brief Wait for a certain amount of current to be available. This will
/// "reseve a spot in the queue". Only one can be active per consumer.
/// @param neededCurrent the amount of power we want to be accounted for (in
/// mW)
/// @param TicksToWait the amount of time to wait for the power to become
/// available
/// @return whether the power could be successfully allocatedy
bool waitForCurrentAllowance(PowerParameters::PowerConsumers consumer,
uint16_t neededCurrent,
uint16_t maxSlackTimeMs = DEFAULT_SLACK_TIME_MS,
uint16_t requestedDurationMs = 0);
/// @brief Put the ESP32 into deep sleep mode, without a method to wake up
/// again. Basically this is a shutdown.
void beginPermanentDeepSleep(void);
//// @brief Get currently granted power
/// @return the amount of power that is currently allocated (in mA)
uint16_t getCurrentCurrent(void);
/// @brief Power consumer data structure
struct CurrentAllowance {
PowerParameters::PowerConsumers consumer;
uint16_t maxSlackTimeMs;
uint16_t requestedDurationMs;
TaskHandle_t taskHandle;
uint16_t neededCurrent;
TickType_t requestedAt;
TickType_t startedAt;
bool granted;
};
// @brief waiting task wakeup reasons
enum PowerWakeupReasons {
POWER_AVAILABLE = 1,
POWER_EXPIRED = 2,
};
protected:
static const uint16_t totalPowerBudget = TOTAL_POWER_MILLIWATTS;
uint16_t freePowerBudget;
std::queue<TaskHandle_t> waitingTasks;
void checkWaitingTasks(void);
bool takePowerIfAvailable(uint16_t neededPower);
// Current above which there will be no new scheduling
uint16_t limitCurrent;
// Absolute maximum current that can be allocated
uint16_t maximumCurrent;
// Current budget that is currently available to limitCurrent
int16_t freeLimitCurrentBudget;
// Current budget that is currently available to maximumCurrent
int16_t freeMaximumCurrentBudget;
// @brief Responsible for selecting the next task to be granted power
void checkWaitingTasks(void);
// @brief Responsible for recalculating the current budgets
void recalculateCurrentBudgets(void);
// @brief Retrieve the current allowance for a given consumer
CurrentAllowance *
getCurrentAllowance(PowerParameters::PowerConsumers consumer);
// @brief Retrieve the current allowance for a given task
CurrentAllowance *getCurrentAllowance(TaskHandle_t taskHandle);
// @brief Retrieve the allowance that will expire next
CurrentAllowance *getNextExpiringAllowance(void);
// @brief Mutex to protect the power scheduler from concurrent access
static portMUX_TYPE mux;
std::vector<PowerScheduler::CurrentAllowance> currentAllowances;
};
PowerScheduler::PowerScheduler(/* args */)
{
// TODO: Create wrappper around all this which handles single-instancing so we look like a normal arduino library from the outside.
this->freePowerBudget = TOTAL_POWER_MILLIWATTS;
}
PowerScheduler::~PowerScheduler()
{
}
static PowerScheduler *powerSchedulerInstance;