board
Solar MPPT Power System
Public Unreviewedby TylerK
A 30 W solar battery charger with regulated 5 V and 3.3 V rails: an STM32G031 running perturb & observe MPPT and a CC/CV charge state machine, steering a TI LM5175 4-switch buck-boost through a single analog control input. Four-layer KiCad board, schematic and PCB generated from Python, with an electrothermal simulation run against the published board file.
main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
/*
* charge_control.h -- CC/CV battery charge state machine, P&O MPPT, and the
* arbitration between them that produces the single VBATT reference voltage
* the analog FB summing network is commanded to.
*
* Control architecture: the LM5175 stage has exactly ONE analog control
* input available to firmware -- the FB summing node, which sets the
* regulated OUTPUT (battery bus) voltage. There is no direct handle on
* input current or input voltage. So every constraint the system needs to
* enforce (charge current limit, battery voltage ceiling, MPPT power
* tracking) is expressed as "what VBATT setpoint would this constraint
* want", and the arbitrator commands the MOST RESTRICTIVE (lowest) of
* those candidate setpoints -- asking for a lower output voltage is always
* the "back off" direction, whichever constraint is currently binding.
*
* MPPT note: the board's ADC set covers VBATT, IIN (panel current) and
* ICHG (charge current), but NOT VIN (panel voltage) directly -- true P*V
* power tracking on the input side isn't available without adding a
* sense divider on a future board rev. Since the battery bus voltage is
* slow-moving relative to a single P&O perturbation-and-settle interval,
* maximizing charge current (ICHG) into that near-constant bus is an
* accepted, commonly-used proxy for maximizing extracted panel power, and
* is what's implemented here. IIN is still used directly for the hard
* panel-side overcurrent backoff (PANEL_IIN_MAX_A).
*/
#ifndef CHARGE_CONTROL_H
#define CHARGE_CONTROL_H
#include <stdint.h>
typedef enum {
CHG_STATE_BULK = 0, /* constant current, MPPT actively perturbing */
CHG_STATE_ABSORPTION, /* constant voltage at BATT_CV_ABSORPTION_V */
CHG_STATE_FLOAT, /* constant voltage at BATT_CV_FLOAT_V, maintenance */
CHG_STATE_FAULT, /* PGOOD lost or a sensor out of range -- output disabled */
} charge_state_t;
typedef struct {
float vbatt;
float iin; /* filtered -- smooth, for the MPPT gradient */
float ichg; /* filtered -- smooth, for the MPPT gradient */
/* Unfiltered copies of the same two currents. Protection limits run on
* these: the exponential filter that makes the MPPT gradient readable
* also delays it, and a limiter acting on a delayed signal permits a
* real overshoot proportional to that delay. Optimise on the smooth
* signal, protect on the fast one. */
float iin_fast;
float ichg_fast;
uint8_t mppt_pgood; /* 1 = LM5175 stage reports power-good */
uint8_t bias_pgood; /* 1 = housekeeping bias reports power-good */
} sensor_frame_t;
typedef struct {
charge_state_t state;
uint32_t state_entry_ms;
/* MPPT (P&O) sub-controller state */
float mppt_ref_v; /* MPPT's own candidate VBATT setpoint */
float mppt_step_v; /* perturbation step size */
int8_t mppt_dir; /* +1 or -1, current perturbation direction */
float mppt_last_ichg;
uint32_t mppt_last_perturb_ms;
uint8_t mppt_armed; /* 1 once a baseline ICHG has been taken */
float mppt_ichg_sum; /* ICHG accumulated across the current window */
uint16_t mppt_ichg_n; /* samples in that accumulation */
/* incremental current limiters, active in every charging state */
float cc_ref_v; /* charge-current limiter's candidate setpoint */
float iin_ref_v; /* panel-current limiter's candidate setpoint */
/* absorption taper must be sustained, so it needs its own timer */
uint32_t taper_start_ms;
uint8_t taper_running;
/* fault debounce counters */
uint8_t fault_count;
uint8_t ok_count;
const char *fault_reason;
/* the final arbitrated output, and which constraint won */
float commanded_vbatt;
const char *limiting_reason;
} charge_control_t;
void charge_control_init(charge_control_t *cc, uint32_t now_ms);
/* Run one control-loop iteration. Call at a steady rate (recommended
* 20-50ms); the MPPT perturbation itself is internally rate-limited to a
* slower cadence (see MPPT_PERTURB_INTERVAL_MS) so it doesn't chase the
* RC filter's own settling time. Returns the commanded VBATT setpoint
* (also available as cc->commanded_vbatt). */
float charge_control_update(charge_control_t *cc, const sensor_frame_t *s, uint32_t now_ms);
#endif /* CHARGE_CONTROL_H */