diff --git a/STM32CubeIDE/Source_Test/Core/Inc/power_stage.h b/STM32CubeIDE/Source_Test/Core/Inc/power_stage.h new file mode 100644 index 0000000..8585ae7 --- /dev/null +++ b/STM32CubeIDE/Source_Test/Core/Inc/power_stage.h @@ -0,0 +1,204 @@ +/** + * @file power_stage.h + * @brief Buck-boost power stage control API. + * + * This module owns the regulator state machine (INIT → IDLE → RUNNING → FAULT), + * the PID outer voltage loop, peak-current-mode inner loop via HRTIM + COMP1, + * slope compensation, and the PD↔Regulator interface (NLSpec §9). + * + * All regulator ISRs (slope comp TIM6, HRTIM period/fault, PID TIM7) are + * implemented as __weak HAL callback overrides within power_stage.c and + * therefore survive CubeMX code regeneration without modification to + * stm32g4xx_it.c. + * + * NLSpec: buck-boost-nlspec-v0_1_2d, §4–§12 + */ + +#ifndef POWER_STAGE_H +#define POWER_STAGE_H + +#include +#include +#include "regulator_config.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========================================================================= + * STATE AND MODE TYPES (NLSpec §4, §5) + * ========================================================================= */ + +/** + * @brief Top-level regulator system states (NLSpec §4). + */ +typedef enum { + PS_STATE_INIT = 0, /**< Power-on; post-init configuration in progress. */ + PS_STATE_IDLE = 1, /**< Outputs off; awaiting enable command. */ + PS_STATE_RUNNING = 2, /**< Actively switching; PID and slope comp running. */ + PS_STATE_FAULT = 3, /**< Latched fault; outputs forced off. */ +} PS_State_t; + +/** + * @brief Operating mode selection (NLSpec §5). + */ +typedef enum { + PS_MODE_BUCK = 0, /**< V_in > V_out: Timer A switching, Timer B static. */ + PS_MODE_BOOST = 1, /**< V_in < V_out: Timer B switching, Timer A static. */ +} PS_Mode_t; + +/* ========================================================================= + * SHARED STATE — PD ↔ Regulator (NLSpec §9.1) + * ========================================================================= */ + +/** + * target_voltage_mv — Negotiated output voltage setpoint. + * Direction: PD stack → Regulator + * Written by: regulator_set_target_voltage(); single aligned 32-bit write (atomic). + * Read by: PID ISR on every execution. + * Units: millivolts. Valid range: 5000–48000. 0 = disable. + */ +extern volatile uint32_t target_voltage_mv; + +/** + * regulator_ready — True when V_out is within the regulation window. + * Direction: Regulator → PD stack + * Written by: PID ISR. Read by: PD stack / application. + */ +extern volatile bool regulator_ready; + +/** + * regulator_fault — True when the regulator is in FAULT state. + * Direction: Regulator → PD stack + * Written by: any ISR that detects a fault condition. + * Read by: PD stack / application. + */ +extern volatile bool regulator_fault; + +/* ========================================================================= + * INITIALISATION AND STATE CONTROL + * ========================================================================= */ + +/** + * @brief Initialise the power stage and transition from INIT to IDLE. + * + * Must be called once, after all CubeMX-generated MX_*_Init() functions have + * completed (including MX_HRTIM1_Init, MX_DAC3_Init, MX_COMP1_Init, + * MX_ADC1_Init, MX_ADC2_Init, MX_TIM6_Init, MX_TIM7_Init). + * + * Performs (NLSpec §4.1): + * - Configure HRTIM Timer A/B Set/Reset sources and backstop CMP1. + * - Set DAC3 CH1 to 0 (zero current setpoint). + * - Start COMP1. + * - Start ADC1 and ADC2 in continuous mode. + * - Precompute slope compensation step size. + * - Configure static (non-switching) leg for default (buck) mode. + * - Enable HRTIM fault inputs FLT1 and FLT2. + * - Configure NVIC priorities for regulator ISRs. + * - Verify no active hardware fault. + * - Transition to IDLE. + */ +void PS_Init(void); + +/** + * @brief Enable the power stage and transition IDLE → RUNNING. + * + * Determines operating mode from measured V_in vs. voltage_mv, configures + * the correct switching/static HRTIM legs, resets the PID integrator, and + * begins soft-start (NLSpec §11.2). + * + * Has no effect if the current state is not IDLE. + * + * @param voltage_mv Target output voltage [5000, 48000] mV. + * @param current_ma Output current limit [0, 5000] mA (reserved for future + * current-limiting wrapper; pass 0 to use hardware max). + */ +void PS_Start(uint32_t voltage_mv, uint32_t current_ma); + +/** + * @brief Disable the power stage and transition RUNNING → IDLE (NLSpec §11.3). + * + * Disables all HRTIM switching outputs, zeroes DAC3 CH1, stops slope + * compensation and PID timers. Safe to call from any state. + */ +void PS_Stop(void); + +/** + * @brief Clear a latched fault condition (NLSpec §4.4). + * + * Transitions FAULT → IDLE only if the hardware fault input (FLT1/FLT2) + * is no longer asserted. Has no effect if not in FAULT state or if the + * hardware fault is still active. + */ +void PS_ClearFault(void); + +/* ========================================================================= + * STATUS QUERIES + * ========================================================================= */ + +/** + * @brief Return the current system state. + * @retval PS_State_t value. + */ +PS_State_t PS_GetState(void); + +/** + * @brief Return the current operating mode (buck or boost). + * @retval PS_Mode_t value. + */ +PS_Mode_t PS_GetMode(void); + +/** + * @brief Return the latest V_out measurement in millivolts. + * Source: ADC1_IN1 (VD_MON). + * @retval V_out in mV. + */ +uint32_t PS_GetVout_mV(void); + +/** + * @brief Return the latest V_in measurement in millivolts. + * Source: ADC2_IN17 (VS_MON). + * @retval V_in in mV. + */ +uint32_t PS_GetVin_mV(void); + +/** + * @brief Return the latest inductor current measurement in milliamps. + * Source: ADC1_IN2 (IL_MON). + * @retval I_L in mA. + */ +uint32_t PS_GetIL_mA(void); + +/* ========================================================================= + * PD ↔ REGULATOR INTERFACE (NLSpec §9.2) + * ========================================================================= */ + +/** + * @brief Set the target output voltage. Safe to call from any context. + * + * - If voltage_mv is 0 and state is RUNNING: transitions to IDLE. + * - If voltage_mv is non-zero and state is RUNNING: PID picks up the new + * setpoint on its next execution. If the change exceeds + * PID_INTEGRATOR_RESET_PCT_DEFAULT, the integrator is reset. + * - If state is IDLE: stores the setpoint but does not start switching. + * An explicit PS_Start() call is required. + * + * @param voltage_mv [0, 48000] mV. 0 means "no voltage requested." + */ +void regulator_set_target_voltage(uint32_t voltage_mv); + +/** + * @brief Stop the regulator. Equivalent to PS_Stop(). Safe from any context. + */ +void regulator_stop(void); + +/** + * @brief Clear a latched fault. Equivalent to PS_ClearFault(). + */ +void regulator_clear_fault(void); + +#ifdef __cplusplus +} +#endif + +#endif /* POWER_STAGE_H */ diff --git a/STM32CubeIDE/Source_Test/Core/Inc/regulator_config.h b/STM32CubeIDE/Source_Test/Core/Inc/regulator_config.h new file mode 100644 index 0000000..e997282 --- /dev/null +++ b/STM32CubeIDE/Source_Test/Core/Inc/regulator_config.h @@ -0,0 +1,526 @@ +/** + * @file regulator_config.h + * @brief Centralized hardware pin mapping and compile-time configuration + * for the buck-boost regulator. + * + * This is the single point of truth for all hardware-derived constants, + * pin assignments, and compile-time configurable parameters as required + * by NLSpec §2.1. + * + * All pin mapping entries include a doc-comment defining: + * - Pin name, MCU port/pin identifier, net label from schematic + * - What it connects to physically, and signal direction + * + * All compile-time configurable constants include a doc-comment stating: + * - Parameter name, expected units, valid range, derivation reference + * + * NLSpec: buck-boost-nlspec-v0_1_2d, §2.1 + * Target MCU: STM32G474RETx (170 MHz Cortex-M4F) + * Board: PD Regulator Prototype Rev 0 + */ + +#ifndef REGULATOR_CONFIG_H +#define REGULATOR_CONFIG_H + +#include "stm32g4xx_hal.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ========================================================================= + * PIN MAPPING — Analog Inputs + * ========================================================================= */ + +/** + * VD_MON — Output voltage sense (V_out). + * MCU pin: PA0 / ADC1_IN1 + * Net: VD_MON on output.kicad_sch + * Connected to: 100 kΩ / 5.82 kΩ resistive divider from V_out rail + * Direction: Analog input + */ +#define PIN_VD_MON_PORT GPIOA +#define PIN_VD_MON_PIN GPIO_PIN_0 +#define ADC_CH_VD_MON ADC_CHANNEL_1 + +/** + * IL_MON — Inductor current sense (shared with COMP1_INP). + * MCU pin: PA1 / ADC1_IN2 / COMP1_INP + * Net: IL_MON on dc-dc.kicad_sch + * Connected to: INA281A2 (U7) output, 50 V/V gain across R5 (5 mΩ shunt) + * Direction: Analog input; also feeds COMP1 non-inverting input directly + */ +#define PIN_IL_MON_PORT GPIOA +#define PIN_IL_MON_PIN GPIO_PIN_1 +#define ADC_CH_IL_MON ADC_CHANNEL_2 + +/** + * VS_MON — Input voltage sense (V_in). + * MCU pin: PA4 / ADC2_IN17 + * Net: VS_MON on input.kicad_sch + * Connected to: 100 kΩ / 5.82 kΩ resistive divider from V_in rail + * Direction: Analog input + * Note: PA4 is only available on ADC2 on this package (not ADC1). + */ +#define PIN_VS_MON_PORT GPIOA +#define PIN_VS_MON_PIN GPIO_PIN_4 +#define ADC_CH_VS_MON ADC_CHANNEL_17 + +/* ========================================================================= + * PIN MAPPING — HRTIM PWM Outputs + * ========================================================================= */ + +/** + * CHA1 — HRTIM Timer A Output 1, drives Q1 (input-side high-side GaN FET). + * MCU pin: PA8 / HRTIM_CHA1 + * Net: CHA1 on dc-dc.kicad_sch + * Direction: PWM output (active switching leg in buck mode) + */ +#define PIN_CHA1_PORT GPIOA +#define PIN_CHA1_PIN GPIO_PIN_8 + +/** + * CHA2 — HRTIM Timer A Output 2, drives Q2 (input-side low-side GaN FET). + * MCU pin: PA9 / HRTIM_CHA2 + * Net: CHA2 on dc-dc.kicad_sch + * Direction: PWM output (complementary to CHA1, dead-time inserted) + */ +#define PIN_CHA2_PORT GPIOA +#define PIN_CHA2_PIN GPIO_PIN_9 + +/** + * CHB1 — HRTIM Timer B Output 1, drives Q3 (output-side high-side GaN FET). + * MCU pin: PA10 / HRTIM_CHB1 + * Net: CHB1 on dc-dc.kicad_sch + * Direction: PWM output (active switching leg in boost mode; + * static high-side hold in buck mode) + */ +#define PIN_CHB1_PORT GPIOA +#define PIN_CHB1_PIN GPIO_PIN_10 + +/** + * CHB2 — HRTIM Timer B Output 2, drives Q4 (output-side low-side GaN FET). + * MCU pin: PA11 / HRTIM_CHB2 + * Net: CHB2 on dc-dc.kicad_sch + * Direction: PWM output (complementary to CHB1, dead-time inserted) + */ +#define PIN_CHB2_PORT GPIOA +#define PIN_CHB2_PIN GPIO_PIN_11 + +/* ========================================================================= + * PIN MAPPING — HRTIM Fault Inputs + * ========================================================================= */ + +/** + * FLT1 — HRTIM Fault Input 1. + * MCU pin: PA12 / HRTIM_FLT1 + * Net: FLT1 on input.kicad_sch + * Connected to: ADM1270ACPZ ~FAULT output (active-low) + * Direction: Digital input, active-low + */ +#define PIN_FLT1_PORT GPIOA +#define PIN_FLT1_PIN GPIO_PIN_12 + +/** + * FLT2 — HRTIM Fault Input 2. + * MCU pin: PA15 / HRTIM_FLT2 + * Net: FLT2 (OPEN:Q5 — physical source unconfirmed) + * Direction: Digital input, active-low + */ +#define PIN_FLT2_PORT GPIOA +#define PIN_FLT2_PIN GPIO_PIN_15 + +/* ========================================================================= + * HRTIM SWITCHING FREQUENCY + * ========================================================================= */ + +/** + * HRTIM_PERIOD_COUNTS — HRTIM period register value; sets the switching frequency. + * Units: HRTIM timer counts (183.82 ps/count at MUL32 prescaler, 5.44 GHz tick) + * Derivation: 170 MHz × 32 / f_sw. See NLSpec §5.5. + * 27200 counts → 200 kHz + * 10880 counts → 500 kHz + * Valid range: [5440, 54400] corresponding to [100 kHz, 1 MHz] + */ +#define HRTIM_PERIOD_COUNTS 27200u + +_Static_assert(HRTIM_PERIOD_COUNTS >= 5440u && HRTIM_PERIOD_COUNTS <= 54400u, + "HRTIM_PERIOD_COUNTS out of valid range [5440, 54400]"); + +/* ========================================================================= + * MAXIMUM DUTY CYCLE / BACKSTOP + * ========================================================================= */ + +/** + * MAX_DUTY_CYCLE_PCT — Maximum charge-phase duty cycle as integer percent. + * Units: percent (0–100) + * Rationale: 85 % leaves 750 ns before period end at 200 kHz for the + * discharge phase and bootstrap refresh window (≥200 ns). + * Constraint: backstop ≤ period − (refresh_ns / tick_ps). + * At 200 kHz: 85 % × 27200 = 23120 counts. See NLSpec §10.3. + * Valid range: [50, 95] + */ +#define MAX_DUTY_CYCLE_PCT 85u + +_Static_assert(MAX_DUTY_CYCLE_PCT >= 50u && MAX_DUTY_CYCLE_PCT <= 95u, + "MAX_DUTY_CYCLE_PCT out of valid range [50, 95]"); + +/** + * MAX_ON_TIME_COUNTS — Hardware backstop Compare 1 register value. + * Units: HRTIM timer counts + * Derivation: HRTIM_PERIOD_COUNTS × MAX_DUTY_CYCLE_PCT / 100 + * At 200 kHz: 27200 × 85 / 100 = 23120 counts ≈ 4.25 µs on-time + */ +#define MAX_ON_TIME_COUNTS (HRTIM_PERIOD_COUNTS * MAX_DUTY_CYCLE_PCT / 100u) + +/* ========================================================================= + * COMPARATOR BLANKING WINDOW + * ========================================================================= */ + +/** + * BLANKING_DURATION_NS — COMP1 blanking window after switching transition. + * Units: nanoseconds + * Rationale: Suppresses IL_MON ringing from parasitic L/C after the FET + * switches. Prevents false COMP1 trips. See NLSpec §5.5. + * Adjust empirically in range 100–500 ns. + * Valid range: [100, 500] + */ +#define BLANKING_DURATION_NS 100u + +_Static_assert(BLANKING_DURATION_NS >= 100u && BLANKING_DURATION_NS <= 500u, + "BLANKING_DURATION_NS out of valid range [100, 500]"); + +/** + * BLANKING_DURATION_COUNTS — Blanking window in HRTIM counts. + * Units: HRTIM timer counts + * Derivation: BLANKING_DURATION_NS × 5.44 counts/ns (at 5.44 GHz tick) + * 100 ns × 5.44 = 544 counts + */ +#define BLANKING_DURATION_COUNTS ((uint32_t)(BLANKING_DURATION_NS * 544u / 100u)) + +/* ========================================================================= + * BOOTSTRAP CAPACITOR REFRESH + * ========================================================================= */ + +/** + * BOOTSTRAP_REFRESH_NS — Bootstrap capacitor refresh pulse minimum duration. + * Units: nanoseconds + * Rationale: uP1966E gate driver requires switch node at GND for ≥200 ns to + * recharge the bootstrap capacitor. Maximum is 500 ns to limit + * inductor energy reversal in boost mode. See NLSpec §5.4. + * Valid range: [200, 500] + */ +#define BOOTSTRAP_REFRESH_NS 200u + +_Static_assert(BOOTSTRAP_REFRESH_NS >= 200u && BOOTSTRAP_REFRESH_NS <= 500u, + "BOOTSTRAP_REFRESH_NS out of valid range [200, 500]"); + +/** + * BOOTSTRAP_REFRESH_COUNTS — Bootstrap refresh pulse duration in HRTIM counts. + * Units: HRTIM timer counts + * Derivation: BOOTSTRAP_REFRESH_NS × 5.44 counts/ns + * 200 ns × 5.44 = 1088 counts + */ +#define BOOTSTRAP_REFRESH_COUNTS ((uint32_t)(BOOTSTRAP_REFRESH_NS * 544u / 100u)) + +/** + * BOOTSTRAP_CMP2_COUNTS — HRTIM Compare 2 value for bootstrap refresh trigger. + * Units: HRTIM timer counts + * Derivation: Period − refresh_counts. Output goes low at this count for the + * static (high-side-held) leg, and returns high at next period reset. + * At 200 kHz: 27200 − 1088 = 26112 counts + */ +#define BOOTSTRAP_CMP2_COUNTS (HRTIM_PERIOD_COUNTS - BOOTSTRAP_REFRESH_COUNTS) + +/* ========================================================================= + * MODE HYSTERESIS + * ========================================================================= */ + +/** + * MODE_HYSTERESIS_MV — Hysteresis band for buck/boost mode selection. + * Units: millivolts + * Derivation: Buck if V_in > V_set + hysteresis; boost if V_in < V_set − hysteresis. + * See NLSpec §5.1. + * Valid range: [100, 5000] + */ +#define MODE_HYSTERESIS_MV 1000u + +_Static_assert(MODE_HYSTERESIS_MV >= 100u && MODE_HYSTERESIS_MV <= 5000u, + "MODE_HYSTERESIS_MV out of valid range [100, 5000]"); + +/* ========================================================================= + * DAC AND CURRENT SCALING (NLSpec §3.5, §6.2) + * ========================================================================= */ + +/** + * DAC_VREF_MV — DAC reference voltage (V_DDA = V_REF+). + * Units: millivolts + * Value: 3300 mV + */ +#define DAC_VREF_MV 3300u + +/** + * DAC_RESOLUTION_COUNTS — DAC full-scale count (12-bit DAC3). + * Units: DAC counts + * Value: 4095 + */ +#define DAC_RESOLUTION_COUNTS 4095u + +/** + * IL_MON_SENS_MV_PER_A — IL_MON signal sensitivity. + * Units: millivolts per ampere + * Derivation: R5 (5 mΩ) × INA281A2 gain (50 V/V) = 0.250 V/A = 250 mV/A. + * See NLSpec §3.3. + */ +#define IL_MON_SENS_MV_PER_A 250u + +/** + * DAC_COUNTS_PER_AMP — DAC counts per ampere of peak inductor current. + * Units: counts / A + * Derivation: (DAC_VREF_MV / DAC_RESOLUTION_COUNTS) / IL_MON_SENS_MV_PER_A × 1000 + * = (3300 / 4095) / 250 × 1000 ≈ 3.22 mA/count → 310 counts/A + * Integer approximation: 310 counts/A. See NLSpec §6.2. + */ +#define DAC_COUNTS_PER_AMP 310u + +/* ========================================================================= + * VOLTAGE MEASUREMENT SCALING (NLSpec §3.4, §14.2) + * ========================================================================= */ + +/** + * VOLTAGE_SCALE_NUM / VOLTAGE_SCALE_DEN — V_out / V_in ADC channel scaling. + * Units: millivolts per ADC count (as a fraction) + * Derivation: Divider ratio = 5820 / (100000 + 5820) = 0.05500 + * V_mV = ADC_raw × (V_ADC_FS_mV / ADC_FS) / ratio + * = ADC_raw × (3300 / 4096) / 0.05500 + * ≈ ADC_raw × 60000 / 4096 (≈ 14.65 mV/count) + * Integer formula: V_mV = (uint32_t)ADC_raw × 60000u / 4096u + */ +#define VOLTAGE_SCALE_NUM 60000u +#define VOLTAGE_SCALE_DEN 4096u + +/** + * IL_MON_SCALE_NUM / IL_MON_SCALE_DEN — Inductor current ADC channel scaling. + * Units: milliamps per ADC count (as a fraction) + * Derivation: I_mA = ADC_raw × V_ADC_FS_mV / (ADC_FS × sensitivity_V_per_A) + * = ADC_raw × 3300 / (4096 × 0.250) + * = ADC_raw × 13200 / 4096 (≈ 3.22 mA/count) + * See NLSpec §14.2. + */ +#define IL_MON_SCALE_NUM 13200u +#define IL_MON_SCALE_DEN 4096u + +/** + * ID_MON_SCALE_NUM / ID_MON_SCALE_DEN — Output current (ID_MON) ADC scaling. + * Units: milliamps per ADC count (as a fraction) + * Derivation: R6 (12 mΩ) × INA281A2 (50 V/V) = 600 mV/A + * I_mA = ADC_raw × 3300 / (4096 × 0.600) + * = ADC_raw × 5500 / 4096 (≈ 1.34 mA/count) + * See NLSpec §14.2. OPEN:Q2 — pin assignment unconfirmed. + */ +#define ID_MON_SCALE_NUM 5500u +#define ID_MON_SCALE_DEN 4096u + +/** + * IS_MON_SCALE_NUM / IS_MON_SCALE_DEN — Input current (IS_MON) ADC scaling. + * Units: milliamps per ADC count (as a fraction) + * Derivation: R25 (5 mΩ, confirmed by physical inspection) × gain (50 V/V) = 250 mV/A + * Identical to IL_MON sensitivity. + * I_mA = ADC_raw × 13200 / 4096 (≈ 3.22 mA/count) + * See NLSpec §3.3, §14.2. + */ +#define IS_MON_SCALE_NUM 13200u +#define IS_MON_SCALE_DEN 4096u + +/* ========================================================================= + * PID OUTPUT LIMITS (NLSpec §8.6) + * ========================================================================= */ + +/** + * PID_OUTPUT_MIN — Minimum PID output value (DAC counts). + * Units: DAC counts (0–4095) + * Rationale: Zero current setpoint — comparator never trips, regulator idle. + */ +#define PID_OUTPUT_MIN 0u + +/** + * PID_OUTPUT_MAX — Maximum PID output value (DAC counts). + * Units: DAC counts (0–4095) + * Derivation: USB PD EPR maximum = 5 A; 5 A × 310 counts/A ≈ 1550 counts. + * OPEN:Q7 — verify inductor/FET ratings exceed 5 A with margin. + * See NLSpec §8.6. + * Valid range: [1, 4095] + */ +#define PID_OUTPUT_MAX 1550u + +_Static_assert(PID_OUTPUT_MAX >= 1u && PID_OUTPUT_MAX <= 4095u, + "PID_OUTPUT_MAX out of valid range [1, 4095]"); + +/* ========================================================================= + * SAFETY THRESHOLDS (NLSpec §10.2) + * ========================================================================= */ + +/** + * OVP_ABSOLUTE_MV — Absolute output overvoltage protection hard cap. + * Units: millivolts + * Derivation: 110 % × 48000 mV (USB PD EPR max) = 52800 mV. See NLSpec §10.2. + * THIS IS A COMPILE-TIME CONSTANT — must not be changed at runtime. + * The hardware ceiling is the ADM1270 OVP at ~60 V. + */ +#define OVP_ABSOLUTE_MV 52800u + +/** + * OVP_RELATIVE_PCT_DEFAULT — Default relative OVP threshold (% of setpoint). + * Units: percent + * Rationale: Output / load protection. See NLSpec §10.2. + * Valid range: [105, 150]. Runtime-mutable tuning parameter. + */ +#define OVP_RELATIVE_PCT_DEFAULT 110u + +/** + * UVP_RELATIVE_PCT_DEFAULT — Default relative UVP threshold (% of setpoint). + * Units: percent + * Rationale: Loss-of-regulation detection. See NLSpec §10.2. + * Valid range: [20, 90]. Runtime-mutable tuning parameter. + */ +#define UVP_RELATIVE_PCT_DEFAULT 50u + +/** + * MAX_CONSECUTIVE_BACKSTOPS_DEFAULT — Max consecutive backstop events before fault. + * Units: count + * Rationale: 3 consecutive periods hitting backstop without COMP1 firing + * indicates a persistent overcurrent / no-trip condition. NLSpec §10.4. + * Valid range: [1, 255]. Runtime-mutable tuning parameter. + */ +#define MAX_CONSECUTIVE_BACKSTOPS_DEFAULT 3u + +/* ========================================================================= + * SOFT-START (NLSpec §11.2) + * ========================================================================= */ + +/** + * SOFTSTART_RAMP_TIME_MS — Soft-start ramp duration from 0 to target voltage. + * Units: milliseconds + * Rationale: Limits inrush current and output voltage overshoot on enable. + * OPEN:Q12 — Dan to confirm desired ramp time. + * Valid range: [1, 100] + */ +#define SOFTSTART_RAMP_TIME_MS 10u + +_Static_assert(SOFTSTART_RAMP_TIME_MS >= 1u && SOFTSTART_RAMP_TIME_MS <= 100u, + "SOFTSTART_RAMP_TIME_MS out of valid range [1, 100]"); + +/* ========================================================================= + * PID EXECUTION RATE (NLSpec §8.2) + * ========================================================================= */ + +/** + * PID_RATE_HZ — PID outer voltage loop execution rate. + * Units: Hz + * Derivation: 20 kHz = every 10th switching period at 200 kHz. See NLSpec §8.2. + * Range: [1000, f_sw]. Default: 20000 Hz. + * Valid range: [1000, 500000] + */ +#define PID_RATE_HZ 20000u + +_Static_assert(PID_RATE_HZ >= 1000u && PID_RATE_HZ <= 500000u, + "PID_RATE_HZ out of valid range [1000, 500000]"); + +/** + * PID_TIMER_PERIOD_COUNTS — TIM7 auto-reload register value for the PID rate. + * Units: timer counts (170 MHz, prescaler = 0) + * Derivation: 170,000,000 / PID_RATE_HZ − 1 + * At 20 kHz: 170,000,000 / 20,000 − 1 = 8499 + */ +#define PID_TIMER_PERIOD_COUNTS (170000000u / PID_RATE_HZ - 1u) + +/* ========================================================================= + * SLOPE COMPENSATION TIMER (NLSpec §7.5) + * ========================================================================= */ + +/** + * SLOPE_COMP_RATE_HZ — Slope compensation staircase timer rate. + * Units: Hz + * Rationale: Must be >> f_sw. At 2 MHz gives 10 steps/period at 200 kHz. + * CPU budget: 10–30 cycles per ISR ≈ 12–36 % load at 2 MHz / 170 MHz. + * See NLSpec §7.5. + * Valid range: [1000000, 10000000] + */ +#define SLOPE_COMP_RATE_HZ 2000000u + +_Static_assert(SLOPE_COMP_RATE_HZ >= 1000000u && SLOPE_COMP_RATE_HZ <= 10000000u, + "SLOPE_COMP_RATE_HZ out of valid range [1 MHz, 10 MHz]"); + +/** + * SLOPE_COMP_TIMER_PERIOD_COUNTS — TIM6 auto-reload register value. + * Units: timer counts (170 MHz, prescaler = 0) + * Derivation: 170,000,000 / SLOPE_COMP_RATE_HZ − 1 + * At 2 MHz: 170,000,000 / 2,000,000 − 1 = 84 + */ +#define SLOPE_COMP_TIMER_PERIOD_COUNTS (170000000u / SLOPE_COMP_RATE_HZ - 1u) + +/** + * SLOPE_A_PER_S_DEFAULT — Default slope compensation rate. + * Units: A/s (amperes per second) + * Derivation: Conservative value exceeding 50 % of worst-case downslope. + * Buck worst case: V_out = 20 V, L = 4.7 µH → downslope = 4.26 A/µs = 4.26e6 A/s. + * 50 % of that = 2.13e6 A/s. Default 4.0e6 A/s provides 1.88× margin. + * OPEN:Q10 — Dan to confirm intended slope value. See NLSpec §7.3. + * Runtime-mutable tuning parameter. + */ +#define SLOPE_A_PER_S_DEFAULT 4000000u + +/* ========================================================================= + * USB PD VOLTAGE LIMITS (NLSpec §1.1, §9.2) + * ========================================================================= */ + +/** + * VSETPOINT_MIN_MV — Minimum valid non-zero voltage setpoint. + * Units: millivolts + * Derivation: USB PD SPR minimum advertised voltage = 5 V. See NLSpec §9.2. + */ +#define VSETPOINT_MIN_MV 5000u + +/** + * VSETPOINT_MAX_MV — Maximum valid voltage setpoint. + * Units: millivolts + * Derivation: USB PD EPR maximum = 48 V. See NLSpec §9.2. + */ +#define VSETPOINT_MAX_MV 48000u + +/** + * VREGULATION_WINDOW_MV — V_out tolerance for regulator_ready assertion. + * Units: millivolts (±) + * Rationale: V_out within ±2 % of setpoint is "in regulation". At 48 V + * that is ±960 mV. 1000 mV is a conservative, integer-friendly bound. + */ +#define VREGULATION_WINDOW_MV 1000u + +/** + * PID_INTEGRATOR_RESET_PCT_DEFAULT — Large-step threshold for integrator reset. + * Units: percent of previous setpoint + * Derivation: Reset integrator if setpoint change > 20 %. See NLSpec §8.8. + * Valid range: [5, 100]. Runtime-mutable tuning parameter. + */ +#define PID_INTEGRATOR_RESET_PCT_DEFAULT 20u + +/* ========================================================================= + * NVIC PRIORITIES (NLSpec §13.2) + * configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 3 (from FreeRTOSConfig.h) + * Regulator ISRs must be priorities 0-2 (above FreeRTOS critical sections). + * ========================================================================= */ + +/** Slope compensation timer (TIM6) — most latency-sensitive. */ +#define NVIC_PRIO_SLOPE_COMP 0u + +/** HRTIM period / fault interrupts — must never be delayed by RTOS. */ +#define NVIC_PRIO_HRTIM 1u + +/** PID timer (TIM7) and paired ADC completion — below HRTIM. */ +#define NVIC_PRIO_PID 2u + +#ifdef __cplusplus +} +#endif + +#endif /* REGULATOR_CONFIG_H */ diff --git a/STM32CubeIDE/Source_Test/Core/Src/main.c b/STM32CubeIDE/Source_Test/Core/Src/main.c index 2068f3d..66f39ec 100644 --- a/STM32CubeIDE/Source_Test/Core/Src/main.c +++ b/STM32CubeIDE/Source_Test/Core/Src/main.c @@ -25,7 +25,7 @@ /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ - +#include "power_stage.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ diff --git a/STM32CubeIDE/Source_Test/Core/Src/power_stage.c b/STM32CubeIDE/Source_Test/Core/Src/power_stage.c new file mode 100644 index 0000000..24bb169 --- /dev/null +++ b/STM32CubeIDE/Source_Test/Core/Src/power_stage.c @@ -0,0 +1,817 @@ +/** + * @file power_stage.c + * @brief Buck-boost power stage control implementation. + * + * Implements the regulator state machine, peak-current-mode control via + * HRTIM + COMP1 + DAC3, slope compensation (TIM6 staircase), and the PID + * outer voltage loop (TIM7). + * + * All ISR entry points are implemented as HAL callback overrides + * (HAL_TIM_PeriodElapsedCallback, HAL_HRTIM_RepetitionEventCallback, + * HAL_HRTIM_Compare1EventCallback, HAL_HRTIM_FaultNotification), which + * survive CubeMX code regeneration without any modification to + * stm32g4xx_it.c. + * + * NLSpec: buck-boost-nlspec-v0_1_2d, §4–§12 + * + * Peripheral handles required (declared in CubeMX-generated main.c): + * hhrtim1 — HRTIM1 (Timer A and Timer B, EEV4, FLT1/FLT2) + * hdac3 — DAC3 Channel 1 (slope-compensated peak current reference) + * hadc1 — ADC1 (VD_MON / IL_MON) + * hadc2 — ADC2 (VS_MON) + * hcomp1 — COMP1 (IL_MON vs DAC3_CH1; output → EEV4) + * htim6 — TIM6 (slope compensation, NVIC priority 0) + * htim7 — TIM7 (PID voltage loop, NVIC priority 2) + */ + +#include "power_stage.h" +#include "regulator_config.h" +#include "main.h" +#include "stm32g4xx_hal.h" +#include + +/* ========================================================================= + * External HAL handles (defined in CubeMX-generated main.c) + * ========================================================================= */ +extern HRTIM_HandleTypeDef hhrtim1; +extern DAC_HandleTypeDef hdac3; +extern ADC_HandleTypeDef hadc1; +extern ADC_HandleTypeDef hadc2; +extern COMP_HandleTypeDef hcomp1; +extern TIM_HandleTypeDef htim6; /* slope compensation */ +extern TIM_HandleTypeDef htim7; /* PID voltage loop */ + +/* ========================================================================= + * SHARED STATE — written here, read by PD stack (NLSpec §9.1) + * ========================================================================= */ +volatile uint32_t target_voltage_mv = 0u; +volatile bool regulator_ready = false; +volatile bool regulator_fault = false; + +/* ========================================================================= + * PRIVATE STATE + * ========================================================================= */ + +/** Current top-level state machine state. */ +static volatile PS_State_t s_state = PS_STATE_INIT; + +/** Current operating mode (buck / boost). */ +static volatile PS_Mode_t s_mode = PS_MODE_BUCK; + +/* --- PID state (NLSpec §8.4, §8.7) --- */ +static volatile float s_pid_integrator = 0.0f; +static volatile float s_pid_error_prev = 0.0f; +static volatile uint32_t s_pid_output_dac = 0u; /* DAC counts, Y-intercept */ + +/** Runtime-mutable PID coefficients (NLSpec §8.7). */ +static float s_pid_kp = 1.0f; +static float s_pid_ki = 100.0f; +static float s_pid_kd = 0.0f; + +/** Runtime-mutable safety thresholds (NLSpec §10.2). */ +static uint8_t s_ovp_pct = OVP_RELATIVE_PCT_DEFAULT; +static uint8_t s_uvp_pct = UVP_RELATIVE_PCT_DEFAULT; +static uint8_t s_max_backstop = MAX_CONSECUTIVE_BACKSTOPS_DEFAULT; +static uint8_t s_integrator_reset_pct = PID_INTEGRATOR_RESET_PCT_DEFAULT; + +/* --- ADC telemetry (updated by PID ISR and background ADC conversions) --- */ +static volatile uint32_t s_vout_mv = 0u; +static volatile uint32_t s_vin_mv = 0u; +static volatile uint32_t s_il_ma = 0u; + +/* --- Slope compensation --- */ +/** + * Precomputed DAC decrement per TIM6 tick. + * Units: DAC counts per SLOPE_COMP_RATE_HZ tick. + * Derivation: slope_a_per_s × DAC_COUNTS_PER_AMP / SLOPE_COMP_RATE_HZ + */ +static volatile uint32_t s_slope_step_dac = 0u; + +/** Runtime-mutable slope value (NLSpec §7.3). */ +static uint32_t s_slope_a_per_s = SLOPE_A_PER_S_DEFAULT; + +/* --- Backstop violation counter (NLSpec §10.4) --- */ +static volatile uint8_t s_consecutive_backstops = 0u; +static volatile bool s_comp1_fired_this_period = false; + +/* --- Soft-start (NLSpec §11.2) --- */ +static volatile bool s_softstart_active = false; +static volatile float s_softstart_setpoint_mv = 0.0f; +static volatile float s_softstart_ramp_mv_per_pid = 0.0f; + +/* ========================================================================= + * PRIVATE HELPER PROTOTYPES + * ========================================================================= */ +static void ps_configure_hrtim_buck(void); +static void ps_configure_hrtim_boost(void); +static void ps_configure_static_leg_buck(void); +static void ps_configure_static_leg_boost(void); +static void ps_disable_all_outputs(void); +static void ps_zero_dac(void); +static void ps_precompute_slope_step(void); +static void ps_enter_fault(void); +static PS_Mode_t ps_select_mode(uint32_t vin_mv, uint32_t vset_mv); + +/* ========================================================================= + * SHARED STATE — PUBLIC GETTERS + * ========================================================================= */ + +PS_State_t PS_GetState(void) { return s_state; } +PS_Mode_t PS_GetMode(void) { return s_mode; } +uint32_t PS_GetVout_mV(void){ return s_vout_mv; } +uint32_t PS_GetVin_mV(void) { return s_vin_mv; } +uint32_t PS_GetIL_mA(void) { return s_il_ma; } + +/* ========================================================================= + * PD ↔ REGULATOR INTERFACE (NLSpec §9.2) + * ========================================================================= */ + +void regulator_set_target_voltage(uint32_t voltage_mv) +{ + if (voltage_mv == 0u) { + /* 0 mV setpoint while RUNNING → transition to IDLE (NLSpec §9.2) */ + if (s_state == PS_STATE_RUNNING) { + PS_Stop(); + } + target_voltage_mv = 0u; + return; + } + + if (voltage_mv > VSETPOINT_MAX_MV) { + voltage_mv = VSETPOINT_MAX_MV; + } + + /* Check whether the step is large enough to warrant an integrator reset */ + if ((s_state == PS_STATE_RUNNING) && (target_voltage_mv > 0u)) { + uint32_t prev = target_voltage_mv; + uint32_t delta = (voltage_mv > prev) ? (voltage_mv - prev) : (prev - voltage_mv); + /* delta > threshold_pct % of prev → reset integrator (NLSpec §8.8) */ + if ((delta * 100u) > ((uint32_t)s_integrator_reset_pct * prev)) { + s_pid_integrator = 0.0f; + s_pid_error_prev = 0.0f; + } + } + + target_voltage_mv = voltage_mv; /* single aligned 32-bit write — atomic on Cortex-M4 */ +} + +void regulator_stop(void) { PS_Stop(); } +void regulator_clear_fault(void) { PS_ClearFault(); } + +/* ========================================================================= + * PS_Init — post-CubeMX initialisation (NLSpec §4.1) + * ========================================================================= */ + +void PS_Init(void) +{ + /* 1. Configure HRTIM Timer A and Timer B Set/Reset sources for buck mode + * (default operating mode). See NLSpec §5.2, §5.5. */ + ps_configure_hrtim_buck(); + + /* 2. Configure the static leg (Timer B) for buck mode: + * CHB1 high throughout period with end-of-period bootstrap refresh. */ + ps_configure_static_leg_buck(); + + /* 3. Set backstop Compare 1 on both Timer A and Timer B. See NLSpec §10.3. */ + __HAL_HRTIM_SETCOMPARE(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_COMPAREUNIT_1, MAX_ON_TIME_COUNTS); + __HAL_HRTIM_SETCOMPARE(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_COMPAREUNIT_1, MAX_ON_TIME_COUNTS); + + /* 4. Set bootstrap refresh Compare 2 on both timers. See NLSpec §5.4. */ + __HAL_HRTIM_SETCOMPARE(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_COMPAREUNIT_2, BOOTSTRAP_CMP2_COUNTS); + __HAL_HRTIM_SETCOMPARE(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_COMPAREUNIT_2, BOOTSTRAP_CMP2_COUNTS); + + /* 5. Zero DAC3 CH1 — comparator threshold = 0, no switching. */ + HAL_DAC_Start(&hdac3, DAC_CHANNEL_1); + ps_zero_dac(); + + /* 6. Start COMP1. See NLSpec §4.1. */ + HAL_COMP_Start(&hcomp1); + + /* 7. Start ADC1 (VD_MON, IL_MON) and ADC2 (VS_MON) in continuous mode. */ + HAL_ADC_Start(&hadc1); + HAL_ADC_Start(&hadc2); + + /* 8. Precompute slope compensation step size. See NLSpec §7.5. */ + ps_precompute_slope_step(); + + /* 9. Configure NVIC priorities for regulator ISRs. See NLSpec §13.2. + * These must be below configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY (3) + * to be above FreeRTOS masking. */ + HAL_NVIC_SetPriority(TIM6_DAC_IRQn, NVIC_PRIO_SLOPE_COMP, 0u); + HAL_NVIC_SetPriority(TIM7_IRQn, NVIC_PRIO_PID, 0u); + HAL_NVIC_SetPriority(HRTIM1_TIMA_IRQn, NVIC_PRIO_HRTIM, 0u); + HAL_NVIC_SetPriority(HRTIM1_TIMB_IRQn, NVIC_PRIO_HRTIM, 0u); + HAL_NVIC_SetPriority(HRTIM1_FLT_IRQn, NVIC_PRIO_HRTIM, 0u); + + /* 10. Enable HRTIM fault inputs FLT1 and FLT2. See NLSpec §10.1. + * The fault polarity and output state (all FETs off = INACTIVE) must + * be configured in the CubeMX project. This call arms the fault + * detection and enables the fault interrupt. */ + HAL_HRTIM_EnableFault(&hhrtim1, HRTIM_FAULT_1); + HAL_HRTIM_EnableFault(&hhrtim1, HRTIM_FAULT_2); + + /* 11. Verify no active fault before declaring IDLE. */ + uint32_t fault_status = HRTIM1->sCommonRegs.ISR; + if (fault_status & (HRTIM_ISR_FLT1 | HRTIM_ISR_FLT2)) { + ps_enter_fault(); + return; + } + + /* 12. All outputs remain inactive (IDLE). Do not start switching yet. */ + s_state = PS_STATE_IDLE; + regulator_fault = false; + regulator_ready = false; +} + +/* ========================================================================= + * PS_Start — IDLE → RUNNING (NLSpec §4.2→4.3, §11.1) + * ========================================================================= */ + +void PS_Start(uint32_t voltage_mv, uint32_t current_ma) +{ + (void)current_ma; /* Reserved for future current-limiting wrapper. */ + + if (s_state != PS_STATE_IDLE) { + return; + } + + if ((voltage_mv < VSETPOINT_MIN_MV) || (voltage_mv > VSETPOINT_MAX_MV)) { + return; + } + + /* Store target. */ + target_voltage_mv = voltage_mv; + + /* Perform a V_in ADC read to determine operating mode. */ + uint32_t vin = s_vin_mv; + PS_Mode_t mode = ps_select_mode(vin, voltage_mv); + + /* Reconfigure HRTIM if mode has changed. */ + if (mode != s_mode) { + if (mode == PS_MODE_BUCK) { + ps_configure_hrtim_buck(); + ps_configure_static_leg_buck(); + } else { + ps_configure_hrtim_boost(); + ps_configure_static_leg_boost(); + } + s_mode = mode; + } + + /* Reset PID state before enabling (NLSpec §8.8). */ + s_pid_integrator = 0.0f; + s_pid_error_prev = 0.0f; + s_pid_output_dac = 0u; + + /* Reset backstop counter. */ + s_consecutive_backstops = 0u; + s_comp1_fired_this_period = false; + + /* Initialise soft-start ramp (NLSpec §11.2). + * Ramp from 0 to target over SOFTSTART_RAMP_TIME_MS × PID_RATE_HZ / 1000 + * PID ticks. During soft-start the integrator is held at zero and only + * the proportional ramp drives the setpoint. */ + uint32_t pid_ticks_for_ramp = (SOFTSTART_RAMP_TIME_MS * PID_RATE_HZ) / 1000u; + if (pid_ticks_for_ramp == 0u) { + pid_ticks_for_ramp = 1u; + } + s_softstart_setpoint_mv = 0.0f; + s_softstart_ramp_mv_per_pid = (float)voltage_mv / (float)pid_ticks_for_ramp; + s_softstart_active = true; + + /* Load initial DAC value = 0 (safe: no current until PID runs). */ + ps_zero_dac(); + + /* Start slope compensation timer (TIM6, priority 0). */ + HAL_TIM_Base_Start_IT(&htim6); + + /* Start HRTIM outputs. */ + if (s_mode == PS_MODE_BUCK) { + /* Enable Timer A (switching) and Timer B (static). */ + HAL_HRTIM_WaveformOutputStart(&hhrtim1, + HRTIM_OUTPUT_TA1 | HRTIM_OUTPUT_TA2 | + HRTIM_OUTPUT_TB1 | HRTIM_OUTPUT_TB2); + } else { + /* Enable Timer B (switching) and Timer A (static). */ + HAL_HRTIM_WaveformOutputStart(&hhrtim1, + HRTIM_OUTPUT_TB1 | HRTIM_OUTPUT_TB2 | + HRTIM_OUTPUT_TA1 | HRTIM_OUTPUT_TA2); + } + + /* Enable HRTIM period (repetition) interrupt for DAC Y-intercept reload. + * RepetitionCounter must be 0 in CubeMX config to fire every period. */ + if (s_mode == PS_MODE_BUCK) { + __HAL_HRTIM_TIMER_ENABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_TIM_IT_REP); + } else { + __HAL_HRTIM_TIMER_ENABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_TIM_IT_REP); + } + + /* Enable HRTIM Compare 1 interrupt for backstop counting (NLSpec §10.4). */ + if (s_mode == PS_MODE_BUCK) { + __HAL_HRTIM_TIMER_ENABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_TIM_IT_CMP1); + } else { + __HAL_HRTIM_TIMER_ENABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_TIM_IT_CMP1); + } + + /* Start PID timer (TIM7, priority 2). */ + HAL_TIM_Base_Start_IT(&htim7); + + s_state = PS_STATE_RUNNING; + regulator_ready = false; + regulator_fault = false; +} + +/* ========================================================================= + * PS_Stop — RUNNING → IDLE (NLSpec §11.3) + * ========================================================================= */ + +void PS_Stop(void) +{ + /* Disable PID and slope comp timers first to prevent further DAC writes. */ + HAL_TIM_Base_Stop_IT(&htim7); + HAL_TIM_Base_Stop_IT(&htim6); + + /* Disable HRTIM interrupts on both timers. */ + __HAL_HRTIM_TIMER_DISABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_TIM_IT_REP | HRTIM_TIM_IT_CMP1); + __HAL_HRTIM_TIMER_DISABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_TIM_IT_REP | HRTIM_TIM_IT_CMP1); + + /* Disable all HRTIM outputs. */ + ps_disable_all_outputs(); + + /* Zero DAC — comparator threshold = 0. */ + ps_zero_dac(); + + s_softstart_active = false; + regulator_ready = false; + + if (s_state != PS_STATE_FAULT) { + s_state = PS_STATE_IDLE; + } +} + +/* ========================================================================= + * PS_ClearFault (NLSpec §4.4) + * ========================================================================= */ + +void PS_ClearFault(void) +{ + if (s_state != PS_STATE_FAULT) { + return; + } + + /* Check whether the hardware fault source is still asserted. */ + uint32_t fault_status = HRTIM1->sCommonRegs.ISR; + if (fault_status & (HRTIM_ISR_FLT1 | HRTIM_ISR_FLT2)) { + /* Hardware fault still active — cannot clear yet. */ + return; + } + + /* Clear HRTIM fault flags. */ + HRTIM1->sCommonRegs.ICR = HRTIM_ICR_FLT1C | HRTIM_ICR_FLT2C; + + /* Reset PID and shared state. */ + s_pid_integrator = 0.0f; + s_pid_error_prev = 0.0f; + s_pid_output_dac = 0u; + s_consecutive_backstops = 0u; + + regulator_fault = false; + regulator_ready = false; + s_state = PS_STATE_IDLE; +} + +/* ========================================================================= + * PRIVATE: HRTIM OUTPUT CONFIGURATION + * ========================================================================= */ + +/** + * Configure Timer A as the switching leg for BUCK mode (NLSpec §5.2, §5.5). + * CHA1: Set = Period (HIGH at period start); Reset = EEV4 | CMP1 (backstop). + * Polarity: ACTIVE = HIGH. + */ +static void ps_configure_hrtim_buck(void) +{ + HRTIM_OutputCfgTypeDef ocfg = {0}; + + /* Timer A Output 1 — switching leg (high-side, Q1). */ + ocfg.Polarity = HRTIM_OUTPUTPOLARITY_HIGH; + ocfg.SetSource = HRTIM_OUTPUTSET_TIMPER; + ocfg.ResetSource = HRTIM_OUTPUTRESET_EEV_4 | + HRTIM_OUTPUTRESET_TIMCMP1; + ocfg.IdleMode = HRTIM_OUTPUTIDLEMODE_NONE; + ocfg.IdleLevel = HRTIM_OUTPUTIDLELEVEL_INACTIVE; + ocfg.FaultLevel = HRTIM_OUTPUTFAULTLEVEL_INACTIVE; + ocfg.ChopperModeEnable = HRTIM_OUTPUTCHOPPERMODE_DISABLED; + ocfg.BurstModeEntryDelayed = HRTIM_OUTPUTBURSTMODEENTRY_REGULAR; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_OUTPUT_TA1, &ocfg); + + /* Timer A Output 2 — complementary low-side (Q2); polarity governed by + * the dead-time insertion configured in CubeMX. No explicit Set/Reset + * needed here — it follows the complement of TA1. */ + ocfg.SetSource = 0U; + ocfg.ResetSource = 0U; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_OUTPUT_TA2, &ocfg); +} + +/** + * Configure Timer B as the switching leg for BOOST mode (NLSpec §5.3, §5.5). + * CHB1: Set = Period (→ LOW due to inverted polarity, charge phase begins); + * Reset = EEV4 | CMP1 (→ HIGH, discharge phase begins). + * Polarity: ACTIVE = LOW (inverted relative to buck). + */ +static void ps_configure_hrtim_boost(void) +{ + HRTIM_OutputCfgTypeDef ocfg = {0}; + + /* Timer B Output 1 — switching leg (high-side, Q3), inverted polarity. */ + ocfg.Polarity = HRTIM_OUTPUTPOLARITY_LOW; + ocfg.SetSource = HRTIM_OUTPUTSET_TIMPER; + ocfg.ResetSource = HRTIM_OUTPUTRESET_EEV_4 | + HRTIM_OUTPUTRESET_TIMCMP1; + ocfg.IdleMode = HRTIM_OUTPUTIDLEMODE_NONE; + ocfg.IdleLevel = HRTIM_OUTPUTIDLELEVEL_INACTIVE; + ocfg.FaultLevel = HRTIM_OUTPUTFAULTLEVEL_INACTIVE; + ocfg.ChopperModeEnable = HRTIM_OUTPUTCHOPPERMODE_DISABLED; + ocfg.BurstModeEntryDelayed = HRTIM_OUTPUTBURSTMODEENTRY_REGULAR; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_OUTPUT_TB1, &ocfg); + + /* Timer B Output 2 — complementary low-side (Q4). */ + ocfg.Polarity = HRTIM_OUTPUTPOLARITY_LOW; + ocfg.SetSource = 0U; + ocfg.ResetSource = 0U; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_OUTPUT_TB2, &ocfg); +} + +/** + * Configure Timer B as the STATIC leg for BUCK mode (NLSpec §5.2, §5.4). + * CHB1 held HIGH for most of period; brief LOW at end-of-period for bootstrap + * refresh (CMP2-triggered, duration = BOOTSTRAP_REFRESH_COUNTS). + */ +static void ps_configure_static_leg_buck(void) +{ + HRTIM_OutputCfgTypeDef ocfg = {0}; + + ocfg.Polarity = HRTIM_OUTPUTPOLARITY_HIGH; + ocfg.SetSource = HRTIM_OUTPUTSET_TIMPER; /* HIGH at period start */ + ocfg.ResetSource = HRTIM_OUTPUTRESET_TIMCMP2; /* LOW at CMP2 (bootstrap) */ + ocfg.IdleMode = HRTIM_OUTPUTIDLEMODE_NONE; + ocfg.IdleLevel = HRTIM_OUTPUTIDLELEVEL_INACTIVE; + ocfg.FaultLevel = HRTIM_OUTPUTFAULTLEVEL_INACTIVE; + ocfg.ChopperModeEnable = HRTIM_OUTPUTCHOPPERMODE_DISABLED; + ocfg.BurstModeEntryDelayed = HRTIM_OUTPUTBURSTMODEENTRY_REGULAR; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_OUTPUT_TB1, &ocfg); + + /* TB2 (low-side, Q4) is complementary and managed by dead-time insertion. */ + ocfg.SetSource = 0U; + ocfg.ResetSource = 0U; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_OUTPUT_TB2, &ocfg); +} + +/** + * Configure Timer A as the STATIC leg for BOOST mode (NLSpec §5.3, §5.4). + * CHA1 held HIGH for most of period; brief LOW at end-of-period for bootstrap + * refresh (CMP2-triggered). + */ +static void ps_configure_static_leg_boost(void) +{ + HRTIM_OutputCfgTypeDef ocfg = {0}; + + ocfg.Polarity = HRTIM_OUTPUTPOLARITY_HIGH; + ocfg.SetSource = HRTIM_OUTPUTSET_TIMPER; /* HIGH at period start */ + ocfg.ResetSource = HRTIM_OUTPUTRESET_TIMCMP2; /* LOW at CMP2 (bootstrap) */ + ocfg.IdleMode = HRTIM_OUTPUTIDLEMODE_NONE; + ocfg.IdleLevel = HRTIM_OUTPUTIDLELEVEL_INACTIVE; + ocfg.FaultLevel = HRTIM_OUTPUTFAULTLEVEL_INACTIVE; + ocfg.ChopperModeEnable = HRTIM_OUTPUTCHOPPERMODE_DISABLED; + ocfg.BurstModeEntryDelayed = HRTIM_OUTPUTBURSTMODEENTRY_REGULAR; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_OUTPUT_TA1, &ocfg); + + /* TA2 (low-side, Q2) follows complement via dead-time insertion. */ + ocfg.SetSource = 0U; + ocfg.ResetSource = 0U; + HAL_HRTIM_WaveformOutputConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_OUTPUT_TA2, &ocfg); +} + +/** Disable all HRTIM outputs (safe state). */ +static void ps_disable_all_outputs(void) +{ + HAL_HRTIM_WaveformOutputStop(&hhrtim1, + HRTIM_OUTPUT_TA1 | HRTIM_OUTPUT_TA2 | + HRTIM_OUTPUT_TB1 | HRTIM_OUTPUT_TB2); +} + +/** Write 0 to DAC3 CH1 (direct register access for speed). */ +static void ps_zero_dac(void) +{ + /* Direct register write — 32-bit aligned, atomic. */ + DAC3->DHR12R1 = 0u; +} + +/** Precompute slope compensation step (DAC counts per TIM6 tick). */ +static void ps_precompute_slope_step(void) +{ + /* slope_step_dac = slope_a_per_s × DAC_COUNTS_PER_AMP / SLOPE_COMP_RATE_HZ + * Use 64-bit intermediate to avoid overflow. */ + uint64_t num = (uint64_t)s_slope_a_per_s * (uint64_t)DAC_COUNTS_PER_AMP; + s_slope_step_dac = (uint32_t)(num / (uint64_t)SLOPE_COMP_RATE_HZ); + if (s_slope_step_dac == 0u) { + s_slope_step_dac = 1u; /* minimum 1 count per tick */ + } +} + +/** Select buck or boost based on V_in vs. V_setpoint with hysteresis. */ +static PS_Mode_t ps_select_mode(uint32_t vin_mv, uint32_t vset_mv) +{ + if (vin_mv > (vset_mv + MODE_HYSTERESIS_MV)) { + return PS_MODE_BUCK; + } + if (vin_mv < (vset_mv > MODE_HYSTERESIS_MV ? + vset_mv - MODE_HYSTERESIS_MV : 0u)) { + return PS_MODE_BOOST; + } + /* Within hysteresis band — keep current mode. */ + return s_mode; +} + +/** Enter FAULT state. Called from any ISR or task context. */ +static void ps_enter_fault(void) +{ + /* Disable timers and outputs immediately. */ + HAL_TIM_Base_Stop_IT(&htim7); + HAL_TIM_Base_Stop_IT(&htim6); + __HAL_HRTIM_TIMER_DISABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, + HRTIM_TIM_IT_REP | HRTIM_TIM_IT_CMP1); + __HAL_HRTIM_TIMER_DISABLE_IT(&hhrtim1, HRTIM_TIMERINDEX_TIMER_B, + HRTIM_TIM_IT_REP | HRTIM_TIM_IT_CMP1); + ps_disable_all_outputs(); + ps_zero_dac(); + + /* Reset PID integrator. */ + s_pid_integrator = 0.0f; + s_pid_error_prev = 0.0f; + s_pid_output_dac = 0u; + s_softstart_active = false; + + s_state = PS_STATE_FAULT; + regulator_ready = false; + regulator_fault = true; +} + +/* ========================================================================= + * HAL CALLBACK OVERRIDES — ISR HANDLERS + * + * These override the __weak HAL callbacks and are safe with CubeMX + * regeneration: they do not require changes to stm32g4xx_it.c. + * The CubeMX-generated IRQ handlers call HAL_TIM_IRQHandler / + * HAL_HRTIM_IRQHandler, which dispatch to these callbacks. + * ========================================================================= */ + +/* ----------------------------------------------------------------------- + * TIM6 ISR — Slope Compensation (NLSpec §7.5) + * NVIC priority 0 (highest — above FreeRTOS and all other regulator ISRs). + * Fires at SLOPE_COMP_RATE_HZ (2 MHz default) while RUNNING. + * Decrements DAC3 CH1 by s_slope_step_dac counts each tick. + * The step size is precomputed at init; never recomputed here (NLSpec §7.5). + * ----------------------------------------------------------------------- */ +void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) +{ + if (htim->Instance == TIM6) + { + /* Slope compensation — decrement DAC3 CH1 each TIM6 tick. */ + if (s_state == PS_STATE_RUNNING) { + uint32_t cur = DAC3->DHR12R1; + if (cur >= s_slope_step_dac) { + DAC3->DHR12R1 = cur - s_slope_step_dac; + } else { + DAC3->DHR12R1 = 0u; + } + } + } + else if (htim->Instance == TIM7) + { + /* PID voltage loop (NLSpec §8). */ + + if (s_state != PS_STATE_RUNNING) { + return; + } + + /* --- Sample ADC values --- + * Note: In the final CubeMX project, ADC1 should be configured with + * VD_MON (IN1) as rank 1 in injected or single-channel continuous mode + * so that HAL_ADC_GetValue returns the V_out measurement. + * ADC2 uses VS_MON (IN17) for V_in. See NLSpec §8.5. */ + uint32_t vout_raw = HAL_ADC_GetValue(&hadc1); + uint32_t vin_raw = HAL_ADC_GetValue(&hadc2); + + /* Convert to millivolts (NLSpec §14.2). */ + uint32_t vout_mv = (uint32_t)(vout_raw * VOLTAGE_SCALE_NUM / VOLTAGE_SCALE_DEN); + uint32_t vin_mv = (uint32_t)(vin_raw * VOLTAGE_SCALE_NUM / VOLTAGE_SCALE_DEN); + + /* Update telemetry (single 32-bit store — atomic). */ + s_vout_mv = vout_mv; + s_vin_mv = vin_mv; + + /* --- Software safety checks (NLSpec §10.2) --- */ + uint32_t vset = target_voltage_mv; + + /* Absolute OVP (compile-time constant — must not be tuned). */ + if (vout_mv > OVP_ABSOLUTE_MV) { + ps_enter_fault(); + return; + } + + if (vset > 0u) { + /* Relative OVP: V_out > setpoint × OVP_pct / 100. */ + if (vout_mv > ((vset * (uint32_t)s_ovp_pct) / 100u)) { + ps_enter_fault(); + return; + } + /* UVP: V_out < setpoint × UVP_pct / 100 (only after soft-start). */ + if (!s_softstart_active && + (vout_mv < ((vset * (uint32_t)s_uvp_pct) / 100u))) { + ps_enter_fault(); + return; + } + /* V_in validation (NLSpec §10.2). */ + if (s_mode == PS_MODE_BUCK) { + if (vin_mv < (vset + MODE_HYSTERESIS_MV / 2u)) { + ps_enter_fault(); + return; + } + } else { + if (vin_mv > (vset > MODE_HYSTERESIS_MV / 2u ? + vset - MODE_HYSTERESIS_MV / 2u : 0u)) { + ps_enter_fault(); + return; + } + } + } + + /* --- Backstop violation count check (NLSpec §10.4) --- */ + if (s_consecutive_backstops >= s_max_backstop) { + ps_enter_fault(); + return; + } + + /* --- Mode re-selection (NLSpec §5.1) --- */ + if (vset > 0u) { + PS_Mode_t new_mode = ps_select_mode(vin_mv, vset); + if (new_mode != s_mode) { + /* Controlled mode transition sequence (NLSpec §5.1). */ + PS_Stop(); + s_mode = new_mode; + PS_Start(vset, 0u); + return; + } + } + + /* --- Soft-start ramp (NLSpec §11.2) --- */ + float effective_setpoint_mv; + if (s_softstart_active) { + s_softstart_setpoint_mv += s_softstart_ramp_mv_per_pid; + if (s_softstart_setpoint_mv >= (float)vset) { + s_softstart_setpoint_mv = (float)vset; + s_softstart_active = false; + } + effective_setpoint_mv = s_softstart_setpoint_mv; + } else { + effective_setpoint_mv = (float)vset; + } + + /* --- PID computation (NLSpec §8.4) --- */ + float dt = 1.0f / (float)PID_RATE_HZ; + float error = effective_setpoint_mv - (float)vout_mv; + + float p_term = s_pid_kp * error; + float i_term = s_pid_integrator + s_pid_ki * error * dt; + float d_term = s_pid_kd * (error - s_pid_error_prev) / dt; + float output = p_term + i_term + d_term; + + /* Anti-windup: clamp integrator (back-calculation, NLSpec §8.4). */ + if (output > (float)PID_OUTPUT_MAX) { + i_term -= (output - (float)PID_OUTPUT_MAX); + output = (float)PID_OUTPUT_MAX; + } else if (output < (float)PID_OUTPUT_MIN) { + i_term -= (output - (float)PID_OUTPUT_MIN); + output = (float)PID_OUTPUT_MIN; + } + + s_pid_integrator = i_term; + s_pid_error_prev = error; + + /* Clamp to DAC range [PID_OUTPUT_MIN, PID_OUTPUT_MAX]. */ + uint32_t dac_counts = (uint32_t)output; + if (dac_counts > PID_OUTPUT_MAX) { + dac_counts = PID_OUTPUT_MAX; + } + + /* Publish new Y-intercept for slope comp to use at next period reset. */ + s_pid_output_dac = dac_counts; /* single 32-bit store — atomic */ + + /* --- Update regulator_ready status --- */ + if (vset > 0u) { + uint32_t diff = (vout_mv > vset) ? (vout_mv - vset) : (vset - vout_mv); + regulator_ready = (diff <= VREGULATION_WINDOW_MV); + } else { + regulator_ready = false; + } + } +} + +/* ----------------------------------------------------------------------- + * HRTIM RepetitionEvent Callback — DAC Y-intercept reload (NLSpec §7.6) + * NVIC priority 1. Fires at start of each switching period (RepetitionCounter + * must be configured as 0 in the CubeMX project for Timer A and Timer B). + * Reloads DAC3 CH1 with s_pid_output_dac BEFORE the blanking window expires. + * Also resets the COMP1-fired flag and checks backstop counter. + * ----------------------------------------------------------------------- */ +void HAL_HRTIM_RepetitionEventCallback(HRTIM_HandleTypeDef *hhrtim, + uint32_t TimerIdx) +{ + (void)hhrtim; + + /* Only process the active switching timer. */ + bool is_active_timer = (s_mode == PS_MODE_BUCK) + ? (TimerIdx == HRTIM_TIMERINDEX_TIMER_A) + : (TimerIdx == HRTIM_TIMERINDEX_TIMER_B); + + if (!is_active_timer || (s_state != PS_STATE_RUNNING)) { + return; + } + + /* --- Backstop counting (NLSpec §10.4) --- */ + if (s_comp1_fired_this_period) { + /* COMP1 fired naturally — reset counter. */ + s_consecutive_backstops = 0u; + } else { + /* COMP1 did not fire — backstop may have fired; increment counter. + * The actual fault check happens in the PID ISR to keep this ISR short. */ + if (s_consecutive_backstops < 255u) { + s_consecutive_backstops++; + } + } + s_comp1_fired_this_period = false; + + /* --- Reload DAC Y-intercept for slope compensation (NLSpec §7.6) --- + * Direct register write is necessary to ensure the DAC is updated before + * the blanking window expires (~544 counts = 100 ns after period reset). + * s_pid_output_dac is written by TIM7 ISR and read here; both are 32-bit + * aligned volatile accesses — atomic on Cortex-M4. */ + DAC3->DHR12R1 = s_pid_output_dac; +} + +/* ----------------------------------------------------------------------- + * HRTIM Compare1 Callback — Backstop event tracking (NLSpec §10.3, §10.4) + * NVIC priority 1. Fires when CMP1 (MAX_ON_TIME_COUNTS) is reached without + * COMP1 tripping. Clears the comp1_fired flag so the RepetitionEvent + * callback counts it as a backstop event. + * Note: COMP1 firing via EEV4 resets the output without generating an ISR. + * We infer COMP1 fired if the CMP1 interrupt does NOT fire in a period. + * ----------------------------------------------------------------------- */ +void HAL_HRTIM_Compare1EventCallback(HRTIM_HandleTypeDef *hhrtim, + uint32_t TimerIdx) +{ + (void)hhrtim; + (void)TimerIdx; + /* CMP1 fired (backstop). COMP1 did not trip naturally this period. */ + /* s_comp1_fired_this_period remains false — counted at next period start. */ +} + +/* ----------------------------------------------------------------------- + * HRTIM Fault Notification — Hardware fault handling (NLSpec §10.1) + * NVIC priority 1. Fires when FLT1 or FLT2 asserts. The hardware has + * already forced all outputs to INACTIVE. Software updates state. + * ----------------------------------------------------------------------- */ +void HAL_HRTIM_FaultNotification(HRTIM_HandleTypeDef *hhrtim, uint32_t Fault) +{ + (void)hhrtim; + (void)Fault; + ps_enter_fault(); +} + +/* ========================================================================= + * MODE TRANSITION HELPER + * + * Called from PID ISR when a mode change is needed. Sequence per NLSpec §5.1: + * 1. Disable outputs. 2. Reconfigure. 3. Reset PID. 4. Soft-start. + * PS_Stop() and PS_Start() implement the full sequence. + * ========================================================================= */ +/* (Implemented inline within the TIM7 PID block above.) */ diff --git a/STM32CubeIDE/Source_Test/Core/Src/state_machine.c b/STM32CubeIDE/Source_Test/Core/Src/state_machine.c index da0b6ba..45c9d7f 100644 --- a/STM32CubeIDE/Source_Test/Core/Src/state_machine.c +++ b/STM32CubeIDE/Source_Test/Core/Src/state_machine.c @@ -1,128 +1,77 @@ #include "state_machine.h" +#include "power_stage.h" #include "main.h" -// Define states -typedef enum { - STATE_INIT, - STATE_MEASURE, - STATE_REGULATE, - STATE_CALCULATE_DUTY, - STATE_ERROR -} State_t; +/* + * Application-level state machine. + * + * The regulator's own state machine (INIT → IDLE → RUNNING → FAULT) is + * fully managed inside power_stage.c. This module is a thin coordinator + * that initialises the power stage and monitors its status from the default + * FreeRTOS task context. + * + * Actual voltage/current regulation is interrupt-driven (TIM6 slope comp, + * HRTIM period, TIM7 PID) and requires no polling from this task. + */ typedef enum { - NONE, - BUCK, - BOOST, - BUCKBOOST, - ERROR -} RegulationType_t; - -static State_t currentState = STATE_INIT; - -static RegulationType_t regulationType = NONE; - -// Define voltage thresholds -#define BUCK_VOLTAGE_THRESHOLD 1.33f -#define BUCK_BUCKBOOST_VOLTAGE_THRESHOLD 1.18f -#define BOOST_BUCKBOOST_VOLTAGE_THRESHOLD 0.85f -#define BOOST_VOLTAGE_THRESHOLD 0.75f + STATE_INIT = 0, + STATE_IDLE = 1, + STATE_RUNNING = 2, + STATE_ERROR = 3, +} AppState_t; -// Example variables -static float outputVoltage = 0.0f; -static float inputVoltage = 0.0f; -static float targetVoltage = 5.0f; -static float dutyCycle = 0.0f; +static AppState_t s_app_state = STATE_INIT; -void StateMachine_Init(void) { - currentState = STATE_INIT; +void StateMachine_Init(void) +{ + s_app_state = STATE_INIT; } -void StateMachine_Task(void) { - switch (currentState) { +void StateMachine_Task(void) +{ + switch (s_app_state) + { case STATE_INIT: - // Initialization logic - currentState = STATE_MEASURE; + /* Initialise the power stage hardware (post-CubeMX MX_* calls). */ + PS_Init(); + s_app_state = STATE_IDLE; break; - case STATE_MEASURE: - // Perform measurement - outputVoltage = 3.3f; // Example - currentState = STATE_REGULATE; - break; - - case STATE_REGULATE: - // Determine regulation type - switch (regulationType) { // TODO: make discrete fumction - case NONE: - // No regulation needed - break; - case BUCK: - // Buck mode - if ((inputVoltage / outputVoltage) < BOOST_VOLTAGE_THRESHOLD) { - // Switch to boost mode - regulationType = BOOST; - // Buck mode config - } - else if ((inputVoltage / outputVoltage) < BUCK_BUCKBOOST_VOLTAGE_THRESHOLD) { - // Switch to buck-boost mode - regulationType = BUCKBOOST; - // Buck-boost mode config - } - break; - case BOOST: - // Boost mode - if ((inputVoltage / outputVoltage) > BUCK_VOLTAGE_THRESHOLD) { - // Switch to buck mode - regulationType = BUCK; - // Boost mode config - } - else if ((inputVoltage / outputVoltage) > BOOST_BUCKBOOST_VOLTAGE_THRESHOLD) { - // Switch to buck-boost mode - regulationType = BUCKBOOST; - // Buck-boost mode config - } - break; - case BUCKBOOST: - // Buck-boost mode - if ((inputVoltage / outputVoltage) > BUCK_VOLTAGE_THRESHOLD) { - // Switch to buck mode - regulationType = BUCK; - // Buck-boost mode config - } - else if ((inputVoltage / outputVoltage) < BOOST_VOLTAGE_THRESHOLD) { - // Switch to boost mode - regulationType = BOOST; - // Buck-boost mode config - } - break; - case ERROR: - // Handle error - //TODO: what is an error? - break; + case STATE_IDLE: + /* Power stage is IDLE — waiting for the USB PD stack to negotiate + * a contract and call regulator_set_target_voltage() + PS_Start(). + * Poll for unexpected fault transitions. */ + if (PS_GetState() == PS_STATE_FAULT) { + s_app_state = STATE_ERROR; + } else if (PS_GetState() == PS_STATE_RUNNING) { + s_app_state = STATE_RUNNING; } - currentState = STATE_CALCULATE_DUTY; break; - case STATE_CALCULATE_DUTY: - // Calculate duty cycle - // TODO: PID control logic - PID.calculate(outputVoltage, targetVoltage); - dutyCycle += PID.error; - if (dutyCycle > 100.0f) { - dutyCycle = 100.0f; - } else if (dutyCycle < 0.0f) { - dutyCycle = 0.0f; + case STATE_RUNNING: + /* Regulation is fully interrupt-driven; nothing to do here except + * monitor for fault conditions. */ + if (PS_GetState() == PS_STATE_FAULT) { + s_app_state = STATE_ERROR; + } else if (PS_GetState() == PS_STATE_IDLE) { + /* Regulator stopped (e.g. 0 V setpoint from PD stack). */ + s_app_state = STATE_IDLE; } break; case STATE_ERROR: - // Handle error - Error_Handler(); + /* Fault is latched. The USB PD stack is expected to detect the + * fault via regulator_fault, disconnect VBUS, and call + * regulator_clear_fault() once the hardware fault clears. + * Transition back to IDLE when the power stage clears itself. */ + if (PS_GetState() == PS_STATE_IDLE) { + s_app_state = STATE_IDLE; + } break; default: - currentState = STATE_ERROR; + s_app_state = STATE_ERROR; break; } } \ No newline at end of file diff --git a/STM32CubeIDE/Source_Test/USBPD/usbpd_dpm_user.c b/STM32CubeIDE/Source_Test/USBPD/usbpd_dpm_user.c index c470f55..bd579ea 100644 --- a/STM32CubeIDE/Source_Test/USBPD/usbpd_dpm_user.c +++ b/STM32CubeIDE/Source_Test/USBPD/usbpd_dpm_user.c @@ -39,7 +39,7 @@ #if !defined(_TRACE) #include "string.h" #endif /* !_TRACE */ - +#include "power_stage.h" /* USER CODE END Includes */ /** @addtogroup STM32_USBPD_APPLICATION @@ -325,41 +325,18 @@ void USBPD_DPM_Notification(uint8_t PortNum, USBPD_NotifyEventValue_TypeDef Even DPM_GUI_PostNotificationMessage(PortNum, EventVal); } /* USER CODE BEGIN USBPD_DPM_Notification */ - /* Manage event notified by the stack? */ + /* Manage event notified by the stack. */ switch(EventVal) { -// case USBPD_NOTIFY_POWER_EXPLICIT_CONTRACT : -// break; -// case USBPD_NOTIFY_REQUEST_ACCEPTED: -// break; -// case USBPD_NOTIFY_REQUEST_REJECTED: -// case USBPD_NOTIFY_REQUEST_WAIT: -// break; -// case USBPD_NOTIFY_POWER_SWAP_TO_SNK_DONE: -// break; -// case USBPD_NOTIFY_STATE_SNK_READY: -// break; -// case USBPD_NOTIFY_HARDRESET_RX: -// case USBPD_NOTIFY_HARDRESET_TX: -// break; -// case USBPD_NOTIFY_STATE_SRC_DISABLED: -// break; -// case USBPD_NOTIFY_ALERT_RECEIVED : -// break; -// case USBPD_NOTIFY_CABLERESET_REQUESTED : -// break; -// case USBPD_NOTIFY_MSG_NOT_SUPPORTED : -// break; -// case USBPD_NOTIFY_PE_DISABLED : -// break; -// case USBPD_NOTIFY_USBSTACK_START: -// break; -// case USBPD_NOTIFY_USBSTACK_STOP: -// break; -// case USBPD_NOTIFY_DATAROLESWAP_DFP : -// break; -// case USBPD_NOTIFY_DATAROLESWAP_UFP : -// break; + case USBPD_NOTIFY_POWER_EXPLICIT_CONTRACT: + /* A PD contract has been established. The negotiated voltage has + * already been set via BSP_USBPD_PWR_VBUSSetVoltage_Fixed() → PS_Start(). + * Nothing more to do here; regulator_ready will reflect output status. */ + break; + case USBPD_NOTIFY_HARDRESET_RX: + case USBPD_NOTIFY_HARDRESET_TX: + /* Hard reset — power stage is stopped by USBPD_DPM_HardReset callback. */ + break; default: DPM_USER_DEBUG_TRACE(PortNum, "ADVICE: USBPD_DPM_Notification:%d", EventVal); break; diff --git a/STM32CubeIDE/Source_Test/USBPD/usbpd_pdo_defs.h b/STM32CubeIDE/Source_Test/USBPD/usbpd_pdo_defs.h index abc451a..f9eb8c6 100644 --- a/STM32CubeIDE/Source_Test/USBPD/usbpd_pdo_defs.h +++ b/STM32CubeIDE/Source_Test/USBPD/usbpd_pdo_defs.h @@ -34,7 +34,7 @@ /* USER CODE END Includes */ /* Define ------------------------------------------------------------------*/ -#define PORT0_NB_SOURCEPDO 1U /* Number of Source PDOs (applicable for port 0) */ +#define PORT0_NB_SOURCEPDO 5U /* Number of Source PDOs (applicable for port 0) — SPR: 5V,9V,12V,15V,20V */ #define PORT0_NB_SINKPDO 0U /* Number of Sink PDOs (applicable for port 0) */ #define PORT1_NB_SOURCEPDO 0U /* Number of Source PDOs (applicable for port 1) */ #define PORT1_NB_SINKPDO 0U /* Number of Sink PDOs (applicable for port 1) */ @@ -77,7 +77,7 @@ typedef struct /* USER CODE BEGIN Exported_Define */ -#define USBPD_CORE_PDO_SRC_FIXED_MAX_CURRENT 3 +#define USBPD_CORE_PDO_SRC_FIXED_MAX_CURRENT 3000 /* 3 A for SPR PDOs 1–4 */ #define USBPD_CORE_PDO_SNK_FIXED_MAX_CURRENT 1500 /* USER CODE END Exported_Define */ @@ -121,34 +121,64 @@ uint8_t USBPD_NbPDO[4] = {(PORT0_NB_SINKPDO), /* Definition of Source PDO for Port 0 */ uint32_t PORT0_PDO_ListSRC[USBPD_MAX_NB_PDO] = { - /* PDO 1 */ + /* + * Source PDO table — USB PD Standard Power Range (SPR) voltages. + * NLSpec §9.4, firmware_plan §4a. + * All PDOs use fixed-voltage format (USBPD_PDO_TYPE_FIXED). + * PDO 1 (mandatory 5 V) carries the capability flags for the port. + */ + + /* PDO 1 — 5 V @ 3 A (mandatory; carries port capability flags) */ ( - USBPD_PDO_TYPE_FIXED | /* Fixed supply PDO */ - - USBPD_PDO_SRC_FIXED_SET_VOLTAGE(5000U) | /* Voltage in mV */ - USBPD_PDO_SRC_FIXED_SET_MAX_CURRENT(100U) | /* Max current in mA */ - USBPD_PDO_SRC_FIXED_PEAKCURRENT_EQUAL | /* Peak Current info */ - - /* Common definitions applicable to all PDOs, defined only in PDO 1 */ - USBPD_PDO_SRC_FIXED_UNCHUNK_NOT_SUPPORTED | /* Unchunked Extended Messages */ - USBPD_PDO_SRC_FIXED_DRD_SUPPORTED | /* Dual-Role Data */ - USBPD_PDO_SRC_FIXED_USBCOMM_NOT_SUPPORTED | /* USB Communications */ - USBPD_PDO_SRC_FIXED_EXT_POWER_NOT_AVAILABLE | /* External Power */ - USBPD_PDO_SRC_FIXED_USBSUSPEND_NOT_SUPPORTED | /* USB Suspend Supported */ - USBPD_PDO_SRC_FIXED_DRP_NOT_SUPPORTED /* Dual-Role Power */ + USBPD_PDO_TYPE_FIXED | + USBPD_PDO_SRC_FIXED_SET_VOLTAGE(5000U) | /* 5000 mV */ + USBPD_PDO_SRC_FIXED_SET_MAX_CURRENT(3000U) | /* 3000 mA */ + USBPD_PDO_SRC_FIXED_PEAKCURRENT_EQUAL | + USBPD_PDO_SRC_FIXED_UNCHUNK_NOT_SUPPORTED | + USBPD_PDO_SRC_FIXED_DRD_SUPPORTED | + USBPD_PDO_SRC_FIXED_USBCOMM_NOT_SUPPORTED | + USBPD_PDO_SRC_FIXED_EXT_POWER_NOT_AVAILABLE | + USBPD_PDO_SRC_FIXED_USBSUSPEND_NOT_SUPPORTED | + USBPD_PDO_SRC_FIXED_DRP_NOT_SUPPORTED ), - /* PDO 2 */ (0x00000000U), + /* PDO 2 — 9 V @ 3 A */ + ( + USBPD_PDO_TYPE_FIXED | + USBPD_PDO_SRC_FIXED_SET_VOLTAGE(9000U) | + USBPD_PDO_SRC_FIXED_SET_MAX_CURRENT(3000U) | + USBPD_PDO_SRC_FIXED_PEAKCURRENT_EQUAL + ), - /* PDO 3 */ (0x00000000U), + /* PDO 3 — 12 V @ 3 A */ + ( + USBPD_PDO_TYPE_FIXED | + USBPD_PDO_SRC_FIXED_SET_VOLTAGE(12000U) | + USBPD_PDO_SRC_FIXED_SET_MAX_CURRENT(3000U) | + USBPD_PDO_SRC_FIXED_PEAKCURRENT_EQUAL + ), - /* PDO 4 */ (0x00000000U), + /* PDO 4 — 15 V @ 3 A */ + ( + USBPD_PDO_TYPE_FIXED | + USBPD_PDO_SRC_FIXED_SET_VOLTAGE(15000U) | + USBPD_PDO_SRC_FIXED_SET_MAX_CURRENT(3000U) | + USBPD_PDO_SRC_FIXED_PEAKCURRENT_EQUAL + ), - /* PDO 5 */ (0x00000000U), + /* PDO 5 — 20 V @ 5 A (maximum SPR power = 100 W) */ + ( + USBPD_PDO_TYPE_FIXED | + USBPD_PDO_SRC_FIXED_SET_VOLTAGE(20000U) | + USBPD_PDO_SRC_FIXED_SET_MAX_CURRENT(5000U) | + USBPD_PDO_SRC_FIXED_PEAKCURRENT_EQUAL + ), - /* PDO 6 */ (0x00000000U), + /* PDO 6 — reserved (EPR AVS 28 V when EPR library available) */ + (0x00000000U), - /* PDO 7 */ (0x00000000U), + /* PDO 7 — reserved (EPR AVS 36 V / 48 V when EPR library available) */ + (0x00000000U), }; diff --git a/STM32CubeIDE/Source_Test/USBPD/usbpd_pwr_user.c b/STM32CubeIDE/Source_Test/USBPD/usbpd_pwr_user.c index a0db0df..b892110 100644 --- a/STM32CubeIDE/Source_Test/USBPD/usbpd_pwr_user.c +++ b/STM32CubeIDE/Source_Test/USBPD/usbpd_pwr_user.c @@ -27,7 +27,7 @@ #endif /* _TRACE */ /* USER CODE BEGIN include */ - +#include "power_stage.h" /* USER CODE END include */ /** @addtogroup BSP @@ -304,7 +304,7 @@ __weak int32_t BSP_USBPD_PWR_VBUSDeInit(uint32_t Instance) __weak int32_t BSP_USBPD_PWR_VBUSOn(uint32_t Instance) { /* USER CODE BEGIN BSP_USBPD_PWR_VBUSOn */ - /* Check if instance is valid */ + /* Check if instance is valid */ int32_t ret; if (Instance >= USBPD_PWR_INSTANCES_NBR) @@ -313,8 +313,24 @@ __weak int32_t BSP_USBPD_PWR_VBUSOn(uint32_t Instance) } else { - ret = BSP_ERROR_FEATURE_NOT_SUPPORTED; - PWR_DEBUG_TRACE(Instance, "ADVICE: Update BSP_USBPD_PWR_VBUSOn"); + /* Enable output MOSFET and assert input-enable GPIO, then start the + * power stage with the last negotiated voltage setpoint (NLSpec §4b). */ + HAL_GPIO_WritePin(Output_dischg_GPIO_Port, Output_dischg_Pin, GPIO_PIN_RESET); + HAL_GPIO_WritePin(Output_en_GPIO_Port, Output_en_Pin, GPIO_PIN_SET); + HAL_GPIO_WritePin(Input_en_GPIO_Port, Input_en_Pin, GPIO_PIN_SET); + + uint32_t vset = target_voltage_mv; + if ((vset >= VSETPOINT_MIN_MV) && (vset <= VSETPOINT_MAX_MV)) + { + PS_Start(vset, 0u); + ret = BSP_ERROR_NONE; + } + else + { + /* No valid setpoint yet — VBUS will be sourced at vSafe5V by default; + * the PD stack will call VBUSSetVoltage_Fixed once a contract is made. */ + ret = BSP_ERROR_NONE; + } } return ret; /* USER CODE END BSP_USBPD_PWR_VBUSOn */ @@ -330,7 +346,7 @@ __weak int32_t BSP_USBPD_PWR_VBUSOn(uint32_t Instance) __weak int32_t BSP_USBPD_PWR_VBUSOff(uint32_t Instance) { /* USER CODE BEGIN BSP_USBPD_PWR_VBUSOff */ - /* Check if instance is valid */ + /* Check if instance is valid */ int32_t ret; if (Instance >= USBPD_PWR_INSTANCES_NBR) @@ -339,8 +355,13 @@ __weak int32_t BSP_USBPD_PWR_VBUSOff(uint32_t Instance) } else { - ret = BSP_ERROR_FEATURE_NOT_SUPPORTED; - PWR_DEBUG_TRACE(Instance, "ADVICE: Update BSP_USBPD_PWR_VBUSOff"); + /* Stop the power stage, enable output discharge resistor, and + * disable input path (NLSpec §4c / firmware_plan §4c). */ + PS_Stop(); + HAL_GPIO_WritePin(Output_dischg_GPIO_Port, Output_dischg_Pin, GPIO_PIN_SET); + HAL_GPIO_WritePin(Output_en_GPIO_Port, Output_en_Pin, GPIO_PIN_RESET); + HAL_GPIO_WritePin(Input_en_GPIO_Port, Input_en_Pin, GPIO_PIN_RESET); + ret = BSP_ERROR_NONE; } return ret; /* USER CODE END BSP_USBPD_PWR_VBUSOff */ @@ -362,13 +383,25 @@ __weak int32_t BSP_USBPD_PWR_VBUSSetVoltage_Fixed(uint32_t Instance, uint32_t MaxOperatingCurrent) { /* USER CODE BEGIN BSP_USBPD_PWR_VBUSSetVoltage_Fixed */ - /* Check if instance is valid */ + /* Check if instance is valid */ int32_t ret = BSP_ERROR_NONE; if (Instance >= USBPD_PWR_INSTANCES_NBR) { ret = BSP_ERROR_WRONG_PARAM; } + else + { + /* Store the negotiated setpoint. If the regulator is already RUNNING, + * the PID picks it up on its next execution. If IDLE, start it now. */ + (void)MaxOperatingCurrent; + regulator_set_target_voltage(VbusTargetInmv); + + if (PS_GetState() == PS_STATE_IDLE) + { + PS_Start(VbusTargetInmv, OperatingCurrent); + } + } return ret; /* USER CODE END BSP_USBPD_PWR_VBUSSetVoltage_Fixed */ } @@ -469,7 +502,7 @@ __weak int32_t BSP_USBPD_PWR_VBUSSetVoltage_APDO(uint32_t Instance, __weak int32_t BSP_USBPD_PWR_VBUSGetVoltage(uint32_t Instance, uint32_t *pVoltage) { /* USER CODE BEGIN BSP_USBPD_PWR_VBUSGetVoltage */ - /* Check if instance is valid */ + /* Check if instance is valid */ int32_t ret; uint32_t val = 0U; @@ -479,8 +512,8 @@ __weak int32_t BSP_USBPD_PWR_VBUSGetVoltage(uint32_t Instance, uint32_t *pVoltag } else { - ret = BSP_ERROR_FEATURE_NOT_SUPPORTED; - PWR_DEBUG_TRACE(Instance, "ADVICE: Update BSP_USBPD_PWR_VBUSGetVoltage"); + val = PS_GetVout_mV(); + ret = BSP_ERROR_NONE; } *pVoltage = val; return ret; @@ -710,7 +743,7 @@ __weak int32_t BSP_USBPD_PWR_RegisterVBUSDetectCallback(uint32_t Instance, __weak int32_t BSP_USBPD_PWR_VBUSIsOn(uint32_t Instance, uint8_t *pState) { /* USER CODE BEGIN BSP_USBPD_PWR_VBUSIsOn */ - /* Check if instance is valid */ + /* Check if instance is valid */ int32_t ret; uint8_t state = 0U; @@ -720,8 +753,8 @@ __weak int32_t BSP_USBPD_PWR_VBUSIsOn(uint32_t Instance, uint8_t *pState) } else { - ret = BSP_ERROR_FEATURE_NOT_SUPPORTED; - PWR_DEBUG_TRACE(Instance, "ADVICE: Update BSP_USBPD_PWR_VBUSIsOn"); + state = (PS_GetState() == PS_STATE_RUNNING) ? 1U : 0U; + ret = BSP_ERROR_NONE; } *pState = state; return ret; diff --git a/STM32CubeIDE/final/Core/Inc/FreeRTOSConfig.h b/STM32CubeIDE/final/Core/Inc/FreeRTOSConfig.h index 131575a..f8241e2 100644 --- a/STM32CubeIDE/final/Core/Inc/FreeRTOSConfig.h +++ b/STM32CubeIDE/final/Core/Inc/FreeRTOSConfig.h @@ -109,7 +109,7 @@ function. */ routine that makes calls to interrupt safe FreeRTOS API functions. DO NOT CALL INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER PRIORITY THAN THIS! (higher priorities are lower numeric values. */ -#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 3 +#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 /* Interrupt priorities used by the kernel port layer itself. These are generic to all Cortex-M ports, and do not rely on any particular library functions. */ diff --git a/STM32CubeIDE/final/Core/Src/main.c b/STM32CubeIDE/final/Core/Src/main.c index 2da2a6f..617786d 100644 --- a/STM32CubeIDE/final/Core/Src/main.c +++ b/STM32CubeIDE/final/Core/Src/main.c @@ -796,7 +796,7 @@ static void MX_UCPD1_Init(void) LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_4, LL_DMA_MDATAALIGN_BYTE); /* UCPD1 interrupt Init */ - NVIC_SetPriority(UCPD1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),3, 0)); + NVIC_SetPriority(UCPD1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),5, 0)); NVIC_EnableIRQ(UCPD1_IRQn); /* USER CODE BEGIN UCPD1_Init 1 */ @@ -820,13 +820,13 @@ static void MX_DMA_Init(void) /* DMA interrupt init */ /* DMA1_Channel2_IRQn interrupt configuration */ - NVIC_SetPriority(DMA1_Channel2_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),3, 0)); + NVIC_SetPriority(DMA1_Channel2_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),5, 0)); NVIC_EnableIRQ(DMA1_Channel2_IRQn); /* DMA1_Channel3_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 3, 0); HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn); /* DMA1_Channel4_IRQn interrupt configuration */ - NVIC_SetPriority(DMA1_Channel4_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),3, 0)); + NVIC_SetPriority(DMA1_Channel4_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),5, 0)); NVIC_EnableIRQ(DMA1_Channel4_IRQn); /* DMA1_Channel5_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 3, 0); diff --git a/STM32CubeIDE/final/Core/Src/stm32g4xx_hal_msp.c b/STM32CubeIDE/final/Core/Src/stm32g4xx_hal_msp.c index 93e572a..baf177a 100644 --- a/STM32CubeIDE/final/Core/Src/stm32g4xx_hal_msp.c +++ b/STM32CubeIDE/final/Core/Src/stm32g4xx_hal_msp.c @@ -635,7 +635,7 @@ void HAL_UART_MspInit(UART_HandleTypeDef* huart) __HAL_LINKDMA(huart,hdmatx,hdma_lpuart1_tx); /* LPUART1 interrupt Init */ - HAL_NVIC_SetPriority(LPUART1_IRQn, 3, 0); + HAL_NVIC_SetPriority(LPUART1_IRQn, 6, 0); HAL_NVIC_EnableIRQ(LPUART1_IRQn); /* USER CODE BEGIN LPUART1_MspInit 1 */ diff --git a/STM32CubeIDE/final/Drivers/CMSIS/Device/ST/STM32G4xx/LICENSE.txt b/STM32CubeIDE/final/Drivers/CMSIS/Device/ST/STM32G4xx/LICENSE.txt index 872e82b..5306686 100644 --- a/STM32CubeIDE/final/Drivers/CMSIS/Device/ST/STM32G4xx/LICENSE.txt +++ b/STM32CubeIDE/final/Drivers/CMSIS/Device/ST/STM32G4xx/LICENSE.txt @@ -1,6 +1,6 @@ -This software component is provided to you as part of a software package and -applicable license terms are in the Package_license file. If you received this -software component outside of a package or without applicable license terms, -the terms of the Apache-2.0 license shall apply. -You may obtain a copy of the Apache-2.0 at: -https://opensource.org/licenses/Apache-2.0 +This software component is provided to you as part of a software package and +applicable license terms are in the Package_license file. If you received this +software component outside of a package or without applicable license terms, +the terms of the Apache-2.0 license shall apply. +You may obtain a copy of the Apache-2.0 at: +https://opensource.org/licenses/Apache-2.0 diff --git a/STM32CubeIDE/final/Drivers/STM32G4xx_HAL_Driver/LICENSE.txt b/STM32CubeIDE/final/Drivers/STM32G4xx_HAL_Driver/LICENSE.txt index 3edc4d1..b40364c 100644 --- a/STM32CubeIDE/final/Drivers/STM32G4xx_HAL_Driver/LICENSE.txt +++ b/STM32CubeIDE/final/Drivers/STM32G4xx_HAL_Driver/LICENSE.txt @@ -1,6 +1,6 @@ -This software component is provided to you as part of a software package and -applicable license terms are in the Package_license file. If you received this -software component outside of a package or without applicable license terms, -the terms of the BSD-3-Clause license shall apply. -You may obtain a copy of the BSD-3-Clause at: -https://opensource.org/licenses/BSD-3-Clause +This software component is provided to you as part of a software package and +applicable license terms are in the Package_license file. If you received this +software component outside of a package or without applicable license terms, +the terms of the BSD-3-Clause license shall apply. +You may obtain a copy of the BSD-3-Clause at: +https://opensource.org/licenses/BSD-3-Clause diff --git a/STM32CubeIDE/final/final.ioc b/STM32CubeIDE/final/final.ioc index 4833967..16c2416 100644 --- a/STM32CubeIDE/final/final.ioc +++ b/STM32CubeIDE/final/final.ioc @@ -113,7 +113,7 @@ Dma.UCPD1_TX.0.SyncRequestNumber=1 Dma.UCPD1_TX.0.SyncSignalID=NONE FREERTOS.IPParameters=Tasks01,configTOTAL_HEAP_SIZE,configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY FREERTOS.Tasks01=defaultTask,0,128,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL -FREERTOS.configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY=3 +FREERTOS.configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY=5 FREERTOS.configTOTAL_HEAP_SIZE=7000 File.Version=6 GPIO.groupedBy=Group By Peripherals @@ -210,15 +210,15 @@ Mcu.UserName=STM32G474RETx MxCube.Version=6.16.1 MxDb.Version=DB.6.0.161 NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false -NVIC.DMA1_Channel2_IRQn=true\:3\:0\:false\:false\:true\:true\:false\:true\:true +NVIC.DMA1_Channel2_IRQn=true\:5\:0\:false\:false\:true\:true\:false\:true\:true NVIC.DMA1_Channel3_IRQn=true\:3\:0\:false\:false\:true\:false\:false\:true\:true -NVIC.DMA1_Channel4_IRQn=true\:3\:0\:true\:false\:true\:true\:false\:true\:true +NVIC.DMA1_Channel4_IRQn=true\:5\:0\:true\:false\:true\:true\:false\:true\:true NVIC.DMA1_Channel5_IRQn=true\:3\:0\:false\:false\:true\:false\:false\:true\:true NVIC.DMA1_Channel6_IRQn=true\:3\:0\:false\:false\:true\:false\:false\:true\:true NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.ForceEnableDMAVector=true NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false -NVIC.LPUART1_IRQn=true\:3\:0\:false\:false\:true\:true\:true\:true\:true +NVIC.LPUART1_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true NVIC.MemoryManagement_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false NVIC.PendSV_IRQn=true\:15\:0\:false\:false\:false\:true\:false\:false\:false @@ -228,7 +228,7 @@ NVIC.SavedPendsvIrqHandlerGenerated=true NVIC.SavedSvcallIrqHandlerGenerated=true NVIC.SavedSystickIrqHandlerGenerated=true NVIC.SysTick_IRQn=true\:15\:0\:false\:false\:true\:true\:false\:true\:false -NVIC.UCPD1_IRQn=true\:3\:0\:false\:false\:true\:true\:true\:false\:true +NVIC.UCPD1_IRQn=true\:5\:0\:false\:false\:true\:true\:true\:false\:true NVIC.UsageFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false PA0.GPIOParameters=GPIO_Label PA0.GPIO_Label=VS_MON diff --git a/STM32CubeIDE/final/final.pdf b/STM32CubeIDE/final/final.pdf new file mode 100644 index 0000000..83b644c Binary files /dev/null and b/STM32CubeIDE/final/final.pdf differ diff --git a/STM32CubeIDE/final/final.txt b/STM32CubeIDE/final/final.txt new file mode 100644 index 0000000..a0a7664 --- /dev/null +++ b/STM32CubeIDE/final/final.txt @@ -0,0 +1,150 @@ +Configuration final +STM32CubeMX 6.16.1 +Date 03/11/2026 +MCU STM32G474RETx + + + +PERIPHERALS MODES FUNCTIONS PINS +ADC1 IN1 Single-ended ADC1_IN1 PA0 +ADC1 IN2 Single-ended ADC1_IN2 PA1 +ADC1 IN7 Single-ended ADC1_IN7 PC1 +ADC1 IN15 Single-ended ADC1_IN15 PB0 +ADC2 IN17 Single-ended ADC2_IN17 PA4 +COMP1 INP COMP1_INP PA1 +COMP1 DAC3 OUT1 COMP1_VS_DAC3OUT1 VP_COMP1_VS_DAC3OUT1 +COMP1 External Output COMP1_OUT PA6 +DAC3 OUT1 Connected to on chip-peripherals only DAC3_VS_DACI1 VP_DAC3_VS_DACI1 +DAC3 OUT2 Connected to on chip-peripherals only DAC3_VS_DACI2 VP_DAC3_VS_DACI2 +HRTIM1 Master_Timer HRTIM1_VS_hrtimMasterTimerNoOutput VP_HRTIM1_VS_hrtimMasterTimerNoOutput +HRTIM1 TA1 and TA2 outputs active HRTIM1_CHA1 PA8 +HRTIM1 TA1 and TA2 outputs active HRTIM1_CHA2 PA9 +HRTIM1 TB1 and TB2 outputs active HRTIM1_CHB1 PA10 +HRTIM1 TB1 and TB2 outputs active HRTIM1_CHB2 PA11 +HRTIM1:EXTERNAL FAULT INPUT LINES Fault_Enable1 HRTIM1_FLT1 PA12 +HRTIM1:EXTERNAL FAULT INPUT LINES Fault_Enable2 HRTIM1_FLT2 PA15 +I2C1 I2C I2C1_SCL PB8-BOOT0 +I2C1 I2C I2C1_SDA PB9 +LPUART1 Asynchronous LPUART1_RX PA3 +LPUART1 Asynchronous LPUART1_TX PA2 +SYS SysTick SYS_VS_Systick VP_SYS_VS_Systick +SYS Dead Battery Signals disabled SYS_VS_DBSignals VP_SYS_VS_DBSignals +UCPD1 Source UCPD1_CC1 PB6 +UCPD1 Source UCPD1_CC2 PB4 + + + +Pin Nb PINs FUNCTIONs LABELs +3 PC14-OSC32_IN* RCC_OSC32_IN RCC_OSC32_IN +4 PC15-OSC32_OUT* RCC_OSC32_OUT RCC_OSC32_OUT +5 PF0-OSC_IN* RCC_OSC_IN RCC_OSC_IN +6 PF1-OSC_OUT* RCC_OSC_OUT RCC_OSC_OUT +9 PC1 ADC1_IN7 ID_MON +12 PA0 ADC1_IN1 VS_MON +13 PA1 COMP1_INP ADC1_IN2 IL_MON +14 PA2 LPUART1_TX +17 PA3 LPUART1_RX +18 PA4 ADC2_IN17 VS_MON +20 PA6 COMP1_OUT +24 PB0 ADC1_IN15 IS_MON +39 PC7 GPIO_Output INPUT_EN +40 PC8 GPIO_Output OUTPUT_EN +41 PC9 GPIO_Output OUTPUT_DIS +42 PA8 HRTIM1_CHA1 PHASE_1_P +43 PA9 HRTIM1_CHA2 PHASE_1_N +44 PA10 HRTIM1_CHB1 PHASE_2_P +45 PA11 HRTIM1_CHB2 PHASE_2_N +46 PA12 HRTIM1_FLT1 VS_GOOD +49 PA13* SYS_JTMS-SWDIO T_SWDIO +50 PA14* SYS_JTCK-SWCLK T_SWCLK +51 PA15 HRTIM1_FLT2 IS_GOOD +56 PB3* SYS_JTDO-SWO T_SWO +57 PB4 UCPD1_CC2 +59 PB6 UCPD1_CC1 +61 PB8-BOOT0 I2C1_SCL +62 PB9 I2C1_SDA +PERIPHERALS MODES FUNCTIONS PINS +ADC1 IN1 Single-ended ADC1_IN1 PA0 +ADC1 IN2 Single-ended ADC1_IN2 PA1 +ADC1 IN7 Single-ended ADC1_IN7 PC1 +ADC1 IN15 Single-ended ADC1_IN15 PB0 +ADC2 IN17 Single-ended ADC2_IN17 PA4 +COMP1 INP COMP1_INP PA1 +COMP1 DAC3 OUT1 COMP1_VS_DAC3OUT1 VP_COMP1_VS_DAC3OUT1 +COMP1 External Output COMP1_OUT PA6 +DAC3 OUT1 Connected to on chip-peripherals only DAC3_VS_DACI1 VP_DAC3_VS_DACI1 +DAC3 OUT2 Connected to on chip-peripherals only DAC3_VS_DACI2 VP_DAC3_VS_DACI2 +HRTIM1 Master_Timer HRTIM1_VS_hrtimMasterTimerNoOutput VP_HRTIM1_VS_hrtimMasterTimerNoOutput +HRTIM1 TA1 and TA2 outputs active HRTIM1_CHA1 PA8 +HRTIM1 TA1 and TA2 outputs active HRTIM1_CHA2 PA9 +HRTIM1 TB1 and TB2 outputs active HRTIM1_CHB1 PA10 +HRTIM1 TB1 and TB2 outputs active HRTIM1_CHB2 PA11 +HRTIM1:EXTERNAL FAULT INPUT LINES Fault_Enable1 HRTIM1_FLT1 PA12 +HRTIM1:EXTERNAL FAULT INPUT LINES Fault_Enable2 HRTIM1_FLT2 PA15 +I2C1 I2C I2C1_SCL PB8-BOOT0 +I2C1 I2C I2C1_SDA PB9 +LPUART1 Asynchronous LPUART1_RX PA3 +LPUART1 Asynchronous LPUART1_TX PA2 +SYS SysTick SYS_VS_Systick VP_SYS_VS_Systick +SYS Dead Battery Signals disabled SYS_VS_DBSignals VP_SYS_VS_DBSignals +UCPD1 Source UCPD1_CC1 PB6 +UCPD1 Source UCPD1_CC2 PB4 + + + +Pin Nb PINs FUNCTIONs LABELs +3 PC14-OSC32_IN* RCC_OSC32_IN RCC_OSC32_IN +4 PC15-OSC32_OUT* RCC_OSC32_OUT RCC_OSC32_OUT +5 PF0-OSC_IN* RCC_OSC_IN RCC_OSC_IN +6 PF1-OSC_OUT* RCC_OSC_OUT RCC_OSC_OUT +9 PC1 ADC1_IN7 ID_MON +12 PA0 ADC1_IN1 VS_MON +13 PA1 COMP1_INP ADC1_IN2 IL_MON +14 PA2 LPUART1_TX +17 PA3 LPUART1_RX +18 PA4 ADC2_IN17 VS_MON +20 PA6 COMP1_OUT +24 PB0 ADC1_IN15 IS_MON +39 PC7 GPIO_Output INPUT_EN +40 PC8 GPIO_Output OUTPUT_EN +41 PC9 GPIO_Output OUTPUT_DIS +42 PA8 HRTIM1_CHA1 PHASE_1_P +43 PA9 HRTIM1_CHA2 PHASE_1_N +44 PA10 HRTIM1_CHB1 PHASE_2_P +45 PA11 HRTIM1_CHB2 PHASE_2_N +46 PA12 HRTIM1_FLT1 VS_GOOD +49 PA13* SYS_JTMS-SWDIO T_SWDIO +50 PA14* SYS_JTCK-SWCLK T_SWCLK +51 PA15 HRTIM1_FLT2 IS_GOOD +56 PB3* SYS_JTDO-SWO T_SWO +57 PB4 UCPD1_CC2 +59 PB6 UCPD1_CC1 +61 PB8-BOOT0 I2C1_SCL +62 PB9 I2C1_SDA + + + +SOFTWARE PROJECT + +Project Settings : +Project Name : final +Project Folder : /home/daniel/GitHub/PD_Charger/STM32CubeIDE/final +Toolchain / IDE : STM32CubeIDE +Firmware Package Name and Version : STM32Cube FW_G4 V1.6.1 + + +Code Generation Settings : +STM32Cube MCU packages and embedded software packs : Copy only the necessary library files +Generate peripheral initialization as a pair of '.c/.h' files per peripheral : No +Backup previously generated files when re-generating : No +Delete previously generated files when not re-generated : Yes +Set all free pins as analog (to optimize the power consumption) : No + + +Toolchains Settings : +Compiler Optimizations : + + + + + diff --git a/plans/NLSpec-Spec/.gitignore b/plans/NLSpec-Spec/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/plans/NLSpec-Spec/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/plans/NLSpec-Spec/LICENSE b/plans/NLSpec-Spec/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/plans/NLSpec-Spec/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plans/NLSpec-Spec/Prior Art/StrongDM - attractor-spec.md b/plans/NLSpec-Spec/Prior Art/StrongDM - attractor-spec.md new file mode 100644 index 0000000..0846d86 --- /dev/null +++ b/plans/NLSpec-Spec/Prior Art/StrongDM - attractor-spec.md @@ -0,0 +1,2083 @@ +# Attractor Specification + +A DOT-based pipeline runner that uses directed graphs (defined in Graphviz DOT syntax) to orchestrate multi-stage AI workflows. Each node in the graph is an AI task (LLM call, human review, conditional branch, parallel fan-out, etc.) and edges define the flow between them. + +--- + +## Table of Contents + +1. [Overview and Goals](#1-overview-and-goals) +2. [DOT DSL Schema](#2-dot-dsl-schema) +3. [Pipeline Execution Engine](#3-pipeline-execution-engine) +4. [Node Handlers](#4-node-handlers) +5. [State and Context](#5-state-and-context) +6. [Human-in-the-Loop (Interviewer Pattern)](#6-human-in-the-loop-interviewer-pattern) +7. [Validation and Linting](#7-validation-and-linting) +8. [Model Stylesheet](#8-model-stylesheet) +9. [Transforms and Extensibility](#9-transforms-and-extensibility) +10. [Condition Expression Language](#10-condition-expression-language) +11. [Definition of Done](#11-definition-of-done) + +--- + +## 1. Overview and Goals + +### 1.1 Problem Statement + +AI-powered software workflows -- code generation, code review, testing, deployment planning -- often require multiple LLM calls chained together with conditional logic, human approvals, and parallel execution. Without a structured orchestration layer, developers either write fragile imperative scripts or build ad-hoc state machines that are difficult to visualize, version, or debug. + +Attractor solves this by letting pipeline authors define multi-stage AI workflows as directed graphs using Graphviz DOT syntax. The graph is the workflow: nodes are tasks, edges are transitions, and attributes configure behavior. The result is a declarative, visual, version-controllable pipeline definition that an execution engine can traverse deterministically. + +### 1.2 Why DOT Syntax + +DOT is chosen as the pipeline definition format for several reasons: + +- **DOT is inherently a graph description language.** Workflow pipelines are directed graphs. Using DOT means the structure (nodes and edges) maps directly to the language's primary construct, rather than being encoded in a data format like YAML or JSON that has no native concept of graphs. +- **Existing tooling.** DOT files can be rendered to SVG/PNG with standard Graphviz tooling, giving pipeline authors immediate visual feedback. Editors, linters, and parsers already exist. +- **Declarative and human-readable.** A `.dot` file is a complete, self-contained workflow definition that can be version-controlled, diffed, and reviewed in pull requests. +- **Constrained extensibility.** By restricting to a well-defined DOT subset (directed graphs only, typed attributes, no HTML labels), the DSL remains predictable while being extensible through custom attributes. + +For reference on DOT syntax, see the Graphviz DOT language specification: https://graphviz.org/doc/info/lang.html + +### 1.3 Design Principles + +**Declarative pipelines.** The `.dot` file declares what the workflow looks like and what each stage should do. The execution engine decides how and when to run each stage. Pipeline authors do not write control flow; they declare graph structure. + +**Pluggable handlers.** Each node type (LLM call, human gate, parallel fan-out) is backed by a handler that implements a common interface. New node types are added by registering new handlers. The execution engine does not know about handler internals. + +**Checkpoint and resume.** After each node completes, the execution engine saves a serializable checkpoint. If the process crashes, execution resumes from the last checkpoint. + +**Human-in-the-loop.** The pipeline can pause at designated nodes, present choices to a human operator, and route based on the human's decision. This supports approval gates, code review, and manual override -- critical for AI workflows where automated judgment may not be sufficient. + +**Edge-based routing.** Transitions between nodes are controlled by conditions, labels, and weights on edges, with runtime condition evaluation. + +### 1.4 Layering and LLM Backends + +Attractor defines the orchestration layer: graph definition, traversal, state management, and extensibility. It does NOT require any specific LLM integration. The codergen handler (Section 4.5) needs a way to call an LLM and get a response -- how you provide that is up to you. + +The codergen handler takes a backend that conforms to the `CodergenBackend` interface (Section 4.5). What that backend does internally is entirely up to the implementor -- use the companion [Coding Agent Loop](./coding-agent-loop-spec.md) and [Unified LLM Client](./unified-llm-spec.md) specs, spawn CLI agents (Claude Code, Codex, Gemini CLI) in subprocesses, run agents in tmux panes with a manager attaching to them, call an LLM API directly, or anything else. The pipeline definition (the DOT file) does not change regardless of backend choice. + +Attractor pipelines are driven by an event stream (Section 9.6). TUI, web, and IDE frontends consume events and submit human-in-the-loop answers. The pipeline engine is headless; the presentation layer is separate. + +--- + +## 2. DOT DSL Schema + +### 2.1 Supported Subset + +Attractor accepts a strict subset of the Graphviz DOT language. The restrictions exist for predictability: one graph per file, directed edges only, no HTML labels, and typed attributes with defaults. + +### 2.2 BNF-Style Grammar + +``` +Graph ::= 'digraph' Identifier '{' Statement* '}' + +Statement ::= GraphAttrStmt + | NodeDefaults + | EdgeDefaults + | SubgraphStmt + | NodeStmt + | EdgeStmt + | GraphAttrDecl + +GraphAttrStmt ::= 'graph' AttrBlock ';'? +NodeDefaults ::= 'node' AttrBlock ';'? +EdgeDefaults ::= 'edge' AttrBlock ';'? +GraphAttrDecl ::= Identifier '=' Value ';'? + +SubgraphStmt ::= 'subgraph' Identifier? '{' Statement* '}' + +NodeStmt ::= Identifier AttrBlock? ';'? +EdgeStmt ::= Identifier ( '->' Identifier )+ AttrBlock? ';'? + +AttrBlock ::= '[' Attr ( ',' Attr )* ']' +Attr ::= Key '=' Value + +Key ::= Identifier | QualifiedId +QualifiedId ::= Identifier ( '.' Identifier )+ + +Value ::= String | Integer | Float | Boolean | Duration +Identifier ::= [A-Za-z_][A-Za-z0-9_]* +String ::= '"' ( '\\"' | '\\n' | '\\t' | '\\\\' | [^"\\] )* '"' +Integer ::= '-'? [0-9]+ +Float ::= '-'? [0-9]* '.' [0-9]+ +Boolean ::= 'true' | 'false' +Duration ::= Integer ( 'ms' | 's' | 'm' | 'h' | 'd' ) + +Direction ::= 'TB' | 'LR' | 'BT' | 'RL' +``` + +### 2.3 Key Constraints + +- **One digraph per file.** Multiple graphs, undirected graphs, and `strict` modifiers are rejected. +- **Bare identifiers for node IDs.** Node IDs must match `[A-Za-z_][A-Za-z0-9_]*`. Human-readable names go in the `label` attribute. +- **Commas required between attributes.** Inside attribute blocks, commas separate key-value pairs for unambiguous parsing. +- **Directed edges only.** `->` is the only edge operator. `--` (undirected) is rejected. +- **Comments supported.** Both `// line` and `/* block */` comments are stripped before parsing. +- **Semicolons optional.** Statement-terminating semicolons are accepted but not required. + +### 2.4 Value Types + +| Type | Syntax | Examples | +|----------|---------------------------------|--------------------------------------| +| String | Double-quoted with escapes | `"Hello world"`, `"line1\nline2"` | +| Integer | Optional sign, digits | `42`, `-1`, `0` | +| Float | Decimal number | `0.5`, `-3.14` | +| Boolean | Literal keywords | `true`, `false` | +| Duration | Integer + unit suffix | `900s`, `15m`, `2h`, `250ms`, `1d` | + +### 2.5 Graph-Level Attributes + +Graph attributes are declared in a `graph [ ... ]` block or as top-level `key = value` declarations. They configure the entire workflow. + +| Key | Type | Default | Description | +|---------------------------|----------|-----------|-------------| +| `goal` | String | `""` | Human-readable goal for the pipeline. Exposed as `$goal` in prompt templates and mirrored into the run context as `graph.goal`. | +| `label` | String | `""` | Display name for the graph (used in visualization). | +| `model_stylesheet` | String | `""` | CSS-like stylesheet for per-node LLM model/provider defaults. See Section 8. | +| `default_max_retry` | Integer | `50` | Global retry ceiling for nodes that omit `max_retries`. | +| `retry_target` | String | `""` | Node ID to jump to if exit is reached with unsatisfied goal gates. | +| `fallback_retry_target` | String | `""` | Secondary jump target if `retry_target` is missing or invalid. | +| `default_fidelity` | String | `""` | Default context fidelity mode (see Section 5.4). | + +### 2.6 Node Attributes + +| Key | Type | Default | Description | +|---------------------|----------|-----------------|-------------| +| `label` | String | node ID | Display name shown in UI, prompts, and telemetry. | +| `shape` | String | `"box"` | Graphviz shape. Determines the default handler type (see mapping table below). | +| `type` | String | `""` | Explicit handler type override. Takes precedence over shape-based resolution. | +| `prompt` | String | `""` | Primary instruction for the stage. Supports `$goal` variable expansion. Falls back to `label` if empty for LLM stages. | +| `max_retries` | Integer | `0` | Number of additional attempts beyond the initial execution. `max_retries=3` means up to 4 total executions. | +| `goal_gate` | Boolean | `false` | If `true`, this node must reach SUCCESS before the pipeline can exit. | +| `retry_target` | String | `""` | Node ID to jump to if this node fails and retries are exhausted. | +| `fallback_retry_target` | String | `""` | Secondary retry target. | +| `fidelity` | String | inherited | Context fidelity mode for this node's LLM session. See Section 5.4. | +| `thread_id` | String | derived | Explicit thread identifier for LLM session reuse under `full` fidelity. | +| `class` | String | `""` | Comma-separated class names for model stylesheet targeting. | +| `timeout` | Duration | unset | Maximum execution time for this node. | +| `llm_model` | String | inherited | LLM model identifier. Overridable by stylesheet. | +| `llm_provider` | String | auto-detected | LLM provider key. Auto-detected from model if unset. | +| `reasoning_effort` | String | `"high"` | LLM reasoning effort: `low`, `medium`, `high`. | +| `auto_status` | Boolean | `false` | If `true` and the handler writes no status, the engine auto-generates a SUCCESS outcome. | +| `allow_partial` | Boolean | `false` | Accept PARTIAL_SUCCESS when retries are exhausted instead of failing. | + +### 2.7 Edge Attributes + +| Key | Type | Default | Description | +|--------------|----------|---------|-------------| +| `label` | String | `""` | Human-facing caption and routing key. Used for preferred-label matching in edge selection. | +| `condition` | String | `""` | Boolean guard expression evaluated against the current context and outcome. See Section 10. | +| `weight` | Integer | `0` | Numeric priority for edge selection. Higher weight wins among equally eligible edges. | +| `fidelity` | String | unset | Override fidelity mode for the target node. Highest precedence in fidelity resolution. | +| `thread_id` | String | unset | Override thread ID for session reuse at the target node. | +| `loop_restart` | Boolean | `false` | When `true`, terminates the current run and re-launches with a fresh log directory. | + +### 2.8 Shape-to-Handler-Type Mapping + +The `shape` attribute on a node determines which handler executes it, unless overridden by an explicit `type` attribute. This table defines the canonical mapping: + +| Shape | Handler Type | Description | +|-------------------|-----------------------|-------------| +| `Mdiamond` | `start` | Pipeline entry point. No-op handler. Every graph must have exactly one. | +| `Msquare` | `exit` | Pipeline exit point. No-op handler. Every graph must have exactly one. | +| `box` | `codergen` | LLM task (code generation, analysis, planning). The default for all nodes without an explicit shape. | +| `hexagon` | `wait.human` | Human-in-the-loop gate. Blocks until a human selects an option. | +| `diamond` | `conditional` | Conditional routing point. Routes based on edge conditions against current context. | +| `component` | `parallel` | Parallel fan-out. Executes multiple branches concurrently. | +| `tripleoctagon` | `parallel.fan_in` | Parallel fan-in. Waits for all branches and consolidates results. | +| `parallelogram` | `tool` | External tool execution (shell command, API call). | +| `house` | `stack.manager_loop` | Supervisor loop. Orchestrates observe/steer/wait cycles over a child pipeline. | + +### 2.9 Chained Edges + +Chained edge declarations are syntactic sugar. The statement: + +``` +A -> B -> C [label="next"] +``` + +expands to two edges: + +``` +A -> B [label="next"] +B -> C [label="next"] +``` + +Edge attributes in a chained declaration apply to all edges in the chain. + +### 2.10 Subgraphs + +Subgraphs serve two purposes: **scoping defaults** and **deriving classes** for the model stylesheet. + +**Scoping defaults:** Attributes declared in a subgraph's `node [ ... ]` block apply to nodes within that subgraph unless the node explicitly overrides them. + +``` +subgraph cluster_loop { + label = "Loop A" + node [thread_id="loop-a", timeout="900s"] + + Plan [label="Plan next step"] + Implement [label="Implement", timeout="1800s"] +} +``` + +Here `Plan` inherits `thread_id="loop-a"` and `timeout="900s"`, while `Implement` inherits `thread_id` but overrides `timeout`. + +**Class derivation:** Subgraph labels can produce CSS-like classes for model stylesheet matching. Nodes inside a subgraph receive the derived class. The class name is derived by lowercasing the label, replacing spaces with hyphens, and stripping non-alphanumeric characters (except hyphens). For example, `label="Loop A"` yields class `loop-a`. + +### 2.11 Node and Edge Default Blocks + +Default blocks set baseline attributes for all subsequent nodes or edges within their scope: + +``` +node [shape=box, timeout="900s"] +edge [weight=0] +``` + +Explicit attributes on individual nodes or edges override these defaults. + +### 2.12 Class Attribute + +The `class` attribute assigns one or more CSS-like class names to a node for model stylesheet targeting: + +``` +review_code [shape=box, class="code,critical", prompt="Review the code"] +``` + +Classes are comma-separated. They can be referenced in the model stylesheet with dot-prefix selectors (`.code`, `.critical`). + +### 2.13 Minimal Examples + +**Simple linear workflow:** + +``` +digraph Simple { + graph [goal="Run tests and report"] + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + run_tests [label="Run Tests", prompt="Run the test suite and report results"] + report [label="Report", prompt="Summarize the test results"] + + start -> run_tests -> report -> exit +} +``` + +**Branching workflow with conditions:** + +``` +digraph Branch { + graph [goal="Implement and validate a feature"] + rankdir=LR + node [shape=box, timeout="900s"] + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + plan [label="Plan", prompt="Plan the implementation"] + implement [label="Implement", prompt="Implement the plan"] + validate [label="Validate", prompt="Run tests"] + gate [shape=diamond, label="Tests passing?"] + + start -> plan -> implement -> validate -> gate + gate -> exit [label="Yes", condition="outcome=success"] + gate -> implement [label="No", condition="outcome!=success"] +} +``` + +**Human gate:** + +``` +digraph Review { + rankdir=LR + + start [shape=Mdiamond, label="Start"] + exit [shape=Msquare, label="Exit"] + + review_gate [ + shape=hexagon, + label="Review Changes", + type="wait.human" + ] + + start -> review_gate + review_gate -> ship_it [label="[A] Approve"] + review_gate -> fixes [label="[F] Fix"] + ship_it -> exit + fixes -> review_gate +} +``` + +--- + +## 3. Pipeline Execution Engine + +### 3.1 Run Lifecycle + +The execution lifecycle proceeds through five phases: + +``` +PARSE -> VALIDATE -> INITIALIZE -> EXECUTE -> FINALIZE +``` + +1. **Parse:** Read the `.dot` source and produce an in-memory Graph model (nodes, edges, attributes). +2. **Validate:** Run lint rules (Section 7). Reject invalid graphs. Warn on suspicious patterns. +3. **Initialize:** Create the run directory, initial context, and checkpoint. Mirror graph attributes into the context. Apply transforms (stylesheet, variable expansion). +4. **Execute:** Traverse the graph from the start node, executing handlers and selecting edges. +5. **Finalize:** Write the final checkpoint, emit completion events, and clean up resources (close sessions, release files). + +### 3.2 Core Execution Loop + +The following pseudocode defines the execution engine's traversal algorithm. This is the heart of the system. + +``` +FUNCTION run(graph, config): + context = new Context() + mirror_graph_attributes(graph, context) + checkpoint = new Checkpoint() + completed_nodes = [] + node_outcomes = {} + + current_node = find_start_node(graph) + -- Resolves by: (1) shape=Mdiamond, (2) id="start" or "Start" + -- Raises error if not found + + WHILE true: + node = graph.nodes[current_node.id] + + -- Step 1: Check for terminal node + IF is_terminal(node): + gate_ok, failed_gate = check_goal_gates(graph, node_outcomes) + IF NOT gate_ok AND failed_gate exists: + retry_target = get_retry_target(failed_gate, graph) + IF retry_target exists: + current_node = graph.nodes[retry_target] + CONTINUE + ELSE: + RAISE "Goal gate unsatisfied and no retry target" + BREAK -- Exit the loop; pipeline complete + + -- Step 2: Execute node handler with retry policy + retry_policy = build_retry_policy(node, graph) + outcome = execute_with_retry(node, context, graph, retry_policy) + + -- Step 3: Record completion + completed_nodes.append(node.id) + node_outcomes[node.id] = outcome + + -- Step 4: Apply context updates from outcome + FOR EACH (key, value) IN outcome.context_updates: + context.set(key, value) + context.set("outcome", outcome.status) + IF outcome.preferred_label is not empty: + context.set("preferred_label", outcome.preferred_label) + + -- Step 5: Save checkpoint + checkpoint = create_checkpoint(context, current_node.id, completed_nodes) + save_checkpoint(checkpoint, logs_root) + + -- Step 6: Select next edge + next_edge = select_edge(node, outcome, context, graph) + IF next_edge is NONE: + IF outcome.status == FAIL: + RAISE "Stage failed with no outgoing fail edge" + BREAK + + -- Step 7: Handle loop_restart + IF next_edge has loop_restart=true: + restart_run(graph, config, start_at=next_edge.target) + RETURN + + -- Step 8: Advance to next node + current_node = graph.nodes[next_edge.to_node] + + RETURN last_outcome +``` + +### 3.3 Edge Selection Algorithm + +After a node completes, the engine selects the next edge from the node's outgoing edges. The selection is deterministic and follows a five-step priority order: + +**Step 1: Condition-matching edges.** Evaluate each edge's `condition` expression (see Section 10) against the current context and outcome. Edges whose condition evaluates to `true` are eligible. Edges with no condition are not considered in this step; they proceed to later steps. + +**Step 2: Preferred label match.** If the node's outcome includes a `preferred_label`, find the first eligible edge (condition-passing or unconditional) whose `label` matches after normalization. Label normalization: lowercase, trim whitespace, strip accelerator prefixes (patterns like `[Y] `, `Y) `, `Y - `). + +**Step 3: Suggested next IDs.** If no label match and the outcome includes `suggested_next_ids`, find the first eligible edge whose target node ID appears in the list. + +**Step 4: Highest weight.** Among remaining eligible unconditional edges, choose the one with the highest `weight` attribute (default 0). + +**Step 5: Lexical tiebreak.** If weights are equal, choose the edge whose target node ID comes first lexicographically. + +``` +FUNCTION select_edge(node, outcome, context, graph): + edges = graph.outgoing_edges(node.id) + IF edges is empty: + RETURN NONE + + -- Step 1: Condition matching + condition_matched = [] + FOR EACH edge IN edges: + IF edge.condition is not empty: + IF evaluate_condition(edge.condition, outcome, context) == true: + condition_matched.append(edge) + IF condition_matched is not empty: + RETURN best_by_weight_then_lexical(condition_matched) + + -- Step 2: Preferred label + IF outcome.preferred_label is not empty: + FOR EACH edge IN edges: + IF normalize_label(edge.label) == normalize_label(outcome.preferred_label): + RETURN edge + + -- Step 3: Suggested next IDs + IF outcome.suggested_next_ids is not empty: + FOR EACH suggested_id IN outcome.suggested_next_ids: + FOR EACH edge IN edges: + IF edge.to_node == suggested_id: + RETURN edge + + -- Step 4 & 5: Weight with lexical tiebreak (unconditional edges only) + unconditional = [e FOR e IN edges WHERE e.condition is empty] + IF unconditional is not empty: + RETURN best_by_weight_then_lexical(unconditional) + + -- Fallback: any edge + RETURN best_by_weight_then_lexical(edges) + + +FUNCTION best_by_weight_then_lexical(edges): + SORT edges BY (weight DESCENDING, to_node ASCENDING) + RETURN edges[0] +``` + +### 3.4 Goal Gate Enforcement + +Nodes with `goal_gate=true` represent critical stages that must succeed before the pipeline can exit. When the traversal reaches a terminal node (shape=Msquare): + +1. Check all visited nodes that have `goal_gate=true`. +2. If any goal gate node has a non-success outcome (not SUCCESS or PARTIAL_SUCCESS), the pipeline cannot exit. +3. Instead, jump to the `retry_target` of the unsatisfied goal gate node. If that is not set, try `fallback_retry_target`. If that is also not set, try the graph-level `retry_target` and `fallback_retry_target`. +4. If no retry target exists at any level, the pipeline fails with an error. + +``` +FUNCTION check_goal_gates(graph, node_outcomes): + FOR EACH (node_id, outcome) IN node_outcomes: + node = graph.nodes[node_id] + IF node.goal_gate == true: + IF outcome.status NOT IN {SUCCESS, PARTIAL_SUCCESS}: + RETURN (false, node) + RETURN (true, NONE) +``` + +### 3.5 Retry Logic + +Each node has a retry policy determined by: + +1. Node attribute `max_retries` (if set) -- number of additional attempts beyond the initial execution +2. Graph attribute `default_max_retry` (fallback) +3. Built-in default: 0 (no retries) + +The `max_retries` attribute specifies additional attempts. So `max_retries=3` means a total of 4 executions (1 initial + 3 retries). Internally this maps to `max_attempts = max_retries + 1`. + +``` +FUNCTION execute_with_retry(node, context, graph, retry_policy): + FOR attempt FROM 1 TO retry_policy.max_attempts: + TRY: + outcome = handler.execute(node, context, graph, logs_root) + CATCH exception: + IF retry_policy.should_retry(exception) AND attempt < retry_policy.max_attempts: + delay = retry_policy.backoff.delay_for_attempt(attempt) + sleep(delay) + CONTINUE + ELSE: + RETURN Outcome(status=FAIL, failure_reason=str(exception)) + + IF outcome.status IN {SUCCESS, PARTIAL_SUCCESS}: + reset_retry_counter(node.id) + RETURN outcome + + IF outcome.status == RETRY: + IF attempt < retry_policy.max_attempts: + increment_retry_counter(node.id) + delay = retry_policy.backoff.delay_for_attempt(attempt) + sleep(delay) + CONTINUE + ELSE: + IF node.allow_partial == true: + RETURN Outcome(status=PARTIAL_SUCCESS, notes="retries exhausted, partial accepted") + RETURN Outcome(status=FAIL, failure_reason="max retries exceeded") + + IF outcome.status == FAIL: + RETURN outcome + + RETURN Outcome(status=FAIL, failure_reason="max retries exceeded") +``` + +### 3.6 Retry Policy + +``` +RetryPolicy: + max_attempts : Integer -- minimum 1 (1 means no retries) + backoff : BackoffConfig -- delay calculation between retries + should_retry : Function(Error) -> Boolean -- predicate for retryable errors + +BackoffConfig: + initial_delay_ms : Integer -- first retry delay in milliseconds (default: 200) + backoff_factor : Float -- multiplier for subsequent delays (default: 2.0) + max_delay_ms : Integer -- cap on delay in milliseconds (default: 60000) + jitter : Boolean -- add random jitter to prevent thundering herd (default: true) +``` + +**Delay calculation:** + +``` +FUNCTION delay_for_attempt(attempt, config): + -- attempt is 1-indexed (first retry is attempt=1) + delay = config.initial_delay_ms * (config.backoff_factor ^ (attempt - 1)) + delay = MIN(delay, config.max_delay_ms) + IF config.jitter: + delay = delay * random_uniform(0.5, 1.5) + RETURN delay +``` + +**Preset policies:** + +| Name | Max Attempts | Initial Delay | Factor | Description | +|--------------|-------------|---------------|--------|-------------| +| `none` | 1 | -- | -- | No retries. Fail immediately on error. | +| `standard` | 5 | 200ms | 2.0 | General-purpose. Delays: 200, 400, 800, 1600, 3200ms. | +| `aggressive` | 5 | 500ms | 2.0 | For unreliable operations. Delays: 500, 1000, 2000, 4000, 8000ms. | +| `linear` | 3 | 500ms | 1.0 | Fixed delay between attempts. Delays: 500, 500, 500ms. | +| `patient` | 3 | 2000ms | 3.0 | Long-running operations. Delays: 2000, 6000, 18000ms. | + +**Default should_retry predicate:** Returns `true` for network errors, rate limit errors (HTTP 429), server errors (HTTP 5xx), and provider-reported transient failures. Returns `false` for authentication errors (HTTP 401, 403), bad request errors (HTTP 400), validation errors, and configuration errors. + +### 3.7 Failure Routing + +When a stage returns FAIL (or retries are exhausted), the engine attempts failure routing in this order: + +1. **Fail edge:** An outgoing edge with `condition="outcome=fail"`. If found, follow it. +2. **Retry target:** Node attribute `retry_target`. Jump to that node. +3. **Fallback retry target:** Node attribute `fallback_retry_target`. Jump to that node. +4. **Pipeline termination:** No failure route found. The pipeline fails with the stage's failure reason. + +### 3.8 Concurrency Model + +The graph traversal is single-threaded. Only one node executes at a time in the top-level graph. This simplifies reasoning about context state and avoids race conditions. + +Parallelism exists within specific node handlers (`parallel`, `parallel.fan_in`) that manage concurrent execution internally. Each parallel branch receives an isolated clone of the context. Branch results are collected but individual branch context changes are not merged back into the parent -- only the handler's outcome and its `context_updates` are applied. + +--- + +## 4. Node Handlers + +### 4.1 Handler Interface + +Every node handler implements a common interface. The execution engine dispatches to the appropriate handler based on the node's `type` attribute (or shape-based resolution if `type` is empty). + +``` +INTERFACE Handler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome + + -- Parameters: + -- node : The parsed Node with all its attributes + -- context : The shared key-value Context for the pipeline run (read/write) + -- graph : The full parsed Graph (for reading outgoing edges, etc.) + -- logs_root : Filesystem path for this run's log/artifact directory + + -- Returns: + -- Outcome : The result of execution (see Section 5.2) +``` + +### 4.2 Handler Registry + +The handler registry maps type strings to handler instances. Resolution follows this order: + +1. **Explicit `type` attribute** on the node (e.g., `type="wait.human"`) +2. **Shape-based resolution** using the shape-to-handler-type mapping table (Section 2.8) +3. **Default handler** (the codergen/LLM handler) + +``` +HandlerRegistry: + handlers : Map -- type string -> handler instance + default_handler : Handler -- fallback handler (typically codergen) + + FUNCTION register(type_string, handler): + handlers[type_string] = handler + -- Registering for an already-registered type replaces the previous handler + + FUNCTION resolve(node) -> Handler: + -- 1. Explicit type attribute + IF node.type is not empty AND node.type IN handlers: + RETURN handlers[node.type] + + -- 2. Shape-based resolution + handler_type = SHAPE_TO_TYPE[node.shape] + IF handler_type IN handlers: + RETURN handlers[handler_type] + + -- 3. Default + RETURN default_handler +``` + +### 4.3 Start Handler + +A no-op handler for the pipeline entry point. Returns SUCCESS immediately without performing any work. + +``` +StartHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + RETURN Outcome(status=SUCCESS) +``` + +Every graph must have exactly one start node (shape=Mdiamond). The lint rules enforce this. + +### 4.4 Exit Handler + +A no-op handler for the pipeline exit point. Returns SUCCESS immediately. Goal gate enforcement is handled by the execution engine (Section 3.4), not by this handler. + +``` +ExitHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + RETURN Outcome(status=SUCCESS) +``` + +Every graph must have exactly one exit node (shape=Msquare). + +### 4.5 Codergen Handler (LLM Task) + +The codergen handler is the default for all nodes that invoke an LLM. It reads the node's prompt, expands template variables, calls the LLM backend (see Section 1.4 for backend options), writes the prompt and response to the logs directory, and returns the outcome. + +``` +CodergenHandler: + backend : CodergenBackend | None + -- The LLM execution backend. Any implementation of the + -- CodergenBackend interface (Section 4.5). None = simulation mode. + + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + -- 1. Build prompt + prompt = node.prompt + IF prompt is empty: + prompt = node.label + prompt = expand_variables(prompt, graph, context) + + -- 2. Write prompt to logs + stage_dir = logs_root + "/" + node.id + "/" + create_directory(stage_dir) + write_file(stage_dir + "prompt.md", prompt) + + -- 3. Call LLM backend + IF backend is not NONE: + TRY: + result = backend.run(node, prompt, context) + IF result is an Outcome: + write_status(stage_dir, result) + RETURN result + response_text = string(result) + CATCH exception: + RETURN Outcome(status=FAIL, failure_reason=str(exception)) + ELSE: + response_text = "[Simulated] Response for stage: " + node.id + + -- 4. Write response to logs + write_file(stage_dir + "response.md", response_text) + + -- 5. Write status and return outcome + outcome = Outcome( + status=SUCCESS, + notes="Stage completed: " + node.id, + context_updates={ + "last_stage": node.id, + "last_response": truncate(response_text, 200) + } + ) + write_status(stage_dir, outcome) + RETURN outcome +``` + +**Variable expansion:** The only built-in template variable is `$goal`, which resolves to the graph-level `goal` attribute. Variable expansion is simple string replacement, not a templating engine. + +**Status file:** The handler writes `status.json` in the stage directory with the Outcome fields serialized as JSON. This file serves as an audit trail and enables the status-file contract: external tools or agents can write `status.json` to communicate outcomes back to the engine. + +#### CodergenBackend Interface + +``` +INTERFACE CodergenBackend: + FUNCTION run(node: Node, prompt: String, context: Context) -> String | Outcome +``` + +How you implement this interface is up to you. The pipeline engine only cares that it gets a String or Outcome back. + +### 4.6 Wait For Human Handler + +Blocks pipeline execution until a human selects an option derived from the node's outgoing edges. This implements the human-in-the-loop pattern (see Section 6 for the full Interviewer protocol). + +``` +WaitForHumanHandler: + interviewer : Interviewer -- the human interaction frontend + + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + -- 1. Derive choices from outgoing edges + edges = graph.outgoing_edges(node.id) + choices = [] + FOR EACH edge IN edges: + label = edge.label OR edge.to_node + key = parse_accelerator_key(label) + choices.append(Choice(key=key, label=label, to=edge.to_node)) + + IF choices is empty: + RETURN Outcome(status=FAIL, failure_reason="No outgoing edges for human gate") + + -- 2. Build question from choices + options = [Option(key=c.key, label=c.label) FOR c IN choices] + question = Question( + text=node.label OR "Select an option:", + type=MULTIPLE_CHOICE, + options=options, + stage=node.id + ) + + -- 3. Present to interviewer and wait for answer + answer = interviewer.ask(question) + + -- 4. Handle timeout/skip + IF answer is TIMEOUT: + default_choice = node.attrs["human.default_choice"] + IF default_choice exists: + -- Use default + ELSE: + RETURN Outcome(status=RETRY, failure_reason="human gate timeout, no default") + + IF answer is SKIPPED: + RETURN Outcome(status=FAIL, failure_reason="human skipped interaction") + + -- 5. Find matching choice + selected = find_choice_matching(answer, choices) + IF selected is NONE: + selected = choices[0] -- fallback to first + + -- 6. Record in context and return + RETURN Outcome( + status=SUCCESS, + suggested_next_ids=[selected.to], + context_updates={ + "human.gate.selected": selected.key, + "human.gate.label": selected.label + } + ) +``` + +**Accelerator key parsing** extracts shortcut keys from edge labels using these patterns: + +| Pattern | Example | Extracted Key | +|-------------------|-------------------|---------------| +| `[K] Label` | `[Y] Yes, deploy` | `Y` | +| `K) Label` | `Y) Yes, deploy` | `Y` | +| `K - Label` | `Y - Yes, deploy` | `Y` | +| First character | `Yes, deploy` | `Y` | + +### 4.7 Conditional Handler + +For diamond-shaped nodes that act as conditional routing points. The handler itself is a no-op that returns SUCCESS; the actual routing is handled by the execution engine's edge selection algorithm (Section 3.3), which evaluates conditions on outgoing edges. + +``` +ConditionalHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + RETURN Outcome( + status=SUCCESS, + notes="Conditional node evaluated: " + node.id + ) +``` + +This design keeps routing logic in the engine (where it can be deterministic and inspectable) rather than in the handler. + +### 4.8 Parallel Handler + +Fans out execution to multiple branches concurrently. Each parallel branch receives an isolated clone of the parent context and runs independently. The handler waits for all branches to complete (or applies a configurable join policy) before returning. + +``` +ParallelHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + -- 1. Identify fan-out edges (all outgoing edges from this node) + branches = graph.outgoing_edges(node.id) + + -- 2. Determine join policy from node attributes + join_policy = node.attrs.get("join_policy", "wait_all") + error_policy = node.attrs.get("error_policy", "continue") + max_parallel = integer(node.attrs.get("max_parallel", "4")) + + -- 3. Execute branches concurrently with bounded parallelism + results = [] + FOR EACH branch IN branches (up to max_parallel at a time): + branch_context = context.clone() + branch_outcome = execute_subgraph(branch.to_node, branch_context, graph, logs_root) + results.append(branch_outcome) + + -- 4. Evaluate join policy + success_count = count(r FOR r IN results WHERE r.status == SUCCESS) + fail_count = count(r FOR r IN results WHERE r.status == FAIL) + + IF join_policy == "wait_all": + IF fail_count == 0: + RETURN Outcome(status=SUCCESS) + ELSE: + RETURN Outcome(status=PARTIAL_SUCCESS) + + IF join_policy == "first_success": + IF success_count > 0: + RETURN Outcome(status=SUCCESS) + ELSE: + RETURN Outcome(status=FAIL) + + -- 5. Store results in context for downstream fan-in + context.set("parallel.results", serialize_results(results)) + RETURN Outcome(status=SUCCESS) +``` + +**Join policies:** + +| Policy | Behavior | +|------------------|----------| +| `wait_all` | All branches must complete. Join satisfied when all are done. | +| `k_of_n` | At least K branches must succeed. | +| `first_success` | Join satisfied as soon as one branch succeeds. Others may be cancelled. | +| `quorum` | At least a configurable fraction of branches must succeed. | + +**Error policies:** + +| Policy | Behavior | +|---------------------|----------| +| `fail_fast` | Cancel all remaining branches on first failure. | +| `continue` | Continue remaining branches. Collect all results. | +| `ignore` | Ignore failures entirely. Return only successful results. | + +### 4.9 Fan-In Handler + +Consolidates results from a preceding parallel node and selects the best candidate. + +``` +FanInHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + -- 1. Read parallel results + results = context.get("parallel.results") + IF results is empty: + RETURN Outcome(status=FAIL, failure_reason="No parallel results to evaluate") + + -- 2. Evaluate candidates + IF node.prompt is not empty: + -- LLM-based evaluation: call LLM to rank candidates + best = llm_evaluate(node.prompt, results) + ELSE: + -- Heuristic: rank by outcome status, then by score + best = heuristic_select(results) + + -- 3. Record winner in context + context_updates = { + "parallel.fan_in.best_id": best.id, + "parallel.fan_in.best_outcome": best.outcome + } + + RETURN Outcome( + status=SUCCESS, + context_updates=context_updates, + notes="Selected best candidate: " + best.id + ) + + +FUNCTION heuristic_select(candidates): + outcome_rank = {SUCCESS: 0, PARTIAL_SUCCESS: 1, RETRY: 2, FAIL: 3} + SORT candidates BY (outcome_rank[c.outcome], -c.score, c.id) + RETURN candidates[0] +``` + +Fan-in runs even when some candidates failed, as long as at least one candidate is available. Only when all candidates fail does fan-in return FAIL. + +### 4.10 Tool Handler + +Executes an external tool (shell command, API call, or other non-LLM operation) configured via node attributes. + +``` +ToolHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + command = node.attrs.get("tool_command", "") + IF command is empty: + RETURN Outcome(status=FAIL, failure_reason="No tool_command specified") + + -- Execute the command + TRY: + result = run_shell_command(command, timeout=node.timeout) + RETURN Outcome( + status=SUCCESS, + context_updates={"tool.output": result.stdout}, + notes="Tool completed: " + command + ) + CATCH exception: + RETURN Outcome(status=FAIL, failure_reason=str(exception)) +``` + +### 4.11 Manager Loop Handler + +Orchestrates sprint-based iteration by supervising a child pipeline. The manager observes the child's telemetry, evaluates progress via a guard function, and optionally steers the child through intervention. + +``` +ManagerLoopHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + child_dotfile = graph.attrs.get("stack.child_dotfile") + poll_interval = parse_duration(node.attrs.get("manager.poll_interval", "45s")) + max_cycles = integer(node.attrs.get("manager.max_cycles", "1000")) + stop_condition = node.attrs.get("manager.stop_condition", "") + actions = split(node.attrs.get("manager.actions", "observe,wait"), ",") + + -- 1. Auto-start child if configured + IF node.attrs.get("stack.child_autostart", "true") == "true": + start_child_pipeline(child_dotfile) + + -- 2. Observation loop + FOR cycle FROM 1 TO max_cycles: + IF "observe" IN actions: + ingest_child_telemetry(context) + + IF "steer" IN actions AND steer_cooldown_elapsed(): + steer_child(context, node) + + -- Evaluate stop conditions + child_status = context.get_string("context.stack.child.status") + IF child_status IN {"completed", "failed"}: + child_outcome = context.get_string("context.stack.child.outcome") + IF child_outcome == "success": + RETURN Outcome(status=SUCCESS, notes="Child completed") + IF child_status == "failed": + RETURN Outcome(status=FAIL, failure_reason="Child failed") + + IF stop_condition is not empty: + IF evaluate_condition(stop_condition, ..., context): + RETURN Outcome(status=SUCCESS, notes="Stop condition satisfied") + + IF "wait" IN actions: + sleep(poll_interval) + + RETURN Outcome(status=FAIL, failure_reason="Max cycles exceeded") +``` + +The manager pattern implements a **supervisor architecture** where: +- **Observe** ingests worker telemetry (active stage, outcomes, retry counts, artifacts) +- **Guard** scores worker progress and routes to continue, intervene, or escalate +- **Steer** writes intervention instructions to the child's active stage directory + +### 4.12 Custom Handlers + +New handler types are added by implementing the Handler interface and registering with the registry: + +``` +-- Define a custom handler +MyCustomHandler: + FUNCTION execute(node, context, graph, logs_root) -> Outcome: + -- Custom logic here + RETURN Outcome(status=SUCCESS) + +-- Register it +registry.register("my_custom_type", MyCustomHandler()) + +-- Reference in DOT file +my_node [type="my_custom_type", shape=box, custom_attr="value"] +``` + +**Handler contract:** +- Handlers MUST be stateless or protect shared mutable state with synchronization. +- Handler panics/exceptions MUST be caught by the engine and converted to FAIL outcomes. +- Handlers SHOULD NOT embed provider-specific logic; LLM orchestration is delegated to the integrated SDK. + +--- + +## 5. State and Context + +### 5.1 Context + +The context is a thread-safe key-value store shared across all stages during a pipeline run. It is the primary mechanism for passing data between nodes. + +``` +Context: + values : Map -- key-value store + lock : ReadWriteLock -- thread safety for parallel access + logs : List -- append-only run log + + FUNCTION set(key, value): + ACQUIRE write lock + values[key] = value + RELEASE write lock + + FUNCTION get(key, default=NONE) -> Any: + ACQUIRE read lock + result = values.get(key, default) + RELEASE read lock + RETURN result + + FUNCTION get_string(key, default="") -> String: + value = get(key) + IF value is NONE: RETURN default + RETURN string(value) + + FUNCTION append_log(entry): + ACQUIRE write lock + logs.append(entry) + RELEASE write lock + + FUNCTION snapshot() -> Map: + -- Returns a serializable copy of all values + ACQUIRE read lock + result = shallow_copy(values) + RELEASE read lock + RETURN result + + FUNCTION clone() -> Context: + -- Deep copy for parallel branch isolation + ACQUIRE read lock + new_context = new Context() + new_context.values = shallow_copy(values) + new_context.logs = copy(logs) + RELEASE read lock + RETURN new_context + + FUNCTION apply_updates(updates): + -- Merge a dictionary of updates into the context + ACQUIRE write lock + FOR EACH (key, value) IN updates: + values[key] = value + RELEASE write lock +``` + +**Built-in context keys set by the engine:** + +| Key | Type | Set By | Description | +|---------------------------------------|---------|----------|-------------| +| `outcome` | String | Engine | Last handler outcome status (`success`, `fail`, etc.) | +| `preferred_label` | String | Engine | Last handler's preferred edge label | +| `graph.goal` | String | Engine | Mirrored from graph `goal` attribute | +| `current_node` | String | Engine | ID of the currently executing node | +| `last_stage` | String | Handler | ID of the last completed stage | +| `last_response` | String | Handler | Truncated text of the last LLM response | +| `internal.retry_count.` | Integer | Engine | Retry counter for a specific node | + +**Context key namespace conventions:** + +| Prefix | Purpose | +|---------------|------------------------------------------------| +| `context.*` | Semantic state shared between nodes | +| `graph.*` | Graph attributes mirrored at initialization | +| `internal.*` | Engine bookkeeping (retry counters, timing) | +| `parallel.*` | Parallel handler state (results, counts) | +| `stack.*` | Supervisor/worker state | +| `human.gate.*`| Human interaction state | +| `work.*` | Per-item context for parallel work items | + +### 5.2 Outcome + +The outcome is the result of executing a node handler. It drives routing decisions and state updates. + +``` +Outcome: + status : StageStatus -- SUCCESS, FAIL, PARTIAL_SUCCESS, RETRY, SKIPPED + preferred_label : String -- which edge label to follow (optional) + suggested_next_ids : List -- explicit next node IDs (optional) + context_updates : Map -- key-value pairs to merge into context + notes : String -- human-readable execution summary + failure_reason : String -- reason for failure (when status is FAIL or RETRY) +``` + +**StageStatus values:** + +| Status | Meaning | +|--------------------|---------| +| `SUCCESS` | Stage completed its work. Proceed to next edge. Reset retry counter. | +| `PARTIAL_SUCCESS` | Stage completed with caveats. Treated as success for routing but notes describe what was incomplete. | +| `RETRY` | Stage requests re-execution. Engine increments retry counter and re-executes if within limits. | +| `FAIL` | Stage failed permanently. Engine looks for a fail edge or terminates the pipeline. | +| `SKIPPED` | Stage was skipped (e.g., condition not met). Proceed without recording an outcome. | + +### 5.3 Checkpoint + +A serializable snapshot of execution state, saved after each node completes. Enables crash recovery and resume. + +``` +Checkpoint: + timestamp : Timestamp -- when this checkpoint was created + current_node : String -- ID of the last completed node + completed_nodes : List -- IDs of all completed nodes in order + node_retries : Map -- retry counters per node + context_values : Map -- serialized snapshot of the context + logs : List -- run log entries + + FUNCTION save(path): + -- Serialize to JSON and write to filesystem + data = { + "timestamp": timestamp, + "current_node": current_node, + "completed_nodes": completed_nodes, + "node_retries": node_retries, + "context": serialize_to_json(context_values), + "logs": logs + } + write_json_file(path, data) + + FUNCTION load(path) -> Checkpoint: + -- Deserialize from JSON file + data = read_json_file(path) + RETURN new Checkpoint from data +``` + +**Resume behavior:** + +1. Load the checkpoint from `{logs_root}/checkpoint.json`. +2. Restore context state from `context_values`. +3. Restore `completed_nodes` to skip already-finished work. +4. Restore retry counters from `node_retries`. +5. Determine the next node to execute (the one after `current_node` in the traversal). +6. If the previous node used `full` fidelity, degrade to `summary:high` for the first resumed node, because in-memory LLM sessions cannot be serialized. After this one degraded hop, subsequent nodes may use `full` fidelity again. + +### 5.4 Context Fidelity + +Context fidelity controls how much prior conversation and state is carried into the next node's LLM session. This is a core mechanism for managing context window usage across multi-stage pipelines. + +``` +FidelityMode ::= 'full' + | 'truncate' + | 'compact' + | 'summary:low' + | 'summary:medium' + | 'summary:high' +``` + +| Mode | Session | Context Carried | Approximate Token Budget | +|------------------|---------|---------------------------------------------------------|--------------------------| +| `full` | Reused (same thread) | Full conversation history preserved | Unbounded (uses compaction) | +| `truncate` | Fresh | Minimal: only graph goal and run ID | Minimal | +| `compact` | Fresh | Structured bullet-point summary: completed stages, outcomes, key context values | Moderate | +| `summary:low` | Fresh | Brief textual summary with minimal event counts | ~600 tokens | +| `summary:medium` | Fresh | Moderate detail: recent stage outcomes, active context values, notable events | ~1500 tokens | +| `summary:high` | Fresh | Detailed: many recent events, tool call summaries, comprehensive context | ~3000 tokens | + +**Fidelity resolution precedence (highest to lowest):** + +1. Edge `fidelity` attribute (on the incoming edge) +2. Target node `fidelity` attribute +3. Graph `default_fidelity` attribute +4. Default: `compact` + +**Thread resolution (for `full` fidelity):** + +When fidelity resolves to `full`, the engine determines a thread key for session reuse: + +1. Target node `thread_id` attribute +2. Edge `thread_id` attribute +3. Graph-level default thread +4. Derived class from enclosing subgraph +5. Fallback: previous node ID + +Nodes that share the same thread key reuse the same LLM session. Nodes with different thread keys start fresh sessions. + +### 5.5 Artifact Store + +The artifact store provides named, typed storage for large stage outputs that do not belong in the context (which should contain only small scalar values for routing and checkpoint serialization). + +``` +ArtifactStore: + artifacts : Map + lock : ReadWriteLock + base_dir : String or NONE -- filesystem directory for file-backed artifacts + + FUNCTION store(artifact_id, name, data) -> ArtifactInfo: + size = byte_size(data) + is_file_backed = (size > FILE_BACKING_THRESHOLD) AND (base_dir is not NONE) + IF is_file_backed: + write data to "{base_dir}/artifacts/{artifact_id}.json" + stored_data = file_path + ELSE: + stored_data = data + info = ArtifactInfo(id=artifact_id, name=name, size=size, is_file_backed=is_file_backed) + artifacts[artifact_id] = (info, stored_data) + RETURN info + + FUNCTION retrieve(artifact_id) -> Any: + IF artifact_id NOT IN artifacts: + RAISE "Artifact not found" + (info, data) = artifacts[artifact_id] + IF info.is_file_backed: + RETURN read_json_file(data) + RETURN data + + FUNCTION has(artifact_id) -> Boolean + FUNCTION list() -> List + FUNCTION remove(artifact_id) + FUNCTION clear() + +ArtifactInfo: + id : String + name : String + size_bytes : Integer + stored_at : Timestamp + is_file_backed : Boolean +``` + +The default file-backing threshold is 100KB. Artifacts below this threshold are stored in memory; above it, they are written to disk. + +### 5.6 Run Directory Structure + +Each pipeline execution produces a directory tree for logging, checkpoints, and artifacts: + +``` +{logs_root}/ + checkpoint.json -- Serialized checkpoint after each node + manifest.json -- Pipeline metadata (name, goal, start time) + {node_id}/ + status.json -- Node execution outcome + prompt.md -- Rendered prompt sent to LLM + response.md -- LLM response text + artifacts/ + {artifact_id}.json -- File-backed artifacts +``` + +--- + +## 6. Human-in-the-Loop (Interviewer Pattern) + +### 6.1 Interviewer Interface + +All human interaction in Attractor goes through an Interviewer interface. This abstraction allows the pipeline to present questions to a human and receive answers through any frontend: CLI, web UI, Slack bot, or a programmatic queue for testing. + +``` +INTERFACE Interviewer: + FUNCTION ask(question: Question) -> Answer + FUNCTION ask_multiple(questions: List) -> List + FUNCTION inform(message: String, stage: String) -> Void +``` + +### 6.2 Question Model + +``` +Question: + text : String -- the question to present to the human + type : QuestionType -- determines the UI and valid answers + options : List