98. Давление и высота: BMP280 и BME280
Датчик давления отдаёт сырые коды АЦП, а превратить их в паскали можно только с коэффициентами, которые завод записал в память каждого экземпляра. Формулы производителя — целочисленные, давление — в 64 битах, и в них есть место, где аккуратный перенос на C превращается в неопределённое поведение. Из давления получается высота — с ловушкой, которая ошибается на сотни метров. BME280 добавляет влажность со своими, странно упакованными коэффициентами.
Подключение по I²C и подтяжки — раздел 95, влажность вообще — раздел 97.
Как это называется в документации
Оригинал Перевод trimming parameters, calibration data калибровочные коэффициенты, записанные на заводе compensation formula формула компенсации: код → физическая величина t_fine «тонкая» температура — промежуточная величина для давления и влажности oversampling передискретизация: несколько измерений на одно значение IIR filter цифровой фильтр с бесконечной импульсной характеристикой forced mode, normal mode однократное и непрерывное измерение chip ID код модели в регистре 0xD0 burst read чтение нескольких регистров подряд за одну транзакцию sea-level pressure, QNH давление, приведённое к уровню моря Q24.8 число с 24 битами целой части и 8 дробной
98.1 BMP280 или BME280
| BMP280 | BME280 | |
|---|---|---|
| меряет | давление и температуру | ещё и влажность |
| код в регистре 0xD0 | 0x58 | 0x60 |
| адрес на I²C | 0x76 или 0x77 — по выводу SDO | так же |
Модули обоих выглядят одинаково, и продают их нередко под чужим именем. Поэтому первое чтение после включения — регистр 0xD0:
#define BMX_REG_CALIB 0x88u /* 24 байта калибровки T и P: 0x88…0x9F */
#define BMX_REG_ID 0xD0u /* 0x58 — BMP280, 0x60 — BME280 */
#define BMX_REG_CTRL 0xF4u /* osrs_t[7:5], osrs_p[4:2], mode[1:0] */
#define BMX_REG_DATA 0xF7u /* давление, температура (и влажность у BME280) подряд */
#define BMP280_ID 0x58u
#define BME280_ID 0x60u
98.2 Калибровка: свои 24 байта у каждого экземпляра
Коэффициенты читают один раз при старте — 24 байта с адреса 0x88, младший байт первым; T1 и P1 без знака, остальные со знаком:
typedef struct {
uint16_t t1;
int16_t t2, t3;
uint16_t p1;
int16_t p2, p3, p4, p5, p6, p7, p8, p9;
} bmx_calib_t;
void bmx_parse(const uint8_t raw[24], bmx_calib_t *c)
{
c->t1 = u16le(raw);
c->t2 = (int16_t)u16le(raw + 2);
c->t3 = (int16_t)u16le(raw + 4);
c->p1 = u16le(raw + 6);
c->p2 = (int16_t)u16le(raw + 8);
c->p3 = (int16_t)u16le(raw + 10);
c->p4 = (int16_t)u16le(raw + 12);
c->p5 = (int16_t)u16le(raw + 14);
c->p6 = (int16_t)u16le(raw + 16);
c->p7 = (int16_t)u16le(raw + 18);
c->p8 = (int16_t)u16le(raw + 20);
c->p9 = (int16_t)u16le(raw + 22);
}
Дальше в разделе — набор из примера в документации: T1 = 27504, T2 = 26435, T3 = −1000, P1 = 36477, P2 = −10685, P3 = 3024, P4 = 2855, P5 = 140, P6 = −7, P7 = 15500, P8 = −14600, P9 = 6000. Программа на компьютере записала их байтами и разобрала обратно этой функцией — значения совпали.
98.3 Коды: одно пакетное чтение
Результат лежит с адреса 0xF7: три байта давления, три байта температуры (и два байта влажности у BME280). Читать их надо одной транзакцией: датчик обновляет регистры между измерениями, и двумя чтениями легко получить давление от одного измерения, а температуру — от другого. Код — 20 бит:
int32_t bmx_adc20(const uint8_t b[3])
{
return (int32_t)((uint32_t)b[0] << 12 | (uint32_t)b[1] << 4 | b[2] >> 4);
}
Байты 65 5A C0 7E ED 00 дают код давления 415148 и код температуры 519888.
Режим задаёт регистр 0xF4: температура ×1, давление ×16, непрерывные
измерения — байт 0x37:
#define BMX_CTRL(osrs_t, osrs_p, mode) ((uint8_t)((osrs_t) << 5 | (osrs_p) << 2 | (mode)))
У BME280 передискретизация влажности — в отдельном регистре 0xF2
(ctrl_hum, биты 2–0), и новое значение вступает в силу только после
записи в 0xF4. Порядок: сначала 0xF2 — например, 0x01, влажность ×1, —
потом 0xF4. После сброса в 0xF2 ноль: влажность не меряется, и два её
байта держат 0x8000.
98.4 Компенсация: сначала температура
Температура считается первой: кроме градусов она даёт t_fine — поправку,
без которой давление и влажность не посчитать.
int32_t bmx_temp(const bmx_calib_t *c, int32_t adc_t, int32_t *t_fine)
{
int32_t v1 = (((adc_t >> 3) - ((int32_t)c->t1 << 1)) * c->t2) >> 11;
int32_t v2 = (((((adc_t >> 4) - c->t1) * ((adc_t >> 4) - c->t1)) >> 12) * c->t3) >> 14;
*t_fine = v1 + v2;
return (*t_fine * 5 + 128) >> 8;
}
uint32_t bmx_press(const bmx_calib_t *c, int32_t adc_p, int32_t t_fine)
{
int64_t v1 = (int64_t)t_fine - 128000;
int64_t v2 = v1 * v1 * c->p6;
v2 += v1 * c->p5 * 131072; /* · 2^17 */
v2 += (int64_t)c->p4 * 34359738368; /* · 2^35 */
v1 = ((v1 * v1 * c->p3) >> 8) + v1 * c->p2 * 4096;
v1 = (((int64_t)1 << 47) + v1) * c->p1 >> 33;
if (v1 == 0)
return 0; /* деление на ноль */
int64_t p = 1048576 - adc_p;
p = ((p * 2147483648 - v2) * 3125) / v1; /* · 2^31 */
v1 = ((int64_t)c->p9 * (p >> 13) * (p >> 13)) >> 25;
v2 = ((int64_t)c->p8 * p) >> 19;
p = ((p + v1 + v2) >> 8) + (int64_t)c->p7 * 16;
return (uint32_t)p;
}
На наборе из примера: температура 2508 — 25,08 °C, t_fine = 128422;
давление 25767233 в формате Q24.8, то есть 25767233 / 256 = 100 653,25 Па.
Те же формулы производителя в плавающей точке дают то же давление
с расхождением меньше сотой доли паскаля.
Где формула ломается при переносе. В оригинале множители 2¹⁷, 2³⁵, 2¹²,
2³¹ и 16 записаны сдвигами влево, а сдвигаемые величины бывают отрицательными.
Сдвиг отрицательного числа влево в C — неопределённое поведение: сегодня
компилятор делает то, что ожидаешь, завтра на другом уровне оптимизации —
не обязан. Поэтому здесь умножение: код выходит тем же, но определённым.
Сдвиги вправо оставлены — для отрицательных чисел они зависят от реализации,
и у armclang и gcc арифметические.
64-битное умножение и деление на Cortex-M3 — это вызовы библиотеки, а не одна команда; раз в измерение это неважно.
98.5 Высота и её ловушка
Высота относительно уровня, где давление p0, — по барометрической формуле:
double altitude_m(double p_pa, double p0_pa)
{
return 44330.0 * (1.0 - pow(p_pa / p0_pa, 1.0 / 5.255));
}
100 653,25 Па при p0 = 101 325 Па — это 56,08 м, а у уровня моря один гектопаскаль — 8,33 м высоты. В этом и ловушка: давление на уровне моря меняется с погодой на десятки гектопаскалей, и если держать p0 постоянным, то при изменении на 30 гПа высота уедет на 245,1 м. Отсюда два правильных способа:
- абсолютная высота — p0 берут от метеослужбы или ставят по месту с известной высотой;
- изменение высоты — этаж, подъём, полёт — считают от давления в начале, и тогда погода за минуты почти не мешает.
98.6 BME280: влажность и полбайта на двоих
У влажности свои коэффициенты: H1 — байт 0xA1, H2…H6 — семь байт с 0xE1. H4 и H5 — числа по 12 бит, и младшие полбайта у них общие — байт 0xE5:
void bme_parse_h(uint8_t a1, const uint8_t e1[7], bme_hcalib_t *h)
{
h->h1 = a1;
h->h2 = (int16_t)u16le(e1);
h->h3 = e1[2];
h->h4 = (int16_t)((int8_t)e1[3] * 16 | (e1[4] & 0x0F));
h->h5 = (int16_t)((int8_t)e1[5] * 16 | e1[4] >> 4);
h->h6 = (int8_t)e1[6];
}
Семь байт 6A 01 00 13 29 03 1E дают H2 = 362, H3 = 0, H4 = 313, H5 = 50,
H6 = 30; с H1 = 75, кодом влажности 30000 и t_fine из 98.4 целая формула
даёт 56317 в формате Q22.10 — 54,997 %, формула в плавающей точке —
55,001 %.
uint32_t bme_hum(const bme_hcalib_t *h, int32_t adc_h, int32_t t_fine)
{
int32_t v = t_fine - 76800;
v = (((adc_h * 16384 - (int32_t)h->h4 * 1048576 - h->h5 * v) + 16384) >> 15)
* (((((((v * h->h6) >> 10) * (((v * h->h3) >> 11) + 32768)) >> 10) + 2097152) * h->h2 + 8192) >> 14);
v -= ((((v >> 15) * (v >> 15)) >> 7) * h->h1) >> 4;
v = v < 0 ? 0 : v > 419430400 ? 419430400 : v;
return (uint32_t)(v >> 12);
}
98.7 Грабли
Давление «ни о чём» при верной схеме. Калибровка не прочитана или разобрана не тем порядком байт — младший идёт первым.
BME280 не даёт влажность. На модуле BMP280: в регистре 0xD0 — 0x58. Проверять код модели до работы.
BME280 отдаёт влажность 0x8000. Не записан 0xF2 или записан после 0xF4 — новое значение вступает в силу только с записью в 0xF4.
Высота плывёт на сотни метров за день. Давление уровня моря меняется с погодой: 30 гПа — 245,1 м. Брать p0 от метеослужбы или считать от старта.
Давление и температура из разных измерений. Прочитаны двумя транзакциями. Читать шесть байт с 0xF7 одним пакетом, у BME280 — восемь, вместе с влажностью.
H4 и H5 — мусор. Не учтено, что они делят байт 0xE5.
Формула «ломается» на другой оптимизации. Сдвиг влево отрицательного числа — неопределённое поведение. Писать умножением.
98.8 Итоги
- Код АЦП без калибровки ничего не значит: 24 байта с 0x88 (у BME280 ещё 0xA1 и 0xE1…0xE7) читают при старте, младший байт первым.
- Сначала температура: она даёт
t_fineдля давления и влажности. - Давление — в 64 битах, в формате Q24.8: 25767233 / 256 = 100 653,25 Па.
- Модель — по регистру 0xD0: 0x58 — BMP280, 0x60 — BME280.
- У BME280 влажность включают отдельно: 0xF2 до 0xF4, иначе её код — 0x8000.
- Один гектопаскаль у уровня моря — 8,33 м, но p0 меняется с погодой: 30 гПа дают 245,1 м.
- Сдвиг влево отрицательного числа в C не определён — в формулах компенсации его заменяют умножением.
Дальше: раздел 99 «Движение: MPU-6050 и ADXL345» — ускорение и угловая скорость, шкалы, калибровка нуля и наклон по акселерометру.