diff --git a/Makefile b/Makefile index 50efd992f..3d6459bf6 100644 --- a/Makefile +++ b/Makefile @@ -165,11 +165,13 @@ endif @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis2mdl/main.go @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/dht/main.go + @md5sum ./build/test.hex DRIVERS = $(wildcard */) NOTESTS = build examples flash semihosting pcd8544 shiftregister st7789 microphone mcp3008 gps microbitmatrix \ hcsr04 ssd1331 ws2812 thermistor apa102 easystepper ssd1351 ili9341 wifinina shifter hub75 \ - hd44780 buzzer ssd1306 espat l9110x st7735 bmi160 l293x + hd44780 buzzer ssd1306 espat l9110x st7735 bmi160 l293x dht TESTS = $(filter-out $(addsuffix /%,$(NOTESTS)),$(DRIVERS)) unit-test: diff --git a/README.md b/README.md index 0bd1625f4..2a7b64129 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ The following 53 devices are supported. | [BMP180 barometer](https://cdn-shop.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf) | I2C | | [BMP280 temperature/barometer](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf) | I2C | | [Buzzer](https://en.wikipedia.org/wiki/Buzzer#Piezoelectric) | GPIO | +| [DHTXX thermometer and humidity sensor](https://cdn-shop.adafruit.com/datasheets/Digital+humidity+and+temperature+sensor+AM2302.pdf) | GPIO | | [DS1307 real time clock](https://datasheets.maximintegrated.com/en/ds/DS1307.pdf) | I2C | | [DS3231 real time clock](https://datasheets.maximintegrated.com/en/ds/DS3231.pdf) | I2C | | [ESP32 as WiFi Coprocessor with Arduino nina-fw](https://github.com/arduino/nina-fw) | SPI | diff --git a/dht/constants.go b/dht/constants.go new file mode 100644 index 000000000..8bed1a3c1 --- /dev/null +++ b/dht/constants.go @@ -0,0 +1,116 @@ +// Package dht provides a driver for DHTXX family temperature and humidity sensors. +// +// [1] Datasheet DHT11: https://www.mouser.com/datasheet/2/758/DHT11-Technical-Data-Sheet-Translated-Version-1143054.pdf +// [2] Datasheet DHT22: https://cdn-shop.adafruit.com/datasheets/Digital+humidity+and+temperature+sensor+AM2302.pdf +// Adafruit C++ driver: https://github.com/adafruit/DHT-sensor-library + +package dht // import "tinygo.org/x/drivers/dht" + +import ( + "encoding/binary" + "machine" + "time" +) + +// enum type for device type +type DeviceType uint8 + +// DeviceType specific parsing of information received from the sensor +func (d DeviceType) extractData(buf []byte) (temp int16, hum uint16) { + if d == DHT11 { + temp = int16(buf[2]) + if buf[3]&0x80 > 0 { + temp = -1 - temp + } + temp *= 10 + temp += int16(buf[3] & 0x0f) + hum = 10*uint16(buf[0]) + uint16(buf[1]) + } else { + hum = binary.LittleEndian.Uint16(buf[0:2]) + temp = int16(buf[3])<<8 + int16(buf[2]&0x7f) + if buf[2]&0x80 > 0 { + temp = -temp + } + } + return +} + +// Celsius and Fahrenheit temperature scales +type TemperatureScale uint8 + +func (t TemperatureScale) convertToFloat(temp int16) float32 { + if t == C { + return float32(temp) / 10 + } else { + // Fahrenheit + return float32(temp)*(9.0/50.) + 32. + } +} + +// All functions return ErrorCode instance as error. This class can be used for more efficient error processing +type ErrorCode uint8 + +const ( + startTimeout = time.Millisecond * 200 + startingLow = time.Millisecond * 20 + + DHT11 DeviceType = iota + DHT22 + + C TemperatureScale = iota + F + + ChecksumError ErrorCode = iota + NoSignalError + NoDataError + UpdateError + UninitializedDataError +) + +// error interface implementation for ErrorCode +func (e ErrorCode) Error() string { + switch e { + case ChecksumError: + // DHT returns ChecksumError if all the data from the sensor was received, but the checksum does not match. + return "checksum mismatch" + case NoSignalError: + // DHT returns NoSignalError if there was no reply from the sensor. Check sensor connection or the correct pin + // sis chosen, + return "no signal" + case NoDataError: + // DHT returns NoDataError if the connection was successfully initialized, but not all 40 bits from + // the sensor is received + return "no data" + case UpdateError: + // DHT returns UpdateError if ReadMeasurements function is called before time specified in UpdatePolicy or + // less than 2 seconds after past measurement + return "cannot update now" + case UninitializedDataError: + // DHT returns UninitializedDataError if user attempts to access data before first measurement + return "no measurements done" + } + // should never be reached + return "unknown error" +} + +// Update policy of the DHT device. UpdateTime cannot be shorter than 2 seconds. According to dht specification sensor +// will return undefined data if update requested less than 2 seconds before last usage +type UpdatePolicy struct { + UpdateTime time.Duration + UpdateAutomatically bool +} + +var ( + // timeout counter equal to number of ticks per 1 millisecond + timeout counter +) + +func init() { + timeout = cyclesPerMillisecond() +} + +func cyclesPerMillisecond() counter { + freq := machine.CPUFrequency() + freq /= 1000 + return counter(freq) +} diff --git a/dht/highfreq.go b/dht/highfreq.go new file mode 100644 index 000000000..2eb9de38f --- /dev/null +++ b/dht/highfreq.go @@ -0,0 +1,6 @@ +// +build mimxrt1062 stm32f405 atsamd51 stm32f103xx k210 stm32f407 + +package dht // import "tinygo.org/x/drivers/dht" + +// This file provides a definition of the counter for boards with frequency higher than 2^8 ticks per millisecond (>64MHz) +type counter uint32 diff --git a/dht/lowfreq.go b/dht/lowfreq.go new file mode 100644 index 000000000..ea974680c --- /dev/null +++ b/dht/lowfreq.go @@ -0,0 +1,6 @@ +// +build arduino atmega1284p nrf52840 digispark nrf52 arduino_nano nrf51 atsamd21 fe310 arduino_nano33 circuitplay_express arduino_mega2560 + +package dht // import "tinygo.org/x/drivers/dht" + +// This file provides a definition of the counter for boards with frequency lower than 2^8 ticks per millisecond (<64MHz) +type counter uint16 diff --git a/dht/thermometer.go b/dht/thermometer.go new file mode 100644 index 000000000..2ee3d9339 --- /dev/null +++ b/dht/thermometer.go @@ -0,0 +1,218 @@ +// Package dht provides a driver for DHTXX family temperature and humidity sensors. +// +// [1] Datasheet DHT11: https://www.mouser.com/datasheet/2/758/DHT11-Technical-Data-Sheet-Translated-Version-1143054.pdf +// [2] Datasheet DHT22: https://cdn-shop.adafruit.com/datasheets/Digital+humidity+and+temperature+sensor+AM2302.pdf +// Adafruit C++ driver: https://github.com/adafruit/DHT-sensor-library + +package dht // import "tinygo.org/x/drivers/dht" + +import ( + "machine" + "time" +) + +// DummyDevice provides a basic interface for DHT devices. +type DummyDevice interface { + ReadMeasurements() error + Measurements() (temperature int16, humidity uint16, err error) + Temperature() (int16, error) + TemperatureFloat(scale TemperatureScale) (float32, error) + Humidity() (uint16, error) + HumidityFloat() (float32, error) +} + +// Basic implementation of the DummyDevice +// This implementation takes measurements from sensor only with ReadMeasurements function +// and does not provide a protection from too frequent calls for measurements. +// Since taking measurements from the sensor is time consuming procedure and blocks interrupts, +// user can avoid any hidden calls to the sensor. +type device struct { + pin machine.Pin + + measurements DeviceType + initialized bool + + temperature int16 + humidity uint16 +} + +// ReadMeasurements reads data from the sensor. +// According to documentation pin should be always, but the t *device restores pin to the state before call. +func (t *device) ReadMeasurements() error { + // initial waiting + state := powerUp(t.pin) + defer t.pin.Set(state) + err := t.read() + if err == nil { + t.initialized = true + } + return err +} + +// Getter for temperature. Temperature method returns temperature as it is sent by device. +// The temperature is measured temperature in Celsius multiplied by 10. +// If no successful measurements for this device was performed, returns UninitializedDataError. +func (t *device) Temperature() (int16, error) { + if !t.initialized { + return 0, UninitializedDataError + } + return t.temperature, nil +} + +// Getter for temperature. TemperatureFloat returns temperature in a given scale. +// If no successful measurements for this device was performed, returns UninitializedDataError. +func (t *device) TemperatureFloat(scale TemperatureScale) (float32, error) { + if !t.initialized { + return 0, UninitializedDataError + } + return scale.convertToFloat(t.temperature), nil +} + +// Getter for humidity. Humidity returns humidity as it is sent by device. +// The humidity is measured in percentages multiplied by 10. +// If no successful measurements for this device was performed, returns UninitializedDataError. +func (t *device) Humidity() (uint16, error) { + if !t.initialized { + return 0, UninitializedDataError + } + return t.humidity, nil +} + +// Getter for humidity. HumidityFloat returns humidity in percentages. +// If no successful measurements for this device was performed, returns UninitializedDataError. +func (t *device) HumidityFloat() (float32, error) { + if !t.initialized { + return 0, UninitializedDataError + } + return float32(t.humidity) / 10., nil +} + +// Perform initialization of the communication protocol. +// Device lowers the voltage on pin for startingLow=20ms and starts listening for response +// Section 5.2 in [1] +func initiateCommunication(p machine.Pin) { + // Send low signal to the device + p.Configure(machine.PinConfig{Mode: machine.PinOutput}) + p.Low() + time.Sleep(startingLow) + // Set pin to high and wait for reply + p.High() + p.Configure(machine.PinConfig{Mode: machine.PinInput}) +} + +// Measurements returns both measurements: temperature and humidity as they sent by the device. +// If no successful measurements for this device was performed, returns UninitializedDataError. +func (t *device) Measurements() (temperature int16, humidity uint16, err error) { + if !t.initialized { + return 0, 0, UninitializedDataError + } + temperature = t.temperature + humidity = t.humidity + err = nil + return +} + +// Main routine that performs communication with the sensor +func (t *device) read() error { + // initialize loop variables + + // buffer for the data sent by the sensor. Sensor sends 40 bits = 5 bytes + bufferData := [5]byte{} + buf := bufferData[:] + + // We perform measurements of the signal from the sensor by counting low and high cycles. + // The bit is determined by the relative length of the high signal to low signal. + // For 1, high signal will be longer than low, for 0---low is longer. + // See section 5.3 [1] + signalsData := [80]counter{} + signals := signalsData[:] + + // Start communication protocol with sensor + initiateCommunication(t.pin) + // Wait for sensor's response and abort if sensor does not reply + err := waitForDataTransmission(t.pin) + if err != nil { + return err + } + // count low and high cycles for sensor's reply + receiveSignals(t.pin, signals) + + // process received signals and store the result in the buffer. Abort if data transmission was interrupted and not + // all 40 bits were received + err = t.extractData(signals[:], buf) + if err != nil { + return err + } + // Compute checksum and compare it to the one in data. Abort if checksum is incorrect + if !isValid(buf[:]) { + return ChecksumError + } + + // Extract temperature and humidity data from buffer + t.temperature, t.humidity = t.measurements.extractData(buf) + return nil +} + +// receiveSignals counts number of low and high cycles. The execution is time critical, so the function disables +// interrupts +func receiveSignals(pin machine.Pin, result []counter) { + i := uint8(0) + machine.UART1.Interrupt.Disable() + defer machine.UART1.Interrupt.Enable() + for ; i < 40; i++ { + result[i*2] = expectChange(pin, false) + result[i*2+1] = expectChange(pin, true) + } +} + +// extractData process signal counters and transforms them into bits. +// if any of the bits were not received (timed-out), returns NoDataError +func (t *device) extractData(signals []counter, buf []uint8) error { + for i := uint8(0); i < 40; i++ { + lowCycle := signals[i*2] + highCycle := signals[i*2+1] + if lowCycle == timeout || highCycle == timeout { + return NoDataError + } + byteN := i >> 3 + buf[byteN] <<= 1 + if highCycle > lowCycle { + buf[byteN] |= 1 + } + } + return nil +} + +// waitForDataTransmission waits for reply from the sensor. +// If no reply received, returns NoSignalError. +// For more details, see section 5.2 in [1] +func waitForDataTransmission(p machine.Pin) error { + // wait for thermometer to pull down + if expectChange(p, true) == timeout { + return NoSignalError + } + //wait for thermometer to pull up + if expectChange(p, false) == timeout { + return NoSignalError + } + // wait for thermometer to pull down and start sending the data + if expectChange(p, true) == timeout { + return NoSignalError + } + return nil +} + +// Constructor function for a DummyDevice implementation. +// This device provides full control to the user. +// It does not do any hidden measurements calls and does not check +// for 2 seconds delay between measurements. +func NewDummyDevice(pin machine.Pin, deviceType DeviceType) DummyDevice { + pin.High() + return &device{ + pin: pin, + measurements: deviceType, + initialized: false, + temperature: 0, + humidity: 0, + } +} diff --git a/dht/timesafethermometer.go b/dht/timesafethermometer.go new file mode 100644 index 000000000..3c4ff4761 --- /dev/null +++ b/dht/timesafethermometer.go @@ -0,0 +1,154 @@ +// Package dht provides a driver for DHTXX family temperature and humidity sensors. +// +// [1] Datasheet DHT11: https://www.mouser.com/datasheet/2/758/DHT11-Technical-Data-Sheet-Translated-Version-1143054.pdf +// [2] Datasheet DHT22: https://cdn-shop.adafruit.com/datasheets/Digital+humidity+and+temperature+sensor+AM2302.pdf +// Adafruit C++ driver: https://github.com/adafruit/DHT-sensor-library + +package dht // import "tinygo.org/x/drivers/dht" + +import ( + "machine" + "time" +) + +// Device interface provides main functionality of the DHTXX sensors. +type Device interface { + DummyDevice + Configure(policy UpdatePolicy) +} + +// managedDevice struct provides time control and optional automatic data retrieval from the sensor. +// It delegates all the functionality to device +type managedDevice struct { + t device + lastUpdate time.Time + policy UpdatePolicy +} + +// Measurements returns both measurements: temperature and humidity as they sent by the device. +// Depending on the UpdatePolicy of the device may update cached measurements. +func (m *managedDevice) Measurements() (temperature int16, humidity uint16, err error) { + err = m.checkForUpdateOnDataRequest() + if err != nil { + return 0, 0, err + } + return m.t.Measurements() +} + +// Getter for temperature. Temperature method returns temperature as it is sent by device. +// The temperature is measured temperature in Celsius multiplied by 10. +// Depending on the UpdatePolicy of the device may update cached measurements. +func (m *managedDevice) Temperature() (temp int16, err error) { + err = m.checkForUpdateOnDataRequest() + if err != nil { + return 0, err + } + temp, err = m.t.Temperature() + return +} + +func (m *managedDevice) checkForUpdateOnDataRequest() (err error) { + // update if necessary + if m.policy.UpdateAutomatically { + err = m.ReadMeasurements() + } + // ignore error if the data was updated recently + // interface comparison does not work in tinygo. Therefore need to cast to explicit type + if code, ok := err.(ErrorCode); ok && code == UpdateError { + err = nil + } + // add error if the data is not initialized + if !m.t.initialized { + err = UninitializedDataError + } + return err +} + +// Getter for temperature. TemperatureFloat returns temperature in a given scale. +// Depending on the UpdatePolicy of the device may update cached measurements. +func (m *managedDevice) TemperatureFloat(scale TemperatureScale) (float32, error) { + err := m.checkForUpdateOnDataRequest() + if err != nil { + return 0, err + } + return m.t.TemperatureFloat(scale) +} + +// Getter for humidity. Humidity returns humidity as it is sent by device. +// The humidity is measured in percentages multiplied by 10. +// Depending on the UpdatePolicy of the device may update cached measurements. +func (m *managedDevice) Humidity() (hum uint16, err error) { + err = m.checkForUpdateOnDataRequest() + if err != nil { + return 0, err + } + return m.t.Humidity() +} + +// Getter for humidity. HumidityFloat returns humidity in percentages. +// Depending on the UpdatePolicy of the device may update cached measurements. +func (m *managedDevice) HumidityFloat() (float32, error) { + err := m.checkForUpdateOnDataRequest() + if err != nil { + return 0, err + } + return m.t.HumidityFloat() +} + +// ReadMeasurements reads data from the sensor. +// The function will return UpdateError if it is called more frequently than specified in UpdatePolicy +func (m *managedDevice) ReadMeasurements() (err error) { + timestamp := time.Now() + if !m.t.initialized || timestamp.Sub(m.lastUpdate) > m.policy.UpdateTime { + err = m.t.ReadMeasurements() + } else { + err = UpdateError + } + if err == nil { + m.lastUpdate = timestamp + } + return +} + +// Configure configures UpdatePolicy for Device. +// Configure checks for policy.UpdateTime and prevent from updating more frequently than specified in [1][2] +// to prevent undefined behaviour of the sensor. +func (m *managedDevice) Configure(policy UpdatePolicy) { + if policy.UpdateAutomatically && policy.UpdateTime < time.Second*2 { + policy.UpdateTime = time.Second * 2 + } + m.policy = policy +} + +// Constructor of the Device implementation. +// This implementation updates data every 2 seconds during data access. +func New(pin machine.Pin, deviceType DeviceType) Device { + pin.High() + return &managedDevice{ + t: device{ + pin: pin, + measurements: deviceType, + initialized: false, + }, + lastUpdate: time.Time{}, + policy: UpdatePolicy{ + UpdateTime: time.Second * 2, + UpdateAutomatically: true, + }, + } +} + +// Constructor of the Device implementation with given UpdatePolicy +func NewWithPolicy(pin machine.Pin, deviceType DeviceType, updatePolicy UpdatePolicy) Device { + pin.High() + result := &managedDevice{ + t: device{ + pin: pin, + measurements: deviceType, + initialized: false, + }, + lastUpdate: time.Time{}, + } + result.Configure(updatePolicy) + return result +} diff --git a/dht/util.go b/dht/util.go new file mode 100644 index 000000000..0581c7376 --- /dev/null +++ b/dht/util.go @@ -0,0 +1,34 @@ +package dht // import "tinygo.org/x/drivers/dht" + +import ( + "machine" + "time" +) + +// Check if the pin is disabled +func powerUp(p machine.Pin) bool { + state := p.Get() + if !state { + p.High() + time.Sleep(startTimeout) + } + return state +} + +func expectChange(p machine.Pin, oldState bool) counter { + cnt := counter(0) + for ; p.Get() == oldState && cnt != timeout; cnt++ { + } + return cnt +} + +func checksum(buf []uint8) uint8 { + return buf[4] +} +func computeChecksum(buf []uint8) uint8 { + return buf[0] + buf[1] + buf[2] + buf[3] +} + +func isValid(buf []uint8) bool { + return checksum(buf) == computeChecksum(buf) +} diff --git a/examples/dht/main.go b/examples/dht/main.go new file mode 100644 index 000000000..5600e6b10 --- /dev/null +++ b/examples/dht/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "machine" + "time" + "tinygo.org/x/drivers/dht" +) + +func main() { + pin := machine.D6 + dhtSensor := dht.New(pin, dht.DHT11) + for { + temp, hum, err := dhtSensor.Measurements() + if err != nil { + fmt.Printf("Temperature: %02d.%d°C, Humidity: %02d.%d%%\n", temp/10, temp%10, hum/10, hum%10) + } else { + fmt.Printf("Could not take measurements from the sensor: %s\n", err.Error()) + } + // Measurements cannot be updated only 2 seconds. More frequent calls will return the same value + time.Sleep(time.Second * 2) + } +}