Real time measurement of kHz signals (i.e. DHT22 devices) and interrupts not viable with Argon


#1

Particle Argon running OS 1.4.2, with AM2302 (Device “A”) connected to D4 pin, and DHT22 (Device “B”) connected to D5 pin. An oscilloscope was used to verify proper signal levels and timing from DHT devices to Argon. Library to support DHT devices was imported from eliteio/PietteTech_DHT. The example program “DHT_2sensor” was compiled within VS code with Particle workbench.

Results: While a 90% valid result may be acceptable (Device “A”), it was concerning that less than 50% valid results for the 2nd (Device “B”). This was from a sample size of 130 continuous samples.

More analysis was accomplished measuring the timing in and around the interrupt routine using an Argon OS 1.4.2. Using pinSetFast(D7) upon entry to the isrCallback and several pinResetFast(D7) placed at the potential exit points of the ISR and an oscilloscope, the measured time from falling edge to beginning of the ISR was between 8us to 44us. The time within the ISR was approx. 12us.

Additionally, acquiring both devices simultaneously produced valid results less than 50% of the time. However, acquiring in a sequential fashion (i.e. start device B after device A has returned), produced valid results better than 96% of the time.

Conclusion: Argon, and maybe all Mesh Particle devices, can’t be used to measure kHz speed signals (8 - 14 kHz, “1” pulse width and “0” pulse width respectively).due to the high latency of entering the interrupt service routine.


#2

I think the best option is to use the I2S peripheral in the nRF52 for decoding DHT22 and similar signals. Even though I2S is a sound protocol, the main thing is that it can store the digital states of the signal over time in buffers. It can also generate arbitrary digital waveforms in the same way, which would be useful for generating 1-Wire signals. The main difference is that the hardware handles all of the data acquisition so there is no worry about interrupt latency. You’d only get an interrupt after the buffer receive or send is complete, where it’s no longer timing critical. This is much better than per-bit interrupts.

One issue in particular with timing-sensitive interrupts is that the mesh radio in Gen 3 devices runs at a high interrupt level, which tends to affect other interrupts. But it can’t be lowered, or the radio would stop running reliably.

I2S would eliminate this issue and I’m pretty sure it’s possible, but it’s non-trivial to implement. I played around with it a bit and I’m pretty sure it’s possible, but I never got it to work.


#3

@rickkas7: I understand the concept, but can’t wrap my head around the clock source as all the timers are utilized (unless I disable mesh or some other connectivity which defies purpose of using Argon/Boron).

Also, I presume I’d need two timers, one to generate a sampling clock (as DHT does not provide clock) and one to time-out the sampling period (~2.5 ms).

Any ideas how and what resources I could use?


#4

The way the I2S peripheral for I2S is that it captures a certain number of bits per channel (16 or 24) for left, then captures the same number of bits on the right channel. This corresponds to a single sample of audio, stereo, usually at a rate of 16000, 32000, or 44100 samples per second. Other sampling rates are possible.

However, the other way of looking at this is that 16 bits per sample at 16000 Hz, stereo, it’s capturing 512,000 digital 1-bit samples per second and saving them in a 64,000 buffer in RAM. (Well, since the devices don’t have that much RAM you probably can’t buffer a whole second, but you get the idea.) 512,000 samples per second is a 1.95 us period per sample.

What I would do is generate the start condition (18 ms low, then 20-40 us high) manually. Switch the pin to INPUT, then capture the data using the I2S peripheral. At worst, this is 50 us + 70 us for a 1 bit, so 130 us per bit = 5 bytes x 8 bits per byte = 40 bits = 5200 us.

With the I2S configured as above:

  • Low period (50 us) = 25.6 samples of 0

  • 0 bit (26-28 us) = 13.3 samples of 1

  • 1 bit (70 us) = 34.9 samples of 1

And since you only have to capture 5.2 ms of data, you only need 2666 samples or 333 bytes of data, so it will easily buffer in RAM without double buffering. I’d capture a little extra to be sure, say 350 bytes.

Once you have the 350 bytes of sample data, you then just count the pulse widths to figure out which bits are 0 and which are 1, then save this as 5 data bytes and calculate the checksum. Since all of the bits have already been read, the is no time sensitivity to this, but the calculation is not that difficult.

You don’t need any timer peripherals and don’t need any interrupts.


#5

There’s been a problem with DHT22 libraries on Gen 3 devices. Interrupt latency causes it to misinterpret bits for interrupt-based libraries (like PietteTech_DHT) and bit-banging isn’t much better. The narrowest pulse is 26-28 us.

I created a new library, DHT22Gen3_RK, that takes an entirely different approach. It uses the nRF52 I2S peripheral not to capture sound data, but as a DMA-based digital signal sampler. It can grab a full DHT22 sample into a 360 byte buffer at a 512 kHz sampling rate. It doesn’t require any interrupts, no timers are required, and it’s completely non-blocking. It doesn’t have to disable interrupts or thread swapping.

I ran a test overnight and captured 14,703 samples every 2.5 seconds with no checksum errors. The library will retry on error, but didn’t have to at all.

It can sample multiple sensors (sequentially, not at the same time), one per GPIO pin. There one annoying caveat: You need to allocate two spare GPIO pins. The I2S peripheral requires that you expose SCK and LRCK on physical GPIO pins or it doesn’t work. These signals are of no use for capturing DHT22 samples, but they still need to be allocated. Most people have a few GPIO left, so hopefully that’s OK.

The API is callback/lambda based, as it takes about 24 milliseconds to query a DHT22. The callback allows the loop to flow freely during this time, but the callback is dispatched from the loop context so you can still do things like Particle.publish from the callback. The callback is particularly helpful if a retry is required because of a bad checksum. Since the DHT22 can only be queried every 2 seconds, retries take a long time.

Repository and instructions: https://github.com/rickkas7/DHT22Gen3_RK


#6

Thanks for doing this. I’ll start playing with it.


#7

New version of DHT22Gen3_RK:

0.0.2 (2019-12-17)

  • Support for DHT11. Actually it worked before, but it wasn’t tested and there wasn’t an example for it.