Started implementing real charge control
Everything untested so far…
This commit is contained in:
parent
0982f8d7c9
commit
6b070bbf86
307
src/charge_control.c
Normal file
307
src/charge_control.c
Normal file
|
@ -0,0 +1,307 @@
|
|||
#include <stdbool.h>
|
||||
|
||||
#include <fxp.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
40
src/charge_control.h
Normal file
40
src/charge_control.h
Normal file
|
@ -0,0 +1,40 @@
|
|||
#ifndef CHARGE_CONTROL_H
|
||||
#define CHARGE_CONTROL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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
|
55
src/config.h
Normal file
55
src/config.h
Normal file
|
@ -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
|
93
src/main.c
93
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++;
|
||||
|
|
Loading…
Reference in a new issue