1-Wire and ENS210 driver stack
ENS210.cpp
Go to the documentation of this file.
1 /**
2  * @file ENS210.cpp
3  * @brief ENS210 temperature and humidity sensor driver.
4  *
5  * This driver operates an ENS210 over a 1-Wire bus using a DS2485 at the
6  * host end and a DS28E18 at the remote end.
7  *
8  * @par References
9  * Vendor-provided Arduino driver:
10  * https://github.com/sciosense/ENS210_driver (2020-04-06, v3).
11  *
12  * @par Stack topology
13  * imxRT1024 -> I2C -> DS2485 -> - - - 1-Wire - - - -> DS28E18 -> I2C -> ENS210
14  *
15  * @par Notes
16  * - Rather than driving the ENS210 directly from a host I2C controller, this
17  * code builds a DS28E18 command sequence and executes it remotely.
18  * - I2C hardware initialization occurs in the platform-specific
19  * DS2485_ExecuteCommand implementation.
20  *
21  * @par Update history
22  * - 27-October-2023 Dave Nadler Initial version.
23  *
24  * @todo Add solderOffset support.
25  * @todo Add conditional debug printf support.
26  */
27 
28 
29 /*
30  * ==========================================================================
31  * imxRT1024 -> I2C -> DS2485 ------> 1-Wire ------> DS28E18 -> I2C -> ENS210
32  * ==========================================================================
33  * This ENS210 driver operates ENS210 via a one-wire bus and DS28E18.
34  * Instead of directly commanding the ENS210 over a host I2C interface,
35  * this code creates a DS28E18 'command sequence' and runs it on the DS28E18.
36  * Layers are:
37  * - code below provides the ENS210 API, using
38  * - DS28E18 driver, which sends command sequence to DS28E18 over 1-Wire via
39  * - one_wire command layer, which calls
40  * - DS2485 driver, which calls
41  * - NXP FSL I2C driver (or any I2C driver you substitute)
42  */
43 
44 // ToDo ENS210: Add solderOffset support
45 // ToDo ENS210: conditional debug printf's
46 
47 
48 #include <assert.h>
49 #include <stdio.h> // Diagnostic printf
50 
51 // FreeRTOS APIs for QwikTest() only
52 #include "FreeRTOS.h"
53 #include "task.h"
54 
55 #include "ENS210.hpp" // public interface for this class
56 
57 // Maxim 1-Wire
58 #include "1wire/one_wire.h"
59 #include "1wire/DS28E18.h"
60 
61 
62 // ========== Internal ENS210 definitions (not part of public interface) ==========
63 enum ENS210_REG : uint8_t { // ENS210 register addresses (not public)
64  ENS210_REG_PART_ID = 0x00,
65  ENS210_REG_UID = 0x04,
66  ENS210_REG_SYS_CTRL = 0x10,
67  ENS210_REG_SYS_STAT = 0x11,
68  ENS210_REG_SENS_RUN = 0x21,
69  ENS210_REG_SENS_START = 0x22,
70  ENS210_REG_SENS_STOP = 0x23,
71  ENS210_REG_SENS_STAT = 0x24,
72  ENS210_REG_T_VAL = 0x30,
73  ENS210_REG_H_VAL = 0x33,
74 };
75 static const uint8_t ENS210_I2C_SlaveAddressShifted = 0x43 << 1u; // left-shift accommodates low-order read/!write bit
76 static const int ENS210_PartID = 0x0210;
77 static const int ENS210_Boot_Time_MS = 2; // Time to boot in ms (also after reset, or going to high power)
78 static const int ENS210_THConv_Single_MS = 130; // Conversion time in ms for single shot T/H measurement
79 static const int ENS210_THConv_Continuous_MS = 238; // Conversion time in ms for continuous T/H measurement
80 
81 static const uint8_t ENS210_reset[] =
82  { ENS210_I2C_SlaveAddressShifted, ENS210_REG_SYS_CTRL, 0x80};// SYS_CTRL = x80 does device reset, boot time - 1.2ms
83 static const uint8_t ENS210_setActive[] =
84  { ENS210_I2C_SlaveAddressShifted, ENS210_REG_SYS_CTRL, 0x00};// Disable low-power (device stays active): SYS_CTRL = x00
85 static const uint8_t ENS210_setContinuousAndStart[] =
86  { ENS210_I2C_SlaveAddressShifted, ENS210_REG_SENS_RUN, 0x03, 0x03};// SENS_RUN=3 SENS_START=3 enables and starts both temperature and humidity
87 
88 // dataStream first byte is starting register, followed by register value(s)
89 void ENS210_T::writeRegisters(const uint8_t *dataStream, int len) {
91  //printf("ENS210_T::writeRegisters register %02X command sequence at index %d\n", dataStream[0], DS28E18_BuildPacket_GetSequencerPacketSize());
92  DS28E18_BuildPacket_I2C_WriteData(dataStream, len);
94 }
95 int ENS210_T::readRegisters(uint8_t firstRegister, int len) {
96  // Note for ENS210: a repeated start is required before reading
97  uint8_t setCAR[2] = { ENS210_I2C_SlaveAddressShifted, firstRegister };
99  //printf("ENS210_T::readRegisters register %02X command sequence at index %d, ", firstRegister, DS28E18_BuildPacket_GetSequencerPacketSize());
100  // First write the starting address to DS28E18 (set the CAR Current Address Register)
102  // Address the Device with read bit set to commence read
103  DS28E18_BuildPacket_I2C_Start(); // repeated start before read start
104  static const unsigned char ENS210_read[] =
105  { ENS210_I2C_SlaveAddressShifted | 0x01 }; // read start adds 'Read' bit to I2C address...
106  DS28E18_BuildPacket_I2C_WriteData(ENS210_read, sizeof(ENS210_read)); // send read to ENS210 address
107  uint8_t resultIdx = DS28E18_BuildPacket_I2C_ReadDataWithNackEnd(len); // finally read back 'len' bytes
109  //printf("result at index %d\n", resultIdx);
110  return resultIdx;
111 }
112 
113 
114 bool ENS210_T::Init() {
115  initOK = false;
116  do {
117  // Initialize Maxim 1-Wire library (beneath the hood, initializes I2C to DS2485 and DS2485)
118  int OneWireInitError = OneWire_Init();
119  assert(OneWireInitError==0);
120  if(OneWireInitError) break;
121 
122  // DS28E18 VDD_SENS (DS28E18 power to the sensor) requires 'Strong Pull-Up' 'SPU' on 1-Wire bus.
123  // That's turned on with DS2485 1-Wire Master Configuration (Register 0) Bit 13: Strong Pullup (SPU).
124  int SPUerror = OneWire_Enable_SPU(true); // DS2485 must provide strong power to 1-Wire bus
125  assert(SPUerror==0);
126  if(SPUerror) break;
127 
128  // ToDo ENS210: Assumes there's only one DS28E18 on 1-Wire bus (controlling ENS210); could search in Init()...
129  bool ds28e18_init_OK = DS28E18_Init(); // global current_DS28E18_ROM_ID is set to last 1-Wire device found
130  assert (ds28e18_init_OK);
131  if(!ds28e18_init_OK) break;
132  OneWireAddress = current_DS28E18_ROM_ID;
133 
134  // For temperature probe, use DS28E18Q+T internal I2C pull-up resistors,
135  // which must be enabled **BEFORE** powering up sensor with hard-VDD-pullup
136  // Turn on DS28E18 1.2k pull-ups for I2C SDA/SCL lines
137  // GPIO_CTRL_HI register, bit position within 4-bit subfield for each output:
138  // 3 SDA/MOSI
139  // 2 GPIOB/MISO (not connected, don't care)
140  // 1 SCL/SCLK
141  // 0 GPIOA/SS# (not connected, don't care)
142  // For 25k pull-up on all IO, PS=0, PW=F; GPIO_CTRL_HI=x0F
143  // For stronger 1.2k pull-up on all IO, PS=0, PW=F; GPIO_CTRL_HI=xF0
144  // No pull-down slew, outputs high or release line depending on pull-up selected: GPIO_CTRL_LO=0x0F
145  // Stronger pull-ups seem to be required (sometimes got 0 values during reads with weak pull-ups)
146  bool configured_GPIO_OK = DS28E18_WriteGpioConfiguration(CONTROL, 0xF0, 0x0F);
147  assert(configured_GPIO_OK);
148  if(!configured_GPIO_OK) break;
149 
150  // =================================== ENS210 I2C ==========================================
151  // The ENS210 I²C interface supports standard (100kbit/s) and fast (400kbit/s) mode.
152  // The device applies all mandatory I²C protocol features for slaves: START, STOP, Acknowledge,
153  // 7-bit slave address. ENS210 does not use clock stretching.
154  // None of the other optional I²C features (10-bit slave address, General Call, Software reset, or Device ID)
155  // are supported, nor are the master features (Synchronization, Arbitration, START byte).
156  bool configured_I2C_OK = DS28E18_WriteConfiguration(KHZ_400, DONT_IGNORE, I2C, /* SPI mode ignored for I2C: */MODE_0);
157  assert(configured_I2C_OK);
158  if(!configured_I2C_OK) break;
159 
160  // Apply power to ENS210 I2C sensor (after setting pull-ups above)
163  DS28E18_BuildPacket_Utility_Delay(DELAY_256msec); // give sensor time to power up 64->256
164  // Write the packet into DS28E18 sequencer memory, run it, and wait long enough for it to complete
165  bool writeAndRun_PowerUp_OK = DS28E18_BuildPacket_WriteAndRun();
166  assert(writeAndRun_PowerUp_OK);
167  if(!writeAndRun_PowerUp_OK) break;
168 
169  // --- Reset ENS210 by writing 0x80 to SYS_CTRL register at Address 0x10 ---
171  writeRegisters(ENS210_reset, sizeof(ENS210_reset));
172  // --- Wait Tboot=1.2ms for reset to complete ---
173  DS28E18_BuildPacket_Utility_Delay(DELAY_8msec); // tBoot can be up to 1.2msec, pad it a bit
174  // --- Set ENS210 active (disable low power) by writing 0x00 to SYS_CTRL register at Address 0x10 ---
175  writeRegisters(ENS210_setActive, sizeof(ENS210_setActive)); // activate ENS210 sensor
176  // --- Delay for boot
177  DS28E18_BuildPacket_Utility_Delay(DELAY_8msec); // tBoot can be up to 1.2msec, give it >2msec 16->256
178  // --- Read SYS_STAT and PART_ID
179  uint8_t SYS_STAT_idx = readRegisters(ENS210_REG_SYS_STAT, 1); // read 1 byte
180  uint8_t PART_ID_idx = readRegisters(ENS210_REG_PART_ID, 2+2+8); // read 2 bytes PART_ID, 2 bytes DIE_REV, and 8 bytes UID
181  // Write the packet into DS28E18 sequencer memory, run it, and wait long enough for it to complete
182  bool writeAndRun_StatusAndPartID_OK = DS28E18_BuildPacket_WriteAndRun();
183  assert(writeAndRun_StatusAndPartID_OK);
184  if(!writeAndRun_StatusAndPartID_OK) break;
185 
186  // read DS28E18 sequencer memory back to host to extract SYS_STAT and PART_ID-DIE_ID-UID values read from sensor
187  uint8_t sequencer_memory[DS28E18_BuildPacket_GetSequencerPacketSize()] = {0};
188  bool initSequencerReadOK = DS28E18_ReadSequencer(0x00, sequencer_memory, sizeof(sequencer_memory));
189  assert(initSequencerReadOK);
190  if(!initSequencerReadOK) break;
191  SYS_STAT = sequencer_memory[SYS_STAT_idx];
192  PART_ID = sequencer_memory[PART_ID_idx+1]<<8 | sequencer_memory[PART_ID_idx]; // looking for 0x0210 - OK
193  printf("ENS210::Init read SYS_STAT=x%02X, PARTID=x%04X\n", SYS_STAT, PART_ID);
194  assert(SYS_STAT_Valid()); // should be 1 in active state - OK
195  assert(PART_ID_Valid());
196  if( !(SYS_STAT_Valid() && PART_ID_Valid()) ) break;
197  uint8_t DIE_REV_idx = PART_ID_idx+2;
198  dieRevision = sequencer_memory[DIE_REV_idx+1]<<8 | sequencer_memory[DIE_REV_idx];
199  uint8_t UID_idx = DIE_REV_idx+2;
200  uniqueDeviceID =
201  (uint64_t)sequencer_memory[UID_idx+7]<<56 |
202  (uint64_t)sequencer_memory[UID_idx+6]<<48 |
203  (uint64_t)sequencer_memory[UID_idx+5]<<40 |
204  (uint64_t)sequencer_memory[UID_idx+4]<<32 |
205  (uint64_t)sequencer_memory[UID_idx+3]<<24 |
206  (uint64_t)sequencer_memory[UID_idx+2]<<16 |
207  (uint64_t)sequencer_memory[UID_idx+1]<< 8 |
208  (uint64_t)sequencer_memory[UID_idx+0]<< 0 ;
209  printf("ENS210::Init read dieRevision=x%02X, uniqueDeviceID=x%016llX\n", dieRevision, uniqueDeviceID);
210 
211  // Set continuous mode and start for both temperature and humidity sensor
213  writeRegisters(ENS210_setContinuousAndStart, sizeof(ENS210_setContinuousAndStart)); // run ENS210 sensors continuously
214  // Immediately after starting continuous, wait max conversion time for both temp and humidity = 238ms
215  DS28E18_BuildPacket_Utility_Delay(DELAY_256msec);
216  // Write the packet into DS28E18 sequencer memory, run it, and wait long enough for it to complete
217  bool started_OK = DS28E18_BuildPacket_WriteAndRun();
218  if(!started_OK) break;
219 
220  initOK = true;
221  } while(0);
222 
223  return initOK;
224 }
225 
226 ENS210_Result_T ENS210_T::Measure() {
227  ENS210_Result_T result;
228 
229  do {
230  if(! initOK) Init();
231  if(! initOK) break; // arrrggg...
232 
233  // Address this ENS210's DS28E18 controller on the 1-wire bus.
234  current_DS28E18_ROM_ID = OneWireAddress;
235 
236  // Set up and run DS28E18 sequencer (inside temperature probe) to read ENS210 temperature and humidity
237  bool readTemperatureAndHumidty_OK;
238  // saved information about loaded sequence...
239  static bool DS28E18_SequenceLoaded = false;
240  static uint8_t T_VAL_idx; // index to beginning of temperature value in (send and receive) sequence
241  static unsigned short TH_readSequenceLength;
242  if(!DS28E18_SequenceLoaded) {
243  // This sequence takes ~215mSec (including read-back below)
244  // Read temperature and humidity: 6 bytes (T_VAL and H_VAL) starting at T_VAL register
246  T_VAL_idx = readRegisters(ENS210_REG_T_VAL, 6);
247  TH_readSequenceLength = DS28E18_GetLastSequenceLength();
248  // Write the packet into DS28E18 sequencer memory, run it, and wait long enough for it to complete
249  readTemperatureAndHumidty_OK = DS28E18_BuildPacket_WriteAndRun();
250  DS28E18_SequenceLoaded = true;
251  } else {
252  // This sequence takes ~140mSec (including read-back below); saves 75mSec by not reloading DS28E18 sequencer
253  readTemperatureAndHumidty_OK = DS28E18_RerunLastSequence(TH_readSequenceLength);
254  }
255  assert(readTemperatureAndHumidty_OK);
256  if(!readTemperatureAndHumidty_OK) {
257  result.status = ENS210_Result_T::Status_I2C_error;
258  return result;
259  }
260 
261  // Read DS28E18 sequencer memory back to host to obtain values read from sensor
262  uint8_t readback2[DS28E18_BuildPacket_GetSequencerPacketSize()] = {0};
263  bool sequencerReadOK = DS28E18_ReadSequencer(0x00, readback2, sizeof(readback2));
264  if(!sequencerReadOK) {
265  result.status = ENS210_Result_T::Status_I2C_error; // could be local I2C to DS2485 (don't know about remote I2C)
266  return result;
267  };
268 
269  //printf("ENS210 T_VAL, H_VAL with checksums: x%02X%02X%02X, %02X%02X%02X\n",
270  // readback2[T_VAL_idx],readback2[T_VAL_idx+1],readback2[T_VAL_idx+2],readback2[T_VAL_idx+3],readback2[T_VAL_idx+4],readback2[T_VAL_idx+5]);
271  // Lambda function extracts raw returned value and verifies checksum
272  auto GetVal = [this](uint8_t *p, uint32_t &val, bool &OK, bool &validCRC) {
273  // Note low-order value is first byte, then high-order, then CRC and "OK" bit
274  val = p[1]<<8 | p[0];
275  OK = p[2] & 0x01;
276  uint8_t recdCRC = (p[2]>>1)&0x7F;
277  uint32_t payload = (((uint32_t)OK)<<16) | val;
278  uint32_t calcdCRC = crc7(payload);
279  validCRC = (recdCRC == calcdCRC);
280  };
281  uint32_t T_val;
282  bool T_OK;
283  bool T_validCRC;
284  GetVal(&readback2[T_VAL_idx], T_val, T_OK, T_validCRC);
285  uint32_t H_val;
286  bool H_OK;
287  bool H_validCRC;
288  GetVal(&readback2[T_VAL_idx+3], H_val, H_OK, H_validCRC);
289  // Verify checksums OK
290  if(!T_validCRC || !H_validCRC) {
291  result.status = ENS210_Result_T::Status_CRC_error;
292  break;
293  }
294  // Verify 'data valid' bits set
295  if(!(T_OK && H_OK)) {
296  result.status = ENS210_Result_T::Status_Invalid;
297  break;
298  }
299  // Store the valid result!
300  result.rawTemperature = T_val - soldercorrection;
301  result.rawHumidity = H_val;
302  result.status = ENS210_Result_T::Status_OK;
303 
304  } while(0);
305 
306  return result;
307 }
308 
309 unsigned long ENS210_T::QwikTest() {
310  // perform a timed measurement
311  unsigned long startTimeMS = xTaskGetTickCount() * portTICK_PERIOD_MS;
312  ENS210_Result_T r = Measure(); // does Init() if not yet completed
313  unsigned long elapsedMS = (xTaskGetTickCount() * portTICK_PERIOD_MS) - startTimeMS;
314  // report results
315  static bool initSummaryPrinted;
316  if(!initSummaryPrinted && initOK) {
317  printf("ENS210::Init read SYS_STAT=x%02X, PARTID=x%04X\n", SYS_STAT, PART_ID);
318  printf("ENS210::Init read dieRevision=x%02X, uniqueDeviceID=x%016llX\n", dieRevision, uniqueDeviceID);
319  initSummaryPrinted = true;
320  }
321  r.DiagPrintf();
322  return elapsedMS;
323 }
324 
325 // Compute the CRC-7 of 'val' (should only have 17 bits)
326 // https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation
327 // 7654 3210
328 // Polynomial 0b 1000 1001 ~ x^7+x^3+x^0
329 // 0x 8 9
330 #define CRC7WIDTH 7 // A 7 bits CRC has polynomial of 7th order, which has 8 terms
331 #define CRC7POLY 0x89 // The 8 coefficients of the polynomial
332 #define CRC7IVEC 0x7F // Initial vector has all 7 bits high
333 // Payload data
334 #define DATA7WIDTH 17
335 #define DATA7MASK ((1UL<<DATA7WIDTH)-1) // 0b 0 1111 1111 1111 1111
336 #define DATA7MSB (1UL<<(DATA7WIDTH-1)) // 0b 1 0000 0000 0000 0000
337 uint32_t ENS210_T::crc7( uint32_t val )
338 {
339  // Setup polynomial
340  uint32_t pol= CRC7POLY;
341  // Align polynomial with data
342  pol = pol << (DATA7WIDTH-CRC7WIDTH-1);
343  // Loop variable (indicates which bit to test, start with highest)
344  uint32_t bit = DATA7MSB;
345  // Make room for CRC value
346  val = val << CRC7WIDTH;
347  bit = bit << CRC7WIDTH;
348  pol = pol << CRC7WIDTH;
349  // Insert initial vector
350  val |= CRC7IVEC;
351  // Apply division until all bits done
352  while( bit & (DATA7MASK<<CRC7WIDTH) ) {
353  if( bit & val ) val ^= pol;
354  bit >>= 1;
355  pol >>= 1;
356  }
357  return val;
358 }
bool DS28E18_WriteGpioConfiguration(DS28E18_target_configuration_register_T CFG_REG_TARGET, uint8_t GPIO_HI, uint8_t GPIO_LO)
Definition: DS28E18.c:635
Measurement result from ENS210.
uint16_t rawHumidity
relative humidity in 1/512RH (ie a value of 51200 means 100% relative humidity)
int DS28E18_BuildPacket_GetSequencerPacketSize()
Get length of locally constructed command sequencer packet.
Definition: DS28E18.c:718
OneWire_ROM_ID_T current_DS28E18_ROM_ID
DS28E18 device addressed for current operations (may be one of many on 1-Wire bus) ...
Definition: DS28E18.c:84
void DS28E18_BuildPacket_ClearSequencerPacket()
Reset local command sequencer packet under construction.
Definition: DS28E18.c:706
void DS28E18_BuildPacket_Utility_Delay(DS28E18_utility_delay_T delayTimeInMsExponent)
Definition: DS28E18.c:1098
The value was read, the CRC matches, and data is valid.
bool DS28E18_BuildPacket_WriteAndRun()
Definition: DS28E18.c:730
void DS28E18_BuildPacket_I2C_WriteData(const uint8_t *i2cData, uint8_t i2cDataSize)
Definition: DS28E18.c:778
unsigned short DS28E18_BuildPacket_I2C_ReadDataWithNackEnd(int readBytes)
Definition: DS28E18.c:822
void DS28E18_BuildPacket_Utility_SensVddOn()
Definition: DS28E18.c:1110
Driver for a DS28E18 slave on a 1-Wire bus.
There was an I2C communication error attempting to read the value.
unsigned short DS28E18_GetLastSequenceLength()
Retrieve length of constructed sequence, for use in DS28E18_RerunLastSequence(length) ...
Definition: DS28E18.c:740
bool DS28E18_WriteConfiguration(DS28E18_protocol_speed_T SPD, DS28E18_ignore_nack_T INACK, DS28E18_protocol_T PROT, DS28E18_spi_mode_T SPI_MODE)
Definition: DS28E18.c:586
The value was read, the CRC matches, but the data is invalid (e.g. the measurement was not yet finish...
uint16_t rawTemperature
temperature in 1/64 Kelvin, corrected for solder offset
int DS28E18_Init()
Definition: DS28E18.c:122
Public C++ interface for the ENS210 temperature and humidity sensor driver.
void DS28E18_BuildPacket_I2C_Start()
Definition: DS28E18.c:755
void DS28E18_BuildPacket_I2C_Stop()
Definition: DS28E18.c:765
bool DS28E18_ReadSequencer(unsigned short nineBitStartingAddress, uint8_t *rxData, unsigned short readLength)
Definition: DS28E18.c:449
The value was read, but the CRC over the payload (valid and data) does not match. ...
bool DS28E18_RerunLastSequence(unsigned int length)
Definition: DS28E18.c:745
General 1-Wire API using the DS2485 1-Wire master.