Battery-Emulator/Software/src/battery/CHADEMO-BATTERY.cpp
2025-04-26 23:43:51 +03:00

1083 lines
42 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "../include.h"
#ifdef CHADEMO_BATTERY
#include "../datalayer/datalayer.h"
#include "../devboard/utils/events.h"
#include "CHADEMO-BATTERY-INTERNAL.h"
#include "CHADEMO-BATTERY.h"
#include "CHADEMO-SHUNTS.h"
/* CHADEMO handling runs at 6.25 times the rate of most other code, so, rather than the
* default value of 12 (for 12 iterations of the 5s value update loop) * 5 for a 60s timeout,
* instead use 75 for 75*0.8s = 60s
*/
#undef CAN_STILL_ALIVE
#define CAN_STILL_ALIVE 75
//#define CH_CAN_DEBUG
static unsigned long setupMillis = 0;
static unsigned long handlerBeforeMillis = 0;
static unsigned long handlerAfterMillis = 0;
/* Do not change code below unless you are sure what you are doing */
static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send
static unsigned long previousMillis5000 =
0; // will store last time a 5s threshold was reached for display during debug
bool plug_inserted = false;
bool vehicle_can_initialized = false;
bool vehicle_can_received = false;
bool vehicle_permission = false;
bool evse_permission = false;
bool precharge_low = false;
bool positive_high = false;
bool contactors_ready = false;
uint8_t framecount = 0;
uint8_t max_discharge_current = 0; //TODO not sure on this one, but really influenced by inverter capability
bool high_current_control_enabled = false; // set to true when high current control is operating
// if true, values from 110.1 and 110.2 should be used instead of 102.3
// and 118 should be used for evse responses
// permissible rate of change is -20A/s to 20A/s relative to 102.3
Mode EVSE_mode = CHADEMO_DISCHARGE;
CHADEMO_STATE CHADEMO_Status = CHADEMO_IDLE;
/* Charge/discharge sequence, indicating applicable V2H guideline
* If sequence number is not agreed upon via H201/H209 between EVSE and Vehicle,
* V2H 1.1 is assumed
* Use CHADEMO_seq to decide whether emitting 209 is necessary
* 0x0 1.0 and earlier
* 0x1 2.0 appendix A
* 0x2 2.0 appendix B
* Unused for now.
uint8_t CHADEMO_seq = 0x0;
*/
bool x201_received = false;
bool x209_sent = false;
struct x100_Vehicle_Charging_Limits x100_chg_lim = {};
struct x101_Vehicle_Charging_Estimate x101_chg_est = {};
struct x102_Vehicle_Charging_Session x102_chg_session = {};
struct x110_Vehicle_Dynamic_Control x110_vehicle_dyn = {};
struct x200_Vehicle_Discharge_Limits x200_discharge_limits = {};
struct x201_Vehicle_Discharge_Estimate x201_discharge_estimate = {};
struct x700_Vehicle_Vendor_ID x700_vendor_id = {};
struct x209_EVSE_Discharge_Estimate x209_evse_dischg_est;
struct x108_EVSE_Capabilities x108_evse_cap;
struct x109_EVSE_Status x109_evse_state;
struct x118_EVSE_Dynamic_Control x118_evse_dyn;
struct x208_EVSE_Discharge_Capability x208_evse_dischg_cap;
CAN_frame CHADEMO_108 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x108,
.data = {0x01, 0xF4, 0x01, 0x0F, 0xB3, 0x01, 0x00, 0x00}};
CAN_frame CHADEMO_109 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x109,
.data = {0x02, 0x00, 0x00, 0x00, 0x01, 0x20, 0xFF, 0xFF}};
//For chademo v2.0 only
CAN_frame CHADEMO_118 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x118,
.data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
// OLD value from skeleton implementation, indicates dynamic control is possible.
// Hardcode above as being incompatible for simplicity in current incarnation.
// .data = {0x10, 0x64, 0x00, 0xB0, 0x00, 0x1E, 0x00, 0x8F}};
// 0x200 : From vehicle-side. A V2X-ready vehicle will send this message to broadcast its “Maximum discharger current”. (It is a similar logic to the limits set in 0x100 or 0x102 during a DC charging session)
// 0x208 : From EVSE-side. A V2X EVSE will use this to send the “present discharger current” during the session, and the “available input current”. (uses similar logic to 0x108 and 0x109 during a DC charging session)
CAN_frame CHADEMO_208 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x208,
.data = {0xFF, 0xF4, 0x01, 0xF0, 0x00, 0x00, 0xFA, 0x00}};
CAN_frame CHADEMO_209 = {.FD = false,
.ext_ID = false,
.DLC = 8,
.ID = 0x209,
.data = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
//This function maps all the values fetched via CAN to the correct parameters used for the inverter
void update_values_battery() {
datalayer.battery.status.real_soc = x102_chg_session.StateOfCharge;
datalayer.battery.status.max_discharge_power_W =
(x200_discharge_limits.MaximumDischargeCurrent * x100_chg_lim.MaximumBatteryVoltage); //In Watts, Convert A to P
datalayer.battery.status.voltage_dV = get_measured_voltage() * 10;
datalayer.battery.info.total_capacity_Wh =
((x101_chg_est.RatedBatteryCapacity / 0.1) *
1000); //(Added in CHAdeMO v1.0.1), maybe handle hardcoded on lower protocol version?
/* TODO max charging rate =
* x200_discharge_limits.MaxRemainingCapacityForCharging /
* x101_chg_est.RatedBatteryCapacity * 100;
*/
datalayer.battery.status.remaining_capacity_Wh = static_cast<uint32_t>(
(static_cast<double>(datalayer.battery.status.real_soc) / 10000) * datalayer.battery.info.total_capacity_Wh);
/* To simulate or NOT to simulate battery cell voltages, that is .. A question.
* Answer for now: Not, because they are not available in any direct manner.
* This will impact Solax inverter support, which uses cell min/max mV to populate
* CAN frames.
*/
if (vehicle_can_received) {
uint8_t chargingrate = 0;
if (x100_chg_lim.ConstantOfChargingRateIndication > 0) {
chargingrate = x102_chg_session.StateOfCharge / x100_chg_lim.ConstantOfChargingRateIndication * 100;
}
}
}
//TODO simplified start/stop helper functions
//see IEEE Table A.26—Charge control termination command pattern on pg58
//for stop conditions
inline void process_vehicle_charging_minimums(CAN_frame rx_frame) {
x100_chg_lim.MinimumChargeCurrent = rx_frame.data.u8[0];
x100_chg_lim.MinimumBatteryVoltage = ((rx_frame.data.u8[3] << 8) | rx_frame.data.u8[2]);
x100_chg_lim.MaximumBatteryVoltage = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]);
x100_chg_lim.ConstantOfChargingRateIndication = rx_frame.data.u8[6];
}
inline void process_vehicle_charging_maximums(CAN_frame rx_frame) {
x101_chg_est.MaxChargingTime10sBit = rx_frame.data.u8[1];
x101_chg_est.MaxChargingTime1minBit = rx_frame.data.u8[2];
x101_chg_est.EstimatedChargingTime = rx_frame.data.u8[3];
x101_chg_est.RatedBatteryCapacity = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[5]);
}
inline void process_vehicle_charging_session(CAN_frame rx_frame) {
uint16_t newTargetBatteryVoltage = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[1]);
uint16_t priorTargetBatteryVoltage = x102_chg_session.TargetBatteryVoltage;
uint8_t newChargingCurrentRequest = rx_frame.data.u8[3];
uint8_t priorChargingCurrentRequest = x102_chg_session.ChargingCurrentRequest;
vehicle_can_initialized = true;
vehicle_permission = digitalRead(CHADEMO_PIN_4);
x102_chg_session.ControlProtocolNumberEV = rx_frame.data.u8[0];
x102_chg_session.f.fault.FaultBatteryOverVoltage = bitRead(rx_frame.data.u8[4], 0);
x102_chg_session.f.fault.FaultBatteryUnderVoltage = bitRead(rx_frame.data.u8[4], 1);
x102_chg_session.f.fault.FaultBatteryCurrentDeviation = bitRead(rx_frame.data.u8[4], 2);
x102_chg_session.f.fault.FaultHighBatteryTemperature = bitRead(rx_frame.data.u8[4], 3);
x102_chg_session.f.fault.FaultBatteryVoltageDeviation = bitRead(rx_frame.data.u8[4], 4);
x102_chg_session.s.status.StatusVehicleChargingEnabled = bitRead(rx_frame.data.u8[5], 0);
x102_chg_session.s.status.StatusVehicleShifterPosition = bitRead(rx_frame.data.u8[5], 1);
x102_chg_session.s.status.StatusChargingError = bitRead(rx_frame.data.u8[5], 2);
x102_chg_session.s.status.StatusVehicle = bitRead(rx_frame.data.u8[5], 3);
x102_chg_session.s.status.StatusNormalStopRequest = bitRead(rx_frame.data.u8[5], 4);
x102_chg_session.s.status.StatusVehicleDischargeCompatible = bitRead(rx_frame.data.u8[5], 7);
x102_chg_session.StateOfCharge = rx_frame.data.u8[6];
//NOTE: behavior differs in the case of high current control (x110 support TBD)
// In that mode, ChargingCurrentRequest is set to 0xFF when >= 1 A is specified in
// in “request charging current (for extended) (x110.1, x110.2),” and then afterward in x102,
// it will not be updated
x102_chg_session.ChargingCurrentRequest = newChargingCurrentRequest;
x102_chg_session.TargetBatteryVoltage = newTargetBatteryVoltage;
#ifdef DEBUG_LOG
//Note on p131
uint8_t chargingrate = 0;
if (x100_chg_lim.ConstantOfChargingRateIndication > 0) {
chargingrate = x102_chg_session.StateOfCharge / x100_chg_lim.ConstantOfChargingRateIndication * 100;
logging.print("Charge Rate (kW): ");
logging.println(chargingrate);
}
#endif
//Table A.26—Charge control termination command patterns -- should echo x108 handling
/* charge/discharge permission signal from vehicle on pin 4 should NOT be sensed before first CAN received from vehicle.
* Also, vehicle CAN should not simultaneously indicate enabled while permissions signal is absent
*
* Either is a logical inconsistency per spec (vehicle has lost state, some wire/pin is broken, etc)
* this should trigger stop and teardown
*/
if ((CHADEMO_Status == CHADEMO_INIT && vehicle_permission) ||
(x102_chg_session.s.status.StatusVehicleChargingEnabled && !vehicle_permission)) {
#ifdef DEBUG_LOG
logging.println("Inconsistent charge/discharge state.");
#endif
CHADEMO_Status = CHADEMO_FAULT;
return;
}
if (x102_chg_session.f.fault.FaultBatteryOverVoltage) {
#ifdef DEBUG_LOG
logging.println("Vehicle indicates fault, battery over voltage.");
#endif
CHADEMO_Status = CHADEMO_STOP;
return;
}
if (x102_chg_session.f.fault.FaultBatteryUnderVoltage) {
#ifdef DEBUG_LOG
logging.println("Vehicle indicates fault, battery under voltage.");
#endif
CHADEMO_Status = CHADEMO_STOP;
return;
}
if (x102_chg_session.f.fault.FaultBatteryCurrentDeviation) {
#ifdef DEBUG_LOG
logging.println("Vehicle indicates fault, battery current deviation. Possible EVSE issue?");
#endif
CHADEMO_Status = CHADEMO_STOP;
return;
}
if (x102_chg_session.f.fault.FaultBatteryVoltageDeviation) {
#ifdef DEBUG_LOG
logging.println("Vehicle indicates fault, battery voltage deviation. Possible EVSE issue?");
#endif
CHADEMO_Status = CHADEMO_STOP;
return;
}
// end
if (priorTargetBatteryVoltage > 0 && newTargetBatteryVoltage == 0) {
CHADEMO_Status = CHADEMO_STOP;
return;
}
//FIXME condition nesting or more stanzas needed here for clear determination of cessation reason
if (CHADEMO_Status == CHADEMO_POWERFLOW && EVSE_mode == CHADEMO_CHARGE && !vehicle_permission) {
#ifdef DEBUG_LOG
logging.println("State of charge ceiling reached or charging interrupted, stop charging");
#endif
CHADEMO_Status = CHADEMO_STOP;
return;
}
if (vehicle_permission && CHADEMO_Status == CHADEMO_NEGOTIATE) {
CHADEMO_Status = CHADEMO_EV_ALLOWED;
#ifdef DEBUG_LOG
logging.println("STATE shift to CHADEMO_EV_ALLOWED in process_vehicle_charging_session()");
#endif
return;
}
// TODO this and the next stanza influence state/control
// and probably don't belong in this function
// consider relocating
if (vehicle_permission && CHADEMO_Status == CHADEMO_EVSE_PREPARE && priorTargetBatteryVoltage == 0 &&
newTargetBatteryVoltage > 0 && x102_chg_session.s.status.StatusVehicleChargingEnabled) {
#ifdef DEBUG_LOG
logging.println("STATE SHIFT to EVSE_START reached in process_vehicle_charging_session()");
#endif
CHADEMO_Status = CHADEMO_EVSE_START;
return;
}
if (vehicle_permission && evse_permission && CHADEMO_Status == CHADEMO_POWERFLOW) {
#ifdef DEBUG_LOG
logging.println("updating vehicle request in process_vehicle_charging_session()");
#endif
return;
}
#ifdef DEBUG_LOG
logging.println("UNHANDLED STATE IN process_vehicle_charging_session()");
#endif
return;
}
/* x200 Vehicle, peer to x208 EVSE */
inline void process_vehicle_charging_limits(CAN_frame rx_frame) {
x200_discharge_limits.MaximumDischargeCurrent = rx_frame.data.u8[0];
x200_discharge_limits.MinimumDischargeVoltage = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[4]);
x200_discharge_limits.MinimumBatteryDischargeLevel = rx_frame.data.u8[6];
x200_discharge_limits.MaxRemainingCapacityForCharging = rx_frame.data.u8[7];
#ifdef DEBUG_LOG
/* unsigned long currentMillis = millis();
if (currentMillis - previousMillis5000 >= INTERVAL_5_S) {
previousMillis5000 = currentMillis;
logging.println("x200 Max remaining capacity for charging/discharging:");
// initially this is set to 0, which is represented as 0xFF
logging.println(0xFF - x200_discharge_limits.MaxRemainingCapacityForCharging);
}
*/
#endif
if (get_measured_voltage() <= x200_discharge_limits.MinimumDischargeVoltage && CHADEMO_Status > CHADEMO_NEGOTIATE) {
#ifdef DEBUG_LOG
logging.println("x200 minimum discharge voltage met or exceeded, stopping.");
#endif
CHADEMO_Status = CHADEMO_STOP;
}
}
/* Vehicle 0x201, peer to EVSE 0x209
* HOWEVER, 201 isn't even emitted in any of the v2x canlogs available
*/
inline void process_vehicle_discharge_estimate(CAN_frame rx_frame) {
unsigned long currentMillis = millis();
x201_discharge_estimate.V2HchargeDischargeSequenceNum = rx_frame.data.u8[0];
x201_discharge_estimate.ApproxDischargeCompletionTime = ((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[1]);
x201_discharge_estimate.AvailableVehicleEnergy = ((rx_frame.data.u8[4] << 8) | rx_frame.data.u8[3]);
#ifdef DEBUG_LOG
if (currentMillis - previousMillis5000 >= INTERVAL_5_S) {
previousMillis5000 = currentMillis;
logging.print("x201 availabile vehicle energy, completion time: ");
logging.println(x201_discharge_estimate.AvailableVehicleEnergy);
logging.print("x201 approx vehicle completion time: ");
logging.println(x201_discharge_estimate.ApproxDischargeCompletionTime);
}
#endif
}
inline void process_vehicle_dynamic_control(CAN_frame rx_frame) {
//SM Dynamic Control = Charging station can increase of decrease "available output current" during charging.
//If you set 0x110 byte 0, bit 0 to 1 you say you can do dynamic control.
//Charging station communicates this in 0x118 byte 0, bit 0
x110_vehicle_dyn.u.status.HighVoltageControlStatus = bitRead(rx_frame.data.u8[0], 2);
x110_vehicle_dyn.u.status.HighCurrentControlStatus = bitRead(rx_frame.data.u8[0], 1);
x110_vehicle_dyn.u.status.DynamicControlStatus = bitRead(rx_frame.data.u8[0], 0);
}
inline void process_vehicle_vendor_ID(CAN_frame rx_frame) {
x700_vendor_id.AutomakerCode = rx_frame.data.u8[0];
x700_vendor_id.OptionalContent =
((rx_frame.data.u8[2] << 8) | rx_frame.data.u8[1]); //Actually more bytes, but not needed for our purpose
}
void handle_incoming_can_frame_battery(CAN_frame rx_frame) {
#ifdef CH_CAN_DEBUG
logging.print(millis()); // Example printout, time, ID, length, data: 7553 1DB 8 FF C0 B9 EA 0 0 2 5D
logging.print(" ");
logging.print(rx_frame.ID, HEX);
logging.print(" ");
logging.print(rx_frame.DLC);
logging.print(" ");
for (int i = 0; i < rx_frame.DLC; ++i) {
logging.print(rx_frame.data.u8[i], HEX);
logging.print(" ");
}
logging.println("");
#endif
// CHADEMO coexists with a CAN-based shunt. Only process CHADEMO-specific IDs
// 202 is unknown
if (!((rx_frame.ID >= 0x100 && rx_frame.ID <= 0x202) || rx_frame.ID == 0x700)) {
return;
}
// used for testing vehicle sanity
vehicle_can_received = true;
/* CHADEMO_INIT state is a transient, used to indicate when CAN
* has not yet been receied from a vehicle
*/
datalayer.battery.status.CAN_battery_still_alive =
CAN_STILL_ALIVE; //We are getting CAN messages from the vehicle, inform the watchdog
switch (rx_frame.ID) {
case 0x100:
process_vehicle_charging_minimums(rx_frame);
break;
case 0x101:
process_vehicle_charging_maximums(rx_frame);
break;
case 0x102:
framecount++;
//the first few frames start as 0x03, then like 20 of 0x01
if (vehicle_can_initialized && framecount < 20) {
return;
}
process_vehicle_charging_session(rx_frame);
/* counter to help discard inital frames with bad SOC data */
break;
case 0x200: //For V2X
process_vehicle_charging_limits(rx_frame);
break;
case 0x201: //For V2X
x201_received = true;
process_vehicle_discharge_estimate(rx_frame);
break;
case 0x110: //Only present on Chademo v2.0
process_vehicle_dynamic_control(rx_frame);
case 0x700:
process_vehicle_vendor_ID(rx_frame);
break;
case 0x202: // unknown. LEAF specific?
default:
break;
}
if (CHADEMO_Status == CHADEMO_INIT) {
// First CAN messages received, entering into negotiation
// TODO consider tracking delta since transition time for expiry
CHADEMO_Status = CHADEMO_NEGOTIATE;
}
handle_chademo_sequence();
}
/* (re)initialize evse structures to pre-charge/discharge states */
void evse_init() {
// Held at 1 until start of charge when set to 0
// returns to 1 when ceasing power flow
// mutually exclusive values
x109_evse_state.s.status.ChgDischStopControl = 1;
x109_evse_state.s.status.EVSE_status = 0;
x109_evse_state.s.status.connector_locked = 0;
x109_evse_state.s.status.battery_incompatible = 0;
x109_evse_state.s.status.ChgDischError = 0;
/* values set during object initialization
x109_evse_state.CHADEMO_protocol_number
x109_evse_state.remaining_time_10s
x109_evse_state.remaining_time_1m
*/
}
/* updates for x108 */
void update_evse_capabilities(CAN_frame& f) {
/* TODO use charger defines/runtime config?
* for now..leave as a future tweak.
* use vehicle requests as a ceiling
*/
/* interpret this as mostly a timing concern, so indicate yes we support EV contactor weld detection
* expectations: <1s after vehicle contactors open, charger output will drop to <=25% of prior voltage
* <2s after vehicle contactors open, charter output voltage will drop to <=10V
*
* see A.10.2.1 Example of welding detection logic on the vehicle
*/
x108_evse_cap.contactor_weld_detection = 0x1;
/* should this be set to MAX_EVSE_OUTPUT_VOLTAGE or x102_chg_session.TargetBatteryVoltage ? */
x108_evse_cap.available_output_voltage = MAX_EVSE_OUTPUT_VOLTAGE;
/* calculate max threshold to protect battery - using vehicle-provided max minus 2% */
x108_evse_cap.threshold_voltage =
x100_chg_lim.MaximumBatteryVoltage - (int)(x100_chg_lim.MaximumBatteryVoltage / 100 * 2);
// Power and voltage may be best derived from config/defines not from the x108 settings elsewhere, ideally
// only set current when voltage > 0, as it is set by x102 TargetBatteryVoltage
if (x108_evse_cap.available_output_voltage) {
x108_evse_cap.available_output_current = MAX_EVSE_POWER_CHARGING / x108_evse_cap.available_output_voltage;
}
/* update Frame - byte 6 and 7 are unused */
CHADEMO_108.data.u8[0] = x108_evse_cap.contactor_weld_detection;
CHADEMO_108.data.u8[1] = lowByte(x108_evse_cap.available_output_voltage);
CHADEMO_108.data.u8[2] = highByte(x108_evse_cap.available_output_voltage);
CHADEMO_108.data.u8[3] = x108_evse_cap.available_output_current;
CHADEMO_108.data.u8[4] = lowByte(x108_evse_cap.threshold_voltage);
CHADEMO_108.data.u8[5] = highByte(x108_evse_cap.threshold_voltage);
}
/* updates for x109 */
void update_evse_status(CAN_frame& f) {
x109_evse_state.s.status.EVSE_status = 1;
x109_evse_state.s.status.EVSE_error = 0;
x109_evse_state.s.status.ChgDischError = 0;
x109_evse_state.s.status.ChgDischStopControl = 0;
/* updated only in state handler
* TODO..why?
x109_evse_state.s.connector_locked = 0;
*/
if (EVSE_mode == CHADEMO_DISCHARGE) {
/* Occasionally oberved as set to 0 when discharging
* this may be true for all V2H versions
* unless it was a logging discrepancy
x109_evse_state.setpoint_HV_VDC = 0;
x109_evse_state.setpoint_HV_IDC = 0;
*/
x109_evse_state.setpoint_HV_VDC =
min(x102_chg_session.TargetBatteryVoltage, x108_evse_cap.available_output_voltage);
x109_evse_state.setpoint_HV_IDC =
min(x102_chg_session.ChargingCurrentRequest, x108_evse_cap.available_output_current);
/* TODO calculate remaining discharge time : for now == 60m */
x109_evse_state.remaining_time_1m = 60;
} else if (EVSE_mode == CHADEMO_CHARGE) {
x109_evse_state.setpoint_HV_VDC = get_measured_voltage();
x109_evse_state.setpoint_HV_IDC = get_measured_current();
/*For posterity if anyone is forced to simulate a shunt
NOTE: these are supposed to be measured values, e.g., from a shunt
If a sensor is not used, we are literally asserting that the measured value is exactly equivalent to the request or max charger capability
this is pretty likely to fail on most vehicles
x109_evse_state.setpoint_HV_VDC =
min(x102_chg_session.TargetBatteryVoltage, x108_evse_cap.available_output_voltage);
x109_evse_state.setpoint_HV_IDC =
min(x102_chg_session.ChargingCurrentRequest, x108_evse_cap.available_output_current);
*/
/* The spec suggests throwing a 109.5.4 = 1 if vehicle curr request 102.3 > evse curr available 108.3,
* but realistically many chargers seem to act tolerant here and stay under limits and supply whatever they are able
*/
/* if power overcommitted, back down to just below while maintaining voltage target */
if (x109_evse_state.setpoint_HV_VDC > 0 &&
x109_evse_state.setpoint_HV_IDC * x109_evse_state.setpoint_HV_VDC > MAX_EVSE_POWER_CHARGING) {
x109_evse_state.setpoint_HV_IDC = floor(MAX_EVSE_POWER_CHARGING / x109_evse_state.setpoint_HV_VDC);
}
/* TODO calculate remaining charge time : for now == 60m */
x109_evse_state.remaining_time_1m = 60;
}
/* Table A.26 - See also Charge control termination command patterns
* This handling must be mirrored in handling for vehicle x102
*
*/
if ((x102_chg_session.TargetBatteryVoltage > x108_evse_cap.available_output_voltage) ||
(x100_chg_lim.MaximumBatteryVoltage < x108_evse_cap.threshold_voltage)) {
//Toggle battery incompatibility flag 109.5.3
x109_evse_state.s.status.EVSE_error = 1;
x109_evse_state.s.status.battery_incompatible = 1;
x109_evse_state.s.status.ChgDischStopControl = 1;
CHADEMO_Status = CHADEMO_FAULT;
}
//CHADEMO_109.data.u8[0] hardcoded to 0x2 for CHAdeMO v1, 1.0.1, 1.1, 1.2
// in initialization
CHADEMO_109.data.u8[0] = x109_evse_state.CHADEMO_protocol_number;
CHADEMO_109.data.u8[1] = lowByte(x109_evse_state.setpoint_HV_VDC);
CHADEMO_109.data.u8[2] = highByte(x109_evse_state.setpoint_HV_VDC);
CHADEMO_109.data.u8[3] = x109_evse_state.setpoint_HV_IDC;
CHADEMO_109.data.u8[4] = x109_evse_state.discharge_compatible;
/* clear statuses/faults, then set explicitly */
CHADEMO_109.data.u8[5] = 0;
CHADEMO_109.data.u8[5] =
x109_evse_state.s.status.EVSE_status | x109_evse_state.s.status.EVSE_error << 1 |
x109_evse_state.s.status.connector_locked << 2 | x109_evse_state.s.status.battery_incompatible << 3 |
x109_evse_state.s.status.ChgDischError << 4 | x109_evse_state.s.status.ChgDischStopControl << 5;
CHADEMO_109.data.u8[6] = x109_evse_state.remaining_time_10s;
CHADEMO_109.data.u8[7] = x109_evse_state.remaining_time_1m;
}
/* Discharge estimates: x209 EVSE = peer to x201 Vehicle
* NOTE: x209 is emitted in CAN logs when x201 isn't even present
* it may not be understood by leaf (or ignored unless >= a certain protocol version or v2h sequence number
*/
void update_evse_discharge_estimate(CAN_frame& f) {
//x209_evse_dischg_est.remaining_discharge_time_1m = x201_discharge_estimate.ApproxDischargeCompletionTime;
//x201_discharge_estimate.AvailableVehicleEnergy = 0;
//do nothing to alter the default initialized values..this may be unneeded
/* TODO
if (x200_discharge_limits.MaxRemainingCapacityForCharging = max charge voltage){
if (x200_discharge_limits.MinimumBatteryDischargeLevel = kwH for v2h<1.0){
if (x200_discharge_limits.MaxRemainingCapacityForCharging = kwH for v2h<1.0){
*/
CHADEMO_209.data.u8[0] = x209_evse_dischg_est.sequence_control_number;
CHADEMO_209.data.u8[1] = lowByte(x209_evse_dischg_est.remaining_discharge_time);
CHADEMO_209.data.u8[2] = highByte(x209_evse_dischg_est.remaining_discharge_time);
}
/* x208 EVSE, peer to 0x200 Vehicle */
void update_evse_discharge_capabilities(CAN_frame& f) {
//present discharge current is a measured value
x208_evse_dischg_cap.present_discharge_current = 0xFF - get_measured_current();
/* Present discharge current is a measured value. In the absence of
a shunt, the evse here is quite literally lying to the vehicle. The spec
seems to suggest this is tolerated unless the current measured on the EV
side continualy exceeds the maximum discharge current by 10amps
x208_evse_dischg_cap.present_discharge_current = 0xFF - 6;
*/
//EVSE maximum current input is partly an inverter-influenced value i.e., min(inverter, vehicle_max_discharge)
//use max_discharge_current variable if nonzero, otherwise tell the vehicle the EVSE will take everything it can give
if (max_discharge_current) {
x208_evse_dischg_cap.available_input_current = 0xFF - max_discharge_current;
} else {
x208_evse_dischg_cap.available_input_current = 0xFF - x200_discharge_limits.MaximumDischargeCurrent;
}
x208_evse_dischg_cap.available_input_voltage = x200_discharge_limits.MinimumDischargeVoltage;
/* calculate min threshold to protect battery - using vehicle-provided minimum plus 2%
*
*As this is partly an inverter-influenced value, should this be a configurable variable backed by a defined default?
* It seems sensible to be MAX(lowest usable voltage of the inverter input, lowest tolerable voltage of the vehicle battery)
* NOT the vehicle minimumDischargeVoltage.
* Thus, why here we are adding a few percent of cushion atop the minimum
* This is the reverse treatment of the lower_threshold_voltage of charging mode
*/
x208_evse_dischg_cap.lower_threshold_voltage =
x200_discharge_limits.MinimumDischargeVoltage + (int)(x200_discharge_limits.MinimumDischargeVoltage / 100 * 2);
/* 0x00 == unused
if (x200_discharge_limits.MinimumBatteryDischargeLevel > 0 &&
x208_evse_dischg_cap.minimum_input_voltage < x200_discharge_limits.MinimumBatteryDischargeLevel){
// stop discharging, but permit charging if mode = bidirectional
}
*/
//TODO might be ideal to do the 0xFF subtraction HERE during serialization, rather than above?
CHADEMO_208.data.u8[0] = x208_evse_dischg_cap.present_discharge_current;
CHADEMO_208.data.u8[1] = lowByte(x208_evse_dischg_cap.available_input_voltage);
CHADEMO_208.data.u8[2] = highByte(x208_evse_dischg_cap.available_input_voltage);
CHADEMO_208.data.u8[3] = x208_evse_dischg_cap.available_input_current;
CHADEMO_208.data.u8[6] = lowByte(x208_evse_dischg_cap.lower_threshold_voltage);
CHADEMO_208.data.u8[7] = highByte(x208_evse_dischg_cap.lower_threshold_voltage);
}
void transmit_can_battery(unsigned long currentMillis) {
handlerBeforeMillis = currentMillis;
handle_chademo_sequence();
handlerAfterMillis = millis();
// Send 100ms CAN Message
if (currentMillis - previousMillis100 >= INTERVAL_100_MS) {
previousMillis100 = currentMillis;
/* no EVSE messages should be sent until the vehicle has
* initiated
*/
// if (CHADEMO_Status <= CHADEMO_INIT || !vehicle_can_received) {
if (CHADEMO_Status <= CHADEMO_INIT) {
return;
}
update_evse_capabilities(CHADEMO_108);
update_evse_status(CHADEMO_109);
update_evse_discharge_capabilities(CHADEMO_208);
update_evse_discharge_estimate(CHADEMO_209);
/* most updates to these EVSE frames are made
* upon receipt of a Vehicle message, as
* that is the limiting factor. Therefore, we
* can generally send as is without tweaks here.
*/
transmit_can_frame(&CHADEMO_108, can_config.battery);
transmit_can_frame(&CHADEMO_109, can_config.battery);
/* TODO for dynamic control: can send x118 with byte 6 bit 0 set to 0 for 1s (before flipping back to 1) as a way of giving vehicle a chance to update 101.1 and 101.2
* within 6 seconds of x118 toggle.
* Then 109.6 and 109.7 reset remaining charging time
* see A.11.5.3.1.3 Remaining charging time (H109.6, H109.7) for a better description
*/
if (EVSE_mode == CHADEMO_DISCHARGE || EVSE_mode == CHADEMO_BIDIRECTIONAL) {
transmit_can_frame(&CHADEMO_208, can_config.battery);
if (x201_received) {
transmit_can_frame(&CHADEMO_209, can_config.battery);
x209_sent = true;
}
}
// TODO need an update_evse_dynamic_control(..) function above before we send 118
// 110.0.0
if (x102_chg_session.ControlProtocolNumberEV >= 0x03) { //Only send the following on Chademo 2.0 vehicles?
#ifdef DEBUG_LOG
//FIXME REMOVE
logging.println("REMOVE: proto 2.0");
#endif
transmit_can_frame(&CHADEMO_118, can_config.battery);
}
}
}
/* A lot of the heavy lifting happens here. This is essentially the state hander. SOME
* state transitions happen in functions before/after this is called.
*
* stages according to IEEE SPEC, with our states mapped into each.
* 1) Standby stage
* CHADEMO_IDLE
* 2) Preparation stage
* CHADEMO_CONNECTED
* CHADEMO_INIT
* CHADEMO_NEGOTIATE
* CHADEMO_EV_ALLOWED
* CHADEMO_EVSE_PREPARE
* CHADEMO_EVSE_START
* CHADEMO_EVSE_CONTACTORS_ENABLED
* 3) Charging/Discharging stage
* CHADEMO_POWERFLOW
* 4) Termination stage
* CHADEMO_STOP
* 5) Emergency stop stage
* CHADEMO_FAULT
*/
void handle_chademo_sequence() {
precharge_low = digitalRead(PRECHARGE_PIN) == LOW;
positive_high = digitalRead(POSITIVE_CONTACTOR_PIN) == HIGH;
contactors_ready = precharge_low && positive_high;
vehicle_permission = digitalRead(CHADEMO_PIN_4);
/* ------------------- State override conditions checks ------------------- */
/* ------------------------------------------------------------------------------ */
if (CHADEMO_Status >= CHADEMO_EV_ALLOWED && x102_chg_session.s.status.StatusVehicleShifterPosition) {
#ifdef DEBUG_LOG
logging.println("Vehicle is not parked, abort.");
#endif
CHADEMO_Status = CHADEMO_STOP;
}
if (CHADEMO_Status >= CHADEMO_EV_ALLOWED && !vehicle_permission) {
#ifdef DEBUG_LOG
logging.println("Vehicle charge/discharge permission ended, stop.");
#endif
CHADEMO_Status = CHADEMO_STOP;
}
/* ------------------- STATE HANDLER ------------------- */
/* ------------------------------------------------------------------------------ */
switch (CHADEMO_Status) {
case CHADEMO_IDLE:
/* this is where we can unlock connector */
digitalWrite(CHADEMO_LOCK, LOW);
plug_inserted = digitalRead(CHADEMO_PIN_7);
if (!plug_inserted) {
#ifdef DEBUG_LOG
// Commented unless needed for debug
// logging.println("CHADEMO plug is not inserted.");
#endif
return;
}
CHADEMO_Status = CHADEMO_CONNECTED;
#ifdef DEBUG_LOG
logging.println("CHADEMO plug is inserted. Provide EVSE power to vehicle to trigger initialization.");
#endif
break;
case CHADEMO_CONNECTED:
#ifdef DEBUG_LOG
// Commented unless needed for debug
//logging.println("CHADEMO_CONNECTED State");
#endif
/* plug_inserted is .. essentially a volatile of sorts, so verify */
if (plug_inserted) {
/* If connection is detectable, jumpstart handshake by
* indicate that the EVSE is ready to begin
*/
digitalWrite(CHADEMO_PIN_2, HIGH);
/* State change to initializing. We will re-enter the handler upon receipt of CAN */
CHADEMO_Status = CHADEMO_INIT;
} else {
/* this potentially-viewed-as-redundant condition checking is candidly
* an expression racy-relaties of the real world. Depending upon
* testing/performance, it may be better to pepper this state handler
* with timers to have higher confidence of certain conditions hitting
* a steady state
*/
#ifdef DEBUG_LOG
logging.println("CHADEMO plug is not inserted, cannot connect d2 relay to begin initialization.");
#endif
CHADEMO_Status = CHADEMO_IDLE;
}
break;
case CHADEMO_INIT:
/* Transient state while awaiting CAN from Vehicle.
* Used for triggers/error handling elsewhere;
* State change to CHADEMO_NEGOTIATE occurs in handle_incoming_can_frame_battery(..)
*/
#ifdef DEBUG_LOG
// logging.println("Awaiting initial vehicle CAN to trigger negotiation");
#endif
evse_init();
break;
case CHADEMO_NEGOTIATE:
/* Vehicle and EVSE dance */
//TODO if pin 4 / j goes high,
#ifdef DEBUG_LOG
// Commented unless needed for debug
// logging.println("CHADEMO_NEGOTIATE State");
#endif
x109_evse_state.s.status.ChgDischStopControl = 1;
break;
case CHADEMO_EV_ALLOWED:
#ifdef DEBUG_LOG
// Commented unless needed for debug
logging.println("CHADEMO_EV_ALLOWED State");
#endif
// If we are in this state, vehicle_permission was already set to true...but re-verify
// that pin 4 (j) reads high
if (vehicle_permission) {
//lock connector here
digitalWrite(CHADEMO_LOCK, HIGH);
//TODO spec requires test to validate solenoid has indeed engaged.
// example uses a comparator/current consumption check around solenoid
x109_evse_state.s.status.connector_locked = true;
CHADEMO_Status = CHADEMO_EVSE_PREPARE;
}
break;
case CHADEMO_EVSE_PREPARE:
#ifdef DEBUG_LOG
// Commented unless needed for debug
logging.println("CHADEMO_EVSE_PREPARE State");
#endif
/* TODO voltage check of output < 20v
* insulation test hypothetically happens here before triggering PIN 10 high
* see Table A.28—Requirements for the insulation test for output DC circuit
Note: required that if 102.5.0 == 0, do not perform evse insulation test
we should not be here in this state unless 102.5.0 was == 1 previously, but check again in case it has changed
simulate via?
if evse_present _voltage + 10 <= vehicle voltage_target {
evse_present_voltage += 10;
} else {
evse_present_voltage = vehicle voltage_target;
}
*/
if (x102_chg_session.s.status.StatusVehicleChargingEnabled) {
if (get_measured_voltage() < 20) {
digitalWrite(CHADEMO_PIN_10, HIGH);
evse_permission = true;
} else {
logging.println("Insulation check measures > 20v ");
}
// likely unnecessary but just to be sure. consider removal
x109_evse_state.s.status.ChgDischStopControl = 1;
x109_evse_state.s.status.EVSE_status = 0;
} else {
CHADEMO_Status = CHADEMO_STOP;
}
//state changes to CHADEMO_EVSE_START only upon receipt of charging session request
break;
case CHADEMO_EVSE_START:
#ifdef DEBUG_LOG
// Commented unless needed for debug
logging.println("CHADEMO_EVSE_START State");
#endif
datalayer.system.status.battery_allows_contactor_closing = true;
x109_evse_state.s.status.ChgDischStopControl = 1;
x109_evse_state.s.status.EVSE_status = 0;
CHADEMO_Status = CHADEMO_EVSE_CONTACTORS_ENABLED;
#ifdef DEBUG_LOG
logging.println("Initiating contactors");
#endif
/* break rather than fall through because contactors are not instantaneous;
* worth giving it a cycle to finish
*/
break;
case CHADEMO_EVSE_CONTACTORS_ENABLED:
#ifdef DEBUG_LOG
// Commented unless needed for debug
logging.println("CHADEMO_EVSE_CONTACTORS State");
#endif
/* check whether contactors ready, because externally dependent upon inverter allow during discharge */
if (contactors_ready) {
#ifdef DEBUG_LOG
logging.println("Contactors ready");
logging.print("Voltage: ");
logging.println(get_measured_voltage());
#endif
/* transition to POWERFLOW state if discharge compatible on both sides */
if (x109_evse_state.discharge_compatible && x102_chg_session.s.status.StatusVehicleDischargeCompatible &&
(EVSE_mode == CHADEMO_DISCHARGE || EVSE_mode == CHADEMO_BIDIRECTIONAL)) {
CHADEMO_Status = CHADEMO_POWERFLOW;
x109_evse_state.s.status.ChgDischStopControl = 0;
x109_evse_state.s.status.EVSE_status = 1;
}
if (EVSE_mode == CHADEMO_CHARGE) {
//TODO not supported currently
//CHADEMO_Status = CHADEMO_POWERFLOW;
//x109_evse_state.s.status.ChgDischStopControl = 0;
}
}
/* break or fall through ? TODO */
break;
case CHADEMO_POWERFLOW:
#ifdef DEBUG_LOG
// Commented unless needed for debug
logging.println("CHADEMO_POWERFLOW State");
#endif
/* POWERFLOW for charging, discharging, and bidirectional */
/* Interpretation */
if (x102_chg_session.s.status.StatusVehicleShifterPosition) {
/* Vehicle is no longer in park */
// vehicle will switch k off, EVSE MAY see j (pin 4) go low
// EVEN IF evse reads read pin 4 to see high, if this condition is true then trigger EVSE charge/discharge stop
// per spec
// SEPARATE check for pin 4/j condition does not depend on this flag. it's an OR condition
}
if (x102_chg_session.TargetBatteryVoltage == 0x00) {
//TODO flag error and do not calculate power in EVSE response?
// probably unnecessary as other flags will be set causing this to be caught
}
if (get_measured_voltage() <= x200_discharge_limits.MinimumDischargeVoltage) {
#ifdef DEBUG_LOG
logging.println("x200 minimum discharge voltage met or exceeded, stopping.");
#endif
CHADEMO_Status = CHADEMO_STOP;
}
// Potentially unnecessary (set in CHADEMO_EVSE_CONTACTORS_ENABLED stanza), but just in case
x109_evse_state.s.status.ChgDischStopControl = 0;
x109_evse_state.s.status.EVSE_status = 1;
break;
case CHADEMO_STOP:
#ifdef DEBUG_LOG
// Commented unless needed for debug
logging.println("CHADEMO_STOP State");
#endif
/* back to CHADEMO_IDLE after teardown */
x109_evse_state.s.status.ChgDischStopControl = 1;
x109_evse_state.s.status.EVSE_status = 0;
x109_evse_state.s.status.battery_incompatible = 0;
evse_permission = false;
vehicle_permission = false;
x209_sent = false;
x201_received = false;
/* protection of EV contactors - IEEE A.7.2.9 Protection of EV contactor
* see also Table A.29—Charging stage and check item
*
* We will re-enter the handler until the amperage drops sufficiently
* and then transition to CHADEMO_IDLE
*/
if (get_measured_current() <= 5 && get_measured_voltage() <= 10) {
/* welding detection ideally here */
digitalWrite(CHADEMO_PIN_10, LOW);
digitalWrite(CHADEMO_PIN_2, LOW);
CHADEMO_Status = CHADEMO_IDLE;
}
break;
case CHADEMO_FAULT:
#ifdef DEBUG_LOG
// Commented unless needed for debug
logging.println("CHADEMO_FAULT State");
#endif
/* Once faulted, never departs CHADEMO_FAULT state unless device is power cycled as a safety measure */
x109_evse_state.s.status.EVSE_error = 1;
x109_evse_state.s.status.ChgDischError = 1;
x109_evse_state.s.status.ChgDischStopControl = 1;
#ifdef DEBUG_LOG
logging.println("CHADEMO fault encountered, tearing down to make safe");
#endif
digitalWrite(CHADEMO_PIN_10, LOW);
digitalWrite(CHADEMO_PIN_2, LOW);
evse_permission = false;
vehicle_permission = false;
x209_sent = false;
x201_received = false;
break;
default:
#ifdef DEBUG_LOG
logging.println("UNHANDLED CHADEMO_STATE, setting FAULT");
#endif
CHADEMO_Status = CHADEMO_FAULT;
break;
}
return;
}
void setup_battery(void) { // Performs one time setup at startup
pinMode(CHADEMO_PIN_2, OUTPUT);
digitalWrite(CHADEMO_PIN_2, LOW);
pinMode(CHADEMO_PIN_10, OUTPUT);
digitalWrite(CHADEMO_PIN_10, LOW);
pinMode(CHADEMO_LOCK, OUTPUT);
digitalWrite(CHADEMO_LOCK, LOW);
pinMode(CHADEMO_PIN_4, INPUT);
pinMode(CHADEMO_PIN_7, INPUT);
strncpy(datalayer.system.info.battery_protocol, "Chademo V2X mode", 63);
datalayer.system.info.battery_protocol[63] = '\0';
CHADEMO_Status = CHADEMO_IDLE;
/* disallow contactors until permissions is granted by vehicle */
datalayer.system.status.battery_allows_contactor_closing = false;
/* Pretend that we know the SOH, assert that it is 99% */
datalayer.battery.status.soh_pptt = 9900;
/* Briefly assert that we're starting at a modest SOC of 30% */
datalayer.battery.status.real_soc = 300;
//TODO Must be user configured, most likely. Artificially capped for the time being
datalayer.battery.status.max_discharge_power_W = 1000;
datalayer.battery.status.max_charge_power_W = 1000;
datalayer.battery.status.current_dA = 0;
datalayer.battery.status.remaining_capacity_Wh = 12000;
//TODO this is probably fine for a baseline, though CHADEMO can go as low as 150v and as high as 1500v in the latest revision
//the below is relative to a 96 cell NMC. lower end is possibly too low
datalayer.battery.info.max_design_voltage_dV =
4040; // 404.4V, over this, charging is not possible (goes into forced discharge)
datalayer.battery.info.min_design_voltage_dV = 2600; // 260.0V under this, discharging further is disabled
/* initialize EVSE data, state, and CAN frame representations */
switch (EVSE_mode) {
case CHADEMO_DISCHARGE:
case CHADEMO_BIDIRECTIONAL:
x109_evse_state.discharge_compatible = true;
break;
case CHADEMO_CHARGE:
x109_evse_state.discharge_compatible = false;
break;
default:
break;
}
x109_evse_state.s.status.ChgDischStopControl = 1;
handle_chademo_sequence();
// ISA_deFAULT(); // ISA Setup - it is sufficient to set it once, because it is saved in SUNT
// ISA_initialize(); // ISA Setup - it is sufficient to set it once, because it is saved in SUNT
// ISA_RESTART();
setupMillis = millis();
}
#endif