ESPHome  2024.5.0
ms8607.cpp
Go to the documentation of this file.
1 #include "ms8607.h"
2 
3 #include "esphome/core/hal.h"
4 #include "esphome/core/helpers.h"
5 #include "esphome/core/log.h"
6 
7 namespace esphome {
8 namespace ms8607 {
9 
11 static const char *const TAG = "ms8607";
12 
14 static const uint8_t MS8607_PT_CMD_RESET = 0x1E;
15 
18 static const uint8_t MS8607_PROM_START = 0xA0;
20 static const uint8_t MS8607_PROM_END = 0xAE;
22 static const uint8_t MS8607_PROM_COUNT = (MS8607_PROM_END - MS8607_PROM_START) >> 1;
23 
25 static const uint8_t MS8607_CMD_H_RESET = 0xFE;
27 static const uint8_t MS8607_CMD_H_MEASURE_NO_HOLD = 0xF5;
29 static const float MS8607_H_TEMP_COEFFICIENT = -0.18;
30 
32 static const uint8_t MS8607_CMD_ADC_READ = 0x00;
33 
34 // TODO: allow OSR to be turned down for speed and/or lower power consumption via configuration.
35 // ms8607 supports 6 different settings
36 
38 static const uint8_t MS8607_CMD_CONV_D1_OSR_8K = 0x4A;
40 static const uint8_t MS8607_CMD_CONV_D2_OSR_8K = 0x5A;
41 
44  NONE = 0,
46  PTH_RESET_FAILED = 1,
48  PT_RESET_FAILED = 2,
50  H_RESET_FAILED = 3,
52  PROM_READ_FAILED = 4,
54  PROM_CRC_FAILED = 5,
55 };
56 
59  NEEDS_RESET,
61  NEEDS_PROM_READ,
63  SUCCESSFUL,
64 };
65 
66 static uint8_t crc4(uint16_t *buffer, size_t length);
67 static uint8_t hsensor_crc_check(uint16_t value);
68 
70  ESP_LOGCONFIG(TAG, "Setting up MS8607...");
73 
74  // I do not know why the device sometimes NACKs the reset command, but
75  // try 3 times in case it's a transitory issue on this boot
76  this->set_retry(
77  "reset", 5, 3,
78  [this](const uint8_t remaining_setup_attempts) {
79  ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_,
80  this->humidity_device_->get_address());
81  // I believe sending the reset command to both addresses is preferable to
82  // skipping humidity if PT fails for some reason.
83  // However, only consider the reset successful if they both ACK
84  bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
85  bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
86 
87  if (!(pt_successful && h_successful)) {
88  ESP_LOGE(TAG, "Resetting I2C devices failed");
89  if (!pt_successful && !h_successful) {
91  } else if (!pt_successful) {
93  } else {
95  }
96 
97  if (remaining_setup_attempts > 0) {
98  this->status_set_error();
99  } else {
100  this->mark_failed();
101  }
102  return RetryResult::RETRY;
103  }
104 
107  this->status_clear_error();
108 
109  // 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
110  this->set_timeout("prom-read", 15, [this]() {
113  this->status_clear_error();
114  } else {
115  this->mark_failed();
116  return;
117  }
118  });
119 
120  return RetryResult::DONE;
121  },
122  5.0f); // executes at now, +5ms, +25ms
123 }
124 
126  if (this->setup_status_ != SetupStatus::SUCCESSFUL) {
127  // setup is still occurring, either because reset had to retry or due to the 15ms
128  // delay needed between reset & reading the PROM values
129  return;
130  }
131 
132  // Updating happens async and sequentially.
133  // Temperature, then pressure, then humidity
135 }
136 
138  ESP_LOGCONFIG(TAG, "MS8607:");
139  LOG_I2C_DEVICE(this);
140  // LOG_I2C_DEVICE doesn't work for humidity, the `address_` is protected. Log using get_address()
141  ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->humidity_device_->get_address());
142  if (this->is_failed()) {
143  ESP_LOGE(TAG, "Communication with MS8607 failed.");
144  switch (this->error_code_) {
146  ESP_LOGE(TAG, "Temperature/Pressure RESET failed");
147  break;
149  ESP_LOGE(TAG, "Humidity RESET failed");
150  break;
152  ESP_LOGE(TAG, "Temperature/Pressure && Humidity RESET failed");
153  break;
155  ESP_LOGE(TAG, "Reading PROM failed");
156  break;
158  ESP_LOGE(TAG, "PROM values failed CRC");
159  break;
160  case ErrorCode::NONE:
161  default:
162  ESP_LOGE(TAG, "Error reason unknown %u", static_cast<uint8_t>(this->error_code_));
163  break;
164  }
165  }
166  LOG_UPDATE_INTERVAL(this);
167  LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
168  LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
169  LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
170 }
171 
173  ESP_LOGD(TAG, "Reading calibration values from PROM");
174 
175  uint16_t buffer[MS8607_PROM_COUNT];
176  bool successful = true;
177 
178  for (uint8_t idx = 0; idx < MS8607_PROM_COUNT; ++idx) {
179  uint8_t const address_to_read = MS8607_PROM_START + (idx * 2);
180  successful &= this->read_byte_16(address_to_read, &buffer[idx]);
181  }
182 
183  if (!successful) {
184  ESP_LOGE(TAG, "Reading calibration values from PROM failed");
186  return false;
187  }
188 
189  ESP_LOGD(TAG, "Checking CRC of calibration values from PROM");
190  uint8_t const expected_crc = (buffer[0] & 0xF000) >> 12; // first 4 bits
191  buffer[0] &= 0x0FFF; // strip CRC from buffer, in order to run CRC
192  uint8_t const actual_crc = crc4(buffer, MS8607_PROM_COUNT);
193 
194  if (expected_crc != actual_crc) {
195  ESP_LOGE(TAG, "Incorrect CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc, actual_crc);
197  return false;
198  }
199 
200  this->calibration_values_.pressure_sensitivity = buffer[1];
201  this->calibration_values_.pressure_offset = buffer[2];
204  this->calibration_values_.reference_temperature = buffer[5];
206  ESP_LOGD(TAG, "Finished reading calibration values");
207 
208  // Skipping reading Humidity PROM, since it doesn't have anything interesting for us
209 
210  return true;
211 }
212 
219 static uint8_t crc4(uint16_t *buffer, size_t length) {
220  uint16_t crc_remainder = 0;
221 
222  // algorithm to add a byte into the crc
223  auto apply_crc = [&crc_remainder](uint8_t next) {
224  crc_remainder ^= next;
225  for (uint8_t bit = 8; bit > 0; --bit) {
226  if (crc_remainder & 0x8000) {
227  crc_remainder = (crc_remainder << 1) ^ 0x3000;
228  } else {
229  crc_remainder = (crc_remainder << 1);
230  }
231  }
232  };
233 
234  // add all the bytes
235  for (size_t idx = 0; idx < length; ++idx) {
236  for (auto byte : decode_value(buffer[idx])) {
237  apply_crc(byte);
238  }
239  }
240  // For the MS8607 CRC, add a pair of zeros to shift the last byte from `buffer` through
241  apply_crc(0);
242  apply_crc(0);
243 
244  return (crc_remainder >> 12) & 0xF; // only the most significant 4 bits
245 }
246 
256 static uint8_t hsensor_crc_check(uint16_t value) {
257  uint32_t polynom = 0x988000; // x^8 + x^5 + x^4 + 1
258  uint32_t msb = 0x800000;
259  uint32_t mask = 0xFF8000;
260  uint32_t result = (uint32_t) value << 8; // Pad with zeros as specified in spec
261 
262  while (msb != 0x80) {
263  // Check if msb of current value is 1 and apply XOR mask
264  if (result & msb) {
265  result = ((result ^ polynom) & mask) | (result & ~mask);
266  }
267 
268  // Shift by one
269  msb >>= 1;
270  mask >>= 1;
271  polynom >>= 1;
272  }
273  return result & 0xFF;
274 }
275 
277  // Tell MS8607 to start ADC conversion of temperature sensor
278  if (!this->write_bytes(MS8607_CMD_CONV_D2_OSR_8K, nullptr, 0)) {
279  this->status_set_warning();
280  return;
281  }
282 
283  auto f = std::bind(&MS8607Component::read_temperature_, this);
284  // datasheet says 17.2ms max conversion time at OSR 8192
285  this->set_timeout("temperature", 20, f);
286 }
287 
289  uint8_t bytes[3]; // 24 bits
290  if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) {
291  this->status_set_warning();
292  return;
293  }
294 
295  const uint32_t d2_raw_temperature = encode_uint32(0, bytes[0], bytes[1], bytes[2]);
296  this->request_read_pressure_(d2_raw_temperature);
297 }
298 
299 void MS8607Component::request_read_pressure_(uint32_t d2_raw_temperature) {
300  if (!this->write_bytes(MS8607_CMD_CONV_D1_OSR_8K, nullptr, 0)) {
301  this->status_set_warning();
302  return;
303  }
304 
305  auto f = std::bind(&MS8607Component::read_pressure_, this, d2_raw_temperature);
306  // datasheet says 17.2ms max conversion time at OSR 8192
307  this->set_timeout("pressure", 20, f);
308 }
309 
310 void MS8607Component::read_pressure_(uint32_t d2_raw_temperature) {
311  uint8_t bytes[3]; // 24 bits
312  if (!this->read_bytes(MS8607_CMD_ADC_READ, bytes, 3)) {
313  this->status_set_warning();
314  return;
315  }
316  const uint32_t d1_raw_pressure = encode_uint32(0, bytes[0], bytes[1], bytes[2]);
317  this->calculate_values_(d2_raw_temperature, d1_raw_pressure);
318 }
319 
320 void MS8607Component::request_read_humidity_(float temperature_float) {
321  if (!this->humidity_device_->write_bytes(MS8607_CMD_H_MEASURE_NO_HOLD, nullptr, 0)) {
322  ESP_LOGW(TAG, "Request to measure humidity failed");
323  this->status_set_warning();
324  return;
325  }
326 
327  auto f = std::bind(&MS8607Component::read_humidity_, this, temperature_float);
328  // datasheet says 15.89ms max conversion time at OSR 8192
329  this->set_timeout("humidity", 20, f);
330 }
331 
332 void MS8607Component::read_humidity_(float temperature_float) {
333  uint8_t bytes[3];
334  if (!this->humidity_device_->read_bytes_raw(bytes, 3)) {
335  ESP_LOGW(TAG, "Failed to read the measured humidity value");
336  this->status_set_warning();
337  return;
338  }
339 
340  // "the measurement is stored into 14 bits. The two remaining LSBs are used for transmitting status information.
341  // Bit1 of the two LSBS must be set to '1'. Bit0 is currently not assigned"
342  uint16_t humidity = encode_uint16(bytes[0], bytes[1]);
343  uint8_t const expected_crc = bytes[2];
344  uint8_t const actual_crc = hsensor_crc_check(humidity);
345  if (expected_crc != actual_crc) {
346  ESP_LOGE(TAG, "Incorrect Humidity CRC value. Provided value 0x%01X != calculated value 0x%01X", expected_crc,
347  actual_crc);
348  this->status_set_warning();
349  return;
350  }
351  if (!(humidity & 0x2)) {
352  // data sheet says Bit1 should always set, but nothing about what happens if it isn't
353  ESP_LOGE(TAG, "Humidity status bit was not set to 1?");
354  }
355  humidity &= ~(0b11); // strip status & unassigned bits from data
356 
357  // map 16 bit humidity value into range [-6%, 118%]
358  float const humidity_partial = double(humidity) / (1 << 16);
359  float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0);
360  float const compensated_humidity_percentage =
361  humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT;
362  ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage);
363 
364  if (this->humidity_sensor_ != nullptr) {
365  this->humidity_sensor_->publish_state(compensated_humidity_percentage);
366  }
367  this->status_clear_warning();
368 }
369 
370 void MS8607Component::calculate_values_(uint32_t d2_raw_temperature, uint32_t d1_raw_pressure) {
371  // Perform the first order pressure/temperature calculation
372 
373  // d_t: "difference between actual and reference temperature" = D2 - [C5] * 2**8
374  const int32_t d_t = int32_t(d2_raw_temperature) - (int32_t(this->calibration_values_.reference_temperature) << 8);
375  // actual temperature as hundredths of degree celsius in range [-4000, 8500]
376  // 2000 + d_t * [C6] / (2**23)
377  int32_t temperature =
378  2000 + ((int64_t(d_t) * this->calibration_values_.temperature_coefficient_of_temperature) >> 23);
379 
380  // offset at actual temperature. [C2] * (2**17) + (d_t * [C4] / (2**6))
381  int64_t pressure_offset = (int64_t(this->calibration_values_.pressure_offset) << 17) +
382  ((int64_t(d_t) * this->calibration_values_.pressure_offset_temperature_coefficient) >> 6);
383  // sensitivity at actual temperature. [C1] * (2**16) + ([C3] * d_t) / (2**7)
384  int64_t pressure_sensitivity =
385  (int64_t(this->calibration_values_.pressure_sensitivity) << 16) +
387 
388  // Perform the second order compensation, for non-linearity over temperature range
389  const int64_t d_t_squared = int64_t(d_t) * d_t;
390  int64_t temperature_2 = 0;
391  int32_t pressure_offset_2 = 0;
392  int32_t pressure_sensitivity_2 = 0;
393  if (temperature < 2000) {
394  // (TEMP - 2000)**2 / 2**4
395  const int32_t low_temperature_adjustment = (temperature - 2000) * (temperature - 2000) >> 4;
396 
397  // T2 = 3 * (d_t**2) / 2**33
398  temperature_2 = (3 * d_t_squared) >> 33;
399  // OFF2 = 61 * (TEMP-2000)**2 / 2**4
400  pressure_offset_2 = 61 * low_temperature_adjustment;
401  // SENS2 = 29 * (TEMP-2000)**2 / 2**4
402  pressure_sensitivity_2 = 29 * low_temperature_adjustment;
403 
404  if (temperature < -1500) {
405  // (TEMP+1500)**2
406  const int32_t very_low_temperature_adjustment = (temperature + 1500) * (temperature + 1500);
407 
408  // OFF2 = OFF2 + 17 * (TEMP+1500)**2
409  pressure_offset_2 += 17 * very_low_temperature_adjustment;
410  // SENS2 = SENS2 + 9 * (TEMP+1500)**2
411  pressure_sensitivity_2 += 9 * very_low_temperature_adjustment;
412  }
413  } else {
414  // T2 = 5 * (d_t**2) / 2**38
415  temperature_2 = (5 * d_t_squared) >> 38;
416  }
417 
418  temperature -= temperature_2;
419  pressure_offset -= pressure_offset_2;
420  pressure_sensitivity -= pressure_sensitivity_2;
421 
422  // Temperature compensated pressure. [1000, 120000] => [10.00 mbar, 1200.00 mbar]
423  const int32_t pressure = (((d1_raw_pressure * pressure_sensitivity) >> 21) - pressure_offset) >> 15;
424 
425  const float temperature_float = temperature / 100.0f;
426  const float pressure_float = pressure / 100.0f;
427  ESP_LOGD(TAG, "Temperature=%0.2f°C, Pressure=%0.2fhPa", temperature_float, pressure_float);
428 
429  if (this->temperature_sensor_ != nullptr) {
430  this->temperature_sensor_->publish_state(temperature_float);
431  }
432  if (this->pressure_sensor_ != nullptr) {
433  this->pressure_sensor_->publish_state(pressure_float); // hPa aka mbar
434  }
435  this->status_clear_warning();
436 
437  if (this->humidity_sensor_ != nullptr) {
438  // now that we have temperature (to compensate the humidity with), kick off that read
439  this->request_read_humidity_(temperature_float);
440  }
441 }
442 
443 } // namespace ms8607
444 } // namespace esphome
bool read_byte_16(uint8_t a_register, uint16_t *data)
Definition: i2c.h:246
This component has not successfully reset the PT & H devices.
Successfully read PROM and ready to update sensors.
void status_set_warning(const char *message="unspecified")
Definition: component.cpp:151
Both the Pressure/Temperature address and the Humidity address failed to reset.
uint8_t pressure
Definition: tt21100.cpp:19
void read_humidity_(float temperature_float)
process async humidity read
Definition: ms8607.cpp:332
uint16_t pressure_sensitivity_temperature_coefficient
Temperature coefficient of pressure sensitivity | TCS. [C3].
Definition: ms8607.h:86
uint16_t temperature_coefficient_of_temperature
Temperature coefficient of the temperature | TEMPSENS. [C6].
Definition: ms8607.h:94
void set_timeout(const std::string &name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition: component.cpp:69
bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len)
Compat APIs All methods below have been added for compatibility reasons.
Definition: i2c.h:212
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition: helpers.h:186
float lerp(float completion, float start, float end)
Linearly interpolate between start and end by completion (between 0 and 1).
Definition: helpers.cpp:95
void request_read_pressure_(uint32_t raw_temperature)
start async pressure read
Definition: ms8607.cpp:299
bool read_bytes_raw(uint8_t *data, uint8_t len)
Definition: i2c.h:216
sensor::Sensor * pressure_sensor_
Definition: ms8607.h:70
ErrorCode error_code_
Keep track of the reason why this component failed, to augment the dumped config. ...
Definition: ms8607.h:98
uint16_t reference_temperature
Reference temperature | T-REF. [C5].
Definition: ms8607.h:92
uint16_t pressure_sensitivity
Pressure sensitivity | SENS-T1. [C1].
Definition: ms8607.h:84
void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function< RetryResult(uint8_t)> &&f, float backoff_increase_factor=1.0f)
Set an retry function with a unique name.
Definition: component.cpp:60
void status_set_error(const char *message="unspecified")
Definition: component.cpp:159
uint16_t pressure_offset
Pressure offset | OFF-T1. [C2].
Definition: ms8607.h:88
sensor::Sensor * temperature_sensor_
Definition: ms8607.h:69
struct esphome::ms8607::MS8607Component::CalibrationValues calibration_values_
sensor::Sensor * humidity_sensor_
Definition: ms8607.h:71
void status_clear_warning()
Definition: component.cpp:166
constexpr14 std::array< uint8_t, sizeof(T)> decode_value(T val)
Decode a value into its constituent bytes (from most to least significant).
Definition: helpers.h:212
Reading the PROM calibration values failed.
void request_read_temperature_()
Start async temperature read.
Definition: ms8607.cpp:276
void publish_state(float state)
Publish a new state to the front-end.
Definition: sensor.cpp:39
The PROM calibration values failed the CRC check.
uint16_t temperature
Definition: sun_gtil2.cpp:26
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition: helpers.h:182
Component hasn&#39;t failed (yet?)
uint8_t address_
store the address of the device on the bus
Definition: i2c.h:269
void status_clear_error()
Definition: component.cpp:172
Asking the Pressure/Temperature sensor to reset failed.
void calculate_values_(uint32_t raw_temperature, uint32_t raw_pressure)
use raw temperature & pressure to calculate & publish values
Definition: ms8607.cpp:370
SetupStatus setup_status_
Current step in the multi-step & possibly delayed setup() process.
Definition: ms8607.h:103
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:118
uint16_t pressure_offset_temperature_coefficient
Temperature coefficient of pressure offset | TCO. [C4].
Definition: ms8607.h:90
Asking the Humidity sensor to reset failed.
MS8607HumidityDevice * humidity_device_
I2CDevice object to communicate with secondary I2C address for the humidity sensor.
Definition: ms8607.h:79
uint16_t length
Definition: tt21100.cpp:12
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
std::vector< uint8_t > bytes
Definition: sml_parser.h:12
Reset commands succeeded, need to wait >= 15ms to read PROM.
void read_pressure_(uint32_t raw_temperature)
process async pressure read
Definition: ms8607.cpp:310
void request_read_humidity_(float temperature_float)
start async humidity read
Definition: ms8607.cpp:320
void read_temperature_()
Process async temperature read.
Definition: ms8607.cpp:288
bool read_calibration_values_from_prom_()
Read and store the Pressure & Temperature calibration settings from the PROM.
Definition: ms8607.cpp:172
bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop=true)
Definition: i2c.h:248