bmx280.c 12.8 KB
/*
 * Copyright (C) 2016 Kees Bakker, SODAQ
 *               2017 Inria
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup     drivers_bmx280
 * @{
 *
 * @file
 * @brief       Device driver implementation for sensors BMX280 (BME280 and BMP280).
 *
 * @author      Kees Bakker <kees@sodaq.com>
 *
 * @}
 */

#include <string.h>
#include <math.h>

#include "log.h"
#include "bmx280.h"
#include "bmx280_internals.h"
#include "bmx280_params.h"
#include "periph/i2c.h"
#include "xtimer.h"

#define ENABLE_DEBUG        (0)
#include "debug.h"

static int read_calibration_data(bmx280_t* dev);
static int do_measurement(const bmx280_t* dev);
static uint8_t get_ctrl_meas(const bmx280_t* dev);
static uint8_t get_status(const bmx280_t* dev);
static uint8_t read_u8_reg(const bmx280_t* dev, uint8_t reg);
static void write_u8_reg(const bmx280_t* dev, uint8_t reg, uint8_t b);
static uint16_t get_uint16_le(const uint8_t *buffer, size_t offset);
static int16_t get_int16_le(const uint8_t *buffer, size_t offset);

#if ENABLE_DEBUG
static void dump_buffer(const char *txt, uint8_t *buffer, size_t size);
#define DUMP_BUFFER(txt, buffer, size)  dump_buffer(txt, buffer, size)
#else
#define DUMP_BUFFER(txt, buffer, size)
#endif

/**
 * @brief   Fine resolution temperature value, also needed for pressure and humidity.
 */
static int32_t t_fine;

/**
 * @brief   The measurement registers, including temperature, pressure and humidity
 *
 * A temporary buffer for the memory map 0xF7..0xFE
 * These are read all at once and then used to compute the three sensor values.
 */
static uint8_t measurement_regs[8];

/*---------------------------------------------------------------------------*
 *                          BMX280 Core API                                  *
 *---------------------------------------------------------------------------*/

int bmx280_init(bmx280_t* dev, const bmx280_params_t* params)
{
    uint8_t chip_id;

    dev->params = *params;

    /* Initialize I2C interface */
    if (i2c_init_master(dev->params.i2c_dev, I2C_SPEED_NORMAL)) {
        DEBUG("[Error] I2C device not enabled\n");
        return BMX280_ERR_I2C;
    }

    /* Read chip ID */
    chip_id = read_u8_reg(dev, BMX280_CHIP_ID_REG);
    if ((chip_id != BME280_CHIP_ID) && (chip_id != BMP280_CHIP_ID)) {
        DEBUG("[Error] Did not detect a BMX280 at address %02x (%02x != %02x or %02x)\n",
              dev->params.i2c_addr, chip_id, BME280_CHIP_ID, BMP280_CHIP_ID);
        return BMX280_ERR_NODEV;
    }

    /* Read compensation data, 0x88..0x9F, 0xA1, 0xE1..0xE7 */
    if (read_calibration_data(dev)) {
        DEBUG("[Error] Could not read calibration data\n");
        return BMX280_ERR_NOCAL;
    }

    return BMX280_OK;
}

/*
 * Returns temperature in DegC, resolution is 0.01 DegC.
 * t_fine carries fine temperature as global value
 */
int16_t bmx280_read_temperature(const bmx280_t* dev)
{
    if (do_measurement(dev) < 0) {
        return INT16_MIN;
    }

    const bmx280_calibration_t *cal = &dev->calibration; /* helper variable */

    /* Read the uncompensated temperature */
    int32_t adc_T = (((uint32_t)measurement_regs[3 + 0]) << 12) |
        (((uint32_t)measurement_regs[3 + 1]) << 4) |
        ((((uint32_t)measurement_regs[3 + 2]) >> 4) & 0x0F);

    /*
     * Compensate the temperature value.
     * The following is code from Bosch's BME280_driver bme280_compensate_temperature_int32()
     * The variable names and the many defines have been modified to make the code
     * more readable.
     */
    int32_t var1;
    int32_t var2;

    var1 = ((((adc_T >> 3) - ((int32_t)cal->dig_T1 << 1))) * ((int32_t)cal->dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((int32_t)cal->dig_T1)) * ((adc_T >> 4) - ((int32_t)cal->dig_T1))) >> 12) *
            ((int32_t)cal->dig_T3)) >> 14;

    /* calculate t_fine (used for pressure and humidity too) */
    t_fine = var1 + var2;

    return (t_fine * 5 + 128) >> 8;
}

/*
 * Returns pressure in Pa
 */
uint32_t bmx280_read_pressure(const bmx280_t *dev)
{
    const bmx280_calibration_t *cal = &dev->calibration; /* helper variable */

    /* Read the uncompensated pressure */
    int32_t adc_P = (((uint32_t)measurement_regs[0 + 0]) << 12) |
        (((uint32_t)measurement_regs[0 + 1]) << 4) |
        ((((uint32_t)measurement_regs[0 + 2]) >> 4) & 0x0F);

    int64_t var1;
    int64_t var2;
    int64_t p_acc;

    /*
     * Compensate the pressure value.
     * The following is code from Bosch's BME280_driver bme280_compensate_pressure_int64()
     * The variable names and the many defines have been modified to make the code
     * more readable.
     */
    var1 = ((int64_t)t_fine) - 128000;
    var2 = var1 * var1 * (int64_t)cal->dig_P6;
    var2 = var2 + ((var1 * (int64_t)cal->dig_P5) << 17);
    var2 = var2 + (((int64_t)cal->dig_P4) << 35);
    var1 = ((var1 * var1 * (int64_t)cal->dig_P3) >> 8) + ((var1 * (int64_t)cal->dig_P2) << 12);
    var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)cal->dig_P1) >> 33;
    /* Avoid division by zero */
    if (var1 == 0) {
        return UINT32_MAX;
    }

    p_acc = 1048576 - adc_P;
    p_acc = (((p_acc << 31) - var2) * 3125) / var1;
    var1 = (((int64_t)cal->dig_P9) * (p_acc >> 13) * (p_acc >> 13)) >> 25;
    var2 = (((int64_t)cal->dig_P8) * p_acc) >> 19;
    p_acc = ((p_acc + var1 + var2) >> 8) + (((int64_t)cal->dig_P7) << 4);

    return p_acc >> 8;
}

#if defined(MODULE_BME280)
uint16_t bme280_read_humidity(const bmx280_t *dev)
{
    const bmx280_calibration_t *cal = &dev->calibration; /* helper variable */

    /* Read the uncompensated pressure */
    int32_t adc_H = (((uint32_t)measurement_regs[6 + 0]) << 8) |
        (((uint32_t)measurement_regs[6 + 1]));

    /*
     * Compensate the humidity value.
     * The following is code from Bosch's BME280_driver bme280_compensate_humidity_int32()
     * The variable names and the many defines have been modified to make the code
     * more readable.
     * The value is first computed as a value in %rH as unsigned 32bit integer
     * in Q22.10 format(22 integer 10 fractional bits).
     */
    int32_t var1;

    /* calculate x1*/
    var1 = (t_fine - ((int32_t)76800));
    /* calculate x1*/
    var1 = (((((adc_H << 14) - (((int32_t)cal->dig_H4) << 20) - (((int32_t)cal->dig_H5) * var1)) +
              ((int32_t)16384)) >> 15) *
            (((((((var1 * ((int32_t)cal->dig_H6)) >> 10) *
                 (((var1 * ((int32_t)cal->dig_H3)) >> 11) + ((int32_t)32768))) >> 10) +
               ((int32_t)2097152)) * ((int32_t)cal->dig_H2) + 8192) >> 14));
    var1 = (var1 - (((((var1 >> 15) * (var1 >> 15)) >> 7) * ((int32_t)cal->dig_H1)) >> 4));
    var1 = (var1 < 0 ? 0 : var1);
    var1 = (var1 > 419430400 ? 419430400 : var1);
    /* First multiply to avoid losing the accuracy after the shift by ten */
    return (100 * ((uint32_t)var1 >> 12)) >> 10;
}
#endif

/**
 * Read compensation data, 0x88..0x9F, 0xA1, 0xE1..0xE7
 *
 * This function reads all calibration bytes at once. These are
 * the registers DIG_T1_LSB (0x88) upto and including DIG_H6 (0xE7).
 */
static int read_calibration_data(bmx280_t* dev)
{
    uint8_t buffer[128];        /* 128 should be enough to read all calibration bytes */
    int nr_bytes;
#ifdef MODULE_BME280
    int nr_bytes_to_read = (BME280_DIG_H6_REG - BMX280_DIG_T1_LSB_REG) + 1;
#else
    int nr_bytes_to_read = (BMX280_DIG_P9_MSB_REG - BMX280_DIG_T1_LSB_REG) + 1;
#endif
    uint8_t offset = 0x88;

    memset(buffer, 0, sizeof(buffer));
    nr_bytes = i2c_read_regs(dev->params.i2c_dev, dev->params.i2c_addr, offset,
                             buffer, nr_bytes_to_read);
    if (nr_bytes != nr_bytes_to_read) {
        LOG_ERROR("Unable to read calibration data\n");
        return -1;
    }
    DUMP_BUFFER("Raw Calibration Data", buffer, nr_bytes);

    /* All little endian */
    dev->calibration.dig_T1 = get_uint16_le(buffer, BMX280_DIG_T1_LSB_REG - offset);
    dev->calibration.dig_T2 = get_int16_le(buffer, BMX280_DIG_T2_LSB_REG - offset);
    dev->calibration.dig_T3 = get_int16_le(buffer, BMX280_DIG_T3_LSB_REG - offset);

    dev->calibration.dig_P1 = get_uint16_le(buffer, BMX280_DIG_P1_LSB_REG - offset);
    dev->calibration.dig_P2 = get_int16_le(buffer, BMX280_DIG_P2_LSB_REG - offset);
    dev->calibration.dig_P3 = get_int16_le(buffer, BMX280_DIG_P3_LSB_REG - offset);
    dev->calibration.dig_P4 = get_int16_le(buffer, BMX280_DIG_P4_LSB_REG - offset);
    dev->calibration.dig_P5 = get_int16_le(buffer, BMX280_DIG_P5_LSB_REG - offset);
    dev->calibration.dig_P6 = get_int16_le(buffer, BMX280_DIG_P6_LSB_REG - offset);
    dev->calibration.dig_P7 = get_int16_le(buffer, BMX280_DIG_P7_LSB_REG - offset);
    dev->calibration.dig_P8 = get_int16_le(buffer, BMX280_DIG_P8_LSB_REG - offset);
    dev->calibration.dig_P9 = get_int16_le(buffer, BMX280_DIG_P9_LSB_REG - offset);

#if defined(MODULE_BME280)
    dev->calibration.dig_H1 = buffer[BME280_DIG_H1_REG - offset];
    dev->calibration.dig_H2 = get_int16_le(buffer, BME280_DIG_H2_LSB_REG - offset);
    dev->calibration.dig_H3 = buffer[BME280_DIG_H3_REG - offset];
    dev->calibration.dig_H4 = (((int16_t)buffer[BME280_DIG_H4_MSB_REG - offset]) << 4) +
        (buffer[BME280_DIG_H4_H5_REG - offset] & 0x0F);
    dev->calibration.dig_H5 = (((int16_t)buffer[BME280_DIG_H5_MSB_REG - offset]) << 4) +
        ((buffer[BME280_DIG_H4_H5_REG - offset] & 0xF0) >> 4);
    dev->calibration.dig_H6 = buffer[BME280_DIG_H6_REG - offset];
#endif

    DEBUG("[INFO] Chip ID = 0x%02X\n", buffer[BMX280_CHIP_ID_REG - offset]);

    /* Config is only be writable in sleep mode */
    (void)i2c_write_reg(dev->params.i2c_dev, dev->params.i2c_addr,
                        BMX280_CTRL_MEAS_REG, 0);

    uint8_t b;

    /* Config Register */
    /* spi3w_en unused */
    b = ((dev->params.t_sb & 7) << 5) | ((dev->params.filter & 7) << 2);
    write_u8_reg(dev, BMX280_CONFIG_REG, b);

#if defined(MODULE_BME280)
    /*
     * Note from the datasheet about ctrl_hum: "Changes to this register only become effective
     * after a write operation to "ctrl_meas".
     * So, set ctrl_hum first.
     */
    b = dev->params.humid_oversample & 7;
    write_u8_reg(dev, BME280_CTRL_HUMIDITY_REG, b);
#endif

    b = ((dev->params.temp_oversample & 7) << 5) |
        ((dev->params.press_oversample & 7) << 2) |
        (dev->params.run_mode & 3);
    write_u8_reg(dev, BMX280_CTRL_MEAS_REG, b);

    return 0;
}

/**
 * @brief Start a measurement and read the registers
 */
static int do_measurement(const bmx280_t* dev)
{
    /*
     * If settings has FORCED mode, then the device go to sleep after
     * it finished the measurement. To read again we have to set the
     * run_mode back to FORCED.
     */
    uint8_t ctrl_meas = get_ctrl_meas(dev);
    uint8_t run_mode = ctrl_meas & 3;
    if (run_mode != dev->params.run_mode) {
        /* Set the run_mode back to what we want. */
        ctrl_meas &= ~3;
        ctrl_meas |= dev->params.run_mode;
        write_u8_reg(dev, BMX280_CTRL_MEAS_REG, ctrl_meas);

        /* Wait for measurement ready? */
        size_t count = 0;
        while (count < 10 && (get_status(dev) & 0x08) != 0) {
            ++count;
        }
        /* What to do when measuring is still on? */
    }
    int nr_bytes;
    int nr_bytes_to_read = sizeof(measurement_regs);
    uint8_t offset = BMX280_PRESSURE_MSB_REG;

    nr_bytes = i2c_read_regs(dev->params.i2c_dev, dev->params.i2c_addr,
                             offset, measurement_regs, nr_bytes_to_read);
    if (nr_bytes != nr_bytes_to_read) {
        LOG_ERROR("Unable to read temperature data\n");
        return -1;
    }
    DUMP_BUFFER("Raw Sensor Data", measurement_regs, nr_bytes);

    return 0;
}

static uint8_t get_ctrl_meas(const bmx280_t* dev)
{
    return read_u8_reg(dev, BMX280_CTRL_MEAS_REG);
}

static uint8_t get_status(const bmx280_t* dev)
{
    return read_u8_reg(dev, BMX280_STAT_REG);
}

static uint8_t read_u8_reg(const bmx280_t* dev, uint8_t reg)
{
    uint8_t b;
    /* Assuming device is correct, it should return 1 (nr bytes) */
    (void)i2c_read_reg(dev->params.i2c_dev, dev->params.i2c_addr, reg, &b);
    return b;
}

static void write_u8_reg(const bmx280_t* dev, uint8_t reg, uint8_t b)
{
    /* Assuming device is correct, it should return 1 (nr bytes) */
    (void)i2c_write_reg(dev->params.i2c_dev, dev->params.i2c_addr, reg, b);
}

static uint16_t get_uint16_le(const uint8_t *buffer, size_t offset)
{
    return (((uint16_t)buffer[offset + 1]) << 8) + buffer[offset];
}

static int16_t get_int16_le(const uint8_t *buffer, size_t offset)
{
    return (((int16_t)buffer[offset + 1]) << 8) + buffer[offset];
}

#if ENABLE_DEBUG
static void dump_buffer(const char *txt, uint8_t *buffer, size_t size)
{
    size_t ix;
    DEBUG("%s\n", txt);
    for (ix = 0; ix < size; ix++) {
        DEBUG("%02X", buffer[ix]);
        if ((ix + 1) == size || (((ix + 1) % 16) == 0)) {
            DEBUG("\n");
        }
    }
}
#endif