Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 208 additions & 5 deletions src/software/power/charger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,224 @@

Charger::Charger()
{
pinMode(HV_SENSE, INPUT);
// Currently unused
pinMode(FLYBACK_FAULT, INPUT);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look in Altium, the FLYBACK_FAULT pin (pin 27) isn't actually connected to anything. Further, there is no independent flyback fault pin on the LT3750 itself. The DONE pin will go high on a fault condition as per the datasheet (page 4 here). Or just look at this image:

Image

I.e: is there any reason to have this pin? It's also unused so perhaps it'd make sense to just delete it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, good catch. That line can be removed. Ideally, DONE would take on fault behaviour as you mentioned, however the DONE pin does not work on at least one powerboard and has not been validated for many other boards. So while I intend to incorporate DONE in the future to take on the role of fault logic, it could also be removed.

pinMode(CHRG_DONE, INPUT);

// DONE is active-low, also have hardware pullup on this pin.
pinMode(CHRG_DONE, INPUT_PULLUP);

pinMode(CHRG, OUTPUT);
digitalWrite(CHRG, LOW);

// ADS7945 bit-banged SPI pins.
pinMode(ADC_CS, OUTPUT);
digitalWrite(ADC_CS, HIGH);

pinMode(ADC_SCLK, OUTPUT);
digitalWrite(ADC_SCLK, LOW);

pinMode(ADC_MISO, INPUT);

// Hardware pulls CH_SEL low to select CH0.
// Can be changed later to include temperature sensing.
}

void Charger::setCapacitorPin(bool pin_state)
void Charger::chargeCapacitors()
{
// TODO: revert (setting low for now bc caps are sketchy)
// LT3750 charging is initiated by a CHARGE rising edge.

if (charge_inhibited_)
{
return;
}

digitalWrite(CHRG, LOW);
delayMicroseconds(LT3750_CHARGE_LOW_TIME_US);
digitalWrite(CHRG, HIGH);

charging_enabled_ = true;
last_charge_edge_ms_ = millis();
}

void Charger::stopCharging()
{
digitalWrite(CHRG, LOW);
charging_enabled_ = false;
}

void Charger::setChargeInhibited(const bool inhibited)
{
charge_inhibited_ = inhibited;

if (charge_inhibited_)
{
stopCharging();
}
}

void Charger::maintainCharge()
{
// Do not enable charging until one valid capacitor-voltage measurement exists.
if (!capacitor_voltage_control_initialized_)
{
return;
}

// During kick/chip guard and pulse time, do not charge.
if (charge_inhibited_)
{
stopCharging();
return;
}

const float voltage = capacitor_voltage_control_;
const uint32_t now = millis();

if (voltage >= CHARGE_STOP_VOLTAGE_V)
{
stopCharging();
return;
}

if (voltage <= CHARGE_RESTART_VOLTAGE_V)
{
const bool retry_due = (now - last_charge_edge_ms_) >= CHARGE_RETRY_INTERVAL_MS;

if (!charging_enabled_ || retry_due)
{
chargeCapacitors();
}
}
}

bool Charger::getChargeDone()
{
// LT3750 DONE is active low.
return digitalRead(CHRG_DONE) == LOW;
}

uint16_t Charger::readAds7945WordBitBang()
{
uint16_t raw = 0;

digitalWrite(ADC_SCLK, LOW);
digitalWrite(ADC_CS, LOW);
delayMicroseconds(ADC_CS_DELAY_US);

for (int i = 0; i < 16; i++)
{
// ADS7945 MSB is available after CS falling edge; subsequent bits update
// after SCLK falling edges. Sampling while SCLK is high is the intended
// first simple mode.
digitalWrite(ADC_SCLK, HIGH);
delayMicroseconds(ADC_CLOCK_DELAY_US);

raw <<= 1;
raw |= digitalRead(ADC_MISO) ? 1 : 0;

digitalWrite(ADC_SCLK, LOW);
delayMicroseconds(ADC_CLOCK_DELAY_US);
}

digitalWrite(ADC_CS, HIGH);
delayMicroseconds(ADC_CS_DELAY_US);

return raw;
}

int16_t Charger::signExtend14(uint16_t raw14)
{
raw14 &= 0x3FFF;

if (raw14 & 0x2000)
{
return static_cast<int16_t>(raw14 | 0xC000);
}

return static_cast<int16_t>(raw14);
}

int16_t Charger::readAds7945SignedCode()
{
// ADS7945 outputs the result of the previous conversion.
// Since CH_SEL is fixed to CH0 and capacitor voltage changes slowly, this is fine.
// Consider modifying if temperature sensing is added.
const uint16_t raw16 = readAds7945WordBitBang();
const uint16_t raw14 = (raw16 >> ADC_RESULT_RIGHT_SHIFT) & 0x3FFF;

return signExtend14(raw14);
}

float Charger::adcCodeToDifferentialVoltage(float code)
{
return code * ADC_VREF / static_cast<float>(ADC_HALF_SCALE);
}

float Charger::adcDifferentialVoltageToCapacitorVoltage(float v_adc_diff)
{
// ADS7945 measures ISO224 differential output.
// ISO224: Vout_diff = Vin / 3, so Vin = Vout_diff / ISO224_GAIN.
const float v_iso_input = v_adc_diff / ISO224_GAIN;

// ISO224 input is the divided capacitor voltage.
return v_iso_input / DIVIDER_RATIO;
}

float Charger::getCapacitorVoltage()
{
return analogRead(HV_SENSE) / RESOLUTION * SCALE_VOLTAGE * VOLTAGE_DIVIDER;
static constexpr int NUM_SAMPLES = 8;

// Calibration from scope measurements
// diagnostic 286.0 V corresponds to actual 196.44 V.
// This linear fit is applied after converting the raw ADC code to a voltage which
// already accounts for the hardware gain and divider
static constexpr float CAP_CAL_ANCHOR_DIAGNOSTIC_V = 286.0f;
static constexpr float CAP_CAL_ANCHOR_ACTUAL_V = 196.44f;
static constexpr float CAP_CAL_SLOPE = 0.7019289f;

// ADS7945 returns the previous conversion, so discard one frame first.
(void)readAds7945SignedCode();

long code_sum = 0;

for (int i = 0; i < NUM_SAMPLES; i++)
{
code_sum += readAds7945SignedCode();
}

const float avg_code = static_cast<float>(code_sum) / static_cast<float>(NUM_SAMPLES);

const float v_adc_diff = adcCodeToDifferentialVoltage(avg_code);

// Uncalibrated diagnostic voltage.
const float raw_capacitor_voltage =
adcDifferentialVoltageToCapacitorVoltage(v_adc_diff);

// Convert diagnostic voltage to calibrated physical capacitor voltage.
const float calibrated_capacitor_voltage =
CAP_CAL_ANCHOR_ACTUAL_V +
CAP_CAL_SLOPE * (raw_capacitor_voltage - CAP_CAL_ANCHOR_DIAGNOSTIC_V);

// Prevent a small negative reported voltage near 0 V.
const float physical_capacitor_voltage =
(calibrated_capacitor_voltage < 0.0f) ? 0.0f : calibrated_capacitor_voltage;

// Non EMA value used in maintainCharge() logic for hysteresis.
capacitor_voltage_control_ = physical_capacitor_voltage;
capacitor_voltage_control_initialized_ = true;

// Initialize from the first real reading so startup does not ramp from 0 V.
if (!capacitor_voltage_ema_initialized_)
{
capacitor_voltage_ema_ = physical_capacitor_voltage;
capacitor_voltage_ema_initialized_ = true;
}
else
{
capacitor_voltage_ema_ +=
CAP_VOLTAGE_EMA_ALPHA * (physical_capacitor_voltage - capacitor_voltage_ema_);
}
return capacitor_voltage_ema_;
}

bool Charger::getFlybackFault()
Expand Down
109 changes: 94 additions & 15 deletions src/software/power/charger.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,115 @@
#include <pins.h>

/*
* Represents the charger on the power board
* Represents the charger and capacitor voltage sensing on the power board.
* capacitor voltage -> divider -> ISO224 -> ADS7945 CH0 -> ESP32 GPIO5/18/21
*/
class Charger
{
public:
/**
* Creates a Charger setting up pins and attaching interrupts.
* Creates a Charger, setting up pins.
*/
Charger();

/**
* Sets the state of the capacitors and whether we should charge them.
* @param pin_state HIGH to begin charging the capacitors. Otherwise, sets the charge
* pin to LOW.
* Starts/restarts an LT3750 charge cycle by creating a low -> high edge
* on CHRG. Does nothing while charging is inhibited.
*
* New edges may be issued periodically while voltage remains below target threshold.
*/
static void setCapacitorPin(bool pin_state);
void chargeCapacitors();

/**
* Returns the voltage of the capacitors
*
* @return voltage of capacitors
* Immediately disables the LT3750 by holding CHRG low.
*/
void stopCharging();

/**
* Forces the charger off while true. Used before and during kicker pulses.
*/
void setChargeInhibited(bool inhibited);

/**
* Maintains capacitor voltage using hysteresis and the latest unfiltered
* calibrated voltage reading.
*/
void maintainCharge();

/**
* Returns the EMA-smoothed capacitor voltage for telemetry.
* Also updates the internal unfiltered control voltage used by maintainCharge().
*/
float getCapacitorVoltage();

/**
* Returns the status of the flyback fault
*
* @return whether the flyback fault is tripped or not
* Returns whether the flyback fault is tripped.
* Kept for compatibility with previous firmware, but unused.
*/
bool getFlybackFault();

/**
* Returns true when LT3750 DONE is active.
* Not supported by all boards, thus unused.
*/
static bool getChargeDone();

private:
static constexpr float VOLTAGE_DIVIDER = 1003.0 / 13.0;
static constexpr float RESOLUTION = 4096.0;
static constexpr float SCALE_VOLTAGE = 3.3;
static uint16_t readAds7945WordBitBang();
static int16_t readAds7945SignedCode();
static int16_t signExtend14(uint16_t raw14);
static float adcCodeToDifferentialVoltage(float code);
static float adcDifferentialVoltageToCapacitorVoltage(float v_adc_diff);

// ADS7945 / analog constants.
//
// 5.0f for REF5050.
static constexpr float ADC_VREF = 5.0f;

static constexpr int ADC_BITS = 14;
static constexpr int ADC_HALF_SCALE = 1 << (ADC_BITS - 1); // 8192

// ADS7945 14-bit result is expected to be left-aligned in the 16-clock frame:
// raw16 bits [15:2] = D13..D0.
static constexpr int ADC_RESULT_RIGHT_SHIFT = 2;

// ISO224 nominal gain: VOUT_DIFF = VIN / 3.
static constexpr float ISO224_GAIN = 1.0f / 3.0f;

// High-voltage divider.
static constexpr float DIVIDER_R_TOP_OHMS = 230000.0f;
static constexpr float DIVIDER_R_BOTTOM_OHMS = 8200.0f;
static constexpr float DIVIDER_RATIO =
DIVIDER_R_BOTTOM_OHMS / (DIVIDER_R_TOP_OHMS + DIVIDER_R_BOTTOM_OHMS);

// Bit-banged SPI timing. Slow and conservative.
static constexpr uint32_t ADC_CLOCK_DELAY_US = 2;
static constexpr uint32_t ADC_CS_DELAY_US = 2;

// LT3750 timing.
static constexpr uint32_t LT3750_CHARGE_LOW_TIME_US = 25; // datasheet min is 20 us

// Capacitor voltage control for maintainCharge() hysteresis.
static constexpr float CHARGE_STOP_VOLTAGE_V = 198.0f;
static constexpr float CHARGE_RESTART_VOLTAGE_V = 190.0f;

// EMA is used for telemetry smoothing, not used for maintainCharge() logic.
static constexpr float CAP_VOLTAGE_EMA_ALPHA =
0.02f; // smaller is smoother but slower to respond

float capacitor_voltage_control_ = 0.0f;
bool capacitor_voltage_control_initialized_ = false;

float capacitor_voltage_ema_ = 0.0f;
bool capacitor_voltage_ema_initialized_ = false;

bool charging_enabled_ = false;
bool charge_inhibited_ = false;

// Time of most recent CHRG edge
uint32_t last_charge_edge_ms_ = 0;

// While voltage is below CHARGE_RESTART_VOLTAGE_V,
// retry charge edges at interval
static constexpr uint32_t CHARGE_RETRY_INTERVAL_MS = 250;
};
Loading
Loading