feat: BME280 baro pressure & ambient temp (Issue #672) #673

Merged
sl-jetson merged 1 commits from sl-firmware/issue-672-bme280-baro-temp into main 2026-03-18 08:27:40 -04:00
6 changed files with 518 additions and 0 deletions
Showing only changes of commit 4318589496 - Show all commits

66
include/baro.h Normal file
View File

@ -0,0 +1,66 @@
#ifndef BARO_H
#define BARO_H
#include <stdint.h>
#include <stdbool.h>
/*
* baro BME280/BMP280 barometric pressure & ambient temperature module
* (Issue #672).
*
* Reads pressure and temperature from the BME280 at BARO_READ_HZ (1 Hz),
* computes pressure altitude using the ISA barometric formula, and publishes
* JLINK_TLM_BARO (0x8D) telemetry to the Orin at BARO_TLM_HZ (1 Hz).
*
* Runs entirely on the Mamba F722S no Orin dependency.
* Altitude is exposed via baro_get_alt_cm() for use by slope compensation
* in the balance PID (Issue #672 requirement).
*
* Usage:
* 1. Call i2c1_init() then bmp280_init() and pass the chip_id result.
* 2. Call baro_tick(now_ms) every ms from the main loop.
* 3. Call baro_get_alt_cm() to read the latest altitude.
*/
/* ---- Configuration ---- */
#define BARO_READ_HZ 1u /* sensor poll rate (Hz) */
#define BARO_TLM_HZ 1u /* JLink telemetry rate (Hz) */
/* ---- Data ---- */
typedef struct {
int32_t pressure_pa; /* barometric pressure (Pa) */
int16_t temp_x10; /* ambient temperature (°C × 10; e.g. 235 = 23.5 °C) */
int32_t alt_cm; /* pressure altitude above ISA sea level (cm) */
int16_t humidity_pct_x10; /* %RH × 10 (BME280 only); -1 if BMP280/absent */
bool valid; /* true once at least one reading has been obtained */
} baro_data_t;
/* ---- API ---- */
/*
* baro_init(chip_id) register chip type from bmp280_init() result.
* chip_id : 0x58 = BMP280, 0x60 = BME280, 0 = absent/not found.
* Call after i2c1_init() and bmp280_init(); no-op if chip_id == 0.
*/
void baro_init(int chip_id);
/*
* baro_tick(now_ms) rate-limited sensor read + JLink telemetry publish.
* Call every ms from the main loop. No-op if chip absent.
* Reads at BARO_READ_HZ; sends JLINK_TLM_BARO at BARO_TLM_HZ.
*/
void baro_tick(uint32_t now_ms);
/*
* baro_get(out) copy latest baro data into *out.
* Returns true on success; false if no valid reading yet.
*/
bool baro_get(baro_data_t *out);
/*
* baro_get_alt_cm() latest pressure altitude (cm above ISA sea level).
* Returns 0 if no valid reading. Used by slope compensation in balance PID.
*/
int32_t baro_get_alt_cm(void);
#endif /* BARO_H */

View File

@ -99,6 +99,7 @@
#define JLINK_TLM_STEERING 0x8Au /* jlink_tlm_steering_t (8 bytes, Issue #616) */
#define JLINK_TLM_LVC 0x8Bu /* jlink_tlm_lvc_t (4 bytes, Issue #613) */
#define JLINK_TLM_ODOM 0x8Cu /* jlink_tlm_odom_t (16 bytes, Issue #632) */
#define JLINK_TLM_BARO 0x8Du /* jlink_tlm_baro_t (12 bytes, Issue #672) */
#define JLINK_TLM_VESC_STATE 0x8Eu /* jlink_tlm_vesc_state_t (22 bytes, Issue #674) */
/* ---- Telemetry STATUS payload (20 bytes, packed) ---- */
@ -240,6 +241,15 @@ typedef struct __attribute__((packed)) {
int16_t speed_mmps; /* linear speed of centre point (mm/s) */
} jlink_tlm_odom_t; /* 16 bytes */
/* ---- Telemetry BARO payload (12 bytes, packed) Issue #672 ---- */
/* Sent at BARO_TLM_HZ (1 Hz); reports ambient pressure, temperature, altitude. */
typedef struct __attribute__((packed)) {
int32_t pressure_pa; /* barometric pressure (Pa) */
int16_t temp_x10; /* ambient temperature (°C × 10; e.g. 235 = 23.5 °C) */
int32_t alt_cm; /* pressure altitude above ISA sea level (cm) */
int16_t humidity_pct_x10; /* %RH × 10 (BME280 only); -1 = BMP280/absent */
} jlink_tlm_baro_t; /* 12 bytes */
/* ---- Telemetry VESC_STATE payload (22 bytes, packed) Issue #674 ---- */
/* Sent at VESC_TLM_HZ (1 Hz) by vesc_can_send_tlm(). */
typedef struct __attribute__((packed)) {
@ -394,6 +404,13 @@ void jlink_send_lvc_tlm(const jlink_tlm_lvc_t *tlm);
*/
void jlink_send_odom_tlm(const jlink_tlm_odom_t *tlm);
/*
* jlink_send_baro_tlm(tlm) - transmit JLINK_TLM_BARO (0x8D) frame
* (18 bytes total) at BARO_TLM_HZ (1 Hz). Issue #672.
* Rate-limiting handled by baro_tick(); call from there only.
*/
void jlink_send_baro_tlm(const jlink_tlm_baro_t *tlm);
/*
* jlink_send_vesc_state_tlm(tlm) - transmit JLINK_TLM_VESC_STATE (0x8E) frame
* (28 bytes total) at VESC_TLM_HZ (1 Hz). Issue #674.

90
src/baro.c Normal file
View File

@ -0,0 +1,90 @@
/*
* baro.c BME280/BMP280 barometric pressure & ambient temperature module
* (Issue #672).
*
* Reads pressure, temperature, and (on BME280) humidity from the sensor at
* BARO_READ_HZ (1 Hz). Computes pressure altitude using bmp280_pressure_to_alt_cm()
* (ISA barometric formula, p0 = 101325 Pa). Publishes JLINK_TLM_BARO (0x8D)
* telemetry to the Orin at BARO_TLM_HZ (1 Hz).
*
* Runs entirely on the Mamba F722S. No Orin dependency.
* baro_get_alt_cm() exposes altitude for slope compensation in the balance PID.
*/
#include "baro.h"
#include "bmp280.h"
#include "jlink.h"
static int s_chip_id = 0; /* 0x58=BMP280, 0x60=BME280, 0=absent */
static baro_data_t s_data; /* latest reading */
static uint32_t s_last_read_ms; /* timestamp of last I2C read */
static uint32_t s_last_tlm_ms; /* timestamp of last telemetry TX */
/* ---- baro_init() ---- */
void baro_init(int chip_id)
{
s_chip_id = chip_id;
s_data.pressure_pa = 0;
s_data.temp_x10 = 0;
s_data.alt_cm = 0;
s_data.humidity_pct_x10 = -1;
s_data.valid = false;
/*
* Initialise timestamps so the first baro_tick() call fires immediately
* (same convention as slope_estimator_init and steering_pid_init).
*/
const uint32_t interval_ms = 1000u / BARO_READ_HZ;
s_last_read_ms = (uint32_t)(-(uint32_t)interval_ms);
s_last_tlm_ms = (uint32_t)(-(uint32_t)(1000u / BARO_TLM_HZ));
}
/* ---- baro_tick() ---- */
void baro_tick(uint32_t now_ms)
{
if (s_chip_id == 0) return;
const uint32_t read_interval_ms = 1000u / BARO_READ_HZ;
if ((now_ms - s_last_read_ms) < read_interval_ms) return;
s_last_read_ms = now_ms;
/* Read pressure (Pa) and temperature (°C × 10) */
bmp280_read(&s_data.pressure_pa, &s_data.temp_x10);
/* Compute pressure altitude: ISA formula, p0 = 101325 Pa */
s_data.alt_cm = bmp280_pressure_to_alt_cm(s_data.pressure_pa);
/* Humidity: BME280 (0x60) only; BMP280 returns -1 */
s_data.humidity_pct_x10 = (s_chip_id == 0x60)
? bmp280_read_humidity()
: (int16_t)-1;
s_data.valid = true;
/* Publish telemetry to Orin via JLink (JLINK_TLM_BARO = 0x8D) */
const uint32_t tlm_interval_ms = 1000u / BARO_TLM_HZ;
if ((now_ms - s_last_tlm_ms) >= tlm_interval_ms) {
s_last_tlm_ms = now_ms;
jlink_tlm_baro_t tlm;
tlm.pressure_pa = s_data.pressure_pa;
tlm.temp_x10 = s_data.temp_x10;
tlm.alt_cm = s_data.alt_cm;
tlm.humidity_pct_x10 = s_data.humidity_pct_x10;
jlink_send_baro_tlm(&tlm);
}
}
/* ---- baro_get() ---- */
bool baro_get(baro_data_t *out)
{
if (!s_data.valid) return false;
*out = s_data;
return true;
}
/* ---- baro_get_alt_cm() ---- */
int32_t baro_get_alt_cm(void)
{
return s_data.valid ? s_data.alt_cm : 0;
}

View File

@ -663,6 +663,31 @@ void jlink_send_odom_tlm(const jlink_tlm_odom_t *tlm)
jlink_tx_locked(frame, sizeof(frame));
}
/* ---- jlink_send_baro_tlm() -- Issue #672 ---- */
void jlink_send_baro_tlm(const jlink_tlm_baro_t *tlm)
{
/*
* Frame: [STX][LEN][0x8D][12 bytes BARO][CRC_hi][CRC_lo][ETX]
* LEN = 1 + 12 = 13; total = 18 bytes
* At 921600 baud: 18×10/921600 0.20 ms safe to block.
*/
static uint8_t frame[18];
const uint8_t plen = (uint8_t)sizeof(jlink_tlm_baro_t); /* 12 */
const uint8_t len = 1u + plen; /* 13 */
frame[0] = JLINK_STX;
frame[1] = len;
frame[2] = JLINK_TLM_BARO;
memcpy(&frame[3], tlm, plen);
uint16_t crc = crc16_xmodem(&frame[2], len);
frame[3 + plen] = (uint8_t)(crc >> 8);
frame[3 + plen + 1] = (uint8_t)(crc & 0xFFu);
frame[3 + plen + 2] = JLINK_ETX;
jlink_tx_locked(frame, sizeof(frame));
}
/* ---- jlink_send_vesc_state_tlm() -- Issue #674 ---- */
void jlink_send_vesc_state_tlm(const jlink_tlm_vesc_state_t *tlm)
{

View File

@ -14,6 +14,7 @@
#include "crsf.h"
#include "i2c1.h"
#include "bmp280.h"
#include "baro.h"
#include "mag.h"
#include "bno055.h"
#include "jetson_cmd.h"
@ -295,6 +296,7 @@ int main(void) {
if (i2c1_init() == 0) {
int chip = bmp280_init();
baro_chip = (chip > 0) ? chip : 0;
baro_init(baro_chip); /* Issue #672: 1 Hz baro read + JLink telemetry */
mag_type = mag_init();
ina219_init(); /* Init INA219 dual motor current monitoring (Issue #214) */
}
@ -371,6 +373,9 @@ int main(void) {
/* LVC: update low-voltage protection state machine (Issue #613) */
lvc_tick(now, battery_read_mv());
/* Baro: 1 Hz BME280 read + JLink telemetry (Issue #672) */
baro_tick(now);
/* Sleep LED: software PWM on LED1 (active-low PC15) driven by PM brightness.
* pm_pwm_phase rolls over each ms; brightness sets duty cycle 0-255. */
pm_pwm_phase++;

315
test/test_baro.c Normal file
View File

@ -0,0 +1,315 @@
/*
* test_baro.c Unit tests for the baro module (Issue #672).
*
* Build (host, no hardware):
* gcc -I include -I test/stubs -DTEST_HOST -lm \
* -o /tmp/test_baro src/baro.c test/test_baro.c
*
* All tests are self-contained; no HAL, no I2C, no UART required.
* bmp280_init/read/humidity/alt and jlink_send_baro_tlm are stubbed below.
*/
/* ---- Stubs ---- */
/* Prevent jlink.h from pulling in HAL / pid_flash types */
#define JLINK_H
#include <stdint.h>
#include <stdbool.h>
/* Minimal jlink_tlm_baro_t matching the real definition */
typedef struct __attribute__((packed)) {
int32_t pressure_pa;
int16_t temp_x10;
int32_t alt_cm;
int16_t humidity_pct_x10;
} jlink_tlm_baro_t;
/* Capture last transmitted baro tlm for inspection */
static jlink_tlm_baro_t g_last_baro_tlm;
static int g_baro_tlm_count = 0;
void jlink_send_baro_tlm(const jlink_tlm_baro_t *tlm)
{
g_last_baro_tlm = *tlm;
g_baro_tlm_count++;
}
/* Stub bmp280_read() — returns configurable values */
static int32_t g_stub_pressure_pa = 101325;
static int16_t g_stub_temp_x10 = 230; /* 23.0 °C */
void bmp280_read(int32_t *pressure_pa, int16_t *temp_x10)
{
*pressure_pa = g_stub_pressure_pa;
*temp_x10 = g_stub_temp_x10;
}
/* Stub bmp280_read_humidity() */
static int16_t g_stub_humidity = 500; /* 50.0 %RH */
int16_t bmp280_read_humidity(void)
{
return g_stub_humidity;
}
/* Stub bmp280_pressure_to_alt_cm() — proportional approx for testing */
int32_t bmp280_pressure_to_alt_cm(int32_t pressure_pa)
{
/* Simplified: sea level = 101325 Pa = 0 cm; decrease of 12 Pa ≈ 100 cm */
return (int32_t)((101325 - pressure_pa) * 100 / 12);
}
/* ---- Include implementation directly ---- */
#include "../src/baro.c"
/* ---- Test framework ---- */
#include <stdio.h>
#include <math.h>
#include <string.h>
static int g_pass = 0;
static int g_fail = 0;
#define ASSERT(cond, msg) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL [%s:%d] %s\n", __FILE__, __LINE__, msg); } \
} while(0)
/* ---- Helper ---- */
static void reset_stubs(void)
{
g_stub_pressure_pa = 101325;
g_stub_temp_x10 = 230;
g_stub_humidity = 500;
g_baro_tlm_count = 0;
memset(&g_last_baro_tlm, 0, sizeof(g_last_baro_tlm));
}
/* ---- Tests ---- */
static void test_init_absent_chip_no_read(void)
{
reset_stubs();
baro_init(0); /* chip absent */
/* baro_tick must be a no-op */
baro_tick(1000u);
ASSERT(g_baro_tlm_count == 0, "absent chip: no telemetry sent");
baro_data_t d;
ASSERT(!baro_get(&d), "absent chip: baro_get returns false");
ASSERT(baro_get_alt_cm() == 0, "absent chip: baro_get_alt_cm returns 0");
}
static void test_first_tick_fires_immediately(void)
{
/*
* After baro_init() timestamps are pre-wound so that the very first
* baro_tick() triggers a read and a telemetry send.
*/
reset_stubs();
baro_init(0x60); /* BME280 */
baro_tick(1000u);
ASSERT(g_baro_tlm_count == 1, "first tick sends telemetry immediately");
baro_data_t d;
ASSERT(baro_get(&d), "first tick: data valid");
}
static void test_data_populated_after_tick(void)
{
reset_stubs();
g_stub_pressure_pa = 100000;
g_stub_temp_x10 = 150; /* 15.0 °C */
g_stub_humidity = 400; /* 40.0 %RH */
baro_init(0x60);
baro_tick(0u);
baro_data_t d;
ASSERT(baro_get(&d), "data valid after tick");
ASSERT(d.pressure_pa == 100000, "pressure_pa correct");
ASSERT(d.temp_x10 == 150, "temp_x10 correct");
ASSERT(d.humidity_pct_x10 == 400, "humidity correct (BME280)");
ASSERT(d.valid, "valid flag set");
}
static void test_altitude_computed(void)
{
reset_stubs();
g_stub_pressure_pa = 101325 - 120; /* 120 Pa below sea level → ~1000 cm */
baro_init(0x58); /* BMP280 */
baro_tick(0u);
baro_data_t d;
baro_get(&d);
/* stub: alt_cm = (101325 - 101205)*100/12 = 12000/12 = 1000 cm */
ASSERT(d.alt_cm == 1000, "altitude computed correctly");
ASSERT(baro_get_alt_cm() == 1000, "baro_get_alt_cm matches");
}
static void test_bmp280_no_humidity(void)
{
reset_stubs();
baro_init(0x58); /* BMP280 — no humidity */
baro_tick(0u);
baro_data_t d;
baro_get(&d);
ASSERT(d.humidity_pct_x10 == (int16_t)-1,
"BMP280: humidity_pct_x10 == -1");
}
static void test_bme280_has_humidity(void)
{
reset_stubs();
g_stub_humidity = 650;
baro_init(0x60); /* BME280 */
baro_tick(0u);
baro_data_t d;
baro_get(&d);
ASSERT(d.humidity_pct_x10 == 650,
"BME280: humidity_pct_x10 populated");
}
static void test_rate_limited_1hz(void)
{
/*
* At BARO_READ_HZ = 1, only one read per second.
* Call baro_tick() every ms for 3000 ms expect 3 sends.
*/
reset_stubs();
baro_init(0x60);
for (uint32_t ms = 0; ms < 3000u; ms++) {
baro_tick(ms);
}
ASSERT(g_baro_tlm_count == 3,
"1 Hz rate limit: 3 sends in 3000 ms");
}
static void test_no_tlm_before_interval(void)
{
reset_stubs();
baro_init(0x60);
/* First tick at ms=0 fires (pre-wound). Next must not fire until ms=1000. */
baro_tick(0u);
ASSERT(g_baro_tlm_count == 1, "first tick sends");
baro_tick(500u); /* only 500 ms later — must not send again */
ASSERT(g_baro_tlm_count == 1, "second tick before interval: no extra send");
}
static void test_tlm_payload_encoding(void)
{
reset_stubs();
g_stub_pressure_pa = 95000;
g_stub_temp_x10 = -50; /* -5.0 °C */
g_stub_humidity = 900;
baro_init(0x60);
baro_tick(0u);
ASSERT(g_baro_tlm_count == 1, "TLM sent");
ASSERT(g_last_baro_tlm.pressure_pa == 95000,
"TLM: pressure_pa encoded");
ASSERT(g_last_baro_tlm.temp_x10 == (int16_t)-50,
"TLM: negative temp encoded");
ASSERT(g_last_baro_tlm.humidity_pct_x10 == 900,
"TLM: humidity encoded");
/* alt_cm via stub: (101325-95000)*100/12 = 632500/12 ≈ 52708 */
ASSERT(g_last_baro_tlm.alt_cm == g_last_baro_tlm.alt_cm, /* sanity */
"TLM: alt_cm field present");
/* verify alt matches baro_get */
baro_data_t d;
baro_get(&d);
ASSERT(g_last_baro_tlm.alt_cm == d.alt_cm,
"TLM: alt_cm matches baro_get");
}
static void test_get_returns_false_before_first_tick(void)
{
reset_stubs();
baro_init(0x60);
/* Trick: call with a time that won't fire yet by advancing only 1 ms
* BUT init pre-winds timestamps so tick at 0 fires. Test the initial
* state by checking valid flag before any tick. */
/* We can observe this by checking the internal s_data.valid = false
* by attempting a get on a freshly reinitialised module.
* We simulate by re-init'ing with chip=0 so tick is no-op. */
baro_init(0); /* chip absent — tick will never fire */
baro_data_t d;
ASSERT(!baro_get(&d), "no tick yet: baro_get returns false");
}
static void test_get_alt_zero_before_valid(void)
{
reset_stubs();
baro_init(0); /* no chip → never valid */
ASSERT(baro_get_alt_cm() == 0, "alt_cm == 0 when no valid data");
}
static void test_data_updates_each_interval(void)
{
/*
* Change stub pressure between ticks.
* Verify baro_get() reflects the latest reading.
*/
reset_stubs();
baro_init(0x58);
g_stub_pressure_pa = 101325;
baro_tick(0u);
baro_data_t d1;
baro_get(&d1);
g_stub_pressure_pa = 100000;
baro_tick(1000u); /* one interval later */
baro_data_t d2;
baro_get(&d2);
ASSERT(d1.pressure_pa == 101325, "first reading correct");
ASSERT(d2.pressure_pa == 100000, "second reading updated");
ASSERT(d1.alt_cm != d2.alt_cm, "alt_cm updates between reads");
}
static void test_reinit_clears_valid(void)
{
reset_stubs();
baro_init(0x60);
baro_tick(0u);
baro_data_t d;
ASSERT(baro_get(&d), "data valid after tick");
/* Re-init with absent chip — valid flag cleared */
baro_init(0);
ASSERT(!baro_get(&d), "after re-init (absent): valid cleared");
ASSERT(baro_get_alt_cm() == 0, "after re-init (absent): alt == 0");
}
/* ---- main ---- */
int main(void)
{
printf("=== test_baro ===\n");
test_init_absent_chip_no_read();
test_first_tick_fires_immediately();
test_data_populated_after_tick();
test_altitude_computed();
test_bmp280_no_humidity();
test_bme280_has_humidity();
test_rate_limited_1hz();
test_no_tlm_before_interval();
test_tlm_payload_encoding();
test_get_returns_false_before_first_tick();
test_get_alt_zero_before_valid();
test_data_updates_each_interval();
test_reinit_clears_valid();
printf("\nResults: %d passed, %d failed\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}