2024-05-27

Reading pressure from a QMP6988

I got hold of an envIII sensor for the indoor humidity, temperature and pressure readings, and bunged it on an RPi via a Grove HAT. This device incorporates an SHT30 for humidity and temperature, and a QMP6988 for pressure (but it also measures temperature for performing some compensation on the pressure). I had no trouble interpreting the SHT30's datasheet, and got the readings out with two I²C calls. The procedure for the QMP6988 is a bit more involved, and its datasheet required some guesswork, so I'm documenting the steps I took in case someone else is having trouble.

Reading the raw coefficients

To perform compensation, you need to read 12 raw integer coefficients, then scale and translate them as real numbers, before combining them with the raw pressure/temperature readings. The raw coefficients are expressed as 25 1-byte constant read-only registers within the device, so you only need to fetch them once, even if you're going to take multiple readings. I used the I2C_RDWR ioctl to write the register being requested, read the value, cancel the request, and confirm the cancellation, in sequence for each register. Each call (re-)used a single buffer:

uint8_t buf;
struct i2c_msg msg = {
  .addr = addr,
  .len = 1,
  .buf = &buf,
};
struct i2c_rdwr_ioctl_data pyld = {
  .msgs = &msg,
  .nmsgs = 1,
};

With fd open on the I²C device, I could request register reg_idx like this:

buf = reg_idx;
msg.flags = 0; // write
if (ioctl(fd, I2C_RDWR, &pyld) < 0)
  throw std::system_error(errno, std::generic_category());

To read, set msg.flags = I2C_M_RD, and call ioctl again. I kept reading as long as ioctl returned negative with errno == EIO.

My understanding of the datasheet is that one should then request register 0xff (as if to cancel the prior request), and keep reading until one gets 0. In fact, my code stopped if it got EIO or a zero, though I don't think I've seen the latter:

buf = 0;
msg.flags = I2C_M_RD;
do {
  if (ioctl(fd, I2C_RDWR, &pyld) < 0) {
    if (errno == EIO) break;
    throw std::system_error(errno, std::generic_category());
  }
  if (buf != 0x00) continue;
  break;
} while (true);

Coefficients' signedness

Ten of the coefficients are 16-bit integers, and the other two are 20-bit. I couldn't find anywhere in the datasheet about their signedness, but I only get reasonable readings if they are treated as signed. I used a wider unsigned type to compose the value from bytes, reinterpreted as the corresponding signed type, then subtracted if the ‘top’ bit was set:

uint_fast32_t val = low_byte;
val |= high_byte << 8;
int_fast32_t ival = val;
if (val & 0x8000)
  ival -= 0x10000;

Scaling and translating the coefficients

Each of the 16-bit integers must be divided by an integer constant, then multipled by a real constant, and then offset by another real. In the datasheet, these real constants are provided under Conversion factor in a table, and a general equation shows how to use them. However, the information for the 20-bit coefficients looks potentially contradictory. In the corresponding table, the Conversion factor column says Offset value (20Q16), while the equation simply says to divide by 16 (so no offset?). I haven't found any definition of this notation, but I think it implies that the original value is 20 bits, with the unit being 1/16. In other words, all you have to do is divide the signed integer by 16, as the equation states.

Taking the raw readings

I used a one-off write to one of the registers to initialize the device (a 2-byte <register, value> message), but I send another 2-byte message to force each reading. After waiting a moment, I read each of the 6 bytes separately, in the same way as reading the coefficients (request, read, cancel, confirm).

The datasheet states that each 24-bit reading should have 223 subtracted from it, but at 24bits[sic] output mode. I thought maybe this meant that the result should be masked with 0xffffff, but that would create a considerable discontinuity, and indeed it does not yield correct results. Simply treat the raw 24-bit value as unsigned, convert it to a signed value (with no sign extension), and do the subtraction.

Units

After applying compensation, the pressure is expressed in Pa, which is stated in the datasheet. Divide by 100 to get hPa or mbar.

The datasheet mentions 256 degreeC as the unit for the compensated temperature. I got meaningful readings by dividing by 256, so I guess it means that the unit is one 256th of a degree C. When you use the compensated temperature to compensate the pressure, just use the value as is; don't divide.

No comments:

Post a Comment