2021-06-07 22:45:17 +02:00
|
|
|
#include <stdbool.h>
|
|
|
|
|
|
|
|
#include <fxp.h>
|
|
|
|
|
|
|
|
#include "power_switch.h"
|
|
|
|
#include "measurement.h"
|
|
|
|
#include "charge_pump.h"
|
2021-06-11 22:35:07 +02:00
|
|
|
#include "rs485.h"
|
2021-06-07 22:45:17 +02:00
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
#include "charge_control.h"
|
|
|
|
|
2021-06-11 22:35:07 +02:00
|
|
|
static const char *CHARGE_STATE_TEXT[] = {
|
|
|
|
"WAIT_CHARGEPUMP",
|
|
|
|
"INITIAL",
|
|
|
|
"TRANSITION",
|
|
|
|
"FLOAT",
|
|
|
|
"SLEEP",
|
|
|
|
"HIGH_TEMPERATURE"
|
|
|
|
};
|
|
|
|
|
|
|
|
static const char *DISCHARGE_STATE_TEXT[] = {
|
|
|
|
"WAIT_CHARGEPUMP",
|
|
|
|
"OK",
|
|
|
|
"VOLTAGE_LOW",
|
|
|
|
"OVERCURRENT"
|
|
|
|
};
|
|
|
|
|
2021-06-07 22:45:17 +02:00
|
|
|
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_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 internal_temperature_limit;
|
|
|
|
static fxp_t internal_temperature_recovery;
|
|
|
|
|
|
|
|
static fxp_t sleep_solar_current;
|
|
|
|
static fxp_t sleep_solar_excess_voltage;
|
|
|
|
|
|
|
|
|
|
|
|
static void control_solar_switch(fxp_t u_bat, fxp_t corridor_high, fxp_t corridor_low)
|
|
|
|
{
|
|
|
|
if(u_bat >= corridor_high) {
|
|
|
|
power_switch_solar_off();
|
|
|
|
} else if(u_bat <= corridor_low) {
|
|
|
|
power_switch_solar_on();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-06-11 22:51:02 +02:00
|
|
|
static void solar_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
|
2021-06-07 22:45:17 +02:00
|
|
|
{
|
2021-06-11 22:51:02 +02:00
|
|
|
uint64_t charge_time_in_state = uptime_ms - charge_state_entered_timestamp;
|
2021-06-07 22:45:17 +02:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2021-06-11 22:51:02 +02:00
|
|
|
// 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) {
|
2021-06-07 22:45:17 +02:00
|
|
|
charge_state = CHARGE_INITIAL;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CHARGE_INITIAL:
|
|
|
|
control_solar_switch(meas->u_bat, u_bat_initial_full, u_bat_initial_low);
|
|
|
|
|
|
|
|
// temperature limit
|
|
|
|
if(meas->temperature > internal_temperature_limit) {
|
|
|
|
charge_state = CHARGE_HIGH_TEMPERATURE;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// time limit for initial charging
|
|
|
|
if(charge_time_in_state > INITIAL_CHARGE_HOLD_TIME) {
|
|
|
|
charge_state = CHARGE_TRANSITION;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// low-current limit (go to sleep at night)
|
|
|
|
if(meas->i_solar < sleep_solar_current) {
|
|
|
|
charge_state = CHARGE_SLEEP;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CHARGE_TRANSITION:
|
|
|
|
// FIXME: dynamically adjust thresholds
|
|
|
|
control_solar_switch(meas->u_bat, u_bat_float_full, u_bat_float_low);
|
|
|
|
|
|
|
|
// temperature limit
|
|
|
|
if(meas->temperature > internal_temperature_limit) {
|
|
|
|
charge_state = CHARGE_HIGH_TEMPERATURE;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// time limit for transition to float charging
|
|
|
|
if(charge_time_in_state > INITIAL_TO_FLOAT_TRANSITION_TIME) {
|
|
|
|
charge_state = CHARGE_FLOAT;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// low-current limit (go to sleep at night)
|
|
|
|
if(meas->i_solar < sleep_solar_current) {
|
|
|
|
charge_state = CHARGE_SLEEP;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CHARGE_FLOAT:
|
|
|
|
control_solar_switch(meas->u_bat, u_bat_float_full, u_bat_float_low);
|
|
|
|
|
|
|
|
// temperature limit
|
|
|
|
if(meas->temperature > internal_temperature_limit) {
|
|
|
|
charge_state = CHARGE_HIGH_TEMPERATURE;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// low-current limit (go to sleep at night)
|
|
|
|
if(meas->i_solar < sleep_solar_current) {
|
|
|
|
charge_state = CHARGE_SLEEP;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
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;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case CHARGE_HIGH_TEMPERATURE:
|
|
|
|
if(charge_state_entered) {
|
|
|
|
power_switch_solar_off();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(meas->temperature < internal_temperature_recovery) {
|
|
|
|
charge_state = CHARGE_WAIT_CHARGEPUMP;
|
2021-06-12 17:07:16 +02:00
|
|
|
break;
|
2021-06-07 22:45:17 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
// unknown state
|
|
|
|
break;
|
|
|
|
}
|
2021-06-11 22:51:02 +02:00
|
|
|
}
|
2021-06-07 22:45:17 +02:00
|
|
|
|
2021-06-11 22:51:02 +02:00
|
|
|
|
|
|
|
static void load_fsm_update(uint64_t uptime_ms, struct MeasurementResult *meas)
|
|
|
|
{
|
|
|
|
uint64_t discharge_time_in_state = uptime_ms - discharge_state_entered_timestamp;
|
2021-06-07 22:45:17 +02:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2021-06-11 22:51:02 +02:00
|
|
|
// 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) {
|
2021-06-07 22:45:17 +02:00
|
|
|
discharge_state = DISCHARGE_VOLTAGE_LOW;
|
|
|
|
}
|
|
|
|
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) {
|
|
|
|
// TODO: maybe only check this 10 ms after load is switched on
|
|
|
|
// to allow for inrush current?
|
|
|
|
discharge_state = DISCHARGE_OVERCURRENT;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(meas->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->u_bat > u_bat_load_on)
|
|
|
|
&& (discharge_time_in_state > LOAD_ON_DELAY)) {
|
|
|
|
discharge_state = DISCHARGE_OK;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case DISCHARGE_OVERCURRENT:
|
|
|
|
// Battery voltage is too low, so keep the load switched off
|
|
|
|
if(discharge_state_entered) {
|
|
|
|
power_switch_load_off();
|
|
|
|
}
|
|
|
|
|
|
|
|
// no way out except reset
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
// unknown state
|
|
|
|
break;
|
|
|
|
}
|
2021-06-11 22:51:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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(U_BAT_REGULATION_CORRIDOR),
|
|
|
|
FXP_FROM_INT(1000));
|
|
|
|
|
2021-06-13 14:55:43 +02:00
|
|
|
u_bat_initial_full = fxp_div(FXP_FROM_INT(U_BAT_INITIAL_FULL), FXP_FROM_INT(1000));
|
2021-06-11 22:51:02 +02:00
|
|
|
u_bat_initial_low = fxp_sub(u_bat_initial_full, u_bat_regulation_corridor);
|
|
|
|
|
|
|
|
u_bat_float_full = fxp_div(FXP_FROM_INT(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(MIN_CHARGE_PUMP_EXCESS_VOLTAGE),
|
|
|
|
FXP_FROM_INT(1000));
|
|
|
|
|
|
|
|
u_bat_load_on = fxp_div(FXP_FROM_INT(U_BAT_LOAD_ON), FXP_FROM_INT(1000));
|
|
|
|
u_bat_load_off = fxp_div(FXP_FROM_INT(U_BAT_LOAD_OFF), FXP_FROM_INT(1000));
|
|
|
|
|
|
|
|
load_current_limit = fxp_div(FXP_FROM_INT(LOAD_CURRENT_LIMIT_MA), FXP_FROM_INT(1000));
|
|
|
|
|
|
|
|
internal_temperature_limit = fxp_div(FXP_FROM_INT(INTERNAL_TEMPERATURE_LIMIT), FXP_FROM_INT(10));
|
|
|
|
internal_temperature_recovery = fxp_div(FXP_FROM_INT(INTERNAL_TEMPERATURE_RECOVERY), FXP_FROM_INT(10));
|
|
|
|
|
|
|
|
sleep_solar_current = fxp_div(FXP_FROM_INT(SLEEP_SOLAR_CURRENT), FXP_FROM_INT(1000));
|
|
|
|
sleep_solar_excess_voltage = fxp_div(FXP_FROM_INT(SLEEP_SOLAR_EXCESS_VOLTAGE), FXP_FROM_INT(1000));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
2021-06-12 17:07:16 +02:00
|
|
|
} else if(((charge_state == CHARGE_SLEEP) || charge_control_is_charge_blocked())
|
|
|
|
&& ((discharge_state == DISCHARGE_VOLTAGE_LOW) || charge_control_is_discharge_blocked())) {
|
2021-06-11 22:51:02 +02:00
|
|
|
// 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);
|
2021-06-07 22:45:17 +02:00
|
|
|
|
|
|
|
charge_state_entered = charge_state != last_charge_state;
|
|
|
|
discharge_state_entered = discharge_state != last_discharge_state;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-06-11 23:56:12 +02:00
|
|
|
bool charge_control_is_idle(void)
|
|
|
|
{
|
|
|
|
return ((charge_state == CHARGE_SLEEP)
|
|
|
|
&& (discharge_state == DISCHARGE_VOLTAGE_LOW));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-06-07 22:45:17 +02:00
|
|
|
bool charge_control_is_charge_blocked(void)
|
|
|
|
{
|
|
|
|
switch(charge_state) {
|
|
|
|
case CHARGE_HIGH_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;
|
|
|
|
}
|
|
|
|
}
|