[ruby] Create DHT11 / 22 class from scratch using datasheet

Postscript

https://github.com/github0013/rpi-dht I tried to gem

$ gem install rpi-dht

problem

I tried using [dht-sensor-ffi] 1, but when I used it for a long time, I got an error and couldn't figure out what was wrong.

Solution

I have no choice but to make the library myself (I can't write C). with ruby

Preliminary matters

First of all, DHT is Single-bus communication (ONE-WIRE). So I think [GPIO4] 4 is that.

--3.3v / 5v as a plus for Raspberry Pi --Minus to Raspberry Pi minus --Data line to [GPIO4] 4 of Raspberry Pi

How to read the data sheet

[DHT11 data sheet] 2 [DHT22 data sheet] 3

Flow until data acquisition

AM2302_pdf-4.png

AM2302_pdf-2-2.png

There is no difference on 11/22 until the final data is taken. The flow is like this

  1. Set GPIO4 to ** output **
  2. Set to HIGH
  3. Set to LOW (within 1-20ms)
  4. Set GPIO4 to ** input **
  5. HIGH is returned once Approximately 20-200us
  6. LOW returns about 80us as Response signal
  7. HIGH returns about 80us as a response signal
  8. ** Data equivalent to 40 bits (5 bytes) flows continuously **
  9. LOW is flowing
  10. Be high all the time

HIGH / LOW is flowing every moment (in microseconds), and you have to read it (so it's tough for ruby ...).

40-bit reading

AM2302_pdf-3.png

Note that it is not simply HIGH = 1 and LOW = 0. After LOW lasts about 50us, it is judged whether HIGH lasts longer than 50us (= 1) or not (= 0) (so it is strict for ruby ...).

Check if HIGH / LOW continued ??? us

For example, if you take Time.now.to_f together when acquiring data, this process will probably take a long time and the data will flow first. For this reason, the data was saved in succession first, and the average number of times that LOW continued after that was saved, and it was judged whether HIGH was above or below the average.

How to use 40 bits

Organize 40 bits into 8-bit bytes,

  1. Humidity upper bite
  2. Humidity lower bite
  3. Temperature upper byte
  4. Temperature lower byte
  5. parity bytes

Divide into.

How to use parity bytes

Humidity upper byte + Humidity lower byte + Temperature upper byte + Temperature lower byte == parity Check with.

Humidity upper byte + Humidity lower byte + Temperature upper byte + Temperature lower byte 00000001 + 00000001 + 00000001 + 00000001 # => 00000100 If the parity is 00000100, it's OK

How to use the upper and lower bytes

For DHT11

Since the precision is low, the lower byte is always 00000000, so you can ignore it. Simply

--Humidity upper byte = Humidity% --Temperature upper byte = Temperature ℃

For DHT22

Both humidity and temperature are expressed in 2 bytes.

Example: 2 bytes of 0000001000001111 (527) assuming that the humidity is 52.7%, which is 52.7% when divided by 10. 2 bytes of 0000000100000111 (263) assuming that the humidity is 26.3 ℃, and dividing this by 10 gives 26.3 ℃

Therefore, the correct value cannot be obtained unless the upper byte is shifted by 8 bits and added to the lower byte.

Example


  humidity = ((humidity_high << 8) + humidity_low) / 10.to_f

Furthermore, only in the case of temperature, minus can be expressed. This depends on whether the high-order bit of temperature starts with 1. Therefore, it is necessary to drop the first bit after confirming whether the first bit is 1.

  is_negative = temp_high & 0b10000000
  temp_high &= 0b01111111

The later calculation is the same as the humidity example above.

Actual data acquisition

Whether or not the data flowing in microseconds can be accurately obtained (whether or not the parity check passes) depends on the degree of processing congestion at that time, so it is not known whether or not data can be reliably obtained each time with one call. .. Therefore, it is necessary to repeatedly execute it multiple times until data can be obtained. In fact, [dht-sensor-ffi] 1 also has [50 tries] 5 by default.

code

DHTBase class


require "rpi_gpio"
RPi::GPIO.set_numbering :bcm #Specify a pin with a bcm-based number

class DHTBase
  CLEAR_SIGNALS = 500 / 1000.to_f # ms
  START_SIGNAL = 1 / 1000.to_f # ms
  VALID_BYTE_SIZE = 5 # humidity_high, humidity_low, temp_high, temp_low, parity
  BITS_IN_BYTE = 8
  HUMIDITY_PRECISION = 10.to_f
  TEMPERATURE_PRECISION = 10.to_f
  ENOUGH_TO_CAPTURE_COUNT = 1000 #Number of times sufficient data can be obtained (about)

  class << self
    def read(pin)
      dht = new(pin)
      dht.send_start_signal
      dht.collect_response_bits
      dht.convert
    end
  end

  def initialize(pin)
    @pin = pin
  end

  def send_start_signal
    RPi::GPIO.setup pin, as: :output
    RPi::GPIO.set_high pin
    sleep(CLEAR_SIGNALS)

    RPi::GPIO.set_low pin
    sleep(START_SIGNAL)
  end

  def collect_response_bits
    RPi::GPIO.setup pin, as: :input, pull: :up
    @bits = ENOUGH_TO_CAPTURE_COUNT.times.collect { RPi::GPIO.high?(pin) }
    release

    break_into_byte_strings
    check_parity!
  end

  private

  attr_reader :pin, :bits, :byte_strings

  def release
    RPi::GPIO.clean_up pin
  end

  def break_by_high_or_low
    # HIGH = true
    # LOW = false
    # [false, false, false, ...]
    # [true, true, true, ...]
    #Summarize like
    last_value = :not_yet
    bits.slice_before do |value|
      (last_value != value).tap { |not_same| last_value = value if not_same }
    end.to_a
  end

  def break_into_byte_strings
    #For this reason, reverse and true from the bottom/After creating a false array pair
    #Data for 5 bytes is prepared for each 8 bits.

    # ture/False array pair = 0 or 1 bit
    #This is 5 bytes, 40 bits=80 array
    # [false, false, false, ...]
    # [true, true, true, ...]
    # [false, false, false, ...]
    # [true, true, true, ...]
    # [false, false, false, ...]
    # [true, true, true, ...]
    # ...
    # ...
    # ...
    #The end always ends with a continuity of false and a long-term continuity of true, and since it is not data, it is not used.
    # [false, false, false, ...]
    # [true, true, true, true, true, true, true, true, true, ...]
    end_part, *low_high_pairs = break_by_high_or_low.reverse.each_slice(2).to_a
    # low_high_pairs = [
    #   ture /false array pair=1 bit
    #1 byte for 8 elements
    #   [
    #     [[true, true ...], [false, false ...]],  1
    #     [[true, true ...], [false, false ...]],  2
    #     [[true, true ...], [false, false ...]],  3
    #     [[true, true ...], [false, false ...]],  4
    #     [[true, true ...], [false, false ...]],  5
    #     [[true, true ...], [false, false ...]],  6
    #     [[true, true ...], [false, false ...]],  7
    #     [[true, true ...], [false, false ...]],  8
    #   ]

    #   ...
    #5 bytes in total
    # ]

    #First true for Response signal/I have a false array but I don't need to read it
    #I'm taking only the last 5
    valid_bytes =
      low_high_pairs.reverse.each_slice(8).to_a.last(VALID_BYTE_SIZE).select do |pair|
        pair.all? { |x| x.is_a?(Array) }
      end

    unless valid_bytes.size == VALID_BYTE_SIZE
      raise "not valid byte set (#{valid_bytes.size}bytes, should be #{
              VALID_BYTE_SIZE
            }bytes)"
    end

    valid_bytes.each do |byte|
      unless byte.size == BITS_IN_BYTE
        raise "not a byte (#{byte.size}bits, should be #{BITS_IN_BYTE}bits)"
      end
    end

    all_falses = valid_bytes.collect { |byte| byte.collect(&:last) }.flatten(1) #From byte to bitwise array in bytes with flatten
    average_false_count = all_falses.sum(&:size) / all_falses.size.to_f

    #Judge whether it is 1 or 0 by comparing with the number of true elements based on the average number of false consecutives equivalent to 50us.
    @byte_strings =
      valid_bytes.collect do |byte|
        byte.collect { |trues, _| average_false_count <= trues.size ? 1 : 0 }.join
      end
  end

  def bytes
    byte_strings.collect { |x| x.to_i(2) }
  end

  def check_parity!
    humidity_high, humidity_low, temp_high, temp_low, parity = bytes
    unless (humidity_high + humidity_low + temp_high + temp_low) == parity
      raise "parity check failed"
    end
  end
end

DHT11 class


class DHT11 < DHTBase
  def convert
    humidity_high, _, temp_high, _, _ = bytes

    humidity = humidity_high
    temperature = temp_high #You should not know the negative temperature
    { humidity: humidity, temperature: temperature }
  end
end

DHT22 class


class DHT22 < DHTBase
  def convert
    humidity_high, humidity_low, temp_high, temp_low, _ = bytes

    is_negative = 0 < (temp_high & 0b10000000)
    temp_high &= 0b01111111

    humidity = ((humidity_high << 8) + humidity_low) / HUMIDITY_PRECISION
    temperature = ((temp_high << 8) + temp_low) / TEMPERATURE_PRECISION
    temperature *= -1 if is_negative
    { humidity: humidity, temperature: temperature }
  end
end

Execution example


100.times do
  begin
    p DHT22.read(4)
    break
  rescue => exception
    p exception
    puts exception.backtrace.first(10).join("\n")
    sleep 0.1
  end
end

Recommended Posts

[ruby] Create DHT11 / 22 class from scratch using datasheet
Try using Cocoa from Ruby
Create a fortune using Ruby
Class in Ruby
Examine HTML elements and create Page class (using Selenide)
[Note] Create a java environment from scratch with docker