From 4b142166bec32bfa8ec9d20225e8e6fdab7a72f2 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:55:48 +0545 Subject: [PATCH] mcu(bmp180): replace in-band sentinel + fix uint16->int16 narrowing (AUDIT-C17) BMP180_ERROR=255 was an in-band sentinel returned by uint16_t I/O helpers (read16, readRawTemperature) on I2C failure. 255 is also a valid uint16 register reading (0x00FF appears across the calibration block and is reachable as a raw temperature/pressure sample), so a sensor failure was indistinguishable from a real reading. getTemperature() additionally narrowed the uint16_t raw read to int16_t before passing to computeB5(). Raw bit-patterns >= 0x8000 (reachable across the BMP180 -40..+85 C operating window) flipped to negative int16_t and sign-extended into computeB5(), producing temperature errors of order 100s of C (e.g. -347 C instead of +51 C for raw UT = 0x8000). Fix: - Internal I/O helpers (read8/read16/readRawTemperature/readRawPressure) now return bool and pass the value through an out-param. None of the new sentinels collide with valid sensor output: * getTemperature -> NaN on error * getPressure -> INT32_MIN on error * getSeaLevelPressure -> INT32_MIN on error - getTemperature() keeps raw as uint16_t and widens value-preservingly via (int32_t)raw before computeB5(). - readRawPressure() reads XLSB through the bool-out-param contract; previously OR'd in 0xFF on I2C fail, silently corrupting the LSB. Verification: test_audit_c17_bmp180_sentinel_and_cast 4/4 PASS, including datasheet UT=27898 -> 15.0 C reproduction and 64/64 finite outputs across a full uint16 sweep (vs 32/32 collapses in the upper half under the buggy narrowing). Full MCU regression 32/32 PASS. Caller-side: no external code references BMP180_ERROR; main.cpp's existing range check at the health-watchdog catches INT32_MIN via the < 30000.0 branch. --- .../9_1_1_C_Cpp_Libraries/BMP180.cpp | 116 ++++---- .../9_1_1_C_Cpp_Libraries/BMP180.h | 16 +- .../9_1_Microcontroller/tests/.gitignore | 1 + 9_Firmware/9_1_Microcontroller/tests/Makefile | 4 + .../test_audit_c17_bmp180_sentinel_and_cast.c | 259 ++++++++++++++++++ 5 files changed, 337 insertions(+), 59 deletions(-) create mode 100644 9_Firmware/9_1_Microcontroller/tests/test_audit_c17_bmp180_sentinel_and_cast.c diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.cpp index 7603ad0..68f2c02 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.cpp @@ -76,8 +76,7 @@ BMP180::BMP180(BMP180_RESOLUTION res_mode) /**************************************************************************/ int32_t BMP180::getPressure(void) { - int32_t UT = 0; - int32_t UP = 0; + int32_t UP_signed = 0; int32_t B3 = 0; int32_t B5 = 0; int32_t B6 = 0; @@ -88,13 +87,16 @@ int32_t BMP180::getPressure(void) uint32_t B4 = 0; uint32_t B7 = 0; - UT = readRawTemperature(); //read uncompensated temperature, 16-bit - if (UT == BMP180_ERROR) return BMP180_ERROR; //error handler, collision on i2c bus + /* AUDIT-C17: uint16_t raw_UT is widened to int32_t value-preserving; + * uint32_t raw_UP fits in int32_t (19-bit max). */ + uint16_t raw_UT = 0; + uint32_t raw_UP = 0; - UP = readRawPressure(); //read uncompensated pressure, 19-bit - if (UP == BMP180_ERROR) return BMP180_ERROR; //error handler, collision on i2c bus + if (!readRawTemperature(&raw_UT)) return INT32_MIN; //I2C error sentinel (cannot collide with valid reading) + if (!readRawPressure(&raw_UP)) return INT32_MIN; - B5 = computeB5(UT); + B5 = computeB5((int32_t)raw_UT); + UP_signed = (int32_t)raw_UP; /* pressure calculation */ B6 = B5 - 4000; @@ -107,9 +109,9 @@ int32_t BMP180::getPressure(void) X2 = ((int32_t)_calCoeff.bmpB1 * ((B6 * B6) >> 12)) >> 16; X3 = ((X1 + X2) + 2) >> 2; B4 = ((uint32_t)_calCoeff.bmpAC4 * (X3 + 32768L)) >> 15; - B7 = (UP - B3) * (50000UL >> _resolution); - - if (B4 == 0) return BMP180_ERROR; //safety check, avoiding division by zero + B7 = (UP_signed - B3) * (50000UL >> _resolution); + + if (B4 == 0) return INT32_MIN; //safety check, avoiding division by zero if (B7 < 0x80000000) pressure = (B7 * 2) / B4; else pressure = (B7 / B4) * 2; @@ -133,10 +135,16 @@ int32_t BMP180::getPressure(void) /**************************************************************************/ float BMP180::getTemperature(void) { - int16_t rawTemperature = readRawTemperature(); + /* AUDIT-C17: was `int16_t rawTemperature = readRawTemperature();` + * which silently narrowed uint16_t→int16_t. Bit-patterns ≥ 0x8000 (reachable + * across the BMP180 -40..+85 °C window) became negative int16_t and + * sign-extended to large negative int32_t inside computeB5(), producing + * temperature errors of order 100s of °C. Keep raw as uint16_t and widen + * to int32_t value-preservingly. */ + uint16_t rawTemperature = 0; + if (!readRawTemperature(&rawTemperature)) return NAN; //I2C error sentinel (cannot collide with any valid float reading) - if (rawTemperature == BMP180_ERROR) return BMP180_ERROR; //error handler, collision on i2c bus - return (float)((computeB5(rawTemperature) + 8) >> 4) / 10; + return (float)((computeB5((int32_t)rawTemperature) + 8) >> 4) / 10; } /**************************************************************************/ @@ -162,8 +170,8 @@ int32_t BMP180::getSeaLevelPressure(int16_t trueAltitude) { int32_t pressure = getPressure(); - if (pressure == BMP180_ERROR) return BMP180_ERROR; - return (pressure / pow(1.0 - (float)trueAltitude / 44330, 5.255)); + if (pressure == INT32_MIN) return INT32_MIN; //propagate I2C error sentinel + return (pressure / pow(1.0 - (float)trueAltitude / 44330, 5.255)); } /**************************************************************************/ @@ -194,7 +202,9 @@ void BMP180::softReset(void) /**************************************************************************/ uint8_t BMP180::readFirmwareVersion(void) { - return read8(BMP180_GET_VERSION_REG); + uint8_t v = 0; + read8(BMP180_GET_VERSION_REG, &v); //best-effort telemetry; v stays 0 on I2C error + return v; } /**************************************************************************/ @@ -206,8 +216,10 @@ uint8_t BMP180::readFirmwareVersion(void) /**************************************************************************/ uint8_t BMP180::readDeviceID(void) { - if (read8(BMP180_GET_ID_REG) == BMP180_CHIP_ID) return 180; - return false; + uint8_t id = 0; + if (!read8(BMP180_GET_ID_REG, &id)) return 0; //I2C error + if (id == BMP180_CHIP_ID) return 180; + return 0; //chip mismatch } /**************************************************************************/ @@ -224,13 +236,11 @@ uint8_t BMP180::readDeviceID(void) /**************************************************************************/ bool BMP180::readCalibrationCoefficients() { - int32_t value = 0; + uint16_t value = 0; for (uint8_t reg = BMP180_CAL_AC1_REG; reg <= BMP180_CAL_MD_REG; reg++) { - value = read16(reg); - - if (value == BMP180_ERROR) return false; //error handler, collision on i2c bus + if (!read16(reg, &value)) return false; //AUDIT-C17: bool out-param signals I2C error without colliding with valid uint16 cal byte (e.g. 0x00FF) switch (reg) { @@ -290,16 +300,16 @@ bool BMP180::readCalibrationCoefficients() Reads raw/uncompensated temperature value, 16-bit */ /**************************************************************************/ -uint16_t BMP180::readRawTemperature(void) +bool BMP180::readRawTemperature(uint16_t* out) { /* send temperature measurement command */ - if (write8(BMP180_START_MEASURMENT_REG, BMP180_GET_TEMPERATURE_CTRL) != true) return BMP180_ERROR; //error handler, collision on i2c bus + if (!write8(BMP180_START_MEASURMENT_REG, BMP180_GET_TEMPERATURE_CTRL)) return false; //I2C error /* set measurement delay */ HAL_Delay(5); - /* read result */ - return read16(BMP180_READ_ADC_MSB_REG); //reads msb + lsb + /* read result (msb + lsb); read16 sets *out only on success */ + return read16(BMP180_READ_ADC_MSB_REG, out); } /**************************************************************************/ @@ -309,10 +319,11 @@ uint16_t BMP180::readRawTemperature(void) Reads raw/uncompensated pressure value, 19-bits */ /**************************************************************************/ -uint32_t BMP180::readRawPressure(void) +bool BMP180::readRawPressure(uint32_t* out) { uint8_t regControl = 0; - uint32_t rawPressure = 0; + uint16_t msb_lsb = 0; + uint8_t xlsb = 0; /* convert resolution to register control */ switch (_resolution) @@ -335,7 +346,7 @@ uint32_t BMP180::readRawPressure(void) } /* send pressure measurement command */ - if (write8(BMP180_START_MEASURMENT_REG, regControl) != true) return BMP180_ERROR; //error handler, collision on i2c bus + if (!write8(BMP180_START_MEASURMENT_REG, regControl)) return false; //I2C error /* set measurement delay */ switch (_resolution) @@ -357,17 +368,15 @@ uint32_t BMP180::readRawPressure(void) break; } - /* read result msb + lsb */ - rawPressure = read16(BMP180_READ_ADC_MSB_REG); //16-bits - if (rawPressure == BMP180_ERROR) return BMP180_ERROR; //error handler, collision on i2c bus - - /* read result xlsb */ - rawPressure <<= 8; - rawPressure |= read8(BMP180_READ_ADC_XLSB_REG); //19-bits + /* read msb+lsb and xlsb separately; signal failure on either */ + if (!read16(BMP180_READ_ADC_MSB_REG, &msb_lsb)) return false; + if (!read8(BMP180_READ_ADC_XLSB_REG, &xlsb)) return false; //AUDIT-C17: previously OR'd in sentinel 0xFF on I2C fail, silently corrupting LSB + uint32_t rawPressure = ((uint32_t)msb_lsb << 8) | xlsb; //19-bits before shift rawPressure >>= (8 - _resolution); - return rawPressure; + *out = rawPressure; + return true; } /**************************************************************************/ @@ -396,20 +405,21 @@ int32_t BMP180::computeB5(int32_t UT) Reads 8-bit value over I2C */ /**************************************************************************/ -uint8_t BMP180::read8(uint8_t reg) +bool BMP180::read8(uint8_t reg, uint8_t* out) { uint8_t data = 0; HAL_StatusTypeDef status; - + // Write register address status = HAL_I2C_Master_Transmit(&hi2c3, BMP180_ADDRESS, ®, 1, I2C_TIMEOUT); - if (status != HAL_OK) return BMP180_ERROR; - + if (status != HAL_OK) return false; + // Read data from register status = HAL_I2C_Master_Receive(&hi2c3, BMP180_ADDRESS, &data, 1, I2C_TIMEOUT); - if (status != HAL_OK) return BMP180_ERROR; - - return data; + if (status != HAL_OK) return false; + + *out = data; + return true; } /**************************************************************************/ @@ -420,24 +430,22 @@ uint8_t BMP180::read8(uint8_t reg) */ /**************************************************************************/ -uint16_t BMP180::read16(uint8_t reg) +bool BMP180::read16(uint8_t reg, uint16_t* out) { uint8_t data[2] = {0, 0}; - uint16_t value = 0; HAL_StatusTypeDef status; - + // Write register address status = HAL_I2C_Master_Transmit(&hi2c3, BMP180_ADDRESS, ®, 1, I2C_TIMEOUT); - if (status != HAL_OK) return BMP180_ERROR; - + if (status != HAL_OK) return false; + // Read 2 bytes from register status = HAL_I2C_Master_Receive(&hi2c3, BMP180_ADDRESS, data, 2, I2C_TIMEOUT); - if (status != HAL_OK) return BMP180_ERROR; - + if (status != HAL_OK) return false; + // Combine bytes (MSB first) - value = (data[0] << 8) | data[1]; - - return value; + *out = ((uint16_t)data[0] << 8) | data[1]; + return true; } diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.h b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.h index 88754bc..cb6269e 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.h +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/BMP180.h @@ -101,7 +101,13 @@ BMP180_CAL_REG; /* misc */ #define BMP180_CHIP_ID 0x55 //id number -#define BMP180_ERROR 255 //returns 255, if communication error is occurred +/* AUDIT-C17: in-band sentinel BMP180_ERROR=255 removed. + * 255 is a perfectly valid uint16_t reading from any cal-coefficient or + * raw-temperature/pressure register, so a sensor failure could not be + * distinguished from a real reading. I/O helpers now return bool and pass + * the value through an out-param; getTemperature() returns NaN on error, + * getPressure()/getSeaLevelPressure() return INT32_MIN. None of these + * sentinels collide with valid sensor output. */ #define BMP180_ADDRESS (0x77 << 1) // HAL requires 7-bit address shifted left by 1 bit @@ -145,11 +151,11 @@ class BMP180 uint8_t _resolution; bool readCalibrationCoefficients(void); - uint16_t readRawTemperature(void); - uint32_t readRawPressure(void); + bool readRawTemperature(uint16_t* out); + bool readRawPressure(uint32_t* out); int32_t computeB5(int32_t UT); - uint8_t read8(uint8_t reg); - uint16_t read16(uint8_t reg); + bool read8(uint8_t reg, uint8_t* out); + bool read16(uint8_t reg, uint16_t* out); bool write8(uint8_t reg, uint8_t control); }; diff --git a/9_Firmware/9_1_Microcontroller/tests/.gitignore b/9_Firmware/9_1_Microcontroller/tests/.gitignore index acc7942..76ad7af 100644 --- a/9_Firmware/9_1_Microcontroller/tests/.gitignore +++ b/9_Firmware/9_1_Microcontroller/tests/.gitignore @@ -23,6 +23,7 @@ test_gap3_emergency_stop_rails # TESTS_STANDALONE test_bug12_pa_cal_loop_inverted test_bug13_dac2_adc_buffer_mismatch +test_audit_c17_bmp180_sentinel_and_cast test_gap3_iwdg_config test_gap3_temperature_max test_gap3_idq_periodic_reread diff --git a/9_Firmware/9_1_Microcontroller/tests/Makefile b/9_Firmware/9_1_Microcontroller/tests/Makefile index 4140b54..1ab768a 100644 --- a/9_Firmware/9_1_Microcontroller/tests/Makefile +++ b/9_Firmware/9_1_Microcontroller/tests/Makefile @@ -72,6 +72,7 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \ test_mcu_a6_recovery_dispatch \ test_mcu_a2_mag_declination \ test_mcu_a4_ocxo_warm_restart \ + test_audit_c17_bmp180_sentinel_and_cast \ test_gap3_iwdg_config \ test_gap3_temperature_max \ test_gap3_idq_periodic_reread \ @@ -183,6 +184,9 @@ test_mcu_a2_mag_declination: test_mcu_a2_mag_declination.c test_mcu_a4_ocxo_warm_restart: test_mcu_a4_ocxo_warm_restart.c $(CC) $(CFLAGS) $< -o $@ +test_audit_c17_bmp180_sentinel_and_cast: test_audit_c17_bmp180_sentinel_and_cast.c + $(CC) $(CFLAGS) $< -lm -o $@ + # Gap-3 safety tests -- mock-only (needs spy log for GPIO sequence) test_gap3_emergency_stop_rails: test_gap3_emergency_stop_rails.c $(MOCK_OBJS) $(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) -o $@ diff --git a/9_Firmware/9_1_Microcontroller/tests/test_audit_c17_bmp180_sentinel_and_cast.c b/9_Firmware/9_1_Microcontroller/tests/test_audit_c17_bmp180_sentinel_and_cast.c new file mode 100644 index 0000000..a928b35 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_audit_c17_bmp180_sentinel_and_cast.c @@ -0,0 +1,259 @@ +/******************************************************************************* + * test_audit_c17_bmp180_sentinel_and_cast.c + * + * AUDIT-C17: BMP180 driver had two latent bugs in its temperature path: + * + * (a) BMP180_ERROR=255 was an in-band sentinel returned by uint16_t + * read16()/readRawTemperature() on I2C error. 255 is also a valid + * uint16_t register reading (0x00FF appears across the calibration + * coefficient block and is reachable as a raw temperature/pressure + * sample). Sensor failure was indistinguishable from a real reading. + * + * (b) getTemperature() narrowed the uint16_t raw value to int16_t before + * calling computeB5(), which takes int32_t. Bit-patterns ≥ 0x8000 + * (reachable across the BMP180 -40..+85 °C operating window) flipped + * to negative int16_t and sign-extended into computeB5(), producing + * temperature errors of order 100s of °C. + * + * Production fix: + * - I/O helpers (read8/read16/readRawTemperature/readRawPressure) now + * return bool and pass the value through an out-param. getTemperature + * returns NaN on error; getPressure/getSeaLevelPressure return + * INT32_MIN. None of these sentinels collide with valid sensor output. + * - getTemperature() keeps raw as uint16_t and widens to int32_t + * value-preservingly: `(int32_t)raw_uint16` instead of `(int16_t)raw`. + * + * This test models the corrected math (computeB5 + getTemperature) plus the + * casting choices and asserts: + * T1: bool out-param signaling is distinguishable from any valid uint16 + * (incl. 0x00FF, 0x8000, 0xFFFF — all of which collided with the old + * BMP180_ERROR=255 OR-with-narrowing scheme). + * T2: corrected widen-cast yields the Bosch-reference result for a + * calibrated sample (datasheet example UT=27898 -> 15.0 °C). + * T3: the buggy narrowing cast produces catastrophically wrong output + * for raw UT = 0x8000 (regression guard — flipping the rawTemperature + * declaration back to int16_t would re-trigger it). + * T4: full-range sweep — no raw uint16 in [0, 65535] should produce + * NaN/error from the corrected pipeline; under the buggy pipeline the + * upper half of the range collapses to negative output. + ******************************************************************************/ +#include +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------------- + * Bosch BMP180 datasheet example calibration (Table 6, datasheet rev 2.5) + * ------------------------------------------------------------------------- */ +typedef struct { + int16_t AC1; + int16_t AC2; + int16_t AC3; + uint16_t AC4; + uint16_t AC5; + uint16_t AC6; + int16_t B1; + int16_t B2; + int16_t MB; + int16_t MC; + int16_t MD; +} BMP180_CAL; + +static const BMP180_CAL DATASHEET_CAL = { + .AC1 = 408, .AC2 = -72, .AC3 = -14383, + .AC4 = 32741, .AC5 = 32757, .AC6 = 23153, + .B1 = 6190, .B2 = 4, + .MB = -32768, .MC = -8711, .MD = 2868, +}; + +/* ------------------------------------------------------------------------- + * Mirrors BMP180::computeB5(int32_t UT) at BMP180.cpp:393-399. + * Identical math; the only thing this test varies is the caller's choice of + * cast on the way in. + * ------------------------------------------------------------------------- */ +static int32_t computeB5(const BMP180_CAL *cal, int32_t UT) +{ + int32_t X1 = ((UT - (int32_t)cal->AC6) * (int32_t)cal->AC5) >> 15; + int32_t X2 = ((int32_t)cal->MC << 11) / (X1 + (int32_t)cal->MD); + return X1 + X2; +} + +/* CORRECTED path: keep raw as uint16_t, widen to int32_t value-preservingly. + * Mirrors the patched BMP180::getTemperature() at BMP180.cpp:136-148. */ +static float getTemperature_fixed(const BMP180_CAL *cal, uint16_t raw) +{ + return (float)((computeB5(cal, (int32_t)raw) + 8) >> 4) / 10.0f; +} + +/* BUGGY path: narrow uint16_t → int16_t before widening to int32_t. + * Mirrors the original BMP180::getTemperature() at HEAD ea2615e. */ +static float getTemperature_buggy(const BMP180_CAL *cal, uint16_t raw) +{ + int16_t narrowed = (int16_t)raw; + return (float)((computeB5(cal, (int32_t)narrowed) + 8) >> 4) / 10.0f; +} + +/* ------------------------------------------------------------------------- + * Mock I/O helper modeling the new bool-out-param contract. + * ------------------------------------------------------------------------- */ +typedef struct { + bool i2c_will_fail; + uint16_t programmed_value; +} MockI2C; + +static bool mock_readRawTemperature(MockI2C *m, uint16_t *out) +{ + if (m->i2c_will_fail) return false; + *out = m->programmed_value; + return true; +} + +/* OLD contract (regression model): in-band BMP180_ERROR=255 sentinel, + * uint16_t return. 255 is a valid reading; we cannot distinguish a real + * raw=255 reading from a sensor failure. */ +#define OLD_BMP180_ERROR 255 +static uint16_t mock_readRawTemperature_old(MockI2C *m) +{ + if (m->i2c_will_fail) return OLD_BMP180_ERROR; + return m->programmed_value; +} + +/* ------------------------------------------------------------------------- + * T1: sentinel separability under the new contract. + * ------------------------------------------------------------------------- */ +static void test_t1_sentinel_separability(void) +{ + printf(" T1: bool-out-param sentinel separability ... "); + + MockI2C m; + uint16_t value; + + /* The exact set called out in the audit memo: every reading that + * collides with BMP180_ERROR=255 under the old in-band scheme. */ + const uint16_t collision_cases[] = { 0, 1, 254, 255, 256, 32767, 32768, 65535 }; + const size_t N = sizeof(collision_cases) / sizeof(collision_cases[0]); + + for (size_t i = 0; i < N; i++) { + m.i2c_will_fail = false; + m.programmed_value = collision_cases[i]; + value = 0xDEAD; + bool ok = mock_readRawTemperature(&m, &value); + assert(ok == true); + assert(value == collision_cases[i]); + } + + /* I2C-error path: bool=false, out untouched. */ + m.i2c_will_fail = true; + value = 0xDEAD; + bool ok = mock_readRawTemperature(&m, &value); + assert(ok == false); + assert(value == 0xDEAD); /* out-param NOT clobbered on error */ + + /* Regression demonstration: under the OLD contract, raw=255 and an I2C + * fault produce the same return value, so the caller cannot tell them + * apart. This is the bug the new contract eliminates. */ + m.i2c_will_fail = false; + m.programmed_value = 255; + uint16_t v_real = mock_readRawTemperature_old(&m); + m.i2c_will_fail = true; + uint16_t v_fault = mock_readRawTemperature_old(&m); + assert(v_real == v_fault); /* old contract: indistinguishable */ + + printf("PASS\n"); +} + +/* ------------------------------------------------------------------------- + * T2: datasheet reference value reproduces under the corrected cast. + * + * Bosch BMP180 datasheet (Section 3.5, "Calculating pressure and + * temperature") worked example: with the calibration above and + * UT=27898, the expected temperature is 15.0 °C. + * ------------------------------------------------------------------------- */ +static void test_t2_datasheet_reference(void) +{ + printf(" T2: datasheet UT=27898 -> 15.0 °C (fixed cast) ... "); + float t = getTemperature_fixed(&DATASHEET_CAL, 27898); + assert(fabsf(t - 15.0f) < 0.05f); + printf("PASS (got %.2f °C)\n", (double)t); +} + +/* ------------------------------------------------------------------------- + * T3: regression guard for the narrowing bug. + * + * For raw UT = 0x8000 (32768), the corrected cast yields ~+51 °C; the + * buggy narrow-cast yields ~-347 °C. The two paths must diverge by + * hundreds of °C — that is exactly the operational hazard. + * ------------------------------------------------------------------------- */ +static void test_t3_narrowing_regression(void) +{ + printf(" T3: raw UT=0x8000 fixed vs buggy diverge by >100 °C ... "); + + float t_fixed = getTemperature_fixed(&DATASHEET_CAL, 0x8000); + float t_buggy = getTemperature_buggy(&DATASHEET_CAL, 0x8000); + + /* Fixed path lands in a plausible (if hot) range. */ + assert(t_fixed > 30.0f && t_fixed < 80.0f); + + /* Buggy path is wildly negative — far outside any real sensor range. */ + assert(t_buggy < -100.0f); + + /* The catastrophic divergence is the actual regression signal. */ + assert(fabsf(t_fixed - t_buggy) > 100.0f); + + printf("PASS (fixed=%.1f, buggy=%.1f, delta=%.1f °C)\n", + (double)t_fixed, (double)t_buggy, + (double)fabsf(t_fixed - t_buggy)); +} + +/* ------------------------------------------------------------------------- + * T4: full uint16 range sweep — fixed path stays finite + monotonic-ish; + * buggy path collapses across the 0x8000 boundary. + * ------------------------------------------------------------------------- */ +static void test_t4_full_range_sweep(void) +{ + printf(" T4: full uint16 sweep — fixed path finite, buggy collapses ... "); + + int total_samples = 0; + int upper_half_samples = 0; + int buggy_collapses = 0; + int fixed_finite = 0; + + /* Sample raw values across the full uint16 range every 1024 LSB — + * enough to exercise the 0x8000 boundary without spamming the log. */ + for (uint32_t raw32 = 0; raw32 <= 0xFFFF; raw32 += 1024) { + uint16_t raw = (uint16_t)raw32; + float t_fix = getTemperature_fixed(&DATASHEET_CAL, raw); + float t_bug = getTemperature_buggy(&DATASHEET_CAL, raw); + + total_samples++; + if (isfinite(t_fix)) fixed_finite++; + + /* Boundary crossing: at raw>=0x8000, the buggy path goes negative + * (wildly) while the fixed path keeps climbing. */ + if (raw >= 0x8000) { + upper_half_samples++; + if (t_bug < -50.0f) buggy_collapses++; + } + } + + assert(fixed_finite == total_samples); /* every sample finite under fixed path */ + assert(buggy_collapses == upper_half_samples); /* every upper-half sample collapsed under buggy path */ + + printf("PASS (fixed_finite=%d/%d, buggy_collapses=%d/%d upper-half)\n", + fixed_finite, total_samples, buggy_collapses, upper_half_samples); +} + +int main(void) +{ + printf("=== AUDIT-C17: BMP180 sentinel separability + signed-cast fix ===\n"); + + test_t1_sentinel_separability(); + test_t2_datasheet_reference(); + test_t3_narrowing_regression(); + test_t4_full_range_sweep(); + + printf("=== ALL PASS ===\n"); + return 0; +}