Embedded HandbookПриложения
Все разделы › XI. Датчики: подключение, схемы, драйверы
STM32F103, STM32F407 · уровень средний · 8 мин чтения
Открыть в интерактивном справочнике

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

BME280 на шине I²C: CSB выбирает I²C, SDO задаёт адрес

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 на 30 гПа — и высота «уехала» на 245 м


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 Итоги

Дальше: раздел 99 «Движение: MPU-6050 и ADXL345» — ускорение и угловая скорость, шкалы, калибровка нуля и наклон по акселерометру.

Открыть в интерактивном справочнике