From 6b070bbf86cf6161fc7e071ae77cdd66be107ee0 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Mon, 7 Jun 2021 22:45:17 +0200 Subject: [PATCH] Started implementing real charge control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything untested so far… --- src/charge_control.c | 307 +++++++++++++++++++++++++++++++++++++++++++ src/charge_control.h | 40 ++++++ src/config.h | 55 ++++++++ src/main.c | 93 ++++++++----- 4 files changed, 463 insertions(+), 32 deletions(-) create mode 100644 src/charge_control.c create mode 100644 src/charge_control.h create mode 100644 src/config.h diff --git a/src/charge_control.c b/src/charge_control.c new file mode 100644 index 0000000..566d624 --- /dev/null +++ b/src/charge_control.c @@ -0,0 +1,307 @@ +#include + +#include + +#include "power_switch.h" +#include "measurement.h" +#include "charge_pump.h" +#include "config.h" + +#include "charge_control.h" + +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(); + } +} + + +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)); + + u_bat_initial_full = fxp_div(FXP_FROM_INT(U_BAT_INITAL_FULL), FXP_FROM_INT(1000)); + 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) { + charge_state_entered_timestamp = uptime_ms; + } + + if(discharge_state_entered) { + discharge_state_entered_timestamp = uptime_ms; + } + + uint64_t charge_time_in_state = uptime_ms - charge_state_entered_timestamp; + uint64_t discharge_time_in_state = uptime_ms - discharge_state_entered_timestamp; + + /* calculate charge pump excess voltage above battery voltage. */ + fxp_t charge_pump_voltage_delta = fxp_sub(meas->u_sw, meas->u_bat); + + /* 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) + && (discharge_state == DISCHARGE_VOLTAGE_LOW)) { + // 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(); + } + } + + /* Charging FSM */ + + 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(); + } + + if(charge_pump_voltage_delta > min_charge_pump_excess_voltage) { + 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; + } + + // time limit for initial charging + if(charge_time_in_state > INITIAL_CHARGE_HOLD_TIME) { + charge_state = CHARGE_TRANSITION; + } + + // low-current limit (go to sleep at night) + if(meas->i_solar < sleep_solar_current) { + charge_state = CHARGE_SLEEP; + } + 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; + } + + // time limit for transition to float charging + if(charge_time_in_state > INITIAL_TO_FLOAT_TRANSITION_TIME) { + charge_state = CHARGE_FLOAT; + } + + // low-current limit (go to sleep at night) + if(meas->i_solar < sleep_solar_current) { + charge_state = CHARGE_SLEEP; + } + 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; + } + + // low-current limit (go to sleep at night) + if(meas->i_solar < sleep_solar_current) { + charge_state = CHARGE_SLEEP; + } + 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; + + + case CHARGE_HIGH_TEMPERATURE: + if(charge_state_entered) { + power_switch_solar_off(); + } + + if(meas->temperature < internal_temperature_recovery) { + charge_state = CHARGE_WAIT_CHARGEPUMP; + } + break; + + default: + // unknown state + break; + } + + /* Load control FSM */ + + 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(); + } + + if(charge_pump_voltage_delta > min_charge_pump_excess_voltage) { + 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; + } + + charge_state_entered = charge_state != last_charge_state; + discharge_state_entered = discharge_state != last_discharge_state; +} + + +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; + } +} diff --git a/src/charge_control.h b/src/charge_control.h new file mode 100644 index 0000000..5cf8b67 --- /dev/null +++ b/src/charge_control.h @@ -0,0 +1,40 @@ +#ifndef CHARGE_CONTROL_H +#define CHARGE_CONTROL_H + +#include +#include + +// forward declarations +struct MeasurementResult; + +enum ChargeState +{ + CHARGE_WAIT_CHARGEPUMP, + CHARGE_INITIAL, + CHARGE_TRANSITION, + CHARGE_FLOAT, + CHARGE_SLEEP, + CHARGE_HIGH_TEMPERATURE +}; + +enum DischargeState +{ + DISCHARGE_WAIT_CHARGEPUMP, + DISCHARGE_OK, + DISCHARGE_VOLTAGE_LOW, + DISCHARGE_OVERCURRENT +}; + +// Error flags + +#define LOAD_OVERCURRENT (1 << 0) +#define CHARGE_PUMP_ERROR (1 << 1) +#define OVER_TEMPERATURE (1 << 2) + +void charge_control_init(void); +void charge_control_update(uint64_t uptime_ms, struct MeasurementResult *meas); + +bool charge_control_is_charge_blocked(void); +bool charge_control_is_discharge_blocked(void); + +#endif // CHARGE_CONTROL_H diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..b9804ce --- /dev/null +++ b/src/config.h @@ -0,0 +1,55 @@ +#ifndef CONFIG_H +#define CONFIG_H + +/* Thresholds for charging control */ + +/* Battery regulation corridor width (in mV). */ +#define U_BAT_REGULATION_CORRIDOR 100 + +/* Initial charge battery voltage threshold (in mV). */ +#define U_BAT_INITAL_FULL 28800 // stop charging if battery voltage reaches this threshold + +/* Transition to floating voltage levels after this time (in ms). */ +#define INITIAL_CHARGE_HOLD_TIME 3600000 + +/* Duration of the transistion from initial charging to float (in ms). */ +#define INITIAL_TO_FLOAT_TRANSITION_TIME 600000 + +/* Float charge battery voltage threshold (in mV). */ +#define U_BAT_FLOAT_FULL 27600 // stop charging if battery voltage reaches this threshold + +/* Minimum voltage difference to U_bat that the solar panels must produce + * before charging is resumed after it was switched off (in mV). */ +#define SLEEP_SOLAR_EXCESS_VOLTAGE 1000 + +/* Minimum charge current required before charging is stopped to save power at + * the charge pump (in mA). */ +#define SLEEP_SOLAR_CURRENT 5 + +/* Maximum allowed microcontroller temperature (in units of 0.1 °C). If this + * temperature is exceeded, charging is stopped. The load is kept on. Do not + * set this too high as the heat has to propagate from the power MOSFETs. */ +#define INTERNAL_TEMPERATURE_LIMIT 500 + +/* Resume operation below this temperature (in units of 0.1 °C). */ +#define INTERNAL_TEMPERATURE_RECOVERY 450 + + +/* Thresholds for load control */ + +/* Voltage above which the load is turned on (in mV). */ +#define U_BAT_LOAD_ON 27000 +/* Voltage below which the load is turned off (in mV). */ +#define U_BAT_LOAD_OFF 24000 + +/* Current at which the overload protection triggers (in mA). */ +#define LOAD_CURRENT_LIMIT_MA 10000 + +/* Minimum voltage that the charge pump must produce above U_bat before any + * power FET is switched on (in mV). */ +#define MIN_CHARGE_PUMP_EXCESS_VOLTAGE 10000 + +/* The minimum time the load must be off before it can be switched on again (in ms). */ +#define LOAD_ON_DELAY 10000 + +#endif // CONFIG_H diff --git a/src/main.c b/src/main.c index 7b5d2bc..c71da8c 100644 --- a/src/main.c +++ b/src/main.c @@ -17,6 +17,7 @@ #include "led_chplex.h" #include "rs485.h" #include "charge_pump.h" +#include "charge_control.h" #include "power_switch.h" #include "measurement.h" @@ -98,6 +99,61 @@ static bool ledtest(uint64_t timebase_ms) } +static void update_leds(uint64_t uptime_ms, struct MeasurementResult *meas_data) +{ + static fxp_t charge_in_mAs = 0; + static fxp_t charge_out_mAs = 0; + + static uint64_t charge_pulse_until = 0; + static uint64_t discharge_pulse_until = 0; + + charge_in_mAs = fxp_add(charge_in_mAs, meas_data->i_solar); + charge_out_mAs = fxp_add(charge_out_mAs, meas_data->i_load); + + if(charge_in_mAs > FXP_FROM_INT(1000)) { + led_chplex_on(LED_CHPLEX_IDX_CHARGE_PULSE); + charge_pulse_until = uptime_ms + 12; + + charge_in_mAs = fxp_sub(charge_in_mAs, FXP_FROM_INT(1000)); + } else if(uptime_ms > charge_pulse_until) { + led_chplex_off(LED_CHPLEX_IDX_CHARGE_PULSE); + } + + if(charge_out_mAs > FXP_FROM_INT(1000)) { + led_chplex_on(LED_CHPLEX_IDX_DISCHARGE_PULSE); + discharge_pulse_until = uptime_ms + 12; + + charge_out_mAs = fxp_sub(charge_out_mAs, FXP_FROM_INT(1000)); + } else if(uptime_ms > discharge_pulse_until) { + led_chplex_off(LED_CHPLEX_IDX_DISCHARGE_PULSE); + } + + if(charge_control_is_charge_blocked()) { + led_chplex_on(LED_CHPLEX_IDX_ERR_TEMP); + } else { + led_chplex_off(LED_CHPLEX_IDX_ERR_TEMP); + } + + if(charge_control_is_discharge_blocked()) { + led_chplex_on(LED_CHPLEX_IDX_ERR_LOAD); + } else { + led_chplex_off(LED_CHPLEX_IDX_ERR_LOAD); + } + + if(power_switch_solar_status()) { + led_chplex_on(LED_CHPLEX_IDX_SOLAR_ON); + } else { + led_chplex_off(LED_CHPLEX_IDX_SOLAR_ON); + } + + if(power_switch_load_status()) { + led_chplex_on(LED_CHPLEX_IDX_LOAD_ON); + } else { + led_chplex_off(LED_CHPLEX_IDX_LOAD_ON); + } +} + + static void report_status(struct MeasurementResult *meas_data) { char number[FXP_STR_MAXLEN]; @@ -163,7 +219,6 @@ int main(void) led_chplex_periodic(); } else if(!startup_done) { charge_pump_start(); - power_switch_load_on(); // FIXME: just for testing! startup_done = true; } else { @@ -173,46 +228,20 @@ int main(void) // completion. This is a good place for tasks that are not critical in // latency, such as updating the LEDs, sending the state over RS485 etc. - if(timebase_ms % 500 == 0) { - led_chplex_toggle(LED_CHPLEX_IDX_DISCHARGE_PULSE); - } - - // FIXME: just for testing - if(timebase_ms % 10000 == 0) { - switchtest++; - - if(switchtest & 0x01) { - power_switch_solar_on(); - led_chplex_on(LED_CHPLEX_IDX_SOLAR_ON); - } else { - power_switch_solar_off(); - led_chplex_off(LED_CHPLEX_IDX_SOLAR_ON); - } - - if(switchtest & 0x02) { - power_switch_load_on(); - led_chplex_on(LED_CHPLEX_IDX_LOAD_ON); - } else { - power_switch_load_off(); - led_chplex_off(LED_CHPLEX_IDX_LOAD_ON); - } - } - + update_leds(timebase_ms, &meas_data); led_chplex_periodic(); // Send the status data from the last cycle. - if(timebase_ms % 1000 == 0) { + if(timebase_ms % 500 == 0) { report_status(&meas_data); } measurement_wait_for_completion(); - if(timebase_ms % 1000 == 100) { measurement_finalize(&meas_data); - } - // Check the protections directly after the measurement finishes. This - // ensures fast reaction time. - // TODO: check_protections(&meas_data); + // Update the charge controller immediately after the measurement. + // This ensures fast reaction time to overcurrent/overvoltage. + charge_control_update(timebase_ms, &meas_data); } timebase_ms++;