Files
qtalker---/firmware/esp32p4-sensor-node/test/test_bmp280_compensate.c
Indiana 0966fa8cfc test: make firmware logic bugs catchable without hardware (Workstream F)
The firmware has never been flashed, and a real bug already reached the
repo because of it: RD03E_FRAME_LEN was 5 for a 6-byte frame, so the footer
check collided with the distance high byte and EVERY distance reading was
garbage — always `lo | 0x5500`, about 218 metres, regardless of what the
sensor saw. That was pure logic with no hardware dependency. It should have
been catchable on a laptop, and there was simply no way to run the code.

Extracted the hardware-free logic out of the three drivers — rd03e_parse,
bmp280_compensate, mems_level — as moves rather than rewrites, carrying the
explanatory comments along with the code they explain. The drivers now own
only their bus I/O and call into the pure units, so nothing changes for the
real device.

`./run_tests.sh` builds them with gcc -Wall -Wextra -Werror plus a
dependency-free assert harness: 175 checks, 0 failed, from a clean tree.

Proven to catch the actual bug rather than assumed to: reintroducing
FRAME_LEN 5 fails four checks, including one that reads "a simple-report
frame is 6 bytes, not 5", plus the truncated-frame and 5-byte-window cases.
Restored, green again.

This does NOT make the firmware verified, and the README says so plainly —
it is called a narrow exception and scoped to pure logic. Wiring, timing,
real register behaviour and the reconstructed RD-03E frame format all still
need the physical board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:17:35 +00:00

168 lines
7.2 KiB
C

// BMP280 compensation tests.
//
// Two kinds of check here, and it is worth being clear which is which:
//
// 1. Byte-order / packing checks. These are exact and they are the same
// class of bug as the RD-03E frame-length bug — a swapped LSB/MSB or a
// mis-shifted XLSB nibble is pure logic and needs no sensor to catch.
//
// 2. Arithmetic checks against the calibration/ADC values that appear in
// Bosch's own worked reference example (dig_T1=27504 ... dig_P9=6000,
// adc_T=519888, adc_P=415148, documented as ~25.08 degC / ~100653 Pa).
// These pin the transcription of the datasheet formulas. They prove the
// maths matches the reference — NOT that a real BMP280 wired to this
// board reports these registers.
#include "../main/bmp280_compensate.h"
#include "test_util.h"
#include <string.h>
// The Bosch reference example's calibration set.
static const uint16_t REF_T1 = 27504;
static const int16_t REF_T2 = 26435;
static const int16_t REF_T3 = -1000;
static const uint16_t REF_P1 = 36477;
static const int16_t REF_P2 = -10685;
static const int16_t REF_P3 = 3024;
static const int16_t REF_P4 = 2855;
static const int16_t REF_P5 = 140;
static const int16_t REF_P6 = -7;
static const int16_t REF_P7 = 15500;
static const int16_t REF_P8 = -14600;
static const int16_t REF_P9 = 6000;
// Pack a coefficient the way the register map stores it: LSB then MSB.
static void put16(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)(v >> 8);
}
static void ref_calib_bytes(uint8_t buf[BMP280_CALIB_LEN]) {
put16(&buf[0], REF_T1);
put16(&buf[2], (uint16_t)REF_T2);
put16(&buf[4], (uint16_t)REF_T3);
put16(&buf[6], REF_P1);
put16(&buf[8], (uint16_t)REF_P2);
put16(&buf[10], (uint16_t)REF_P3);
put16(&buf[12], (uint16_t)REF_P4);
put16(&buf[14], (uint16_t)REF_P5);
put16(&buf[16], (uint16_t)REF_P6);
put16(&buf[18], (uint16_t)REF_P7);
put16(&buf[20], (uint16_t)REF_P8);
put16(&buf[22], (uint16_t)REF_P9);
}
void test_bmp280_compensate(void) {
SUITE("bmp280_compensate");
bmp280_calib_t c;
{
uint8_t buf[BMP280_CALIB_LEN];
ref_calib_bytes(buf);
memset(&c, 0, sizeof(c));
bmp280_calib_from_regs(buf, &c);
// --- calibration decoding: little-endian, signedness preserved ---
CHECK_EQ_U(c.dig_T1, REF_T1, "dig_T1 unsigned little-endian");
CHECK(c.dig_T2 == REF_T2, "dig_T2 signed little-endian");
CHECK(c.dig_T3 == REF_T3, "dig_T3 must stay negative (%d)", (int)c.dig_T3);
CHECK_EQ_U(c.dig_P1, REF_P1, "dig_P1 unsigned little-endian");
CHECK(c.dig_P2 == REF_P2, "dig_P2 must stay negative (%d)", (int)c.dig_P2);
CHECK(c.dig_P3 == REF_P3, "dig_P3");
CHECK(c.dig_P4 == REF_P4, "dig_P4");
CHECK(c.dig_P5 == REF_P5, "dig_P5");
CHECK(c.dig_P6 == REF_P6, "dig_P6 must stay negative (%d)", (int)c.dig_P6);
CHECK(c.dig_P7 == REF_P7, "dig_P7");
CHECK(c.dig_P8 == REF_P8, "dig_P8 must stay negative (%d)", (int)c.dig_P8);
CHECK(c.dig_P9 == REF_P9, "dig_P9");
// dig_T1 = 27504 = 0x6B70, so bytes are 0x70 then 0x6B. A swapped
// decode would give 0x706B = 28779.
CHECK_EQ_U(buf[0], 0x70, "calib byte 0 is the LSB");
CHECK_EQ_U(buf[1], 0x6B, "calib byte 1 is the MSB");
}
// --- 20-bit ADC word decoding ---------------------------------------
{
// adc = MSB<<12 | LSB<<4 | XLSB>>4.
// 519888 = 0x7EED0 -> MSB 0x7E, LSB 0xED, XLSB top nibble 0x0.
// 415148 = 0x655AC -> MSB 0x65, LSB 0x5A, XLSB top nibble 0xC.
const uint8_t raw[BMP280_RAW_LEN] = {
0x65, 0x5A, 0xC0, // pressure (0xF7..0xF9)
0x7E, 0xED, 0x00, // temperature (0xFA..0xFC)
};
int32_t adc_P = 0, adc_T = 0;
bmp280_adc_from_regs(raw, &adc_P, &adc_T);
CHECK_EQ_U(adc_P, 415148, "adc_P: pressure comes FIRST in the burst read");
CHECK_EQ_U(adc_T, 519888, "adc_T: temperature comes SECOND in the burst read");
// The XLSB's low nibble is padding and must be discarded.
const uint8_t raw2[BMP280_RAW_LEN] = {
0x65, 0x5A, 0xCF, // low nibble of XLSB set — must be ignored
0x7E, 0xED, 0x0F,
};
bmp280_adc_from_regs(raw2, &adc_P, &adc_T);
CHECK_EQ_U(adc_P, 415148, "adc_P ignores the XLSB's low nibble");
CHECK_EQ_U(adc_T, 519888, "adc_T ignores the XLSB's low nibble");
}
// --- the reference worked example ------------------------------------
double t_fine = 0.0;
{
double temp_c = bmp280_compensate_temperature(&c, 519888, &t_fine);
CHECK_NEAR(temp_c, 25.08, 0.02, "Bosch reference adc_T yields ~25.08 degC");
CHECK(t_fine > 0.0, "t_fine is written for the pressure stage");
double press_pa = bmp280_compensate_pressure(&c, 415148, t_fine);
CHECK_NEAR(press_pa, 100653.0, 2.0, "Bosch reference adc_P yields ~100653 Pa");
// Sanity in the unit the driver actually reports (hPa).
CHECK(press_pa / 100.0 > 800.0 && press_pa / 100.0 < 1100.0,
"pressure in hPa lands in a physically plausible band (%.2f)", press_pa / 100.0);
}
// --- physical sanity: temperature moves the right way ----------------
{
double tf_cold = 0.0, tf_hot = 0.0;
double cold = bmp280_compensate_temperature(&c, 400000, &tf_cold);
double hot = bmp280_compensate_temperature(&c, 600000, &tf_hot);
CHECK(cold < hot, "a larger raw temperature ADC means a warmer reading");
CHECK(tf_cold < tf_hot, "t_fine tracks temperature");
CHECK(cold > -50.0 && hot < 100.0,
"both readings stay in the sensor's operating band (%.2f, %.2f)", cold, hot);
}
// --- physical sanity: pressure falls monotonically with altitude -----
{
// Raw pressure ADC is inversely related to pressure in this part
// (the formula starts from 1048576 - adc_P), so sweeping adc_P
// upward is a stand-in for climbing. Pressure must fall the whole
// way, with no sign flip or discontinuity.
double prev = 1e18;
for (int32_t adc_P = 380000; adc_P <= 460000; adc_P += 5000) {
double p = bmp280_compensate_pressure(&c, adc_P, t_fine);
CHECK(p < prev, "pressure decreases monotonically at adc_P=%d (%.2f >= %.2f)",
(int)adc_P, p, prev);
CHECK(p > 50000.0 && p < 130000.0,
"pressure stays physically plausible at adc_P=%d (%.2f Pa)", (int)adc_P, p);
prev = p;
}
}
// --- the divide-by-zero guard returns 0, it does not crash -----------
{
// An all-zero calibration block is what you get if the I2C read
// silently failed. dig_P1 == 0 makes var1 == 0.
bmp280_calib_t zero;
memset(&zero, 0, sizeof(zero));
double p = bmp280_compensate_pressure(&zero, 415148, 100000.0);
CHECK(p == 0.0, "var1 == 0 must return exactly 0.0, not inf/NaN (got %.6f)", p);
// Same story if only dig_P1 is zero but the rest is real.
bmp280_calib_t no_p1 = c;
no_p1.dig_P1 = 0;
double p2 = bmp280_compensate_pressure(&no_p1, 415148, t_fine);
CHECK(p2 == 0.0, "dig_P1 == 0 must return exactly 0.0 (got %.6f)", p2);
}
}