Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dht20: add I2C driver for DHT20 temperature and humidity sensor #693

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions dht20/dht20.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Package dht20 implements a driver for the DHT20 temperature and humidity sensor.
//
// Datasheet: https://cdn-shop.adafruit.com/product-files/5183/5193_DHT20.pdf

package dht20

import (
"errors"
"time"

"tinygo.org/x/drivers"
)

var (
errUpdateCalledTooSoon = errors.New("Update() called within 80ms is invalid")
)

// Device wraps an I2C connection to a DHT20 device.
type Device struct {
bus drivers.I2C
Address uint16
data [8]uint8
temperature float32
humidity float32
prevAccessTime time.Time
}

// New creates a new DHT20 connection. The I2C bus must already be
// configured.
//
// This function only creates the Device object, it does not touch the device.
func New(bus drivers.I2C) Device {
return Device{
bus: bus,
Address: defaultAddress, // Using the address defined in registers.go
}
}

// Configure sets up the device for communication and initializes the registers if needed.
func (d *Device) Configure() error {
// Get the status word
d.data[0] = 0x71
err := d.bus.Tx(d.Address, d.data[:1], d.data[:1])
if err != nil {
return err
}

if d.data[0] != 0x18 {
// Initialize registers
err := d.initRegisters()
if err != nil {
return err
}
}
// Set the previous access time to the current time
d.prevAccessTime = time.Now()
return nil
}

// initRegisters initializes the registers 0x1B, 0x1C, and 0x1E to 0x00.
func (d *Device) initRegisters() error {
// Initialize register 0x1B
d.data[0] = 0x1B
d.data[1] = 0x00
err := d.bus.Tx(d.Address, d.data[:2], nil)
if err != nil {
return err
}

// Initialize register 0x1C
d.data[0] = 0x1C
d.data[1] = 0x00
err = d.bus.Tx(d.Address, d.data[:2], nil)
if err != nil {
return err
}

// Initialize register 0x1E
d.data[0] = 0x1E
d.data[1] = 0x00
err = d.bus.Tx(d.Address, d.data[:2], nil)
if err != nil {
return err
}

return nil
}

// Update reads data from the sensor and updates the temperature and humidity values.
// Note that the values obtained by this function are from the previous call to Update.
// If you want to use the most recent values, shorten the interval at which Update is called.
func (d *Device) Update(which drivers.Measurement) error {
if which&drivers.Temperature == 0 && which&drivers.Humidity == 0 {
return nil
}

// Check if 80ms have passed since the last access
if time.Since(d.prevAccessTime) < 80*time.Millisecond {
return errUpdateCalledTooSoon
}

// Check the status word Bit[7]
d.data[0] = 0x71
err := d.bus.Tx(d.Address, d.data[:1], d.data[:1])
if err != nil {
return err
}
if (d.data[0] & 0x80) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this case? The measurements are only updated when this is true... This case should be inverted and return an error if measurements are not updated as per the Sensor interface functioning.

if (d.data[0] & 0x80) != 0 {
    return errDHTBitSet
}

// Read 7 bytes of data from the sensor
err := d.bus.Tx(d.Address, nil, d.data[:7])
if err != nil {
return err
}
rawHumidity := uint32(d.data[1])<<12 | uint32(d.data[2])<<4 | uint32(d.data[3])>>4
rawTemperature := uint32(d.data[3]&0x0F)<<16 | uint32(d.data[4])<<8 | uint32(d.data[5])

// Convert raw values to human-readable values
d.humidity = float32(rawHumidity) / 1048576.0 * 100
d.temperature = float32(rawTemperature)/1048576.0*200 - 50

// Trigger the next measurement
d.data[0] = 0xAC
d.data[1] = 0x33
d.data[2] = 0x00
err = d.bus.Tx(d.Address, d.data[:3], nil)
if err != nil {
return err
}

// Update the previous access time to the current time
d.prevAccessTime = time.Now()
}
return nil
}

// Temperature returns the last measured temperature.
func (d *Device) Temperature() float32 {
return d.temperature
}

// Humidity returns the last measured humidity.
func (d *Device) Humidity() float32 {
return d.humidity
}
Comment on lines +137 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I'm just noticing this now- you are using floats! This would present a problem on certain devices with no FPU, causing lots of CPU cycles to be spent on processing the data.

It was not explicit in the Sensor PR, but I'd expect these methods to return an integer lest the sensor send back a binary floating point representation.

Copy link
Contributor

@soypat soypat Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using integer arithmetic and representing the humidity and temperature as fixed point integers the code could take the following form (have not tested, did some basic arithmetic to get the numbers):

rawHumidity := uint32(d.data[1])<<12 | uint32(d.data[2])<<4 | uint32(d.data[3])>>4
rawTemperature := uint32(d.data[3]&0x0F)<<16 | uint32(d.data[4])<<8 | uint32(d.data[5])

// humidity contains millipercent
d.humidity = int32(rawHumidity / 10) 
// temperature contains millicelsius
d.temperature = int32(rawTemperature/5) - 50000

If users need floating point representations (which they do) we could create a sensor package that contains something of this sort of logic:

type Thermometer interface {
    drivers.Sensor
    // Temperature returns fixed point representation of temperature in milicelsius [mC]. 
    Temperature() int32
}

func ReadKelvin32(t Thermometer) (float32, error) {
     err := t.Update(drivers.Temperature)
     if err != nil {
        return err
     }
     v := t.Temperature()
     return float32(v)/1000 + 273.15
}

// more advanced ADC configurations...
type sensorConversion struct {
    s drivers.Sensor
    gets []func() int32
    conversions []func(v int32) float32
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #332 for a previous attempt

6 changes: 6 additions & 0 deletions dht20/registers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dht20

// Constants/addresses used for I2C.

// The I2C address which this device listens to.
const defaultAddress = 0x38
36 changes: 36 additions & 0 deletions examples/dht20/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"machine"
"strconv"
"time"

"tinygo.org/x/drivers"
"tinygo.org/x/drivers/dht20"
)

var (
i2c = machine.I2C0
)

func main() {
i2c.Configure(machine.I2CConfig{})
sensor := dht20.New(i2c)
sensor.Configure()

// Trigger the first measurement
sensor.Update(drivers.AllMeasurements)

for {
time.Sleep(1 * time.Second)

// Update sensor dasta
sensor.Update(drivers.AllMeasurements)
temp := sensor.Temperature()
hum := sensor.Humidity()

// Note: The sensor values are from the previous measurement (1 second ago)
println("Temperature:", strconv.FormatFloat(float64(temp), 'f', 2, 64), "°C")
println("Humidity:", strconv.FormatFloat(float64(hum), 'f', 2, 64), "%")
}
}
1 change: 1 addition & 0 deletions smoketest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ tinygo build -size short -o ./build/test.hex -target=hifive1b ./examples/ssd1351
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis2mdl/main.go
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/max72xx/main.go
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/dht/main.go
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/dht20/main.go
# tinygo build -size short -o ./build/test.hex -target=arduino ./examples/keypad4x4/main.go
tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8523/
tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/alarm/
Expand Down
Loading