From 6b3b8f5f17e20dd3df9b6d4ec930aa28e4a2480d Mon Sep 17 00:00:00 2001 From: Mikhail Kozorovitskiy Date: Thu, 2 Apr 2026 11:43:55 -0500 Subject: [PATCH 1/3] fw/drivers: add BMP390 barometer driver and pressure/altitude API Implement full BMP390 barometer driver with I2C communication, calibration data parsing, and configurable oversampling/filtering. Add pressure service applib layer exposing barometric pressure (Pa) and altitude (cm) to watchapps via the native SDK. Includes: - BMP390 driver: init, forced-mode reads, compensation math per datasheet - Pressure service: subscribe/unsubscribe, caching, ISA altitude calc - I2C burst read support for nRF5 platform - Console command for live pressure/altitude readout - Exported symbols for native SDK integration Co-Authored-By: Claude Opus 4.6 --- src/fw/applib/pressure_service.c | 177 ++++++ src/fw/applib/pressure_service.h | 97 ++++ src/fw/applib/pressure_service_private.h | 21 + src/fw/console/prompt_commands.h | 7 + src/fw/drivers/i2c/nrf5.c | 32 +- src/fw/drivers/pressure.h | 36 ++ src/fw/drivers/pressure/bmp390.c | 534 +++++++++++++++++- .../process_management/pebble_process_info.h | 3 +- src/fw/process_state/app_state/app_state.c | 9 + src/fw/process_state/app_state/app_state.h | 3 + .../generate_native_sdk/exported_symbols.json | 56 +- waftools/openocd_swd_cmsisdap.cfg | 1 + 12 files changed, 944 insertions(+), 32 deletions(-) create mode 100644 src/fw/applib/pressure_service.c create mode 100644 src/fw/applib/pressure_service.h create mode 100644 src/fw/applib/pressure_service_private.h diff --git a/src/fw/applib/pressure_service.c b/src/fw/applib/pressure_service.c new file mode 100644 index 000000000..198f945f7 --- /dev/null +++ b/src/fw/applib/pressure_service.c @@ -0,0 +1,177 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "pressure_service.h" +#include "pressure_service_private.h" + +#include "drivers/pressure.h" +#include "process_state/app_state/app_state.h" +#include "system/logging.h" + +#include + +#define STANDARD_SEA_LEVEL_PA 101325 + +// Timer intervals in ms for each ODR preset +static uint32_t prv_odr_to_interval_ms(PressureODR odr) { + switch (odr) { + case PRESSURE_ODR_1HZ: return 1000; + case PRESSURE_ODR_5HZ: return 200; + case PRESSURE_ODR_10HZ: return 100; + case PRESSURE_ODR_25HZ: return 40; + case PRESSURE_ODR_50HZ: return 20; + default: return 1000; + } +} + +static PressureServiceState *prv_get_state(void) { + return app_state_get_pressure_state(); +} + +static void prv_compute_altitude(PressureServiceState *state, PressureData *data) { + int32_t ref = state->ref_pressure_pa; + if (ref <= 0) { + ref = STANDARD_SEA_LEVEL_PA; + } + + if (state->use_full_formula) { + data->altitude_cm = pressure_get_altitude_full_cm(data->pressure_pa, ref); + } else { + data->altitude_cm = pressure_get_altitude_cm(data->pressure_pa, ref); + } +} + +static void prv_timer_callback(void *context) { + PressureServiceState *state = prv_get_state(); + if (!state->handler) { + return; + } + + PressureData data = { 0 }; + if (!pressure_read(&data.pressure_pa, &data.temperature_centideg)) { + return; + } + + prv_compute_altitude(state, &data); + state->handler(&data); +} + +void pressure_service_subscribe(PressureDataHandler handler, PressureODR odr) { + PressureServiceState *state = prv_get_state(); + + // Clean up any existing subscription + if (state->poll_timer) { + app_timer_cancel(state->poll_timer); + state->poll_timer = NULL; + } + + state->handler = handler; + state->odr = odr; + + if (!handler) { + return; + } + + // Configure hardware for the requested rate + pressure_set_odr(odr); + + uint32_t interval_ms = prv_odr_to_interval_ms(odr); + state->poll_timer = app_timer_register_repeatable(interval_ms, + prv_timer_callback, + NULL, + true /* repeating */); +} + +void pressure_service_unsubscribe(void) { + PressureServiceState *state = prv_get_state(); + + if (state->poll_timer) { + app_timer_cancel(state->poll_timer); + state->poll_timer = NULL; + } + state->handler = NULL; + + // Drop hardware back to low-power mode + pressure_set_odr(PRESSURE_ODR_1HZ); +} + +bool pressure_service_set_data_rate(PressureODR odr) { + PressureServiceState *state = prv_get_state(); + + if (!state->handler || !state->poll_timer) { + return false; + } + + if (!pressure_set_odr(odr)) { + return false; + } + + state->odr = odr; + uint32_t interval_ms = prv_odr_to_interval_ms(odr); + app_timer_reschedule(state->poll_timer, interval_ms); + return true; +} + +bool pressure_service_set_reference(void) { + PressureServiceState *state = prv_get_state(); + + int32_t pressure_pa; + if (!pressure_read(&pressure_pa, NULL)) { + return false; + } + + state->ref_pressure_pa = pressure_pa; + return true; +} + +void pressure_service_set_reference_pressure(int32_t ref_pressure_pa) { + PressureServiceState *state = prv_get_state(); + state->ref_pressure_pa = ref_pressure_pa; +} + +void pressure_service_use_full_formula(bool enable) { + PressureServiceState *state = prv_get_state(); + state->use_full_formula = enable; +} + +bool pressure_service_peek(PressureData *data) { + if (!data) { + return false; + } + + PressureServiceState *state = prv_get_state(); + + if (!pressure_read(&data->pressure_pa, &data->temperature_centideg)) { + return false; + } + + prv_compute_altitude(state, data); + return true; +} + +int32_t pressure_service_get_altitude_cm(int32_t pressure_pa, int32_t ref_pressure_pa) { + PressureServiceState *state = prv_get_state(); + if (state->use_full_formula) { + return pressure_get_altitude_full_cm(pressure_pa, ref_pressure_pa); + } else { + return pressure_get_altitude_cm(pressure_pa, ref_pressure_pa); + } +} + +void pressure_service_state_init(PressureServiceState *state) { + *state = (PressureServiceState) { + .handler = NULL, + .poll_timer = NULL, + .ref_pressure_pa = 0, + .odr = PRESSURE_ODR_1HZ, + .use_full_formula = false, + }; +} + +void pressure_service_state_deinit(PressureServiceState *state) { + if (state->poll_timer) { + app_timer_cancel(state->poll_timer); + state->poll_timer = NULL; + } + state->handler = NULL; +} diff --git a/src/fw/applib/pressure_service.h b/src/fw/applib/pressure_service.h new file mode 100644 index 000000000..721a898bf --- /dev/null +++ b/src/fw/applib/pressure_service.h @@ -0,0 +1,97 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include "drivers/pressure.h" + +#include +#include + +//! @addtogroup Foundation +//! @{ +//! @addtogroup EventService +//! @{ +//! @addtogroup PressureService +//! +//! \brief Using the Pebble barometric pressure sensor +//! +//! The PressureService provides access to the barometric pressure sensor for +//! altitude tracking. Apps can subscribe to periodic pressure/altitude readings, +//! configure the data rate, set a reference altitude, and choose between a fast +//! linear approximation or the full barometric formula for altitude calculation. +//! +//! Typical usage for a skydiving altimeter: +//! \code +//! // On the ground, before jump: +//! pressure_service_subscribe(my_handler, PRESSURE_ODR_1HZ); +//! pressure_service_set_reference(); // zero altitude here +//! pressure_service_use_full_formula(true); +//! +//! // When altitude > 30m, switch to high rate: +//! pressure_service_set_data_rate(PRESSURE_ODR_25HZ); +//! +//! // On landing, drop back: +//! pressure_service_set_data_rate(PRESSURE_ODR_1HZ); +//! \endcode +//! @{ + +//! Pressure/altitude data delivered to the app handler +typedef struct { + int32_t pressure_pa; //!< Absolute pressure in Pascals + int32_t temperature_centideg; //!< Temperature in 0.01 degrees C + int32_t altitude_cm; //!< Altitude in centimeters relative to reference +} PressureData; + +//! Callback type for pressure data events +//! @param data Pointer to the latest pressure reading +typedef void (*PressureDataHandler)(PressureData *data); + +//! Subscribe to the pressure data service. Once subscribed, the handler is +//! called at the rate specified by \p odr. +//! @param handler Callback to receive pressure data +//! @param odr The desired output data rate +void pressure_service_subscribe(PressureDataHandler handler, PressureODR odr); + +//! Unsubscribe from the pressure data service. +void pressure_service_unsubscribe(void); + +//! Change the data rate while subscribed. +//! This reconfigures the sensor hardware for the new rate. +//! @param odr The desired output data rate +//! @return true if the rate was changed successfully +bool pressure_service_set_data_rate(PressureODR odr); + +//! Capture the current pressure as the reference (altitude = 0). +//! Subsequent altitude readings will be relative to this reference. +//! If no reference is set, altitude is computed relative to standard +//! sea-level pressure (101325 Pa). +//! @return true if the reference was captured successfully +bool pressure_service_set_reference(void); + +//! Set the reference pressure to an explicit value. +//! @param ref_pressure_pa Reference pressure in Pascals +void pressure_service_set_reference_pressure(int32_t ref_pressure_pa); + +//! Enable or disable the full barometric formula for altitude calculation. +//! When disabled, a faster linear approximation is used (accurate within ~1000m). +//! When enabled, a lookup-table-based hypsometric formula is used (accurate to ~7500m). +//! @param enable true to use the full formula, false for linear approximation +void pressure_service_use_full_formula(bool enable); + +//! Read the current pressure data without a subscription (one-shot). +//! @param[out] data Pointer to a PressureData struct to fill +//! @return true if the read succeeded +bool pressure_service_peek(PressureData *data); + +//! Calculate altitude relative to a given reference pressure. +//! Uses the full hypsometric formula or linear approximation depending on +//! the current formula setting (see \ref pressure_service_use_full_formula). +//! @param pressure_pa Current pressure in Pascals +//! @param ref_pressure_pa Reference pressure in Pascals (e.g. 101325 for MSL) +//! @return altitude in centimeters relative to the reference pressure +int32_t pressure_service_get_altitude_cm(int32_t pressure_pa, int32_t ref_pressure_pa); + +//! @} // end addtogroup PressureService +//! @} // end addtogroup EventService +//! @} // end addtogroup Foundation diff --git a/src/fw/applib/pressure_service_private.h b/src/fw/applib/pressure_service_private.h new file mode 100644 index 000000000..d000dc5eb --- /dev/null +++ b/src/fw/applib/pressure_service_private.h @@ -0,0 +1,21 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include "pressure_service.h" +#include "app_timer.h" + +typedef struct PressureServiceState { + PressureDataHandler handler; + AppTimer *poll_timer; + int32_t ref_pressure_pa; //!< Reference pressure for altitude (0 = use standard 101325 Pa) + PressureODR odr; + bool use_full_formula; +} PressureServiceState; + +//! Initialize the pressure service state for a new app. Called during app_state_init. +void pressure_service_state_init(PressureServiceState *state); + +//! Clean up any active subscriptions. Called during app teardown. +void pressure_service_state_deinit(PressureServiceState *state); diff --git a/src/fw/console/prompt_commands.h b/src/fw/console/prompt_commands.h index 45da26b41..57e39827c 100644 --- a/src/fw/console/prompt_commands.h +++ b/src/fw/console/prompt_commands.h @@ -64,6 +64,10 @@ extern void command_accel_status(void); extern void command_accel_selftest(void); extern void command_accel_softreset(void); +extern void command_pressure_read(void); +extern void command_pressure_debug(void); +extern void command_pressure_reinit(void); + extern void command_dump_flash(const char*, const char*); extern void command_crc_flash(const char*, const char*); extern void command_format_flash(void); @@ -297,6 +301,9 @@ extern void command_force_deepwfi(const char *arg); static const Command s_prompt_commands[] = { // PULSE entry point, needed for anything PULSE-related to work { "PULSEv1", pulse_start, 0 }, + { "baroreinit", command_pressure_reinit, 0 }, + { "barodbg", command_pressure_debug, 0 }, + { "baro", command_pressure_read, 0 }, #if KEEP_NON_ESSENTIAL_COMMANDS == 1 // ==================================================================================== // NOTE: The following commands are used by test automation. diff --git a/src/fw/drivers/i2c/nrf5.c b/src/fw/drivers/i2c/nrf5.c index 08e59c7ba..552a74d38 100644 --- a/src/fw/drivers/i2c/nrf5.c +++ b/src/fw/drivers/i2c/nrf5.c @@ -8,6 +8,8 @@ #include "system/passert.h" #include "drivers/periph_config.h" + +#include #include "FreeRTOS.h" #include @@ -61,21 +63,34 @@ void i2c_hal_abort_transfer(I2CBus *bus) { void i2c_hal_init_transfer(I2CBus *bus) { } +// Buffer for combining register address + write data into a single TX transfer. +// TXTX (two-part TX) on nRF52840 can fail to transmit the secondary buffer +// correctly due to TWIM peripheral errata. Using a single TX avoids this. +#define I2C_WRITE_BUF_MAX 32 +static uint8_t s_write_buf[I2C_WRITE_BUF_MAX]; + void i2c_hal_start_transfer(I2CBus *bus) { nrfx_twim_xfer_desc_t desc; - + desc.address = bus->state->transfer.device_address >> 1; if (bus->state->transfer.type == I2CTransferType_SendRegisterAddress) { if (bus->state->transfer.direction == I2CTransferDirection_Read) { desc.type = NRFX_TWIM_XFER_TXRX; + desc.primary_length = 1; + desc.p_primary_buf = &bus->state->transfer.register_address; + desc.secondary_length = bus->state->transfer.size; + desc.p_secondary_buf = bus->state->transfer.data; } else { - desc.type = NRFX_TWIM_XFER_TXTX; + // Combine register address + data into one TX to avoid TXTX errata + PBL_ASSERTN(bus->state->transfer.size + 1 <= I2C_WRITE_BUF_MAX); + s_write_buf[0] = bus->state->transfer.register_address; + memcpy(&s_write_buf[1], bus->state->transfer.data, bus->state->transfer.size); + desc.type = NRFX_TWIM_XFER_TX; + desc.primary_length = bus->state->transfer.size + 1; + desc.p_primary_buf = s_write_buf; + desc.secondary_length = 0; + desc.p_secondary_buf = NULL; } - desc.primary_length = 1; - desc.p_primary_buf = &bus->state->transfer.register_address; - - desc.secondary_length = bus->state->transfer.size; - desc.p_secondary_buf = bus->state->transfer.data; } else { if (bus->state->transfer.direction == I2CTransferDirection_Read) { desc.type = NRFX_TWIM_XFER_RX; @@ -85,8 +100,9 @@ void i2c_hal_start_transfer(I2CBus *bus) { desc.primary_length = bus->state->transfer.size; desc.p_primary_buf = bus->state->transfer.data; desc.secondary_length = 0; + desc.p_secondary_buf = NULL; } - + nrfx_err_t rv = nrfx_twim_xfer(&bus->hal->twim, &desc, 0); PBL_ASSERTN(rv == NRFX_SUCCESS); } diff --git a/src/fw/drivers/pressure.h b/src/fw/drivers/pressure.h index 05a583748..8504d6cd6 100644 --- a/src/fw/drivers/pressure.h +++ b/src/fw/drivers/pressure.h @@ -4,6 +4,42 @@ #pragma once #include +#include //! Initialize the pressure sensor driver. Call this once at startup. void pressure_init(void); + +//! Read the current pressure and temperature from the sensor. +//! @param pressure_pa Output: pressure in Pascals (NULL to skip) +//! @param temperature_centideg Output: temperature in 0.01 degrees C (NULL to skip) +//! @return true if read succeeded, false on error or if sensor not initialized +bool pressure_read(int32_t *pressure_pa, int32_t *temperature_centideg); + +//! Calculate altitude from pressure using a linear approximation of the +//! barometric formula. Accurate within ~1000m of the reference pressure altitude. +//! @param pressure_pa Current pressure in Pascals +//! @param sea_level_pa Reference sea-level pressure in Pascals (e.g. 101325) +//! @return altitude in centimeters relative to the reference pressure +int32_t pressure_get_altitude_cm(int32_t pressure_pa, int32_t sea_level_pa); + +//! Calculate altitude using the full hypsometric barometric formula with a +//! lookup table. Accurate across the full range 0–7500m. +//! @param pressure_pa Current pressure in Pascals +//! @param ref_pressure_pa Reference pressure in Pascals (e.g. ground-level reading) +//! @return altitude in centimeters relative to the reference pressure +int32_t pressure_get_altitude_full_cm(int32_t pressure_pa, int32_t ref_pressure_pa); + +//! BMP390 output data rate presets +typedef enum { + PRESSURE_ODR_1HZ = 0, //!< 1 Hz — low power, good for background monitoring + PRESSURE_ODR_5HZ, //!< 5 Hz — moderate rate + PRESSURE_ODR_10HZ, //!< 10 Hz — responsive + PRESSURE_ODR_25HZ, //!< 25 Hz — high rate, suitable for skydiving + PRESSURE_ODR_50HZ, //!< 50 Hz — very high rate +} PressureODR; + +//! Configure the sensor output data rate and adjust oversampling/filter +//! to match. Higher rates reduce oversampling for faster response. +//! @param odr The desired output data rate +//! @return true if configuration succeeded +bool pressure_set_odr(PressureODR odr); diff --git a/src/fw/drivers/pressure/bmp390.c b/src/fw/drivers/pressure/bmp390.c index 7d5446a18..8cb2d4c5d 100644 --- a/src/fw/drivers/pressure/bmp390.c +++ b/src/fw/drivers/pressure/bmp390.c @@ -4,39 +4,531 @@ #include "board/board.h" #include "drivers/pressure.h" #include "drivers/i2c.h" +#include "console/prompt.h" #include "system/logging.h" -#define BMP390_CHIP_ID 0x00 -#define BMP390_CHIP_ID_VALUE 0x60 -#define BMP390_PWR_CTRL 0x1B +#include "kernel/util/delay.h" +#include +#include -static bool prv_read_register(I2CSlavePort *i2c, uint8_t register_address, uint8_t *result) { - i2c_use(i2c); - bool rv = i2c_write_block(i2c, 1, ®ister_address); - if (rv) - rv = i2c_read_block(i2c, 1, result); - i2c_release(i2c); +// BMP390 Register Map +#define BMP390_REG_CHIP_ID 0x00 +#define BMP390_REG_DATA 0x04 // 6 bytes: press[0:2], temp[0:2] +#define BMP390_REG_INT_STATUS 0x11 +#define BMP390_REG_PWR_CTRL 0x1B +#define BMP390_REG_OSR 0x1C +#define BMP390_REG_ODR 0x1D +#define BMP390_REG_CONFIG 0x1F +#define BMP390_REG_CALIB_DATA 0x31 // 21 bytes of calibration coefficients +#define BMP390_REG_CMD 0x7E + +#define BMP390_CHIP_ID_VALUE 0x60 +#define BMP390_CALIB_DATA_LEN 21 +#define BMP390_DATA_LEN 6 + +// PWR_CTRL bits +#define BMP390_PRESS_EN (1 << 0) +#define BMP390_TEMP_EN (1 << 1) +#define BMP390_MODE_NORMAL (3 << 4) +#define BMP390_MODE_FORCED (1 << 4) + +// Oversampling settings (for OSR register) +// pressure osr[2:0], temperature osr[5:3] +#define BMP390_OSR_P_1X 0x00 // 1x pressure oversampling +#define BMP390_OSR_P_2X 0x01 // 2x pressure oversampling +#define BMP390_OSR_P_4X 0x02 // 4x pressure oversampling +#define BMP390_OSR_P_8X 0x03 // 8x pressure oversampling +#define BMP390_OSR_T_1X (0x00 << 3) // 1x temperature oversampling + +// IIR filter coefficients (for CONFIG register, bits [3:1]) +#define BMP390_IIR_COEFF_0 (0x00 << 1) // Off +#define BMP390_IIR_COEFF_1 (0x01 << 1) +#define BMP390_IIR_COEFF_4 (0x02 << 1) + +// ODR register values (from BMP390 datasheet Table 22) +#define BMP390_ODR_200HZ 0x00 +#define BMP390_ODR_100HZ 0x01 +#define BMP390_ODR_50HZ 0x02 +#define BMP390_ODR_25HZ 0x03 +#define BMP390_ODR_12P5HZ 0x04 +#define BMP390_ODR_6P25HZ 0x05 +#define BMP390_ODR_3P125HZ 0x06 +#define BMP390_ODR_1P5625HZ 0x07 +#define BMP390_ODR_0P78125HZ 0x08 +#define BMP390_ODR_0P390HZ 0x09 + +// INT_STATUS bits +#define BMP390_DRDY (1 << 3) + +// Calibration coefficient storage +typedef struct { + uint16_t par_t1; + uint16_t par_t2; + int8_t par_t3; + int16_t par_p1; + int16_t par_p2; + int8_t par_p3; + int8_t par_p4; + uint16_t par_p5; + uint16_t par_p6; + int8_t par_p7; + int8_t par_p8; + int16_t par_p9; + int8_t par_p10; + int8_t par_p11; + int64_t t_lin; // linearized temperature for pressure compensation +} BMP390CalibData; + +static BMP390CalibData s_calib; +static bool s_initialized; + +// I2C helpers using the PebbleOS I2C API +static bool prv_read_register(uint8_t reg, uint8_t *result) { + i2c_use(I2C_BMP390); + bool rv = i2c_read_register(I2C_BMP390, reg, result); + i2c_release(I2C_BMP390); + return rv; +} + +static bool prv_read_register_block(uint8_t reg, uint32_t len, uint8_t *buf) { + i2c_use(I2C_BMP390); + bool rv = i2c_read_register_block(I2C_BMP390, reg, len, buf); + i2c_release(I2C_BMP390); return rv; } -static bool prv_write_register(I2CSlavePort *i2c, uint8_t register_address, uint8_t datum) { - i2c_use(i2c); - uint8_t d[2] = { register_address, datum }; - bool rv = i2c_write_block(i2c, 2, d); - i2c_release(i2c); +static bool prv_write_register(uint8_t reg, uint8_t value) { + i2c_use(I2C_BMP390); + bool rv = i2c_write_register(I2C_BMP390, reg, value); + i2c_release(I2C_BMP390); return rv; } +static void prv_parse_calib_data(const uint8_t *reg_data) { + s_calib.par_t1 = (uint16_t)(reg_data[1] << 8 | reg_data[0]); + s_calib.par_t2 = (uint16_t)(reg_data[3] << 8 | reg_data[2]); + s_calib.par_t3 = (int8_t)reg_data[4]; + s_calib.par_p1 = (int16_t)(reg_data[6] << 8 | reg_data[5]); + s_calib.par_p2 = (int16_t)(reg_data[8] << 8 | reg_data[7]); + s_calib.par_p3 = (int8_t)reg_data[9]; + s_calib.par_p4 = (int8_t)reg_data[10]; + s_calib.par_p5 = (uint16_t)(reg_data[12] << 8 | reg_data[11]); + s_calib.par_p6 = (uint16_t)(reg_data[14] << 8 | reg_data[13]); + s_calib.par_p7 = (int8_t)reg_data[15]; + s_calib.par_p8 = (int8_t)reg_data[16]; + s_calib.par_p9 = (int16_t)(reg_data[18] << 8 | reg_data[17]); + s_calib.par_p10 = (int8_t)reg_data[19]; + s_calib.par_p11 = (int8_t)reg_data[20]; +} + +// Integer compensation from Bosch BMP3 Sensor API (BST-BMP390-DS002) +// Returns temperature in units of 0.01 degrees C +static int64_t prv_compensate_temperature(uint32_t uncomp_temp) { + int64_t partial_data1 = (int64_t)(uncomp_temp - ((int64_t)256 * s_calib.par_t1)); + int64_t partial_data2 = (int64_t)(s_calib.par_t2 * partial_data1); + int64_t partial_data3 = (int64_t)(partial_data1 * partial_data1); + int64_t partial_data4 = (int64_t)partial_data3 * s_calib.par_t3; + int64_t partial_data5 = (int64_t)((int64_t)(partial_data2 * 262144) + partial_data4); + int64_t partial_data6 = (int64_t)(partial_data5 / 4294967296); + + // Store t_lin for pressure compensation + s_calib.t_lin = partial_data6; + + return (int64_t)((partial_data6 * 25) / 16384); +} + +// Integer compensation from Bosch BMP3 Sensor API (BST-BMP390-DS002) +// Returns pressure in units of Pa * 100 (i.e. 1/100 Pa) +static uint64_t prv_compensate_pressure(uint32_t uncomp_press) { + int64_t partial_data1 = s_calib.t_lin * s_calib.t_lin; + int64_t partial_data2 = partial_data1 / 64; + int64_t partial_data3 = (partial_data2 * s_calib.t_lin) / 256; + int64_t partial_data4 = (s_calib.par_p8 * partial_data3) / 32; + int64_t partial_data5 = (s_calib.par_p7 * partial_data1) * 16; + int64_t partial_data6 = (s_calib.par_p6 * s_calib.t_lin) * 4194304; + int64_t offset = (s_calib.par_p5 * (int64_t)140737488355328) + + partial_data4 + partial_data5 + partial_data6; + + partial_data2 = (s_calib.par_p4 * partial_data3) / 32; + partial_data4 = (s_calib.par_p3 * partial_data1) * 4; + partial_data5 = ((int64_t)s_calib.par_p2 - (int64_t)16384) * s_calib.t_lin * 2097152; + int64_t sensitivity = (((int64_t)s_calib.par_p1 - (int64_t)16384) * (int64_t)70368744177664) + + partial_data2 + partial_data4 + partial_data5; + + partial_data1 = (sensitivity / 16777216) * uncomp_press; + partial_data2 = s_calib.par_p10 * s_calib.t_lin; + partial_data3 = partial_data2 + ((int64_t)65536 * s_calib.par_p9); + partial_data4 = (partial_data3 * uncomp_press) / 8192; + // Split to avoid overflow + partial_data5 = (uncomp_press * (partial_data4 / 10)) / 512; + partial_data5 = partial_data5 * 10; + partial_data6 = (int64_t)uncomp_press * (int64_t)uncomp_press; + partial_data2 = (s_calib.par_p11 * partial_data6) / 65536; + partial_data3 = (partial_data2 * uncomp_press) / 128; + partial_data4 = (offset / 4) + partial_data1 + partial_data5 + partial_data3; + + return (uint64_t)((uint64_t)partial_data4 * 25) / (uint64_t)1099511627776; +} + void pressure_init(void) { - bool rv; - uint8_t result; + uint8_t chip_id; + s_initialized = false; + + if (!prv_read_register(BMP390_REG_CHIP_ID, &chip_id) || + chip_id != BMP390_CHIP_ID_VALUE) { + PBL_LOG_DBG("BMP390 probe failed; chip_id 0x%02x", chip_id); + return; + } + + PBL_LOG_DBG("BMP390 found, chip_id=0x%02x", chip_id); - rv = prv_read_register(I2C_BMP390, BMP390_CHIP_ID, &result); - if (!rv || result != BMP390_CHIP_ID_VALUE) { - PBL_LOG_DBG("BMP390 probe failed; rv %d, result 0x%02x", rv, result); + // Soft reset to known state + if (!prv_write_register(BMP390_REG_CMD, 0xB6)) { + PBL_LOG_DBG("BMP390 soft reset FAILED"); + } + + // Poll status register for cmd_rdy (bit 4) after reset, with timeout + for (int i = 0; i < 50; i++) { + delay_us(1000); + uint8_t status; + if (prv_read_register(0x03, &status) && (status & 0x10)) { + PBL_LOG_DBG("BMP390 ready after %d ms", i + 1); + break; + } + } + + // Read factory calibration data + uint8_t calib_raw[BMP390_CALIB_DATA_LEN]; + if (!prv_read_register_block(BMP390_REG_CALIB_DATA, BMP390_CALIB_DATA_LEN, calib_raw)) { + PBL_LOG_DBG("BMP390 calibration read failed"); + return; + } + prv_parse_calib_data(calib_raw); + + // Configure: 8x pressure oversampling, 1x temperature, IIR filter coeff 4 + prv_write_register(BMP390_REG_OSR, BMP390_OSR_P_8X | BMP390_OSR_T_1X); + prv_write_register(BMP390_REG_CONFIG, 0x02 << 1); // IIR filter coeff = 4 + + // Enable pressure + temperature sensors (stay in sleep — reads trigger forced mode) + prv_write_register(BMP390_REG_PWR_CTRL, BMP390_PRESS_EN | BMP390_TEMP_EN); + + s_initialized = true; + PBL_LOG_DBG("BMP390 initialized"); +} + +// Trigger a forced-mode measurement and wait for data ready +static bool prv_trigger_measurement(void) { + // Trigger forced measurement + prv_write_register(BMP390_REG_PWR_CTRL, + BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_FORCED); + + // Wait for data ready (8x oversampling @ ~5ms conversion = ~40ms typical) + for (int i = 0; i < 100; i++) { + delay_us(1000); + uint8_t int_status; + if (prv_read_register(BMP390_REG_INT_STATUS, &int_status) && + (int_status & BMP390_DRDY)) { + return true; + } + } + return false; +} + +bool pressure_read(int32_t *pressure_pa, int32_t *temperature_centideg) { + if (!s_initialized) { + return false; + } + + // Trigger forced-mode measurement and wait for completion + if (!prv_trigger_measurement()) { + return false; + } + + // Read 6 bytes of sensor data: pressure[0:2], temperature[0:2] + uint8_t data[BMP390_DATA_LEN]; + if (!prv_read_register_block(BMP390_REG_DATA, BMP390_DATA_LEN, data)) { + return false; + } + + uint32_t uncomp_press = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16); + uint32_t uncomp_temp = (uint32_t)data[3] | ((uint32_t)data[4] << 8) | ((uint32_t)data[5] << 16); + + // Temperature must be compensated first — it sets t_lin used by pressure compensation + int64_t temp = prv_compensate_temperature(uncomp_temp); + uint64_t press = prv_compensate_pressure(uncomp_press); + + // temp is in 0.01 degC, press is in Pa * 100 + if (temperature_centideg) { + *temperature_centideg = (int32_t)temp; + } + if (pressure_pa) { + *pressure_pa = (int32_t)(press / 100); // Convert to Pa + } + + return true; +} + +int32_t pressure_get_altitude_cm(int32_t pressure_pa, int32_t sea_level_pa) { + // Barometric formula approximation using integer math: + // altitude = 44330 * (1 - (P/P0)^0.1903) + // + // For integer approximation, use a first-order Taylor expansion around P0: + // altitude ≈ (P0 - P) * 8435 / P0 (in cm, valid within ~1000m of sea level) + // + // 8435 comes from: 44330 * 0.1903 * 100 (cm conversion) ≈ 843500 / 100 + // More precisely: dh/dP at sea level = -RT/(Mg) ≈ -8.43m/hPa = -0.0843m/Pa + // In cm: 8.43 cm per Pa of pressure difference + + if (sea_level_pa <= 0) { + return 0; + } + + int64_t delta = (int64_t)(sea_level_pa - pressure_pa); + return (int32_t)((delta * 843500) / sea_level_pa); +} + +// Lookup table for full barometric formula: h = 44330 * (1 - (P/P0)^0.190263) +// Index i: altitude_cm for pressure ratio (1024 - i*5) / 1024 +// 129 entries covering 0–7547m +static const int32_t s_altitude_table[] = { + 0, 4127, 8269, 12429, 16605, 20798, 25008, 29236, + 33480, 37743, 42022, 46320, 50636, 54969, 59322, 63692, + 68082, 72490, 76917, 81364, 85829, 90315, 94820, 99346, + 103891, 108457, 113044, 117651, 122279, 126929, 131600, 136293, + 141008, 145744, 150504, 155286, 160090, 164918, 169769, 174644, + 179543, 184465, 189412, 194384, 199381, 204403, 209451, 214524, + 219623, 224749, 229902, 235081, 240288, 245523, 250785, 256076, + 261396, 266744, 272122, 277530, 282968, 288436, 293935, 299466, + 305028, 310622, 316249, 321909, 327602, 333328, 339089, 344885, + 350716, 356583, 362486, 368425, 374402, 380416, 386468, 392560, + 398690, 404861, 411072, 417324, 423618, 429954, 436333, 442756, + 449223, 455735, 462293, 468897, 475548, 482247, 488995, 495793, + 502640, 509539, 516490, 523494, 530552, 537664, 544832, 552057, + 559339, 566681, 574082, 581543, 589067, 596655, 604306, 612023, + 619807, 627660, 635582, 643574, 651640, 659779, 667993, 676285, + 684655, 693106, 701638, 710255, 718957, 727746, 736626, 745597, + 754662, +}; + +#define ALTITUDE_TABLE_ENTRIES 129 +#define ALTITUDE_TABLE_STEP 5 // ratio step per index (at 1024 scale) + +// Use 18-bit precision for the pressure ratio instead of 10-bit. +// At 10 bits, 1 LSB ≈ 98 Pa ≈ 8.3m (27ft) — way too coarse. +// At 18 bits, 1 LSB ≈ 0.38 Pa ≈ 3cm — smooth enough for an altimeter. +#define RATIO_SHIFT 18 +#define RATIO_SCALE (1 << RATIO_SHIFT) // 262144 +#define PRECISION_MULT (RATIO_SCALE / 1024) // 256 +#define TABLE_STEP_SCALED (ALTITUDE_TABLE_STEP * PRECISION_MULT) // 1280 + +int32_t pressure_get_altitude_full_cm(int32_t pressure_pa, int32_t ref_pressure_pa) { + if (ref_pressure_pa <= 0 || pressure_pa <= 0) { + return 0; + } + + // Compute pressure ratio in 1/262144 units: ratio = P * 2^18 / P0 + uint32_t ratio = (uint32_t)(((uint64_t)pressure_pa << RATIO_SHIFT) / ref_pressure_pa); + + // Determine sign: if pressure > reference, altitude is negative (below reference) + bool negative = (pressure_pa > ref_pressure_pa); + if (negative) { + // Mirror: compute altitude for the inverse ratio + ratio = (uint32_t)(((uint64_t)ref_pressure_pa << RATIO_SHIFT) / pressure_pa); + } + + // Clamp to table range + if (ratio >= RATIO_SCALE) { + return 0; + } + uint32_t inv = RATIO_SCALE - ratio; // 0 at reference, increases with altitude + + // Table lookup with linear interpolation + uint32_t idx = inv / TABLE_STEP_SCALED; + uint32_t frac = inv % TABLE_STEP_SCALED; + + if (idx >= ALTITUDE_TABLE_ENTRIES - 1) { + return negative ? -s_altitude_table[ALTITUDE_TABLE_ENTRIES - 1] + : s_altitude_table[ALTITUDE_TABLE_ENTRIES - 1]; + } + + int32_t alt_low = s_altitude_table[idx]; + int32_t alt_high = s_altitude_table[idx + 1]; + int32_t altitude = alt_low + ((int64_t)(alt_high - alt_low) * (int32_t)frac) / TABLE_STEP_SCALED; + + return negative ? -altitude : altitude; +} + +bool pressure_set_odr(PressureODR odr) { + if (!s_initialized) { + return false; + } + + // Each ODR preset configures: ODR register, oversampling, and IIR filter. + // Higher rates need lower oversampling to keep up with the output data rate. + uint8_t odr_reg, osr_reg, config_reg; + switch (odr) { + case PRESSURE_ODR_1HZ: + odr_reg = BMP390_ODR_1P5625HZ; + osr_reg = BMP390_OSR_P_8X | BMP390_OSR_T_1X; + config_reg = BMP390_IIR_COEFF_4; + break; + case PRESSURE_ODR_5HZ: + odr_reg = BMP390_ODR_6P25HZ; + osr_reg = BMP390_OSR_P_8X | BMP390_OSR_T_1X; + config_reg = BMP390_IIR_COEFF_4; + break; + case PRESSURE_ODR_10HZ: + odr_reg = BMP390_ODR_12P5HZ; + osr_reg = BMP390_OSR_P_4X | BMP390_OSR_T_1X; + config_reg = BMP390_IIR_COEFF_1; + break; + case PRESSURE_ODR_25HZ: + odr_reg = BMP390_ODR_25HZ; + osr_reg = BMP390_OSR_P_2X | BMP390_OSR_T_1X; + config_reg = BMP390_IIR_COEFF_0; + break; + case PRESSURE_ODR_50HZ: + odr_reg = BMP390_ODR_50HZ; + osr_reg = BMP390_OSR_P_1X | BMP390_OSR_T_1X; + config_reg = BMP390_IIR_COEFF_0; + break; + default: + return false; + } + + // Switch to forced mode briefly to apply config cleanly, then back to normal + prv_write_register(BMP390_REG_PWR_CTRL, BMP390_PRESS_EN | BMP390_TEMP_EN); + prv_write_register(BMP390_REG_ODR, odr_reg); + prv_write_register(BMP390_REG_OSR, osr_reg); + prv_write_register(BMP390_REG_CONFIG, config_reg); + prv_write_register(BMP390_REG_PWR_CTRL, + BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_NORMAL); + + return true; +} + +void command_pressure_read(void) { + int32_t pressure_pa = 0; + int32_t temperature_centideg = 0; + + if (pressure_read(&pressure_pa, &temperature_centideg)) { + char buffer[64]; + prompt_send_response_fmt(buffer, sizeof(buffer), + "Pressure: %" PRId32 " Pa", pressure_pa); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Temp: %" PRId32 ".%02" PRId32 " C", + temperature_centideg / 100, + temperature_centideg >= 0 + ? temperature_centideg % 100 + : (-temperature_centideg) % 100); + int32_t alt_cm = pressure_get_altitude_cm(pressure_pa, 101325); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Alt (est): %" PRId32 ".%02" PRId32 " m", + alt_cm / 100, + (alt_cm >= 0 ? alt_cm : -alt_cm) % 100); } else { - PBL_LOG_DBG("found the BMP390, setting to low power"); - (void) prv_write_register(I2C_BMP390, BMP390_PWR_CTRL, 0); + prompt_send_response("Pressure read FAILED"); } } + +void command_pressure_reinit(void) { + char buffer[80]; + + // Read chip ID + uint8_t chip_id; + bool ok = prv_read_register(BMP390_REG_CHIP_ID, &chip_id); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Chip ID: 0x%02x (read %s)", chip_id, ok ? "ok" : "FAIL"); + + // Write PWR_CTRL: enable sensors + normal mode + uint8_t pwr = BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_NORMAL; + prv_write_register(BMP390_REG_PWR_CTRL, pwr); + uint8_t readback; + prv_read_register(BMP390_REG_PWR_CTRL, &readback); + prompt_send_response_fmt(buffer, sizeof(buffer), + "PWR_CTRL: wrote 0x%02x read 0x%02x", pwr, readback); + + // If mode bits didn't stick, try forced mode + if ((readback & 0x30) == 0) { + uint8_t forced = BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_FORCED; + prv_write_register(BMP390_REG_PWR_CTRL, forced); + prv_read_register(BMP390_REG_PWR_CTRL, &readback); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Forced: wrote 0x%02x read 0x%02x", forced, readback); + + // Wait for measurement in forced mode + delay_us(100000); + + prv_read_register(BMP390_REG_PWR_CTRL, &readback); + uint8_t int_status; + prv_read_register(BMP390_REG_INT_STATUS, &int_status); + prompt_send_response_fmt(buffer, sizeof(buffer), + "After wait: PWR_CTRL=0x%02x DRDY=%d", + readback, (int_status & BMP390_DRDY) ? 1 : 0); + } + + // Read data + uint8_t data[BMP390_DATA_LEN]; + prv_read_register_block(BMP390_REG_DATA, BMP390_DATA_LEN, data); + uint32_t p = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16); + uint32_t t = (uint32_t)data[3] | ((uint32_t)data[4] << 8) | ((uint32_t)data[5] << 16); + prompt_send_response_fmt(buffer, sizeof(buffer), + "Raw P: %" PRIu32 " T: %" PRIu32, p, t); + + // OSR readback + uint8_t osr_rb; + prv_read_register(BMP390_REG_OSR, &osr_rb); + prompt_send_response_fmt(buffer, sizeof(buffer), "OSR: 0x%02x", osr_rb); +} + +void command_pressure_debug(void) { + if (!s_initialized) { + prompt_send_response("BMP390 not initialized"); + return; + } + + char buffer[80]; + + // Read raw data + uint8_t data[BMP390_DATA_LEN]; + if (!prv_read_register_block(BMP390_REG_DATA, BMP390_DATA_LEN, data)) { + prompt_send_response("Raw read failed"); + return; + } + + uint32_t uncomp_press = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16); + uint32_t uncomp_temp = (uint32_t)data[3] | ((uint32_t)data[4] << 8) | ((uint32_t)data[5] << 16); + + prompt_send_response_fmt(buffer, sizeof(buffer), + "Raw P: %" PRIu32 " T: %" PRIu32, uncomp_press, uncomp_temp); + + // Read PWR_CTRL to verify sensor mode + uint8_t pwr_ctrl; + prv_read_register(BMP390_REG_PWR_CTRL, &pwr_ctrl); + prompt_send_response_fmt(buffer, sizeof(buffer), + "PWR_CTRL: 0x%02x", pwr_ctrl); + + // Read INT_STATUS to check data ready + uint8_t int_status; + prv_read_register(BMP390_REG_INT_STATUS, &int_status); + prompt_send_response_fmt(buffer, sizeof(buffer), + "INT_STATUS: 0x%02x (DRDY=%d)", int_status, + (int_status & BMP390_DRDY) ? 1 : 0); + + // Dump key calibration coefficients + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_t1=%u par_t2=%u par_t3=%d", + s_calib.par_t1, s_calib.par_t2, s_calib.par_t3); + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_p1=%d par_p2=%d par_p3=%d par_p4=%d", + s_calib.par_p1, s_calib.par_p2, s_calib.par_p3, s_calib.par_p4); + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_p5=%u par_p6=%u par_p7=%d par_p8=%d", + s_calib.par_p5, s_calib.par_p6, s_calib.par_p7, s_calib.par_p8); + prompt_send_response_fmt(buffer, sizeof(buffer), + "par_p9=%d par_p10=%d par_p11=%d", + s_calib.par_p9, s_calib.par_p10, s_calib.par_p11); + prompt_send_response_fmt(buffer, sizeof(buffer), + "t_lin=%" PRId64, s_calib.t_lin); +} diff --git a/src/fw/process_management/pebble_process_info.h b/src/fw/process_management/pebble_process_info.h index 1cf391130..a701cfd30 100644 --- a/src/fw/process_management/pebble_process_info.h +++ b/src/fw/process_management/pebble_process_info.h @@ -155,9 +155,10 @@ typedef enum { // sdk.major:0x5 .minor:0x57 -- Add moddable_createMachine (rev 90) // sdk.major:0x5 .minor:0x58 -- Add size 60 LECO font (rev 91) // sdk.major:0x5 .minor:0x59 -- Add flags to ModdableCreationRecord (rev 92) +// sdk.major:0x5 .minor:0x5a -- Add PressureService API (rev 93) #define PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR 0x5 -#define PROCESS_INFO_CURRENT_SDK_VERSION_MINOR 0x59 +#define PROCESS_INFO_CURRENT_SDK_VERSION_MINOR 0x5a // The first SDK to ship with 2.x APIs #define PROCESS_INFO_FIRST_2X_SDK_VERSION_MAJOR 0x4 diff --git a/src/fw/process_state/app_state/app_state.c b/src/fw/process_state/app_state/app_state.c index 858b81f2e..5f67db835 100644 --- a/src/fw/process_state/app_state/app_state.c +++ b/src/fw/process_state/app_state/app_state.c @@ -77,6 +77,8 @@ typedef struct { HealthServiceState health_service_state; + PressureServiceState pressure_service_state; + LocaleInfo locale_info; ContentIndicatorsBuffer content_indicators_buffer; @@ -200,6 +202,8 @@ NOINLINE void app_state_init(void) { health_service_state_init(app_state_get_health_service_state()); + pressure_service_state_init(app_state_get_pressure_state()); + locale_init_app_locale(app_state_get_locale_info()); content_indicator_init_buffer(app_state_get_content_indicators_buffer()); @@ -217,6 +221,7 @@ NOINLINE void app_state_init(void) { NOINLINE void app_state_deinit(void) { animation_private_state_deinit(&s_app_state_ptr->animation_state); health_service_state_deinit(app_state_get_health_service_state()); + pressure_service_state_deinit(app_state_get_pressure_state()); unobstructed_area_service_deinit(app_state_get_unobstructed_area_state()); } @@ -317,6 +322,10 @@ HealthServiceState *app_state_get_health_service_state(void) { return &s_app_state_ptr->health_service_state; } +PressureServiceState *app_state_get_pressure_state(void) { + return &s_app_state_ptr->pressure_service_state; +} + ContentIndicatorsBuffer *app_state_get_content_indicators_buffer(void) { return &s_app_state_ptr->content_indicators_buffer; } diff --git a/src/fw/process_state/app_state/app_state.h b/src/fw/process_state/app_state/app_state.h index cffd85a6a..7bb7c78ea 100644 --- a/src/fw/process_state/app_state/app_state.h +++ b/src/fw/process_state/app_state/app_state.h @@ -18,6 +18,7 @@ #include "applib/graphics/text_render.h" #include "applib/health_service.h" #include "applib/health_service_private.h" +#include "applib/pressure_service_private.h" #include "applib/pbl_std/locale.h" #include "applib/plugin_service_private.h" #include "applib/tick_timer_service.h" @@ -123,6 +124,8 @@ ContentIndicatorsBuffer *app_state_get_content_indicators_buffer(void); HealthServiceState *app_state_get_health_service_state(void); +PressureServiceState *app_state_get_pressure_state(void); + RecognizerList *app_state_get_recognizer_list(void); JsRuntimeContext *app_state_get_js_runtime_context(void); diff --git a/tools/generate_native_sdk/exported_symbols.json b/tools/generate_native_sdk/exported_symbols.json index 27af1f3ad..c47bca81b 100644 --- a/tools/generate_native_sdk/exported_symbols.json +++ b/tools/generate_native_sdk/exported_symbols.json @@ -4,7 +4,7 @@ "You should also make sure you are obeying our API design guidelines:", "https://pebbletechnology.atlassian.net/wiki/display/DEV/SDK+API+Design+Guidelines" ], - "revision" : "92", + "revision" : "93", "version" : "2.0", "files": [ "fw/drivers/ambient_light.h", @@ -51,6 +51,8 @@ "fw/applib/app_launch_reason.h", "fw/applib/cpu_cache.h", "fw/applib/health_service.h", + "fw/drivers/pressure.h", + "fw/applib/pressure_service.h", "fw/applib/moddable/moddable.h", "fw/applib/platform.h", "fw/applib/tick_timer_service.h", @@ -689,7 +691,57 @@ "name": "health_service_get_measurement_system_for_display", "addedRevision": "80" } - ]}] + ]}, { + "type": "group", + "name": "PressureService", + "exports": [ + { + "type": "type", + "name": "PressureODR", + "addedRevision": "93" + }, { + "type": "type", + "name": "PressureData", + "addedRevision": "93" + }, { + "type": "type", + "name": "PressureDataHandler", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_subscribe", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_unsubscribe", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_data_rate", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_reference", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_reference_pressure", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_use_full_formula", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_peek", + "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_get_altitude_cm", + "addedRevision": "93" + } + ] + }] }, { "type": "group", "name": "DataLogging", diff --git a/waftools/openocd_swd_cmsisdap.cfg b/waftools/openocd_swd_cmsisdap.cfg index 84c4e7899..d3c18976a 100644 --- a/waftools/openocd_swd_cmsisdap.cfg +++ b/waftools/openocd_swd_cmsisdap.cfg @@ -1,2 +1,3 @@ source [find interface/cmsis-dap.cfg] +cmsis_dap_vid_pid 0x2e8a 0x000c transport select swd From 07c75c23ffb9a0f067f0bef9d27c292c6e1fd8b7 Mon Sep 17 00:00:00 2001 From: Mikhail Kozorovitskiy Date: Thu, 2 Apr 2026 13:24:56 -0500 Subject: [PATCH 2/3] fw/applib: add DEFINE_SYSCALL wrappers to pressure_service Pressure service functions are exported via the SDK jump table but ran in unprivileged app context, crashing when stored apps tried to access hardware drivers. Wrap all exported functions with DEFINE_SYSCALL and add a sys_pressure_read_and_compute syscall for the timer callback path. Also register the altimeter stored app in the system app registry. Co-Authored-By: Claude Opus 4.6 --- src/fw/applib/pressure_service.c | 38 ++++++++++++++----- .../normal/system_app_registry_list.json | 8 ++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/fw/applib/pressure_service.c b/src/fw/applib/pressure_service.c index 198f945f7..99fc7a33c 100644 --- a/src/fw/applib/pressure_service.c +++ b/src/fw/applib/pressure_service.c @@ -6,6 +6,7 @@ #include "drivers/pressure.h" #include "process_state/app_state/app_state.h" +#include "syscall/syscall_internal.h" #include "system/logging.h" #include @@ -41,6 +42,19 @@ static void prv_compute_altitude(PressureServiceState *state, PressureData *data } } +// Syscall wrapper for hardware access from the timer callback, which runs in +// unprivileged app context. +DEFINE_SYSCALL(bool, sys_pressure_read_and_compute, PressureData *data) { + PressureServiceState *state = prv_get_state(); + + if (!pressure_read(&data->pressure_pa, &data->temperature_centideg)) { + return false; + } + + prv_compute_altitude(state, data); + return true; +} + static void prv_timer_callback(void *context) { PressureServiceState *state = prv_get_state(); if (!state->handler) { @@ -48,15 +62,18 @@ static void prv_timer_callback(void *context) { } PressureData data = { 0 }; - if (!pressure_read(&data.pressure_pa, &data.temperature_centideg)) { + if (!sys_pressure_read_and_compute(&data)) { return; } - prv_compute_altitude(state, &data); state->handler(&data); } -void pressure_service_subscribe(PressureDataHandler handler, PressureODR odr) { +// All exported functions use DEFINE_SYSCALL to escalate privileges, since they +// are called from unprivileged app code via the SDK jump table but need to +// access hardware drivers (pressure_read, pressure_set_odr). + +DEFINE_SYSCALL(void, pressure_service_subscribe, PressureDataHandler handler, PressureODR odr) { PressureServiceState *state = prv_get_state(); // Clean up any existing subscription @@ -82,7 +99,7 @@ void pressure_service_subscribe(PressureDataHandler handler, PressureODR odr) { true /* repeating */); } -void pressure_service_unsubscribe(void) { +DEFINE_SYSCALL(void, pressure_service_unsubscribe, void) { PressureServiceState *state = prv_get_state(); if (state->poll_timer) { @@ -95,7 +112,7 @@ void pressure_service_unsubscribe(void) { pressure_set_odr(PRESSURE_ODR_1HZ); } -bool pressure_service_set_data_rate(PressureODR odr) { +DEFINE_SYSCALL(bool, pressure_service_set_data_rate, PressureODR odr) { PressureServiceState *state = prv_get_state(); if (!state->handler || !state->poll_timer) { @@ -112,7 +129,7 @@ bool pressure_service_set_data_rate(PressureODR odr) { return true; } -bool pressure_service_set_reference(void) { +DEFINE_SYSCALL(bool, pressure_service_set_reference, void) { PressureServiceState *state = prv_get_state(); int32_t pressure_pa; @@ -124,17 +141,17 @@ bool pressure_service_set_reference(void) { return true; } -void pressure_service_set_reference_pressure(int32_t ref_pressure_pa) { +DEFINE_SYSCALL(void, pressure_service_set_reference_pressure, int32_t ref_pressure_pa) { PressureServiceState *state = prv_get_state(); state->ref_pressure_pa = ref_pressure_pa; } -void pressure_service_use_full_formula(bool enable) { +DEFINE_SYSCALL(void, pressure_service_use_full_formula, bool enable) { PressureServiceState *state = prv_get_state(); state->use_full_formula = enable; } -bool pressure_service_peek(PressureData *data) { +DEFINE_SYSCALL(bool, pressure_service_peek, PressureData *data) { if (!data) { return false; } @@ -149,7 +166,8 @@ bool pressure_service_peek(PressureData *data) { return true; } -int32_t pressure_service_get_altitude_cm(int32_t pressure_pa, int32_t ref_pressure_pa) { +DEFINE_SYSCALL(int32_t, pressure_service_get_altitude_cm, int32_t pressure_pa, + int32_t ref_pressure_pa) { PressureServiceState *state = prv_get_state(); if (state->use_full_formula) { return pressure_get_altitude_full_cm(pressure_pa, ref_pressure_pa); diff --git a/src/fw/shell/normal/system_app_registry_list.json b/src/fw/shell/normal/system_app_registry_list.json index 64fb65bbb..19d139142 100644 --- a/src/fw/shell/normal/system_app_registry_list.json +++ b/src/fw/shell/normal/system_app_registry_list.json @@ -707,6 +707,14 @@ "uuid": "cf1e816a-9db0-4511-bbb8-f60c48ca8fac", "bin_resource_id": "RESOURCE_ID_STORED_APP_GOLF", "icon_resource_id": "DEFAULT_MENU_ICON" + }, + { + "id": -100, + "enum": "ALTIMETER", + "name": "Altimeter", + "uuid": "a1e0e4c3-7b2d-4f8a-9c01-d3f5a6b7e8f9", + "bin_resource_id": "RESOURCE_ID_STORED_APP_ALTIMETER", + "icon_resource_id": "DEFAULT_MENU_ICON" } ] } From db63d3e8ff0c5293734e15f26ec2a0877a181525 Mon Sep 17 00:00:00 2001 From: Mikhail Kozorovitskiy Date: Fri, 3 Apr 2026 13:48:01 -0500 Subject: [PATCH 3/3] fw/drivers: add mutex, per-bus I2C buffer, IIR filter mode API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Protect BMP390 reads/writes with a mutex for thread safety - Move nRF5 I2C write buffer from static global to per-bus state (errata #219 workaround now safe for multi-bus configs) - Add PressureFilterMode enum and pressure_set_filter_mode() driver API - Decouple IIR filter config from ODR presets — filter mode is now independently controllable - Add pressure_service_set_filter_mode() applib syscall + SDK export - Add BMP390 unit tests with fake I2C layer - Add standalone altimeter app skeleton Signed-off-by: Mikhail Kozorovitskiy --- src/fw/applib/pressure_service.c | 4 + src/fw/applib/pressure_service.h | 7 + src/fw/drivers/i2c/definitions.h | 8 + src/fw/drivers/i2c/nrf5.c | 15 +- src/fw/drivers/pressure.h | 13 + src/fw/drivers/pressure/bmp390.c | 68 ++- stored_apps/altimeter/appinfo.json | 18 + stored_apps/altimeter/src/altimeter.c | 195 +++++++++ tests/fakes/fake_i2c.c | 121 ++++++ tests/fakes/fake_i2c.h | 25 ++ tests/fw/drivers/test_pressure_bmp390.c | 393 ++++++++++++++++++ tests/fw/drivers/wscript | 6 + .../generate_native_sdk/exported_symbols.json | 8 + 13 files changed, 861 insertions(+), 20 deletions(-) create mode 100644 stored_apps/altimeter/appinfo.json create mode 100644 stored_apps/altimeter/src/altimeter.c create mode 100644 tests/fakes/fake_i2c.c create mode 100644 tests/fakes/fake_i2c.h create mode 100644 tests/fw/drivers/test_pressure_bmp390.c diff --git a/src/fw/applib/pressure_service.c b/src/fw/applib/pressure_service.c index 99fc7a33c..0f5e7f28c 100644 --- a/src/fw/applib/pressure_service.c +++ b/src/fw/applib/pressure_service.c @@ -166,6 +166,10 @@ DEFINE_SYSCALL(bool, pressure_service_peek, PressureData *data) { return true; } +DEFINE_SYSCALL(bool, pressure_service_set_filter_mode, PressureFilterMode mode) { + return pressure_set_filter_mode(mode); +} + DEFINE_SYSCALL(int32_t, pressure_service_get_altitude_cm, int32_t pressure_pa, int32_t ref_pressure_pa) { PressureServiceState *state = prv_get_state(); diff --git a/src/fw/applib/pressure_service.h b/src/fw/applib/pressure_service.h index 721a898bf..5855e6a30 100644 --- a/src/fw/applib/pressure_service.h +++ b/src/fw/applib/pressure_service.h @@ -92,6 +92,13 @@ bool pressure_service_peek(PressureData *data); //! @return altitude in centimeters relative to the reference pressure int32_t pressure_service_get_altitude_cm(int32_t pressure_pa, int32_t ref_pressure_pa); +//! Set the hardware IIR filter mode on the pressure sensor. +//! Use PRESSURE_FILTER_NONE for fastest response (e.g. skydiving altimeters). +//! Use PRESSURE_FILTER_SMOOTH for smoother readings (e.g. weather monitoring). +//! @param mode The desired filter mode +//! @return true if the mode was set successfully +bool pressure_service_set_filter_mode(PressureFilterMode mode); + //! @} // end addtogroup PressureService //! @} // end addtogroup EventService //! @} // end addtogroup Foundation diff --git a/src/fw/drivers/i2c/definitions.h b/src/fw/drivers/i2c/definitions.h index 01d0be457..ff41beba0 100644 --- a/src/fw/drivers/i2c/definitions.h +++ b/src/fw/drivers/i2c/definitions.h @@ -64,6 +64,14 @@ typedef struct I2CBusState { int user_count; #if MICRO_FAMILY_STM32F4 RtcTicks last_rail_stop_ticks; +#endif +#if MICRO_FAMILY_NRF5 + // Per-bus write buffer for nRF52840 TWIM errata #219 workaround. + // DMA reads from this buffer during the transfer, so it must outlive the call + // to nrfx_twim_xfer(). Currently safe because transfers are synchronous. + // Will NOT work if transfers become async without additional lifetime guarantees. +#define I2C_WRITE_BUF_MAX 32 + uint8_t write_buf[I2C_WRITE_BUF_MAX]; #endif SemaphoreHandle_t event_semaphore; PebbleMutex *bus_mutex; diff --git a/src/fw/drivers/i2c/nrf5.c b/src/fw/drivers/i2c/nrf5.c index 552a74d38..fb82d3037 100644 --- a/src/fw/drivers/i2c/nrf5.c +++ b/src/fw/drivers/i2c/nrf5.c @@ -63,12 +63,6 @@ void i2c_hal_abort_transfer(I2CBus *bus) { void i2c_hal_init_transfer(I2CBus *bus) { } -// Buffer for combining register address + write data into a single TX transfer. -// TXTX (two-part TX) on nRF52840 can fail to transmit the secondary buffer -// correctly due to TWIM peripheral errata. Using a single TX avoids this. -#define I2C_WRITE_BUF_MAX 32 -static uint8_t s_write_buf[I2C_WRITE_BUF_MAX]; - void i2c_hal_start_transfer(I2CBus *bus) { nrfx_twim_xfer_desc_t desc; @@ -81,13 +75,14 @@ void i2c_hal_start_transfer(I2CBus *bus) { desc.secondary_length = bus->state->transfer.size; desc.p_secondary_buf = bus->state->transfer.data; } else { - // Combine register address + data into one TX to avoid TXTX errata + // Combine register address + data into one TX (nRF52840 TWIM errata #219) PBL_ASSERTN(bus->state->transfer.size + 1 <= I2C_WRITE_BUF_MAX); - s_write_buf[0] = bus->state->transfer.register_address; - memcpy(&s_write_buf[1], bus->state->transfer.data, bus->state->transfer.size); + bus->state->write_buf[0] = bus->state->transfer.register_address; + memcpy(&bus->state->write_buf[1], bus->state->transfer.data, + bus->state->transfer.size); desc.type = NRFX_TWIM_XFER_TX; desc.primary_length = bus->state->transfer.size + 1; - desc.p_primary_buf = s_write_buf; + desc.p_primary_buf = bus->state->write_buf; desc.secondary_length = 0; desc.p_secondary_buf = NULL; } diff --git a/src/fw/drivers/pressure.h b/src/fw/drivers/pressure.h index 8504d6cd6..ad253c8b7 100644 --- a/src/fw/drivers/pressure.h +++ b/src/fw/drivers/pressure.h @@ -43,3 +43,16 @@ typedef enum { //! @param odr The desired output data rate //! @return true if configuration succeeded bool pressure_set_odr(PressureODR odr); + +//! IIR filter modes for the pressure sensor +typedef enum { + PRESSURE_FILTER_NONE = 0, //!< No IIR filtering — fastest response + PRESSURE_FILTER_SMOOTH, //!< IIR filtering — smoother readings, adds latency +} PressureFilterMode; + +//! Set the hardware IIR filter mode. Takes effect on the next +//! pressure_set_odr() call or immediately if already in normal mode. +//! Defaults to PRESSURE_FILTER_NONE. +//! @param mode The desired filter mode +//! @return true if the mode was set successfully +bool pressure_set_filter_mode(PressureFilterMode mode); diff --git a/src/fw/drivers/pressure/bmp390.c b/src/fw/drivers/pressure/bmp390.c index 8cb2d4c5d..25d819bd9 100644 --- a/src/fw/drivers/pressure/bmp390.c +++ b/src/fw/drivers/pressure/bmp390.c @@ -5,6 +5,7 @@ #include "drivers/pressure.h" #include "drivers/i2c.h" #include "console/prompt.h" +#include "os/mutex.h" #include "system/logging.h" #include "kernel/util/delay.h" @@ -44,7 +45,10 @@ // IIR filter coefficients (for CONFIG register, bits [3:1]) #define BMP390_IIR_COEFF_0 (0x00 << 1) // Off #define BMP390_IIR_COEFF_1 (0x01 << 1) -#define BMP390_IIR_COEFF_4 (0x02 << 1) +#define BMP390_IIR_COEFF_3 (0x02 << 1) +#define BMP390_IIR_COEFF_7 (0x03 << 1) +#define BMP390_IIR_COEFF_15 (0x04 << 1) +#define BMP390_IIR_COEFF_31 (0x05 << 1) // ODR register values (from BMP390 datasheet Table 22) #define BMP390_ODR_200HZ 0x00 @@ -82,6 +86,8 @@ typedef struct { static BMP390CalibData s_calib; static bool s_initialized; +static PebbleMutex *s_mutex; +static PressureFilterMode s_filter_mode; // I2C helpers using the PebbleOS I2C API static bool prv_read_register(uint8_t reg, uint8_t *result) { @@ -174,6 +180,11 @@ static uint64_t prv_compensate_pressure(uint32_t uncomp_press) { void pressure_init(void) { uint8_t chip_id; s_initialized = false; + s_filter_mode = PRESSURE_FILTER_NONE; + + if (!s_mutex) { + s_mutex = mutex_create(); + } if (!prv_read_register(BMP390_REG_CHIP_ID, &chip_id) || chip_id != BMP390_CHIP_ID_VALUE) { @@ -206,9 +217,9 @@ void pressure_init(void) { } prv_parse_calib_data(calib_raw); - // Configure: 8x pressure oversampling, 1x temperature, IIR filter coeff 4 + // Configure: 8x pressure oversampling, 1x temperature, IIR off by default prv_write_register(BMP390_REG_OSR, BMP390_OSR_P_8X | BMP390_OSR_T_1X); - prv_write_register(BMP390_REG_CONFIG, 0x02 << 1); // IIR filter coeff = 4 + prv_write_register(BMP390_REG_CONFIG, BMP390_IIR_COEFF_0); // Enable pressure + temperature sensors (stay in sleep — reads trigger forced mode) prv_write_register(BMP390_REG_PWR_CTRL, BMP390_PRESS_EN | BMP390_TEMP_EN); @@ -240,14 +251,18 @@ bool pressure_read(int32_t *pressure_pa, int32_t *temperature_centideg) { return false; } + mutex_lock(s_mutex); + // Trigger forced-mode measurement and wait for completion if (!prv_trigger_measurement()) { + mutex_unlock(s_mutex); return false; } // Read 6 bytes of sensor data: pressure[0:2], temperature[0:2] uint8_t data[BMP390_DATA_LEN]; if (!prv_read_register_block(BMP390_REG_DATA, BMP390_DATA_LEN, data)) { + mutex_unlock(s_mutex); return false; } @@ -258,6 +273,8 @@ bool pressure_read(int32_t *pressure_pa, int32_t *temperature_centideg) { int64_t temp = prv_compensate_temperature(uncomp_temp); uint64_t press = prv_compensate_pressure(uncomp_press); + mutex_unlock(s_mutex); + // temp is in 0.01 degC, press is in Pa * 100 if (temperature_centideg) { *temperature_centideg = (int32_t)temp; @@ -359,45 +376,58 @@ int32_t pressure_get_altitude_full_cm(int32_t pressure_pa, int32_t ref_pressure_ return negative ? -altitude : altitude; } +// IIR coefficients for each ODR when filtering is enabled +static uint8_t prv_iir_for_odr(PressureODR odr) { + switch (odr) { + case PRESSURE_ODR_1HZ: return BMP390_IIR_COEFF_7; + case PRESSURE_ODR_5HZ: return BMP390_IIR_COEFF_7; + case PRESSURE_ODR_10HZ: return BMP390_IIR_COEFF_7; + case PRESSURE_ODR_25HZ: return BMP390_IIR_COEFF_15; + case PRESSURE_ODR_50HZ: return BMP390_IIR_COEFF_31; + default: return BMP390_IIR_COEFF_0; + } +} + bool pressure_set_odr(PressureODR odr) { if (!s_initialized) { return false; } + mutex_lock(s_mutex); + // Each ODR preset configures: ODR register, oversampling, and IIR filter. // Higher rates need lower oversampling to keep up with the output data rate. - uint8_t odr_reg, osr_reg, config_reg; + uint8_t odr_reg, osr_reg; switch (odr) { case PRESSURE_ODR_1HZ: odr_reg = BMP390_ODR_1P5625HZ; osr_reg = BMP390_OSR_P_8X | BMP390_OSR_T_1X; - config_reg = BMP390_IIR_COEFF_4; break; case PRESSURE_ODR_5HZ: odr_reg = BMP390_ODR_6P25HZ; osr_reg = BMP390_OSR_P_8X | BMP390_OSR_T_1X; - config_reg = BMP390_IIR_COEFF_4; break; case PRESSURE_ODR_10HZ: odr_reg = BMP390_ODR_12P5HZ; osr_reg = BMP390_OSR_P_4X | BMP390_OSR_T_1X; - config_reg = BMP390_IIR_COEFF_1; break; case PRESSURE_ODR_25HZ: odr_reg = BMP390_ODR_25HZ; osr_reg = BMP390_OSR_P_2X | BMP390_OSR_T_1X; - config_reg = BMP390_IIR_COEFF_0; break; case PRESSURE_ODR_50HZ: odr_reg = BMP390_ODR_50HZ; osr_reg = BMP390_OSR_P_1X | BMP390_OSR_T_1X; - config_reg = BMP390_IIR_COEFF_0; break; default: + mutex_unlock(s_mutex); return false; } - // Switch to forced mode briefly to apply config cleanly, then back to normal + uint8_t config_reg = (s_filter_mode == PRESSURE_FILTER_SMOOTH) + ? prv_iir_for_odr(odr) : BMP390_IIR_COEFF_0; + + // Switch to sleep to apply config cleanly, then back to normal prv_write_register(BMP390_REG_PWR_CTRL, BMP390_PRESS_EN | BMP390_TEMP_EN); prv_write_register(BMP390_REG_ODR, odr_reg); prv_write_register(BMP390_REG_OSR, osr_reg); @@ -405,6 +435,24 @@ bool pressure_set_odr(PressureODR odr) { prv_write_register(BMP390_REG_PWR_CTRL, BMP390_PRESS_EN | BMP390_TEMP_EN | BMP390_MODE_NORMAL); + mutex_unlock(s_mutex); + return true; +} + +bool pressure_set_filter_mode(PressureFilterMode mode) { + if (!s_initialized) { + return false; + } + + mutex_lock(s_mutex); + s_filter_mode = mode; + + // Apply immediately: update the CONFIG register + uint8_t config_reg = (mode == PRESSURE_FILTER_SMOOTH) + ? BMP390_IIR_COEFF_7 : BMP390_IIR_COEFF_0; + prv_write_register(BMP390_REG_CONFIG, config_reg); + + mutex_unlock(s_mutex); return true; } diff --git a/stored_apps/altimeter/appinfo.json b/stored_apps/altimeter/appinfo.json new file mode 100644 index 000000000..019336dee --- /dev/null +++ b/stored_apps/altimeter/appinfo.json @@ -0,0 +1,18 @@ +{ + "uuid": "a1e0e4c3-7b2d-4f8a-9c01-d3f5a6b7e8f9", + "shortName": "Altimeter", + "longName": "Altimeter", + "companyName": "PebbleOS", + "versionCode": 1.0, + "versionLabel": "1.0", + "watchapp": { + "watchface": false, + "onlyShownOnCommunication": false + }, + "appKeys": { + "dummy": 0 + }, + "resources": { + "media": [] + } +} diff --git a/stored_apps/altimeter/src/altimeter.c b/stored_apps/altimeter/src/altimeter.c new file mode 100644 index 000000000..5c0829dc4 --- /dev/null +++ b/stored_apps/altimeter/src/altimeter.c @@ -0,0 +1,195 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include + +typedef struct { + Window *window; + StatusBarLayer *status_layer; + TextLayer *altitude_label; + TextLayer *altitude_value; + TextLayer *altitude_unit; + TextLayer *pressure_label; + TextLayer *pressure_value; + char altitude_buf[16]; + char pressure_buf[32]; + PressureODR current_odr; + bool ref_set; + bool auto_high_rate; +} AppData; + +static AppData s_data; + +static const char *odr_name(PressureODR odr) { + switch (odr) { + case PRESSURE_ODR_1HZ: return "1 Hz"; + case PRESSURE_ODR_5HZ: return "5 Hz"; + case PRESSURE_ODR_10HZ: return "10 Hz"; + case PRESSURE_ODR_25HZ: return "25 Hz"; + case PRESSURE_ODR_50HZ: return "50 Hz"; + default: return "?"; + } +} + +static void update_pressure_label(AppData *data, int32_t pressure_pa) { + int32_t hpa = pressure_pa / 100; + int32_t hpa_frac = (pressure_pa % 100) / 10; + snprintf(data->pressure_buf, sizeof(data->pressure_buf), "%ld.%ld hPa (%s)", + (long)hpa, (long)hpa_frac, odr_name(data->current_odr)); + text_layer_set_text(data->pressure_value, data->pressure_buf); +} + +static void pressure_handler(PressureData *pdata) { + AppData *data = &s_data; + + // Altitude in feet (1 cm = 0.0328084 ft, use integer math: ft = cm * 328084 / 10000000) + int32_t alt_cm = pdata->altitude_cm; + int32_t alt_ft_x10 = (int32_t)((int64_t)alt_cm * 328084 / 1000000); + int32_t alt_ft = alt_ft_x10 / 10; + int32_t alt_frac = (alt_ft_x10 < 0 ? -alt_ft_x10 : alt_ft_x10) % 10; + snprintf(data->altitude_buf, sizeof(data->altitude_buf), "%ld.%ld", + (long)alt_ft, (long)alt_frac); + text_layer_set_text(data->altitude_value, data->altitude_buf); + + // Auto-switch to 25Hz when above 50ft relative altitude + int32_t abs_ft = alt_ft < 0 ? -alt_ft : alt_ft; + if (!data->auto_high_rate && abs_ft > 50) { + data->auto_high_rate = true; + data->current_odr = PRESSURE_ODR_25HZ; + pressure_service_set_data_rate(data->current_odr); + } else if (data->auto_high_rate && abs_ft <= 50) { + data->auto_high_rate = false; + data->current_odr = PRESSURE_ODR_1HZ; + pressure_service_set_data_rate(data->current_odr); + } + + // Pressure in hPa with rate in parentheses + update_pressure_label(data, pdata->pressure_pa); +} + +static void up_click_handler(ClickRecognizerRef recognizer, void *context) { + AppData *data = &s_data; + if (data->current_odr < PRESSURE_ODR_50HZ) { + data->current_odr++; + pressure_service_set_data_rate(data->current_odr); + } +} + +static void down_click_handler(ClickRecognizerRef recognizer, void *context) { + AppData *data = &s_data; + if (data->current_odr > PRESSURE_ODR_1HZ) { + data->current_odr--; + pressure_service_set_data_rate(data->current_odr); + } +} + +static void select_click_handler(ClickRecognizerRef recognizer, void *context) { + AppData *data = &s_data; + pressure_service_set_reference(); + data->ref_set = true; + vibes_short_pulse(); +} + +static void config_provider(void *context) { + window_single_click_subscribe(BUTTON_ID_UP, up_click_handler); + window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler); + window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler); +} + +static void window_load(Window *window) { + AppData *data = &s_data; + Layer *root = window_get_root_layer(window); + GRect bounds = layer_get_bounds(root); + int16_t w = bounds.size.w; + + // Status bar — white on black + data->status_layer = status_bar_layer_create(); + status_bar_layer_set_colors(data->status_layer, GColorBlack, GColorWhite); + layer_add_child(root, status_bar_layer_get_layer(data->status_layer)); + + int16_t y = STATUS_BAR_LAYER_HEIGHT + 2; + + // "REL ALT" label + data->altitude_label = text_layer_create(GRect(0, y, w, 18)); + text_layer_set_font(data->altitude_label, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text(data->altitude_label, "REL ALTI"); + text_layer_set_text_alignment(data->altitude_label, GTextAlignmentCenter); + text_layer_set_text_color(data->altitude_label, GColorWhite); + text_layer_set_background_color(data->altitude_label, GColorClear); + layer_add_child(root, text_layer_get_layer(data->altitude_label)); + y += 18; + + // Altitude value — big numbers + data->altitude_value = text_layer_create(GRect(0, y, w, 58)); + text_layer_set_font(data->altitude_value, + fonts_get_system_font(FONT_KEY_LECO_42_NUMBERS)); + text_layer_set_text(data->altitude_value, "--.-"); + text_layer_set_text_alignment(data->altitude_value, GTextAlignmentCenter); + text_layer_set_text_color(data->altitude_value, GColorWhite); + text_layer_set_background_color(data->altitude_value, GColorClear); + layer_add_child(root, text_layer_get_layer(data->altitude_value)); + y += 50; + + // Unit label + data->altitude_unit = text_layer_create(GRect(0, y, w, 18)); + text_layer_set_font(data->altitude_unit, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text(data->altitude_unit, "feet"); + text_layer_set_text_alignment(data->altitude_unit, GTextAlignmentCenter); + text_layer_set_text_color(data->altitude_unit, GColorWhite); + text_layer_set_background_color(data->altitude_unit, GColorClear); + layer_add_child(root, text_layer_get_layer(data->altitude_unit)); + y += 20; + + // Pressure label + data->pressure_label = text_layer_create(GRect(0, y, w, 14)); + text_layer_set_font(data->pressure_label, fonts_get_system_font(FONT_KEY_GOTHIC_14)); + text_layer_set_text(data->pressure_label, "PRESSURE"); + text_layer_set_text_alignment(data->pressure_label, GTextAlignmentCenter); + text_layer_set_text_color(data->pressure_label, GColorWhite); + text_layer_set_background_color(data->pressure_label, GColorClear); + layer_add_child(root, text_layer_get_layer(data->pressure_label)); + y += 14; + + // Pressure value + rate + data->pressure_value = text_layer_create(GRect(0, y, w, 24)); + text_layer_set_font(data->pressure_value, + fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD)); + text_layer_set_text(data->pressure_value, "--.- hPa (1 Hz)"); + text_layer_set_text_alignment(data->pressure_value, GTextAlignmentCenter); + text_layer_set_text_color(data->pressure_value, GColorWhite); + text_layer_set_background_color(data->pressure_value, GColorClear); + layer_add_child(root, text_layer_get_layer(data->pressure_value)); + + data->current_odr = PRESSURE_ODR_1HZ; + + // Keep backlight on while altimeter is running + light_enable(true); + + // Subscribe to pressure service and zero on launch + pressure_service_subscribe(pressure_handler, data->current_odr); + pressure_service_use_full_formula(true); + pressure_service_set_reference(); + data->ref_set = true; +} + +static void window_unload(Window *window) { + light_enable(false); + pressure_service_unsubscribe(); +} + +static void handle_init(void) { + AppData *data = &s_data; + data->window = window_create(); + window_set_background_color(data->window, GColorBlack); + window_set_window_handlers(data->window, (WindowHandlers) { + .load = window_load, + .unload = window_unload, + }); + window_set_click_config_provider(data->window, config_provider); + window_stack_push(data->window, true); +} + +int main(void) { + handle_init(); + app_event_loop(); +} diff --git a/tests/fakes/fake_i2c.c b/tests/fakes/fake_i2c.c new file mode 100644 index 000000000..1d39c5b1c --- /dev/null +++ b/tests/fakes/fake_i2c.c @@ -0,0 +1,121 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "fake_i2c.h" +#include "drivers/i2c.h" + +#include + +#define REG_COUNT 256 +#define WRITE_LOG_MAX 256 + +typedef struct { + uint8_t reg; + uint8_t value; +} WriteLogEntry; + +static uint8_t s_registers[REG_COUNT]; +static bool s_fail; +static FakeI2CWriteHook s_write_hook; +static void *s_write_hook_context; +static WriteLogEntry s_write_log[WRITE_LOG_MAX]; +static int s_write_log_count; + +// Provide the I2C_BMP390 symbol the driver expects +static I2CSlavePort s_fake_slave; +I2CSlavePort *const I2C_BMP390 = &s_fake_slave; + +void fake_i2c_init(void) { + memset(s_registers, 0, sizeof(s_registers)); + s_fail = false; + s_write_hook = NULL; + s_write_hook_context = NULL; + s_write_log_count = 0; +} + +void fake_i2c_set_register(uint8_t reg, uint8_t value) { + s_registers[reg] = value; +} + +void fake_i2c_set_register_block(uint8_t reg, const uint8_t *data, uint32_t len) { + for (uint32_t i = 0; i < len && (reg + i) < REG_COUNT; i++) { + s_registers[reg + i] = data[i]; + } +} + +uint8_t fake_i2c_get_register(uint8_t reg) { + return s_registers[reg]; +} + +void fake_i2c_set_fail(bool fail) { + s_fail = fail; +} + +void fake_i2c_set_write_hook(FakeI2CWriteHook hook, void *context) { + s_write_hook = hook; + s_write_hook_context = context; +} + +bool fake_i2c_was_written(uint8_t reg, uint8_t value) { + for (int i = 0; i < s_write_log_count; i++) { + if (s_write_log[i].reg == reg && s_write_log[i].value == value) { + return true; + } + } + return false; +} + +uint8_t fake_i2c_last_written(uint8_t reg) { + for (int i = s_write_log_count - 1; i >= 0; i--) { + if (s_write_log[i].reg == reg) { + return s_write_log[i].value; + } + } + return 0; +} + +// I2C API stubs + +void i2c_use(I2CSlavePort *slave) { + (void)slave; +} + +void i2c_release(I2CSlavePort *slave) { + (void)slave; +} + +bool i2c_read_register(I2CSlavePort *slave, uint8_t register_address, uint8_t *result) { + (void)slave; + if (s_fail) return false; + *result = s_registers[register_address]; + return true; +} + +bool i2c_read_register_block(I2CSlavePort *slave, uint8_t register_address_start, + uint32_t read_size, uint8_t *result_buffer) { + (void)slave; + if (s_fail) return false; + for (uint32_t i = 0; i < read_size && (register_address_start + i) < REG_COUNT; i++) { + result_buffer[i] = s_registers[register_address_start + i]; + } + return true; +} + +bool i2c_write_register(I2CSlavePort *slave, uint8_t register_address, uint8_t value) { + (void)slave; + if (s_fail) return false; + + s_registers[register_address] = value; + + if (s_write_log_count < WRITE_LOG_MAX) { + s_write_log[s_write_log_count].reg = register_address; + s_write_log[s_write_log_count].value = value; + s_write_log_count++; + } + + if (s_write_hook) { + s_write_hook(register_address, value, s_write_hook_context); + } + + return true; +} diff --git a/tests/fakes/fake_i2c.h b/tests/fakes/fake_i2c.h new file mode 100644 index 000000000..150303090 --- /dev/null +++ b/tests/fakes/fake_i2c.h @@ -0,0 +1,25 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include +#include + +typedef void (*FakeI2CWriteHook)(uint8_t reg, uint8_t value, void *context); + +void fake_i2c_init(void); + +void fake_i2c_set_register(uint8_t reg, uint8_t value); + +void fake_i2c_set_register_block(uint8_t reg, const uint8_t *data, uint32_t len); + +uint8_t fake_i2c_get_register(uint8_t reg); + +void fake_i2c_set_fail(bool fail); + +void fake_i2c_set_write_hook(FakeI2CWriteHook hook, void *context); + +bool fake_i2c_was_written(uint8_t reg, uint8_t value); + +uint8_t fake_i2c_last_written(uint8_t reg); diff --git a/tests/fw/drivers/test_pressure_bmp390.c b/tests/fw/drivers/test_pressure_bmp390.c new file mode 100644 index 000000000..eb0710abe --- /dev/null +++ b/tests/fw/drivers/test_pressure_bmp390.c @@ -0,0 +1,393 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#include "clar.h" + +#include "stubs_logging.h" +#include "stubs_mutex.h" +#include "stubs_passert.h" +#include "stubs_pebble_tasks.h" +#include "stubs_prompt.h" +#include "stubs_syscall_internal.h" + +#include "drivers/pressure.h" +#include "fakes/fake_i2c.h" + +#include + +// Stub for delay_us — no-op in tests +void delay_us(uint32_t us) { (void)us; } +void delay_init(void) { } + +// BMP390 register addresses (must match driver) +#define REG_CHIP_ID 0x00 +#define REG_STATUS 0x03 +#define REG_DATA 0x04 +#define REG_INT_STATUS 0x11 +#define REG_PWR_CTRL 0x1B +#define REG_OSR 0x1C +#define REG_ODR 0x1D +#define REG_CONFIG 0x1F +#define REG_CALIB_DATA 0x31 +#define REG_CMD 0x7E + +#define CHIP_ID_VALUE 0x60 +#define CALIB_DATA_LEN 21 + +// PWR_CTRL bit definitions +#define PRESS_EN (1 << 0) +#define TEMP_EN (1 << 1) +#define MODE_FORCED (1 << 4) +#define MODE_NORMAL (3 << 4) + +// IIR filter coefficients (CONFIG register bits [3:1]) +#define IIR_COEFF_0 (0x00 << 1) +#define IIR_COEFF_7 (0x03 << 1) +#define IIR_COEFF_15 (0x04 << 1) +#define IIR_COEFF_31 (0x05 << 1) + +// INT_STATUS bits +#define DRDY_BIT (1 << 3) + +// Known calibration data from a real BMP390 — produces verifiable output +static const uint8_t s_test_calib_data[CALIB_DATA_LEN] = { + // par_t1 (uint16): 27356 = 0x6ACC + 0xCC, 0x6A, + // par_t2 (uint16): 18789 = 0x4965 + 0x65, 0x49, + // par_t3 (int8): -7 + 0xF9, + // par_p1 (int16): 217 = 0x00D9 + 0xD9, 0x00, + // par_p2 (int16): -163 = 0xFF5D + 0x5D, 0xFF, + // par_p3 (int8): 14 + 0x0E, + // par_p4 (int8): 0 + 0x00, + // par_p5 (uint16): 25352 = 0x6308 + 0x08, 0x63, + // par_p6 (uint16): 29536 = 0x7360 + 0x60, 0x73, + // par_p7 (int8): -1 + 0xFF, + // par_p8 (int8): -7 + 0xF9, + // par_p9 (int16): 13000 = 0x32C8 + 0xC8, 0x32, + // par_p10 (int8): 29 + 0x1D, + // par_p11 (int8): 0 + 0x00, +}; + +// Hook for simulating BMP390 behavior on register writes +static void prv_bmp390_write_hook(uint8_t reg, uint8_t value, void *context) { + if (reg == REG_CMD && value == 0xB6) { + // Soft reset: set status.cmd_rdy (bit 4) so driver sees device ready + fake_i2c_set_register(REG_STATUS, 0x10); + } + + if (reg == REG_PWR_CTRL && (value & MODE_FORCED)) { + // Forced mode: set DRDY after "measurement" + fake_i2c_set_register(REG_INT_STATUS, DRDY_BIT); + } +} + +// Hook that clears normal mode bits (simulates the hardware bug) +static void prv_normal_mode_bug_hook(uint8_t reg, uint8_t value, void *context) { + prv_bmp390_write_hook(reg, value, context); + + if (reg == REG_PWR_CTRL) { + uint8_t cleared = value & ~MODE_NORMAL; + fake_i2c_set_register(REG_PWR_CTRL, cleared); + } +} + +static void prv_setup_bmp390_registers(void) { + fake_i2c_init(); + fake_i2c_set_register(REG_CHIP_ID, CHIP_ID_VALUE); + fake_i2c_set_register(REG_STATUS, 0x10); // cmd_rdy + fake_i2c_set_register_block(REG_CALIB_DATA, s_test_calib_data, CALIB_DATA_LEN); + fake_i2c_set_write_hook(prv_bmp390_write_hook, NULL); +} + +// Set up raw sensor data registers (pressure + temperature, 6 bytes at 0x04) +static void prv_set_sensor_data(uint32_t raw_pressure, uint32_t raw_temperature) { + uint8_t data[6]; + data[0] = (raw_pressure >> 0) & 0xFF; + data[1] = (raw_pressure >> 8) & 0xFF; + data[2] = (raw_pressure >> 16) & 0xFF; + data[3] = (raw_temperature >> 0) & 0xFF; + data[4] = (raw_temperature >> 8) & 0xFF; + data[5] = (raw_temperature >> 16) & 0xFF; + fake_i2c_set_register_block(REG_DATA, data, 6); +} + +// ---- Setup / Teardown ---- + +void test_pressure_bmp390__initialize(void) { + prv_setup_bmp390_registers(); +} + +void test_pressure_bmp390__cleanup(void) { + fake_i2c_set_register(REG_CHIP_ID, 0x00); + fake_i2c_set_fail(false); + pressure_init(); +} + +// ---- Init Tests ---- + +void test_pressure_bmp390__init_success(void) { + pressure_init(); + // After init, driver should have written OSR register + cl_assert(fake_i2c_was_written(REG_OSR, 0x03)); // 8x pressure, 1x temp + // IIR defaults to off (FILTER_NONE) + cl_assert(fake_i2c_was_written(REG_CONFIG, IIR_COEFF_0)); + // PWR_CTRL should enable sensors without mode bits (sleep) + cl_assert(fake_i2c_was_written(REG_PWR_CTRL, PRESS_EN | TEMP_EN)); +} + +void test_pressure_bmp390__init_bad_chip_id(void) { + fake_i2c_set_register(REG_CHIP_ID, 0xAA); + pressure_init(); + cl_assert(!fake_i2c_was_written(REG_OSR, 0x03)); +} + +void test_pressure_bmp390__init_i2c_failure(void) { + fake_i2c_set_fail(true); + pressure_init(); + int32_t pressure, temp; + cl_assert(!pressure_read(&pressure, &temp)); +} + +// ---- Forced Mode Read Tests ---- + +void test_pressure_bmp390__read_triggers_forced_mode(void) { + pressure_init(); + prv_set_sensor_data(6892135, 8360755); + + int32_t pressure_pa, temp_centideg; + cl_assert(pressure_read(&pressure_pa, &temp_centideg)); + + cl_assert(fake_i2c_was_written(REG_PWR_CTRL, + PRESS_EN | TEMP_EN | MODE_FORCED)); + + cl_assert(pressure_pa > 80000); + cl_assert(pressure_pa < 120000); + cl_assert(temp_centideg > -4000); + cl_assert(temp_centideg < 8500); +} + +void test_pressure_bmp390__read_not_initialized(void) { + int32_t pressure, temp; + cl_assert(!pressure_read(&pressure, &temp)); +} + +void test_pressure_bmp390__read_null_params(void) { + pressure_init(); + prv_set_sensor_data(6892135, 8360755); + + cl_assert(pressure_read(NULL, NULL)); + + int32_t pressure; + cl_assert(pressure_read(&pressure, NULL)); + cl_assert(pressure > 80000); + + int32_t temp; + cl_assert(pressure_read(NULL, &temp)); +} + +// ---- ODR Configuration Tests ---- + +void test_pressure_bmp390__set_odr_25hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x03); // BMP390_ODR_25HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x01); // OSR_P_2X | OSR_T_1X + // Default filter mode is NONE — IIR should be off + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_50hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_50HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x02); // BMP390_ODR_50HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x00); // OSR_P_1X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_1hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_1HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x07); // BMP390_ODR_1P5625HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x03); // OSR_P_8X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_5hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_5HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x05); // BMP390_ODR_6P25HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x03); // OSR_P_8X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_10hz(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_10HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_ODR), 0x04); // BMP390_ODR_12P5HZ + cl_assert_equal_i(fake_i2c_last_written(REG_OSR), 0x02); // OSR_P_4X | OSR_T_1X + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__set_odr_enables_normal_mode(void) { + pressure_init(); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + uint8_t last_pwr = fake_i2c_last_written(REG_PWR_CTRL); + cl_assert_equal_i(last_pwr, PRESS_EN | TEMP_EN | MODE_NORMAL); +} + +void test_pressure_bmp390__set_odr_not_initialized(void) { + cl_assert(!pressure_set_odr(PRESSURE_ODR_25HZ)); +} + +// ---- Filter Mode Tests ---- + +void test_pressure_bmp390__filter_mode_default_none(void) { + pressure_init(); + // Init should write IIR off + cl_assert(fake_i2c_was_written(REG_CONFIG, IIR_COEFF_0)); +} + +void test_pressure_bmp390__filter_mode_smooth_applies_iir(void) { + pressure_init(); + cl_assert(pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH)); + + // Should immediately write IIR coeff to CONFIG + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_7); +} + +void test_pressure_bmp390__filter_mode_none_clears_iir(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_filter_mode(PRESSURE_FILTER_NONE)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__filter_smooth_odr_25hz(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_15); +} + +void test_pressure_bmp390__filter_smooth_odr_50hz(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_odr(PRESSURE_ODR_50HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_31); +} + +void test_pressure_bmp390__filter_smooth_odr_1hz(void) { + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH); + cl_assert(pressure_set_odr(PRESSURE_ODR_1HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_7); +} + +void test_pressure_bmp390__filter_none_odr_25hz(void) { + // Verify that FILTER_NONE overrides ODR-specific IIR + pressure_init(); + pressure_set_filter_mode(PRESSURE_FILTER_NONE); + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert_equal_i(fake_i2c_last_written(REG_CONFIG), IIR_COEFF_0); +} + +void test_pressure_bmp390__filter_mode_not_initialized(void) { + cl_assert(!pressure_set_filter_mode(PRESSURE_FILTER_SMOOTH)); +} + +// ---- Normal Mode Bug Simulation ---- + +void test_pressure_bmp390__normal_mode_bits_written(void) { + fake_i2c_set_write_hook(prv_normal_mode_bug_hook, NULL); + pressure_init(); + + cl_assert(pressure_set_odr(PRESSURE_ODR_25HZ)); + + cl_assert(fake_i2c_was_written(REG_PWR_CTRL, + PRESS_EN | TEMP_EN | MODE_NORMAL)); + + uint8_t readback = fake_i2c_get_register(REG_PWR_CTRL); + cl_assert_equal_i(readback & MODE_NORMAL, 0); +} + +// ---- Altitude Calculation Tests ---- + +void test_pressure_bmp390__altitude_cm_sea_level(void) { + int32_t alt = pressure_get_altitude_cm(101325, 101325); + cl_assert_equal_i(alt, 0); +} + +void test_pressure_bmp390__altitude_cm_above_sea_level(void) { + int32_t alt = pressure_get_altitude_cm(100500, 101325); + cl_assert(alt > 6000); + cl_assert(alt < 8000); +} + +void test_pressure_bmp390__altitude_cm_below_sea_level(void) { + int32_t alt = pressure_get_altitude_cm(102000, 101325); + cl_assert(alt < 0); +} + +void test_pressure_bmp390__altitude_cm_zero_reference(void) { + int32_t alt = pressure_get_altitude_cm(101325, 0); + cl_assert_equal_i(alt, 0); +} + +void test_pressure_bmp390__altitude_full_sea_level(void) { + int32_t alt = pressure_get_altitude_full_cm(101325, 101325); + cl_assert_equal_i(alt, 0); +} + +void test_pressure_bmp390__altitude_full_skydive_exit(void) { + // ~4000m (~13000ft): pressure approximately 61640 Pa + int32_t alt = pressure_get_altitude_full_cm(61640, 101325); + cl_assert(alt > 380000); + cl_assert(alt < 420000); +} + +void test_pressure_bmp390__altitude_full_negative(void) { + int32_t alt = pressure_get_altitude_full_cm(103000, 101325); + cl_assert(alt < 0); +} + +void test_pressure_bmp390__altitude_full_zero_input(void) { + cl_assert_equal_i(pressure_get_altitude_full_cm(0, 101325), 0); + cl_assert_equal_i(pressure_get_altitude_full_cm(101325, 0), 0); +} + +// ---- Compensation Math Consistency ---- + +void test_pressure_bmp390__read_consistency(void) { + pressure_init(); + prv_set_sensor_data(6892135, 8360755); + + int32_t p1, t1, p2, t2; + cl_assert(pressure_read(&p1, &t1)); + cl_assert(pressure_read(&p2, &t2)); + cl_assert_equal_i(p1, p2); + cl_assert_equal_i(t1, t2); +} diff --git a/tests/fw/drivers/wscript b/tests/fw/drivers/wscript index e1b947098..e486bf634 100644 --- a/tests/fw/drivers/wscript +++ b/tests/fw/drivers/wscript @@ -24,4 +24,10 @@ def build(ctx): override_includes=['dummy_board'], platforms=['silk']) + clar(ctx, + sources_ant_glob='src/fw/drivers/pressure/bmp390.c' + ' tests/fakes/fake_i2c.c', + test_sources_ant_glob='test_pressure_bmp390.c', + override_includes=['dummy_board']) + # vim:filetype=python diff --git a/tools/generate_native_sdk/exported_symbols.json b/tools/generate_native_sdk/exported_symbols.json index c47bca81b..56d3a6c9a 100644 --- a/tools/generate_native_sdk/exported_symbols.json +++ b/tools/generate_native_sdk/exported_symbols.json @@ -707,6 +707,10 @@ "type": "type", "name": "PressureDataHandler", "addedRevision": "93" + }, { + "type": "type", + "name": "PressureFilterMode", + "addedRevision": "93" }, { "type": "function", "name": "pressure_service_subscribe", @@ -739,6 +743,10 @@ "type": "function", "name": "pressure_service_get_altitude_cm", "addedRevision": "93" + }, { + "type": "function", + "name": "pressure_service_set_filter_mode", + "addedRevision": "93" } ] }]