Files
ardupilot/libraries/AP_Baro/AP_Baro_KellerLD.cpp
T
vshie 8ec46e1c7e AP_Baro: KellerLD: reject samples flagged as invalid by status byte
The 5-byte measurement read from the Keller 4LD...9LD family starts
with a status byte whose layout is defined by the "Communication
Protocol 4 LD...9 LD" spec. Until now the driver read that byte and
immediately threw it away, so the only rejection criterion on a
measurement was that neither the raw pressure nor the raw temperature
were exactly zero. On an I2C bus with jitter, EMI on long tether
cables, or contention with other devices, this lets through single-
sample glitches that propagate straight into SCALED_PRESSURE2 and from
there into the EKF as apparent altitude/depth spikes.

Honor the status byte: a healthy sample has bit 7 = 0, bit 6 = 1,
operating mode bits (4-3) = 0, and the memory checksum error bit
(2) = 0. Any other pattern is rejected as an invalid read. BUSY
(bit 5) is intentionally not checked here; the BlueRobotics reference
implementation found it unreliable when returned as part of the
5-byte block read, and polling it cleanly would require a separate
1-byte transaction that is out of scope for this change.

This is a minimal behavioural fix; no API, parameter, or other
driver changes.

Made-with: Cursor
2026-05-20 11:47:47 -03:00

337 lines
10 KiB
C++

/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AP_Baro_KellerLD.h"
#if AP_BARO_KELLERLD_ENABLED
#include <stdio.h>
#include <AP_Math/AP_Math.h>
#define KELLER_DEBUG 0
#if KELLER_DEBUG
# define Debug(fmt, args ...) do {printf(fmt "\n", ## args);} while(0)
#else
# define Debug(fmt, args ...)
#endif
extern const AP_HAL::HAL &hal;
// sensor metadata register
static const uint8_t CMD_METADATA_PMODE = 0x12;
// Measurement range registers
static const uint8_t CMD_PRANGE_MIN_MSB = 0x13;
static const uint8_t CMD_PRANGE_MIN_LSB = 0x14;
static const uint8_t CMD_PRANGE_MAX_MSB = 0x15;
static const uint8_t CMD_PRANGE_MAX_LSB = 0x16;
// write to this address to start pressure measurement
static const uint8_t CMD_REQUEST_MEASUREMENT = 0xAC;
// Status byte handling, per Keller LD "Communication Protocol 4LD-9LD" (v2.6, pg 7)
// bits 7-6 : reserved, must be 01
// bit 5 : ignored, known to be unreliable in block reads
// bits 4-3 : operating mode (00 = normal measurement)
// bit 2 : checksum error? (0 = ok, 1 = error)
// bits 1-0 : don't care
static const uint8_t STATUS_REQUIRED_MASK = 0b11011100;
static const uint8_t STATUS_REQUIRED_BITS = 0b01000000;
AP_Baro_KellerLD::AP_Baro_KellerLD(AP_Baro &baro, AP_HAL::Device &dev)
: AP_Baro_Backend(baro)
, _dev(&dev)
{
}
// Look for the device on the bus and see if it responds appropriately
AP_Baro_Backend *AP_Baro_KellerLD::probe(AP_Baro &baro, AP_HAL::Device &dev)
{
AP_Baro_KellerLD *sensor = NEW_NOTHROW AP_Baro_KellerLD(baro, dev);
if (!sensor || !sensor->_init()) {
delete sensor;
return nullptr;
}
return sensor;
}
// convenience function to work around device transfer oddities
bool AP_Baro_KellerLD::transfer_with_delays(uint8_t *send, uint8_t sendlen, uint8_t *recv, uint8_t recvlen)
{
if (!_dev->transfer(send, sendlen, nullptr, 0)) {
return false;
}
hal.scheduler->delay(1);
if(!_dev->transfer(nullptr, 0, recv, recvlen)) {
return false;
}
hal.scheduler->delay(1);
return true;
}
// This device has some undocumented finicky quirks and requires
// delays when reading out the measurement range, but for some reason
// this isn't an issue when requesting measurements. This is why we
// need to split the transfers with delays like this. (Using
// AP_HAL::I2CDevice::set_split_transfers will not work with these
// sensors)
bool AP_Baro_KellerLD::read_measurement_limit(float *limit, uint8_t msb_addr, uint8_t lsb_addr)
{
uint8_t data[3];
if (!transfer_with_delays(&msb_addr, 1, data, ARRAY_SIZE(data))) {
return false;
}
const uint16_t ms_word = (data[1] << 8) | data[2];
Debug("0x%02x: %d [%d, %d, %d]", msb_addr, ms_word, data[0], data[1], data[2]);
if (!transfer_with_delays(&lsb_addr, 1, data, ARRAY_SIZE(data))) {
return false;
}
const uint16_t ls_word = (data[1] << 8) | data[2];
Debug("0x%02x: %d [%d, %d, %d]", lsb_addr, ls_word, data[0], data[1], data[2]);
const uint32_t cal_data = (ms_word << 16) | ls_word;
memcpy(limit, &cal_data, sizeof(*limit));
if (isinf(*limit) || isnan(*limit)) {
return false;
}
Debug("data: %d, float: %.2f", cal_data, _p_min);
return true;
}
bool AP_Baro_KellerLD::read_cal()
{
// Read out pressure measurement range
if (!read_measurement_limit(&_p_min, CMD_PRANGE_MIN_MSB, CMD_PRANGE_MIN_LSB)) {
return false;
}
if (!read_measurement_limit(&_p_max, CMD_PRANGE_MAX_MSB, CMD_PRANGE_MAX_LSB)) {
return false;
}
if (_p_max <= _p_min) {
return false;
}
return true;
}
// Read sensor P-Mode type and set pressure reference offset
// This determines the pressure offset based on the type of sensor
// vented to atmosphere, gauged to vacuum, or gauged to standard sea-level pressure
bool AP_Baro_KellerLD::read_mode_type()
{
uint8_t cmd = CMD_METADATA_PMODE;
uint8_t data[3];
if (!transfer_with_delays(&cmd, 1, data, ARRAY_SIZE(data))) {
return false;
}
// Byte 3, Bit 0 & 1: Represents P-Mode
// "Communication Protocol 4 LD…9 LD", Version 2.6 pg 12 of 25
// https://keller-druck.com/?d=VeMYAQBxgoSNjUSHbdnBTU
_p_mode = (SensorMode)(data[2] & 0b11);
// update pressure offset based on P-Mode
switch (_p_mode) {
case SensorMode::PR_MODE:
// PR-Mode vented gauge sensor
// pressure reads zero when the pressure outside is equal to the pressure inside the enclosure
_p_mode_offset = _frontend.get_pressure(0);
break;
case SensorMode::PA_MODE:
// PA-Mode sealed gauge sensor
// pressure reads zero when the pressure outside is equal to 1.0 bar
// i.e., the pressure at which the vent is sealed
_p_mode_offset = 1.0;
break;
case SensorMode::PAA_MODE:
// PAA-mode Absolute sensor (zero at vacuum)
_p_mode_offset = 0.0;
break;
case SensorMode::UNDEFINED:
// we should give an error here
printf("KellerLD Device Mode UNDEFINED\n");
return false;
}
return true;
}
// We read out the measurement range to be used in raw value conversions
bool AP_Baro_KellerLD::_init()
{
if (!_dev) {
return false;
}
WITH_SEMAPHORE(_dev->get_semaphore());
// high retries for init
_dev->set_retries(10);
if (!read_cal()) {
printf("Cal read bad!\n");
return false;
}
if (!read_mode_type()) {
printf("Mode_Type read bad!\n");
return false;
}
printf("Keller LD found on bus %u address 0x%02x\n", _dev->bus_num(), _dev->get_bus_address());
// Send a command to take a measurement
_dev->transfer(&CMD_REQUEST_MEASUREMENT, 1, nullptr, 0);
memset(&_accum, 0, sizeof(_accum));
_instance = _frontend.register_sensor();
_dev->set_device_type(DEVTYPE_BARO_KELLERLD);
set_bus_id(_instance, _dev->get_bus_id());
_frontend.set_type(_instance, AP_Baro::BARO_TYPE_WATER);
// lower retries for run
_dev->set_retries(3);
// The sensor needs time to take a deep breath after reading out the calibration...
hal.scheduler->delay(150);
// Request 50Hz update
// The sensor really struggles with any jitter in timing at 100Hz, and will sometimes start reading out all zeros
_dev->register_periodic_callback(20 * AP_USEC_PER_MSEC,
FUNCTOR_BIND_MEMBER(&AP_Baro_KellerLD::_timer, void));
return true;
}
// Read out most recent measurement from sensor hw
bool AP_Baro_KellerLD::_read()
{
uint8_t data[5];
if (!_dev->transfer(nullptr, 0, data, sizeof(data))) {
Debug("Keller LD read failed!");
return false;
}
const uint8_t status = data[0];
uint16_t pressure_raw = (data[1] << 8) | data[2];
uint16_t temperature_raw = (data[3] << 8) | data[4];
#if KELLER_DEBUG
static uint8_t samples = 0;
if (samples < 3) {
samples++;
Debug("data: [%d, %d, %d, %d, %d]", data[0], data[1], data[2], data[3], data[4]);
Debug("pressure_raw: %d\ttemperature_raw: %d", pressure_raw, temperature_raw);
}
#endif
// Reject if the sensor is not in the normal measurement state, or is reporting an error.
if ((status & STATUS_REQUIRED_MASK) != STATUS_REQUIRED_BITS) {
Debug("Keller: bad status 0x%02x", status);
return false;
}
if (pressure_raw == 0 || temperature_raw == 0) {
Debug("Keller: bad read");
return false;
}
if (!pressure_ok(pressure_raw)) {
return false;
}
WITH_SEMAPHORE(_sem);
_update_and_wrap_accumulator(pressure_raw, temperature_raw, 128);
return true;
}
// Periodic callback, regular update at 50Hz
// Read out most recent measurement, and request another
// Max conversion time according to datasheet is ~8ms, so
// max update rate is ~125Hz, yet we struggle to get consistent
// performance/data at 100Hz
void AP_Baro_KellerLD::_timer(void)
{
_read();
_dev->transfer(&CMD_REQUEST_MEASUREMENT, 1, nullptr, 0);
}
// Accumulate a reading, shrink if necessary to prevent overflow
void AP_Baro_KellerLD::_update_and_wrap_accumulator(uint16_t pressure, uint16_t temperature, uint8_t max_count)
{
_accum.sum_pressure += pressure;
_accum.sum_temperature += temperature;
_accum.num_samples += 1;
if (_accum.num_samples == max_count) {
_accum.sum_pressure /= 2;
_accum.sum_temperature /= 2;
_accum.num_samples /= 2;
}
}
// Take the average of accumulated values and push to frontend
void AP_Baro_KellerLD::update()
{
float sum_pressure, sum_temperature;
float num_samples;
// update _p_mode_offset if vented guage
if (_p_mode == SensorMode::PR_MODE) {
// we need to get the pressure from on-board barometer
_p_mode_offset = _frontend.get_pressure(0);
}
{
WITH_SEMAPHORE(_sem);
if (_accum.num_samples == 0) {
return;
}
sum_pressure = _accum.sum_pressure;
sum_temperature = _accum.sum_temperature;
num_samples = _accum.num_samples;
memset(&_accum, 0, sizeof(_accum));
}
uint16_t raw_pressure_avg = sum_pressure / num_samples;
uint16_t raw_temperature_avg = sum_temperature / num_samples;
// per datasheet
float pressure = (raw_pressure_avg - 16384) * (_p_max - _p_min) / 32768 + _p_min + _p_mode_offset;
pressure *= 100000; // bar -> Pascal
float temperature = ((raw_temperature_avg >> 4) - 24) * 0.05f - 50;
_copy_to_frontend(_instance, pressure, temperature);
}
#endif // AP_BARO_KELLERLD_ENABLED