понеділок, 27 лютого 2017 р.

Проста бібліотека для роботи з гігрометрами DHT11 і DHT22

Приклад виміру.
В попередньому пості ми розглянули трішки нетипові для цього блогу пристрої -- гігрометри. Наведу також приклад коду для роботи із ними. На жаль, код доволі примітивний -- зроблений на явних затримках. Значно елегантнішим було б зробити скінчений автомат на перериваннях. Однак, як завжди, часу жахливо забракло, усвідомив -- якщо буду чекати, поки допишу, то опублікую добре якщо в цьому році. То викладаю, як є -- в ролі "технологічної демонстрації", хай і неелегантної. 

Цей пост цілком може швидко застаріти -- майте на увазі.

Огляд  та використання

Бібліотека:
  • Дозволяє під'єднувати довільну кількість гігрометрів -- поки вистачить пінів.
  • Як і попередня бібліотечка для LCD5110, ця -- повністю динамічна, єдиний необхідний пристрою пін задається при ініціалізації. (Зі всіма плюсами і мінусами такого підходу. Обговорення див. у пості за посиланням вище, у розділі "Технічні подробиці").
  • Потребує мікросекундні таймери та можливість вимірювати мікросекундні інтервали. Вона користується кодом, запропонованим тут: "Мікросекундні затримки та відлік мікросекунд для STM32".
  • Сама бібліотека складається із двох файлів, dhtxx.h і dhtxx.c.
  • Поки -- чисто С-на бібліотека.
Скачати бібліотеку можна тут. Архів включає бібліотечку для роботи із мікросекундними інтервалами, але вважає, що в проекті є файл gpio.h, згенерований STM32CubeMX.


Дескриптором гігрометра є структура DHTxx_hygrometer_t. Ця структура містить:
  • порт і номер піна для взаємодії із пристроєм, 
  • його тип (DHT11, DHT22, і т.д.), 
  • час з моменту попереднього звертання у мілісекундах,
  • буфер.
Для її ініціалізації використовується спеціальна функція:

1
2
DHTxx_errors init_DHTxx(DHTxx_hygrometer_t* conf, DHTxx_types type,
                        uint16_t data_pin, GPIO_TypeDef *data_port);

де коди помилок та можливі типи пристроїв, у даний момент:

1
2
3
4
5
6
7
typedef enum {DHTXX_OK = 0, DHTXX_NO_CONN = 1, DHTXX_CS_ERROR = 2,
              DHTXX_TIMEOUT = 0x30,
              DHTXX_UNKNOWN_DEVICE = 0x40,
            }
DHTxx_errors;

typedef enum {DHT_Unknown = 0, DHT11 = 1, DHT22 = 2, DHT21 = 3} DHTxx_types;

Щоб прочитати дані із пристрою та помістити їх у буфер, що є в дескрипторі, служить функція:

1
DHTxx_errors read_raw_DHTxx(DHTxx_hygrometer_t* conf, int force);

Якщо з часу попереднього звертання до пристрою не минув мінімальний час між вимірами, ця функція не робить нічого, якщо force == 0. Виміри здійснюються примусово, якщо force != 0.

Отримані від пристрою дані можна проінтерпретувати за допомогою наступних чотирьох функцій:


1
2
3
4
5
int get_temperature_DHTxx(DHTxx_hygrometer_t* conf);
int get_humidity_DHTxx(DHTxx_hygrometer_t* conf);

inline double fget_temperature_DHTxx(DHTxx_hygrometer_t* conf);
inline double fget_humidity_DHTxx(DHTxx_hygrometer_t* conf);

Перші дві із них повертають результат у вигляді fixed-point, де десятковою є остання цифра, других дві -- повертають результат як double.

Крім того, зараз ще є дві публічних функції із очевидними назвами:


1
2
int ms_before_first_read(DHTxx_hygrometer_t* conf);
int ms_before_next_read(DHTxx_hygrometer_t* conf);

Використовувати все це можна якось так:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdlib.h> // Для abs()
#include "dhtxx.h"

....................................................

DHTxx_hygrometer_t hr11_1;
init_DHTxx(&hr11_1, DHT11, GPIO_PIN_3, GPIOA);
HAL_Delay(ms_before_first_read(&hr11_1));

while (1)
{
 res = read_raw_DHTxx(&hr11_1, false);
 if (res==DHTXX_OK)
 {
  int DHT_temp = get_temperature_DHTxx(&hr11_1);
  int DHT_press = get_humidity_DHTxx(&hr11_1);
  printf("DHT11_1: RH=%02d.%d%% T=%d.%dC\n",
   DHT_press/10, DHT_press%10, DHT_temp/10, abs(DHT_temp)%10);
 
 }
 if (res==DHTXX_CS_ERROR)
 {
  printf("DHT11_1: Checksum ERROR\n");
 }
 if (res==DHTXX_NO_CONN)
 {
  printf("DHT11_1: NO CONNECTION\n");
 }
 HAL_Delay(2000);
}

Ось і все.

Подробиці реалізації

Загальні зауваження:
  • Дрібні функції реалізовую як inline -- активно не люблю макроси в такій ролі, С++-ник, все ж,але і робити такі дрібні функції повноцінними -- соромно неефективно.
  • Всі службові функції зроблено статичними.
  • Бібліотека вважає, що тактування піна ввімкнено раніше, але, на загал, переконфігуровує його кожен раз самостійно.
PVS-studio не знайшла підозрілих місць в коді бібліотеки.

Але реалізація таки доволі потворна...

Параметри конкретних пристроїв задано константами:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//! Мінімальний час між вимірами, мілісекунди
#define DHT11_MIN_MEASURES_PAUSE 1000
#define DHT22_MIN_MEASURES_PAUSE 2000
#define DHT21_MIN_MEASURES_PAUSE 2000
#define DHT_Unknown_MIN_MEASURES_PAUSE DHT22_MIN_MEASURES_PAUSE

//! Час перед першим виміром після включення, мілісекунди
#define DHT11_FIRST_MEASURES_PAUSE 1000
#define DHT22_FIRST_MEASURES_PAUSE 2000
#define DHT21_FIRST_MEASURES_PAUSE 2000
#define DHT_Unknown_FIRST_MEASURES_PAUSE DHT22_FIRST_MEASURES_PAUSE

//! Час сигналу для початку виміру
#define DHT11_START_LENGTH 20000
#define DHT22_START_LENGTH 600
#define DHT21_START_LENGTH 600

Щоб збільшити загальність коду, створено функції, які, по типу пристрою, повертають величину відповідного його параметру, наприклад:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static inline int get_start_signal_length(DHTxx_hygrometer_t* conf)
{
 switch(conf->type)
 {
 case DHT11:
  return DHT11_START_LENGTH;
  break;
 case DHT22:
  return DHT22_START_LENGTH;
 case DHT21:
  return DHT21_START_LENGTH;
  break;
 default:
  //Unknown device. Cannot proceed without start impulse time.
  return -1;
 }
}

Для абстрагування конкретної апаратури створено ряд крихітних функцій:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static inline int read_pin(DHTxx_hygrometer_t* conf)
{
 return HAL_GPIO_ReadPin(conf->data_port, conf->data_pin);
}

static inline void write_pin(DHTxx_hygrometer_t* conf, int pinstate)
{
 HAL_GPIO_WritePin(conf->data_port, conf->data_pin, pinstate);
}

static inline uint32_t local_get_ms()
{
 return HAL_GetTick();
}

Пін спочатку повинен працювати на вивід, а потім -- на ввід. Для переключення використано повну переініціалізацію -- на жаль, HAL не має відповідних функцій, безпосередня маніпуляція регістрами знизить портабельність бібліотеки, а швидкодія тут не критична. Але таке рішення все рівно видається мені відверто грубим...


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline void set_as_open_drain_output(DHTxx_hygrometer_t* conf)
{
 //! Мені не подобається цей код! Але готових функцій для зміни
 //! ролі піна на льоту в HAL немає, використовувати два піни --
 //! ще більше збочення, а лізти в регістри неохота -- тоді буде
 //! купа ifdef, навіть для STM32F3 i STM32F1
 GPIO_InitTypeDef GPIO_InitStructOut;
 GPIO_InitStructOut.Pin  = conf->data_pin;
 GPIO_InitStructOut.Mode = GPIO_MODE_OUTPUT_OD;
 GPIO_InitStructOut.Pull = GPIO_NOPULL;
 GPIO_InitStructOut.Speed = GPIO_SPEED_FREQ_HIGH;
 HAL_GPIO_Init(conf->data_port, &GPIO_InitStructOut);
}

static inline void set_as_input(DHTxx_hygrometer_t* conf)
{
 //! Див. комент в тілі set_open_drain_output
 GPIO_InitTypeDef GPIO_InitStructIn;
 GPIO_InitStructIn.Pin  = conf->data_pin;
 GPIO_InitStructIn.Mode = GPIO_MODE_INPUT;
 GPIO_InitStructIn.Pull = GPIO_NOPULL;
 GPIO_InitStructIn.Speed = GPIO_SPEED_FREQ_HIGH;
 HAL_GPIO_Init(conf->data_port, &GPIO_InitStructIn);
}

Читання одного імпульсу просте, користується активним очікуванням:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static int read_pulse(DHTxx_hygrometer_t* conf){
 us_reset_counter();

  while(read_pin(conf) == GPIO_PIN_RESET &&
    get_us() < global_timeout_us ){}
 uint16_t before = get_us();
 if(before > global_timeout_us)
 {
#ifdef STDIO_DEBUG
  printf("before: %d\n", before);
#endif
  return -1;
 }
 while(read_pin(conf) == GPIO_PIN_SET &&
    get_us() < global_timeout_us ){}
 uint16_t after = get_us();
 if(after > global_timeout_us)
 {
#ifdef STDIO_DEBUG
  printf("before: %d\n", before);
  printf("after: %d\n", after);
#endif
  return -1;
 }
 return after-before;
}

Зверніть увагу на макрос STDIO_DEBUG, який вмикає вивід додаткової відладочної інформації.

З її допомогою здійснюється читання одного біта:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static int8_t read_bit(DHTxx_hygrometer_t* conf, int *pulse_length)
{
 int res = read_pulse(conf);
 if(pulse_length)
  *pulse_length = res;

 if(res == -1)
  return -1;
 if( res > 0 && res < ZERO_LENGTH_DHT11 )
  return 0;
 else
  return 1;
}

Нарешті, читання показів сенсора:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
DHTxx_errors read_raw_DHTxx(DHTxx_hygrometer_t* conf, int force)
{
 if(conf->last_read_time - local_get_ms() < get_min_measures_pause(conf)
    && !force)
  return DHTXX_OK;

 int start_time = get_start_signal_length(conf);
 if(start_time < 0 )
  return DHTXX_UNKNOWN_DEVICE;

 set_as_open_drain_output(conf);
 write_pin(conf, SET);   // Get ready
 udelay(250);            // More or less arbitrary
 write_pin(conf, RESET); // Put to zero -- command DHT to start
 udelay(start_time);
 write_pin(conf, SET);   // Release line part one -- it will be pulled-up
 
 set_as_input(conf);

 uint16_t dt[42];
 int8_t res;
 //! Wait for line to go up -- not a bit
 res = read_bit(conf, 0);
 if(res == -1)
  return DHTXX_NO_CONN;
 //! Not really a bit -- just acknowledge
 res = read_bit(conf, 0);
 if(res == -1)
  return DHTXX_NO_CONN;
 for(int i = 0 ; i<40; i++){
         res =  read_bit(conf, 0);
         if(res == -1)
         return DHTXX_NO_CONN;
  dt[i] = res;
 }

 //convert data
  for(int i = 0; i<buf_size(conf); i++){
   conf->buf[i] = 0;
   for(int j = 0; j<8; j++){
    conf->buf[i] <<= 1;
    conf->buf[i] |= dt[i*8+j];
   }
  }

 //calculate checksum
 uint8_t check_sum = 0;
 for(int i = 0; i<4; i++){
  check_sum += *(conf->buf+i);
 }

 if (conf->buf[4] != check_sum )
  return DHTXX_CS_ERROR;

 conf->last_read_time = local_get_ms();
 return DHTXX_OK;
}


В рядках 3-5 перевіряється, чи не зарано ми хочемо міряти.

В рядках 11-16 подається команда запуску DHTxx. Далі, в рядку 18 пін переходить в режим читання.

Після того, як мікроконтролер відпускає лінію, вона чомусь зростає дуже повільно, а, найгірше -- час зростання дуже варіює, спостерігав час від 8 до 35 мкс. Тому, замість ставити довгі затримки -- це можливо, бо імпульс підтвердження від гігрометра триває аж 80 мкс, але відверто негарно, просто вважаю, що читається такий псевдобіт -- рядок 23. Наступним псевдобітом читаємо імпульс-підтвердження від пристрою -- рядок 27.

Нарешті, цикл у рядках 30-35 читає 40 біт даних. Якщо біт не прочитався -- не було правильної форми імпульсу, повертається код помилки DHTXX_NO_CONN -- "No Connection".

Після цього, в рядках 38-44, ці біти пакуються у байти, а в рядках 47-53 перевіряється контрольна сума. Якщо вона правильна -- повертається DHTXX_OK -- читання даних успішне, інакше код помилки обчислення контрольної суми: DHTXX_CS_ERROR.

Залишається проінтерпретувати отримані байти:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int get_temperature_DHTxx(DHTxx_hygrometer_t* conf)
{
 uint16_t data;
 switch(conf->type)
 {
 case DHT11:
  return 10 * conf->buf[2];
  break;
 case DHT22:
 case DHT21:
  // Clear sign for shift
  data = ( (conf->buf[2] & 0x7F) << 8 ) + conf->buf[3];
  return (conf->buf[2] & 0x80) ? -data : data;
  break;
 default:
  return -1;
 }
}

int get_humidity_DHTxx(DHTxx_hygrometer_t* conf)
{
 switch(conf->type)
 {
 case DHT11:
  return 10 * conf->buf[0];
  break;
 case DHT22:
 case DHT21:
  return (conf->buf[0] << 8) + conf->buf[1];
  break;
 default:
  return -1;
 }
}

Ось і все. Сподіваюся, я її колись таки перепишу нормально, а поки --

Дякую за увагу!

4 коментарі:

  1. Дяки-дяки!!! Дуже класний бложек))
    Коли буде наступна серія?)))

    ВідповістиВидалити
    Відповіді
    1. Дякую! :=)

      На жаль, останнім часом жахливо бракувало сил і часу щось писати. Зовсім скоро будуть нові. Правда, поки на зовсім іншу тему.

      Видалити
  2. йой як круто! Дуже класний блог)
    Коли наступний пост?))

    ВідповістиВидалити
    Відповіді
    1. Дякую! :=)

      На жаль, останнім часом жахливо бракувало сил і часу щось писати. Зовсім скоро будуть нові. Правда, поки на зовсім іншу тему -- вимірювання часу виконання коду і все таке. :=)

      Видалити