Thomas Kolb
19735ee550
Whenever overload is detected, the time that must pass before the load is turned on again is doubled. If the load was on for 5 minutes, the retry time is reset to the configured value.
490 lines
13 KiB
C
490 lines
13 KiB
C
#include <stdbool.h>
|
||
|
||
#include <fxp.h>
|
||
|
||
#include "bmp280.h"
|
||
#include "power_switch.h"
|
||
#include "measurement.h"
|
||
#include "charge_pump.h"
|
||
#include "rs485.h"
|
||
#include "flash_config.h"
|
||
#include "addon_io.h"
|
||
|
||
#include "charge_control.h"
|
||
|
||
static const char *CHARGE_STATE_TEXT[CHARGE_NUM_STATES] = {
|
||
"WAIT_CHARGEPUMP",
|
||
"INITIAL",
|
||
"INITIAL_HOLD",
|
||
"TRANSITION",
|
||
"FLOAT",
|
||
"SLEEP",
|
||
"HIGH_INTERNAL_TEMPERATURE",
|
||
"LOW_EXTERNAL_TEMPERATURE"
|
||
};
|
||
|
||
static const char *DISCHARGE_STATE_TEXT[DISCHARGE_NUM_STATES] = {
|
||
"WAIT_CHARGEPUMP",
|
||
"OK",
|
||
"VOLTAGE_LOW",
|
||
"OVERCURRENT",
|
||
"OVERCURRENT_DELAY"
|
||
};
|
||
|
||
static enum ChargeState charge_state;
|
||
static enum DischargeState discharge_state;
|
||
|
||
static bool charge_state_entered;
|
||
static bool discharge_state_entered;
|
||
|
||
static uint64_t charge_state_entered_timestamp;
|
||
static uint64_t discharge_state_entered_timestamp;
|
||
|
||
static fxp_t u_bat_regulation_corridor;
|
||
|
||
static fxp_t u_bat_initial_full;
|
||
static fxp_t u_bat_initial_low;
|
||
|
||
static fxp_t u_bat_initial_hold_cancel;
|
||
|
||
static fxp_t u_bat_float_full;
|
||
static fxp_t u_bat_float_low;
|
||
|
||
static fxp_t min_charge_pump_excess_voltage;
|
||
|
||
static fxp_t u_bat_load_on;
|
||
static fxp_t u_bat_load_off;
|
||
|
||
static fxp_t load_current_limit;
|
||
static fxp_t load_current_limit_delay;
|
||
|
||
static fxp_t internal_temperature_limit;
|
||
static fxp_t internal_temperature_recovery;
|
||
|
||
static fxp_t external_temperature_limit;
|
||
static fxp_t external_temperature_recovery;
|
||
|
||
static fxp_t sleep_solar_current;
|
||
static fxp_t sleep_solar_excess_voltage;
|
||
|
||
static uint32_t overload_retry_time;
|
||
|
||
|
||
static enum ChargeState control_solar_charging(
|
||
fxp_t corridor_high,
|
||
fxp_t corridor_low,
|
||
uint64_t uptime_ms,
|
||
struct MeasurementResult *meas,
|
||
enum ChargeState current_state,
|
||
uint64_t time_in_state)
|
||
{
|
||
static uint64_t last_switch_change_time = 0;
|
||
uint64_t solar_switch_onoff_duration = uptime_ms - last_switch_change_time;
|
||
|
||
bool last_switch_state = power_switch_solar_status();
|
||
|
||
if(meas->u_bat >= corridor_high) {
|
||
power_switch_solar_off();
|
||
} else if(meas->u_bat <= corridor_low) {
|
||
power_switch_solar_on();
|
||
}
|
||
|
||
bool current_switch_state = power_switch_solar_status();
|
||
|
||
if(last_switch_state != current_switch_state) { // switch changed
|
||
last_switch_change_time = uptime_ms;
|
||
solar_switch_onoff_duration = 0;
|
||
}
|
||
|
||
// internal temperature limit: prevent overheating of the power transistors.
|
||
if(meas->avg_temperature > internal_temperature_limit) {
|
||
return CHARGE_HIGH_INTERNAL_TEMPERATURE;
|
||
}
|
||
|
||
// external temperature limit: prevent charging the battery if temperature is too low.
|
||
if(bmp280_are_measurements_valid() &&
|
||
bmp280_get_temperature() < external_temperature_limit) {
|
||
return CHARGE_LOW_EXTERNAL_TEMPERATURE;
|
||
}
|
||
|
||
// low-current limit (go to sleep at night)
|
||
if((time_in_state > FLASH_CONFIG_SLEEP_STATE_DELAY)
|
||
&& (current_switch_state == true)
|
||
&& (solar_switch_onoff_duration > FLASH_CONFIG_SLEEP_SWITCH_DELAY)
|
||
&& (meas->avg_i_solar < sleep_solar_current)) {
|
||
return CHARGE_SLEEP;
|
||
}
|
||
|
||
return current_state;
|
||
}
|
||
|
||
|
||
static void solar_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
|
||
{
|
||
uint64_t charge_time_in_state = uptime_ms - charge_state_entered_timestamp;
|
||
|
||
switch(charge_state) {
|
||
case CHARGE_WAIT_CHARGEPUMP:
|
||
// force the solar switch off until the charge pump voltage reaches a safe level.
|
||
if(charge_state_entered) {
|
||
power_switch_solar_off();
|
||
}
|
||
|
||
// calculate charge pump output excess voltage over battery voltage
|
||
// and compare to the threshold
|
||
if(fxp_sub(meas->u_sw, meas->u_bat) > min_charge_pump_excess_voltage) {
|
||
charge_state = CHARGE_INITIAL;
|
||
}
|
||
break;
|
||
|
||
case CHARGE_INITIAL:
|
||
charge_state = control_solar_charging(
|
||
u_bat_initial_full,
|
||
u_bat_initial_low,
|
||
uptime_ms,
|
||
meas,
|
||
charge_state,
|
||
charge_time_in_state);
|
||
|
||
// switch to hold state when high threshold is reached
|
||
if(meas->u_bat >= u_bat_initial_full) {
|
||
charge_state = CHARGE_INITIAL_HOLD;
|
||
}
|
||
|
||
break;
|
||
|
||
case CHARGE_INITIAL_HOLD:
|
||
charge_state = control_solar_charging(
|
||
u_bat_initial_full,
|
||
u_bat_initial_low,
|
||
uptime_ms,
|
||
meas,
|
||
charge_state,
|
||
charge_time_in_state);
|
||
|
||
// cancel charge hold if battery voltage is below threshold
|
||
if(meas->u_bat <= u_bat_initial_hold_cancel) {
|
||
charge_state = CHARGE_INITIAL;
|
||
}
|
||
|
||
// time limit for initial hold charging
|
||
if(charge_time_in_state > FLASH_CONFIG_INITIAL_CHARGE_HOLD_TIME) {
|
||
charge_state = CHARGE_TRANSITION;
|
||
}
|
||
|
||
break;
|
||
|
||
case CHARGE_TRANSITION:
|
||
if(charge_time_in_state < FLASH_CONFIG_INITIAL_TO_FLOAT_TRANSITION_TIME) {
|
||
// dynamically adjust thresholds
|
||
fxp_t u_bat_full =
|
||
fxp_add(u_bat_initial_full,
|
||
fxp_mult(
|
||
fxp_sub(u_bat_float_full, u_bat_initial_full),
|
||
fxp_div(charge_time_in_state, FLASH_CONFIG_INITIAL_TO_FLOAT_TRANSITION_TIME)));
|
||
|
||
fxp_t u_bat_low = fxp_sub(u_bat_full, u_bat_regulation_corridor);
|
||
|
||
charge_state = control_solar_charging(
|
||
u_bat_full,
|
||
u_bat_low,
|
||
uptime_ms,
|
||
meas,
|
||
charge_state,
|
||
charge_time_in_state);
|
||
} else {
|
||
// time limit for transition to float charging reached
|
||
charge_state = CHARGE_FLOAT;
|
||
break;
|
||
}
|
||
|
||
break;
|
||
|
||
case CHARGE_FLOAT:
|
||
charge_state = control_solar_charging(
|
||
u_bat_float_full,
|
||
u_bat_float_low,
|
||
uptime_ms,
|
||
meas,
|
||
charge_state,
|
||
charge_time_in_state);
|
||
|
||
// temperature limit
|
||
if(meas->temperature > internal_temperature_limit) {
|
||
charge_state = CHARGE_HIGH_INTERNAL_TEMPERATURE;
|
||
break;
|
||
}
|
||
break;
|
||
|
||
case CHARGE_SLEEP:
|
||
if(charge_state_entered) {
|
||
power_switch_solar_off();
|
||
}
|
||
|
||
{
|
||
fxp_t solar_excess_voltage = fxp_sub(meas->u_solar, meas->u_bat);
|
||
|
||
if(solar_excess_voltage > sleep_solar_excess_voltage) {
|
||
// resume operation
|
||
charge_state = CHARGE_WAIT_CHARGEPUMP;
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
|
||
|
||
case CHARGE_HIGH_INTERNAL_TEMPERATURE:
|
||
if(charge_state_entered) {
|
||
power_switch_solar_off();
|
||
}
|
||
|
||
if(meas->temperature < internal_temperature_recovery) {
|
||
charge_state = CHARGE_WAIT_CHARGEPUMP;
|
||
break;
|
||
}
|
||
break;
|
||
|
||
|
||
case CHARGE_LOW_EXTERNAL_TEMPERATURE:
|
||
if(charge_state_entered) {
|
||
power_switch_solar_off();
|
||
|
||
// switch on the heater via the isolated I/O addon board
|
||
addon_io_iso_out_on(0);
|
||
}
|
||
|
||
// this state can only be entered if the BMP280 measurement is valid, so
|
||
// no need to check it again here.
|
||
if(bmp280_get_temperature() > external_temperature_recovery) {
|
||
charge_state = CHARGE_WAIT_CHARGEPUMP;
|
||
|
||
// switch the heater off again when this state is left
|
||
addon_io_iso_out_off(0);
|
||
break;
|
||
}
|
||
break;
|
||
|
||
|
||
default:
|
||
// unknown state
|
||
break;
|
||
}
|
||
}
|
||
|
||
|
||
static void load_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
|
||
{
|
||
uint64_t discharge_time_in_state = uptime_ms - discharge_state_entered_timestamp;
|
||
|
||
switch(discharge_state) {
|
||
case DISCHARGE_WAIT_CHARGEPUMP:
|
||
// force the load off until the charge pump voltage reaches a safe level.
|
||
if(discharge_state_entered) {
|
||
power_switch_load_off();
|
||
}
|
||
|
||
// calculate charge pump output excess voltage over battery voltage
|
||
// and compare to the threshold
|
||
if(fxp_sub(meas->u_sw, meas->u_bat) > min_charge_pump_excess_voltage) {
|
||
discharge_state = DISCHARGE_OK;
|
||
}
|
||
break;
|
||
|
||
case DISCHARGE_OK:
|
||
// Battery voltage is in a safe range, so keep the load switched on
|
||
if(discharge_state_entered) {
|
||
power_switch_load_on();
|
||
}
|
||
|
||
if((meas->i_load > load_current_limit)
|
||
&& (discharge_time_in_state > FLASH_CONFIG_LOAD_CURRENT_INRUSH_TIME)) {
|
||
if(load_current_limit_delay == 0) {
|
||
// switch off immediately
|
||
power_switch_load_off();
|
||
discharge_state = DISCHARGE_OVERCURRENT;
|
||
} else {
|
||
discharge_state = DISCHARGE_OVERCURRENT_DELAY;
|
||
}
|
||
}
|
||
|
||
if((overload_retry_time != FLASH_CONFIG_OVERLOAD_RETRY_TIME) &&
|
||
(discharge_time_in_state > 300000)) {
|
||
// overload did not trigger for 5 minutes, so we assume it’s stable and
|
||
// reset the retry time delay.
|
||
overload_retry_time = FLASH_CONFIG_OVERLOAD_RETRY_TIME;
|
||
}
|
||
|
||
if(meas->avg_u_bat < u_bat_load_off) {
|
||
discharge_state = DISCHARGE_VOLTAGE_LOW;
|
||
}
|
||
break;
|
||
|
||
case DISCHARGE_VOLTAGE_LOW:
|
||
// Battery voltage is too low, so keep the load switched off
|
||
if(discharge_state_entered) {
|
||
power_switch_load_off();
|
||
}
|
||
|
||
// Can only switch on again after a specific amount of time has passed
|
||
if((meas->avg_u_bat > u_bat_load_on)
|
||
&& (discharge_time_in_state > FLASH_CONFIG_LOAD_ON_DELAY)) {
|
||
discharge_state = DISCHARGE_WAIT_CHARGEPUMP;
|
||
}
|
||
break;
|
||
|
||
case DISCHARGE_OVERCURRENT_DELAY:
|
||
if(meas->i_load < load_current_limit) {
|
||
// current recovered
|
||
discharge_state = DISCHARGE_OK;
|
||
} else if(discharge_time_in_state >= FLASH_CONFIG_OVERLOAD_DELAY_TIME) {
|
||
// switch off immediately
|
||
power_switch_load_off();
|
||
discharge_state = DISCHARGE_OVERCURRENT;
|
||
}
|
||
break;
|
||
|
||
case DISCHARGE_OVERCURRENT:
|
||
// Current limit reached
|
||
if(discharge_state_entered) {
|
||
power_switch_load_off();
|
||
}
|
||
|
||
// Overload recovery
|
||
if(discharge_time_in_state >= overload_retry_time) {
|
||
// double the overload retry time for the next turn if it is less than 7 days
|
||
if(overload_retry_time < (7*24*3600*1000)) {
|
||
overload_retry_time *= 2;
|
||
}
|
||
|
||
discharge_state = DISCHARGE_WAIT_CHARGEPUMP;
|
||
}
|
||
break;
|
||
|
||
default:
|
||
// unknown state
|
||
break;
|
||
}
|
||
}
|
||
|
||
|
||
void charge_control_init(void)
|
||
{
|
||
charge_state = CHARGE_WAIT_CHARGEPUMP;
|
||
discharge_state = DISCHARGE_WAIT_CHARGEPUMP;
|
||
|
||
charge_state_entered = true;
|
||
discharge_state_entered = true;
|
||
|
||
/* calculate thresholds */
|
||
u_bat_regulation_corridor = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_REGULATION_CORRIDOR),
|
||
FXP_FROM_INT(1000));
|
||
|
||
u_bat_initial_full = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_INITIAL_FULL), FXP_FROM_INT(1000));
|
||
u_bat_initial_low = fxp_sub(u_bat_initial_full, u_bat_regulation_corridor);
|
||
|
||
u_bat_initial_hold_cancel = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_INITIAL_HOLD_CANCEL), FXP_FROM_INT(1000));
|
||
|
||
u_bat_float_full = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_FLOAT_FULL), FXP_FROM_INT(1000));
|
||
u_bat_float_low = fxp_sub(u_bat_float_full, u_bat_regulation_corridor);
|
||
|
||
min_charge_pump_excess_voltage = fxp_div(FXP_FROM_INT(FLASH_CONFIG_MIN_CHARGE_PUMP_EXCESS_VOLTAGE),
|
||
FXP_FROM_INT(1000));
|
||
|
||
u_bat_load_on = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_LOAD_ON), FXP_FROM_INT(1000));
|
||
u_bat_load_off = fxp_div(FXP_FROM_INT(FLASH_CONFIG_U_BAT_LOAD_OFF), FXP_FROM_INT(1000));
|
||
|
||
load_current_limit = fxp_div(FXP_FROM_INT(FLASH_CONFIG_LOAD_CURRENT_LIMIT_MA), FXP_FROM_INT(1000));
|
||
|
||
internal_temperature_limit = fxp_div(FXP_FROM_INT(FLASH_CONFIG_INTERNAL_TEMPERATURE_LIMIT), FXP_FROM_INT(10));
|
||
internal_temperature_recovery = fxp_div(FXP_FROM_INT(FLASH_CONFIG_INTERNAL_TEMPERATURE_RECOVERY), FXP_FROM_INT(10));
|
||
|
||
external_temperature_limit = fxp_div(FXP_FROM_INT(FLASH_CONFIG_EXTERNAL_TEMPERATURE_LIMIT), FXP_FROM_INT(10));
|
||
external_temperature_recovery = fxp_div(FXP_FROM_INT(FLASH_CONFIG_EXTERNAL_TEMPERATURE_RECOVERY), FXP_FROM_INT(10));
|
||
|
||
sleep_solar_current = fxp_div(FXP_FROM_INT(FLASH_CONFIG_SLEEP_SOLAR_CURRENT), FXP_FROM_INT(1000));
|
||
sleep_solar_excess_voltage = fxp_div(FXP_FROM_INT(FLASH_CONFIG_SLEEP_SOLAR_EXCESS_VOLTAGE), FXP_FROM_INT(1000));
|
||
|
||
overload_retry_time = FLASH_CONFIG_OVERLOAD_RETRY_TIME;
|
||
}
|
||
|
||
|
||
void charge_control_update(uint64_t uptime_ms, struct MeasurementResult *meas)
|
||
{
|
||
/* state change tracking for efficient transistions. */
|
||
enum ChargeState last_charge_state = charge_state;
|
||
enum DischargeState last_discharge_state = discharge_state;
|
||
|
||
if(charge_state_entered) {
|
||
rs485_enqueue("STATE:CHARGE:");
|
||
rs485_enqueue(CHARGE_STATE_TEXT[charge_state]);
|
||
rs485_enqueue("\n");
|
||
charge_state_entered_timestamp = uptime_ms;
|
||
}
|
||
|
||
if(discharge_state_entered) {
|
||
rs485_enqueue("STATE:DISCHG:");
|
||
rs485_enqueue(DISCHARGE_STATE_TEXT[discharge_state]);
|
||
rs485_enqueue("\n");
|
||
discharge_state_entered_timestamp = uptime_ms;
|
||
}
|
||
|
||
/* generalized charge pump control */
|
||
if(charge_state_entered || discharge_state_entered) {
|
||
if(charge_state == CHARGE_WAIT_CHARGEPUMP
|
||
|| discharge_state == DISCHARGE_WAIT_CHARGEPUMP) {
|
||
// either charge or discharge control is waiting for the charge
|
||
// pump, so power it up!
|
||
charge_pump_start();
|
||
} else if(((charge_state == CHARGE_SLEEP) || charge_control_is_charge_blocked())
|
||
&& ((discharge_state == DISCHARGE_VOLTAGE_LOW) || charge_control_is_discharge_blocked())) {
|
||
// no power from the solar panel and the battery voltage is too
|
||
// low, so both switches are off and we can safely stop the charge
|
||
// pump
|
||
charge_pump_stop();
|
||
}
|
||
}
|
||
|
||
solar_fsm_update(uptime_ms, meas);
|
||
load_fsm_update(uptime_ms, meas);
|
||
|
||
charge_state_entered = charge_state != last_charge_state;
|
||
discharge_state_entered = discharge_state != last_discharge_state;
|
||
}
|
||
|
||
|
||
bool charge_control_is_idle(void)
|
||
{
|
||
return (((charge_state == CHARGE_SLEEP)
|
||
|| (charge_state == CHARGE_HIGH_INTERNAL_TEMPERATURE)
|
||
|| (charge_state == CHARGE_LOW_EXTERNAL_TEMPERATURE))
|
||
&& ((discharge_state == DISCHARGE_VOLTAGE_LOW)
|
||
|| (discharge_state == DISCHARGE_OVERCURRENT)));
|
||
}
|
||
|
||
|
||
bool charge_control_is_charge_blocked(void)
|
||
{
|
||
switch(charge_state) {
|
||
case CHARGE_HIGH_INTERNAL_TEMPERATURE:
|
||
case CHARGE_LOW_EXTERNAL_TEMPERATURE:
|
||
case CHARGE_WAIT_CHARGEPUMP:
|
||
return true;
|
||
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
bool charge_control_is_discharge_blocked(void)
|
||
{
|
||
switch(discharge_state) {
|
||
case DISCHARGE_OVERCURRENT:
|
||
case DISCHARGE_WAIT_CHARGEPUMP:
|
||
return true;
|
||
|
||
default:
|
||
return false;
|
||
}
|
||
}
|