mirror of
https://github.com/rfivet/stm32bringup.git
synced 2025-02-21 05:37:09 -05:00
435 lines
13 KiB
HTML
435 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>3.1 DHT11 Humidity & Temperature</title>
|
||
<link type="text/css" rel="stylesheet" href="style.css">
|
||
</head>
|
||
<body>
|
||
<h1>3.1 DHT11 Humidity & Temperature</h1>
|
||
|
||
The DHT11 is a low cost humidity and temperature sensor from Aosong
|
||
which is easy to buy online. It is not popular as it has a non standard
|
||
communication protocol and its precision is ±5% for humidity and ±2°C
|
||
for temperature so it’s often overlooked for more expensive solution.
|
||
|
||
<h2>Hardware consideration</h2>
|
||
|
||
The DHT11 comes in a 4 pin package where only 3 pins are used: vcc, gnd
|
||
and data io. It can be powered at 5V or 3.3V. At 3.3V I can connect its
|
||
data io pin to any of the STM32 GPIO pins, if I want to test how it
|
||
behaves when powered at 5V, I will have to use one of the 5V tolerant
|
||
pin of the STM32.
|
||
<p>
|
||
The io data line when idle need to be at high level, so a pull up
|
||
resistor is necessary.
|
||
<p>
|
||
I will use 3.3V, connect DHT11 data io pin to STM32 GPIOA0. The small
|
||
DHT11 boards I use all have a pull up resistor between vcc and io data.
|
||
<p>
|
||
<img alt="DHT11 Boards" src="img/31_dht11.png">
|
||
|
||
<h2>Measurement frequency</h2>
|
||
|
||
The DHT11 needs one second to settle after power up, after that it can
|
||
be queried no more often than every two seconds.
|
||
<p>
|
||
This requirement is easy to implement based on uptime 1 second counter.
|
||
|
||
<h2>Communication protocol</h2>
|
||
|
||
In idle state, the io data line is kept high by the pull up resistor and
|
||
DHT11 is waiting for a request.
|
||
<p>
|
||
To request data, the STM32 needs to keep the io data line at low level
|
||
for more than 18 ms.
|
||
<p>
|
||
Once the STM32 releases the line, the pull up will bring the level back to up
|
||
and the DHT11 will assert the beginning of transmission by first pulling it
|
||
down for 80 µs then up for 80 µs.
|
||
<p>
|
||
The DHT11 will transmit 40 bits (5 bytes), high bit first, encoding a
|
||
zero as 50 µs low followed by 26-28 µs high and a one as 50 µs low
|
||
followed by 70 µs high.
|
||
<p>
|
||
The last bit is followed by 50 µs low to signal the end of transmission
|
||
and the return to idle state.
|
||
<p>
|
||
To implement this protocol on STM32 side:
|
||
<ul>
|
||
<li> GPIO pin as output low during 18 ms.
|
||
|
||
<li> GPIO pin as input to sample the line at a frequency that allow
|
||
differentiation of a 26-28 µs high (encoding a zero) versus a 70 µs
|
||
high (encoding a one).
|
||
</ul>
|
||
<h2>Data encoding</h2>
|
||
|
||
The 5 transmitted bytes hold humidity, temperature and checksum.
|
||
<ul>
|
||
<li> Byte 0: Integer part of humidity value.
|
||
|
||
<li> Byte 1: Fractional part of humidity value. Equals to zero.
|
||
|
||
<li> Byte 2: Integer part of temperature value.
|
||
|
||
<li> Byte 3: One digit fractional part of temperature value. 0-9, bit 7
|
||
indicate if temperature is below zero.
|
||
|
||
<li> Byte 4: Cheksum of bytes 0-3.
|
||
</ul>
|
||
The STM32F030 has no support for floating point representation so I will
|
||
use ad hoc representation of the temperature value.
|
||
|
||
<h2>Implementing Low Level API</h2>
|
||
|
||
I need to implement the following low level functionalities:
|
||
<ul>
|
||
<li> Configure a GPIOA pin as <b>input</b>. This is the default mode of most
|
||
GPIOA pins. In input mode the line is floating and the level can be
|
||
read.
|
||
|
||
<li> Configure a GPIOA pin as <b>output</b>. The level will be asserted by
|
||
STM32. The default output value is LOW.
|
||
|
||
<li> <b>Read</b> a GPIOA pin level, either HIGH or LOW.
|
||
|
||
<li> <b>Sleep</b> for a duration specified in µs.
|
||
</ul>
|
||
This is a minimal set of function based on the known state of the
|
||
system, <b>GPIOA</b> has already been enabled as I am using serial
|
||
transmission, the pins output level default to <b>LOW</b>, so I only need to
|
||
configure the pin as an output to pull down the line.
|
||
<p>
|
||
The sleep granularity is <b>1 µs</b>, I need to assert the line LOW for at
|
||
least 18 ms, so <b>1 ms</b> or even <b>10 ms</b> granularity would be fine.
|
||
<p>
|
||
I add the following lines to <b>system.h</b> to declare the interface I am
|
||
going to implement.
|
||
|
||
<pre>
|
||
/* GPIOA low level API ********************************************************/
|
||
|
||
typedef enum {
|
||
LOW = 0,
|
||
HIGH
|
||
} iolvl_t ;
|
||
|
||
void gpioa_input( int pin) ; /* Configure GPIOA pin as input */
|
||
void gpioa_output( int pin) ; /* Configure GPIOA pin as output */
|
||
iolvl_t gpioa_read( int pin) ; /* Read level of GPIOA pin */
|
||
|
||
void usleep( unsigned usecs) ; /* wait at least usec µs */
|
||
</pre>
|
||
|
||
I make a copy of <b>txeie.c</b> into <b>gpioa.c</b> to implement the new API.
|
||
|
||
<pre>
|
||
#define IDR 4
|
||
|
||
/* GPIOA low level API ********************************************************/
|
||
|
||
void gpioa_input( int pin) { /* Configure GPIOA pin as input */
|
||
GPIOA[ MODER] &= ~(3 << (pin * 2)) ; /* Apin as input [00] */
|
||
}
|
||
|
||
void gpioa_output( int pin) { /* Configure GPIOA pin as output */
|
||
GPIOA[ MODER] |= 1 << (pin * 2) ; /* Apin output (over [00]) */
|
||
}
|
||
|
||
iolvl_t gpioa_read( int pin) { /* Read level of GPIOA pin */
|
||
return LOW != (GPIOA[ IDR] & (1 << pin)) ;
|
||
}
|
||
</pre>
|
||
|
||
I didn’t use the GPIO Input Data Register (<b>IDR</b>) until now, so I add it
|
||
to the registers description.
|
||
<p>
|
||
<code>gpioa_output()</code> implementation is minimal. I know I am switching
|
||
only between input and output mode, so I don’t need to mask the bit field
|
||
first.
|
||
<p>
|
||
I use the System Tick to implement <code>usleep()</code>.
|
||
|
||
<pre>
|
||
void usleep( unsigned usecs) { /* wait at least usec µs */
|
||
#if CLOCK / 8000000 < 1
|
||
# error HCLOCK below 8 MHz
|
||
#endif
|
||
usecs = SYSTICK_CVR - (CLOCK / 8000000 * usecs) ;
|
||
while( SYSTICK_CVR > usecs) ;
|
||
}
|
||
</pre>
|
||
|
||
The System Tick generates an interrupt every second but I can read the
|
||
Current Value Register (<b>CVR</b>) to pause for smaller time period.
|
||
<p>
|
||
As I will read the sensor just after a new second count, I know that the
|
||
<b>CVR</b> value is close to maximum and I don’t need to care for a roll
|
||
over.
|
||
<p>
|
||
SysTick input clock is <b>HCLK/8</b>, this implementation will work for
|
||
<b>HCLK</b> equal to a multiple of 8 MHz (8, 16, 24, 32, 40, 48).
|
||
|
||
<h2>DHT11 API</h2>
|
||
|
||
I create the header file <b>dht11.h</b> with the following interface.
|
||
|
||
<pre>
|
||
/* dht11.h -- DHT11 API */
|
||
|
||
typedef enum {
|
||
DHT11_SUCCESS,
|
||
DHT11_FAIL_TOUT,
|
||
DHT11_FAIL_CKSUM
|
||
} dht11_retv_t ;
|
||
|
||
|
||
/* 5 .. 95 %RH, -20 .. 60 °C */
|
||
extern unsigned char dht11_humid ; /* 5 .. 95 %RH */
|
||
extern signed char dht11_tempc ; /* -20 .. 60 °C */
|
||
extern unsigned char dht11_tempf ; /* .0 .. .9 °C */
|
||
|
||
|
||
void dht11_init( void) ;
|
||
dht11_retv_t dht11_read( void) ;
|
||
</pre>
|
||
|
||
Usage:
|
||
<ul>
|
||
<li> Initialization: <code>dht11_init()</code>, once at startup.
|
||
|
||
<li> Call: <code>dht11_read()</code>, not more often than every two seconds,
|
||
starting one second after voltage stabilizes.
|
||
|
||
<li> Test for error: transmission protocol is based on strict timing and
|
||
data integrity is insured by checksum, so Timeout and Checksum error
|
||
need to be checked.
|
||
|
||
<li> Measurement available through global variables holding humidity,
|
||
integer part of temperature and one digit fractional part of
|
||
temperature.
|
||
</ul>
|
||
Based on this API, I write <b>dht11main.c</b>.
|
||
|
||
<pre>
|
||
/* dht11main.c -- sample DHT11 sensor */
|
||
#include <stdio.h>
|
||
|
||
#include "system.h"
|
||
#include "dht11.h"
|
||
|
||
int main() {
|
||
static unsigned last ;
|
||
|
||
dht11_init() ;
|
||
for( ;;)
|
||
if( last == uptime)
|
||
yield() ;
|
||
else {
|
||
last = uptime ;
|
||
if( last & 1) /* every 2 seconds starting 1 second after boot */
|
||
switch( dht11_read()) {
|
||
case DHT11_SUCCESS:
|
||
printf( "%u%%RH, %d.%uC\n", dht11_humid, dht11_tempc,
|
||
dht11_tempf) ;
|
||
break ;
|
||
case DHT11_FAIL_TOUT:
|
||
puts( "Timeout") ;
|
||
break ;
|
||
case DHT11_FAIL_CKSUM:
|
||
puts( "Cksum error") ;
|
||
}
|
||
}
|
||
}
|
||
</pre>
|
||
|
||
<h2>DHT11 API implementation</h2>
|
||
|
||
I first translate the specs into <b>pseudocode</b>.
|
||
|
||
<pre>
|
||
dht11_retv_t dht11_read( void) {
|
||
unsigned char values[ 5] ;
|
||
|
||
/* Host START: pulls line down for > 18ms then release line, pull-up raises to HIGH */
|
||
dht11_output() ;
|
||
usleep( 18000) ;
|
||
dht11_input() ;
|
||
|
||
/* DHT START: takes line, 80µs low then 80µs high */
|
||
wait_level( LOW) ; /* HIGH -> LOW, starts 80µs low */
|
||
wait_level( HIGH) ; /* LOW -> HIGH, ends 80µs low, starts 80µs high */
|
||
|
||
/* DHT transmits 40 bits, high bit first
|
||
* 0 coded as 50µs low then 26~28µs high
|
||
* 1 coded as 50µs low then 70µs high
|
||
*/
|
||
wait_level( LOW) ; /* HIGH -> LOW, ends 80µs high, starts 50µs low */
|
||
|
||
unsigned char sum = 0 ;
|
||
unsigned char v = 0 ;
|
||
for( int idx = 0 ; idx <= 4 ; idx += 1) {
|
||
sum += v ;
|
||
v = 0 ;
|
||
for( unsigned char curbit = 128 ; curbit ; curbit >>= 1) {
|
||
/* Measure duration of HIGH level */
|
||
wait_level( HIGH) ; /* LOW -> HIGH, ends 50µs low, starts timed high */
|
||
wait_level( LOW) ; /* HIGH -> LOW, timed high ends, starts 50µs low */
|
||
/* Set bit based on measured HIGH duration */
|
||
if( duration is 70µs) /* 0 == 26~28µs, 1 == 70µs */
|
||
v |= curbit ;
|
||
}
|
||
|
||
values[ idx] = v ;
|
||
}
|
||
|
||
/* DHT STOP: releases line after 50µs, pull-up raises to HIGH */
|
||
wait_level( HIGH) ; /* LOW -> HIGH, ends 50µs low, dht has released the line */
|
||
|
||
if( sum != values[ 4])
|
||
return DHT11_FAIL_CKSUM ;
|
||
|
||
dht11_humid = values[ 0] ;
|
||
dht11_tempc = values[ 2] ;
|
||
dht11_tempf = values[ 3] ;
|
||
if( dht11_tempf & 0x80) {
|
||
dht11_tempc *= -1 ;
|
||
dht11_tempf &= 0x7F ;
|
||
}
|
||
|
||
return DHT11_SUCCESS ;
|
||
}
|
||
</pre>
|
||
|
||
To turn this pseudocode into real implementation I need to code
|
||
<ul>
|
||
<li> <code>wait_level()</code>: wait for a line transmission and triggers a
|
||
timeout if there is none.
|
||
|
||
<li> a way to measure the duration of a HIGH level.
|
||
</ul>
|
||
I implement <code>wait_level()</code> as a macro, triggering a timeout when the
|
||
number of retries reach a limit. Originally, I set MAX_RETRIES to 999,
|
||
later I tune it to be large enough for the highest frequency supported
|
||
by STM32F030 (48 MHz).
|
||
|
||
<pre>
|
||
#define MAX_RETRIES 200 /* at 48 MHz, 160 retries for 80 µs HIGH */
|
||
#define is_not_LOW( a) a != LOW
|
||
#define is_not_HIGH( a) a == LOW
|
||
#define wait_level( lvl) \
|
||
retries = MAX_RETRIES ; \
|
||
while( is_not_##lvl( dht11_bread())) \
|
||
if( retries-- == 0) \
|
||
return DHT11_FAIL_TOUT
|
||
</pre>
|
||
|
||
<code>wait_level()</code> allows me to measure the duration of a wait in retries
|
||
unit. As DHT11 starts transmission by 80µs LOW followed by 80µs HIGH, I
|
||
can measure 80µs in retries unit. This is all I need to calibrate the timing
|
||
measurement.
|
||
<p>
|
||
I can do this calibration every time the DHT11 starts transmission, this
|
||
way I don’t need to update some constant if I change the frequency of my
|
||
system clock.
|
||
|
||
<pre>
|
||
/* DHT transmits 40 bits, high bit first
|
||
* 0 coded as 50µs low then 26~28µs high
|
||
* 1 coded as 50µs low then 70µs high
|
||
*/
|
||
wait_level( LOW) ; /* HIGH -> LOW, ends, 80µs high, starts 50µs low */
|
||
int threshold = (MAX_RETRIES + retries) / 2 ;
|
||
</pre>
|
||
|
||
Based on the measured duration of 80µs, I can define a threshold at
|
||
40µs. Later to identify if a bit transmitted was a 0 (26~28µs) or a 1
|
||
(70µs), I will check if its duration is below or higher than the
|
||
threshold.
|
||
|
||
<pre>
|
||
wait_level( LOW) ; /* HIGH -> LOW, timed high ends, starts 50µs low */
|
||
/* Set bit based on measured HIGH duration */
|
||
if( retries < threshold) /* false == 26~28µs, true == 70µs */
|
||
v |= curbit ;
|
||
</pre>
|
||
|
||
To finalize <code>dht11_read()</code>, I declare retries before the first
|
||
<code>wait_level()</code>.
|
||
|
||
<pre>
|
||
/* DHT START: takes line, 80µs low then 80µs high */
|
||
int retries ; /* retry counter */
|
||
wait_level( LOW) ; /* HIGH -> LOW, starts 80µs low */
|
||
</pre>
|
||
|
||
There is still a bit of pseudocode left as I need to map
|
||
<code>dht11_input()</code>, <code>dht11_output()</code> and
|
||
<code>dht11_bread()</code> to actual GPIO peripheral, pin and low level
|
||
functions. I am using GPIOA pin 0.
|
||
|
||
<pre>
|
||
/* dht11.c -- DHT11 humidity and temperature sensor reading */
|
||
|
||
#include "dht11.h" /* implements DHT11 API */
|
||
|
||
#include "system.h" /* usleep(), gpioa_*() */
|
||
|
||
|
||
#define DIO 0
|
||
|
||
#define dht11_input() gpioa_input( DIO)
|
||
#define dht11_output() gpioa_output( DIO)
|
||
#define dht11_bread() gpioa_read( DIO)
|
||
|
||
/* 5 .. 95 %RH, -20 .. 60 °C */
|
||
unsigned char dht11_humid ; /* 5 .. 95 %RH */
|
||
signed char dht11_tempc ; /* -20 .. 60 °C */
|
||
unsigned char dht11_tempf ; /* .0 .. .9 °C */
|
||
|
||
void dht11_init( void) {
|
||
dht11_input() ;
|
||
}
|
||
</pre>
|
||
|
||
After adding the includes, global variables declarations and the
|
||
implementation of <code>dht11_init()</code>, I can build and test.
|
||
|
||
<h2>Build and basic test</h2>
|
||
|
||
I add the new composition to <b>Makefile</b>
|
||
|
||
<pre>SRCS = startup.txeie.c gpioa.c dht11main.c dht11.c</pre>
|
||
|
||
Build completes successfully
|
||
|
||
<pre>
|
||
$ make
|
||
f030f4.elf from startup.txeie.o gpioa.o dht11main.o dht11.o
|
||
text data bss dec hex filename
|
||
1877 0 24 1901 76d f030f4.elf
|
||
f030f4.hex
|
||
f030f4.bin
|
||
</pre>
|
||
|
||
Flashing the board and starting execution, I can see a new output every
|
||
two seconds.
|
||
<p>
|
||
<img alt="DHT11 output" src="img/31_output.png">
|
||
<p>
|
||
The humidity value seems off the mark. So I need to investigate what’s
|
||
the issue.
|
||
|
||
<h2>Checkpoint</h2>
|
||
|
||
I have implemented DHT11 protocol using polling and auto timing calibration. I
|
||
can read the values reported by the DHT11 sensor.
|
||
<p>
|
||
<a href="32_errata.html">Next</a>, I will investigate if the values read are
|
||
correct.
|
||
|
||
<hr>© 2020-2024 Renaud Fivet
|
||
</body>
|
||
</html>
|