# Trace code for DHT11 driver
###### tags: `Linux kernel` `Arduino` `trace code`
## [Datesheet](https://www.mouser.com/datasheet/2/758/DHT11-Technical-Data-Sheet-Translated-Version-1143054.pdf)
> When MCU sends a start signal, DHT11 changes from the low-power-consumption mode to the running-mode, waiting for MCU completing the start signal.

看到只有一根 pin 在溝通,又沒有 baudrate 設定之類的,一開始還有點疑惑是怎麼傳資料的。



Data 判斷 0 or 1 是由 pin high 的時間長度決定。
一次傳輸 5 個 byte(溫度 2 bytes, 濕度 2 bytes, check sum 1 byte)。
資料解析覺得沒什麼特別,比較在意抓資料的實作。
## Trace code
### [Arduino library](https://github.com/adafruit/DHT-sensor-library/blob/master/DHT.cpp)(b2be3b)
首先是最前面 handshake 的 18 ms,實際上是等 20 ms。
```c++
bool DHT::read(bool for// First set data line low for a period according to sensor type
pinMode(_pin, OUTPUT);
digitalWrite(_pin, LOW);
switch (_type) {
case DHT22:
case DHT21:
delayMicroseconds(1100); // data sheet says "at least 1ms"
break;
case DHT11:
default:
delay(20); // data sheet says at least 18ms, 20ms just to be safe
break;
...
}
```
接著是 input pull up 若干時間,由 user 設定,規格是 20~40 us。
值得注意的是這裡不能是 output high,不然會跟 sensor 打架。
```c
// End the start signal by setting data line high for 40 microseconds.
pinMode(_pin, INPUT_PULLUP);
// Delay a moment to let sensor pull data line low.
delayMicroseconds(pullTime);
```
接著等 sensor 拉 low 80 us,再拉 high 80 us。
不過實際上只要這兩個時間不要超過 1 ms 就可以,這裡的實作沒有強迫一定要 80 us。
```c
// First expect a low signal for ~80 microseconds followed by a high signal
// for ~80 microseconds again.
if (expectPulse(LOW) == TIMEOUT) {
DEBUG_PRINTLN(F("DHT timeout waiting for start signal low pulse."));
_lastresult = false;
return _lastresult;
}
if (expectPulse(HIGH) == TIMEOUT) {
DEBUG_PRINTLN(F("DHT timeout waiting for start signal high pulse."));
_lastresult = false;
return _lastresult;
}
```
接著就是讀 40 個 bit 的資料,這裡有點巧妙。
因為不論資料如何,都會是 low -> high 不斷重複,只是差在 high 的時間長短。拉 low 的時間是 50 us,拉 high 的時間 26~28 us for 0, 70 us for 1。
所以只要比較 low 跟 high 的 count 大小就可以知道是 0 還是 1。
```c
for (int i = 0; i < 80; i += 2) {
cycles[i] = expectPulse(LOW);
cycles[i + 1] = expectPulse(HIGH);
}
// Inspect pulses and determine which ones are 0 (high state cycle count < low
// state cycle count), or 1 (high state cycle count > low state cycle count).
for (int i = 0; i < 40; ++i) {
uint32_t lowCycles = cycles[2 * i];
uint32_t highCycles = cycles[2 * i + 1];
if ((lowCycles == TIMEOUT) || (highCycles == TIMEOUT)) {
DEBUG_PRINTLN(F("DHT timeout waiting for pulse."));
_lastresult = false;
return _lastresult;
}
data[i / 8] <<= 1;
// Now compare the low and high cycle times to see if the bit is a 0 or 1.
if (highCycles > lowCycles) {
// High cycles are greater than 50us low cycle count, must be a 1.
data[i / 8] |= 1;
}
// Else high cycles are less than (or equal to, a weird case) the 50us low
// cycle count so this must be a zero. Nothing needs to be changed in the
// stored data.
}
```
所以這個實作沒有那麼在意時間,只是比較相對時間而已。
話說整個過程會花了 23~25 ms,真的是夭壽慢。
### [Linux kernel](https://github.com/torvalds/linux/blob/master/drivers/iio/humidity/dht11.c#L188)(b229b6)
DHT11 在 linux kernel 裡是報 2 個 channel(溫度 & 濕度)。
```c
static const struct iio_chan_spec dht11_chan_spec[] = {
{ .type = IIO_TEMP,
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), },
{ .type = IIO_HUMIDITYRELATIVE,
.info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), }
};
static int dht11_read_raw(struct iio_dev *iio_dev,
const struct iio_chan_spec *chan,
int *val, int *val2, long m)
{
...
ret = IIO_VAL_INT;
if (chan->type == IIO_TEMP)
*val = dht11->temperature;
else if (chan->type == IIO_HUMIDITYRELATIVE)
*val = dht11->humidity;
else
ret = -EINVAL;
return ret;
}
```
不過主要還是看抓資料的方式跟 Arduino library 有什麼不同。
從資料結構上看應該是紀錄每次 interrupt 的時間跟 gpio 的狀態。
```c
#define DHT11_EDGES_PREAMBLE 2
#define DHT11_BITS_PER_READ 40
#define DHT11_EDGES_PER_READ (2 * DHT11_BITS_PER_READ + \
DHT11_EDGES_PREAMBLE + 1)
struct dht11 {
...
int num_edges;
struct {s64 ts; int value; } edges[DHT11_EDGES_PER_READ];
};
```
一開始也是先拉 high,sleep 18~20 ms。
```c
#define DHT11_START_TRANSMISSION_MIN 18000 /* us */
#define DHT11_START_TRANSMISSION_MAX 20000 /* us */
dht11->num_edges = 0;
ret = gpiod_direction_output(dht11->gpiod, 0);
usleep_range(DHT11_START_TRANSMISSION_MIN,
DHT11_START_TRANSMISSION_MAX);
```
接下來是把 io 切成 input(這裡 linux 的板子不一定像 Arduino 那樣有內部的 pull up,所以要靠 user 自己去把上拉電阻給加上去),然後註冊 ```dht11_handle_irq```(for rising or falling),接著等待完成。
```c
ret = gpiod_direction_input(dht11->gpiod);
ret = request_irq(dht11->irq, dht11_handle_irq,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
iio_dev->name, iio_dev);
ret = wait_for_completion_killable_timeout(&dht11->completion, HZ);
free_irq(dht11->irq, iio_dev);
```
ISR 裡做的事跟看資料結構時想得差不多。
```c
static irqreturn_t dht11_handle_irq(int irq, void *data)
{
struct iio_dev *iio = data;
struct dht11 *dht11 = iio_priv(iio);
if (dht11->num_edges < DHT11_EDGES_PER_READ && dht11->num_edges >= 0) {
dht11->edges[dht11->num_edges].ts = ktime_get_boottime_ns();
dht11->edges[dht11->num_edges++].value =
gpiod_get_value(dht11->gpiod);
if (dht11->num_edges >= DHT11_EDGES_PER_READ)
complete(&dht11->completion);
}
return IRQ_HANDLED;
}
```
等待完成之後就是 decode。理想清況下 ```offset``` 會是 2(```DHT11_EDGES_PREAMBLE```)。
檢查每兩次 edge 的時間差有沒有超過 49 us 來判斷 0 or 1,而且前的那個 edge 必須是 high。
```c
#define DHT11_THRESHOLD 49000 /* ns */
static int dht11_decode(struct dht11 *dht11, int offset)
{
int i, t;
char bits[DHT11_BITS_PER_READ];
unsigned char temp_int, temp_dec, hum_int, hum_dec, checksum;
for (i = 0; i < DHT11_BITS_PER_READ; ++i) {
t = dht11->edges[offset + 2 * i + 2].ts -
dht11->edges[offset + 2 * i + 1].ts;
if (!dht11->edges[offset + 2 * i + 1].value) {
dev_dbg(dht11->dev,
"lost synchronisation at edge %d\n",
offset + 2 * i + 1);
return -EIO;
}
bits[i] = t > DHT11_THRESHOLD;
}
...
}
```
至於為什麼要有 offset,我一開始看了一陣子是覺得不需要,最後也是靠 git blame 才找到原因。
> Instead of guessing where the data starts, we now just try to decode from
every possible start position. This causes no additional overhead if we
properly received the full preamble and only costs a few extra CPU cycles
in the case where the preamble is corrupted. This is much more efficient
than to return an error to userspace and start over again.
https://github.com/torvalds/linux/commit/22acc120a141ce0a3b6e98799d15970ef687bc70
```c
offset = DHT11_EDGES_PREAMBLE +
dht11->num_edges - DHT11_EDGES_PER_READ;
for (; offset >= 0; --offset) {
ret = dht11_decode(dht11, offset);
if (!ret)
break;
}
```