diff --git a/Images/Gen7FW/.rte-gen7/generated/CMakeLists.txt b/Images/Gen7FW/.rte-gen7/generated/CMakeLists.txt index a6d8b299..2fb4b043 100644 --- a/Images/Gen7FW/.rte-gen7/generated/CMakeLists.txt +++ b/Images/Gen7FW/.rte-gen7/generated/CMakeLists.txt @@ -82,6 +82,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE Src/Inverter/Command/Commands/CanCommands.cpp Src/Inverter/Command/Commands/OpenLoopCommands.cpp Src/Inverter/Command/Commands/TraceCommands.cpp + Src/Inverter/Command/Commands/I2cCommands.cpp Src/Inverter/Control/ControlSupervisor.cpp Src/Inverter/Control/OpenLoopController.cpp Src/Inverter/Control/FocControlManager.cpp @@ -131,6 +132,10 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE Src/Inverter/Drivers/Sensors/ApplicationSensors.cpp Src/Inverter/Drivers/Sensors/SpikeRecorder.cpp Src/Inverter/Drivers/Sensors/SampleScheduler.cpp + Src/Inverter/Drivers/I2C/HalI2cBus.cpp + Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp + Src/Inverter/Drivers/I2C/RailMonitor.cpp + Src/Inverter/Drivers/I2C/I2cSensors.cpp Src/Inverter/Drivers/CAN/CanBus.cpp Src/Inverter/Drivers/CAN/CanProtocolTransport.cpp Src/Inverter/Drivers/CAN/CanSession.cpp diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/AppState.h b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/AppState.h index cb431bfa..1e973f4f 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/AppState.h +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/AppState.h @@ -1,8 +1,8 @@ #pragma once -#include "../../generated/domain_vsense_generated.h" #include "../../generated/domain_adc_isr_generated.h" -#include "../../generated/domain_tim_isr_generated.h" #include "../../generated/domain_app_loop_generated.h" +#include "../../generated/domain_tim_isr_generated.h" +#include "../../generated/domain_vsense_generated.h" /* ============================================================================ * RTE codegen top-level state container. @@ -53,4 +53,3 @@ struct AppState { * markers. Individual domain structs are owned by the generated code. */ extern AppState appState; - diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Control/FaultManager.h b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Control/FaultManager.h index ca1760c2..f259bcd2 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Control/FaultManager.h +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Control/FaultManager.h @@ -51,6 +51,9 @@ enum class FaultSource : uint32_t { OvertemperatureMotor = 1u << 25, /**< Motor temperature above limit */ OvertemperatureInverter = 1u << 26, /**< Board temperature above limit */ CurrentSensorRef = 1u << 27, /**< Current-sensor reference out of window */ + OnboardOvertemperature = 1u << 28, /**< I2C5 onboard temp sensor over limit */ + RailOvervoltage = 1u << 29, /**< I2C4 rail monitor bus overvoltage */ + RailUndervoltage = 1u << 30, /**< I2C4 rail monitor bus undervoltage */ }; constexpr FaultSource operator|(FaultSource a, FaultSource b) { @@ -112,6 +115,9 @@ enum class FaultReason : uint8_t { OvertemperatureInv3, OvertemperatureMotor, SensorRefOutOfRange, + OnboardOvertemperature, + RailOvervoltage, + RailUndervoltage, Count }; @@ -238,6 +244,9 @@ class FaultManager { { FaultSource::OvertemperatureMotor, "OvertemperatureMotor", "Temperature", "motor temperature above limit", FaultSeverity::Critical }, { FaultSource::OvertemperatureInverter, "OvertemperatureInverter", "Temperature", "board temperature above limit", FaultSeverity::Critical }, { FaultSource::CurrentSensorRef, "CurrentSensorRef", "Current Sense", "current-sensor reference out of window", FaultSeverity::Warning }, + { FaultSource::OnboardOvertemperature, "OnboardOvertemperature", "Temperature", "I2C5 onboard temp sensor over limit", FaultSeverity::Warning }, + { FaultSource::RailOvervoltage, "RailOvervoltage", "Rail Monitor", "I2C4 rail bus overvoltage", FaultSeverity::Warning }, + { FaultSource::RailUndervoltage, "RailUndervoltage", "Rail Monitor", "I2C4 rail bus undervoltage", FaultSeverity::Warning }, }; }; diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/HalI2cBus.h b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/HalI2cBus.h new file mode 100644 index 00000000..db9d9c7a --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/HalI2cBus.h @@ -0,0 +1,34 @@ +#pragma once + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include "i2c.h" + +namespace Inverter { + +/** + * @brief II2cBus backed by a HAL I2C handle (firmware builds only). + * + * Blocking polling-mode HAL calls with a caller-supplied timeout; no + * interrupts or DMA. Errors bump public counters for shell diagnostics. + */ +class HalI2cBus : public II2cBus { +public: + explicit HalI2cBus(I2C_HandleTypeDef* hi2c) : m_hi2c(hi2c) {} + + bool transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t timeout_ms) override; + bool isReady(uint8_t addr7, uint32_t timeout_ms) override; + + uint32_t errorCount() const { return m_errors; } + uint32_t timeoutCount() const { return m_timeouts; } + +private: + I2C_HandleTypeDef* m_hi2c; + uint32_t m_errors = 0; + uint32_t m_timeouts = 0; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/I2cBus.h b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/I2cBus.h new file mode 100644 index 00000000..8f4eb46b --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/I2cBus.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +namespace Inverter { + +/** + * @brief Abstract I2C master port — the single funnel for all I2C driver I/O. + * + * Every Gen7 I2C driver (on-board temp sensor, rail monitor) talks to the bus + * exclusively through this interface, so the drivers and their register + * decode/encode math can be compiled and unit-tested on the host against a + * mock bus (see tests/host/), with no HAL dependency. + * + * Transaction model matches the register-pointer devices we drive + * (TMP102-family, INA226/INA228/INA3221): + * transfer(addr, tx={reg[, data...]}, rx=n) == + * START, addr+W, tx bytes, [repeated START, addr+R, rx bytes], STOP + * A null/empty @p rx reads nothing (pure register write); @p tx always starts + * with the register pointer byte. + * + * All calls are blocking-with-timeout and must only be made from main-loop / + * shell context — never from an ISR. + */ +class II2cBus { +public: + virtual ~II2cBus() = default; + + /** + * @brief Register-pointer transaction (see class comment). + * @param addr7 7-bit slave address (0x08..0x77). + * @param tx Bytes to write; tx[0] is the register pointer. + * @param tx_len Number of bytes in @p tx (>= 1). + * @param rx Receive buffer; may be nullptr when rx_len == 0. + * @param rx_len Bytes to read after a repeated START (0 = write only). + * @param timeout_ms Per-phase bus timeout. + * @return true on success; false on NACK/bus error/timeout. + */ + virtual bool transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t timeout_ms) = 0; + + /** + * @brief Address-only presence probe (HAL_I2C_IsDeviceReady equivalent). + * @return true if the slave ACKed its address. + */ + virtual bool isReady(uint8_t addr7, uint32_t timeout_ms) = 0; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/I2cSensors.h b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/I2cSensors.h new file mode 100644 index 00000000..5db85944 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/I2cSensors.h @@ -0,0 +1,113 @@ +#pragma once + +#include "Inverter/Drivers/I2C/HalI2cBus.h" +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" +#include "Inverter/Drivers/I2C/RailMonitor.h" + +#include +#include + +namespace Inverter { + +/** + * @brief Application-level owner of the Gen7 I2C sensors. + * + * Owns the two bus objects (I2C5 = on-board temperature, I2C4 = rail + * monitor), the configurable parameters, the poll/re-probe scheduling, and + * the telemetry + warning-fault wiring. Follows the ApplicationSensors + * pattern: init() never blocks boot (short-timeout probe, one log line when + * absent), update() runs from the main loop only (never from an ISR) at + * POLL_MS cadence, and absent devices are lazily re-probed every + * REPROBE_MS. + * + * Persistent config (RteParamStore KV, `config set ` + + * `config saveall`): + * OnbTemp.Addr 7-bit temp-sensor address (default 0x48) + * OnbTemp.CritC over-temperature warning (default 85 degC) + * RailMon.Addr 7-bit rail-monitor address (default 0x40) + * RailMon.ShuntOhm shunt resistor [ohm] (default 0.005) + * RailMon.MaxA max expected current [A] (default 10) + * RailMon.OvV rail OV warning [V] (default 0 = disabled) + * RailMon.UvV rail UV warning [V] (default 0 = disabled) + * + * Telemetry keys: onb_temp_c, rail_v, rail_a, rail_w (plus rail2_v/rail2_a + * and rail3_v/rail3_a for the INA3221's extra channels). + * + * Warning faults (latched, log-only): OnboardOvertemperature, + * RailOvervoltage, RailUndervoltage. + */ +class I2cSensors { +public: + bool init(); + void update(); + void reloadConfig(); + + /* Status for the shell commands. */ + bool tempPresent() const { return m_temp.part() != OnboardTempSensor::Part::None; } + float lastTempC() const { return m_temp_c; } + float tempCritC() const { return m_temp_crit_c; } + const OnboardTempSensor& tempSensor() const { return m_temp; } + + bool railPresent() const { return m_rail.part() != RailMonitor::Part::None; } + const RailMonitor& railMonitor() const { return m_rail; } + const RailMonitor::Sample& railSample(uint8_t ch) const { return m_rail_sample[ch < RailMonitor::MAX_CHANNELS ? ch : 0]; } + + HalI2cBus& tempBus() { return m_bus_temp; } + HalI2cBus& railBus() { return m_bus_rail; } + + /* Scan a bus for the `i2cscan` shell command; counts ACKed addresses in + * 0x08..0x77 into out[0..119]. Returns the number found. */ + uint32_t scan(uint8_t bus, uint8_t* out_addrs, uint32_t max_out); + +private: + static constexpr uint32_t POLL_MS = 200; /**< 5 Hz */ + static constexpr uint32_t REPROBE_MS = 5000; /**< lazy re-probe */ + static constexpr uint32_t PROBE_TIMEOUT_MS = 20; /**< boot-blocking cap */ + static constexpr uint32_t POLL_TIMEOUT_MS = 10; + static constexpr uint32_t FAULT_SUSTAIN_MS = 500; + static constexpr float HYST_C = 5.0f; + static constexpr float HYST_V = 0.5f; + static constexpr uint8_t MAX_POLL_ERRORS = 3; /**< -> mark absent */ + + bool probeTemp(); + bool probeRail(); + void pollTemp(uint32_t now_ms); + void pollRail(uint32_t now_ms); + void evaluateFaults(uint32_t now_ms); + + HalI2cBus m_bus_temp{&hi2c5}; /**< onboard temp bus */ + HalI2cBus m_bus_rail{&hi2c4}; /**< rail monitor bus */ + OnboardTempSensor m_temp; + RailMonitor m_rail; + + float m_temp_c = NAN; + RailMonitor::Sample m_rail_sample[RailMonitor::MAX_CHANNELS] = {}; + + uint8_t m_temp_addr = 0x48; + float m_temp_crit_c = 85.0f; + uint8_t m_rail_addr = 0x40; + float m_shunt_ohm = 0.005f; + float m_rail_max_a = 10.0f; + float m_rail_ov_v = 0.0f; /**< 0 = disabled */ + float m_rail_uv_v = 0.0f; /**< 0 = disabled */ + + uint8_t m_temp_errors = 0; + uint8_t m_rail_errors = 0; + uint32_t m_last_poll_ms = 0; + uint32_t m_temp_reprobe_ms = 0; + uint32_t m_rail_reprobe_ms = 0; + + bool m_ot_cond = false, m_ot_raised = false; + uint32_t m_ot_since_ms = 0; + bool m_ov_cond = false, m_ov_raised = false; + uint32_t m_ov_since_ms = 0; + bool m_uv_cond = false, m_uv_raised = false; + uint32_t m_uv_since_ms = 0; + + bool m_initialized = false; +}; + +/** @brief Global I2C sensors instance. */ +I2cSensors& i2cSensors(); + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/OnboardTempSensor.h b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/OnboardTempSensor.h new file mode 100644 index 00000000..5066badd --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/OnboardTempSensor.h @@ -0,0 +1,94 @@ +#pragma once + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include + +namespace Inverter { + +/** + * @brief On-board digital temperature sensor (I2C5, PF0/PF1). + * + * The assembled part number is not yet known, so this driver targets the + * ubiquitous pointer-register, 12-bit, 0.0625 degC/LSB family all of which + * share register map and decode math: + * - TI TMP102 (no device-id register) + * - TI TMP1075 (DEVICE_ID reg 0x0F = 0x7500) + * - NXP PCT2075 (no device-id register) + * + * Register map (big-endian 16-bit words behind an 8-bit pointer): + * 0x00 Temperature (12-bit code in bits 15:4; 13-bit when EM=1) + * 0x01 Config (EM = bit 4 selects 13-bit extended mode) + * 0x02 Tlow, 0x03 Thigh -- unused here, kept for the alert pin feature + * 0x0F DEVICE_ID (TMP1075 only; reads 0x0000 elsewhere) + * + * init() probes with a short timeout and never blocks boot; a missing device + * leaves part() == Part::None and the caller keeps re-probing lazily. + * + * All decode/encode math is static and free of bus state so the host test + * harness (tests/host/) can verify vectors directly. + */ +class OnboardTempSensor { +public: + enum class Part : uint8_t { + None = 0, /**< not detected / not initialised */ + Generic = 1, /**< TMP102 / PCT2075-compatible (no readable ID) */ + Tmp1075 = 2, /**< DEVICE_ID 0x7500 matched */ + }; + + static constexpr uint8_t REG_TEMP = 0x00; + static constexpr uint8_t REG_CONFIG = 0x01; + static constexpr uint8_t REG_TLOW = 0x02; + static constexpr uint8_t REG_THIGH = 0x03; + static constexpr uint8_t REG_ID = 0x0F; + static constexpr uint16_t TMP1075_DEVICE_ID = 0x7500; + static constexpr float LSB_C = 0.0625f; + + /** + * @brief Probe the sensor and latch its identity. + * @return true if the address ACKed (device present, even if the ID + * register reads back 0 -> Generic); false on NACK/bus error. + */ + bool init(II2cBus& bus, uint8_t addr7, uint32_t timeout_ms); + + /** + * @brief Read the temperature register and decode it. + * @return false on bus error; @p temp_c untouched on failure. + */ + bool poll(float& temp_c, uint32_t timeout_ms) const; + + Part part() const { return m_part; } + uint8_t address() const { return m_addr; } + bool extendedMode() const { return m_extended; } + const char* partName() const; + + /* -------- pure register math (host-testable) -------- */ + + /** + * @brief Decode the 16-bit temperature register to degC. + * @param reg Register value, host byte order (hi<<8|lo). + * @param extended true when config EM=1 (13-bit, value in bits 15:3). + * + * 12-bit (EM=0): degC = arithmetic_sar(reg, 4) * 0.0625 + * 13-bit (EM=1): degC = arithmetic_sar(reg, 3) * 0.0625 + * E.g. 0x7FF0 -> 127.9375 C; 0x1900 -> 25.0 C; EM 0x0C80 -> 25.0 C. + */ + static float decodeTempC(uint16_t reg, bool extended); + + /** @brief Encode degC to a 16-bit temperature register (tests/mock). */ + static uint16_t encodeTempReg(float temp_c, bool extended); + + /** @brief true when config register value has the EM (13-bit) bit set. */ + static bool configIsExtended(uint16_t cfg) { return (cfg & 0x0010u) != 0u; } + +private: + bool readReg(uint8_t reg, uint16_t& value, uint32_t timeout_ms) const; + bool writeReg(uint8_t reg, uint16_t value, uint32_t timeout_ms) const; + + II2cBus* m_bus = nullptr; + uint8_t m_addr = 0; + Part m_part = Part::None; + bool m_extended = false; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/RailMonitor.h b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/RailMonitor.h new file mode 100644 index 00000000..8519ee3a --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/Inverter/Drivers/I2C/RailMonitor.h @@ -0,0 +1,156 @@ +#pragma once + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include +#include + +namespace Inverter { + +/** + * @brief TI rail monitor on I2C4 (PD12/PF15): bus voltage + shunt current + * (+ derived power) for one monitored rail. + * + * The assembled part number is not yet known, so init() auto-detects the + * actual device with a capability/ID table, trying each candidate family: + * + * | Part | ID check | Shunt present | CAL reg | + * |---------|---------------------------------|---------------|---------| + * | INA226 | reg 0xFE=0x5449, 0xFF=0x2260 | 0x01 (16b) | 0x05 | + * | INA3221 | reg 0xFE=0x5449, 0xFF=0x3220 | 0x01/03/05 | none | + * | INA228 | reg 0x3E=0x5449, (0x3F>>4)=0x228| 0x04 (24b) | 0x02 | + * + * INA226/INA3221 probe first (they alias register space with INA228's ID + * registers); the INA228 probe only runs when the 0xFE/0xFF reads fail or + * mismatch, so a 16-bit part can never be mistaken for a 20-bit one. + * + * All calculations run from a single calibration model per part; the raw + * decode/encode math is static and host-testable (tests/host/). + */ +class RailMonitor { +public: + enum class Part : uint8_t { + None = 0, + INA226 = 1, + INA228 = 2, + INA3221 = 3, + }; + + struct Config { + float shunt_ohm; /**< shunt resistor [ohm] */ + float max_expected_a; /**< max expected rail current [A] (CAL ref) */ + }; + + struct Sample { + float bus_v; + float current_a; + float power_w; + }; + + static constexpr uint8_t MAX_CHANNELS = 3; /**< INA3221 channel count */ + + /* Shared register addresses. */ + static constexpr uint8_t REG_INA226_CONFIG = 0x00; + static constexpr uint8_t REG_INA226_SHUNT = 0x01; + static constexpr uint8_t REG_INA226_BUS = 0x02; + static constexpr uint8_t REG_INA226_POWER = 0x03; + static constexpr uint8_t REG_INA226_CURRENT = 0x04; + static constexpr uint8_t REG_INA226_CAL = 0x05; + static constexpr uint8_t REG_INA226_MASK_EN = 0x06; + /* Mask/Enable (0x06) bit 2 = OVF math overflow: shunt/current/power + * math overflowed (|current| beyond the calibrated full-scale). */ + static constexpr uint16_t INA226_OVF = 0x0004; + static constexpr uint8_t REG_INA228_CONFIG = 0x00; + /* INA228 CONFIG bit 4 = ADCRANGE (0: +/-163.84mV, 1: +/-40.96mV). */ + static constexpr uint16_t INA228_CFG_ADCRANGE = 0x0010; + /* INA226/INA3221 CONFIG mode bits [2:0] = 111: continuous shunt+bus. */ + static constexpr uint16_t INA226_CFG_MODE_CONT = 0x0007; + /* INA3221 CONFIG bits 14:12 = CH1..CH3 enable. */ + static constexpr uint16_t INA3221_CFG_CH_EN = 0x7000; + static constexpr uint8_t REG_INA_MFG_ID = 0xFE; /**< 226 + 3221 */ + static constexpr uint8_t REG_INA_DIE_ID = 0xFF; /**< 226 + 3221 */ + + static constexpr uint8_t REG_INA228_SHUNT_CAL = 0x02; + static constexpr uint8_t REG_INA228_VSHUNT = 0x04; + static constexpr uint8_t REG_INA228_VBUS = 0x05; + static constexpr uint8_t REG_INA228_DIETEMP = 0x06; + static constexpr uint8_t REG_INA228_CURRENT = 0x07; + static constexpr uint8_t REG_INA228_POWER = 0x08; + static constexpr uint8_t REG_INA228_MFG_ID = 0x3E; + static constexpr uint8_t REG_INA228_DEV_ID = 0x3F; + + /* ID-register expectations (TI manufacturer ID is ASCII "TI"). */ + static constexpr uint16_t TI_MFG_ID = 0x5449; + static constexpr uint16_t INA226_DIE_ID = 0x2260; + static constexpr uint16_t INA3221_DIE_ID = 0x3220; + static constexpr uint16_t INA228_DEV_ID_HI = 0x2280; /**< id<<4, rev=0 */ + + /** + * @brief Probe + identify + configure the rail monitor at @p addr7. + * @return true when a known part was found and configured. + * false on NACK (nothing there) or an unrecognised ID pair + * (the raw IDs remain readable via lastMfgId()/lastDevId()). + */ + bool init(II2cBus& bus, uint8_t addr7, const Config& cfg, uint32_t timeout_ms); + + /** + * @brief Read one rail's bus voltage / shunt current / derived power. + * @param channel INA3221: 0..2; single-channel parts ignore it (use 0). + * @return false on bus error or uninitialised device; out untouched. + */ + bool poll(uint8_t channel, Sample& out, uint32_t timeout_ms) const; + + Part part() const { return m_part; } + uint8_t channels() const { return (m_part == Part::INA3221) ? MAX_CHANNELS + : (m_part == Part::None ? 0 : 1); } + const char* partName() const; + uint8_t address() const { return m_addr; } + uint16_t shuntCalValue() const { return m_cal; } + uint16_t lastMfgId() const { return m_last_mfg; } + uint16_t lastDevId() const { return m_last_dev; } + float currentLsbA() const { return m_current_lsb; } + + /* -------- INA226 pure math (host-testable) -------- + * Current_LSB = MaxExpectedA / 32768; CAL = 0.00512 / (Current_LSB*Rsh) + * Bus LSB = 1.25 mV; shunt LSB = 2.5 uV; power LSB = 25 * Current_LSB. */ + static float ina226CurrentLsb(float max_expected_a); + static uint16_t ina226CalValue(float current_lsb_a, float shunt_ohm); + static float ina226BusVFromRaw(uint16_t raw) { return static_cast(raw) * 1.25e-3f; } + static float ina226ShuntVFromRaw(int16_t raw) { return static_cast(raw) * 2.5e-6f; } + static float ina226CurrentFromRaw(int16_t raw, float current_lsb_a); + static float ina226PowerFromRaw(uint16_t raw, float current_lsb_a); + + /* -------- INA228 pure math (20-bit ADC, 24-bit registers) -------- + * Current_LSB = MaxExpectedA/2^19; SHUNT_CAL = 13107.2e6 * LSB * Rsh + * (ADCRANGE=0). Results sit left-aligned in 24-bit registers; Vbus LSB + * = 195.3125 uV; power LSB = 3.2 * Current_LSB. */ + static float ina228CurrentLsb(float max_expected_a); + static uint16_t ina228ShuntCalValue(float current_lsb_a, float shunt_ohm); + static int32_t signExtend24(uint32_t raw24); + static float ina228BusVFromRaw(uint32_t raw24); + static float ina228CurrentFromRaw(uint32_t raw24, float current_lsb_a); + static float ina228PowerFromRaw(uint32_t raw24, float current_lsb_a); + static float ina228DieTempCFromRaw(uint16_t raw); + + /* -------- INA3221 pure math (no CAL: current computed in firmware) -- + * Bus LSB = 8 mV (bits 15:3); shunt LSB = 40 uV (signed, bits 15:3). */ + static float ina3221BusVFromRaw(uint16_t raw) { return static_cast(raw >> 3) * 8e-3f; } + static float ina3221ShuntVFromRaw(int16_t raw) { return static_cast(raw >> 3) * 40e-6f; } + static float ina3221CurrentA(float shunt_v, float shunt_ohm); + +private: + bool readReg16(uint8_t reg, uint16_t& value, uint32_t timeout_ms) const; + bool writeReg16(uint8_t reg, uint16_t value, uint32_t timeout_ms) const; + bool readReg24(uint8_t reg, uint32_t& value, uint32_t timeout_ms) const; + + II2cBus* m_bus = nullptr; + uint8_t m_addr = 0; + Part m_part = Part::None; + uint16_t m_cal = 0; + float m_current_lsb = 0.0f; + float m_shunt_ohm = 0.0f; + uint16_t m_last_mfg = 0; + uint16_t m_last_dev = 0; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Inc/i2c.h b/Images/Gen7FW/.rte-gen7/generated/Inc/i2c.h new file mode 100644 index 00000000..6b640bf5 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Inc/i2c.h @@ -0,0 +1,54 @@ +/* USER CODE BEGIN Header */ +/** + ****************************************************************************** + * @file i2c.h + * @brief This file contains all the function prototypes for + * the i2c.c file + ****************************************************************************** + * @attention + * + * Copyright (c) 2026 STMicroelectronics. + * All rights reserved. + * + * This software is licensed under terms that can be found in the LICENSE file + * in the root directory of this software component. + * If no LICENSE file comes with this software, it is provided AS-IS. + * + ****************************************************************************** + */ +/* USER CODE END Header */ +/* Define to prevent recursive inclusion -------------------------------------*/ +#ifndef __I2C_H__ +#define __I2C_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Includes ------------------------------------------------------------------*/ +#include "main.h" + +/* USER CODE BEGIN Includes */ + +/* USER CODE END Includes */ + +extern I2C_HandleTypeDef hi2c4; + +extern I2C_HandleTypeDef hi2c5; + +/* USER CODE BEGIN Private defines */ + +/* USER CODE END Private defines */ + +void MX_I2C4_Init(void); +void MX_I2C5_Init(void); + +/* USER CODE BEGIN Prototypes */ + +/* USER CODE END Prototypes */ + +#ifdef __cplusplus +} +#endif + +#endif /* __I2C_H__ */ diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Command/CommandInitializer.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Command/CommandInitializer.cpp index ac6b9781..9107cafc 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Command/CommandInitializer.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Command/CommandInitializer.cpp @@ -11,6 +11,7 @@ void registerFocCommands(CommandManager& mgr); void registerCanCommands(CommandManager& mgr); void registerOpenLoopCommands(CommandManager& mgr); void registerTraceCommands(CommandManager& mgr); +void registerI2cCommands(CommandManager& mgr); /* TIME_DOMAIN: APPLICATION_COMMAND_REGISTRATION * Registers all shell commands at boot. Commands execute in main-loop context. @@ -30,4 +31,5 @@ void initializeCommands() { registerCanCommands(mgr); registerOpenLoopCommands(mgr); registerTraceCommands(mgr); + registerI2cCommands(mgr); } diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Command/Commands/I2cCommands.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Command/Commands/I2cCommands.cpp new file mode 100644 index 00000000..1b44a17e --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Command/Commands/I2cCommands.cpp @@ -0,0 +1,163 @@ +#include "Inverter/Command/CommandInterface.h" +#include "Inverter/Command/CommandContext.h" +#include "Inverter/Drivers/I2C/I2cSensors.h" +#include "Inverter/Telemetry.h" + +#include +#include +#include + +using Inverter::I2cSensors; +using Inverter::i2cSensors; + +/** + * @brief Scan an I2C bus for ACKing addresses: `i2cscan [4|5]` (default both). + * + * Intended as the FIRST bring-up command on new Gen7 hardware (see + * docs/I2C_HW_Bringup.md). Probes 0x08..0x77 with a 2 ms timeout from shell + * context; worst case roughly 250 ms per bus. + */ +class I2cScanCommand : public CommandInterface { +public: + I2cScanCommand() + : CommandInterface("i2cscan", "Scan I2C bus 4 (rail mon) / 5 (temp) for devices", + ArgSpec{"bus", "4/5", 4.0f, 5.0f, 0.0f, false, ArgSpec::INT}) {} + + void execute(const ArgValue* args, CommandContext&) override { + I2cSensors& s = i2cSensors(); + uint8_t buses[2] = {4, 5}; + uint8_t nbus = 2; + if (args[0].present) { + const int32_t b = args[0].i_val; + if (b != 4 && b != 5) { + Telemetry::printf("[SHELL] usage: i2cscan [4|5]"); + return; + } + buses[0] = static_cast(b); + nbus = 1; + } + for (uint8_t i = 0; i < nbus; ++i) { + uint8_t found[16] = {0}; + const uint32_t n = s.scan(buses[i], found, sizeof(found)); + if (n == 0) { + Telemetry::printf("[SHELL] I2C%u: no devices found", buses[i]); + continue; + } + char line[128]; + int pos = snprintf(line, sizeof(line), "[SHELL] I2C%u devices (%lu):", + buses[i], static_cast(n)); + /* Cap pos at the buffer end: snprintf returns what *would* have + * been written, which can exceed the buffer when many devices + * answer. */ + if (pos >= static_cast(sizeof(line))) { + pos = static_cast(sizeof(line)) - 1; + } + for (uint32_t d = 0; d < n && pos > 0; ++d) { + const size_t room = sizeof(line) - static_cast(pos); + const int w = snprintf(&line[pos], room, " 0x%02X", found[d]); + if (w < 0 || static_cast(w) >= room) { + break; /* buffer full: stop appending */ + } + pos += w; + } + Telemetry::printf("%s", line); + } + Telemetry::printf("[SHELL] note: expected 0x48 on I2C5 (temp), 0x40 on I2C4 (rail mon)"); + } +}; + +/** + * @brief On-board temperature sensor status: `onb_temp` / `onb_temp reload`. + */ +class OnbTempCommand : public CommandInterface { +public: + OnbTempCommand() + : CommandInterface("onb_temp", "Onboard I2C temp sensor status (arg 'reload' re-reads config)", + ArgSpec{"subcommand", "", 0.0f, 0.0f, 0.0f, false, ArgSpec::STRING}) {} + + void execute(const ArgValue* args, CommandContext&) override { + I2cSensors& s = i2cSensors(); + if (args[0].present && strcasecmp(args[0].s_val, "reload") == 0) { + s.reloadConfig(); + return; + } + const auto& t = s.tempSensor(); + if (!s.tempPresent()) { + Telemetry::printf("[SHELL] onb_temp: ABSENT @ I2C5/0x%02X (lazy re-probe every 5 s)", + t.address()); + return; + } + Telemetry::printf("[SHELL] onb_temp: %s @ I2C5/0x%02X mode=%s T=%.2f C (crit %.0f C)", + t.partName(), t.address(), + t.extendedMode() ? "13-bit" : "12-bit", + static_cast(s.lastTempC()), + static_cast(s.tempCritC())); + const uint32_t err = s.tempBus().errorCount(); + const uint32_t tmo = s.tempBus().timeoutCount(); + if (err || tmo) { + Telemetry::printf("[SHELL] bus errors=%lu timeouts=%lu", + static_cast(err), + static_cast(tmo)); + } + } +}; + +/** + * @brief Rail monitor status: `rails` / `rails reload`. + */ +class RailsCommand : public CommandInterface { +public: + RailsCommand() + : CommandInterface("rails", "I2C rail monitor status: part, V/A/W per rail (arg 'reload' re-reads config)", + ArgSpec{"subcommand", "", 0.0f, 0.0f, 0.0f, false, ArgSpec::STRING}) {} + + void execute(const ArgValue* args, CommandContext&) override { + I2cSensors& s = i2cSensors(); + if (args[0].present && strcasecmp(args[0].s_val, "reload") == 0) { + s.reloadConfig(); + return; + } + const auto& r = s.railMonitor(); + if (!s.railPresent()) { + Telemetry::printf("[SHELL] rail_mon: ABSENT @ I2C4/0x%02X (lazy re-probe every 5 s); " + "last IDs mfg=0x%04X dev=0x%04X", + r.address(), r.lastMfgId(), r.lastDevId()); + return; + } + Telemetry::printf("[SHELL] rail_mon: %s @ I2C4/0x%02X CAL=0x%04X Ilsb=%.6f A", + r.partName(), r.address(), r.shuntCalValue(), + static_cast(r.currentLsbA())); + const uint8_t nch = r.channels(); + for (uint8_t ch = 0; ch < nch && ch < Inverter::RailMonitor::MAX_CHANNELS; ++ch) { + const auto& smp = s.railSample(ch); + if (std::isfinite(smp.bus_v)) { + Telemetry::printf("[SHELL] rail%u: %.3f V %.3f A %.2f W", + ch + 1, + static_cast(smp.bus_v), + static_cast(smp.current_a), + static_cast(smp.power_w)); + } else { + Telemetry::printf("[SHELL] rail%u: no sample yet", ch + 1); + } + } + const uint32_t err = s.railBus().errorCount(); + const uint32_t tmo = s.railBus().timeoutCount(); + if (err || tmo) { + Telemetry::printf("[SHELL] bus errors=%lu timeouts=%lu", + static_cast(err), + static_cast(tmo)); + } + } +}; + +static I2cScanCommand sI2cScanCmd; +static OnbTempCommand sOnbTempCmd; +static RailsCommand sRailsCmd; + +#include "Inverter/Command/CommandManager.h" + +void registerI2cCommands(CommandManager& mgr) { + mgr.registerCommand(&sI2cScanCmd); + mgr.registerCommand(&sOnbTempCmd); + mgr.registerCommand(&sRailsCmd); +} diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Control/FaultManager.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Control/FaultManager.cpp index b112aa41..bf2df6bd 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Control/FaultManager.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Control/FaultManager.cpp @@ -73,6 +73,9 @@ const char* faultReasonString(FaultReason r) { case FaultReason::OvertemperatureInv3: return "inverter sensor 3 over limit"; case FaultReason::OvertemperatureMotor: return "motor over temperature limit"; case FaultReason::SensorRefOutOfRange: return "reference out of window"; + case FaultReason::OnboardOvertemperature: return "onboard temp over limit (I2C5)"; + case FaultReason::RailOvervoltage: return "rail bus overvoltage (I2C4)"; + case FaultReason::RailUndervoltage: return "rail bus undervoltage (I2C4)"; case FaultReason::Count: break; } return "unknown"; diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/HalI2cBus.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/HalI2cBus.cpp new file mode 100644 index 00000000..8c53d133 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/HalI2cBus.cpp @@ -0,0 +1,49 @@ +#include "Inverter/Drivers/I2C/HalI2cBus.h" + +namespace Inverter { + +bool HalI2cBus::transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t timeout_ms) { + if ((tx == nullptr) || (tx_len == 0)) { + return false; + } + HAL_StatusTypeDef st; + if ((rx != nullptr) && (rx_len > 0)) { + /* tx[0] = register pointer, then repeated-start read. */ + st = HAL_I2C_Mem_Read(m_hi2c, static_cast(addr7) << 1, tx[0], + I2C_MEMADD_SIZE_8BIT, rx, rx_len, timeout_ms); + } else { + /* tx = {pointer, data...}; a lone pointer byte is a valid write of + * zero data bytes (sets the device read pointer). */ + st = HAL_I2C_Mem_Write(m_hi2c, static_cast(addr7) << 1, tx[0], + I2C_MEMADD_SIZE_8BIT, + const_cast(tx + 1), + static_cast(tx_len - 1), timeout_ms); + } + if (st == HAL_OK) { + return true; + } + if (st == HAL_TIMEOUT) { + ++m_timeouts; + } else { + ++m_errors; + } + /* A failed transaction can leave the peripheral busy; drop the sticky + * error flags so the next poll starts clean. */ + __HAL_I2C_CLEAR_FLAG(m_hi2c, I2C_FLAG_AF); + return false; +} + +bool HalI2cBus::isReady(uint8_t addr7, uint32_t timeout_ms) { + const HAL_StatusTypeDef st = + HAL_I2C_IsDeviceReady(m_hi2c, static_cast(addr7) << 1, + 1U /* trials */, timeout_ms); + if (st == HAL_TIMEOUT) { + ++m_timeouts; + } + return st == HAL_OK; +} + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/I2cSensors.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/I2cSensors.cpp new file mode 100644 index 00000000..d9fd72e8 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/I2cSensors.cpp @@ -0,0 +1,275 @@ +#include "Inverter/Drivers/I2C/I2cSensors.h" + +#include "Inverter/Control/FaultManager.h" +#include "Inverter/Drivers/Storage/RteParamStore.h" +#include "Inverter/Telemetry.h" + +#include "main.h" +#include "i2c.h" + +namespace Inverter { + +namespace { +I2cSensors s_instance; +} // namespace + +I2cSensors& i2cSensors() { + return s_instance; +} + +namespace { +/* KV load mirror of ApplicationSensors::loadConfig: defaults are persisted + * on first boot so `config list` shows the tunables. */ +float loadOne(const char* key, float def, bool persist_defaults) { + float value = def; + if (!RteParamStore::isReady()) { + return def; + } + if (!RteParamStore::get(key, &value) && persist_defaults) { + RteParamStore::set(key, def); + value = def; + } + return value; +} +} // namespace + +bool I2cSensors::init() { + m_temp_c = NAN; + for (uint8_t i = 0; i < RailMonitor::MAX_CHANNELS; ++i) { + m_rail_sample[i] = {NAN, NAN, NAN}; + } + + const bool persist = true; + const float addr_t = loadOne("OnbTemp.Addr", 0x48, persist); + m_temp_crit_c = loadOne("OnbTemp.CritC", 85.0f, persist); + const float addr_r = loadOne("RailMon.Addr", 0x40, persist); + m_shunt_ohm = loadOne("RailMon.ShuntOhm", 0.005f, persist); + m_rail_max_a = loadOne("RailMon.MaxA", 10.0f, persist); + m_rail_ov_v = loadOne("RailMon.OvV", 0.0f, persist); + m_rail_uv_v = loadOne("RailMon.UvV", 0.0f, persist); + if (persist && RteParamStore::isReady()) { + RteParamStore::flush(); + } + + /* Clamp addresses to valid 7-bit range. */ + m_temp_addr = (addr_t >= 8.0f && addr_t <= 119.0f) + ? static_cast(addr_t) : 0x48; + m_rail_addr = (addr_r >= 8.0f && addr_r <= 119.0f) + ? static_cast(addr_r) : 0x40; + + const bool t_ok = probeTemp(); + const bool r_ok = probeRail(); + Telemetry::printf("[I2C] onb_temp: %s @ I2C5/0x%02X", + t_ok ? m_temp.partName() : "absent", m_temp_addr); + Telemetry::printf("[I2C] rail_mon: %s @ I2C4/0x%02X (Rsh=%.3f ohm, Imax=%.1f A)", + r_ok ? m_rail.partName() : "absent", m_rail_addr, + static_cast(m_shunt_ohm), + static_cast(m_rail_max_a)); + + m_initialized = true; + const uint32_t now = HAL_GetTick(); + m_last_poll_ms = now; + return t_ok || r_ok; +} + +bool I2cSensors::probeTemp() { + m_temp_errors = 0; + const bool ok = m_temp.init(m_bus_temp, m_temp_addr, PROBE_TIMEOUT_MS); + if (!ok) { + /* Absent devices are re-probed lazily from update(). */ + m_temp_reprobe_ms = HAL_GetTick(); + } + return ok; +} + +bool I2cSensors::probeRail() { + m_rail_errors = 0; + const RailMonitor::Config cfg{m_shunt_ohm, m_rail_max_a}; + const bool ok = m_rail.init(m_bus_rail, m_rail_addr, cfg, PROBE_TIMEOUT_MS); + if (!ok) { + m_rail_reprobe_ms = HAL_GetTick(); + } + return ok; +} + +void I2cSensors::pollTemp(uint32_t now_ms) { + (void)now_ms; + float t = NAN; + if (m_temp.poll(t, POLL_TIMEOUT_MS)) { + m_temp_errors = 0; + if (std::isfinite(t)) { + m_temp_c = t; + Telemetry::log("onb_temp_c", t); + } + return; + } + if (++m_temp_errors >= MAX_POLL_ERRORS) { + Telemetry::printf("[I2C] onb_temp lost @ 0x%02X, re-arming probe", m_temp_addr); + m_temp.init(m_bus_temp, m_temp_addr, PROBE_TIMEOUT_MS); /* resets part to None */ + m_temp_reprobe_ms = now_ms; + } +} + +void I2cSensors::pollRail(uint32_t now_ms) { + bool ok = true; + const uint8_t nch = m_rail.channels(); + for (uint8_t ch = 0; ch < nch && ch < RailMonitor::MAX_CHANNELS; ++ch) { + RailMonitor::Sample s{}; + if (m_rail.poll(ch, s, POLL_TIMEOUT_MS)) { + m_rail_sample[ch] = s; + } else { + ok = false; + } + } + if (ok && nch > 0) { + m_rail_errors = 0; + Telemetry::log("rail_v", m_rail_sample[0].bus_v); + Telemetry::log("rail_a", m_rail_sample[0].current_a); + Telemetry::log("rail_w", m_rail_sample[0].power_w); + if (nch > 1) { + Telemetry::log("rail2_v", m_rail_sample[1].bus_v); + Telemetry::log("rail2_a", m_rail_sample[1].current_a); + Telemetry::log("rail3_v", m_rail_sample[2].bus_v); + Telemetry::log("rail3_a", m_rail_sample[2].current_a); + } + return; + } + if (nch > 0 && ++m_rail_errors >= MAX_POLL_ERRORS) { + Telemetry::printf("[I2C] rail_mon lost @ 0x%02X, re-arming probe", m_rail_addr); + m_rail.init(m_bus_rail, m_rail_addr, {m_shunt_ohm, m_rail_max_a}, + PROBE_TIMEOUT_MS); /* resets part to None */ + m_rail_reprobe_ms = now_ms; + } +} + +void I2cSensors::evaluateFaults(uint32_t now_ms) { + /* Over-temperature: 5 degC hysteresis, 500 ms sustain, Warning fault. */ + if (tempPresent() && std::isfinite(m_temp_c)) { + const bool cond = m_ot_cond ? (m_temp_c > (m_temp_crit_c - HYST_C)) + : (m_temp_c > m_temp_crit_c); + if (cond && !m_ot_cond) { + m_ot_since_ms = now_ms; + } + m_ot_cond = cond; + if (!cond) { + m_ot_raised = false; + } else if (!m_ot_raised && (now_ms - m_ot_since_ms) >= FAULT_SUSTAIN_MS) { + m_ot_raised = true; + FaultManager::instance().raise(FaultSource::OnboardOvertemperature, + FaultReason::OnboardOvertemperature); + } + } else { + m_ot_cond = false; + m_ot_raised = false; + } + + /* Rail OV/UV: only when the threshold is enabled (> 0) and the channel is + * producing finite values. 0.5 V hysteresis, 500 ms sustain. */ + const float v = m_rail_sample[0].bus_v; + const bool v_ok = railPresent() && std::isfinite(v); + + auto eval = [&now_ms, this](float threshold, bool& cond, bool& raised, + uint32_t& since, FaultSource src, FaultReason reason, + bool above, float val, bool enabled) { + if (!enabled) { + cond = false; + raised = false; + return; + } + const bool c = cond ? (above ? val > (threshold - HYST_V) + : val < (threshold + HYST_V)) + : (above ? val > threshold : val < threshold); + if (c && !cond) { + since = now_ms; + } + cond = c; + if (!c) { + raised = false; + } else if (!raised && (now_ms - since) >= FAULT_SUSTAIN_MS) { + raised = true; + FaultManager::instance().raise(src, reason); + } + }; + + eval(m_rail_ov_v, m_ov_cond, m_ov_raised, m_ov_since_ms, + FaultSource::RailOvervoltage, FaultReason::RailOvervoltage, + true, v, v_ok && m_rail_ov_v > 0.0f); + eval(m_rail_uv_v, m_uv_cond, m_uv_raised, m_uv_since_ms, + FaultSource::RailUndervoltage, FaultReason::RailUndervoltage, + false, v, v_ok && m_rail_uv_v > 0.0f); +} + +uint32_t I2cSensors::scan(uint8_t bus, uint8_t* out_addrs, uint32_t max_out) { + HalI2cBus* b = (bus == 5) ? &m_bus_temp : (bus == 4) ? &m_bus_rail : nullptr; + if (b == nullptr || out_addrs == nullptr) { + return 0; + } + uint32_t found = 0; + for (uint8_t addr = 0x08; addr <= 0x77; ++addr) { + if (b->isReady(addr, 2)) { + if (found < max_out) { + out_addrs[found++] = addr; + } + } + } + return found; +} + +void I2cSensors::reloadConfig() { + /* Re-read KV without persisting (values may be shell-set), then re-probe + * both devices against the (possibly new) addresses/calibration. */ + const bool persist = false; + const float addr_t = loadOne("OnbTemp.Addr", 0x48, persist); + m_temp_crit_c = loadOne("OnbTemp.CritC", 85.0f, persist); + const float addr_r = loadOne("RailMon.Addr", 0x40, persist); + m_shunt_ohm = loadOne("RailMon.ShuntOhm", 0.005f, persist); + m_rail_max_a = loadOne("RailMon.MaxA", 10.0f, persist); + m_rail_ov_v = loadOne("RailMon.OvV", 0.0f, persist); + m_rail_uv_v = loadOne("RailMon.UvV", 0.0f, persist); + if (addr_t >= 8.0f && addr_t <= 119.0f) m_temp_addr = static_cast(addr_t); + if (addr_r >= 8.0f && addr_r <= 119.0f) m_rail_addr = static_cast(addr_r); + + const bool t_ok = probeTemp(); + const bool r_ok = probeRail(); + Telemetry::printf("[I2C] config reloaded: onb_temp=%s @ 0x%02X rail_mon=%s @ 0x%02X", + t_ok ? m_temp.partName() : "absent", m_temp_addr, + r_ok ? m_rail.partName() : "absent", m_rail_addr); +} + +void I2cSensors::update() { + if (!m_initialized) { + return; + } + const uint32_t now_ms = HAL_GetTick(); + + /* Lazy re-probe of absent devices (short timeout, at most once every + * REPROBE_MS per device — never delays boot or the main loop). */ + if (!tempPresent() && (now_ms - m_temp_reprobe_ms) >= REPROBE_MS) { + m_temp_reprobe_ms = now_ms; + if (probeTemp()) { + Telemetry::printf("[I2C] onb_temp appeared: %s @ 0x%02X", + m_temp.partName(), m_temp_addr); + } + } + if (!railPresent() && (now_ms - m_rail_reprobe_ms) >= REPROBE_MS) { + m_rail_reprobe_ms = now_ms; + if (probeRail()) { + Telemetry::printf("[I2C] rail_mon appeared: %s @ 0x%02X", + m_rail.partName(), m_rail_addr); + } + } + + if ((now_ms - m_last_poll_ms) >= POLL_MS) { + m_last_poll_ms = now_ms; + if (tempPresent()) { + pollTemp(now_ms); + } + if (railPresent()) { + pollRail(now_ms); + } + } + + evaluateFaults(now_ms); +} + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp new file mode 100644 index 00000000..bbf27dc2 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp @@ -0,0 +1,100 @@ +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" + +#include + +namespace Inverter { + +const char* OnboardTempSensor::partName() const { + switch (m_part) { + case Part::Tmp1075: return "TMP1075"; + case Part::Generic: return "TMP102/PCT2075-family"; + default: return "none"; + } +} + +float OnboardTempSensor::decodeTempC(uint16_t reg, bool extended) { + /* Arithmetic right shift of the signed 16-bit word: the temperature code + * is left-aligned, so the sign bit propagates correctly in both modes. */ + const int16_t s = static_cast(reg); + const int16_t code = extended ? static_cast(s >> 3) + : static_cast(s >> 4); + return static_cast(code) * LSB_C; +} + +uint16_t OnboardTempSensor::encodeTempReg(float temp_c, bool extended) { + const long code = lroundf(temp_c / LSB_C); + const int shift = extended ? 3 : 4; + return static_cast(static_cast( + static_cast(code) << shift)); +} + +bool OnboardTempSensor::readReg(uint8_t reg, uint16_t& value, + uint32_t timeout_ms) const { + uint8_t buf[2] = {0, 0}; + if (!m_bus->transfer(m_addr, ®, 1, buf, 2, timeout_ms)) { + return false; + } + value = static_cast((static_cast(buf[0]) << 8) | buf[1]); + return true; +} + +bool OnboardTempSensor::writeReg(uint8_t reg, uint16_t value, + uint32_t timeout_ms) const { + const uint8_t buf[3] = { + reg, + static_cast(value >> 8), + static_cast(value & 0xFFu), + }; + return m_bus->transfer(m_addr, buf, 3, nullptr, 0, timeout_ms); +} + +bool OnboardTempSensor::init(II2cBus& bus, uint8_t addr7, uint32_t timeout_ms) { + m_bus = &bus; + m_addr = addr7; + m_part = Part::None; + m_extended = false; + + if (!bus.isReady(addr7, timeout_ms)) { + return false; + } + + /* Part identification via the DEVICE_ID register. TMP1075 answers + * 0x7500; TMP102 and PCT2075 have no ID register and return 0x0000 (or + * the read fails), which we treat as the generic family member — the + * temperature decode is identical either way. */ + uint16_t id = 0; + if (readReg(REG_ID, id, timeout_ms) && (id == TMP1075_DEVICE_ID)) { + m_part = Part::Tmp1075; + } else { + m_part = Part::Generic; + } + + /* Latch the EM bit so poll() decodes the right code width — TMP102-only + * feature. The TMP1075 has NO extended mode and its config register PORs + * to 0x00FF with every low "Not used" bit reading 1, so an unconditional + * EM check on bit 4 latches a bogus 13-bit decode and reads 2x the real + * temperature (e.g. 50 degC at 25 degC hardware). Only trust EM when the + * part was NOT positively ID'd as TMP1075 (SBOS854F Table 7-5). A + * PCT2075 can't be ID'd at all — see the note there: its Conf is one + * byte, whose second byte is undefined. */ + uint16_t cfg = 0; + if (m_part != Part::Tmp1075 && readReg(REG_CONFIG, cfg, timeout_ms)) { + m_extended = configIsExtended(cfg); + } + + return true; +} + +bool OnboardTempSensor::poll(float& temp_c, uint32_t timeout_ms) const { + if ((m_bus == nullptr) || (m_part == Part::None)) { + return false; + } + uint16_t raw = 0; + if (!readReg(REG_TEMP, raw, timeout_ms)) { + return false; + } + temp_c = decodeTempC(raw, m_extended); + return true; +} + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/RailMonitor.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/RailMonitor.cpp new file mode 100644 index 00000000..713d6c8d --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/I2C/RailMonitor.cpp @@ -0,0 +1,316 @@ +#include "Inverter/Drivers/I2C/RailMonitor.h" + +namespace Inverter { + +const char* RailMonitor::partName() const { + switch (m_part) { + case Part::INA226: return "INA226"; + case Part::INA228: return "INA228"; + case Part::INA3221: return "INA3221"; + default: return "none"; + } +} + +/* ---------------- INA226 math ---------------- */ + +float RailMonitor::ina226CurrentLsb(float max_expected_a) { + return max_expected_a / 32768.0f; +} + +uint16_t RailMonitor::ina226CalValue(float current_lsb_a, float shunt_ohm) { + /* CAL = 0.00512 / (Current_LSB * RSHUNT), rounded to the nearest 16-bit + * register value (quantisation error is +/-1 LSB either way). */ + const float denom = current_lsb_a * shunt_ohm; + if (denom <= 0.0f) { + return 0; + } + long cal = lround(0.00512 / static_cast(denom)); + if (cal > 0x7FFF) { /* CAL bit15 is reserved; clamp rather than wrap. */ + cal = 0x7FFF; + } else if (cal < 1) { + cal = 1; + } + return static_cast(cal); +} + +float RailMonitor::ina226CurrentFromRaw(int16_t raw, float current_lsb_a) { + return static_cast(raw) * current_lsb_a; +} + +float RailMonitor::ina226PowerFromRaw(uint16_t raw, float current_lsb_a) { + return static_cast(raw) * 25.0f * current_lsb_a; +} + +/* ---------------- INA228 math ---------------- */ + +float RailMonitor::ina228CurrentLsb(float max_expected_a) { + return max_expected_a / 524288.0f; /* 2^19, signed 20-bit positive range */ +} + +uint16_t RailMonitor::ina228ShuntCalValue(float current_lsb_a, float shunt_ohm) { + /* SHUNT_CAL = 13107.2e6 * Current_LSB * RSHUNT (ADCRANGE = 0), rounded + * to the nearest register value. The double intermediate keeps the + * exact mathematical result (e.g. 10 A / 5 mOhm = exactly 1250) from + * being truncated to 1249 by float rounding of the inputs. */ + const double cal = 13107.2e6 * static_cast(current_lsb_a) * + static_cast(shunt_ohm); + if (cal < 1.0) { + return 0; + } + if (cal > 0xFFFF) { + return 0xFFFF; + } + return static_cast(lround(cal)); +} + +int32_t RailMonitor::signExtend24(uint32_t raw24) { + raw24 &= 0x00FFFFFFu; + return (raw24 & 0x00800000u) != 0u + ? static_cast(raw24 | 0xFF000000u) + : static_cast(raw24); +} + +float RailMonitor::ina228BusVFromRaw(uint32_t raw24) { + /* 20-bit result left-aligned in the 24-bit register. */ + const int32_t code = signExtend24(raw24) >> 4; + return static_cast(code) * 195.3125e-6f; +} + +float RailMonitor::ina228CurrentFromRaw(uint32_t raw24, float current_lsb_a) { + const int32_t code = signExtend24(raw24) >> 4; + return static_cast(code) * current_lsb_a; +} + +float RailMonitor::ina228PowerFromRaw(uint32_t raw24, float current_lsb_a) { + /* POWER is a full 24-bit register (bits 23:0) on the INA228 — unlike + * VBUS/CURRENT, which hold 20-bit data in bits 23:4. Do not right-shift; + * a 20-bit decode would under-report power 16x. (SLYS021 §7.6.1.9.) */ + const uint32_t code = raw24 & 0x00FFFFFFu; + return static_cast(code) * 3.2f * current_lsb_a; +} + +float RailMonitor::ina228DieTempCFromRaw(uint16_t raw) { + return static_cast(static_cast(raw)) * 7.8125e-3f; +} + +/* ---------------- INA3221 math ---------------- */ + +float RailMonitor::ina3221CurrentA(float shunt_v, float shunt_ohm) { + return (shunt_ohm > 0.0f) ? (shunt_v / shunt_ohm) : 0.0f; +} + +/* ---------------- bus primitives ---------------- */ + +bool RailMonitor::readReg16(uint8_t reg, uint16_t& value, + uint32_t timeout_ms) const { + uint8_t buf[2] = {0, 0}; + if (!m_bus->transfer(m_addr, ®, 1, buf, 2, timeout_ms)) { + return false; + } + value = static_cast((static_cast(buf[0]) << 8) | buf[1]); + return true; +} + +bool RailMonitor::writeReg16(uint8_t reg, uint16_t value, + uint32_t timeout_ms) const { + const uint8_t buf[3] = { + reg, + static_cast(value >> 8), + static_cast(value & 0xFFu), + }; + return m_bus->transfer(m_addr, buf, 3, nullptr, 0, timeout_ms); +} + +bool RailMonitor::readReg24(uint8_t reg, uint32_t& value, + uint32_t timeout_ms) const { + uint8_t buf[3] = {0, 0, 0}; + if (!m_bus->transfer(m_addr, ®, 1, buf, 3, timeout_ms)) { + return false; + } + value = (static_cast(buf[0]) << 16) | + (static_cast(buf[1]) << 8) | + static_cast(buf[2]); + return true; +} + +/* ---------------- init / poll ---------------- */ + +bool RailMonitor::init(II2cBus& bus, uint8_t addr7, const Config& cfg, + uint32_t timeout_ms) { + m_bus = &bus; + m_addr = addr7; + m_part = Part::None; + m_cal = 0; + m_current_lsb = 0.0f; + m_shunt_ohm = cfg.shunt_ohm; + m_last_mfg = 0; + m_last_dev = 0; + + if (cfg.shunt_ohm <= 0.0f || cfg.max_expected_a <= 0.0f) { + return false; /* cannot calibrate without a sane shunt model */ + } + if (!bus.isReady(addr7, timeout_ms)) { + return false; + } + + /* Capability table, tried in register-collision-safe order: + * 1) INA226/INA3221 style ID registers 0xFE/0xFF. + * 2) INA228 style ID registers 0x3E/0x3F (only when 1) fails cleanly, + * since on an INA226/3221 those addresses read as 0. */ + uint16_t mfg = 0, dev = 0; + bool legacy_ok = readReg16(REG_INA_MFG_ID, mfg, timeout_ms) && + readReg16(REG_INA_DIE_ID, dev, timeout_ms); + if (legacy_ok && mfg == TI_MFG_ID) { + m_last_mfg = mfg; + m_last_dev = dev; + /* Die-ID registers are DID[15:4] + RID[3:0] (revision): US-fab INA226 + * revs report 0x2261, and treating the nibble as part of the ID + * rejects legitimate parts. Mask the RID nibble (SBOS547 Table 7-1 + * note 3 / Table 7-15; INA3221 ID register has the same layout). */ + if ((dev & 0xFFF0u) == INA226_DIE_ID) { + m_part = Part::INA226; + m_current_lsb = ina226CurrentLsb(cfg.max_expected_a); + m_cal = ina226CalValue(m_current_lsb, cfg.shunt_ohm); + /* Continuous shunt+bus, 1.1 ms conversions (POR default). The + * CAL write is what makes the current/power registers valid. + * RMW the MODE bits too: a warm boot could leave a stale + * triggered/shutdown mode behind (POR CONFIG = 0x4127). */ + uint16_t config = 0; + if (!writeReg16(REG_INA226_CAL, m_cal, timeout_ms)) { + return false; + } + if (!readReg16(REG_INA226_CONFIG, config, timeout_ms)) { + return false; + } + if ((config & INA226_CFG_MODE_CONT) != INA226_CFG_MODE_CONT) { + config = static_cast((config & ~INA226_CFG_MODE_CONT) | + INA226_CFG_MODE_CONT); + return writeReg16(REG_INA226_CONFIG, config, timeout_ms); + } + return true; + } + if ((dev & 0xFFF0u) == INA3221_DIE_ID) { + m_part = Part::INA3221; + /* RMW: enable all three channels (kernel programs CH_EN; disabled + * channels hold stale data that must never read back as valid) + * and force continuous shunt+bus mode after warm boots. */ + uint16_t config = 0; + if (!readReg16(REG_INA226_CONFIG, config, timeout_ms)) { + return false; + } + const uint16_t want = static_cast( + (config | INA3221_CFG_CH_EN | INA226_CFG_MODE_CONT)); + if (want != config) { + return writeReg16(REG_INA226_CONFIG, want, timeout_ms); + } + return true; + } + return false; /* known TI family but unsupported die */ + } + + mfg = 0; dev = 0; + const bool got228 = readReg16(REG_INA228_MFG_ID, mfg, timeout_ms) && + readReg16(REG_INA228_DEV_ID, dev, timeout_ms); + if (got228) { + /* Record the raw IDs regardless of match so a rejected probe stays + * diagnosable via lastMfgId()/lastDevId() (same contract as the + * legacy-ID branch above). */ + m_last_mfg = mfg; + m_last_dev = dev; + } + if (got228 && mfg == TI_MFG_ID && (dev & 0xFFF0u) == INA228_DEV_ID_HI) { + m_part = Part::INA228; + m_current_lsb = ina228CurrentLsb(cfg.max_expected_a); + m_cal = ina228ShuntCalValue(m_current_lsb, cfg.shunt_ohm); + /* POR defaults keep ADCRANGE=0 (+/-163.84 mV shunt swing) and + * continuous conversion of bus/shunt/temp, but a warm boot could + * leave ADCRANGE=1 behind (4x finer shunt LSB -> currents would + * read 4x high under the ADCRANGE=0 math). RMW CONFIG to force + * ADCRANGE=0; everything else is left at the running config. */ + uint16_t config228 = 0; + if (m_cal == 0 || + !writeReg16(REG_INA228_SHUNT_CAL, m_cal, timeout_ms) || + !readReg16(REG_INA228_CONFIG, config228, timeout_ms)) { + return false; + } + if ((config228 & INA228_CFG_ADCRANGE) != 0u) { + config228 = static_cast(config228 & ~INA228_CFG_ADCRANGE); + return writeReg16(REG_INA228_CONFIG, config228, timeout_ms); + } + return true; + } + return false; +} + +bool RailMonitor::poll(uint8_t channel, Sample& out, + uint32_t timeout_ms) const { + if ((m_bus == nullptr) || (m_part == Part::None)) { + return false; + } + if (channel >= channels()) { + return false; + } + + switch (m_part) { + case Part::INA226: { + uint16_t bus = 0, power = 0; + int16_t current = 0; + uint16_t cur_raw = 0; + if (!readReg16(REG_INA226_BUS, bus, timeout_ms) || + !readReg16(REG_INA226_CURRENT, cur_raw, timeout_ms) || + !readReg16(REG_INA226_POWER, power, timeout_ms)) { + return false; + } + current = static_cast(cur_raw); + /* A raw current at the rails means the chip's internal + * shunt*CAL math may have overflowed (the count wraps to the + * opposite sign when |current| exceeds the calibrated + * full-scale). Confirm via the Mask/Enable math-overflow flag + * before trusting a near-full-scale sample; the OVF read only + * costs one transaction at the rails. */ + if (current >= 32760 || current <= -32768) { + uint16_t mask_en = 0; + if (readReg16(REG_INA226_MASK_EN, mask_en, timeout_ms) && + (mask_en & INA226_OVF) != 0u) { + return false; /* math overflow: sample is a wrapped rail */ + } + } + out.bus_v = ina226BusVFromRaw(bus); + out.current_a = ina226CurrentFromRaw(current, m_current_lsb); + out.power_w = ina226PowerFromRaw(power, m_current_lsb); + return true; + } + case Part::INA228: { + uint32_t bus = 0, current = 0, power = 0; + if (!readReg24(REG_INA228_VBUS, bus, timeout_ms) || + !readReg24(REG_INA228_CURRENT, current, timeout_ms) || + !readReg24(REG_INA228_POWER, power, timeout_ms)) { + return false; + } + out.bus_v = ina228BusVFromRaw(bus); + out.current_a = ina228CurrentFromRaw(current, m_current_lsb); + out.power_w = ina228PowerFromRaw(power, m_current_lsb); + return true; + } + case Part::INA3221: { + /* Ch n (1-based in the datasheet): shunt 0x01+2n, bus 0x02+2n. */ + const uint8_t reg_shunt = static_cast(0x01 + 2 * channel); + const uint8_t reg_bus = static_cast(0x02 + 2 * channel); + uint16_t shunt_raw = 0, bus_raw = 0; + if (!readReg16(reg_bus, bus_raw, timeout_ms) || + !readReg16(reg_shunt, shunt_raw, timeout_ms)) { + return false; + } + const float shunt_v = + ina3221ShuntVFromRaw(static_cast(shunt_raw)); + out.bus_v = ina3221BusVFromRaw(static_cast(bus_raw)); + out.current_a = ina3221CurrentA(shunt_v, m_shunt_ohm); + out.power_w = out.bus_v * out.current_a; /* no power reg; derive */ + return true; + } + default: + return false; + } +} + +} // namespace Inverter diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/PWM/pwm.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/PWM/pwm.cpp index 6bd09ccf..f395e493 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/PWM/pwm.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/PWM/pwm.cpp @@ -568,4 +568,3 @@ void PWM_PrintSPWMState(void) (double)spwm_modulation_index, (double)du, (double)dv, (double)dw); } - diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/Sensors/PhaseCurrentADC.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/Sensors/PhaseCurrentADC.cpp index fbb98abb..8cebfa4a 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/Sensors/PhaseCurrentADC.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/Drivers/Sensors/PhaseCurrentADC.cpp @@ -654,4 +654,3 @@ extern "C" void HAL_ADC_LevelOutOfWindowCallback(ADC_HandleTypeDef* hadc) { Inverter::FaultReason::AdcWatchdogTrip); } } - diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/InverterMain.cpp b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/InverterMain.cpp index 73e7d8d1..0b063e82 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/InverterMain.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/Src/Inverter/InverterMain.cpp @@ -24,6 +24,7 @@ #include "Inverter/Drivers/Sensors/DcLinkVoltageSensor.h" #include "Inverter/Drivers/Sensors/DcLinkCurrentSensor.h" #include "Inverter/Drivers/Sensors/EncoderADC.h" +#include "Inverter/Drivers/I2C/I2cSensors.h" #include "Inverter/Drivers/CAN/CanBus.h" #include "Inverter/Drivers/CAN/CanSession.h" #include "Inverter/Drivers/CAN/FdcanFault.h" @@ -40,10 +41,10 @@ #include "spi.h" #include "cy15b102q_driver.h" #include "ontime_logger.h" -#include "../../generated/domain_vsense_generated.h" #include "../../generated/domain_adc_isr_generated.h" -#include "../../generated/domain_tim_isr_generated.h" #include "../../generated/domain_app_loop_generated.h" +#include "../../generated/domain_tim_isr_generated.h" +#include "../../generated/domain_vsense_generated.h" /* Global RTE codegen state variable. Referenced by app::Init/Step * calls inserted at // RTE_EMIT markers. */ @@ -168,6 +169,11 @@ static void init() * conversions and never blocks. */ Inverter::appSensors().init(); + /* Gen7 I2C sensors (I2C5 onboard temp, I2C4 rail monitor). Probes with a + * short timeout; missing devices never block boot and are re-probed every + * 5 s from the main loop. */ + Inverter::i2cSensors().init(); + /* CAN buses (KV enables; no-op when both disabled). */ Inverter::canBus().init(); @@ -257,6 +263,9 @@ static void loop() /* Application sensors (temps/throttle): harvest + recompute; never blocks. */ Inverter::appSensors().update(); + /* I2C sensors: 5 Hz poll, lazy re-probe, Warning-fault evaluation. */ + Inverter::i2cSensors().update(); + /* CAN: drain TX queues, bus-off recovery, session heartbeat watch. */ Inverter::traceRecorder().update(); Inverter::canBus().update(); @@ -310,4 +319,3 @@ extern "C" void InverterMain_Run(void) InverterMain::loop(); } } - diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/i2c.c b/Images/Gen7FW/.rte-gen7/generated/Src/i2c.c new file mode 100644 index 00000000..14ce7de9 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/Src/i2c.c @@ -0,0 +1,269 @@ +/* USER CODE BEGIN Header */ +/** + ****************************************************************************** + * @file i2c.c + * @brief This file provides code for the configuration + * of the I2C instances. + ****************************************************************************** + * @attention + * + * Copyright (c) 2026 STMicroelectronics. + * All rights reserved. + * + * This software is licensed under terms that can be found in the LICENSE file + * in the root directory of this software component. + * If no LICENSE file comes with this software, it is provided AS-IS. + * + ****************************************************************************** + * + * WARNING (CubeMX regen): this file is hand-maintained to match what CubeMX + * generates from STM32CubeMX.ioc for the two new Gen7 buses: + * I2C4 PD12=SCL / PF15=SDA (VOLTAGE_MONITOR_I2C_*) Ti rail monitor + * I2C5 PF0 =SDA / PF1 =SCL (ONBOARD_TEMP_SENSE_I2C_*) board temp sensor + * A CubeMX regeneration that emits its own i2c.c would duplicate these + * definitions; delete the regenerated copy (or merge it back here) and keep + * the MX_I2C4_Init()/MX_I2C5_Init() calls in main.c in exactly one place. + * + * Note: drivers are polled from the main loop with blocking HAL calls and a + * short timeout; no I2C interrupt or DMA mode is used, so no NVIC lines are + * enabled for I2C4/I2C5. + * + ****************************************************************************** + */ +/* USER CODE END Header */ +/* Includes ------------------------------------------------------------------*/ +#include "i2c.h" + +/* USER CODE BEGIN 0 */ + +/* USER CODE END 0 */ + +I2C_HandleTypeDef hi2c4; +I2C_HandleTypeDef hi2c5; + +/* I2C4 init function */ +void MX_I2C4_Init(void) +{ + + /* USER CODE BEGIN I2C4_Init 0 */ + + /* USER CODE END I2C4_Init 0 */ + + /* USER CODE BEGIN I2C4_Init 1 */ + + /* USER CODE END I2C4_Init 1 */ + hi2c4.Instance = I2C4; + /* .ioc: Timing = 0x60404E72 with the 137.5 MHz I2C kernel clock + * (-> ~100 kHz, rise/fall 100 ns per the .ioc). */ + hi2c4.Init.Timing = 0x60404E72; + hi2c4.Init.OwnAddress1 = 0; + hi2c4.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; + hi2c4.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; + hi2c4.Init.OwnAddress2 = 0; + hi2c4.Init.OwnAddress2Masks = I2C_OA2_NOMASK; + hi2c4.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; + hi2c4.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; + if (HAL_I2C_Init(&hi2c4) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Analog filter + */ + if (HAL_I2CEx_ConfigAnalogFilter(&hi2c4, I2C_ANALOGFILTER_ENABLE) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Digital filter + */ + if (HAL_I2CEx_ConfigDigitalFilter(&hi2c4, 0) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN I2C4_Init 2 */ + + /* USER CODE END I2C4_Init 2 */ + +} +/* I2C5 init function */ +void MX_I2C5_Init(void) +{ + + /* USER CODE BEGIN I2C5_Init 0 */ + + /* USER CODE END I2C5_Init 0 */ + + /* USER CODE BEGIN I2C5_Init 1 */ + + /* USER CODE END I2C5_Init 1 */ + hi2c5.Instance = I2C5; + /* .ioc: Timing = 0x60404E72 with the 137.5 MHz I2C kernel clock + * (-> ~100 kHz, rise/fall 100 ns per the .ioc). */ + hi2c5.Init.Timing = 0x60404E72; + hi2c5.Init.OwnAddress1 = 0; + hi2c5.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; + hi2c5.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; + hi2c5.Init.OwnAddress2 = 0; + hi2c5.Init.OwnAddress2Masks = I2C_OA2_NOMASK; + hi2c5.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; + hi2c5.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; + if (HAL_I2C_Init(&hi2c5) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Analog filter + */ + if (HAL_I2CEx_ConfigAnalogFilter(&hi2c5, I2C_ANALOGFILTER_ENABLE) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Digital filter + */ + if (HAL_I2CEx_ConfigDigitalFilter(&hi2c5, 0) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN I2C5_Init 2 */ + + /* USER CODE END I2C5_Init 2 */ + +} + +void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle) +{ + + GPIO_InitTypeDef GPIO_InitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + if(i2cHandle->Instance==I2C4) + { + /* USER CODE BEGIN I2C4_MspInit 0 */ + + /* USER CODE END I2C4_MspInit 0 */ + + /** Initializes the peripherals clock + */ + /* Default mux (RCC_I2C4CLKSOURCE_D3PCLK1), pinned here so the 0x60404E72 + * timing constants always pair with the .ioc's 137.5 MHz kernel clock. */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_I2C4; + PeriphClkInitStruct.I2c4ClockSelection = RCC_I2C4CLKSOURCE_D3PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) + { + Error_Handler(); + } + + __HAL_RCC_GPIOD_CLK_ENABLE(); + __HAL_RCC_GPIOF_CLK_ENABLE(); + /**I2C4 GPIO Configuration + PD12 ------> I2C4_SCL + PF15 ------> I2C4_SDA + */ + GPIO_InitStruct.Pin = VOLTAGE_MONITOR_I2C_SCL_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF4_I2C4; + HAL_GPIO_Init(VOLTAGE_MONITOR_I2C_SCL_GPIO_Port, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = VOLTAGE_MONITOR_I2C_SDA_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF4_I2C4; + HAL_GPIO_Init(VOLTAGE_MONITOR_I2C_SDA_GPIO_Port, &GPIO_InitStruct); + + /* I2C4 clock enable */ + __HAL_RCC_I2C4_CLK_ENABLE(); + /* USER CODE BEGIN I2C4_MspInit 1 */ + + /* USER CODE END I2C4_MspInit 1 */ + } + else if(i2cHandle->Instance==I2C5) + { + /* USER CODE BEGIN I2C5_MspInit 0 */ + + /* USER CODE END I2C5_MspInit 0 */ + + /** Initializes the peripherals clock + */ + /* Default mux (RCC_I2C1235CLKSOURCE_D2PCLK1), pinned here for the same + * reason as I2C4 above (.ioc: I2C123Freq_Value = 137.5 MHz). */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_I2C5; + PeriphClkInitStruct.I2c1235ClockSelection = RCC_I2C1235CLKSOURCE_D2PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) + { + Error_Handler(); + } + + __HAL_RCC_GPIOF_CLK_ENABLE(); + /**I2C5 GPIO Configuration + PF0 ------> I2C5_SDA + PF1 ------> I2C5_SCL + */ + /* NOTE: AF6 (GPIO_AF6_I2C5) per the STM32H723 alternate-function table for + * PF0/PF1 when mapped to I2C5 (AF4 on these pins is I2C2). The .ioc does + * not record the AF selection, so confirm against a CubeMX regen on first + * hardware bring-up (see docs/I2C_HW_Bringup.md). */ + GPIO_InitStruct.Pin = ONBOARD_TEMP_SENSE_I2C_SDA_Pin|ONBOARD_TEMP_SENSE_I2C_SCL_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF6_I2C5; + HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); + + /* I2C5 clock enable */ + __HAL_RCC_I2C5_CLK_ENABLE(); + /* USER CODE BEGIN I2C5_MspInit 1 */ + + /* USER CODE END I2C5_MspInit 1 */ + } +} + +void HAL_I2C_MspDeInit(I2C_HandleTypeDef* i2cHandle) +{ + + if(i2cHandle->Instance==I2C4) + { + /* USER CODE BEGIN I2C4_MspDeInit 0 */ + + /* USER CODE END I2C4_MspDeInit 0 */ + /* Peripheral clock disable */ + __HAL_RCC_I2C4_CLK_DISABLE(); + + /**I2C4 GPIO Configuration + PD12 ------> I2C4_SCL + PF15 ------> I2C4_SDA + */ + HAL_GPIO_DeInit(VOLTAGE_MONITOR_I2C_SCL_GPIO_Port, VOLTAGE_MONITOR_I2C_SCL_Pin); + + HAL_GPIO_DeInit(VOLTAGE_MONITOR_I2C_SDA_GPIO_Port, VOLTAGE_MONITOR_I2C_SDA_Pin); + + /* USER CODE BEGIN I2C4_MspDeInit 1 */ + + /* USER CODE END I2C4_MspDeInit 1 */ + } + else if(i2cHandle->Instance==I2C5) + { + /* USER CODE BEGIN I2C5_MspDeInit 0 */ + + /* USER CODE END I2C5_MspDeInit 0 */ + /* Peripheral clock disable */ + __HAL_RCC_I2C5_CLK_DISABLE(); + + /**I2C5 GPIO Configuration + PF0 ------> I2C5_SDA + PF1 ------> I2C5_SCL + */ + HAL_GPIO_DeInit(GPIOF, ONBOARD_TEMP_SENSE_I2C_SDA_Pin|ONBOARD_TEMP_SENSE_I2C_SCL_Pin); + + /* USER CODE BEGIN I2C5_MspDeInit 1 */ + + /* USER CODE END I2C5_MspDeInit 1 */ + } +} + +/* USER CODE BEGIN 1 */ + +/* USER CODE END 1 */ diff --git a/Images/Gen7FW/.rte-gen7/generated/Src/main.c b/Images/Gen7FW/.rte-gen7/generated/Src/main.c index ee862770..9b42fdb3 100644 --- a/Images/Gen7FW/.rte-gen7/generated/Src/main.c +++ b/Images/Gen7FW/.rte-gen7/generated/Src/main.c @@ -22,6 +22,7 @@ #include "dma.h" #include "fdcan.h" #include "spi.h" +#include "i2c.h" #include "tim.h" #include "usart.h" #include "gpio.h" @@ -111,6 +112,12 @@ int main(void) MX_FDCAN2_Init(); MX_SPI4_Init(); MX_SPI2_Init(); + /* Hand-maintained (see Src/i2c.c header): the .ioc already describes I2C4 + * (PD12/PF15, VOLTAGE_MONITOR_I2C_*) and I2C5 (PF0/PF1, + * ONBOARD_TEMP_SENSE_I2C_*). A CubeMX regeneration must NOT emit its own + * MX_I2C4_Init/MX_I2C5_Init (or duplicate i2c.c) alongside these calls. */ + MX_I2C4_Init(); + MX_I2C5_Init(); /* USER CODE BEGIN 2 */ InverterMain_Run(); /* USER CODE END 2 */ diff --git a/Images/Gen7FW/.rte-gen7/generated/cmake/stm32cubemx/CMakeLists.txt b/Images/Gen7FW/.rte-gen7/generated/cmake/stm32cubemx/CMakeLists.txt index 86a183d0..83c61e0f 100644 --- a/Images/Gen7FW/.rte-gen7/generated/cmake/stm32cubemx/CMakeLists.txt +++ b/Images/Gen7FW/.rte-gen7/generated/cmake/stm32cubemx/CMakeLists.txt @@ -26,6 +26,7 @@ set(MX_Application_Src ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/dma.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/fdcan.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/spi.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/i2c.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/tim.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/usart.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/stm32h7xx_it.c diff --git a/Images/Gen7FW/.rte-gen7/generated/docs/I2C_HW_Bringup.md b/Images/Gen7FW/.rte-gen7/generated/docs/I2C_HW_Bringup.md new file mode 100644 index 00000000..656a05d9 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/docs/I2C_HW_Bringup.md @@ -0,0 +1,131 @@ +# Gen7 I2C Hardware Bring-up + +Gen7 hardware adds two I2C buses. Per the `STM32CubeMX.ioc` (ground truth): + +| Bus | Function | Pins | Kernel clock | Timing reg | Result | +|------|------------------------|-------------------------------|--------------|------------|----------| +| I2C5 | Onboard temp sensor | PF0 = SDA, PF1 = SCL | 137.5 MHz | 0x60404E72 | ~100 kHz | +| I2C4 | Voltage/rail monitor | PD12 = SCL, PF15 = SDA | 137.5 MHz | 0x60404E72 | ~100 kHz | + +Labels: `ONBOARD_TEMP_SENSE_I2C_SDA/SCL` (I2C5), `VOLTAGE_MONITOR_I2C_SCL/SDA` (I2C4). +7-bit addressing, single address mode, general call off, clock stretching +allowed. Rise/fall time budget 100 ns per the `.ioc`. `MX_I2C4_Init()` and +`MX_I2C5_Init()` live in `Src/i2c.c` (hand-maintained CubeMX-style; a CubeMX +regen must not duplicate them — see the warning banner at the top of +`Src/i2c.c` and `main.c`). + +**Bring-up caveat:** PF0/PF1 carry I2C5 on **AF6** in the driver (`AF4` on these +pins is I2C2). The `.ioc` does not record the AF selection, so confirm against +real hardware (or a CubeMX regen diff) at first power-up. + +## Expected parts + +The exact assembled part numbers are **not yet known**; the firmware +auto-detects against the real candidate families. + +### I2C5 — on-board temperature sensor (default addr 0x48, range 0x48–0x4F) + +Any of the pointer-register 12-bit family; all share register map and decode +(0.0625 °C/LSB, code in bits 15:4 of register 0x00): + +| Candidate | Distinguishing feature | +|------------|-----------------------------------------------------| +| TMP1075 | `DEVICE_ID` reg 0x0F = `0x7500` -> identified as TMP1075 | +| TMP102 | no ID register (0x0F reads 0x0000) -> "generic" | +| PCT2075 | no ID register (0x0F reads 0x0000) -> "generic" | + +Extended 13-bit mode (config `EM` bit) is decoded automatically from the +device's config register. + +### I2C4 — rail monitor (default addr 0x40, configurable) + +Bus voltage + shunt current + power. Auto-detect order: + +| Order | Part | ID check | CAL written | Channels | +|-------|---------|-------------------------------------------|--------------|----------| +| 1 | INA226 | reg 0xFE = `0x5449` ("TI"), 0xFF = `0x2260` | `CAL = 0.00512 / (Ilsb * Rsh)`, Ilsb = MaxA/2^15 | 1 | +| 2 | INA3221 | reg 0xFE = `0x5449`, 0xFF = `0x3220` | none (current computed in firmware) | 3 | +| 3 | INA228 | reg 0x3E = `0x5449`, (0x3F >> 4) = `0x228` | `SHUNT_CAL = 13107.2e6 * Ilsb * Rsh` (ADCRANGE=0), Ilsb = MaxA/2^19 | 1 | + +Known TI parts with an unrecognised die ID are *not* configured — the rails +stay absent and `rails` prints the raw mfg/dev IDs so support can be added. + +## First power-up / wiring check order + +1. `i2cscan` — scan both buses (or `i2cscan 5` / `i2cscan 4`). Expected: + `0x48` ACKs on I2C5, `0x40` ACKs on I2C4. + - Nothing at all on one bus -> check the connector/continuity and that the + expected pull-ups are populated (SDA/SCL should idle high, ~3.3 V). + - SDA and SCL both stuck low -> swapped short / solder bridge. + - In-regs garbage vs no-ACK distinguishes wiring from addressing issues. +2. `onb_temp` — should identify the part (TMP1075 vs generic) and report a + plausible board temperature. Sanity check: breathe on the sensor, + watch `onb_temp_c` rise on the telemetry stream. +3. `rails` — should identify the monitor (INA226/INA228/INA3221) and print the + cal value and rail readings. Cross-check bus voltage against a DMM. +4. Only then trust the telemetry stream / warning faults. + +## Shell commands + +| Command | Action | +|----------------------|------------------------------------------------------------| +| `i2cscan [4\|5]` | ACK scan of I2C4/I2C5 (0x08–0x77), prints device list | +| `onb_temp` | Part, address, last temperature, bus error counters | +| `onb_temp reload` | Re-read KV config, re-probe (after `config set`) | +| `rails` | Part, CAL, per-rail V/A/W, bus error counters, last IDs | +| `rails reload` | Re-read KV config, re-probe (after `config set`) | + +## Config keys (FRAM via RteParamStore; `config set `, then +`config saveall` to persist; `onb_temp reload` / `rails reload` applies them +without reboot) + +| Key | Default | Meaning | +|--------------------|---------|-------------------------------------------| +| `OnbTemp.Addr` | 72 (0x48) | 7-bit temp sensor address | +| `OnbTemp.CritC` | 85 | over-temperature Warning threshold [degC] | +| `RailMon.Addr` | 64 (0x40) | 7-bit rail monitor address | +| `RailMon.ShuntOhm` | 0.005 | shunt resistor [ohm] | +| `RailMon.MaxA` | 10 | max expected current [A] (CAL reference) | +| `RailMon.OvV` | 0 | rail overvoltage Warning [V]; 0 = off | +| `RailMon.UvV` | 0 | rail undervoltage Warning [V]; 0 = off | + +## Telemetry keys + +| Key | Source | +|--------------|-------------------------------------------| +| `onb_temp_c` | onboard temperature [degC] (I2C5) | +| `rail_v` | rail 1 bus voltage [V] | +| `rail_a` | rail 1 current [A] | +| `rail_w` | rail 1 power [W] (INA3221: V*I derived) | +| `rail2_v`, `rail2_a`, `rail3_v`, `rail3_a` | INA3221 channels 2/3 only | + +## Warning faults (non-latching-trip, log-only Warning severity) + +`OnboardOvertemperature` (5 degC hysteresis), `RailOvervoltage`, +`RailUndervoltage` (0.5 V hysteresis); all sustain 500 ms before raising. +The drivers are polled from the main loop only — never from an ISR — at 5 Hz +(`POLL_MS = 200`), with a 20 ms probe timeout at init so a missing device can +never delay boot; absent devices are re-probed every 5 s. Three consecutive +poll failures mark a device absent (one log line) and re-arm the probe. + +## Host-side verification + +`tests/host/build_and_run.sh` compiles `OnboardTempSensor.cpp` and +`RailMonitor.cpp` for the host (plain g++ -std=c++17, no HAL) against mock +register models of TMP1075/TMP102, INA226, INA3221 and INA228, and asserts the +decode/encode math plus the detect/absent/error paths. All I2C traffic in +the drivers funnels through `II2cBus::transfer()` / `II2cBus::isReady()` +(`Inc/Inverter/Drivers/I2C/I2cBus.h`), which is what makes this possible. + +## Assumptions made (verify on real hardware) + +- TMP1075 `DEVICE_ID` = `0x7500`; TMP102/PCT2075 read `0x0000` at pointer 0x0F. +- INA226 die ID `0x2260`, INA3221 die ID `0x3220`, INA228 device ID bits + [15:4] = `0x228` (rev bits ignored). +- INA226 internal transfer: `CURRENT = SHUNT*CAL/2048`, + `POWER = CURRENT*BUS/20000` — used by the mock, matching the datasheet. +- INA228 register fields are 20-bit, left-aligned in 24-bit registers + (consistent with the Adafruit INA228 driver). +- PF0/PF1 = I2C5 on AF6 (see top of file). +- 100 kHz standard-mode timing from `0x60404E72` at 137.5 MHz kernel clock: + tLOW ≈ 5.8 µs, tHIGH ≈ 4.0 µs. diff --git a/Images/Gen7FW/.rte-gen7/generated/dump-main.sh b/Images/Gen7FW/.rte-gen7/generated/dump-main.sh new file mode 100755 index 00000000..ad8137b8 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/dump-main.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Dump the MainProcessor flash image over UART through the CoProcessor bridge. +# The CoProcessor enters BRIDGE mode, which drives the Main MCU BOOT0/NRST +# lines and then transparently forwards bytes to the STM32H7 ROM bootloader. +# +# Usage: +# ./dump-main.sh [SERIAL_PORT] [OUTPUT_FILE] +# +# Defaults: +# SERIAL_PORT -> auto-detected CoProcessor VCP, or /dev/ttyACM0 +# OUTPUT_FILE -> backup-main-YYYYMMDD-HHMMSS.bin + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STM32_PROGRAMMER="${STM32_PROGRAMMER:-/home/tliao/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI}" + +PORT="${1:-}" +OUTPUT_FILE="${2:-}" + +# STM32H723ZGTx has 1 MiB internal Flash starting at 0x08000000 +FLASH_ADDR="0x08000000" +FLASH_SIZE="0x100000" + +# Default output file with timestamp +if [[ -z "${OUTPUT_FILE}" ]]; then + OUTPUT_FILE="${SCRIPT_DIR}/backup-main-$(date +%Y%m%d-%H%M%S).bin" +fi + +# --- Serial port auto-detection ----------------------------------------------- + +if [[ -z "${PORT}" ]]; then + if [[ -d /dev/serial/by-id ]]; then + mapfile -t CANDIDATES < <(ls -1 /dev/serial/by-id/usb-OpenVVVF_* 2>/dev/null || true) + if [[ ${#CANDIDATES[@]} -eq 1 ]]; then + PORT="${CANDIDATES[0]}" + echo "==> Auto-detected CoProcessor VCP: ${PORT}" + elif [[ ${#CANDIDATES[@]} -gt 1 ]]; then + echo "ERROR: Multiple OpenVVVF VCPs found:" >&2 + printf ' %s\n' "${CANDIDATES[@]}" >&2 + echo "Specify one explicitly as the first argument." >&2 + exit 1 + fi + fi + + if [[ -z "${PORT}" ]]; then + PORT="/dev/ttyACM0" + echo "==> Falling back to default VCP: ${PORT}" + fi +fi + +if [[ ! -e "${PORT}" ]]; then + echo "ERROR: Serial port does not exist: ${PORT}" >&2 + exit 1 +fi + +if ! command -v python3 &>/dev/null; then + echo "ERROR: python3 is required for the bootloader handshake." >&2 + exit 1 +fi + +python3 - <<'PY' +import sys +import importlib.util +if importlib.util.find_spec("serial") is None: + print("ERROR: Python 'pyserial' module is not installed.", file=sys.stderr) + print("Install it with: python3 -m pip install pyserial", file=sys.stderr) + raise SystemExit(1) +PY + +# --- Put CoProcessor into bootloader bridge mode ------------------------------ + +echo "==> Entering bootloader bridge mode on ${PORT}..." + +python3 - "${PORT}" <<'PY' +import sys +import serial +import time + +port = sys.argv[1] + +try: + ser = serial.Serial(port, baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=0.1) +except Exception as e: + print(f"ERROR: Could not open {port}: {e}", file=sys.stderr) + sys.exit(1) + +with ser: + ser.write(b'BRIDGE\r\n') + ser.flush() + + deadline = time.time() + 8.0 + ok = False + while time.time() < deadline: + try: + line = ser.readline() + except serial.SerialException: + break + if line: + text = line.decode('ascii', errors='replace').strip() + print(f" CoProcessor: {text}") + if 'BRIDGE OK' in text: + ok = True + break + if 'BOOTLOADER FAILED' in text: + print("ERROR: Coprocessor could not enter bootloader mode.", file=sys.stderr) + sys.exit(1) + + if not ok: + print("ERROR: Timeout waiting for BRIDGE OK from coprocessor.", file=sys.stderr) + sys.exit(1) + + # Drain any trailing debug text so the programmer does not see stale bytes. + time.sleep(0.2) + ser.reset_input_buffer() +PY + +echo "==> Bootloader bridge active." + +# --- Dump flash via STM32CubeProgrammer CLI ----------------------------------- + +# Note: parity must be EVEN for the STM32 ROM bootloader UART interface. +echo "==> Reading ${FLASH_SIZE} bytes from ${FLASH_ADDR} into ${OUTPUT_FILE}..." +"${STM32_PROGRAMMER}" -c port="${PORT}" br=115200 P=EVEN db=8 sb=1 \ + -u "${FLASH_ADDR}" "${FLASH_SIZE}" "${OUTPUT_FILE}" + +echo "==> Dump complete: ${OUTPUT_FILE}" +ls -lh "${OUTPUT_FILE}" + +echo "" +echo "NOTE: The CoProcessor is still in bootloader bridge mode." +echo " Reset the CoProcessor (power cycle or reset button) to return it to normal operation." diff --git a/Images/Gen7FW/.rte-gen7/generated/generated/domain_adc_isr_generated.cpp b/Images/Gen7FW/.rte-gen7/generated/generated/domain_adc_isr_generated.cpp index 993be2fb..94bcd18a 100644 --- a/Images/Gen7FW/.rte-gen7/generated/generated/domain_adc_isr_generated.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/generated/domain_adc_isr_generated.cpp @@ -2,6 +2,7 @@ #include "domain_adc_isr_generated.h" #include "platform_api.h" +#include namespace app { diff --git a/Images/Gen7FW/.rte-gen7/generated/generated/domain_app_loop_generated.cpp b/Images/Gen7FW/.rte-gen7/generated/generated/domain_app_loop_generated.cpp index a6dc5de3..dfda70c5 100644 --- a/Images/Gen7FW/.rte-gen7/generated/generated/domain_app_loop_generated.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/generated/domain_app_loop_generated.cpp @@ -2,6 +2,7 @@ #include "domain_app_loop_generated.h" #include "platform_api.h" +#include namespace app { diff --git a/Images/Gen7FW/.rte-gen7/generated/generated/domain_tim_isr_generated.cpp b/Images/Gen7FW/.rte-gen7/generated/generated/domain_tim_isr_generated.cpp index 9723c770..53df80de 100644 --- a/Images/Gen7FW/.rte-gen7/generated/generated/domain_tim_isr_generated.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/generated/domain_tim_isr_generated.cpp @@ -2,6 +2,7 @@ #include "domain_tim_isr_generated.h" #include "platform_api.h" +#include namespace app { diff --git a/Images/Gen7FW/.rte-gen7/generated/generated/domain_vsense_generated.cpp b/Images/Gen7FW/.rte-gen7/generated/generated/domain_vsense_generated.cpp index 3cafdc76..e32e6651 100644 --- a/Images/Gen7FW/.rte-gen7/generated/generated/domain_vsense_generated.cpp +++ b/Images/Gen7FW/.rte-gen7/generated/generated/domain_vsense_generated.cpp @@ -2,6 +2,7 @@ #include "domain_vsense_generated.h" #include "platform_api.h" +#include namespace app { diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/.gitignore b/Images/Gen7FW/.rte-gen7/generated/tests/host/.gitignore new file mode 100644 index 00000000..89f9ac04 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/build_and_run.sh b/Images/Gen7FW/.rte-gen7/generated/tests/host/build_and_run.sh new file mode 100755 index 00000000..55f1e4a0 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/build_and_run.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# Host-side verification for the Gen7 I2C drivers. +# +# Compiles OnboardTempSensor + RailMonitor against a mock I2C master (plain +# g++ -std=c++17, no external dependencies, no HAL) and runs the assertion +# suites. Exit code 0 = all checks passed. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +FW_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +BUILD_DIR="${SCRIPT_DIR}/out" + +CXX="${CXX:-g++}" +CXXFLAGS=( + -std=c++17 + -O2 + -g + -Wall + -Wextra + -Wconversion + -Wsign-conversion + -I"${FW_ROOT}/Inc" + -I"${SCRIPT_DIR}" +) + +mkdir -p "${BUILD_DIR}" + +SOURCES=( + "${SCRIPT_DIR}/test_main.cpp" + "${SCRIPT_DIR}/test_temp.cpp" + "${SCRIPT_DIR}/test_rail.cpp" + "${SCRIPT_DIR}/test_temp_adversarial.cpp" + "${SCRIPT_DIR}/test_rail_adversarial.cpp" + "${FW_ROOT}/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp" + "${FW_ROOT}/Src/Inverter/Drivers/I2C/RailMonitor.cpp" +) + +echo "==> Compiling host harness..." +"${CXX}" "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "${BUILD_DIR}/i2c_driver_tests" + +echo "==> Running tests..." +"${BUILD_DIR}/i2c_driver_tests" diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/mock_i2c.h b/Images/Gen7FW/.rte-gen7/generated/tests/host/mock_i2c.h new file mode 100644 index 00000000..0191a9f5 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/mock_i2c.h @@ -0,0 +1,456 @@ +#pragma once + +/** + * @brief Mock I2C master + slave device register models for host-side driver + * verification (no MCU, no HAL — substitute for real Gen7 hardware). + * + * The models implement the register-visible behavior of the candidate parts: + * - Tmp1075Model: TMP102/TMP1075/PCT2075 pointer-register family. + * TMP1075 flavor: DEVICE_ID (0x0F) = 0x7500, config POR 0x00FF (SBOS854F + * Table 7-5), no EM bit. TMP102/PCT2075 have no ID register; the generic + * flavor masks the pointer to 2 bits like the TMP102 (assumed), so a + * probe at 0x0F aliases THIGH (0x5000). Either way init() must classify + * the part as Generic and keep 12-bit decode. + * - Ina226Model: 16-bit part, MFG_ID(0xFE)=0x5449, DIE_ID(0xFF)=0x2260. + * Calibration transfer implemented per INA226 datasheet: + * CURRENT = (SHUNT * CAL) / 2048, POWER = (CURRENT * BUS) / 20000. + * - Ina3221Model: MFG_ID(0xFE)=0x5449, DIE_ID(0xFF)=0x3220, 3 channels, + * 8 mV / 40 uV LSB in bits 15:3. No CAL (current computed by firmware). + * - Ina228Model: 20-bit ADC in 24-bit registers, MFG_ID(0x3E)=0x5449, + * DEVICE_ID(0x3F)=0x2280 (id in bits 15:4 per Adafruit INA228 driver). + * CURRENT/POWER are computed from VSHUNT x the HELD SHUNT_CAL like the + * real silicon (identity: cur_code = vsh_code * 4096 / cal, verified for + * ADCRANGE=0 against the driver's cal formula), so a stale/wrong CAL or a + * stale CONFIG.ADCRANGE now perturbs readings exactly as on hardware. + * POWER is a straight 24-bit unsigned register (verified against Linux + * kernel ina238.c: 24-bit block read used unshifted; POWER_LIMIT is + * "compared against the 24-bit power register"); an earlier revision of + * this mock encoded it 20-bit left-aligned like VBUS/CURRENT — that was + * wrong and masked driver bug R1 (see test_rail_adversarial.cpp). + * ADCRANGE (CONFIG bit 4) is modelled: with ADCRANGE=1 the VSHUNT LSB is + * 78.125 nV (4x finer), so a range-0 SHUNT_CAL reads currents 4x high. + */ + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include +#include +#include +#include +#include +#include + +namespace hosttest { + +class MockDevice { +public: + virtual ~MockDevice() = default; + /** Handle write phase; data[0] is the register pointer. false = NACK. */ + virtual bool onWrite(const uint8_t* data, uint8_t len) = 0; + /** + * Whether the device ACKs the (repeated-start) read phase at the pointer + * latched by the last write phase. Default: ACK (all models). Strict + * variants override to NAK reads at unimplemented pointers. + */ + virtual bool readPhaseAcks() const { return true; } + /** Fill the read phase buffer at the current pointer. */ + virtual void onRead(uint8_t* out, uint8_t len) = 0; +}; + +class MockI2cBus : public Inverter::II2cBus { +public: + /** One completed transfer as seen by the driver (for sequence checks). + * A lone-pointer write + N-byte read in a single transfer is the only + * way in II2cBus to do "pointer write, repeated START, read" (see + * I2cBus.h); tx_len/rx_len plus tx0 let tests assert exact shapes. */ + struct Txn { + uint8_t addr; + uint8_t tx0; /**< register pointer byte (tx[0]) */ + uint8_t tx1; /**< first data byte on writes (0 if absent) */ + uint8_t tx2; /**< second data byte on writes (0 if absent) */ + uint8_t tx_len; + uint8_t rx_len; + bool ok; + }; + + void attach(uint8_t addr7, MockDevice* dev) { m_devices[addr7] = dev; } + void detach(uint8_t addr7) { m_devices.erase(addr7); } + uint32_t transferCount() const { return m_transfers; } + const std::vector& txnLog() const { return m_log; } + void clearLog() { m_log.clear(); } + + bool transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t /*timeout_ms*/) override { + ++m_transfers; + Txn t{addr7, + (tx != nullptr && tx_len > 0) ? tx[0] : static_cast(0), + (tx != nullptr && tx_len > 1) ? tx[1] : static_cast(0), + (tx != nullptr && tx_len > 2) ? tx[2] : static_cast(0), + tx_len, rx_len, false}; + auto it = m_devices.find(addr7); + if (it == m_devices.end()) { + m_log.push_back(t); + return false; /* NACK: nothing at this address */ + } + if (tx != nullptr && tx_len > 0 && !it->second->onWrite(tx, tx_len)) { + m_log.push_back(t); + return false; + } + if (rx != nullptr && rx_len > 0) { + if (!it->second->readPhaseAcks()) { + m_log.push_back(t); + return false; /* strict device NAKs the read phase */ + } + it->second->onRead(rx, rx_len); + } + t.ok = true; + m_log.push_back(t); + return true; + } + + bool isReady(uint8_t addr7, uint32_t /*timeout_ms*/) override { + return m_devices.count(addr7) != 0; + } + +private: + std::map m_devices; + std::vector m_log; + uint32_t m_transfers = 0; +}; + +/* ---------------- TMP102/TMP1075/PCT2075 ---------------- */ + +class Tmp1075Model : public MockDevice { +public: + /** + * @param has_id true: TMP1075 — DEVICE_ID 0x0F = 0x7500, 4-bit pointer + * mask, config POR 0x00FF (low "Not used" bits all read 1, + * SBOS854F Table 7-5). The real TMP1075 has NO EM bit; + * the 0x00FF POR makes bit 4 read set, which is exactly + * why the driver must not trust EM on this part. + * false: TMP102-flavor generic — 2-bit pointer mask, so a + * probe at 0x0F aliases THIGH (0x5000) instead of reading + * 0x0000 (SBOS397 pointer register: only P1/P0 used; + * masking of reserved bits assumed, not re-verified); + * config POR 0x60A0 (SBOS397, assumed — only the EM=0 bit + * matters to the driver). + */ + explicit Tmp1075Model(bool has_id, uint8_t ptr_mask = 0) + : m_ptr_mask(ptr_mask != 0 ? ptr_mask + : (has_id ? 0x0F : 0x03)) { + m_regs[0x00] = 0x1900; /* 25.0 degC */ + m_regs[0x02] = 0x4B00; /* Tlow 75 C */ + m_regs[0x03] = 0x5000; /* Thigh 80 C */ + if (has_id) { + m_regs[0x01] = 0x00FFu; /* TMP1075 config POR */ + m_regs[0x0F] = 0x7500u; /* DEVICE_ID */ + } else { + m_regs[0x01] = 0x60A0u; /* TMP102 config POR (EM=0) */ + /* no 0x0F register: the probe aliases THIGH via m_ptr_mask */ + } + } + + void setTempRaw16(uint16_t reg) { m_regs[0x00] = reg; } + void setConfig(uint16_t cfg) { m_regs[0x01] = cfg; } + uint16_t config() const { return m_regs.at(0x01); } + uint8_t pointer() const { return m_pointer; } + + /** Make reads at (masked) pointer p NAK the read phase — e.g. a strict + * part whose unimplemented pointers refuse the read instead of 0x0000 + * (assumed; datasheet silent on reserved-pointer behaviour). */ + void nakReadPointer(uint8_t p) { m_nak_reads.insert(p & m_ptr_mask); } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = static_cast(data[0] & m_ptr_mask); + if (len >= 3) { + m_regs[m_pointer] = + static_cast((data[1] << 8) | data[2]); + } + return true; + } + + bool readPhaseAcks() const override { + return m_nak_reads.count(m_pointer) == 0; + } + + void onRead(uint8_t* out, uint8_t len) override { + const uint16_t v = m_regs.count(m_pointer) ? m_regs[m_pointer] : 0; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + +private: + std::map m_regs; + std::set m_nak_reads; + uint8_t m_pointer = 0; + uint8_t m_ptr_mask; +}; + +/* ---------------- INA226 ---------------- */ + +class Ina226Model : public MockDevice { +public: + Ina226Model() { + m_regs[0x00] = 0x4127; /* config POR default */ + m_regs[0xFE] = 0x5449; /* "TI" */ + m_regs[0xFF] = 0x2260; /* die id INA226 */ + } + + /** Physical stimulus; the chip-computed current/power registers update + * whenever CAL or the stimulus changes (datasheet transfer function). */ + void setMeasurement(float bus_v, float shunt_v) { + const long shunt_raw = lround(shunt_v / 2.5e-6); + const long bus_raw = lround(bus_v / 1.25e-3); + m_shunt_raw = static_cast(static_cast(shunt_raw)); + m_bus_raw = static_cast(bus_raw); + recompute(); + } + + /** Drop-in raw register stimuli (e.g. 16-bit boundary patterns) — the + * chip-computed current/power registers follow per the datasheet transfer + * function, exactly as after setMeasurement(). */ + void setShuntRaw(uint16_t raw) { m_shunt_raw = raw; recompute(); } + void setBusRaw(uint16_t raw) { m_bus_raw = raw; recompute(); } + + uint16_t cal() const { return m_regs.at(0x05); } + uint16_t currentReg() const { return m_current_raw; } + uint16_t powerReg() const { return m_power_raw; } + + /** Override the die ID (0xFF). US-fab INA226 revs report 0x2261, so the + * driver match must mask the RID nibble (SBOS547 Table 7-15 note). */ + void setDieId(uint16_t v) { m_regs[0xFF] = v; } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = data[0]; + if (len >= 3) { + switch (m_pointer) { + case 0x00: case 0x05: case 0x06: case 0x07: + m_regs[m_pointer] = + static_cast((data[1] << 8) | data[2]); + break; + default: + break; /* result/ID regs are read-only */ + } + if (m_pointer == 0x05) { + recompute(); + } + } + return true; + } + + void onRead(uint8_t* out, uint8_t len) override { + uint16_t v = 0; + if (m_pointer == 0x01) v = m_shunt_raw; + else if (m_pointer == 0x02) v = m_bus_raw; + else if (m_pointer == 0x03) v = m_power_raw; + else if (m_pointer == 0x04) v = m_current_raw; + else if (m_regs.count(m_pointer)) v = m_regs[m_pointer]; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + +private: + void recompute() { + const uint16_t cal = m_regs.count(0x05) ? m_regs[0x05] : 0; + /* CURRENT = (SHUNT * CAL) / 2048 (datasheet eq.). */ + const int32_t cur = + static_cast(static_cast(m_shunt_raw)) * + static_cast(cal) / 2048; + m_current_raw = static_cast(static_cast(cur)); + /* POWER = (CURRENT * BUS) / 20000 (datasheet eq.). */ + const uint32_t p = + (static_cast(static_cast(cur)) * + m_bus_raw) / 20000; + m_power_raw = static_cast(p & 0xFFFFu); + } + + std::map m_regs; + uint8_t m_pointer = 0; + uint16_t m_shunt_raw = 0; + uint16_t m_bus_raw = 0; + uint16_t m_current_raw = 0; + uint16_t m_power_raw = 0; +}; + +/* ---------------- INA3221 ---------------- */ + +class Ina3221Model : public MockDevice { +public: + Ina3221Model() { + m_regs[0x00] = 0x7127; /* config POR default */ + m_regs[0xFE] = 0x5449; + m_regs[0xFF] = 0x3220; + } + + /** + * Channel 0..2 (datasheet channels 1..3). Writes the channel's measured + * registers ONLY when the channel is enabled in CONFIG — a disabled + * channel stops converting and its registers keep their old contents. + * (Channel-enable bits 14..12 verified vs Linux kernel ina3221.c, + * CHx_EN(x) = BIT(14-x); POR 0x7127 enables all three. The held-stale + * contents themselves are 'assumed': the kernel driver refuses to read + * disabled channels at all, so hardware behaviour there is unverified.) + */ + void setMeasurement(uint8_t ch, float bus_v, float shunt_v) { + if (!channelEnabled(ch)) return; + const long bus_raw = lround(bus_v / 8e-3); + const long shunt_raw = lround(shunt_v / 40e-6); + const uint8_t reg_shunt = static_cast(0x01 + 2 * ch); + const uint8_t reg_bus = static_cast(0x02 + 2 * ch); + m_regs[reg_shunt] = + static_cast(static_cast(shunt_raw) << 3); + m_regs[reg_bus] = + static_cast(static_cast(bus_raw) << 3); + } + + bool channelEnabled(uint8_t ch) const { + const uint16_t cfg = configRaw(); + return (cfg & static_cast(0x4000u >> ch)) != 0u; + } + void setConfig(uint16_t cfg) { m_regs[0x00] = cfg; } + uint16_t configRaw() const { + return m_regs.count(0x00) ? m_regs.at(0x00) : 0x7127; + } + + /** Override the die ID (0xFF) to emulate a different silicon revision + * (0x3221 &c.) and prove the driver's RID-nibble-masked match. */ + void setDieId(uint16_t v) { m_regs[0xFF] = v; } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = data[0]; + if (len >= 3 && m_pointer == 0x00) { + m_regs[0x00] = + static_cast((data[1] << 8) | data[2]); + } + return true; + } + + void onRead(uint8_t* out, uint8_t len) override { + const uint16_t v = m_regs.count(m_pointer) ? m_regs[m_pointer] : 0; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + +private: + std::map m_regs; + uint8_t m_pointer = 0; +}; + +/* ---------------- INA228 ---------------- */ + +class Ina228Model : public MockDevice { +public: + /** CONFIG bit 4 selects the high-resolution shunt range (verified via + * Linux kernel ina238.c: INA238_CONFIG_ADCRANGE = BIT(4)). */ + static constexpr uint16_t CONFIG_ADCRANGE = 0x0010u; + + explicit Ina228Model(float shunt_ohm) : m_shunt_ohm(shunt_ohm) { + /* 16-bit registers are stored MSB-aligned in the 24-bit container. */ + m_regs[0x00] = 0; /* CONFIG POR 0x0000: ADCRANGE=0 */ + m_regs[0x02] = 0x1000u << 8; /* SHUNT_CAL POR 0x1000 (assumed: same + * as the value the kernel programs) */ + m_regs[0x3E] = 0x5449u << 8; + m_regs[0x3F] = 0x2281u << 8; /* DEVID 0x228 in bits 15:4, rev 1 — + * real parts read 0x2281, forcing the + * driver to mask the revision nibble. */ + /* 24-bit result registers start at 0. */ + m_regs[0x04] = 0; m_regs[0x05] = 0; m_regs[0x06] = 0; + m_regs[0x07] = 0; m_regs[0x08] = 0; + } + + /** + * @brief Set the electrical stimulus; result registers are derived the way + * the silicon derives them: + * VSHUNT : 20-bit left-aligned, LSB 312.5 nV (ADCRANGE=0) or + * 78.125 nV (ADCRANGE=1) + * VBUS : 20-bit left-aligned, LSB 195.3125 uV + * CURRENT : 20-bit left-aligned, = VSHUNT_code * 4096 / SHUNT_CAL + * (chip computes from the held CAL, not from physics — + * algebraically identical to (Vshunt/Rshunt)/Current_LSB + * when CAL matches the driver's formula) + * POWER : 24-bit straight value, LSB = 3.2 * Current_LSB, + * = VBUS * CURRENT_code / 3.2, clamped at 0xFFFFFF + * (saturation behaviour assumed). + */ + void setMeasurement(float bus_v, float shunt_v, float die_c = 45.0f) { + const bool range4 = (configRaw16() & CONFIG_ADCRANGE) != 0; + const double vsh_lsb = range4 ? 78.125e-9 : 312.5e-9; + const long vsh_code = lround(shunt_v / vsh_lsb); + const long bus_code = lround(bus_v / 195.3125e-6); + const double cal = m_regs.count(0x02) + ? static_cast(m_regs[0x02] >> 8) + : 0.0; + const long cur_code = + (cal > 0.0) ? lround(static_cast(vsh_code) * 4096.0 / cal) + : 0; + long pow_code = lround(static_cast(bus_v) * + static_cast(cur_code) / 3.2); + if (pow_code > 0x00FFFFFFL) pow_code = 0x00FFFFFFL; /* assumed */ + const long tmp_code = lround(die_c / 7.8125e-3); + set24(0x04, vsh_code); /* VSHUNT: 20-bit left-aligned */ + set24(0x05, bus_code); /* VBUS: 20-bit left-aligned */ + /* DIETEMP: 16-bit register (7.8125 mdegC/LSB), MSB-aligned. */ + m_regs[0x06] = (static_cast(tmp_code) & 0xFFFFu) << 8; + set24(0x07, cur_code); /* CURRENT: 20-bit left-aligned */ + /* POWER: straight 24-bit value (verified vs Linux ina238.c: + * read unshifted, LSB = 3.2 * Current_LSB). */ + m_regs[0x08] = static_cast(pow_code) & 0x00FFFFFFu; + } + + uint16_t shuntCal() const { return static_cast(m_regs.at(0x02) >> 8); } + uint32_t powerRaw24() const { return m_regs.at(0x08) & 0x00FFFFFFu; } + + /** Simulate warm-boot / externally-programmed register state. */ + void setConfigRaw16(uint16_t v) { m_regs[0x00] = static_cast(v) << 8; } + uint16_t configRaw16() const { + return static_cast(m_regs.at(0x00) >> 8); + } + void setDevId(uint16_t v) { m_regs[0x3F] = static_cast(v) << 8; } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = data[0]; + if (len >= 3) { + switch (m_pointer) { + case 0x00: case 0x01: case 0x02: case 0x03: + m_regs[m_pointer] = + static_cast((data[1] << 8) | data[2]) << 8; + break; /* 16-bit registers stored MSB-aligned */ + default: + break; + } + } + return true; + } + + void onRead(uint8_t* out, uint8_t len) override { + uint32_t v = m_regs.count(m_pointer) ? m_regs[m_pointer] : 0; + /* 16-bit registers are stored MSB-aligned; reading 2 bytes returns + * the top 16 bits exactly like the 16-bit parts. */ + if (len >= 1) out[0] = static_cast(v >> 16); + if (len >= 2) out[1] = static_cast(v >> 8); + if (len >= 3) out[2] = static_cast(v); + for (uint8_t i = 3; i < len; ++i) out[i] = 0; + } + +private: + void set24(uint8_t reg, long code20) { + /* 20-bit result, left-aligned (CODE<<4) in the 24-bit register + * (VSHUNT/VBUS/CURRENT layout verified vs Linux ina238.c + * ina238_read_field_s20: >>4 then sign-extend bit 19). Overrange + * wraps in the 20-bit field (assumed; saturation not verified). */ + const int32_t c = static_cast(code20) & 0x000FFFFF; + const int32_t v = c << 4; + m_regs[reg] = static_cast(v) & 0x00FFFFFFu; + } + + std::map m_regs; + uint8_t m_pointer = 0; + float m_shunt_ohm; +}; + +} // namespace hosttest diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/test_framework.h b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_framework.h new file mode 100644 index 00000000..5413be0d --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_framework.h @@ -0,0 +1,56 @@ +#pragma once + +/** + * @brief Minimal assertion helpers for the host harness (no dependencies). + */ + +#include +#include + +namespace hosttest { + +struct Counters { + int passed = 0; + int failed = 0; + int total = 0; +}; + +inline Counters g_checks; + +inline void report(bool ok, const char* expr, const char* file, int line) { + ++g_checks.total; + if (ok) { + ++g_checks.passed; + std::printf(" PASS %s\n", expr); + } else { + ++g_checks.failed; + std::printf(" FAIL %s (%s:%d)\n", expr, file, line); + } +} + +inline void reportNear(double actual, double expect, double tol, + const char* expr, const char* file, int line) { + const bool ok = std::fabs(actual - expect) <= tol; + ++g_checks.total; + if (ok) { + ++g_checks.passed; + std::printf(" PASS %s = %.6f (expect %.6f +/- %.4g)\n", + expr, actual, expect, tol); + } else { + ++g_checks.failed; + std::printf(" FAIL %s = %.6f, expected %.6f +/- %.4g (%s:%d)\n", + expr, actual, expect, tol, file, line); + } +} + +#define CHECK(cond) \ + ::hosttest::report((cond), #cond, __FILE__, __LINE__) +#define CHECK_NEAR(actual, expect, tol) \ + ::hosttest::reportNear((actual), (expect), (tol), #actual, __FILE__, __LINE__) + +void test_temp_suite(); +void test_rail_suite(); +void test_temp_adv_suite(); +void test_rail_adv_suite(); + +} // namespace hosttest diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/test_main.cpp b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_main.cpp new file mode 100644 index 00000000..c78df535 --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_main.cpp @@ -0,0 +1,19 @@ +#include "test_framework.h" + +#include + +int main() { + std::printf("=== Gen7 I2C driver host verification ===\n"); + hosttest::test_temp_suite(); + hosttest::test_rail_suite(); + hosttest::test_temp_adv_suite(); + hosttest::test_rail_adv_suite(); + + std::printf("=== %d/%d checks passed", hosttest::g_checks.passed, + hosttest::g_checks.total); + if (hosttest::g_checks.failed != 0) { + std::printf(" (%d FAILED)", hosttest::g_checks.failed); + } + std::printf(" ===\n"); + return hosttest::g_checks.failed == 0 ? 0 : 1; +} diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/test_rail.cpp b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_rail.cpp new file mode 100644 index 00000000..e143ea9c --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_rail.cpp @@ -0,0 +1,247 @@ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/RailMonitor.h" + +#include + +using Inverter::RailMonitor; +using hosttest::Ina226Model; +using hosttest::Ina228Model; +using hosttest::Ina3221Model; +using hosttest::MockI2cBus; + +namespace { + +constexpr float RSHUNT = 0.005f; /* 5 mOhm */ +constexpr float MAX_A = 10.0f; /* mission scenario: 5 mOhm / 10 A */ + +/* ---------- INA226 pure math ---------- */ +void test_ina226_math() { + std::printf("[rail] INA226 decode/cal math\n"); + const float lsb = RailMonitor::ina226CurrentLsb(MAX_A); + CHECK_NEAR(lsb, 10.0 / 32768.0, 1e-9); + + /* CAL = 0.00512 / (Current_LSB * Rshunt) = 3355.44 truncated. */ + const uint16_t cal = RailMonitor::ina226CalValue(lsb, RSHUNT); + CHECK(cal == 3355u); + + CHECK_NEAR(RailMonitor::ina226BusVFromRaw(19200), 24.0, 1e-3); /* 1.25 mV/LSB */ + CHECK_NEAR(RailMonitor::ina226ShuntVFromRaw(10000), 0.025, 1e-7); /* 2.5 uV/LSB */ + CHECK_NEAR(RailMonitor::ina226ShuntVFromRaw(-5000), -0.0125, 1e-7); + CHECK_NEAR(RailMonitor::ina226CurrentFromRaw(16384, lsb), 5.0, 1e-3); + CHECK_NEAR(RailMonitor::ina226CurrentFromRaw(-8192, lsb), -2.5, 1e-3); + CHECK_NEAR(RailMonitor::ina226PowerFromRaw(15725, lsb), 120.0, 0.06); +} + +/* ---------- INA226 end-to-end via mock bus ---------- */ +void test_ina226_detect_and_poll() { + std::printf("[rail] INA226 detected at 0x40, 5 mOhm / 10 A\n"); + MockI2cBus bus; + Ina226Model dev; + dev.setMeasurement(24.0f, 0.025f); /* 24 V bus, 25 mV shunt -> 5 A */ + bus.attach(0x40, &dev); + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(mon.init(bus, 0x40, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::INA226); + CHECK(mon.channels() == 1); + CHECK(mon.shuntCalValue() == 3355u); + CHECK(dev.cal() == 3355u); + CHECK(mon.lastMfgId() == 0x5449u); + CHECK(mon.lastDevId() == 0x2260u); + + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + /* current reg = 10000 * 3355 / 2048 = 16381 -> 16381 * 10/32768 A */ + CHECK_NEAR(s.bus_v, 24.0, 0.002); + CHECK_NEAR(s.current_a, 4.9994, 0.005); + CHECK_NEAR(s.power_w, 119.97, 0.10); + + /* Reverse current (regenerating): -12.5 mV shunt -> -2.5 A. */ + dev.setMeasurement(24.0f, -0.0125f); + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.current_a, -2.4996, 0.005); +} + +/* ---------- INA228 pure math ---------- */ +void test_ina228_math() { + std::printf("[rail] INA228 decode/cal math\n"); + const float lsb = RailMonitor::ina228CurrentLsb(MAX_A); + CHECK_NEAR(lsb, 10.0 / 524288.0, 1e-12); + + /* SHUNT_CAL = 13107.2e6 * lsb * Rshunt = 1250 exactly at 10 A / 5 mOhm. */ + CHECK(RailMonitor::ina228ShuntCalValue(lsb, RSHUNT) == 1250u); + + /* 24 V bus: code = 24 / 195.3125 uV = 122880, reg = code << 4. */ + const uint32_t bus_reg = static_cast(122880) << 4; + CHECK_NEAR(RailMonitor::ina228BusVFromRaw(bus_reg), 24.0, 2e-4); + + const uint32_t cur_reg = static_cast(104858) << 4; + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(cur_reg, lsb), 2.0, 2e-5); + + /* Negative current: -1 A -> code -52429 rounds to -52429 -> two's + * complement 20-bit left-aligned in 24-bit word. */ + const int32_t code = -52429; + const uint32_t cur_reg_neg = + (static_cast(code) << 4) & 0x00FFFFFFu; + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(cur_reg_neg, lsb), -1.0, 2e-4); + + /* POWER is a straight 24-bit register, LSB = 3.2 * Current_LSB (verified + * vs Linux kernel ina238.c: read unshifted; a 20-bit-left-aligned decode + * under-reports 16x). */ + CHECK_NEAR(RailMonitor::ina228PowerFromRaw(393216u, lsb), 24.0, 5e-3); + /* Bits above bit 19 are real power data, not sign extension: */ + CHECK_NEAR(RailMonitor::ina228PowerFromRaw(0x600000u, lsb), 384.0, 0.05); + CHECK_NEAR(RailMonitor::ina228DieTempCFromRaw(5760), 45.0, 1e-3); + + /* sign extension */ + CHECK(RailMonitor::signExtend24(0x00400000) == 0x00400000); + CHECK(RailMonitor::signExtend24(0x00F00000) < 0); + CHECK(RailMonitor::signExtend24(0x00000000) == 0); +} + +/* ---------- INA228 end-to-end via mock bus ---------- */ +void test_ina228_detect_and_poll() { + std::printf("[rail] INA228 detected at 0x41, 5 mOhm / 10 A\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + bus.attach(0x41, &dev); + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(mon.init(bus, 0x41, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::INA228); + CHECK(mon.channels() == 1); + CHECK(mon.shuntCalValue() == 1250u); + CHECK(dev.shuntCal() == 1250u); + + /* The mock derives CURRENT from the held SHUNT_CAL like the silicon, so + * the CAL written by init() IS what makes this reading correct. */ + dev.setMeasurement(12.0f, 0.010f); /* 12 V, 10 mV -> 2 A, 24 W */ + + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 12.0, 2e-4); + /* cur code = 32000 * 4096 / 1250 = 104857.6 -> 104858 -> 2.0000076 A */ + CHECK_NEAR(s.current_a, 2.0, 1e-4); + /* pow code = 12 * 104858 / 3.2 = 393217.5 -> 393218 -> 24.000122 W */ + CHECK_NEAR(s.power_w, 24.0, 2e-3); + CHECK(dev.powerRaw24() == 393218u); /* straight 24-bit, NOT <<4 */ +} + +/* ---------- INA3221 pure math + end-to-end ---------- */ +void test_ina3221_math_and_poll() { + std::printf("[rail] INA3221 3-channel at 0x40, 5 mOhm\n"); + CHECK_NEAR(RailMonitor::ina3221BusVFromRaw(0x5DC0), 24.0, 1e-3); + CHECK_NEAR(RailMonitor::ina3221ShuntVFromRaw(0x1388), 0.025, 1e-7); + /* Negative shunt: -312 raw -> reg 0xF640 -> -12.48 mV. */ + CHECK_NEAR(RailMonitor::ina3221ShuntVFromRaw( + static_cast(0xF640)), -0.01248, 1e-7); + CHECK_NEAR(RailMonitor::ina3221CurrentA(0.025f, RSHUNT), 5.0, 1e-6); + + MockI2cBus bus; + Ina3221Model dev; + bus.attach(0x40, &dev); + dev.setMeasurement(0, 24.0f, 0.025f); /* ch1: 24 V, 5 A */ + dev.setMeasurement(1, 3.3f, 0.0025f); /* ch2: 3.3 V, 0.5 A */ + dev.setMeasurement(2, 5.0f, 0.0f); /* ch3: 5 V, 0 A */ + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(mon.init(bus, 0x40, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::INA3221); + CHECK(mon.channels() == 3); + CHECK(mon.shuntCalValue() == 0u); /* no CAL register on this part */ + + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 24.0, 0.005); + CHECK_NEAR(s.current_a, 5.0, 0.002); + CHECK_NEAR(s.power_w, 120.0, 0.05); + + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 3.3, 0.005); + CHECK_NEAR(s.current_a, 0.5, 0.01); + CHECK_NEAR(s.power_w, 1.65, 0.03); + + CHECK(mon.poll(2, s, 10)); + CHECK_NEAR(s.bus_v, 5.0, 0.005); + CHECK_NEAR(s.current_a, 0.0, 0.01); + + /* Out-of-range channel must fail, not wrap. */ + CHECK(!mon.poll(3, s, 10)); +} + +/* ---------- auto-detect order / negative paths ---------- */ +void test_absent() { + std::printf("[rail] absent device (NACK on probe address)\n"); + MockI2cBus bus; /* empty bus */ + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(!mon.init(bus, 0x40, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + CHECK(mon.channels() == 0); + RailMonitor::Sample s{1.0f, 1.0f, 1.0f}; + CHECK(!mon.poll(0, s, 10)); + CHECK(bus.transferCount() == 0); /* isReady answered the probe */ +} + +void test_unknown_die() { + std::printf("[rail] unknown die id -> not detected\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + /* Corrupt the die id: 0xFE/0xFF are read-only in the model, so emulate a + * different die by re-attaching a bare device with foreign IDs. */ + class ForeignTi : public hosttest::MockDevice { + public: + bool onWrite(const uint8_t* d, uint8_t) override { m_ptr = d[0]; return true; } + void onRead(uint8_t* out, uint8_t len) override { + uint16_t v = 0; + if (m_ptr == 0xFE) v = 0x5449; + if (m_ptr == 0xFF) v = 0x9999; /* some future TI part */ + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + private: + uint8_t m_ptr = 0; + } foreign; + MockI2cBus bus2; + bus2.attach(0x44, &foreign); + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(!mon.init(bus2, 0x44, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + CHECK(mon.lastMfgId() == 0x5449u); + CHECK(mon.lastDevId() == 0x9999u); +} + +void test_bad_config_rejected() { + std::printf("[rail] invalid calibration config rejected\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + RailMonitor mon; + CHECK(!mon.init(bus, 0x40, RailMonitor::Config{0.0f, MAX_A}, 20)); + CHECK(!mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, 0.0f}, 20)); + CHECK(!mon.init(bus, 0x40, RailMonitor::Config{-0.005f, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + CHECK(bus.transferCount() == 0); /* rejected before touching the bus */ +} + +} // namespace + +void hosttest::test_rail_suite() { + test_ina226_math(); + test_ina226_detect_and_poll(); + test_ina228_math(); + test_ina228_detect_and_poll(); + test_ina3221_math_and_poll(); + test_absent(); + test_unknown_die(); + test_bad_config_rejected(); +} diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/test_rail_adversarial.cpp b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_rail_adversarial.cpp new file mode 100644 index 00000000..b1e3c3dc --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_rail_adversarial.cpp @@ -0,0 +1,429 @@ +/* Adversarial coverage for RailMonitor (INA226 / INA228 / INA3221). + * + * Provenance tags (see also test_temp_adversarial.cpp): + * [V] verified against public datasheet-derived sources this session: + * Linux kernel ina2xx.c / ina238.c / ina3221.c (TI-authored or + * datasheet-derived, hardware-tested), Adafruit INA228 library. + * [A] assumed / mock-informed: mock and driver share the assumption and it + * could not be re-verified against datasheet text. + * [D] documented/characterized driver behavior. + */ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/RailMonitor.h" + +#include + +using Inverter::RailMonitor; +using hosttest::Ina226Model; +using hosttest::Ina228Model; +using hosttest::Ina3221Model; +using hosttest::MockI2cBus; + +namespace { + +constexpr float RSHUNT = 0.005f; /* 5 mOhm */ +constexpr float MAX_A = 10.0f; + +/* ---------- INA226: calibration that does NOT divide evenly ---------- */ + +void test_ina226_cal_worked_example() { + std::printf("[rail-adv] INA226 CAL: uneven + datasheet worked example\n"); + /* Task-cited worked example: with Current_LSB = 1 mA/bit and Rshunt = + * 2 mOhm the CAL register is exactly 0x0A00 = 2560 (CAL = 0.00512 / + * (LSB*R); the 0.00512 constant is corroborated by the kernel ina2xx + * fixed-CAL=2048 scheme). The "3 A / 10 mOhm" parameterization of that + * example could not be re-verified against the datasheet PDF (not + * fetchable) — provenance [A]; the formula case below is exact. */ + CHECK(RailMonitor::ina226CalValue(0.001f, 0.002f) == 0x0A00u); + + /* Same example through the driver's own convention (LSB = max/2^15): + * LSB = 91.552734375 uA -> CAL = 5592.4 -> rounds to 5592 (0x15D8), NOT + * 0x0A00 — that example number presumes a normalized LSB. [D] */ + const float lsb3a = RailMonitor::ina226CurrentLsb(3.0f); + CHECK_NEAR(lsb3a, 3.0 / 32768.0, 1e-12); + CHECK(RailMonitor::ina226CalValue(lsb3a, 0.01f) == 5592u); +} + +void test_ina226_end_to_end_uneven_cal() { + std::printf("[rail-adv] INA226 e2e, 3 A / 10 mOhm (uneven CAL)\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x42, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x42, RailMonitor::Config{0.01f, 3.0f}, 20)); + CHECK(mon.shuntCalValue() == 5592u); + CHECK(dev.cal() == 5592u); + + /* Full-scale-ish: 30 mV shunt = 3.0 A on a 10 mOhm shunt. */ + dev.setMeasurement(12.0f, 0.030f); + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 12.0, 0.002); + /* reg = 12000*5592/2048 = 32765 (chip truncates) -> 32765*3/32768 A */ + CHECK_NEAR(s.current_a, 2.9997, 5e-4); + /* reg = 32765*9600/20000 = 15727 -> 15727*25*3/32768 = 35.9959 W */ + CHECK_NEAR(s.power_w, 36.0, 0.01); +} + +/* Slightly over max_expected_a, the INA226 current register wraps past + * int16 full scale. Physical rail is +10.5 A; the driver reports a large + * NEGATIVE current. [A: two's-complement wrap is mock-informed; the real + * part at least sets MATH_OVERFLOW (OVF) in Mask/Enable — which this driver + * never reads. See report.] */ +void test_ina226_overrange_wraps_negative() { + std::printf("[rail-adv] INA226 over-max current wraps (10 mOhm? no, 5 mOhm, 10 A max)\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + + dev.setMeasurement(24.0f, 0.0525f); /* 52.5 mV -> 10.5 A > 10 A max */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + /* reg = 21000*3355/2048 = 34401 -> wraps int16 to -31135 */ + CHECK(dev.currentReg() == static_cast(-31135)); + CHECK_NEAR(s.current_a, -9.5016, 0.001); + /* nonsense-positive power follows the wrapped register */ + CHECK_NEAR(s.power_w, 251.95, 0.05); +} + +/* Raw shunt 0x8000 = -32768 codes (one LSB beyond +full-scale magnitude): + * the register math overflows twice. Physical truth: -81.92 mV = -16.38 A + * on a 5 mOhm shunt; the driver reports +3.62 A. [A: wrap behavior is + * mock-informed]. Exercises the driver's int16 path on the min pattern. */ +void test_ina226_shunt_raw_min_negative() { + std::printf("[rail-adv] INA226 shunt raw 0x8000 (min negative)\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + + dev.setShuntRaw(0x8000); + dev.setBusRaw(28800); /* 36 V */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + /* -32768*3355/2048 = -53680 -> int16 wrap 11856 */ + CHECK(dev.currentReg() == 11856u); + CHECK_NEAR(s.current_a, 3.6182, 0.001); + CHECK_NEAR(s.power_w, 130.25, 0.01); +} + +void test_ina226_cal_clamps() { + std::printf("[rail-adv] INA226 CAL register clamps\n"); + /* Tiny LSB * tiny shunt -> CAL far past 0x7FFF: driver clamps to 0x7FFF. + * [A: the "bit 15 reserved" rationale is from the driver comment; the + * clamp itself is driver design — flagged for the datasheet review.] */ + CHECK(RailMonitor::ina226CalValue(RailMonitor::ina226CurrentLsb(0.1f), + 0.0005f) == 0x7FFFu); + /* Huge LSB * shunt -> CAL rounds to 0: driver clamps up to 1 so the + * chip's current/power registers stay defined. */ + CHECK(RailMonitor::ina226CalValue(RailMonitor::ina226CurrentLsb(13107.2f), + 0.1f) == 1u); + /* Full-scale power decode headroom: 65535 * 25 * LSB. (Unreachable in + * rated use: 36 V bus * full-scale current ~ 47184 codes.) */ + CHECK_NEAR(RailMonitor::ina226PowerFromRaw( + 0xFFFFu, RailMonitor::ina226CurrentLsb(MAX_A)), + 499.99, 0.02); +} + +/* ---------- INA228: 20-bit boundaries, ADCRANGE, big power ---------- */ + +void test_ina228_20bit_boundaries() { + std::printf("[rail-adv] INA228 20-bit sign/boundary decode\n"); + const float lsb = RailMonitor::ina228CurrentLsb(MAX_A); /* 10/2^19 */ + /* Min negative code (-524288): raw 0x800000 -> exactly -max_expected_a */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x00800000u, lsb), -10.0, 1e-4); + /* Max positive code (524287): raw 0x7FFFF0 */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x007FFFF0u, lsb), 9.99998, 1e-4); + /* -1 code: raw 0xFFFFF0 — top nibble 0xF is sign extension of bit 19 */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x00FFFFF0u, lsb), + -1.90735e-5, 1e-7); + /* Reserved low nibble (bits 3:0 always 0 on the part [V: kernel comment]) + * must not leak into the code if ever set by noise: >>4 discards. */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x00000008u, lsb), 0.0, 1e-9); + /* VBUS at the 85 V common-mode ceiling: 435200 << 4 = 0x6A4000. */ + CHECK_NEAR(RailMonitor::ina228BusVFromRaw(0x006A4000u), 85.0, 1e-3); + /* [D] VBUS shares the signed-20-bit decode: a bit-19-set pattern decodes + * negative (-102.4 V). Physically unreachable (max 85 V = code 435200), + * but the symmetric decode is characterized here, same as the kernel's + * read_field_s20 path. [V] */ + CHECK_NEAR(RailMonitor::ina228BusVFromRaw(0x00800000u), -102.4, 0.01); + /* Negative die temperature: 0xE800 = -6144 * 7.8125 mC = -48 C. */ + CHECK_NEAR(RailMonitor::ina228DieTempCFromRaw(0xE800u), -48.0, 1e-3); + CHECK(RailMonitor::signExtend24(0x00FFFFFFu) == -1); + CHECK(RailMonitor::signExtend24(0x007FFFFFu) == 0x007FFFFF); +} + +/* Power needing >20 bits: 80 V * 9.5 A = 760 W -> power code 12,451,850, + * which fills bits 23:21. Regression guard for the fixed 24-bit decode — + * a 20-bit-left-aligned decode would lose this entirely. [V: Linux ina238.c + * reads POWER as straight 24-bit.] */ +void test_ina228_large_power_24bit() { + std::printf("[rail-adv] INA228 power register >20 bits\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + bus.attach(0x41, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.shuntCalValue() == 1250u); + CHECK(dev.shuntCal() == 1250u); + + dev.setMeasurement(80.0f, 0.0475f); /* 47.5 mV / 5 mOhm = 9.5 A; 760 W */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 80.0, 0.01); + CHECK_NEAR(s.current_a, 9.5001, 1e-3); + CHECK_NEAR(s.power_w, 760.0, 0.1); + CHECK(dev.powerRaw24() == 12451850u); /* 0xBDF4CA, bits above 19 set */ +} + +/* Warm-boot hazard: the part was left in ADCRANGE=1 (4x finer shunt LSB) by + * a previous run. init() now RMWs CONFIG to force ADCRANGE=0, so the + * range-0 SHUNT_CAL the driver computes stays consistent. [V: ADCRANGE bit + * position and the x4 SHUNT_CAL rule — kernel ina238.c and Adafruit INA228; + * A: the mock's chip-internal coupling model.] */ +void test_ina228_stale_adcrange() { + std::printf("[rail-adv] INA228 stale ADCRANGE=1 cleared at init\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + dev.setConfigRaw16(Ina228Model::CONFIG_ADCRANGE); /* warm-boot state */ + bus.attach(0x41, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA228); + /* init must have CLEARED ADCRANGE with a CONFIG read-modify-write. */ + CHECK((dev.configRaw16() & Ina228Model::CONFIG_ADCRANGE) == 0u); + + dev.setMeasurement(12.0f, 0.010f); /* physical: 2 A, 24 W */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.current_a, 2.0, 1e-3); /* correct after the RMW */ + CHECK_NEAR(s.power_w, 24.0, 0.02); +} + +/* Revision-nibble masking on every family's ID registers. [V: the INA228 + * mock's default 0x2281 (rev 1) already exercises one mask path; INA226 US + * fab revs report 0x2261 per SBOS547 note per parent.] */ +void test_die_id_revision_masking() { + std::printf("[rail-adv] die-ID revision nibble masking\n"); + { + MockI2cBus bus; + Ina226Model dev; + dev.setDieId(0x2261); /* silicon rev 1 */ + bus.attach(0x40, &dev); + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA226); + CHECK(mon.lastDevId() == 0x2261u); + } + { + MockI2cBus bus; + Ina3221Model dev; + dev.setDieId(0x3227); + bus.attach(0x40, &dev); + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA3221); + CHECK(mon.lastDevId() == 0x3227u); + } + { /* 0x2290: a different die in the same package style must NOT match. */ + MockI2cBus bus; + Ina228Model dev(RSHUNT); + dev.setDevId(0x2290); + bus.attach(0x41, &dev); + RailMonitor mon; + CHECK(!mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + /* The 0x3E/0x3F probe records the raw IDs on rejection too, keeping + * the header's "raw IDs remain readable" contract for unknown parts. */ + CHECK(mon.lastMfgId() == 0x5449u); + CHECK(mon.lastDevId() == 0x2290u); + } +} + +/* ---------- INA3221: disabled channels ---------- */ + +/* Driver has no channel-enable awareness: it neither reads nor writes the + * INA3221 CONFIG register, so channels() is always 3 and poll() reads a + * disabled channel's registers without complaint. [V: kernel ina3221.c + * refuses to report disabled channels (-ENODATA); the RTE driver returns + * their stale/zero contents as if live — reported.] */ +void test_ina3221_disabled_channel_reads() { + std::printf("[rail-adv] INA3221 disabled channel read-through\n"); + MockI2cBus bus; + Ina3221Model dev; + dev.setConfig(0x5127); /* CH2 (bit 13) disabled before any "ADC run" */ + bus.attach(0x40, &dev); + dev.setMeasurement(0, 24.0f, 0.025f); + dev.setMeasurement(1, 3.3f, 0.0025f); /* dropped: channel disabled */ + dev.setMeasurement(2, 5.0f, 0.0f); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA3221); + CHECK(mon.channels() == 3); /* advertised regardless of CH_EN bits [D] */ + + RailMonitor::Sample s{9.0f, 9.0f, 9.0f}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 24.0, 0.005); + /* Disabled channel: poll() SUCCEEDS but returns never-converted zeros — + * indistinguishable from a genuinely idle rail. [A: held-value vs zero + * on disabled channels unverified vs datasheet text] */ + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 0.0, 1e-6); + CHECK_NEAR(s.current_a, 0.0, 1e-6); +} + +/* Disabled AFTER a conversion: registers hold the last value, so the driver + * reports stale data as live. [A: hold behavior mock-informed] */ +void test_ina3221_disabled_channel_stale() { + std::printf("[rail-adv] INA3221 disable-after-convert returns stale\n"); + MockI2cBus bus; + Ina3221Model dev; + bus.attach(0x40, &dev); + dev.setMeasurement(1, 3.3f, 0.0025f); /* converted while enabled */ + dev.setConfig(0x5127); /* then CH2 disabled */ + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 3.3, 0.005); + CHECK_NEAR(s.current_a, 0.5, 0.01); + + /* Re-enable and convert: live data again. */ + dev.setConfig(0x7127); + dev.setMeasurement(1, 7.5f, 0.0125f); + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 7.5, 0.005); + CHECK_NEAR(s.current_a, 2.5, 0.005); +} + +/* ---------- item 5: exact bus sequences ---------- */ + +bool anyBareRead(const MockI2cBus& bus) { + for (const auto& t : bus.txnLog()) { + if (t.rx_len > 0 && t.tx_len == 0) return true; + } + return false; +} + +/* INA226: probe reads 0xFE/0xFF (pointer-write + repeated-start read in one + * transfer), then a 3-byte CAL write, then a CONFIG read to confirm the MODE + * bits are continuous (POR 0x4127 already is, so no write-back). poll reads + * BUS, CURRENT, POWER. The written CAL bytes on the wire are asserted + * MSB-first. INA226 requires the register-pointer write before each read + * [V: standard SMBus read-word; the strictness the real part enforces past + * that is not observable through II2cBus — see report]. */ +void test_ina226_bus_sequence() { + std::printf("[rail-adv] INA226 bus transaction sequence\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + + const auto& log = bus.txnLog(); + CHECK(log.size() == 7u); + CHECK(log[0].tx0 == 0xFE && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0xFF && log[1].tx_len == 1 && log[1].rx_len == 2); + CHECK(log[2].tx0 == 0x05 && log[2].tx_len == 3 && log[2].rx_len == 0); + CHECK(log[2].tx1 == 0x0D && log[2].tx2 == 0x1B); /* CAL 3355, MSB first */ + CHECK(log[3].tx0 == 0x00 && log[3].tx_len == 1 && log[3].rx_len == 2); /* CONFIG RMW read */ + CHECK(log[4].tx0 == 0x02 && log[4].rx_len == 2); /* BUS */ + CHECK(log[5].tx0 == 0x04 && log[5].rx_len == 2); /* CURRENT */ + CHECK(log[6].tx0 == 0x03 && log[6].rx_len == 2); /* POWER */ + CHECK(!anyBareRead(bus)); +} + +/* INA228: probe tries the legacy 0xFE/0xFF first (mismatch), then 0x3E/0x3F, + * writes SHUNT_CAL (1250 = 0x04E2), then reads CONFIG (0x00) to force + * ADCRANGE=0 — a write-back only happens if the bit was set (covered by + * test_ina228_stale_adcrange). poll does 3-byte reads. */ +void test_ina228_bus_sequence() { + std::printf("[rail-adv] INA228 bus transaction sequence\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + bus.attach(0x41, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + + const auto& log = bus.txnLog(); + CHECK(log.size() == 9u); + CHECK(log[0].tx0 == 0xFE && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0xFF && log[1].tx_len == 1 && log[1].rx_len == 2); + CHECK(log[2].tx0 == 0x3E && log[2].tx_len == 1 && log[2].rx_len == 2); + CHECK(log[3].tx0 == 0x3F && log[3].tx_len == 1 && log[3].rx_len == 2); + CHECK(log[4].tx0 == 0x02 && log[4].tx_len == 3 && log[4].rx_len == 0); + CHECK(log[4].tx1 == 0x04 && log[4].tx2 == 0xE2); /* SHUNT_CAL 1250 = 0x04E2 */ + CHECK(log[5].tx0 == 0x00 && log[5].rx_len == 2); /* CONFIG RMW read */ + CHECK(log[6].tx0 == 0x05 && log[6].rx_len == 3); /* VBUS 24-bit */ + CHECK(log[7].tx0 == 0x07 && log[7].rx_len == 3); /* CURRENT 24-bit */ + CHECK(log[8].tx0 == 0x08 && log[8].rx_len == 3); /* POWER 24-bit */ + /* CONFIG was at POR (ADCRANGE=0), so the RMW must NOT write it back. */ + bool wrote_config = false; + for (const auto& t : log) { + if (t.tx0 == 0x00 && t.tx_len == 3) wrote_config = true; + } + CHECK(!wrote_config); + CHECK(!anyBareRead(bus)); +} + +/* INA3221 poll order: 2-byte read of the BUS register first, then the SHUNT + * register, per channel (channel n: 0x02+2n then 0x01+2n). init() adds a + * CONFIG (0x00) read-modify-write after the ID probe: CH_EN[14:12] all set + + * MODE[2:0]=111; with the POR value 0x7127 both hold, so no write-back. [D] */ +void test_ina3221_bus_sequence() { + std::printf("[rail-adv] INA3221 bus transaction sequence\n"); + MockI2cBus bus; + Ina3221Model dev; + bus.attach(0x40, &dev); + dev.setMeasurement(1, 3.3f, 0.0025f); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(1, s, 10)); + + const auto& log = bus.txnLog(); + CHECK(log.size() == 5u); + CHECK(log[0].tx0 == 0xFE && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0xFF && log[1].tx_len == 1 && log[1].rx_len == 2); + CHECK(log[2].tx0 == 0x00 && log[2].tx_len == 1 && log[2].rx_len == 2); /* CONFIG RMW read */ + CHECK(log[3].tx0 == 0x04 && log[3].tx_len == 1 && log[3].rx_len == 2); + CHECK(log[4].tx0 == 0x03 && log[4].tx_len == 1 && log[4].rx_len == 2); + CHECK(!anyBareRead(bus)); +} +} // namespace + +void hosttest::test_rail_adv_suite() { + test_ina226_cal_worked_example(); + test_ina226_end_to_end_uneven_cal(); + test_ina226_overrange_wraps_negative(); + test_ina226_shunt_raw_min_negative(); + test_ina226_cal_clamps(); + test_ina228_20bit_boundaries(); + test_ina228_large_power_24bit(); + test_ina228_stale_adcrange(); + test_die_id_revision_masking(); + test_ina3221_disabled_channel_reads(); + test_ina3221_disabled_channel_stale(); + test_ina226_bus_sequence(); + test_ina228_bus_sequence(); + test_ina3221_bus_sequence(); +} diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/test_temp.cpp b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_temp.cpp new file mode 100644 index 00000000..45b16b9e --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_temp.cpp @@ -0,0 +1,182 @@ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" + +using Inverter::OnboardTempSensor; +using hosttest::MockI2cBus; +using hosttest::Tmp1075Model; + +namespace { + +constexpr uint8_t ADDR = 0x48; + +/* Pure decode math: TMP102-family register -> degC. */ +void test_decode_vectors() { + std::printf("[temp] register decode vectors\n"); + /* 12-bit normal mode: code = reg >> 4 (arithmetic), 0.0625 degC/LSB. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x7FF0, false), 127.9375, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x1900, false), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x0000, false), 0.0, 1e-6); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFF10, false), -0.9375, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xF900, false), -7.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x9000, false), -112.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x8000, false), -128.0, 1e-4); + + /* 13-bit extended mode (EM=1): code = reg >> 3 (arithmetic). */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x0C80, true), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x7FF8, true), 255.9375, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xF380, true), -25.0, 1e-4); + + /* EM bit extraction from a config word (EM = bit 4 of the low byte). */ + CHECK(!OnboardTempSensor::configIsExtended(0x0000)); + CHECK(OnboardTempSensor::configIsExtended(0x0010)); + CHECK(OnboardTempSensor::configIsExtended(0x0050)); /* CR1|EM */ + CHECK(!OnboardTempSensor::configIsExtended(0x1040)); /* high-byte bits */ +} + +/* Encode round-trip used by the mock to program temperatures. */ +void test_encode_roundtrip() { + std::printf("[temp] encode/decode round-trip\n"); + const float temps[] = {-40.0f, -7.0f, 0.0f, 25.0f, 85.0f, 127.9f}; + for (float t : temps) { + const uint16_t reg = OnboardTempSensor::encodeTempReg(t, false); + CHECK_NEAR(OnboardTempSensor::decodeTempC(reg, false), t, 0.0625); + } +} + +/* TMP1075 on the bus: detected, identified by DEVICE_ID 0x7500, converts. */ +void test_tmp1075_present() { + std::printf("[temp] TMP1075 present at 0x48\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Tmp1075); + CHECK(sensor.address() == ADDR); + CHECK(!sensor.extendedMode()); + + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(25.0f, false)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); + + /* Negative temperature exercise. */ + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(-7.0f, false)); + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, -7.0, 1e-3); + /* -40 C (cold-start corner). */ + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(-40.0f, false)); + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, -40.0, 0.07); +} + +/* No DEVICE_ID register -> generic family member, still converts. */ +void test_generic_present() { + std::printf("[temp] generic (TMP102/PCT2075-style) device at 0x49\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + bus.attach(0x49, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, 0x49, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(55.5f, false)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 55.5, 0.07); +} + +/* EM=1 in config -> 13-bit decode on the wire. EM is a TMP102-class-only + * feature: the real TMP1075 has no EM bit and its config PORs to 0x00FF, so + * this test must use the generic (TMP102-flavored) model. */ +void test_extended_mode() { + std::printf("[temp] extended (13-bit) mode, generic TMP102 flavor\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + bus.attach(ADDR, &dev); + dev.setConfig(0x0010); /* EM=1 */ + dev.setTempRaw16(0x0C80); /* 25.0 C in 13-bit mode */ + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + CHECK(sensor.extendedMode()); + + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); +} + +/* Real TMP1075: config PORs to 0x00FF with every "Not used" low bit reading + * 1 (SBOS854F Table 7-5), so an unconditional EM-bit (bit 4) check would + * latch a bogus 13-bit decode and report 2x the real temperature. The + * driver must ignore EM whenever the part was ID'd as TMP1075. */ +void test_tmp1075_config_por_is_not_em() { + std::printf("[temp] TMP1075 config POR 0x00FF must not engage EM\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + CHECK(dev.config() == 0x00FFu); + dev.setTempRaw16(0x1900); /* 25.0 C (12-bit is the only TMP1075 width) */ + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Tmp1075); + CHECK(!sensor.extendedMode()); /* bit 4 of 0x00FF is 1: must be ignored */ + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); /* 2x decode would read 50.0 C */ +} + +/* NACK at the address: init must fail cleanly, poll must not read. */ +void test_absent() { + std::printf("[temp] absent device (NACK)\n"); + MockI2cBus bus; + OnboardTempSensor sensor; + CHECK(!sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::None); + float t = 123.0f; + CHECK(!sensor.poll(t, 10)); + CHECK_NEAR(t, 123.0, 1e-6); /* output untouched on failure */ + /* poll() on an uninitialised sensor must not touch the bus. */ + OnboardTempSensor fresh; + CHECK(!fresh.poll(t, 10)); + /* The absent-probe used isReady only; no register transfers happened. */ + CHECK(bus.transferCount() == 0); +} + +/* Device vanishes after a successful init (connector pulled): subsequent + * polls NACK and must fail cleanly with the output untouched. */ +void test_loss_of_device() { + std::printf("[temp] device lost after init\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + + bus.detach(ADDR); + float t2 = 123.0f; + CHECK(!sensor.poll(t2, 10)); + CHECK_NEAR(t2, 123.0, 1e-6); /* failure leaves the output untouched */ +} + +} // namespace + +void hosttest::test_temp_suite() { + test_decode_vectors(); + test_encode_roundtrip(); + test_tmp1075_present(); + test_generic_present(); + test_extended_mode(); + test_tmp1075_config_por_is_not_em(); + test_absent(); + test_loss_of_device(); +} diff --git a/Images/Gen7FW/.rte-gen7/generated/tests/host/test_temp_adversarial.cpp b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_temp_adversarial.cpp new file mode 100644 index 00000000..a62b174e --- /dev/null +++ b/Images/Gen7FW/.rte-gen7/generated/tests/host/test_temp_adversarial.cpp @@ -0,0 +1,271 @@ +/* Adversarial coverage for OnboardTempSensor (TMP1075/TMP102/PCT2075 path). + * + * Focus: incorrect-but-self-consistent behavior the happy-path tests would + * NOT catch — 12/13-bit boundary patterns, low-LSB discard, EM/data-width + * mismatches, pointer-register aliasing on generic parts, reserved-pointer + * NAKs, byte order on the wire, and exact bus transaction shapes. + * + * Provenance tags: + * [V] verified against a public datasheet-derived source (Linux kernel + * drivers, vendor library notes) during this pass + * [A] assumed / mock-informed (mock encodes the same assumption as the + * driver author; could not be re-verified against datasheet text) + * [D] documented driver behavior (characterized, not necessarily a bug) + */ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" + +#include +#include + +using Inverter::OnboardTempSensor; +using hosttest::MockI2cBus; +using hosttest::Tmp1075Model; + +namespace { + +constexpr uint8_t ADDR = 0x48; + +/* Serves hard-coded wire bytes per pointer, like a logic-analyzer capture. */ +class TraceDevice : public hosttest::MockDevice { +public: + void serve(uint8_t ptr, uint8_t b0, uint8_t b1) { + m_bytes[ptr] = (static_cast(b0) << 8) | b1; + } + bool onWrite(const uint8_t* data, uint8_t /*len*/) override { + m_ptr = data[0]; + return true; + } + void onRead(uint8_t* out, uint8_t len) override { + const uint16_t v = m_bytes.count(m_ptr) ? m_bytes[m_ptr] : 0; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } +private: + std::map m_bytes; + uint8_t m_ptr = 0; +}; + +/* 12-bit boundary/odd-LSB decode vectors — computed independently of both + * mock and driver (hand arithmetic on the two's-complement left-aligned + * format). Table rows marked [A] additionally appear in the TMP102 + * datasheet temperature table; the PDF was not fetchable in this session. */ +void test_decode_12bit_boundaries() { + std::printf("[temp-adv] 12-bit boundary decode\n"); + /* 0x9600 == +150 degC is NOT representable in 12-bit (code 2400 > 2047 + * wraps to -1696); +150 requires EM=1. [A: TMP102 table] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x9600, false), -106.0, 1e-4); + /* Datasheet table row: -55 degC -> 1100 1001 0000 0000. [A] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xC900, false), -55.0, 1e-4); + /* Smallest negative step: code -1, and with garbage low nibble (0xFFFF + * must still be -0.0625, not +anything — low 4 bits are not data). */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFFF0, false), -0.0625, 1e-6); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFFFF, false), -0.0625, 1e-6); + /* 0x800F: bits below the 12-bit field must be DISCARDED, not rounded — + * SAR must give -128.0, never -127.9375. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x800F, false), -128.0, 1e-4); + /* Odd LSB patterns inside the ignored nibble must not change the code. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x190F, false), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x1900, false), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x19C0, false), 25.75, 1e-4); +} + +/* EM (13-bit) boundary vectors and mode/data-width mismatches. */ +void test_decode_13bit_boundaries() { + std::printf("[temp-adv] 13-bit (EM) boundary decode\n"); + /* +150 degC max in EM: code 2400 -> 2400<<3 = 0x4B00. [A: TMP102 table] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x4B00, true), 150.0, 1e-4); + /* -55 degC in EM: -880 -> 0xC90<<3 = 0xE480. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xE480, true), -55.0, 1e-4); + /* 13-bit span extremes. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x8000, true), -256.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFFFF, true), -0.0625, 1e-6); + + /* EM=1 applied to a 12-bit-formatted pattern (0x1900 = 25 degC 12-bit) + * DOUBLES the reading -> 50.0. [D: why the driver latches EM at init] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x1900, true), 50.0, 1e-4); + /* EM=0 applied to 13-bit-formatted data (150 degC = 0x4B00) HALVES it. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x4B00, false), 75.0, 1e-4); +} + +/* The EM extractor itself is mode-blind; per-part policy lives in init(). + * 0x00FF is the real TMP1075 config POR (SBOS854F Table 7-5 [V]) — bit 4 set. */ +void test_em_bit_extraction_corners() { + std::printf("[temp-adv] config EM-bit corners\n"); + CHECK(OnboardTempSensor::configIsExtended(0xFFFF)); + CHECK(OnboardTempSensor::configIsExtended(0x00F0)); + CHECK(!OnboardTempSensor::configIsExtended(0x00EF)); + CHECK(OnboardTempSensor::configIsExtended(0x00FF)); /* TMP1075 POR: ignored + by init() only because the part is positively ID'd — see test_temp.cpp + test_tmp1075_config_por_is_not_em. */ +} + +/* encode/decode round trips in both widths, incl. values only EM can hold. + * Plus: encoding +150 degC in 12-bit PRODUCES the ambiguous 0x9600 — the + * reg<->degC relation is not round-trippable at the boundary. [D] */ +void test_encode_decode_boundaries() { + std::printf("[temp-adv] encode/decode boundary round trips\n"); + const struct { float t; bool ext; } cases[] = { + {150.0f, true}, {-256.0f, true}, {-55.4375f, false}, {127.9375f, false}, + }; + for (const auto& c : cases) { + const uint16_t reg = OnboardTempSensor::encodeTempReg(c.t, c.ext); + CHECK_NEAR(OnboardTempSensor::decodeTempC(reg, c.ext), c.t, 1e-4); + } + CHECK(OnboardTempSensor::encodeTempReg(150.0f, false) == 0x9600u); + CHECK_NEAR(OnboardTempSensor::decodeTempC( + OnboardTempSensor::encodeTempReg(150.0f, false), false), + -106.0, 1e-4); +} + +/* TMP102-class generic part: pointer register masks to P1:P0, so the driver's + * DEVICE_ID probe at 0x0F lands on THIGH (POR 0x5000), not 0x0000. [A on the + * masking; SBOS397 pointer figure says only P1/P0 are used.] Either way the + * driver must classify Generic and convert normally. */ +void test_generic_pointer_aliasing() { + std::printf("[temp-adv] TMP102 probe-at-0x0F aliases THIGH\n"); + MockI2cBus bus; + Tmp1075Model dev(false); /* generic = TMP102 flavor, 2-bit pointer */ + bus.attach(ADDR, &dev); + + /* Direct read at pointer 0x0F returns THIGH bytes, not 0x0000: */ + uint8_t out[2] = {0, 0}; + const uint8_t ptr = 0x0F; + CHECK(bus.transfer(ADDR, &ptr, 1, out, 2, 10)); + CHECK(out[0] == 0x50 && out[1] == 0x00); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); /* 0x5000 != 0x7500 */ + CHECK(!sensor.extendedMode()); /* TMP102 config POR 0x60A0 -> EM=0 */ + dev.setTempRaw16(0x4B00); /* 75 degC, 12-bit */ + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 75.0, 1e-3); +} + +/* A stricter generic part may NAK the read phase at an unimplemented pointer + * (PCT2075-style reserved pointer). [A: datasheet silent; chosen mock + * behavior]. init() must survive and still classify Generic. */ +void test_generic_nak_on_id_probe() { + std::printf("[temp-adv] generic part NAKs reserved-pointer reads\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + dev.nakReadPointer(0x0F); /* masked to 0x03 in the model */ + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + dev.setTempRaw16(0x1900); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); +} + +/* Config register unreadable (NAK) on a generic part whose ADC is in EM: + * init() must not fail, must leave EM=0, and poll() then decodes the + * 13-bit-formatted data as 12-bit (150 degC hardware reads 75 degC). + * [D: characterized fallback; mismatched-width data is a hardware config + * error the driver cannot detect from the temp register alone.] */ +void test_config_read_failure_falls_back_to_12bit() { + std::printf("[temp-adv] config NAK -> 12-bit fallback\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + dev.nakReadPointer(0x01); + dev.setTempRaw16(0x4B00); /* 150 degC if EM were latched */ + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + CHECK(!sensor.extendedMode()); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 75.0, 1e-3); +} + +/* Real wire bytes are MSB-first (I2C convention; INA226 datasheet quote + * "All data bytes are transmitted most significant byte first" [V via + * third-party library docs]). Serve an analyzer-style capture and check the + * driver decodes MSB-first — and characterize what a byte-swapped capture + * (wiring/tooling bug) would silently produce. */ +void test_wire_byte_order() { + std::printf("[temp-adv] wire byte order (MSB first)\n"); + MockI2cBus bus; + TraceDevice dev; + dev.serve(0x0F, 0x75, 0x00); /* DEVICE_ID -> TMP1075 */ + dev.serve(0x00, 0x19, 0x80); /* 25.5 degC, MSB first */ + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Tmp1075); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.5, 1e-3); + + /* Byte-swapped capture of the same register: 0x8019 -> -127.9375 degC. + * The decode direction is pinned by this test; nothing in the driver can + * detect a swapped physical bus. [D] */ + dev.serve(0x00, 0x80, 0x19); + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, -127.9375, 1e-3); +} + +/* Exact transaction shapes. Every read is a 1-byte pointer write + N-byte + * read within ONE transfer (the II2cBus contract for "pointer write, + * repeated START, read"; HalI2cBus realizes it with HAL_I2C_Mem_Read). + * A STOP-separated sequence is not expressible through II2cBus, so asserting + * (tx_len==1 whenever rx_len>0) covers the strict part of the requirement. + * [V: read-only inspection of HalI2cBus.cpp] */ +void test_transaction_shapes() { + std::printf("[temp-adv] transaction shapes\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + + const auto& log = bus.txnLog(); + /* TMP1075: init reads ONLY DEVICE_ID (config read skipped by the EM fix), + * poll reads the temp register -> exactly 2 transfers. */ + CHECK(log.size() == 2u); + CHECK(log[0].tx0 == 0x0F && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0x00 && log[1].tx_len == 1 && log[1].rx_len == 2); + for (const auto& txn : log) { + CHECK(txn.ok); + CHECK(!(txn.rx_len > 0 && txn.tx_len == 0)); /* never a bare read */ + } + + /* Generic flavor also reads CONFIG during init -> 3 transfers. */ + MockI2cBus bus2; + Tmp1075Model dev2(false); + bus2.attach(ADDR, &dev2); + OnboardTempSensor s2; + CHECK(s2.init(bus2, ADDR, 20)); + const auto& log2 = bus2.txnLog(); + CHECK(log2.size() == 2u); + CHECK(log2[0].tx0 == 0x0F && log2[1].tx0 == 0x01); + CHECK(log2[0].tx_len == 1 && log2[0].rx_len == 2); +} + +} // namespace + +void hosttest::test_temp_adv_suite() { + test_decode_12bit_boundaries(); + test_decode_13bit_boundaries(); + test_em_bit_extraction_corners(); + test_encode_decode_boundaries(); + test_generic_pointer_aliasing(); + test_generic_nak_on_id_probe(); + test_config_read_failure_falls_back_to_12bit(); + test_wire_byte_order(); + test_transaction_shapes(); +} diff --git a/Images/Gen7FW/MainProcessor/CMakeLists.txt b/Images/Gen7FW/MainProcessor/CMakeLists.txt index a6d8b299..2fb4b043 100644 --- a/Images/Gen7FW/MainProcessor/CMakeLists.txt +++ b/Images/Gen7FW/MainProcessor/CMakeLists.txt @@ -82,6 +82,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE Src/Inverter/Command/Commands/CanCommands.cpp Src/Inverter/Command/Commands/OpenLoopCommands.cpp Src/Inverter/Command/Commands/TraceCommands.cpp + Src/Inverter/Command/Commands/I2cCommands.cpp Src/Inverter/Control/ControlSupervisor.cpp Src/Inverter/Control/OpenLoopController.cpp Src/Inverter/Control/FocControlManager.cpp @@ -131,6 +132,10 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE Src/Inverter/Drivers/Sensors/ApplicationSensors.cpp Src/Inverter/Drivers/Sensors/SpikeRecorder.cpp Src/Inverter/Drivers/Sensors/SampleScheduler.cpp + Src/Inverter/Drivers/I2C/HalI2cBus.cpp + Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp + Src/Inverter/Drivers/I2C/RailMonitor.cpp + Src/Inverter/Drivers/I2C/I2cSensors.cpp Src/Inverter/Drivers/CAN/CanBus.cpp Src/Inverter/Drivers/CAN/CanProtocolTransport.cpp Src/Inverter/Drivers/CAN/CanSession.cpp diff --git a/Images/Gen7FW/MainProcessor/Inc/Inverter/Control/FaultManager.h b/Images/Gen7FW/MainProcessor/Inc/Inverter/Control/FaultManager.h index ca1760c2..f259bcd2 100644 --- a/Images/Gen7FW/MainProcessor/Inc/Inverter/Control/FaultManager.h +++ b/Images/Gen7FW/MainProcessor/Inc/Inverter/Control/FaultManager.h @@ -51,6 +51,9 @@ enum class FaultSource : uint32_t { OvertemperatureMotor = 1u << 25, /**< Motor temperature above limit */ OvertemperatureInverter = 1u << 26, /**< Board temperature above limit */ CurrentSensorRef = 1u << 27, /**< Current-sensor reference out of window */ + OnboardOvertemperature = 1u << 28, /**< I2C5 onboard temp sensor over limit */ + RailOvervoltage = 1u << 29, /**< I2C4 rail monitor bus overvoltage */ + RailUndervoltage = 1u << 30, /**< I2C4 rail monitor bus undervoltage */ }; constexpr FaultSource operator|(FaultSource a, FaultSource b) { @@ -112,6 +115,9 @@ enum class FaultReason : uint8_t { OvertemperatureInv3, OvertemperatureMotor, SensorRefOutOfRange, + OnboardOvertemperature, + RailOvervoltage, + RailUndervoltage, Count }; @@ -238,6 +244,9 @@ class FaultManager { { FaultSource::OvertemperatureMotor, "OvertemperatureMotor", "Temperature", "motor temperature above limit", FaultSeverity::Critical }, { FaultSource::OvertemperatureInverter, "OvertemperatureInverter", "Temperature", "board temperature above limit", FaultSeverity::Critical }, { FaultSource::CurrentSensorRef, "CurrentSensorRef", "Current Sense", "current-sensor reference out of window", FaultSeverity::Warning }, + { FaultSource::OnboardOvertemperature, "OnboardOvertemperature", "Temperature", "I2C5 onboard temp sensor over limit", FaultSeverity::Warning }, + { FaultSource::RailOvervoltage, "RailOvervoltage", "Rail Monitor", "I2C4 rail bus overvoltage", FaultSeverity::Warning }, + { FaultSource::RailUndervoltage, "RailUndervoltage", "Rail Monitor", "I2C4 rail bus undervoltage", FaultSeverity::Warning }, }; }; diff --git a/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/HalI2cBus.h b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/HalI2cBus.h new file mode 100644 index 00000000..db9d9c7a --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/HalI2cBus.h @@ -0,0 +1,34 @@ +#pragma once + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include "i2c.h" + +namespace Inverter { + +/** + * @brief II2cBus backed by a HAL I2C handle (firmware builds only). + * + * Blocking polling-mode HAL calls with a caller-supplied timeout; no + * interrupts or DMA. Errors bump public counters for shell diagnostics. + */ +class HalI2cBus : public II2cBus { +public: + explicit HalI2cBus(I2C_HandleTypeDef* hi2c) : m_hi2c(hi2c) {} + + bool transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t timeout_ms) override; + bool isReady(uint8_t addr7, uint32_t timeout_ms) override; + + uint32_t errorCount() const { return m_errors; } + uint32_t timeoutCount() const { return m_timeouts; } + +private: + I2C_HandleTypeDef* m_hi2c; + uint32_t m_errors = 0; + uint32_t m_timeouts = 0; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/I2cBus.h b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/I2cBus.h new file mode 100644 index 00000000..8f4eb46b --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/I2cBus.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +namespace Inverter { + +/** + * @brief Abstract I2C master port — the single funnel for all I2C driver I/O. + * + * Every Gen7 I2C driver (on-board temp sensor, rail monitor) talks to the bus + * exclusively through this interface, so the drivers and their register + * decode/encode math can be compiled and unit-tested on the host against a + * mock bus (see tests/host/), with no HAL dependency. + * + * Transaction model matches the register-pointer devices we drive + * (TMP102-family, INA226/INA228/INA3221): + * transfer(addr, tx={reg[, data...]}, rx=n) == + * START, addr+W, tx bytes, [repeated START, addr+R, rx bytes], STOP + * A null/empty @p rx reads nothing (pure register write); @p tx always starts + * with the register pointer byte. + * + * All calls are blocking-with-timeout and must only be made from main-loop / + * shell context — never from an ISR. + */ +class II2cBus { +public: + virtual ~II2cBus() = default; + + /** + * @brief Register-pointer transaction (see class comment). + * @param addr7 7-bit slave address (0x08..0x77). + * @param tx Bytes to write; tx[0] is the register pointer. + * @param tx_len Number of bytes in @p tx (>= 1). + * @param rx Receive buffer; may be nullptr when rx_len == 0. + * @param rx_len Bytes to read after a repeated START (0 = write only). + * @param timeout_ms Per-phase bus timeout. + * @return true on success; false on NACK/bus error/timeout. + */ + virtual bool transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t timeout_ms) = 0; + + /** + * @brief Address-only presence probe (HAL_I2C_IsDeviceReady equivalent). + * @return true if the slave ACKed its address. + */ + virtual bool isReady(uint8_t addr7, uint32_t timeout_ms) = 0; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/I2cSensors.h b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/I2cSensors.h new file mode 100644 index 00000000..5db85944 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/I2cSensors.h @@ -0,0 +1,113 @@ +#pragma once + +#include "Inverter/Drivers/I2C/HalI2cBus.h" +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" +#include "Inverter/Drivers/I2C/RailMonitor.h" + +#include +#include + +namespace Inverter { + +/** + * @brief Application-level owner of the Gen7 I2C sensors. + * + * Owns the two bus objects (I2C5 = on-board temperature, I2C4 = rail + * monitor), the configurable parameters, the poll/re-probe scheduling, and + * the telemetry + warning-fault wiring. Follows the ApplicationSensors + * pattern: init() never blocks boot (short-timeout probe, one log line when + * absent), update() runs from the main loop only (never from an ISR) at + * POLL_MS cadence, and absent devices are lazily re-probed every + * REPROBE_MS. + * + * Persistent config (RteParamStore KV, `config set ` + + * `config saveall`): + * OnbTemp.Addr 7-bit temp-sensor address (default 0x48) + * OnbTemp.CritC over-temperature warning (default 85 degC) + * RailMon.Addr 7-bit rail-monitor address (default 0x40) + * RailMon.ShuntOhm shunt resistor [ohm] (default 0.005) + * RailMon.MaxA max expected current [A] (default 10) + * RailMon.OvV rail OV warning [V] (default 0 = disabled) + * RailMon.UvV rail UV warning [V] (default 0 = disabled) + * + * Telemetry keys: onb_temp_c, rail_v, rail_a, rail_w (plus rail2_v/rail2_a + * and rail3_v/rail3_a for the INA3221's extra channels). + * + * Warning faults (latched, log-only): OnboardOvertemperature, + * RailOvervoltage, RailUndervoltage. + */ +class I2cSensors { +public: + bool init(); + void update(); + void reloadConfig(); + + /* Status for the shell commands. */ + bool tempPresent() const { return m_temp.part() != OnboardTempSensor::Part::None; } + float lastTempC() const { return m_temp_c; } + float tempCritC() const { return m_temp_crit_c; } + const OnboardTempSensor& tempSensor() const { return m_temp; } + + bool railPresent() const { return m_rail.part() != RailMonitor::Part::None; } + const RailMonitor& railMonitor() const { return m_rail; } + const RailMonitor::Sample& railSample(uint8_t ch) const { return m_rail_sample[ch < RailMonitor::MAX_CHANNELS ? ch : 0]; } + + HalI2cBus& tempBus() { return m_bus_temp; } + HalI2cBus& railBus() { return m_bus_rail; } + + /* Scan a bus for the `i2cscan` shell command; counts ACKed addresses in + * 0x08..0x77 into out[0..119]. Returns the number found. */ + uint32_t scan(uint8_t bus, uint8_t* out_addrs, uint32_t max_out); + +private: + static constexpr uint32_t POLL_MS = 200; /**< 5 Hz */ + static constexpr uint32_t REPROBE_MS = 5000; /**< lazy re-probe */ + static constexpr uint32_t PROBE_TIMEOUT_MS = 20; /**< boot-blocking cap */ + static constexpr uint32_t POLL_TIMEOUT_MS = 10; + static constexpr uint32_t FAULT_SUSTAIN_MS = 500; + static constexpr float HYST_C = 5.0f; + static constexpr float HYST_V = 0.5f; + static constexpr uint8_t MAX_POLL_ERRORS = 3; /**< -> mark absent */ + + bool probeTemp(); + bool probeRail(); + void pollTemp(uint32_t now_ms); + void pollRail(uint32_t now_ms); + void evaluateFaults(uint32_t now_ms); + + HalI2cBus m_bus_temp{&hi2c5}; /**< onboard temp bus */ + HalI2cBus m_bus_rail{&hi2c4}; /**< rail monitor bus */ + OnboardTempSensor m_temp; + RailMonitor m_rail; + + float m_temp_c = NAN; + RailMonitor::Sample m_rail_sample[RailMonitor::MAX_CHANNELS] = {}; + + uint8_t m_temp_addr = 0x48; + float m_temp_crit_c = 85.0f; + uint8_t m_rail_addr = 0x40; + float m_shunt_ohm = 0.005f; + float m_rail_max_a = 10.0f; + float m_rail_ov_v = 0.0f; /**< 0 = disabled */ + float m_rail_uv_v = 0.0f; /**< 0 = disabled */ + + uint8_t m_temp_errors = 0; + uint8_t m_rail_errors = 0; + uint32_t m_last_poll_ms = 0; + uint32_t m_temp_reprobe_ms = 0; + uint32_t m_rail_reprobe_ms = 0; + + bool m_ot_cond = false, m_ot_raised = false; + uint32_t m_ot_since_ms = 0; + bool m_ov_cond = false, m_ov_raised = false; + uint32_t m_ov_since_ms = 0; + bool m_uv_cond = false, m_uv_raised = false; + uint32_t m_uv_since_ms = 0; + + bool m_initialized = false; +}; + +/** @brief Global I2C sensors instance. */ +I2cSensors& i2cSensors(); + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/OnboardTempSensor.h b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/OnboardTempSensor.h new file mode 100644 index 00000000..5066badd --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/OnboardTempSensor.h @@ -0,0 +1,94 @@ +#pragma once + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include + +namespace Inverter { + +/** + * @brief On-board digital temperature sensor (I2C5, PF0/PF1). + * + * The assembled part number is not yet known, so this driver targets the + * ubiquitous pointer-register, 12-bit, 0.0625 degC/LSB family all of which + * share register map and decode math: + * - TI TMP102 (no device-id register) + * - TI TMP1075 (DEVICE_ID reg 0x0F = 0x7500) + * - NXP PCT2075 (no device-id register) + * + * Register map (big-endian 16-bit words behind an 8-bit pointer): + * 0x00 Temperature (12-bit code in bits 15:4; 13-bit when EM=1) + * 0x01 Config (EM = bit 4 selects 13-bit extended mode) + * 0x02 Tlow, 0x03 Thigh -- unused here, kept for the alert pin feature + * 0x0F DEVICE_ID (TMP1075 only; reads 0x0000 elsewhere) + * + * init() probes with a short timeout and never blocks boot; a missing device + * leaves part() == Part::None and the caller keeps re-probing lazily. + * + * All decode/encode math is static and free of bus state so the host test + * harness (tests/host/) can verify vectors directly. + */ +class OnboardTempSensor { +public: + enum class Part : uint8_t { + None = 0, /**< not detected / not initialised */ + Generic = 1, /**< TMP102 / PCT2075-compatible (no readable ID) */ + Tmp1075 = 2, /**< DEVICE_ID 0x7500 matched */ + }; + + static constexpr uint8_t REG_TEMP = 0x00; + static constexpr uint8_t REG_CONFIG = 0x01; + static constexpr uint8_t REG_TLOW = 0x02; + static constexpr uint8_t REG_THIGH = 0x03; + static constexpr uint8_t REG_ID = 0x0F; + static constexpr uint16_t TMP1075_DEVICE_ID = 0x7500; + static constexpr float LSB_C = 0.0625f; + + /** + * @brief Probe the sensor and latch its identity. + * @return true if the address ACKed (device present, even if the ID + * register reads back 0 -> Generic); false on NACK/bus error. + */ + bool init(II2cBus& bus, uint8_t addr7, uint32_t timeout_ms); + + /** + * @brief Read the temperature register and decode it. + * @return false on bus error; @p temp_c untouched on failure. + */ + bool poll(float& temp_c, uint32_t timeout_ms) const; + + Part part() const { return m_part; } + uint8_t address() const { return m_addr; } + bool extendedMode() const { return m_extended; } + const char* partName() const; + + /* -------- pure register math (host-testable) -------- */ + + /** + * @brief Decode the 16-bit temperature register to degC. + * @param reg Register value, host byte order (hi<<8|lo). + * @param extended true when config EM=1 (13-bit, value in bits 15:3). + * + * 12-bit (EM=0): degC = arithmetic_sar(reg, 4) * 0.0625 + * 13-bit (EM=1): degC = arithmetic_sar(reg, 3) * 0.0625 + * E.g. 0x7FF0 -> 127.9375 C; 0x1900 -> 25.0 C; EM 0x0C80 -> 25.0 C. + */ + static float decodeTempC(uint16_t reg, bool extended); + + /** @brief Encode degC to a 16-bit temperature register (tests/mock). */ + static uint16_t encodeTempReg(float temp_c, bool extended); + + /** @brief true when config register value has the EM (13-bit) bit set. */ + static bool configIsExtended(uint16_t cfg) { return (cfg & 0x0010u) != 0u; } + +private: + bool readReg(uint8_t reg, uint16_t& value, uint32_t timeout_ms) const; + bool writeReg(uint8_t reg, uint16_t value, uint32_t timeout_ms) const; + + II2cBus* m_bus = nullptr; + uint8_t m_addr = 0; + Part m_part = Part::None; + bool m_extended = false; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/RailMonitor.h b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/RailMonitor.h new file mode 100644 index 00000000..8519ee3a --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Inc/Inverter/Drivers/I2C/RailMonitor.h @@ -0,0 +1,156 @@ +#pragma once + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include +#include + +namespace Inverter { + +/** + * @brief TI rail monitor on I2C4 (PD12/PF15): bus voltage + shunt current + * (+ derived power) for one monitored rail. + * + * The assembled part number is not yet known, so init() auto-detects the + * actual device with a capability/ID table, trying each candidate family: + * + * | Part | ID check | Shunt present | CAL reg | + * |---------|---------------------------------|---------------|---------| + * | INA226 | reg 0xFE=0x5449, 0xFF=0x2260 | 0x01 (16b) | 0x05 | + * | INA3221 | reg 0xFE=0x5449, 0xFF=0x3220 | 0x01/03/05 | none | + * | INA228 | reg 0x3E=0x5449, (0x3F>>4)=0x228| 0x04 (24b) | 0x02 | + * + * INA226/INA3221 probe first (they alias register space with INA228's ID + * registers); the INA228 probe only runs when the 0xFE/0xFF reads fail or + * mismatch, so a 16-bit part can never be mistaken for a 20-bit one. + * + * All calculations run from a single calibration model per part; the raw + * decode/encode math is static and host-testable (tests/host/). + */ +class RailMonitor { +public: + enum class Part : uint8_t { + None = 0, + INA226 = 1, + INA228 = 2, + INA3221 = 3, + }; + + struct Config { + float shunt_ohm; /**< shunt resistor [ohm] */ + float max_expected_a; /**< max expected rail current [A] (CAL ref) */ + }; + + struct Sample { + float bus_v; + float current_a; + float power_w; + }; + + static constexpr uint8_t MAX_CHANNELS = 3; /**< INA3221 channel count */ + + /* Shared register addresses. */ + static constexpr uint8_t REG_INA226_CONFIG = 0x00; + static constexpr uint8_t REG_INA226_SHUNT = 0x01; + static constexpr uint8_t REG_INA226_BUS = 0x02; + static constexpr uint8_t REG_INA226_POWER = 0x03; + static constexpr uint8_t REG_INA226_CURRENT = 0x04; + static constexpr uint8_t REG_INA226_CAL = 0x05; + static constexpr uint8_t REG_INA226_MASK_EN = 0x06; + /* Mask/Enable (0x06) bit 2 = OVF math overflow: shunt/current/power + * math overflowed (|current| beyond the calibrated full-scale). */ + static constexpr uint16_t INA226_OVF = 0x0004; + static constexpr uint8_t REG_INA228_CONFIG = 0x00; + /* INA228 CONFIG bit 4 = ADCRANGE (0: +/-163.84mV, 1: +/-40.96mV). */ + static constexpr uint16_t INA228_CFG_ADCRANGE = 0x0010; + /* INA226/INA3221 CONFIG mode bits [2:0] = 111: continuous shunt+bus. */ + static constexpr uint16_t INA226_CFG_MODE_CONT = 0x0007; + /* INA3221 CONFIG bits 14:12 = CH1..CH3 enable. */ + static constexpr uint16_t INA3221_CFG_CH_EN = 0x7000; + static constexpr uint8_t REG_INA_MFG_ID = 0xFE; /**< 226 + 3221 */ + static constexpr uint8_t REG_INA_DIE_ID = 0xFF; /**< 226 + 3221 */ + + static constexpr uint8_t REG_INA228_SHUNT_CAL = 0x02; + static constexpr uint8_t REG_INA228_VSHUNT = 0x04; + static constexpr uint8_t REG_INA228_VBUS = 0x05; + static constexpr uint8_t REG_INA228_DIETEMP = 0x06; + static constexpr uint8_t REG_INA228_CURRENT = 0x07; + static constexpr uint8_t REG_INA228_POWER = 0x08; + static constexpr uint8_t REG_INA228_MFG_ID = 0x3E; + static constexpr uint8_t REG_INA228_DEV_ID = 0x3F; + + /* ID-register expectations (TI manufacturer ID is ASCII "TI"). */ + static constexpr uint16_t TI_MFG_ID = 0x5449; + static constexpr uint16_t INA226_DIE_ID = 0x2260; + static constexpr uint16_t INA3221_DIE_ID = 0x3220; + static constexpr uint16_t INA228_DEV_ID_HI = 0x2280; /**< id<<4, rev=0 */ + + /** + * @brief Probe + identify + configure the rail monitor at @p addr7. + * @return true when a known part was found and configured. + * false on NACK (nothing there) or an unrecognised ID pair + * (the raw IDs remain readable via lastMfgId()/lastDevId()). + */ + bool init(II2cBus& bus, uint8_t addr7, const Config& cfg, uint32_t timeout_ms); + + /** + * @brief Read one rail's bus voltage / shunt current / derived power. + * @param channel INA3221: 0..2; single-channel parts ignore it (use 0). + * @return false on bus error or uninitialised device; out untouched. + */ + bool poll(uint8_t channel, Sample& out, uint32_t timeout_ms) const; + + Part part() const { return m_part; } + uint8_t channels() const { return (m_part == Part::INA3221) ? MAX_CHANNELS + : (m_part == Part::None ? 0 : 1); } + const char* partName() const; + uint8_t address() const { return m_addr; } + uint16_t shuntCalValue() const { return m_cal; } + uint16_t lastMfgId() const { return m_last_mfg; } + uint16_t lastDevId() const { return m_last_dev; } + float currentLsbA() const { return m_current_lsb; } + + /* -------- INA226 pure math (host-testable) -------- + * Current_LSB = MaxExpectedA / 32768; CAL = 0.00512 / (Current_LSB*Rsh) + * Bus LSB = 1.25 mV; shunt LSB = 2.5 uV; power LSB = 25 * Current_LSB. */ + static float ina226CurrentLsb(float max_expected_a); + static uint16_t ina226CalValue(float current_lsb_a, float shunt_ohm); + static float ina226BusVFromRaw(uint16_t raw) { return static_cast(raw) * 1.25e-3f; } + static float ina226ShuntVFromRaw(int16_t raw) { return static_cast(raw) * 2.5e-6f; } + static float ina226CurrentFromRaw(int16_t raw, float current_lsb_a); + static float ina226PowerFromRaw(uint16_t raw, float current_lsb_a); + + /* -------- INA228 pure math (20-bit ADC, 24-bit registers) -------- + * Current_LSB = MaxExpectedA/2^19; SHUNT_CAL = 13107.2e6 * LSB * Rsh + * (ADCRANGE=0). Results sit left-aligned in 24-bit registers; Vbus LSB + * = 195.3125 uV; power LSB = 3.2 * Current_LSB. */ + static float ina228CurrentLsb(float max_expected_a); + static uint16_t ina228ShuntCalValue(float current_lsb_a, float shunt_ohm); + static int32_t signExtend24(uint32_t raw24); + static float ina228BusVFromRaw(uint32_t raw24); + static float ina228CurrentFromRaw(uint32_t raw24, float current_lsb_a); + static float ina228PowerFromRaw(uint32_t raw24, float current_lsb_a); + static float ina228DieTempCFromRaw(uint16_t raw); + + /* -------- INA3221 pure math (no CAL: current computed in firmware) -- + * Bus LSB = 8 mV (bits 15:3); shunt LSB = 40 uV (signed, bits 15:3). */ + static float ina3221BusVFromRaw(uint16_t raw) { return static_cast(raw >> 3) * 8e-3f; } + static float ina3221ShuntVFromRaw(int16_t raw) { return static_cast(raw >> 3) * 40e-6f; } + static float ina3221CurrentA(float shunt_v, float shunt_ohm); + +private: + bool readReg16(uint8_t reg, uint16_t& value, uint32_t timeout_ms) const; + bool writeReg16(uint8_t reg, uint16_t value, uint32_t timeout_ms) const; + bool readReg24(uint8_t reg, uint32_t& value, uint32_t timeout_ms) const; + + II2cBus* m_bus = nullptr; + uint8_t m_addr = 0; + Part m_part = Part::None; + uint16_t m_cal = 0; + float m_current_lsb = 0.0f; + float m_shunt_ohm = 0.0f; + uint16_t m_last_mfg = 0; + uint16_t m_last_dev = 0; +}; + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Inc/i2c.h b/Images/Gen7FW/MainProcessor/Inc/i2c.h new file mode 100644 index 00000000..6b640bf5 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Inc/i2c.h @@ -0,0 +1,54 @@ +/* USER CODE BEGIN Header */ +/** + ****************************************************************************** + * @file i2c.h + * @brief This file contains all the function prototypes for + * the i2c.c file + ****************************************************************************** + * @attention + * + * Copyright (c) 2026 STMicroelectronics. + * All rights reserved. + * + * This software is licensed under terms that can be found in the LICENSE file + * in the root directory of this software component. + * If no LICENSE file comes with this software, it is provided AS-IS. + * + ****************************************************************************** + */ +/* USER CODE END Header */ +/* Define to prevent recursive inclusion -------------------------------------*/ +#ifndef __I2C_H__ +#define __I2C_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Includes ------------------------------------------------------------------*/ +#include "main.h" + +/* USER CODE BEGIN Includes */ + +/* USER CODE END Includes */ + +extern I2C_HandleTypeDef hi2c4; + +extern I2C_HandleTypeDef hi2c5; + +/* USER CODE BEGIN Private defines */ + +/* USER CODE END Private defines */ + +void MX_I2C4_Init(void); +void MX_I2C5_Init(void); + +/* USER CODE BEGIN Prototypes */ + +/* USER CODE END Prototypes */ + +#ifdef __cplusplus +} +#endif + +#endif /* __I2C_H__ */ diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/Command/CommandInitializer.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/Command/CommandInitializer.cpp index ac6b9781..9107cafc 100644 --- a/Images/Gen7FW/MainProcessor/Src/Inverter/Command/CommandInitializer.cpp +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/Command/CommandInitializer.cpp @@ -11,6 +11,7 @@ void registerFocCommands(CommandManager& mgr); void registerCanCommands(CommandManager& mgr); void registerOpenLoopCommands(CommandManager& mgr); void registerTraceCommands(CommandManager& mgr); +void registerI2cCommands(CommandManager& mgr); /* TIME_DOMAIN: APPLICATION_COMMAND_REGISTRATION * Registers all shell commands at boot. Commands execute in main-loop context. @@ -30,4 +31,5 @@ void initializeCommands() { registerCanCommands(mgr); registerOpenLoopCommands(mgr); registerTraceCommands(mgr); + registerI2cCommands(mgr); } diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/Command/Commands/I2cCommands.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/Command/Commands/I2cCommands.cpp new file mode 100644 index 00000000..1b44a17e --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/Command/Commands/I2cCommands.cpp @@ -0,0 +1,163 @@ +#include "Inverter/Command/CommandInterface.h" +#include "Inverter/Command/CommandContext.h" +#include "Inverter/Drivers/I2C/I2cSensors.h" +#include "Inverter/Telemetry.h" + +#include +#include +#include + +using Inverter::I2cSensors; +using Inverter::i2cSensors; + +/** + * @brief Scan an I2C bus for ACKing addresses: `i2cscan [4|5]` (default both). + * + * Intended as the FIRST bring-up command on new Gen7 hardware (see + * docs/I2C_HW_Bringup.md). Probes 0x08..0x77 with a 2 ms timeout from shell + * context; worst case roughly 250 ms per bus. + */ +class I2cScanCommand : public CommandInterface { +public: + I2cScanCommand() + : CommandInterface("i2cscan", "Scan I2C bus 4 (rail mon) / 5 (temp) for devices", + ArgSpec{"bus", "4/5", 4.0f, 5.0f, 0.0f, false, ArgSpec::INT}) {} + + void execute(const ArgValue* args, CommandContext&) override { + I2cSensors& s = i2cSensors(); + uint8_t buses[2] = {4, 5}; + uint8_t nbus = 2; + if (args[0].present) { + const int32_t b = args[0].i_val; + if (b != 4 && b != 5) { + Telemetry::printf("[SHELL] usage: i2cscan [4|5]"); + return; + } + buses[0] = static_cast(b); + nbus = 1; + } + for (uint8_t i = 0; i < nbus; ++i) { + uint8_t found[16] = {0}; + const uint32_t n = s.scan(buses[i], found, sizeof(found)); + if (n == 0) { + Telemetry::printf("[SHELL] I2C%u: no devices found", buses[i]); + continue; + } + char line[128]; + int pos = snprintf(line, sizeof(line), "[SHELL] I2C%u devices (%lu):", + buses[i], static_cast(n)); + /* Cap pos at the buffer end: snprintf returns what *would* have + * been written, which can exceed the buffer when many devices + * answer. */ + if (pos >= static_cast(sizeof(line))) { + pos = static_cast(sizeof(line)) - 1; + } + for (uint32_t d = 0; d < n && pos > 0; ++d) { + const size_t room = sizeof(line) - static_cast(pos); + const int w = snprintf(&line[pos], room, " 0x%02X", found[d]); + if (w < 0 || static_cast(w) >= room) { + break; /* buffer full: stop appending */ + } + pos += w; + } + Telemetry::printf("%s", line); + } + Telemetry::printf("[SHELL] note: expected 0x48 on I2C5 (temp), 0x40 on I2C4 (rail mon)"); + } +}; + +/** + * @brief On-board temperature sensor status: `onb_temp` / `onb_temp reload`. + */ +class OnbTempCommand : public CommandInterface { +public: + OnbTempCommand() + : CommandInterface("onb_temp", "Onboard I2C temp sensor status (arg 'reload' re-reads config)", + ArgSpec{"subcommand", "", 0.0f, 0.0f, 0.0f, false, ArgSpec::STRING}) {} + + void execute(const ArgValue* args, CommandContext&) override { + I2cSensors& s = i2cSensors(); + if (args[0].present && strcasecmp(args[0].s_val, "reload") == 0) { + s.reloadConfig(); + return; + } + const auto& t = s.tempSensor(); + if (!s.tempPresent()) { + Telemetry::printf("[SHELL] onb_temp: ABSENT @ I2C5/0x%02X (lazy re-probe every 5 s)", + t.address()); + return; + } + Telemetry::printf("[SHELL] onb_temp: %s @ I2C5/0x%02X mode=%s T=%.2f C (crit %.0f C)", + t.partName(), t.address(), + t.extendedMode() ? "13-bit" : "12-bit", + static_cast(s.lastTempC()), + static_cast(s.tempCritC())); + const uint32_t err = s.tempBus().errorCount(); + const uint32_t tmo = s.tempBus().timeoutCount(); + if (err || tmo) { + Telemetry::printf("[SHELL] bus errors=%lu timeouts=%lu", + static_cast(err), + static_cast(tmo)); + } + } +}; + +/** + * @brief Rail monitor status: `rails` / `rails reload`. + */ +class RailsCommand : public CommandInterface { +public: + RailsCommand() + : CommandInterface("rails", "I2C rail monitor status: part, V/A/W per rail (arg 'reload' re-reads config)", + ArgSpec{"subcommand", "", 0.0f, 0.0f, 0.0f, false, ArgSpec::STRING}) {} + + void execute(const ArgValue* args, CommandContext&) override { + I2cSensors& s = i2cSensors(); + if (args[0].present && strcasecmp(args[0].s_val, "reload") == 0) { + s.reloadConfig(); + return; + } + const auto& r = s.railMonitor(); + if (!s.railPresent()) { + Telemetry::printf("[SHELL] rail_mon: ABSENT @ I2C4/0x%02X (lazy re-probe every 5 s); " + "last IDs mfg=0x%04X dev=0x%04X", + r.address(), r.lastMfgId(), r.lastDevId()); + return; + } + Telemetry::printf("[SHELL] rail_mon: %s @ I2C4/0x%02X CAL=0x%04X Ilsb=%.6f A", + r.partName(), r.address(), r.shuntCalValue(), + static_cast(r.currentLsbA())); + const uint8_t nch = r.channels(); + for (uint8_t ch = 0; ch < nch && ch < Inverter::RailMonitor::MAX_CHANNELS; ++ch) { + const auto& smp = s.railSample(ch); + if (std::isfinite(smp.bus_v)) { + Telemetry::printf("[SHELL] rail%u: %.3f V %.3f A %.2f W", + ch + 1, + static_cast(smp.bus_v), + static_cast(smp.current_a), + static_cast(smp.power_w)); + } else { + Telemetry::printf("[SHELL] rail%u: no sample yet", ch + 1); + } + } + const uint32_t err = s.railBus().errorCount(); + const uint32_t tmo = s.railBus().timeoutCount(); + if (err || tmo) { + Telemetry::printf("[SHELL] bus errors=%lu timeouts=%lu", + static_cast(err), + static_cast(tmo)); + } + } +}; + +static I2cScanCommand sI2cScanCmd; +static OnbTempCommand sOnbTempCmd; +static RailsCommand sRailsCmd; + +#include "Inverter/Command/CommandManager.h" + +void registerI2cCommands(CommandManager& mgr) { + mgr.registerCommand(&sI2cScanCmd); + mgr.registerCommand(&sOnbTempCmd); + mgr.registerCommand(&sRailsCmd); +} diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/Control/FaultManager.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/Control/FaultManager.cpp index b112aa41..bf2df6bd 100644 --- a/Images/Gen7FW/MainProcessor/Src/Inverter/Control/FaultManager.cpp +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/Control/FaultManager.cpp @@ -73,6 +73,9 @@ const char* faultReasonString(FaultReason r) { case FaultReason::OvertemperatureInv3: return "inverter sensor 3 over limit"; case FaultReason::OvertemperatureMotor: return "motor over temperature limit"; case FaultReason::SensorRefOutOfRange: return "reference out of window"; + case FaultReason::OnboardOvertemperature: return "onboard temp over limit (I2C5)"; + case FaultReason::RailOvervoltage: return "rail bus overvoltage (I2C4)"; + case FaultReason::RailUndervoltage: return "rail bus undervoltage (I2C4)"; case FaultReason::Count: break; } return "unknown"; diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/HalI2cBus.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/HalI2cBus.cpp new file mode 100644 index 00000000..8c53d133 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/HalI2cBus.cpp @@ -0,0 +1,49 @@ +#include "Inverter/Drivers/I2C/HalI2cBus.h" + +namespace Inverter { + +bool HalI2cBus::transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t timeout_ms) { + if ((tx == nullptr) || (tx_len == 0)) { + return false; + } + HAL_StatusTypeDef st; + if ((rx != nullptr) && (rx_len > 0)) { + /* tx[0] = register pointer, then repeated-start read. */ + st = HAL_I2C_Mem_Read(m_hi2c, static_cast(addr7) << 1, tx[0], + I2C_MEMADD_SIZE_8BIT, rx, rx_len, timeout_ms); + } else { + /* tx = {pointer, data...}; a lone pointer byte is a valid write of + * zero data bytes (sets the device read pointer). */ + st = HAL_I2C_Mem_Write(m_hi2c, static_cast(addr7) << 1, tx[0], + I2C_MEMADD_SIZE_8BIT, + const_cast(tx + 1), + static_cast(tx_len - 1), timeout_ms); + } + if (st == HAL_OK) { + return true; + } + if (st == HAL_TIMEOUT) { + ++m_timeouts; + } else { + ++m_errors; + } + /* A failed transaction can leave the peripheral busy; drop the sticky + * error flags so the next poll starts clean. */ + __HAL_I2C_CLEAR_FLAG(m_hi2c, I2C_FLAG_AF); + return false; +} + +bool HalI2cBus::isReady(uint8_t addr7, uint32_t timeout_ms) { + const HAL_StatusTypeDef st = + HAL_I2C_IsDeviceReady(m_hi2c, static_cast(addr7) << 1, + 1U /* trials */, timeout_ms); + if (st == HAL_TIMEOUT) { + ++m_timeouts; + } + return st == HAL_OK; +} + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/I2cSensors.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/I2cSensors.cpp new file mode 100644 index 00000000..d9fd72e8 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/I2cSensors.cpp @@ -0,0 +1,275 @@ +#include "Inverter/Drivers/I2C/I2cSensors.h" + +#include "Inverter/Control/FaultManager.h" +#include "Inverter/Drivers/Storage/RteParamStore.h" +#include "Inverter/Telemetry.h" + +#include "main.h" +#include "i2c.h" + +namespace Inverter { + +namespace { +I2cSensors s_instance; +} // namespace + +I2cSensors& i2cSensors() { + return s_instance; +} + +namespace { +/* KV load mirror of ApplicationSensors::loadConfig: defaults are persisted + * on first boot so `config list` shows the tunables. */ +float loadOne(const char* key, float def, bool persist_defaults) { + float value = def; + if (!RteParamStore::isReady()) { + return def; + } + if (!RteParamStore::get(key, &value) && persist_defaults) { + RteParamStore::set(key, def); + value = def; + } + return value; +} +} // namespace + +bool I2cSensors::init() { + m_temp_c = NAN; + for (uint8_t i = 0; i < RailMonitor::MAX_CHANNELS; ++i) { + m_rail_sample[i] = {NAN, NAN, NAN}; + } + + const bool persist = true; + const float addr_t = loadOne("OnbTemp.Addr", 0x48, persist); + m_temp_crit_c = loadOne("OnbTemp.CritC", 85.0f, persist); + const float addr_r = loadOne("RailMon.Addr", 0x40, persist); + m_shunt_ohm = loadOne("RailMon.ShuntOhm", 0.005f, persist); + m_rail_max_a = loadOne("RailMon.MaxA", 10.0f, persist); + m_rail_ov_v = loadOne("RailMon.OvV", 0.0f, persist); + m_rail_uv_v = loadOne("RailMon.UvV", 0.0f, persist); + if (persist && RteParamStore::isReady()) { + RteParamStore::flush(); + } + + /* Clamp addresses to valid 7-bit range. */ + m_temp_addr = (addr_t >= 8.0f && addr_t <= 119.0f) + ? static_cast(addr_t) : 0x48; + m_rail_addr = (addr_r >= 8.0f && addr_r <= 119.0f) + ? static_cast(addr_r) : 0x40; + + const bool t_ok = probeTemp(); + const bool r_ok = probeRail(); + Telemetry::printf("[I2C] onb_temp: %s @ I2C5/0x%02X", + t_ok ? m_temp.partName() : "absent", m_temp_addr); + Telemetry::printf("[I2C] rail_mon: %s @ I2C4/0x%02X (Rsh=%.3f ohm, Imax=%.1f A)", + r_ok ? m_rail.partName() : "absent", m_rail_addr, + static_cast(m_shunt_ohm), + static_cast(m_rail_max_a)); + + m_initialized = true; + const uint32_t now = HAL_GetTick(); + m_last_poll_ms = now; + return t_ok || r_ok; +} + +bool I2cSensors::probeTemp() { + m_temp_errors = 0; + const bool ok = m_temp.init(m_bus_temp, m_temp_addr, PROBE_TIMEOUT_MS); + if (!ok) { + /* Absent devices are re-probed lazily from update(). */ + m_temp_reprobe_ms = HAL_GetTick(); + } + return ok; +} + +bool I2cSensors::probeRail() { + m_rail_errors = 0; + const RailMonitor::Config cfg{m_shunt_ohm, m_rail_max_a}; + const bool ok = m_rail.init(m_bus_rail, m_rail_addr, cfg, PROBE_TIMEOUT_MS); + if (!ok) { + m_rail_reprobe_ms = HAL_GetTick(); + } + return ok; +} + +void I2cSensors::pollTemp(uint32_t now_ms) { + (void)now_ms; + float t = NAN; + if (m_temp.poll(t, POLL_TIMEOUT_MS)) { + m_temp_errors = 0; + if (std::isfinite(t)) { + m_temp_c = t; + Telemetry::log("onb_temp_c", t); + } + return; + } + if (++m_temp_errors >= MAX_POLL_ERRORS) { + Telemetry::printf("[I2C] onb_temp lost @ 0x%02X, re-arming probe", m_temp_addr); + m_temp.init(m_bus_temp, m_temp_addr, PROBE_TIMEOUT_MS); /* resets part to None */ + m_temp_reprobe_ms = now_ms; + } +} + +void I2cSensors::pollRail(uint32_t now_ms) { + bool ok = true; + const uint8_t nch = m_rail.channels(); + for (uint8_t ch = 0; ch < nch && ch < RailMonitor::MAX_CHANNELS; ++ch) { + RailMonitor::Sample s{}; + if (m_rail.poll(ch, s, POLL_TIMEOUT_MS)) { + m_rail_sample[ch] = s; + } else { + ok = false; + } + } + if (ok && nch > 0) { + m_rail_errors = 0; + Telemetry::log("rail_v", m_rail_sample[0].bus_v); + Telemetry::log("rail_a", m_rail_sample[0].current_a); + Telemetry::log("rail_w", m_rail_sample[0].power_w); + if (nch > 1) { + Telemetry::log("rail2_v", m_rail_sample[1].bus_v); + Telemetry::log("rail2_a", m_rail_sample[1].current_a); + Telemetry::log("rail3_v", m_rail_sample[2].bus_v); + Telemetry::log("rail3_a", m_rail_sample[2].current_a); + } + return; + } + if (nch > 0 && ++m_rail_errors >= MAX_POLL_ERRORS) { + Telemetry::printf("[I2C] rail_mon lost @ 0x%02X, re-arming probe", m_rail_addr); + m_rail.init(m_bus_rail, m_rail_addr, {m_shunt_ohm, m_rail_max_a}, + PROBE_TIMEOUT_MS); /* resets part to None */ + m_rail_reprobe_ms = now_ms; + } +} + +void I2cSensors::evaluateFaults(uint32_t now_ms) { + /* Over-temperature: 5 degC hysteresis, 500 ms sustain, Warning fault. */ + if (tempPresent() && std::isfinite(m_temp_c)) { + const bool cond = m_ot_cond ? (m_temp_c > (m_temp_crit_c - HYST_C)) + : (m_temp_c > m_temp_crit_c); + if (cond && !m_ot_cond) { + m_ot_since_ms = now_ms; + } + m_ot_cond = cond; + if (!cond) { + m_ot_raised = false; + } else if (!m_ot_raised && (now_ms - m_ot_since_ms) >= FAULT_SUSTAIN_MS) { + m_ot_raised = true; + FaultManager::instance().raise(FaultSource::OnboardOvertemperature, + FaultReason::OnboardOvertemperature); + } + } else { + m_ot_cond = false; + m_ot_raised = false; + } + + /* Rail OV/UV: only when the threshold is enabled (> 0) and the channel is + * producing finite values. 0.5 V hysteresis, 500 ms sustain. */ + const float v = m_rail_sample[0].bus_v; + const bool v_ok = railPresent() && std::isfinite(v); + + auto eval = [&now_ms, this](float threshold, bool& cond, bool& raised, + uint32_t& since, FaultSource src, FaultReason reason, + bool above, float val, bool enabled) { + if (!enabled) { + cond = false; + raised = false; + return; + } + const bool c = cond ? (above ? val > (threshold - HYST_V) + : val < (threshold + HYST_V)) + : (above ? val > threshold : val < threshold); + if (c && !cond) { + since = now_ms; + } + cond = c; + if (!c) { + raised = false; + } else if (!raised && (now_ms - since) >= FAULT_SUSTAIN_MS) { + raised = true; + FaultManager::instance().raise(src, reason); + } + }; + + eval(m_rail_ov_v, m_ov_cond, m_ov_raised, m_ov_since_ms, + FaultSource::RailOvervoltage, FaultReason::RailOvervoltage, + true, v, v_ok && m_rail_ov_v > 0.0f); + eval(m_rail_uv_v, m_uv_cond, m_uv_raised, m_uv_since_ms, + FaultSource::RailUndervoltage, FaultReason::RailUndervoltage, + false, v, v_ok && m_rail_uv_v > 0.0f); +} + +uint32_t I2cSensors::scan(uint8_t bus, uint8_t* out_addrs, uint32_t max_out) { + HalI2cBus* b = (bus == 5) ? &m_bus_temp : (bus == 4) ? &m_bus_rail : nullptr; + if (b == nullptr || out_addrs == nullptr) { + return 0; + } + uint32_t found = 0; + for (uint8_t addr = 0x08; addr <= 0x77; ++addr) { + if (b->isReady(addr, 2)) { + if (found < max_out) { + out_addrs[found++] = addr; + } + } + } + return found; +} + +void I2cSensors::reloadConfig() { + /* Re-read KV without persisting (values may be shell-set), then re-probe + * both devices against the (possibly new) addresses/calibration. */ + const bool persist = false; + const float addr_t = loadOne("OnbTemp.Addr", 0x48, persist); + m_temp_crit_c = loadOne("OnbTemp.CritC", 85.0f, persist); + const float addr_r = loadOne("RailMon.Addr", 0x40, persist); + m_shunt_ohm = loadOne("RailMon.ShuntOhm", 0.005f, persist); + m_rail_max_a = loadOne("RailMon.MaxA", 10.0f, persist); + m_rail_ov_v = loadOne("RailMon.OvV", 0.0f, persist); + m_rail_uv_v = loadOne("RailMon.UvV", 0.0f, persist); + if (addr_t >= 8.0f && addr_t <= 119.0f) m_temp_addr = static_cast(addr_t); + if (addr_r >= 8.0f && addr_r <= 119.0f) m_rail_addr = static_cast(addr_r); + + const bool t_ok = probeTemp(); + const bool r_ok = probeRail(); + Telemetry::printf("[I2C] config reloaded: onb_temp=%s @ 0x%02X rail_mon=%s @ 0x%02X", + t_ok ? m_temp.partName() : "absent", m_temp_addr, + r_ok ? m_rail.partName() : "absent", m_rail_addr); +} + +void I2cSensors::update() { + if (!m_initialized) { + return; + } + const uint32_t now_ms = HAL_GetTick(); + + /* Lazy re-probe of absent devices (short timeout, at most once every + * REPROBE_MS per device — never delays boot or the main loop). */ + if (!tempPresent() && (now_ms - m_temp_reprobe_ms) >= REPROBE_MS) { + m_temp_reprobe_ms = now_ms; + if (probeTemp()) { + Telemetry::printf("[I2C] onb_temp appeared: %s @ 0x%02X", + m_temp.partName(), m_temp_addr); + } + } + if (!railPresent() && (now_ms - m_rail_reprobe_ms) >= REPROBE_MS) { + m_rail_reprobe_ms = now_ms; + if (probeRail()) { + Telemetry::printf("[I2C] rail_mon appeared: %s @ 0x%02X", + m_rail.partName(), m_rail_addr); + } + } + + if ((now_ms - m_last_poll_ms) >= POLL_MS) { + m_last_poll_ms = now_ms; + if (tempPresent()) { + pollTemp(now_ms); + } + if (railPresent()) { + pollRail(now_ms); + } + } + + evaluateFaults(now_ms); +} + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp new file mode 100644 index 00000000..bbf27dc2 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp @@ -0,0 +1,100 @@ +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" + +#include + +namespace Inverter { + +const char* OnboardTempSensor::partName() const { + switch (m_part) { + case Part::Tmp1075: return "TMP1075"; + case Part::Generic: return "TMP102/PCT2075-family"; + default: return "none"; + } +} + +float OnboardTempSensor::decodeTempC(uint16_t reg, bool extended) { + /* Arithmetic right shift of the signed 16-bit word: the temperature code + * is left-aligned, so the sign bit propagates correctly in both modes. */ + const int16_t s = static_cast(reg); + const int16_t code = extended ? static_cast(s >> 3) + : static_cast(s >> 4); + return static_cast(code) * LSB_C; +} + +uint16_t OnboardTempSensor::encodeTempReg(float temp_c, bool extended) { + const long code = lroundf(temp_c / LSB_C); + const int shift = extended ? 3 : 4; + return static_cast(static_cast( + static_cast(code) << shift)); +} + +bool OnboardTempSensor::readReg(uint8_t reg, uint16_t& value, + uint32_t timeout_ms) const { + uint8_t buf[2] = {0, 0}; + if (!m_bus->transfer(m_addr, ®, 1, buf, 2, timeout_ms)) { + return false; + } + value = static_cast((static_cast(buf[0]) << 8) | buf[1]); + return true; +} + +bool OnboardTempSensor::writeReg(uint8_t reg, uint16_t value, + uint32_t timeout_ms) const { + const uint8_t buf[3] = { + reg, + static_cast(value >> 8), + static_cast(value & 0xFFu), + }; + return m_bus->transfer(m_addr, buf, 3, nullptr, 0, timeout_ms); +} + +bool OnboardTempSensor::init(II2cBus& bus, uint8_t addr7, uint32_t timeout_ms) { + m_bus = &bus; + m_addr = addr7; + m_part = Part::None; + m_extended = false; + + if (!bus.isReady(addr7, timeout_ms)) { + return false; + } + + /* Part identification via the DEVICE_ID register. TMP1075 answers + * 0x7500; TMP102 and PCT2075 have no ID register and return 0x0000 (or + * the read fails), which we treat as the generic family member — the + * temperature decode is identical either way. */ + uint16_t id = 0; + if (readReg(REG_ID, id, timeout_ms) && (id == TMP1075_DEVICE_ID)) { + m_part = Part::Tmp1075; + } else { + m_part = Part::Generic; + } + + /* Latch the EM bit so poll() decodes the right code width — TMP102-only + * feature. The TMP1075 has NO extended mode and its config register PORs + * to 0x00FF with every low "Not used" bit reading 1, so an unconditional + * EM check on bit 4 latches a bogus 13-bit decode and reads 2x the real + * temperature (e.g. 50 degC at 25 degC hardware). Only trust EM when the + * part was NOT positively ID'd as TMP1075 (SBOS854F Table 7-5). A + * PCT2075 can't be ID'd at all — see the note there: its Conf is one + * byte, whose second byte is undefined. */ + uint16_t cfg = 0; + if (m_part != Part::Tmp1075 && readReg(REG_CONFIG, cfg, timeout_ms)) { + m_extended = configIsExtended(cfg); + } + + return true; +} + +bool OnboardTempSensor::poll(float& temp_c, uint32_t timeout_ms) const { + if ((m_bus == nullptr) || (m_part == Part::None)) { + return false; + } + uint16_t raw = 0; + if (!readReg(REG_TEMP, raw, timeout_ms)) { + return false; + } + temp_c = decodeTempC(raw, m_extended); + return true; +} + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/RailMonitor.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/RailMonitor.cpp new file mode 100644 index 00000000..713d6c8d --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/Drivers/I2C/RailMonitor.cpp @@ -0,0 +1,316 @@ +#include "Inverter/Drivers/I2C/RailMonitor.h" + +namespace Inverter { + +const char* RailMonitor::partName() const { + switch (m_part) { + case Part::INA226: return "INA226"; + case Part::INA228: return "INA228"; + case Part::INA3221: return "INA3221"; + default: return "none"; + } +} + +/* ---------------- INA226 math ---------------- */ + +float RailMonitor::ina226CurrentLsb(float max_expected_a) { + return max_expected_a / 32768.0f; +} + +uint16_t RailMonitor::ina226CalValue(float current_lsb_a, float shunt_ohm) { + /* CAL = 0.00512 / (Current_LSB * RSHUNT), rounded to the nearest 16-bit + * register value (quantisation error is +/-1 LSB either way). */ + const float denom = current_lsb_a * shunt_ohm; + if (denom <= 0.0f) { + return 0; + } + long cal = lround(0.00512 / static_cast(denom)); + if (cal > 0x7FFF) { /* CAL bit15 is reserved; clamp rather than wrap. */ + cal = 0x7FFF; + } else if (cal < 1) { + cal = 1; + } + return static_cast(cal); +} + +float RailMonitor::ina226CurrentFromRaw(int16_t raw, float current_lsb_a) { + return static_cast(raw) * current_lsb_a; +} + +float RailMonitor::ina226PowerFromRaw(uint16_t raw, float current_lsb_a) { + return static_cast(raw) * 25.0f * current_lsb_a; +} + +/* ---------------- INA228 math ---------------- */ + +float RailMonitor::ina228CurrentLsb(float max_expected_a) { + return max_expected_a / 524288.0f; /* 2^19, signed 20-bit positive range */ +} + +uint16_t RailMonitor::ina228ShuntCalValue(float current_lsb_a, float shunt_ohm) { + /* SHUNT_CAL = 13107.2e6 * Current_LSB * RSHUNT (ADCRANGE = 0), rounded + * to the nearest register value. The double intermediate keeps the + * exact mathematical result (e.g. 10 A / 5 mOhm = exactly 1250) from + * being truncated to 1249 by float rounding of the inputs. */ + const double cal = 13107.2e6 * static_cast(current_lsb_a) * + static_cast(shunt_ohm); + if (cal < 1.0) { + return 0; + } + if (cal > 0xFFFF) { + return 0xFFFF; + } + return static_cast(lround(cal)); +} + +int32_t RailMonitor::signExtend24(uint32_t raw24) { + raw24 &= 0x00FFFFFFu; + return (raw24 & 0x00800000u) != 0u + ? static_cast(raw24 | 0xFF000000u) + : static_cast(raw24); +} + +float RailMonitor::ina228BusVFromRaw(uint32_t raw24) { + /* 20-bit result left-aligned in the 24-bit register. */ + const int32_t code = signExtend24(raw24) >> 4; + return static_cast(code) * 195.3125e-6f; +} + +float RailMonitor::ina228CurrentFromRaw(uint32_t raw24, float current_lsb_a) { + const int32_t code = signExtend24(raw24) >> 4; + return static_cast(code) * current_lsb_a; +} + +float RailMonitor::ina228PowerFromRaw(uint32_t raw24, float current_lsb_a) { + /* POWER is a full 24-bit register (bits 23:0) on the INA228 — unlike + * VBUS/CURRENT, which hold 20-bit data in bits 23:4. Do not right-shift; + * a 20-bit decode would under-report power 16x. (SLYS021 §7.6.1.9.) */ + const uint32_t code = raw24 & 0x00FFFFFFu; + return static_cast(code) * 3.2f * current_lsb_a; +} + +float RailMonitor::ina228DieTempCFromRaw(uint16_t raw) { + return static_cast(static_cast(raw)) * 7.8125e-3f; +} + +/* ---------------- INA3221 math ---------------- */ + +float RailMonitor::ina3221CurrentA(float shunt_v, float shunt_ohm) { + return (shunt_ohm > 0.0f) ? (shunt_v / shunt_ohm) : 0.0f; +} + +/* ---------------- bus primitives ---------------- */ + +bool RailMonitor::readReg16(uint8_t reg, uint16_t& value, + uint32_t timeout_ms) const { + uint8_t buf[2] = {0, 0}; + if (!m_bus->transfer(m_addr, ®, 1, buf, 2, timeout_ms)) { + return false; + } + value = static_cast((static_cast(buf[0]) << 8) | buf[1]); + return true; +} + +bool RailMonitor::writeReg16(uint8_t reg, uint16_t value, + uint32_t timeout_ms) const { + const uint8_t buf[3] = { + reg, + static_cast(value >> 8), + static_cast(value & 0xFFu), + }; + return m_bus->transfer(m_addr, buf, 3, nullptr, 0, timeout_ms); +} + +bool RailMonitor::readReg24(uint8_t reg, uint32_t& value, + uint32_t timeout_ms) const { + uint8_t buf[3] = {0, 0, 0}; + if (!m_bus->transfer(m_addr, ®, 1, buf, 3, timeout_ms)) { + return false; + } + value = (static_cast(buf[0]) << 16) | + (static_cast(buf[1]) << 8) | + static_cast(buf[2]); + return true; +} + +/* ---------------- init / poll ---------------- */ + +bool RailMonitor::init(II2cBus& bus, uint8_t addr7, const Config& cfg, + uint32_t timeout_ms) { + m_bus = &bus; + m_addr = addr7; + m_part = Part::None; + m_cal = 0; + m_current_lsb = 0.0f; + m_shunt_ohm = cfg.shunt_ohm; + m_last_mfg = 0; + m_last_dev = 0; + + if (cfg.shunt_ohm <= 0.0f || cfg.max_expected_a <= 0.0f) { + return false; /* cannot calibrate without a sane shunt model */ + } + if (!bus.isReady(addr7, timeout_ms)) { + return false; + } + + /* Capability table, tried in register-collision-safe order: + * 1) INA226/INA3221 style ID registers 0xFE/0xFF. + * 2) INA228 style ID registers 0x3E/0x3F (only when 1) fails cleanly, + * since on an INA226/3221 those addresses read as 0. */ + uint16_t mfg = 0, dev = 0; + bool legacy_ok = readReg16(REG_INA_MFG_ID, mfg, timeout_ms) && + readReg16(REG_INA_DIE_ID, dev, timeout_ms); + if (legacy_ok && mfg == TI_MFG_ID) { + m_last_mfg = mfg; + m_last_dev = dev; + /* Die-ID registers are DID[15:4] + RID[3:0] (revision): US-fab INA226 + * revs report 0x2261, and treating the nibble as part of the ID + * rejects legitimate parts. Mask the RID nibble (SBOS547 Table 7-1 + * note 3 / Table 7-15; INA3221 ID register has the same layout). */ + if ((dev & 0xFFF0u) == INA226_DIE_ID) { + m_part = Part::INA226; + m_current_lsb = ina226CurrentLsb(cfg.max_expected_a); + m_cal = ina226CalValue(m_current_lsb, cfg.shunt_ohm); + /* Continuous shunt+bus, 1.1 ms conversions (POR default). The + * CAL write is what makes the current/power registers valid. + * RMW the MODE bits too: a warm boot could leave a stale + * triggered/shutdown mode behind (POR CONFIG = 0x4127). */ + uint16_t config = 0; + if (!writeReg16(REG_INA226_CAL, m_cal, timeout_ms)) { + return false; + } + if (!readReg16(REG_INA226_CONFIG, config, timeout_ms)) { + return false; + } + if ((config & INA226_CFG_MODE_CONT) != INA226_CFG_MODE_CONT) { + config = static_cast((config & ~INA226_CFG_MODE_CONT) | + INA226_CFG_MODE_CONT); + return writeReg16(REG_INA226_CONFIG, config, timeout_ms); + } + return true; + } + if ((dev & 0xFFF0u) == INA3221_DIE_ID) { + m_part = Part::INA3221; + /* RMW: enable all three channels (kernel programs CH_EN; disabled + * channels hold stale data that must never read back as valid) + * and force continuous shunt+bus mode after warm boots. */ + uint16_t config = 0; + if (!readReg16(REG_INA226_CONFIG, config, timeout_ms)) { + return false; + } + const uint16_t want = static_cast( + (config | INA3221_CFG_CH_EN | INA226_CFG_MODE_CONT)); + if (want != config) { + return writeReg16(REG_INA226_CONFIG, want, timeout_ms); + } + return true; + } + return false; /* known TI family but unsupported die */ + } + + mfg = 0; dev = 0; + const bool got228 = readReg16(REG_INA228_MFG_ID, mfg, timeout_ms) && + readReg16(REG_INA228_DEV_ID, dev, timeout_ms); + if (got228) { + /* Record the raw IDs regardless of match so a rejected probe stays + * diagnosable via lastMfgId()/lastDevId() (same contract as the + * legacy-ID branch above). */ + m_last_mfg = mfg; + m_last_dev = dev; + } + if (got228 && mfg == TI_MFG_ID && (dev & 0xFFF0u) == INA228_DEV_ID_HI) { + m_part = Part::INA228; + m_current_lsb = ina228CurrentLsb(cfg.max_expected_a); + m_cal = ina228ShuntCalValue(m_current_lsb, cfg.shunt_ohm); + /* POR defaults keep ADCRANGE=0 (+/-163.84 mV shunt swing) and + * continuous conversion of bus/shunt/temp, but a warm boot could + * leave ADCRANGE=1 behind (4x finer shunt LSB -> currents would + * read 4x high under the ADCRANGE=0 math). RMW CONFIG to force + * ADCRANGE=0; everything else is left at the running config. */ + uint16_t config228 = 0; + if (m_cal == 0 || + !writeReg16(REG_INA228_SHUNT_CAL, m_cal, timeout_ms) || + !readReg16(REG_INA228_CONFIG, config228, timeout_ms)) { + return false; + } + if ((config228 & INA228_CFG_ADCRANGE) != 0u) { + config228 = static_cast(config228 & ~INA228_CFG_ADCRANGE); + return writeReg16(REG_INA228_CONFIG, config228, timeout_ms); + } + return true; + } + return false; +} + +bool RailMonitor::poll(uint8_t channel, Sample& out, + uint32_t timeout_ms) const { + if ((m_bus == nullptr) || (m_part == Part::None)) { + return false; + } + if (channel >= channels()) { + return false; + } + + switch (m_part) { + case Part::INA226: { + uint16_t bus = 0, power = 0; + int16_t current = 0; + uint16_t cur_raw = 0; + if (!readReg16(REG_INA226_BUS, bus, timeout_ms) || + !readReg16(REG_INA226_CURRENT, cur_raw, timeout_ms) || + !readReg16(REG_INA226_POWER, power, timeout_ms)) { + return false; + } + current = static_cast(cur_raw); + /* A raw current at the rails means the chip's internal + * shunt*CAL math may have overflowed (the count wraps to the + * opposite sign when |current| exceeds the calibrated + * full-scale). Confirm via the Mask/Enable math-overflow flag + * before trusting a near-full-scale sample; the OVF read only + * costs one transaction at the rails. */ + if (current >= 32760 || current <= -32768) { + uint16_t mask_en = 0; + if (readReg16(REG_INA226_MASK_EN, mask_en, timeout_ms) && + (mask_en & INA226_OVF) != 0u) { + return false; /* math overflow: sample is a wrapped rail */ + } + } + out.bus_v = ina226BusVFromRaw(bus); + out.current_a = ina226CurrentFromRaw(current, m_current_lsb); + out.power_w = ina226PowerFromRaw(power, m_current_lsb); + return true; + } + case Part::INA228: { + uint32_t bus = 0, current = 0, power = 0; + if (!readReg24(REG_INA228_VBUS, bus, timeout_ms) || + !readReg24(REG_INA228_CURRENT, current, timeout_ms) || + !readReg24(REG_INA228_POWER, power, timeout_ms)) { + return false; + } + out.bus_v = ina228BusVFromRaw(bus); + out.current_a = ina228CurrentFromRaw(current, m_current_lsb); + out.power_w = ina228PowerFromRaw(power, m_current_lsb); + return true; + } + case Part::INA3221: { + /* Ch n (1-based in the datasheet): shunt 0x01+2n, bus 0x02+2n. */ + const uint8_t reg_shunt = static_cast(0x01 + 2 * channel); + const uint8_t reg_bus = static_cast(0x02 + 2 * channel); + uint16_t shunt_raw = 0, bus_raw = 0; + if (!readReg16(reg_bus, bus_raw, timeout_ms) || + !readReg16(reg_shunt, shunt_raw, timeout_ms)) { + return false; + } + const float shunt_v = + ina3221ShuntVFromRaw(static_cast(shunt_raw)); + out.bus_v = ina3221BusVFromRaw(static_cast(bus_raw)); + out.current_a = ina3221CurrentA(shunt_v, m_shunt_ohm); + out.power_w = out.bus_v * out.current_a; /* no power reg; derive */ + return true; + } + default: + return false; + } +} + +} // namespace Inverter diff --git a/Images/Gen7FW/MainProcessor/Src/Inverter/InverterMain.cpp b/Images/Gen7FW/MainProcessor/Src/Inverter/InverterMain.cpp index ecfe8972..e92ffab2 100644 --- a/Images/Gen7FW/MainProcessor/Src/Inverter/InverterMain.cpp +++ b/Images/Gen7FW/MainProcessor/Src/Inverter/InverterMain.cpp @@ -24,6 +24,7 @@ #include "Inverter/Drivers/Sensors/DcLinkVoltageSensor.h" #include "Inverter/Drivers/Sensors/DcLinkCurrentSensor.h" #include "Inverter/Drivers/Sensors/EncoderADC.h" +#include "Inverter/Drivers/I2C/I2cSensors.h" #include "Inverter/Drivers/CAN/CanBus.h" #include "Inverter/Drivers/CAN/CanSession.h" #include "Inverter/Drivers/CAN/FdcanFault.h" @@ -164,6 +165,11 @@ static void init() * conversions and never blocks. */ Inverter::appSensors().init(); + /* Gen7 I2C sensors (I2C5 onboard temp, I2C4 rail monitor). Probes with a + * short timeout; missing devices never block boot and are re-probed every + * 5 s from the main loop. */ + Inverter::i2cSensors().init(); + /* CAN buses (KV enables; no-op when both disabled). */ Inverter::canBus().init(); @@ -253,6 +259,9 @@ static void loop() /* Application sensors (temps/throttle): harvest + recompute; never blocks. */ Inverter::appSensors().update(); + /* I2C sensors: 5 Hz poll, lazy re-probe, Warning-fault evaluation. */ + Inverter::i2cSensors().update(); + /* CAN: drain TX queues, bus-off recovery, session heartbeat watch. */ Inverter::traceRecorder().update(); Inverter::canBus().update(); diff --git a/Images/Gen7FW/MainProcessor/Src/i2c.c b/Images/Gen7FW/MainProcessor/Src/i2c.c new file mode 100644 index 00000000..14ce7de9 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/Src/i2c.c @@ -0,0 +1,269 @@ +/* USER CODE BEGIN Header */ +/** + ****************************************************************************** + * @file i2c.c + * @brief This file provides code for the configuration + * of the I2C instances. + ****************************************************************************** + * @attention + * + * Copyright (c) 2026 STMicroelectronics. + * All rights reserved. + * + * This software is licensed under terms that can be found in the LICENSE file + * in the root directory of this software component. + * If no LICENSE file comes with this software, it is provided AS-IS. + * + ****************************************************************************** + * + * WARNING (CubeMX regen): this file is hand-maintained to match what CubeMX + * generates from STM32CubeMX.ioc for the two new Gen7 buses: + * I2C4 PD12=SCL / PF15=SDA (VOLTAGE_MONITOR_I2C_*) Ti rail monitor + * I2C5 PF0 =SDA / PF1 =SCL (ONBOARD_TEMP_SENSE_I2C_*) board temp sensor + * A CubeMX regeneration that emits its own i2c.c would duplicate these + * definitions; delete the regenerated copy (or merge it back here) and keep + * the MX_I2C4_Init()/MX_I2C5_Init() calls in main.c in exactly one place. + * + * Note: drivers are polled from the main loop with blocking HAL calls and a + * short timeout; no I2C interrupt or DMA mode is used, so no NVIC lines are + * enabled for I2C4/I2C5. + * + ****************************************************************************** + */ +/* USER CODE END Header */ +/* Includes ------------------------------------------------------------------*/ +#include "i2c.h" + +/* USER CODE BEGIN 0 */ + +/* USER CODE END 0 */ + +I2C_HandleTypeDef hi2c4; +I2C_HandleTypeDef hi2c5; + +/* I2C4 init function */ +void MX_I2C4_Init(void) +{ + + /* USER CODE BEGIN I2C4_Init 0 */ + + /* USER CODE END I2C4_Init 0 */ + + /* USER CODE BEGIN I2C4_Init 1 */ + + /* USER CODE END I2C4_Init 1 */ + hi2c4.Instance = I2C4; + /* .ioc: Timing = 0x60404E72 with the 137.5 MHz I2C kernel clock + * (-> ~100 kHz, rise/fall 100 ns per the .ioc). */ + hi2c4.Init.Timing = 0x60404E72; + hi2c4.Init.OwnAddress1 = 0; + hi2c4.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; + hi2c4.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; + hi2c4.Init.OwnAddress2 = 0; + hi2c4.Init.OwnAddress2Masks = I2C_OA2_NOMASK; + hi2c4.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; + hi2c4.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; + if (HAL_I2C_Init(&hi2c4) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Analog filter + */ + if (HAL_I2CEx_ConfigAnalogFilter(&hi2c4, I2C_ANALOGFILTER_ENABLE) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Digital filter + */ + if (HAL_I2CEx_ConfigDigitalFilter(&hi2c4, 0) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN I2C4_Init 2 */ + + /* USER CODE END I2C4_Init 2 */ + +} +/* I2C5 init function */ +void MX_I2C5_Init(void) +{ + + /* USER CODE BEGIN I2C5_Init 0 */ + + /* USER CODE END I2C5_Init 0 */ + + /* USER CODE BEGIN I2C5_Init 1 */ + + /* USER CODE END I2C5_Init 1 */ + hi2c5.Instance = I2C5; + /* .ioc: Timing = 0x60404E72 with the 137.5 MHz I2C kernel clock + * (-> ~100 kHz, rise/fall 100 ns per the .ioc). */ + hi2c5.Init.Timing = 0x60404E72; + hi2c5.Init.OwnAddress1 = 0; + hi2c5.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; + hi2c5.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; + hi2c5.Init.OwnAddress2 = 0; + hi2c5.Init.OwnAddress2Masks = I2C_OA2_NOMASK; + hi2c5.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; + hi2c5.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; + if (HAL_I2C_Init(&hi2c5) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Analog filter + */ + if (HAL_I2CEx_ConfigAnalogFilter(&hi2c5, I2C_ANALOGFILTER_ENABLE) != HAL_OK) + { + Error_Handler(); + } + + /** Configure Digital filter + */ + if (HAL_I2CEx_ConfigDigitalFilter(&hi2c5, 0) != HAL_OK) + { + Error_Handler(); + } + /* USER CODE BEGIN I2C5_Init 2 */ + + /* USER CODE END I2C5_Init 2 */ + +} + +void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle) +{ + + GPIO_InitTypeDef GPIO_InitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + if(i2cHandle->Instance==I2C4) + { + /* USER CODE BEGIN I2C4_MspInit 0 */ + + /* USER CODE END I2C4_MspInit 0 */ + + /** Initializes the peripherals clock + */ + /* Default mux (RCC_I2C4CLKSOURCE_D3PCLK1), pinned here so the 0x60404E72 + * timing constants always pair with the .ioc's 137.5 MHz kernel clock. */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_I2C4; + PeriphClkInitStruct.I2c4ClockSelection = RCC_I2C4CLKSOURCE_D3PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) + { + Error_Handler(); + } + + __HAL_RCC_GPIOD_CLK_ENABLE(); + __HAL_RCC_GPIOF_CLK_ENABLE(); + /**I2C4 GPIO Configuration + PD12 ------> I2C4_SCL + PF15 ------> I2C4_SDA + */ + GPIO_InitStruct.Pin = VOLTAGE_MONITOR_I2C_SCL_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF4_I2C4; + HAL_GPIO_Init(VOLTAGE_MONITOR_I2C_SCL_GPIO_Port, &GPIO_InitStruct); + + GPIO_InitStruct.Pin = VOLTAGE_MONITOR_I2C_SDA_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF4_I2C4; + HAL_GPIO_Init(VOLTAGE_MONITOR_I2C_SDA_GPIO_Port, &GPIO_InitStruct); + + /* I2C4 clock enable */ + __HAL_RCC_I2C4_CLK_ENABLE(); + /* USER CODE BEGIN I2C4_MspInit 1 */ + + /* USER CODE END I2C4_MspInit 1 */ + } + else if(i2cHandle->Instance==I2C5) + { + /* USER CODE BEGIN I2C5_MspInit 0 */ + + /* USER CODE END I2C5_MspInit 0 */ + + /** Initializes the peripherals clock + */ + /* Default mux (RCC_I2C1235CLKSOURCE_D2PCLK1), pinned here for the same + * reason as I2C4 above (.ioc: I2C123Freq_Value = 137.5 MHz). */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_I2C5; + PeriphClkInitStruct.I2c1235ClockSelection = RCC_I2C1235CLKSOURCE_D2PCLK1; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) + { + Error_Handler(); + } + + __HAL_RCC_GPIOF_CLK_ENABLE(); + /**I2C5 GPIO Configuration + PF0 ------> I2C5_SDA + PF1 ------> I2C5_SCL + */ + /* NOTE: AF6 (GPIO_AF6_I2C5) per the STM32H723 alternate-function table for + * PF0/PF1 when mapped to I2C5 (AF4 on these pins is I2C2). The .ioc does + * not record the AF selection, so confirm against a CubeMX regen on first + * hardware bring-up (see docs/I2C_HW_Bringup.md). */ + GPIO_InitStruct.Pin = ONBOARD_TEMP_SENSE_I2C_SDA_Pin|ONBOARD_TEMP_SENSE_I2C_SCL_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; + GPIO_InitStruct.Pull = GPIO_PULLUP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + GPIO_InitStruct.Alternate = GPIO_AF6_I2C5; + HAL_GPIO_Init(GPIOF, &GPIO_InitStruct); + + /* I2C5 clock enable */ + __HAL_RCC_I2C5_CLK_ENABLE(); + /* USER CODE BEGIN I2C5_MspInit 1 */ + + /* USER CODE END I2C5_MspInit 1 */ + } +} + +void HAL_I2C_MspDeInit(I2C_HandleTypeDef* i2cHandle) +{ + + if(i2cHandle->Instance==I2C4) + { + /* USER CODE BEGIN I2C4_MspDeInit 0 */ + + /* USER CODE END I2C4_MspDeInit 0 */ + /* Peripheral clock disable */ + __HAL_RCC_I2C4_CLK_DISABLE(); + + /**I2C4 GPIO Configuration + PD12 ------> I2C4_SCL + PF15 ------> I2C4_SDA + */ + HAL_GPIO_DeInit(VOLTAGE_MONITOR_I2C_SCL_GPIO_Port, VOLTAGE_MONITOR_I2C_SCL_Pin); + + HAL_GPIO_DeInit(VOLTAGE_MONITOR_I2C_SDA_GPIO_Port, VOLTAGE_MONITOR_I2C_SDA_Pin); + + /* USER CODE BEGIN I2C4_MspDeInit 1 */ + + /* USER CODE END I2C4_MspDeInit 1 */ + } + else if(i2cHandle->Instance==I2C5) + { + /* USER CODE BEGIN I2C5_MspDeInit 0 */ + + /* USER CODE END I2C5_MspDeInit 0 */ + /* Peripheral clock disable */ + __HAL_RCC_I2C5_CLK_DISABLE(); + + /**I2C5 GPIO Configuration + PF0 ------> I2C5_SDA + PF1 ------> I2C5_SCL + */ + HAL_GPIO_DeInit(GPIOF, ONBOARD_TEMP_SENSE_I2C_SDA_Pin|ONBOARD_TEMP_SENSE_I2C_SCL_Pin); + + /* USER CODE BEGIN I2C5_MspDeInit 1 */ + + /* USER CODE END I2C5_MspDeInit 1 */ + } +} + +/* USER CODE BEGIN 1 */ + +/* USER CODE END 1 */ diff --git a/Images/Gen7FW/MainProcessor/Src/main.c b/Images/Gen7FW/MainProcessor/Src/main.c index ee862770..9b42fdb3 100644 --- a/Images/Gen7FW/MainProcessor/Src/main.c +++ b/Images/Gen7FW/MainProcessor/Src/main.c @@ -22,6 +22,7 @@ #include "dma.h" #include "fdcan.h" #include "spi.h" +#include "i2c.h" #include "tim.h" #include "usart.h" #include "gpio.h" @@ -111,6 +112,12 @@ int main(void) MX_FDCAN2_Init(); MX_SPI4_Init(); MX_SPI2_Init(); + /* Hand-maintained (see Src/i2c.c header): the .ioc already describes I2C4 + * (PD12/PF15, VOLTAGE_MONITOR_I2C_*) and I2C5 (PF0/PF1, + * ONBOARD_TEMP_SENSE_I2C_*). A CubeMX regeneration must NOT emit its own + * MX_I2C4_Init/MX_I2C5_Init (or duplicate i2c.c) alongside these calls. */ + MX_I2C4_Init(); + MX_I2C5_Init(); /* USER CODE BEGIN 2 */ InverterMain_Run(); /* USER CODE END 2 */ diff --git a/Images/Gen7FW/MainProcessor/cmake/stm32cubemx/CMakeLists.txt b/Images/Gen7FW/MainProcessor/cmake/stm32cubemx/CMakeLists.txt index 86a183d0..83c61e0f 100644 --- a/Images/Gen7FW/MainProcessor/cmake/stm32cubemx/CMakeLists.txt +++ b/Images/Gen7FW/MainProcessor/cmake/stm32cubemx/CMakeLists.txt @@ -26,6 +26,7 @@ set(MX_Application_Src ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/dma.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/fdcan.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/spi.c + ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/i2c.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/tim.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/usart.c ${CMAKE_CURRENT_SOURCE_DIR}/../../Src/stm32h7xx_it.c diff --git a/Images/Gen7FW/MainProcessor/docs/I2C_HW_Bringup.md b/Images/Gen7FW/MainProcessor/docs/I2C_HW_Bringup.md new file mode 100644 index 00000000..656a05d9 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/docs/I2C_HW_Bringup.md @@ -0,0 +1,131 @@ +# Gen7 I2C Hardware Bring-up + +Gen7 hardware adds two I2C buses. Per the `STM32CubeMX.ioc` (ground truth): + +| Bus | Function | Pins | Kernel clock | Timing reg | Result | +|------|------------------------|-------------------------------|--------------|------------|----------| +| I2C5 | Onboard temp sensor | PF0 = SDA, PF1 = SCL | 137.5 MHz | 0x60404E72 | ~100 kHz | +| I2C4 | Voltage/rail monitor | PD12 = SCL, PF15 = SDA | 137.5 MHz | 0x60404E72 | ~100 kHz | + +Labels: `ONBOARD_TEMP_SENSE_I2C_SDA/SCL` (I2C5), `VOLTAGE_MONITOR_I2C_SCL/SDA` (I2C4). +7-bit addressing, single address mode, general call off, clock stretching +allowed. Rise/fall time budget 100 ns per the `.ioc`. `MX_I2C4_Init()` and +`MX_I2C5_Init()` live in `Src/i2c.c` (hand-maintained CubeMX-style; a CubeMX +regen must not duplicate them — see the warning banner at the top of +`Src/i2c.c` and `main.c`). + +**Bring-up caveat:** PF0/PF1 carry I2C5 on **AF6** in the driver (`AF4` on these +pins is I2C2). The `.ioc` does not record the AF selection, so confirm against +real hardware (or a CubeMX regen diff) at first power-up. + +## Expected parts + +The exact assembled part numbers are **not yet known**; the firmware +auto-detects against the real candidate families. + +### I2C5 — on-board temperature sensor (default addr 0x48, range 0x48–0x4F) + +Any of the pointer-register 12-bit family; all share register map and decode +(0.0625 °C/LSB, code in bits 15:4 of register 0x00): + +| Candidate | Distinguishing feature | +|------------|-----------------------------------------------------| +| TMP1075 | `DEVICE_ID` reg 0x0F = `0x7500` -> identified as TMP1075 | +| TMP102 | no ID register (0x0F reads 0x0000) -> "generic" | +| PCT2075 | no ID register (0x0F reads 0x0000) -> "generic" | + +Extended 13-bit mode (config `EM` bit) is decoded automatically from the +device's config register. + +### I2C4 — rail monitor (default addr 0x40, configurable) + +Bus voltage + shunt current + power. Auto-detect order: + +| Order | Part | ID check | CAL written | Channels | +|-------|---------|-------------------------------------------|--------------|----------| +| 1 | INA226 | reg 0xFE = `0x5449` ("TI"), 0xFF = `0x2260` | `CAL = 0.00512 / (Ilsb * Rsh)`, Ilsb = MaxA/2^15 | 1 | +| 2 | INA3221 | reg 0xFE = `0x5449`, 0xFF = `0x3220` | none (current computed in firmware) | 3 | +| 3 | INA228 | reg 0x3E = `0x5449`, (0x3F >> 4) = `0x228` | `SHUNT_CAL = 13107.2e6 * Ilsb * Rsh` (ADCRANGE=0), Ilsb = MaxA/2^19 | 1 | + +Known TI parts with an unrecognised die ID are *not* configured — the rails +stay absent and `rails` prints the raw mfg/dev IDs so support can be added. + +## First power-up / wiring check order + +1. `i2cscan` — scan both buses (or `i2cscan 5` / `i2cscan 4`). Expected: + `0x48` ACKs on I2C5, `0x40` ACKs on I2C4. + - Nothing at all on one bus -> check the connector/continuity and that the + expected pull-ups are populated (SDA/SCL should idle high, ~3.3 V). + - SDA and SCL both stuck low -> swapped short / solder bridge. + - In-regs garbage vs no-ACK distinguishes wiring from addressing issues. +2. `onb_temp` — should identify the part (TMP1075 vs generic) and report a + plausible board temperature. Sanity check: breathe on the sensor, + watch `onb_temp_c` rise on the telemetry stream. +3. `rails` — should identify the monitor (INA226/INA228/INA3221) and print the + cal value and rail readings. Cross-check bus voltage against a DMM. +4. Only then trust the telemetry stream / warning faults. + +## Shell commands + +| Command | Action | +|----------------------|------------------------------------------------------------| +| `i2cscan [4\|5]` | ACK scan of I2C4/I2C5 (0x08–0x77), prints device list | +| `onb_temp` | Part, address, last temperature, bus error counters | +| `onb_temp reload` | Re-read KV config, re-probe (after `config set`) | +| `rails` | Part, CAL, per-rail V/A/W, bus error counters, last IDs | +| `rails reload` | Re-read KV config, re-probe (after `config set`) | + +## Config keys (FRAM via RteParamStore; `config set `, then +`config saveall` to persist; `onb_temp reload` / `rails reload` applies them +without reboot) + +| Key | Default | Meaning | +|--------------------|---------|-------------------------------------------| +| `OnbTemp.Addr` | 72 (0x48) | 7-bit temp sensor address | +| `OnbTemp.CritC` | 85 | over-temperature Warning threshold [degC] | +| `RailMon.Addr` | 64 (0x40) | 7-bit rail monitor address | +| `RailMon.ShuntOhm` | 0.005 | shunt resistor [ohm] | +| `RailMon.MaxA` | 10 | max expected current [A] (CAL reference) | +| `RailMon.OvV` | 0 | rail overvoltage Warning [V]; 0 = off | +| `RailMon.UvV` | 0 | rail undervoltage Warning [V]; 0 = off | + +## Telemetry keys + +| Key | Source | +|--------------|-------------------------------------------| +| `onb_temp_c` | onboard temperature [degC] (I2C5) | +| `rail_v` | rail 1 bus voltage [V] | +| `rail_a` | rail 1 current [A] | +| `rail_w` | rail 1 power [W] (INA3221: V*I derived) | +| `rail2_v`, `rail2_a`, `rail3_v`, `rail3_a` | INA3221 channels 2/3 only | + +## Warning faults (non-latching-trip, log-only Warning severity) + +`OnboardOvertemperature` (5 degC hysteresis), `RailOvervoltage`, +`RailUndervoltage` (0.5 V hysteresis); all sustain 500 ms before raising. +The drivers are polled from the main loop only — never from an ISR — at 5 Hz +(`POLL_MS = 200`), with a 20 ms probe timeout at init so a missing device can +never delay boot; absent devices are re-probed every 5 s. Three consecutive +poll failures mark a device absent (one log line) and re-arm the probe. + +## Host-side verification + +`tests/host/build_and_run.sh` compiles `OnboardTempSensor.cpp` and +`RailMonitor.cpp` for the host (plain g++ -std=c++17, no HAL) against mock +register models of TMP1075/TMP102, INA226, INA3221 and INA228, and asserts the +decode/encode math plus the detect/absent/error paths. All I2C traffic in +the drivers funnels through `II2cBus::transfer()` / `II2cBus::isReady()` +(`Inc/Inverter/Drivers/I2C/I2cBus.h`), which is what makes this possible. + +## Assumptions made (verify on real hardware) + +- TMP1075 `DEVICE_ID` = `0x7500`; TMP102/PCT2075 read `0x0000` at pointer 0x0F. +- INA226 die ID `0x2260`, INA3221 die ID `0x3220`, INA228 device ID bits + [15:4] = `0x228` (rev bits ignored). +- INA226 internal transfer: `CURRENT = SHUNT*CAL/2048`, + `POWER = CURRENT*BUS/20000` — used by the mock, matching the datasheet. +- INA228 register fields are 20-bit, left-aligned in 24-bit registers + (consistent with the Adafruit INA228 driver). +- PF0/PF1 = I2C5 on AF6 (see top of file). +- 100 kHz standard-mode timing from `0x60404E72` at 137.5 MHz kernel clock: + tLOW ≈ 5.8 µs, tHIGH ≈ 4.0 µs. diff --git a/Images/Gen7FW/MainProcessor/tests/host/.gitignore b/Images/Gen7FW/MainProcessor/tests/host/.gitignore new file mode 100644 index 00000000..89f9ac04 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/Images/Gen7FW/MainProcessor/tests/host/build_and_run.sh b/Images/Gen7FW/MainProcessor/tests/host/build_and_run.sh new file mode 100755 index 00000000..55f1e4a0 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/build_and_run.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# Host-side verification for the Gen7 I2C drivers. +# +# Compiles OnboardTempSensor + RailMonitor against a mock I2C master (plain +# g++ -std=c++17, no external dependencies, no HAL) and runs the assertion +# suites. Exit code 0 = all checks passed. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +FW_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" +BUILD_DIR="${SCRIPT_DIR}/out" + +CXX="${CXX:-g++}" +CXXFLAGS=( + -std=c++17 + -O2 + -g + -Wall + -Wextra + -Wconversion + -Wsign-conversion + -I"${FW_ROOT}/Inc" + -I"${SCRIPT_DIR}" +) + +mkdir -p "${BUILD_DIR}" + +SOURCES=( + "${SCRIPT_DIR}/test_main.cpp" + "${SCRIPT_DIR}/test_temp.cpp" + "${SCRIPT_DIR}/test_rail.cpp" + "${SCRIPT_DIR}/test_temp_adversarial.cpp" + "${SCRIPT_DIR}/test_rail_adversarial.cpp" + "${FW_ROOT}/Src/Inverter/Drivers/I2C/OnboardTempSensor.cpp" + "${FW_ROOT}/Src/Inverter/Drivers/I2C/RailMonitor.cpp" +) + +echo "==> Compiling host harness..." +"${CXX}" "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "${BUILD_DIR}/i2c_driver_tests" + +echo "==> Running tests..." +"${BUILD_DIR}/i2c_driver_tests" diff --git a/Images/Gen7FW/MainProcessor/tests/host/mock_i2c.h b/Images/Gen7FW/MainProcessor/tests/host/mock_i2c.h new file mode 100644 index 00000000..0191a9f5 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/mock_i2c.h @@ -0,0 +1,456 @@ +#pragma once + +/** + * @brief Mock I2C master + slave device register models for host-side driver + * verification (no MCU, no HAL — substitute for real Gen7 hardware). + * + * The models implement the register-visible behavior of the candidate parts: + * - Tmp1075Model: TMP102/TMP1075/PCT2075 pointer-register family. + * TMP1075 flavor: DEVICE_ID (0x0F) = 0x7500, config POR 0x00FF (SBOS854F + * Table 7-5), no EM bit. TMP102/PCT2075 have no ID register; the generic + * flavor masks the pointer to 2 bits like the TMP102 (assumed), so a + * probe at 0x0F aliases THIGH (0x5000). Either way init() must classify + * the part as Generic and keep 12-bit decode. + * - Ina226Model: 16-bit part, MFG_ID(0xFE)=0x5449, DIE_ID(0xFF)=0x2260. + * Calibration transfer implemented per INA226 datasheet: + * CURRENT = (SHUNT * CAL) / 2048, POWER = (CURRENT * BUS) / 20000. + * - Ina3221Model: MFG_ID(0xFE)=0x5449, DIE_ID(0xFF)=0x3220, 3 channels, + * 8 mV / 40 uV LSB in bits 15:3. No CAL (current computed by firmware). + * - Ina228Model: 20-bit ADC in 24-bit registers, MFG_ID(0x3E)=0x5449, + * DEVICE_ID(0x3F)=0x2280 (id in bits 15:4 per Adafruit INA228 driver). + * CURRENT/POWER are computed from VSHUNT x the HELD SHUNT_CAL like the + * real silicon (identity: cur_code = vsh_code * 4096 / cal, verified for + * ADCRANGE=0 against the driver's cal formula), so a stale/wrong CAL or a + * stale CONFIG.ADCRANGE now perturbs readings exactly as on hardware. + * POWER is a straight 24-bit unsigned register (verified against Linux + * kernel ina238.c: 24-bit block read used unshifted; POWER_LIMIT is + * "compared against the 24-bit power register"); an earlier revision of + * this mock encoded it 20-bit left-aligned like VBUS/CURRENT — that was + * wrong and masked driver bug R1 (see test_rail_adversarial.cpp). + * ADCRANGE (CONFIG bit 4) is modelled: with ADCRANGE=1 the VSHUNT LSB is + * 78.125 nV (4x finer), so a range-0 SHUNT_CAL reads currents 4x high. + */ + +#include "Inverter/Drivers/I2C/I2cBus.h" + +#include +#include +#include +#include +#include +#include + +namespace hosttest { + +class MockDevice { +public: + virtual ~MockDevice() = default; + /** Handle write phase; data[0] is the register pointer. false = NACK. */ + virtual bool onWrite(const uint8_t* data, uint8_t len) = 0; + /** + * Whether the device ACKs the (repeated-start) read phase at the pointer + * latched by the last write phase. Default: ACK (all models). Strict + * variants override to NAK reads at unimplemented pointers. + */ + virtual bool readPhaseAcks() const { return true; } + /** Fill the read phase buffer at the current pointer. */ + virtual void onRead(uint8_t* out, uint8_t len) = 0; +}; + +class MockI2cBus : public Inverter::II2cBus { +public: + /** One completed transfer as seen by the driver (for sequence checks). + * A lone-pointer write + N-byte read in a single transfer is the only + * way in II2cBus to do "pointer write, repeated START, read" (see + * I2cBus.h); tx_len/rx_len plus tx0 let tests assert exact shapes. */ + struct Txn { + uint8_t addr; + uint8_t tx0; /**< register pointer byte (tx[0]) */ + uint8_t tx1; /**< first data byte on writes (0 if absent) */ + uint8_t tx2; /**< second data byte on writes (0 if absent) */ + uint8_t tx_len; + uint8_t rx_len; + bool ok; + }; + + void attach(uint8_t addr7, MockDevice* dev) { m_devices[addr7] = dev; } + void detach(uint8_t addr7) { m_devices.erase(addr7); } + uint32_t transferCount() const { return m_transfers; } + const std::vector& txnLog() const { return m_log; } + void clearLog() { m_log.clear(); } + + bool transfer(uint8_t addr7, + const uint8_t* tx, uint8_t tx_len, + uint8_t* rx, uint8_t rx_len, + uint32_t /*timeout_ms*/) override { + ++m_transfers; + Txn t{addr7, + (tx != nullptr && tx_len > 0) ? tx[0] : static_cast(0), + (tx != nullptr && tx_len > 1) ? tx[1] : static_cast(0), + (tx != nullptr && tx_len > 2) ? tx[2] : static_cast(0), + tx_len, rx_len, false}; + auto it = m_devices.find(addr7); + if (it == m_devices.end()) { + m_log.push_back(t); + return false; /* NACK: nothing at this address */ + } + if (tx != nullptr && tx_len > 0 && !it->second->onWrite(tx, tx_len)) { + m_log.push_back(t); + return false; + } + if (rx != nullptr && rx_len > 0) { + if (!it->second->readPhaseAcks()) { + m_log.push_back(t); + return false; /* strict device NAKs the read phase */ + } + it->second->onRead(rx, rx_len); + } + t.ok = true; + m_log.push_back(t); + return true; + } + + bool isReady(uint8_t addr7, uint32_t /*timeout_ms*/) override { + return m_devices.count(addr7) != 0; + } + +private: + std::map m_devices; + std::vector m_log; + uint32_t m_transfers = 0; +}; + +/* ---------------- TMP102/TMP1075/PCT2075 ---------------- */ + +class Tmp1075Model : public MockDevice { +public: + /** + * @param has_id true: TMP1075 — DEVICE_ID 0x0F = 0x7500, 4-bit pointer + * mask, config POR 0x00FF (low "Not used" bits all read 1, + * SBOS854F Table 7-5). The real TMP1075 has NO EM bit; + * the 0x00FF POR makes bit 4 read set, which is exactly + * why the driver must not trust EM on this part. + * false: TMP102-flavor generic — 2-bit pointer mask, so a + * probe at 0x0F aliases THIGH (0x5000) instead of reading + * 0x0000 (SBOS397 pointer register: only P1/P0 used; + * masking of reserved bits assumed, not re-verified); + * config POR 0x60A0 (SBOS397, assumed — only the EM=0 bit + * matters to the driver). + */ + explicit Tmp1075Model(bool has_id, uint8_t ptr_mask = 0) + : m_ptr_mask(ptr_mask != 0 ? ptr_mask + : (has_id ? 0x0F : 0x03)) { + m_regs[0x00] = 0x1900; /* 25.0 degC */ + m_regs[0x02] = 0x4B00; /* Tlow 75 C */ + m_regs[0x03] = 0x5000; /* Thigh 80 C */ + if (has_id) { + m_regs[0x01] = 0x00FFu; /* TMP1075 config POR */ + m_regs[0x0F] = 0x7500u; /* DEVICE_ID */ + } else { + m_regs[0x01] = 0x60A0u; /* TMP102 config POR (EM=0) */ + /* no 0x0F register: the probe aliases THIGH via m_ptr_mask */ + } + } + + void setTempRaw16(uint16_t reg) { m_regs[0x00] = reg; } + void setConfig(uint16_t cfg) { m_regs[0x01] = cfg; } + uint16_t config() const { return m_regs.at(0x01); } + uint8_t pointer() const { return m_pointer; } + + /** Make reads at (masked) pointer p NAK the read phase — e.g. a strict + * part whose unimplemented pointers refuse the read instead of 0x0000 + * (assumed; datasheet silent on reserved-pointer behaviour). */ + void nakReadPointer(uint8_t p) { m_nak_reads.insert(p & m_ptr_mask); } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = static_cast(data[0] & m_ptr_mask); + if (len >= 3) { + m_regs[m_pointer] = + static_cast((data[1] << 8) | data[2]); + } + return true; + } + + bool readPhaseAcks() const override { + return m_nak_reads.count(m_pointer) == 0; + } + + void onRead(uint8_t* out, uint8_t len) override { + const uint16_t v = m_regs.count(m_pointer) ? m_regs[m_pointer] : 0; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + +private: + std::map m_regs; + std::set m_nak_reads; + uint8_t m_pointer = 0; + uint8_t m_ptr_mask; +}; + +/* ---------------- INA226 ---------------- */ + +class Ina226Model : public MockDevice { +public: + Ina226Model() { + m_regs[0x00] = 0x4127; /* config POR default */ + m_regs[0xFE] = 0x5449; /* "TI" */ + m_regs[0xFF] = 0x2260; /* die id INA226 */ + } + + /** Physical stimulus; the chip-computed current/power registers update + * whenever CAL or the stimulus changes (datasheet transfer function). */ + void setMeasurement(float bus_v, float shunt_v) { + const long shunt_raw = lround(shunt_v / 2.5e-6); + const long bus_raw = lround(bus_v / 1.25e-3); + m_shunt_raw = static_cast(static_cast(shunt_raw)); + m_bus_raw = static_cast(bus_raw); + recompute(); + } + + /** Drop-in raw register stimuli (e.g. 16-bit boundary patterns) — the + * chip-computed current/power registers follow per the datasheet transfer + * function, exactly as after setMeasurement(). */ + void setShuntRaw(uint16_t raw) { m_shunt_raw = raw; recompute(); } + void setBusRaw(uint16_t raw) { m_bus_raw = raw; recompute(); } + + uint16_t cal() const { return m_regs.at(0x05); } + uint16_t currentReg() const { return m_current_raw; } + uint16_t powerReg() const { return m_power_raw; } + + /** Override the die ID (0xFF). US-fab INA226 revs report 0x2261, so the + * driver match must mask the RID nibble (SBOS547 Table 7-15 note). */ + void setDieId(uint16_t v) { m_regs[0xFF] = v; } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = data[0]; + if (len >= 3) { + switch (m_pointer) { + case 0x00: case 0x05: case 0x06: case 0x07: + m_regs[m_pointer] = + static_cast((data[1] << 8) | data[2]); + break; + default: + break; /* result/ID regs are read-only */ + } + if (m_pointer == 0x05) { + recompute(); + } + } + return true; + } + + void onRead(uint8_t* out, uint8_t len) override { + uint16_t v = 0; + if (m_pointer == 0x01) v = m_shunt_raw; + else if (m_pointer == 0x02) v = m_bus_raw; + else if (m_pointer == 0x03) v = m_power_raw; + else if (m_pointer == 0x04) v = m_current_raw; + else if (m_regs.count(m_pointer)) v = m_regs[m_pointer]; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + +private: + void recompute() { + const uint16_t cal = m_regs.count(0x05) ? m_regs[0x05] : 0; + /* CURRENT = (SHUNT * CAL) / 2048 (datasheet eq.). */ + const int32_t cur = + static_cast(static_cast(m_shunt_raw)) * + static_cast(cal) / 2048; + m_current_raw = static_cast(static_cast(cur)); + /* POWER = (CURRENT * BUS) / 20000 (datasheet eq.). */ + const uint32_t p = + (static_cast(static_cast(cur)) * + m_bus_raw) / 20000; + m_power_raw = static_cast(p & 0xFFFFu); + } + + std::map m_regs; + uint8_t m_pointer = 0; + uint16_t m_shunt_raw = 0; + uint16_t m_bus_raw = 0; + uint16_t m_current_raw = 0; + uint16_t m_power_raw = 0; +}; + +/* ---------------- INA3221 ---------------- */ + +class Ina3221Model : public MockDevice { +public: + Ina3221Model() { + m_regs[0x00] = 0x7127; /* config POR default */ + m_regs[0xFE] = 0x5449; + m_regs[0xFF] = 0x3220; + } + + /** + * Channel 0..2 (datasheet channels 1..3). Writes the channel's measured + * registers ONLY when the channel is enabled in CONFIG — a disabled + * channel stops converting and its registers keep their old contents. + * (Channel-enable bits 14..12 verified vs Linux kernel ina3221.c, + * CHx_EN(x) = BIT(14-x); POR 0x7127 enables all three. The held-stale + * contents themselves are 'assumed': the kernel driver refuses to read + * disabled channels at all, so hardware behaviour there is unverified.) + */ + void setMeasurement(uint8_t ch, float bus_v, float shunt_v) { + if (!channelEnabled(ch)) return; + const long bus_raw = lround(bus_v / 8e-3); + const long shunt_raw = lround(shunt_v / 40e-6); + const uint8_t reg_shunt = static_cast(0x01 + 2 * ch); + const uint8_t reg_bus = static_cast(0x02 + 2 * ch); + m_regs[reg_shunt] = + static_cast(static_cast(shunt_raw) << 3); + m_regs[reg_bus] = + static_cast(static_cast(bus_raw) << 3); + } + + bool channelEnabled(uint8_t ch) const { + const uint16_t cfg = configRaw(); + return (cfg & static_cast(0x4000u >> ch)) != 0u; + } + void setConfig(uint16_t cfg) { m_regs[0x00] = cfg; } + uint16_t configRaw() const { + return m_regs.count(0x00) ? m_regs.at(0x00) : 0x7127; + } + + /** Override the die ID (0xFF) to emulate a different silicon revision + * (0x3221 &c.) and prove the driver's RID-nibble-masked match. */ + void setDieId(uint16_t v) { m_regs[0xFF] = v; } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = data[0]; + if (len >= 3 && m_pointer == 0x00) { + m_regs[0x00] = + static_cast((data[1] << 8) | data[2]); + } + return true; + } + + void onRead(uint8_t* out, uint8_t len) override { + const uint16_t v = m_regs.count(m_pointer) ? m_regs[m_pointer] : 0; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + +private: + std::map m_regs; + uint8_t m_pointer = 0; +}; + +/* ---------------- INA228 ---------------- */ + +class Ina228Model : public MockDevice { +public: + /** CONFIG bit 4 selects the high-resolution shunt range (verified via + * Linux kernel ina238.c: INA238_CONFIG_ADCRANGE = BIT(4)). */ + static constexpr uint16_t CONFIG_ADCRANGE = 0x0010u; + + explicit Ina228Model(float shunt_ohm) : m_shunt_ohm(shunt_ohm) { + /* 16-bit registers are stored MSB-aligned in the 24-bit container. */ + m_regs[0x00] = 0; /* CONFIG POR 0x0000: ADCRANGE=0 */ + m_regs[0x02] = 0x1000u << 8; /* SHUNT_CAL POR 0x1000 (assumed: same + * as the value the kernel programs) */ + m_regs[0x3E] = 0x5449u << 8; + m_regs[0x3F] = 0x2281u << 8; /* DEVID 0x228 in bits 15:4, rev 1 — + * real parts read 0x2281, forcing the + * driver to mask the revision nibble. */ + /* 24-bit result registers start at 0. */ + m_regs[0x04] = 0; m_regs[0x05] = 0; m_regs[0x06] = 0; + m_regs[0x07] = 0; m_regs[0x08] = 0; + } + + /** + * @brief Set the electrical stimulus; result registers are derived the way + * the silicon derives them: + * VSHUNT : 20-bit left-aligned, LSB 312.5 nV (ADCRANGE=0) or + * 78.125 nV (ADCRANGE=1) + * VBUS : 20-bit left-aligned, LSB 195.3125 uV + * CURRENT : 20-bit left-aligned, = VSHUNT_code * 4096 / SHUNT_CAL + * (chip computes from the held CAL, not from physics — + * algebraically identical to (Vshunt/Rshunt)/Current_LSB + * when CAL matches the driver's formula) + * POWER : 24-bit straight value, LSB = 3.2 * Current_LSB, + * = VBUS * CURRENT_code / 3.2, clamped at 0xFFFFFF + * (saturation behaviour assumed). + */ + void setMeasurement(float bus_v, float shunt_v, float die_c = 45.0f) { + const bool range4 = (configRaw16() & CONFIG_ADCRANGE) != 0; + const double vsh_lsb = range4 ? 78.125e-9 : 312.5e-9; + const long vsh_code = lround(shunt_v / vsh_lsb); + const long bus_code = lround(bus_v / 195.3125e-6); + const double cal = m_regs.count(0x02) + ? static_cast(m_regs[0x02] >> 8) + : 0.0; + const long cur_code = + (cal > 0.0) ? lround(static_cast(vsh_code) * 4096.0 / cal) + : 0; + long pow_code = lround(static_cast(bus_v) * + static_cast(cur_code) / 3.2); + if (pow_code > 0x00FFFFFFL) pow_code = 0x00FFFFFFL; /* assumed */ + const long tmp_code = lround(die_c / 7.8125e-3); + set24(0x04, vsh_code); /* VSHUNT: 20-bit left-aligned */ + set24(0x05, bus_code); /* VBUS: 20-bit left-aligned */ + /* DIETEMP: 16-bit register (7.8125 mdegC/LSB), MSB-aligned. */ + m_regs[0x06] = (static_cast(tmp_code) & 0xFFFFu) << 8; + set24(0x07, cur_code); /* CURRENT: 20-bit left-aligned */ + /* POWER: straight 24-bit value (verified vs Linux ina238.c: + * read unshifted, LSB = 3.2 * Current_LSB). */ + m_regs[0x08] = static_cast(pow_code) & 0x00FFFFFFu; + } + + uint16_t shuntCal() const { return static_cast(m_regs.at(0x02) >> 8); } + uint32_t powerRaw24() const { return m_regs.at(0x08) & 0x00FFFFFFu; } + + /** Simulate warm-boot / externally-programmed register state. */ + void setConfigRaw16(uint16_t v) { m_regs[0x00] = static_cast(v) << 8; } + uint16_t configRaw16() const { + return static_cast(m_regs.at(0x00) >> 8); + } + void setDevId(uint16_t v) { m_regs[0x3F] = static_cast(v) << 8; } + + bool onWrite(const uint8_t* data, uint8_t len) override { + m_pointer = data[0]; + if (len >= 3) { + switch (m_pointer) { + case 0x00: case 0x01: case 0x02: case 0x03: + m_regs[m_pointer] = + static_cast((data[1] << 8) | data[2]) << 8; + break; /* 16-bit registers stored MSB-aligned */ + default: + break; + } + } + return true; + } + + void onRead(uint8_t* out, uint8_t len) override { + uint32_t v = m_regs.count(m_pointer) ? m_regs[m_pointer] : 0; + /* 16-bit registers are stored MSB-aligned; reading 2 bytes returns + * the top 16 bits exactly like the 16-bit parts. */ + if (len >= 1) out[0] = static_cast(v >> 16); + if (len >= 2) out[1] = static_cast(v >> 8); + if (len >= 3) out[2] = static_cast(v); + for (uint8_t i = 3; i < len; ++i) out[i] = 0; + } + +private: + void set24(uint8_t reg, long code20) { + /* 20-bit result, left-aligned (CODE<<4) in the 24-bit register + * (VSHUNT/VBUS/CURRENT layout verified vs Linux ina238.c + * ina238_read_field_s20: >>4 then sign-extend bit 19). Overrange + * wraps in the 20-bit field (assumed; saturation not verified). */ + const int32_t c = static_cast(code20) & 0x000FFFFF; + const int32_t v = c << 4; + m_regs[reg] = static_cast(v) & 0x00FFFFFFu; + } + + std::map m_regs; + uint8_t m_pointer = 0; + float m_shunt_ohm; +}; + +} // namespace hosttest diff --git a/Images/Gen7FW/MainProcessor/tests/host/test_framework.h b/Images/Gen7FW/MainProcessor/tests/host/test_framework.h new file mode 100644 index 00000000..5413be0d --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/test_framework.h @@ -0,0 +1,56 @@ +#pragma once + +/** + * @brief Minimal assertion helpers for the host harness (no dependencies). + */ + +#include +#include + +namespace hosttest { + +struct Counters { + int passed = 0; + int failed = 0; + int total = 0; +}; + +inline Counters g_checks; + +inline void report(bool ok, const char* expr, const char* file, int line) { + ++g_checks.total; + if (ok) { + ++g_checks.passed; + std::printf(" PASS %s\n", expr); + } else { + ++g_checks.failed; + std::printf(" FAIL %s (%s:%d)\n", expr, file, line); + } +} + +inline void reportNear(double actual, double expect, double tol, + const char* expr, const char* file, int line) { + const bool ok = std::fabs(actual - expect) <= tol; + ++g_checks.total; + if (ok) { + ++g_checks.passed; + std::printf(" PASS %s = %.6f (expect %.6f +/- %.4g)\n", + expr, actual, expect, tol); + } else { + ++g_checks.failed; + std::printf(" FAIL %s = %.6f, expected %.6f +/- %.4g (%s:%d)\n", + expr, actual, expect, tol, file, line); + } +} + +#define CHECK(cond) \ + ::hosttest::report((cond), #cond, __FILE__, __LINE__) +#define CHECK_NEAR(actual, expect, tol) \ + ::hosttest::reportNear((actual), (expect), (tol), #actual, __FILE__, __LINE__) + +void test_temp_suite(); +void test_rail_suite(); +void test_temp_adv_suite(); +void test_rail_adv_suite(); + +} // namespace hosttest diff --git a/Images/Gen7FW/MainProcessor/tests/host/test_main.cpp b/Images/Gen7FW/MainProcessor/tests/host/test_main.cpp new file mode 100644 index 00000000..c78df535 --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/test_main.cpp @@ -0,0 +1,19 @@ +#include "test_framework.h" + +#include + +int main() { + std::printf("=== Gen7 I2C driver host verification ===\n"); + hosttest::test_temp_suite(); + hosttest::test_rail_suite(); + hosttest::test_temp_adv_suite(); + hosttest::test_rail_adv_suite(); + + std::printf("=== %d/%d checks passed", hosttest::g_checks.passed, + hosttest::g_checks.total); + if (hosttest::g_checks.failed != 0) { + std::printf(" (%d FAILED)", hosttest::g_checks.failed); + } + std::printf(" ===\n"); + return hosttest::g_checks.failed == 0 ? 0 : 1; +} diff --git a/Images/Gen7FW/MainProcessor/tests/host/test_rail.cpp b/Images/Gen7FW/MainProcessor/tests/host/test_rail.cpp new file mode 100644 index 00000000..e143ea9c --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/test_rail.cpp @@ -0,0 +1,247 @@ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/RailMonitor.h" + +#include + +using Inverter::RailMonitor; +using hosttest::Ina226Model; +using hosttest::Ina228Model; +using hosttest::Ina3221Model; +using hosttest::MockI2cBus; + +namespace { + +constexpr float RSHUNT = 0.005f; /* 5 mOhm */ +constexpr float MAX_A = 10.0f; /* mission scenario: 5 mOhm / 10 A */ + +/* ---------- INA226 pure math ---------- */ +void test_ina226_math() { + std::printf("[rail] INA226 decode/cal math\n"); + const float lsb = RailMonitor::ina226CurrentLsb(MAX_A); + CHECK_NEAR(lsb, 10.0 / 32768.0, 1e-9); + + /* CAL = 0.00512 / (Current_LSB * Rshunt) = 3355.44 truncated. */ + const uint16_t cal = RailMonitor::ina226CalValue(lsb, RSHUNT); + CHECK(cal == 3355u); + + CHECK_NEAR(RailMonitor::ina226BusVFromRaw(19200), 24.0, 1e-3); /* 1.25 mV/LSB */ + CHECK_NEAR(RailMonitor::ina226ShuntVFromRaw(10000), 0.025, 1e-7); /* 2.5 uV/LSB */ + CHECK_NEAR(RailMonitor::ina226ShuntVFromRaw(-5000), -0.0125, 1e-7); + CHECK_NEAR(RailMonitor::ina226CurrentFromRaw(16384, lsb), 5.0, 1e-3); + CHECK_NEAR(RailMonitor::ina226CurrentFromRaw(-8192, lsb), -2.5, 1e-3); + CHECK_NEAR(RailMonitor::ina226PowerFromRaw(15725, lsb), 120.0, 0.06); +} + +/* ---------- INA226 end-to-end via mock bus ---------- */ +void test_ina226_detect_and_poll() { + std::printf("[rail] INA226 detected at 0x40, 5 mOhm / 10 A\n"); + MockI2cBus bus; + Ina226Model dev; + dev.setMeasurement(24.0f, 0.025f); /* 24 V bus, 25 mV shunt -> 5 A */ + bus.attach(0x40, &dev); + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(mon.init(bus, 0x40, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::INA226); + CHECK(mon.channels() == 1); + CHECK(mon.shuntCalValue() == 3355u); + CHECK(dev.cal() == 3355u); + CHECK(mon.lastMfgId() == 0x5449u); + CHECK(mon.lastDevId() == 0x2260u); + + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + /* current reg = 10000 * 3355 / 2048 = 16381 -> 16381 * 10/32768 A */ + CHECK_NEAR(s.bus_v, 24.0, 0.002); + CHECK_NEAR(s.current_a, 4.9994, 0.005); + CHECK_NEAR(s.power_w, 119.97, 0.10); + + /* Reverse current (regenerating): -12.5 mV shunt -> -2.5 A. */ + dev.setMeasurement(24.0f, -0.0125f); + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.current_a, -2.4996, 0.005); +} + +/* ---------- INA228 pure math ---------- */ +void test_ina228_math() { + std::printf("[rail] INA228 decode/cal math\n"); + const float lsb = RailMonitor::ina228CurrentLsb(MAX_A); + CHECK_NEAR(lsb, 10.0 / 524288.0, 1e-12); + + /* SHUNT_CAL = 13107.2e6 * lsb * Rshunt = 1250 exactly at 10 A / 5 mOhm. */ + CHECK(RailMonitor::ina228ShuntCalValue(lsb, RSHUNT) == 1250u); + + /* 24 V bus: code = 24 / 195.3125 uV = 122880, reg = code << 4. */ + const uint32_t bus_reg = static_cast(122880) << 4; + CHECK_NEAR(RailMonitor::ina228BusVFromRaw(bus_reg), 24.0, 2e-4); + + const uint32_t cur_reg = static_cast(104858) << 4; + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(cur_reg, lsb), 2.0, 2e-5); + + /* Negative current: -1 A -> code -52429 rounds to -52429 -> two's + * complement 20-bit left-aligned in 24-bit word. */ + const int32_t code = -52429; + const uint32_t cur_reg_neg = + (static_cast(code) << 4) & 0x00FFFFFFu; + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(cur_reg_neg, lsb), -1.0, 2e-4); + + /* POWER is a straight 24-bit register, LSB = 3.2 * Current_LSB (verified + * vs Linux kernel ina238.c: read unshifted; a 20-bit-left-aligned decode + * under-reports 16x). */ + CHECK_NEAR(RailMonitor::ina228PowerFromRaw(393216u, lsb), 24.0, 5e-3); + /* Bits above bit 19 are real power data, not sign extension: */ + CHECK_NEAR(RailMonitor::ina228PowerFromRaw(0x600000u, lsb), 384.0, 0.05); + CHECK_NEAR(RailMonitor::ina228DieTempCFromRaw(5760), 45.0, 1e-3); + + /* sign extension */ + CHECK(RailMonitor::signExtend24(0x00400000) == 0x00400000); + CHECK(RailMonitor::signExtend24(0x00F00000) < 0); + CHECK(RailMonitor::signExtend24(0x00000000) == 0); +} + +/* ---------- INA228 end-to-end via mock bus ---------- */ +void test_ina228_detect_and_poll() { + std::printf("[rail] INA228 detected at 0x41, 5 mOhm / 10 A\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + bus.attach(0x41, &dev); + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(mon.init(bus, 0x41, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::INA228); + CHECK(mon.channels() == 1); + CHECK(mon.shuntCalValue() == 1250u); + CHECK(dev.shuntCal() == 1250u); + + /* The mock derives CURRENT from the held SHUNT_CAL like the silicon, so + * the CAL written by init() IS what makes this reading correct. */ + dev.setMeasurement(12.0f, 0.010f); /* 12 V, 10 mV -> 2 A, 24 W */ + + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 12.0, 2e-4); + /* cur code = 32000 * 4096 / 1250 = 104857.6 -> 104858 -> 2.0000076 A */ + CHECK_NEAR(s.current_a, 2.0, 1e-4); + /* pow code = 12 * 104858 / 3.2 = 393217.5 -> 393218 -> 24.000122 W */ + CHECK_NEAR(s.power_w, 24.0, 2e-3); + CHECK(dev.powerRaw24() == 393218u); /* straight 24-bit, NOT <<4 */ +} + +/* ---------- INA3221 pure math + end-to-end ---------- */ +void test_ina3221_math_and_poll() { + std::printf("[rail] INA3221 3-channel at 0x40, 5 mOhm\n"); + CHECK_NEAR(RailMonitor::ina3221BusVFromRaw(0x5DC0), 24.0, 1e-3); + CHECK_NEAR(RailMonitor::ina3221ShuntVFromRaw(0x1388), 0.025, 1e-7); + /* Negative shunt: -312 raw -> reg 0xF640 -> -12.48 mV. */ + CHECK_NEAR(RailMonitor::ina3221ShuntVFromRaw( + static_cast(0xF640)), -0.01248, 1e-7); + CHECK_NEAR(RailMonitor::ina3221CurrentA(0.025f, RSHUNT), 5.0, 1e-6); + + MockI2cBus bus; + Ina3221Model dev; + bus.attach(0x40, &dev); + dev.setMeasurement(0, 24.0f, 0.025f); /* ch1: 24 V, 5 A */ + dev.setMeasurement(1, 3.3f, 0.0025f); /* ch2: 3.3 V, 0.5 A */ + dev.setMeasurement(2, 5.0f, 0.0f); /* ch3: 5 V, 0 A */ + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(mon.init(bus, 0x40, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::INA3221); + CHECK(mon.channels() == 3); + CHECK(mon.shuntCalValue() == 0u); /* no CAL register on this part */ + + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 24.0, 0.005); + CHECK_NEAR(s.current_a, 5.0, 0.002); + CHECK_NEAR(s.power_w, 120.0, 0.05); + + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 3.3, 0.005); + CHECK_NEAR(s.current_a, 0.5, 0.01); + CHECK_NEAR(s.power_w, 1.65, 0.03); + + CHECK(mon.poll(2, s, 10)); + CHECK_NEAR(s.bus_v, 5.0, 0.005); + CHECK_NEAR(s.current_a, 0.0, 0.01); + + /* Out-of-range channel must fail, not wrap. */ + CHECK(!mon.poll(3, s, 10)); +} + +/* ---------- auto-detect order / negative paths ---------- */ +void test_absent() { + std::printf("[rail] absent device (NACK on probe address)\n"); + MockI2cBus bus; /* empty bus */ + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(!mon.init(bus, 0x40, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + CHECK(mon.channels() == 0); + RailMonitor::Sample s{1.0f, 1.0f, 1.0f}; + CHECK(!mon.poll(0, s, 10)); + CHECK(bus.transferCount() == 0); /* isReady answered the probe */ +} + +void test_unknown_die() { + std::printf("[rail] unknown die id -> not detected\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + /* Corrupt the die id: 0xFE/0xFF are read-only in the model, so emulate a + * different die by re-attaching a bare device with foreign IDs. */ + class ForeignTi : public hosttest::MockDevice { + public: + bool onWrite(const uint8_t* d, uint8_t) override { m_ptr = d[0]; return true; } + void onRead(uint8_t* out, uint8_t len) override { + uint16_t v = 0; + if (m_ptr == 0xFE) v = 0x5449; + if (m_ptr == 0xFF) v = 0x9999; /* some future TI part */ + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } + private: + uint8_t m_ptr = 0; + } foreign; + MockI2cBus bus2; + bus2.attach(0x44, &foreign); + + RailMonitor mon; + const RailMonitor::Config cfg{RSHUNT, MAX_A}; + CHECK(!mon.init(bus2, 0x44, cfg, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + CHECK(mon.lastMfgId() == 0x5449u); + CHECK(mon.lastDevId() == 0x9999u); +} + +void test_bad_config_rejected() { + std::printf("[rail] invalid calibration config rejected\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + RailMonitor mon; + CHECK(!mon.init(bus, 0x40, RailMonitor::Config{0.0f, MAX_A}, 20)); + CHECK(!mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, 0.0f}, 20)); + CHECK(!mon.init(bus, 0x40, RailMonitor::Config{-0.005f, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + CHECK(bus.transferCount() == 0); /* rejected before touching the bus */ +} + +} // namespace + +void hosttest::test_rail_suite() { + test_ina226_math(); + test_ina226_detect_and_poll(); + test_ina228_math(); + test_ina228_detect_and_poll(); + test_ina3221_math_and_poll(); + test_absent(); + test_unknown_die(); + test_bad_config_rejected(); +} diff --git a/Images/Gen7FW/MainProcessor/tests/host/test_rail_adversarial.cpp b/Images/Gen7FW/MainProcessor/tests/host/test_rail_adversarial.cpp new file mode 100644 index 00000000..b1e3c3dc --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/test_rail_adversarial.cpp @@ -0,0 +1,429 @@ +/* Adversarial coverage for RailMonitor (INA226 / INA228 / INA3221). + * + * Provenance tags (see also test_temp_adversarial.cpp): + * [V] verified against public datasheet-derived sources this session: + * Linux kernel ina2xx.c / ina238.c / ina3221.c (TI-authored or + * datasheet-derived, hardware-tested), Adafruit INA228 library. + * [A] assumed / mock-informed: mock and driver share the assumption and it + * could not be re-verified against datasheet text. + * [D] documented/characterized driver behavior. + */ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/RailMonitor.h" + +#include + +using Inverter::RailMonitor; +using hosttest::Ina226Model; +using hosttest::Ina228Model; +using hosttest::Ina3221Model; +using hosttest::MockI2cBus; + +namespace { + +constexpr float RSHUNT = 0.005f; /* 5 mOhm */ +constexpr float MAX_A = 10.0f; + +/* ---------- INA226: calibration that does NOT divide evenly ---------- */ + +void test_ina226_cal_worked_example() { + std::printf("[rail-adv] INA226 CAL: uneven + datasheet worked example\n"); + /* Task-cited worked example: with Current_LSB = 1 mA/bit and Rshunt = + * 2 mOhm the CAL register is exactly 0x0A00 = 2560 (CAL = 0.00512 / + * (LSB*R); the 0.00512 constant is corroborated by the kernel ina2xx + * fixed-CAL=2048 scheme). The "3 A / 10 mOhm" parameterization of that + * example could not be re-verified against the datasheet PDF (not + * fetchable) — provenance [A]; the formula case below is exact. */ + CHECK(RailMonitor::ina226CalValue(0.001f, 0.002f) == 0x0A00u); + + /* Same example through the driver's own convention (LSB = max/2^15): + * LSB = 91.552734375 uA -> CAL = 5592.4 -> rounds to 5592 (0x15D8), NOT + * 0x0A00 — that example number presumes a normalized LSB. [D] */ + const float lsb3a = RailMonitor::ina226CurrentLsb(3.0f); + CHECK_NEAR(lsb3a, 3.0 / 32768.0, 1e-12); + CHECK(RailMonitor::ina226CalValue(lsb3a, 0.01f) == 5592u); +} + +void test_ina226_end_to_end_uneven_cal() { + std::printf("[rail-adv] INA226 e2e, 3 A / 10 mOhm (uneven CAL)\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x42, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x42, RailMonitor::Config{0.01f, 3.0f}, 20)); + CHECK(mon.shuntCalValue() == 5592u); + CHECK(dev.cal() == 5592u); + + /* Full-scale-ish: 30 mV shunt = 3.0 A on a 10 mOhm shunt. */ + dev.setMeasurement(12.0f, 0.030f); + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 12.0, 0.002); + /* reg = 12000*5592/2048 = 32765 (chip truncates) -> 32765*3/32768 A */ + CHECK_NEAR(s.current_a, 2.9997, 5e-4); + /* reg = 32765*9600/20000 = 15727 -> 15727*25*3/32768 = 35.9959 W */ + CHECK_NEAR(s.power_w, 36.0, 0.01); +} + +/* Slightly over max_expected_a, the INA226 current register wraps past + * int16 full scale. Physical rail is +10.5 A; the driver reports a large + * NEGATIVE current. [A: two's-complement wrap is mock-informed; the real + * part at least sets MATH_OVERFLOW (OVF) in Mask/Enable — which this driver + * never reads. See report.] */ +void test_ina226_overrange_wraps_negative() { + std::printf("[rail-adv] INA226 over-max current wraps (10 mOhm? no, 5 mOhm, 10 A max)\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + + dev.setMeasurement(24.0f, 0.0525f); /* 52.5 mV -> 10.5 A > 10 A max */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + /* reg = 21000*3355/2048 = 34401 -> wraps int16 to -31135 */ + CHECK(dev.currentReg() == static_cast(-31135)); + CHECK_NEAR(s.current_a, -9.5016, 0.001); + /* nonsense-positive power follows the wrapped register */ + CHECK_NEAR(s.power_w, 251.95, 0.05); +} + +/* Raw shunt 0x8000 = -32768 codes (one LSB beyond +full-scale magnitude): + * the register math overflows twice. Physical truth: -81.92 mV = -16.38 A + * on a 5 mOhm shunt; the driver reports +3.62 A. [A: wrap behavior is + * mock-informed]. Exercises the driver's int16 path on the min pattern. */ +void test_ina226_shunt_raw_min_negative() { + std::printf("[rail-adv] INA226 shunt raw 0x8000 (min negative)\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + + dev.setShuntRaw(0x8000); + dev.setBusRaw(28800); /* 36 V */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + /* -32768*3355/2048 = -53680 -> int16 wrap 11856 */ + CHECK(dev.currentReg() == 11856u); + CHECK_NEAR(s.current_a, 3.6182, 0.001); + CHECK_NEAR(s.power_w, 130.25, 0.01); +} + +void test_ina226_cal_clamps() { + std::printf("[rail-adv] INA226 CAL register clamps\n"); + /* Tiny LSB * tiny shunt -> CAL far past 0x7FFF: driver clamps to 0x7FFF. + * [A: the "bit 15 reserved" rationale is from the driver comment; the + * clamp itself is driver design — flagged for the datasheet review.] */ + CHECK(RailMonitor::ina226CalValue(RailMonitor::ina226CurrentLsb(0.1f), + 0.0005f) == 0x7FFFu); + /* Huge LSB * shunt -> CAL rounds to 0: driver clamps up to 1 so the + * chip's current/power registers stay defined. */ + CHECK(RailMonitor::ina226CalValue(RailMonitor::ina226CurrentLsb(13107.2f), + 0.1f) == 1u); + /* Full-scale power decode headroom: 65535 * 25 * LSB. (Unreachable in + * rated use: 36 V bus * full-scale current ~ 47184 codes.) */ + CHECK_NEAR(RailMonitor::ina226PowerFromRaw( + 0xFFFFu, RailMonitor::ina226CurrentLsb(MAX_A)), + 499.99, 0.02); +} + +/* ---------- INA228: 20-bit boundaries, ADCRANGE, big power ---------- */ + +void test_ina228_20bit_boundaries() { + std::printf("[rail-adv] INA228 20-bit sign/boundary decode\n"); + const float lsb = RailMonitor::ina228CurrentLsb(MAX_A); /* 10/2^19 */ + /* Min negative code (-524288): raw 0x800000 -> exactly -max_expected_a */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x00800000u, lsb), -10.0, 1e-4); + /* Max positive code (524287): raw 0x7FFFF0 */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x007FFFF0u, lsb), 9.99998, 1e-4); + /* -1 code: raw 0xFFFFF0 — top nibble 0xF is sign extension of bit 19 */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x00FFFFF0u, lsb), + -1.90735e-5, 1e-7); + /* Reserved low nibble (bits 3:0 always 0 on the part [V: kernel comment]) + * must not leak into the code if ever set by noise: >>4 discards. */ + CHECK_NEAR(RailMonitor::ina228CurrentFromRaw(0x00000008u, lsb), 0.0, 1e-9); + /* VBUS at the 85 V common-mode ceiling: 435200 << 4 = 0x6A4000. */ + CHECK_NEAR(RailMonitor::ina228BusVFromRaw(0x006A4000u), 85.0, 1e-3); + /* [D] VBUS shares the signed-20-bit decode: a bit-19-set pattern decodes + * negative (-102.4 V). Physically unreachable (max 85 V = code 435200), + * but the symmetric decode is characterized here, same as the kernel's + * read_field_s20 path. [V] */ + CHECK_NEAR(RailMonitor::ina228BusVFromRaw(0x00800000u), -102.4, 0.01); + /* Negative die temperature: 0xE800 = -6144 * 7.8125 mC = -48 C. */ + CHECK_NEAR(RailMonitor::ina228DieTempCFromRaw(0xE800u), -48.0, 1e-3); + CHECK(RailMonitor::signExtend24(0x00FFFFFFu) == -1); + CHECK(RailMonitor::signExtend24(0x007FFFFFu) == 0x007FFFFF); +} + +/* Power needing >20 bits: 80 V * 9.5 A = 760 W -> power code 12,451,850, + * which fills bits 23:21. Regression guard for the fixed 24-bit decode — + * a 20-bit-left-aligned decode would lose this entirely. [V: Linux ina238.c + * reads POWER as straight 24-bit.] */ +void test_ina228_large_power_24bit() { + std::printf("[rail-adv] INA228 power register >20 bits\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + bus.attach(0x41, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.shuntCalValue() == 1250u); + CHECK(dev.shuntCal() == 1250u); + + dev.setMeasurement(80.0f, 0.0475f); /* 47.5 mV / 5 mOhm = 9.5 A; 760 W */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 80.0, 0.01); + CHECK_NEAR(s.current_a, 9.5001, 1e-3); + CHECK_NEAR(s.power_w, 760.0, 0.1); + CHECK(dev.powerRaw24() == 12451850u); /* 0xBDF4CA, bits above 19 set */ +} + +/* Warm-boot hazard: the part was left in ADCRANGE=1 (4x finer shunt LSB) by + * a previous run. init() now RMWs CONFIG to force ADCRANGE=0, so the + * range-0 SHUNT_CAL the driver computes stays consistent. [V: ADCRANGE bit + * position and the x4 SHUNT_CAL rule — kernel ina238.c and Adafruit INA228; + * A: the mock's chip-internal coupling model.] */ +void test_ina228_stale_adcrange() { + std::printf("[rail-adv] INA228 stale ADCRANGE=1 cleared at init\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + dev.setConfigRaw16(Ina228Model::CONFIG_ADCRANGE); /* warm-boot state */ + bus.attach(0x41, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA228); + /* init must have CLEARED ADCRANGE with a CONFIG read-modify-write. */ + CHECK((dev.configRaw16() & Ina228Model::CONFIG_ADCRANGE) == 0u); + + dev.setMeasurement(12.0f, 0.010f); /* physical: 2 A, 24 W */ + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.current_a, 2.0, 1e-3); /* correct after the RMW */ + CHECK_NEAR(s.power_w, 24.0, 0.02); +} + +/* Revision-nibble masking on every family's ID registers. [V: the INA228 + * mock's default 0x2281 (rev 1) already exercises one mask path; INA226 US + * fab revs report 0x2261 per SBOS547 note per parent.] */ +void test_die_id_revision_masking() { + std::printf("[rail-adv] die-ID revision nibble masking\n"); + { + MockI2cBus bus; + Ina226Model dev; + dev.setDieId(0x2261); /* silicon rev 1 */ + bus.attach(0x40, &dev); + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA226); + CHECK(mon.lastDevId() == 0x2261u); + } + { + MockI2cBus bus; + Ina3221Model dev; + dev.setDieId(0x3227); + bus.attach(0x40, &dev); + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA3221); + CHECK(mon.lastDevId() == 0x3227u); + } + { /* 0x2290: a different die in the same package style must NOT match. */ + MockI2cBus bus; + Ina228Model dev(RSHUNT); + dev.setDevId(0x2290); + bus.attach(0x41, &dev); + RailMonitor mon; + CHECK(!mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::None); + /* The 0x3E/0x3F probe records the raw IDs on rejection too, keeping + * the header's "raw IDs remain readable" contract for unknown parts. */ + CHECK(mon.lastMfgId() == 0x5449u); + CHECK(mon.lastDevId() == 0x2290u); + } +} + +/* ---------- INA3221: disabled channels ---------- */ + +/* Driver has no channel-enable awareness: it neither reads nor writes the + * INA3221 CONFIG register, so channels() is always 3 and poll() reads a + * disabled channel's registers without complaint. [V: kernel ina3221.c + * refuses to report disabled channels (-ENODATA); the RTE driver returns + * their stale/zero contents as if live — reported.] */ +void test_ina3221_disabled_channel_reads() { + std::printf("[rail-adv] INA3221 disabled channel read-through\n"); + MockI2cBus bus; + Ina3221Model dev; + dev.setConfig(0x5127); /* CH2 (bit 13) disabled before any "ADC run" */ + bus.attach(0x40, &dev); + dev.setMeasurement(0, 24.0f, 0.025f); + dev.setMeasurement(1, 3.3f, 0.0025f); /* dropped: channel disabled */ + dev.setMeasurement(2, 5.0f, 0.0f); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + CHECK(mon.part() == RailMonitor::Part::INA3221); + CHECK(mon.channels() == 3); /* advertised regardless of CH_EN bits [D] */ + + RailMonitor::Sample s{9.0f, 9.0f, 9.0f}; + CHECK(mon.poll(0, s, 10)); + CHECK_NEAR(s.bus_v, 24.0, 0.005); + /* Disabled channel: poll() SUCCEEDS but returns never-converted zeros — + * indistinguishable from a genuinely idle rail. [A: held-value vs zero + * on disabled channels unverified vs datasheet text] */ + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 0.0, 1e-6); + CHECK_NEAR(s.current_a, 0.0, 1e-6); +} + +/* Disabled AFTER a conversion: registers hold the last value, so the driver + * reports stale data as live. [A: hold behavior mock-informed] */ +void test_ina3221_disabled_channel_stale() { + std::printf("[rail-adv] INA3221 disable-after-convert returns stale\n"); + MockI2cBus bus; + Ina3221Model dev; + bus.attach(0x40, &dev); + dev.setMeasurement(1, 3.3f, 0.0025f); /* converted while enabled */ + dev.setConfig(0x5127); /* then CH2 disabled */ + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 3.3, 0.005); + CHECK_NEAR(s.current_a, 0.5, 0.01); + + /* Re-enable and convert: live data again. */ + dev.setConfig(0x7127); + dev.setMeasurement(1, 7.5f, 0.0125f); + CHECK(mon.poll(1, s, 10)); + CHECK_NEAR(s.bus_v, 7.5, 0.005); + CHECK_NEAR(s.current_a, 2.5, 0.005); +} + +/* ---------- item 5: exact bus sequences ---------- */ + +bool anyBareRead(const MockI2cBus& bus) { + for (const auto& t : bus.txnLog()) { + if (t.rx_len > 0 && t.tx_len == 0) return true; + } + return false; +} + +/* INA226: probe reads 0xFE/0xFF (pointer-write + repeated-start read in one + * transfer), then a 3-byte CAL write, then a CONFIG read to confirm the MODE + * bits are continuous (POR 0x4127 already is, so no write-back). poll reads + * BUS, CURRENT, POWER. The written CAL bytes on the wire are asserted + * MSB-first. INA226 requires the register-pointer write before each read + * [V: standard SMBus read-word; the strictness the real part enforces past + * that is not observable through II2cBus — see report]. */ +void test_ina226_bus_sequence() { + std::printf("[rail-adv] INA226 bus transaction sequence\n"); + MockI2cBus bus; + Ina226Model dev; + bus.attach(0x40, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + + const auto& log = bus.txnLog(); + CHECK(log.size() == 7u); + CHECK(log[0].tx0 == 0xFE && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0xFF && log[1].tx_len == 1 && log[1].rx_len == 2); + CHECK(log[2].tx0 == 0x05 && log[2].tx_len == 3 && log[2].rx_len == 0); + CHECK(log[2].tx1 == 0x0D && log[2].tx2 == 0x1B); /* CAL 3355, MSB first */ + CHECK(log[3].tx0 == 0x00 && log[3].tx_len == 1 && log[3].rx_len == 2); /* CONFIG RMW read */ + CHECK(log[4].tx0 == 0x02 && log[4].rx_len == 2); /* BUS */ + CHECK(log[5].tx0 == 0x04 && log[5].rx_len == 2); /* CURRENT */ + CHECK(log[6].tx0 == 0x03 && log[6].rx_len == 2); /* POWER */ + CHECK(!anyBareRead(bus)); +} + +/* INA228: probe tries the legacy 0xFE/0xFF first (mismatch), then 0x3E/0x3F, + * writes SHUNT_CAL (1250 = 0x04E2), then reads CONFIG (0x00) to force + * ADCRANGE=0 — a write-back only happens if the bit was set (covered by + * test_ina228_stale_adcrange). poll does 3-byte reads. */ +void test_ina228_bus_sequence() { + std::printf("[rail-adv] INA228 bus transaction sequence\n"); + MockI2cBus bus; + Ina228Model dev(RSHUNT); + bus.attach(0x41, &dev); + + RailMonitor mon; + CHECK(mon.init(bus, 0x41, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(0, s, 10)); + + const auto& log = bus.txnLog(); + CHECK(log.size() == 9u); + CHECK(log[0].tx0 == 0xFE && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0xFF && log[1].tx_len == 1 && log[1].rx_len == 2); + CHECK(log[2].tx0 == 0x3E && log[2].tx_len == 1 && log[2].rx_len == 2); + CHECK(log[3].tx0 == 0x3F && log[3].tx_len == 1 && log[3].rx_len == 2); + CHECK(log[4].tx0 == 0x02 && log[4].tx_len == 3 && log[4].rx_len == 0); + CHECK(log[4].tx1 == 0x04 && log[4].tx2 == 0xE2); /* SHUNT_CAL 1250 = 0x04E2 */ + CHECK(log[5].tx0 == 0x00 && log[5].rx_len == 2); /* CONFIG RMW read */ + CHECK(log[6].tx0 == 0x05 && log[6].rx_len == 3); /* VBUS 24-bit */ + CHECK(log[7].tx0 == 0x07 && log[7].rx_len == 3); /* CURRENT 24-bit */ + CHECK(log[8].tx0 == 0x08 && log[8].rx_len == 3); /* POWER 24-bit */ + /* CONFIG was at POR (ADCRANGE=0), so the RMW must NOT write it back. */ + bool wrote_config = false; + for (const auto& t : log) { + if (t.tx0 == 0x00 && t.tx_len == 3) wrote_config = true; + } + CHECK(!wrote_config); + CHECK(!anyBareRead(bus)); +} + +/* INA3221 poll order: 2-byte read of the BUS register first, then the SHUNT + * register, per channel (channel n: 0x02+2n then 0x01+2n). init() adds a + * CONFIG (0x00) read-modify-write after the ID probe: CH_EN[14:12] all set + + * MODE[2:0]=111; with the POR value 0x7127 both hold, so no write-back. [D] */ +void test_ina3221_bus_sequence() { + std::printf("[rail-adv] INA3221 bus transaction sequence\n"); + MockI2cBus bus; + Ina3221Model dev; + bus.attach(0x40, &dev); + dev.setMeasurement(1, 3.3f, 0.0025f); + + RailMonitor mon; + CHECK(mon.init(bus, 0x40, RailMonitor::Config{RSHUNT, MAX_A}, 20)); + RailMonitor::Sample s{}; + CHECK(mon.poll(1, s, 10)); + + const auto& log = bus.txnLog(); + CHECK(log.size() == 5u); + CHECK(log[0].tx0 == 0xFE && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0xFF && log[1].tx_len == 1 && log[1].rx_len == 2); + CHECK(log[2].tx0 == 0x00 && log[2].tx_len == 1 && log[2].rx_len == 2); /* CONFIG RMW read */ + CHECK(log[3].tx0 == 0x04 && log[3].tx_len == 1 && log[3].rx_len == 2); + CHECK(log[4].tx0 == 0x03 && log[4].tx_len == 1 && log[4].rx_len == 2); + CHECK(!anyBareRead(bus)); +} +} // namespace + +void hosttest::test_rail_adv_suite() { + test_ina226_cal_worked_example(); + test_ina226_end_to_end_uneven_cal(); + test_ina226_overrange_wraps_negative(); + test_ina226_shunt_raw_min_negative(); + test_ina226_cal_clamps(); + test_ina228_20bit_boundaries(); + test_ina228_large_power_24bit(); + test_ina228_stale_adcrange(); + test_die_id_revision_masking(); + test_ina3221_disabled_channel_reads(); + test_ina3221_disabled_channel_stale(); + test_ina226_bus_sequence(); + test_ina228_bus_sequence(); + test_ina3221_bus_sequence(); +} diff --git a/Images/Gen7FW/MainProcessor/tests/host/test_temp.cpp b/Images/Gen7FW/MainProcessor/tests/host/test_temp.cpp new file mode 100644 index 00000000..45b16b9e --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/test_temp.cpp @@ -0,0 +1,182 @@ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" + +using Inverter::OnboardTempSensor; +using hosttest::MockI2cBus; +using hosttest::Tmp1075Model; + +namespace { + +constexpr uint8_t ADDR = 0x48; + +/* Pure decode math: TMP102-family register -> degC. */ +void test_decode_vectors() { + std::printf("[temp] register decode vectors\n"); + /* 12-bit normal mode: code = reg >> 4 (arithmetic), 0.0625 degC/LSB. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x7FF0, false), 127.9375, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x1900, false), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x0000, false), 0.0, 1e-6); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFF10, false), -0.9375, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xF900, false), -7.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x9000, false), -112.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x8000, false), -128.0, 1e-4); + + /* 13-bit extended mode (EM=1): code = reg >> 3 (arithmetic). */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x0C80, true), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x7FF8, true), 255.9375, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xF380, true), -25.0, 1e-4); + + /* EM bit extraction from a config word (EM = bit 4 of the low byte). */ + CHECK(!OnboardTempSensor::configIsExtended(0x0000)); + CHECK(OnboardTempSensor::configIsExtended(0x0010)); + CHECK(OnboardTempSensor::configIsExtended(0x0050)); /* CR1|EM */ + CHECK(!OnboardTempSensor::configIsExtended(0x1040)); /* high-byte bits */ +} + +/* Encode round-trip used by the mock to program temperatures. */ +void test_encode_roundtrip() { + std::printf("[temp] encode/decode round-trip\n"); + const float temps[] = {-40.0f, -7.0f, 0.0f, 25.0f, 85.0f, 127.9f}; + for (float t : temps) { + const uint16_t reg = OnboardTempSensor::encodeTempReg(t, false); + CHECK_NEAR(OnboardTempSensor::decodeTempC(reg, false), t, 0.0625); + } +} + +/* TMP1075 on the bus: detected, identified by DEVICE_ID 0x7500, converts. */ +void test_tmp1075_present() { + std::printf("[temp] TMP1075 present at 0x48\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Tmp1075); + CHECK(sensor.address() == ADDR); + CHECK(!sensor.extendedMode()); + + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(25.0f, false)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); + + /* Negative temperature exercise. */ + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(-7.0f, false)); + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, -7.0, 1e-3); + /* -40 C (cold-start corner). */ + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(-40.0f, false)); + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, -40.0, 0.07); +} + +/* No DEVICE_ID register -> generic family member, still converts. */ +void test_generic_present() { + std::printf("[temp] generic (TMP102/PCT2075-style) device at 0x49\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + bus.attach(0x49, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, 0x49, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + + dev.setTempRaw16(OnboardTempSensor::encodeTempReg(55.5f, false)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 55.5, 0.07); +} + +/* EM=1 in config -> 13-bit decode on the wire. EM is a TMP102-class-only + * feature: the real TMP1075 has no EM bit and its config PORs to 0x00FF, so + * this test must use the generic (TMP102-flavored) model. */ +void test_extended_mode() { + std::printf("[temp] extended (13-bit) mode, generic TMP102 flavor\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + bus.attach(ADDR, &dev); + dev.setConfig(0x0010); /* EM=1 */ + dev.setTempRaw16(0x0C80); /* 25.0 C in 13-bit mode */ + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + CHECK(sensor.extendedMode()); + + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); +} + +/* Real TMP1075: config PORs to 0x00FF with every "Not used" low bit reading + * 1 (SBOS854F Table 7-5), so an unconditional EM-bit (bit 4) check would + * latch a bogus 13-bit decode and report 2x the real temperature. The + * driver must ignore EM whenever the part was ID'd as TMP1075. */ +void test_tmp1075_config_por_is_not_em() { + std::printf("[temp] TMP1075 config POR 0x00FF must not engage EM\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + CHECK(dev.config() == 0x00FFu); + dev.setTempRaw16(0x1900); /* 25.0 C (12-bit is the only TMP1075 width) */ + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Tmp1075); + CHECK(!sensor.extendedMode()); /* bit 4 of 0x00FF is 1: must be ignored */ + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); /* 2x decode would read 50.0 C */ +} + +/* NACK at the address: init must fail cleanly, poll must not read. */ +void test_absent() { + std::printf("[temp] absent device (NACK)\n"); + MockI2cBus bus; + OnboardTempSensor sensor; + CHECK(!sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::None); + float t = 123.0f; + CHECK(!sensor.poll(t, 10)); + CHECK_NEAR(t, 123.0, 1e-6); /* output untouched on failure */ + /* poll() on an uninitialised sensor must not touch the bus. */ + OnboardTempSensor fresh; + CHECK(!fresh.poll(t, 10)); + /* The absent-probe used isReady only; no register transfers happened. */ + CHECK(bus.transferCount() == 0); +} + +/* Device vanishes after a successful init (connector pulled): subsequent + * polls NACK and must fail cleanly with the output untouched. */ +void test_loss_of_device() { + std::printf("[temp] device lost after init\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + + bus.detach(ADDR); + float t2 = 123.0f; + CHECK(!sensor.poll(t2, 10)); + CHECK_NEAR(t2, 123.0, 1e-6); /* failure leaves the output untouched */ +} + +} // namespace + +void hosttest::test_temp_suite() { + test_decode_vectors(); + test_encode_roundtrip(); + test_tmp1075_present(); + test_generic_present(); + test_extended_mode(); + test_tmp1075_config_por_is_not_em(); + test_absent(); + test_loss_of_device(); +} diff --git a/Images/Gen7FW/MainProcessor/tests/host/test_temp_adversarial.cpp b/Images/Gen7FW/MainProcessor/tests/host/test_temp_adversarial.cpp new file mode 100644 index 00000000..a62b174e --- /dev/null +++ b/Images/Gen7FW/MainProcessor/tests/host/test_temp_adversarial.cpp @@ -0,0 +1,271 @@ +/* Adversarial coverage for OnboardTempSensor (TMP1075/TMP102/PCT2075 path). + * + * Focus: incorrect-but-self-consistent behavior the happy-path tests would + * NOT catch — 12/13-bit boundary patterns, low-LSB discard, EM/data-width + * mismatches, pointer-register aliasing on generic parts, reserved-pointer + * NAKs, byte order on the wire, and exact bus transaction shapes. + * + * Provenance tags: + * [V] verified against a public datasheet-derived source (Linux kernel + * drivers, vendor library notes) during this pass + * [A] assumed / mock-informed (mock encodes the same assumption as the + * driver author; could not be re-verified against datasheet text) + * [D] documented driver behavior (characterized, not necessarily a bug) + */ +#include "test_framework.h" +#include "mock_i2c.h" + +#include "Inverter/Drivers/I2C/OnboardTempSensor.h" + +#include +#include + +using Inverter::OnboardTempSensor; +using hosttest::MockI2cBus; +using hosttest::Tmp1075Model; + +namespace { + +constexpr uint8_t ADDR = 0x48; + +/* Serves hard-coded wire bytes per pointer, like a logic-analyzer capture. */ +class TraceDevice : public hosttest::MockDevice { +public: + void serve(uint8_t ptr, uint8_t b0, uint8_t b1) { + m_bytes[ptr] = (static_cast(b0) << 8) | b1; + } + bool onWrite(const uint8_t* data, uint8_t /*len*/) override { + m_ptr = data[0]; + return true; + } + void onRead(uint8_t* out, uint8_t len) override { + const uint16_t v = m_bytes.count(m_ptr) ? m_bytes[m_ptr] : 0; + if (len >= 1) out[0] = static_cast(v >> 8); + if (len >= 2) out[1] = static_cast(v & 0xFF); + for (uint8_t i = 2; i < len; ++i) out[i] = 0; + } +private: + std::map m_bytes; + uint8_t m_ptr = 0; +}; + +/* 12-bit boundary/odd-LSB decode vectors — computed independently of both + * mock and driver (hand arithmetic on the two's-complement left-aligned + * format). Table rows marked [A] additionally appear in the TMP102 + * datasheet temperature table; the PDF was not fetchable in this session. */ +void test_decode_12bit_boundaries() { + std::printf("[temp-adv] 12-bit boundary decode\n"); + /* 0x9600 == +150 degC is NOT representable in 12-bit (code 2400 > 2047 + * wraps to -1696); +150 requires EM=1. [A: TMP102 table] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x9600, false), -106.0, 1e-4); + /* Datasheet table row: -55 degC -> 1100 1001 0000 0000. [A] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xC900, false), -55.0, 1e-4); + /* Smallest negative step: code -1, and with garbage low nibble (0xFFFF + * must still be -0.0625, not +anything — low 4 bits are not data). */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFFF0, false), -0.0625, 1e-6); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFFFF, false), -0.0625, 1e-6); + /* 0x800F: bits below the 12-bit field must be DISCARDED, not rounded — + * SAR must give -128.0, never -127.9375. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x800F, false), -128.0, 1e-4); + /* Odd LSB patterns inside the ignored nibble must not change the code. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x190F, false), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x1900, false), 25.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x19C0, false), 25.75, 1e-4); +} + +/* EM (13-bit) boundary vectors and mode/data-width mismatches. */ +void test_decode_13bit_boundaries() { + std::printf("[temp-adv] 13-bit (EM) boundary decode\n"); + /* +150 degC max in EM: code 2400 -> 2400<<3 = 0x4B00. [A: TMP102 table] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x4B00, true), 150.0, 1e-4); + /* -55 degC in EM: -880 -> 0xC90<<3 = 0xE480. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xE480, true), -55.0, 1e-4); + /* 13-bit span extremes. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x8000, true), -256.0, 1e-4); + CHECK_NEAR(OnboardTempSensor::decodeTempC(0xFFFF, true), -0.0625, 1e-6); + + /* EM=1 applied to a 12-bit-formatted pattern (0x1900 = 25 degC 12-bit) + * DOUBLES the reading -> 50.0. [D: why the driver latches EM at init] */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x1900, true), 50.0, 1e-4); + /* EM=0 applied to 13-bit-formatted data (150 degC = 0x4B00) HALVES it. */ + CHECK_NEAR(OnboardTempSensor::decodeTempC(0x4B00, false), 75.0, 1e-4); +} + +/* The EM extractor itself is mode-blind; per-part policy lives in init(). + * 0x00FF is the real TMP1075 config POR (SBOS854F Table 7-5 [V]) — bit 4 set. */ +void test_em_bit_extraction_corners() { + std::printf("[temp-adv] config EM-bit corners\n"); + CHECK(OnboardTempSensor::configIsExtended(0xFFFF)); + CHECK(OnboardTempSensor::configIsExtended(0x00F0)); + CHECK(!OnboardTempSensor::configIsExtended(0x00EF)); + CHECK(OnboardTempSensor::configIsExtended(0x00FF)); /* TMP1075 POR: ignored + by init() only because the part is positively ID'd — see test_temp.cpp + test_tmp1075_config_por_is_not_em. */ +} + +/* encode/decode round trips in both widths, incl. values only EM can hold. + * Plus: encoding +150 degC in 12-bit PRODUCES the ambiguous 0x9600 — the + * reg<->degC relation is not round-trippable at the boundary. [D] */ +void test_encode_decode_boundaries() { + std::printf("[temp-adv] encode/decode boundary round trips\n"); + const struct { float t; bool ext; } cases[] = { + {150.0f, true}, {-256.0f, true}, {-55.4375f, false}, {127.9375f, false}, + }; + for (const auto& c : cases) { + const uint16_t reg = OnboardTempSensor::encodeTempReg(c.t, c.ext); + CHECK_NEAR(OnboardTempSensor::decodeTempC(reg, c.ext), c.t, 1e-4); + } + CHECK(OnboardTempSensor::encodeTempReg(150.0f, false) == 0x9600u); + CHECK_NEAR(OnboardTempSensor::decodeTempC( + OnboardTempSensor::encodeTempReg(150.0f, false), false), + -106.0, 1e-4); +} + +/* TMP102-class generic part: pointer register masks to P1:P0, so the driver's + * DEVICE_ID probe at 0x0F lands on THIGH (POR 0x5000), not 0x0000. [A on the + * masking; SBOS397 pointer figure says only P1/P0 are used.] Either way the + * driver must classify Generic and convert normally. */ +void test_generic_pointer_aliasing() { + std::printf("[temp-adv] TMP102 probe-at-0x0F aliases THIGH\n"); + MockI2cBus bus; + Tmp1075Model dev(false); /* generic = TMP102 flavor, 2-bit pointer */ + bus.attach(ADDR, &dev); + + /* Direct read at pointer 0x0F returns THIGH bytes, not 0x0000: */ + uint8_t out[2] = {0, 0}; + const uint8_t ptr = 0x0F; + CHECK(bus.transfer(ADDR, &ptr, 1, out, 2, 10)); + CHECK(out[0] == 0x50 && out[1] == 0x00); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); /* 0x5000 != 0x7500 */ + CHECK(!sensor.extendedMode()); /* TMP102 config POR 0x60A0 -> EM=0 */ + dev.setTempRaw16(0x4B00); /* 75 degC, 12-bit */ + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 75.0, 1e-3); +} + +/* A stricter generic part may NAK the read phase at an unimplemented pointer + * (PCT2075-style reserved pointer). [A: datasheet silent; chosen mock + * behavior]. init() must survive and still classify Generic. */ +void test_generic_nak_on_id_probe() { + std::printf("[temp-adv] generic part NAKs reserved-pointer reads\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + dev.nakReadPointer(0x0F); /* masked to 0x03 in the model */ + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + dev.setTempRaw16(0x1900); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.0, 1e-3); +} + +/* Config register unreadable (NAK) on a generic part whose ADC is in EM: + * init() must not fail, must leave EM=0, and poll() then decodes the + * 13-bit-formatted data as 12-bit (150 degC hardware reads 75 degC). + * [D: characterized fallback; mismatched-width data is a hardware config + * error the driver cannot detect from the temp register alone.] */ +void test_config_read_failure_falls_back_to_12bit() { + std::printf("[temp-adv] config NAK -> 12-bit fallback\n"); + MockI2cBus bus; + Tmp1075Model dev(false); + dev.nakReadPointer(0x01); + dev.setTempRaw16(0x4B00); /* 150 degC if EM were latched */ + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Generic); + CHECK(!sensor.extendedMode()); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 75.0, 1e-3); +} + +/* Real wire bytes are MSB-first (I2C convention; INA226 datasheet quote + * "All data bytes are transmitted most significant byte first" [V via + * third-party library docs]). Serve an analyzer-style capture and check the + * driver decodes MSB-first — and characterize what a byte-swapped capture + * (wiring/tooling bug) would silently produce. */ +void test_wire_byte_order() { + std::printf("[temp-adv] wire byte order (MSB first)\n"); + MockI2cBus bus; + TraceDevice dev; + dev.serve(0x0F, 0x75, 0x00); /* DEVICE_ID -> TMP1075 */ + dev.serve(0x00, 0x19, 0x80); /* 25.5 degC, MSB first */ + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + CHECK(sensor.part() == OnboardTempSensor::Part::Tmp1075); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, 25.5, 1e-3); + + /* Byte-swapped capture of the same register: 0x8019 -> -127.9375 degC. + * The decode direction is pinned by this test; nothing in the driver can + * detect a swapped physical bus. [D] */ + dev.serve(0x00, 0x80, 0x19); + CHECK(sensor.poll(t, 10)); + CHECK_NEAR(t, -127.9375, 1e-3); +} + +/* Exact transaction shapes. Every read is a 1-byte pointer write + N-byte + * read within ONE transfer (the II2cBus contract for "pointer write, + * repeated START, read"; HalI2cBus realizes it with HAL_I2C_Mem_Read). + * A STOP-separated sequence is not expressible through II2cBus, so asserting + * (tx_len==1 whenever rx_len>0) covers the strict part of the requirement. + * [V: read-only inspection of HalI2cBus.cpp] */ +void test_transaction_shapes() { + std::printf("[temp-adv] transaction shapes\n"); + MockI2cBus bus; + Tmp1075Model dev(true); + bus.attach(ADDR, &dev); + + OnboardTempSensor sensor; + CHECK(sensor.init(bus, ADDR, 20)); + float t = 0.0f; + CHECK(sensor.poll(t, 10)); + + const auto& log = bus.txnLog(); + /* TMP1075: init reads ONLY DEVICE_ID (config read skipped by the EM fix), + * poll reads the temp register -> exactly 2 transfers. */ + CHECK(log.size() == 2u); + CHECK(log[0].tx0 == 0x0F && log[0].tx_len == 1 && log[0].rx_len == 2); + CHECK(log[1].tx0 == 0x00 && log[1].tx_len == 1 && log[1].rx_len == 2); + for (const auto& txn : log) { + CHECK(txn.ok); + CHECK(!(txn.rx_len > 0 && txn.tx_len == 0)); /* never a bare read */ + } + + /* Generic flavor also reads CONFIG during init -> 3 transfers. */ + MockI2cBus bus2; + Tmp1075Model dev2(false); + bus2.attach(ADDR, &dev2); + OnboardTempSensor s2; + CHECK(s2.init(bus2, ADDR, 20)); + const auto& log2 = bus2.txnLog(); + CHECK(log2.size() == 2u); + CHECK(log2[0].tx0 == 0x0F && log2[1].tx0 == 0x01); + CHECK(log2[0].tx_len == 1 && log2[0].rx_len == 2); +} + +} // namespace + +void hosttest::test_temp_adv_suite() { + test_decode_12bit_boundaries(); + test_decode_13bit_boundaries(); + test_em_bit_extraction_corners(); + test_encode_decode_boundaries(); + test_generic_pointer_aliasing(); + test_generic_nak_on_id_probe(); + test_config_read_failure_falls_back_to_12bit(); + test_wire_byte_order(); + test_transaction_shapes(); +}