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.
This commit is contained in:
Jason
2026-04-29 18:55:48 +05:45
parent ea2615ef84
commit 4b142166be
5 changed files with 337 additions and 59 deletions
@@ -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, &reg, 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, &reg, 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;
}
@@ -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);
};
@@ -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
@@ -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 $@
@@ -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 <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
/* -------------------------------------------------------------------------
* 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;
}