Приклад виміру. |
Цей пост цілком може швидко застаріти -- майте на увазі.
Огляд та використання
Бібліотека:
- Дозволяє під'єднувати довільну кількість гігрометрів -- поки вистачить пінів.
- Як і попередня бібліотечка для 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 не знайшла
підозрілих місць в коді бібліотеки.
Але реалізація таки доволі потворна...
Параметри конкретних пристроїв задано константами:
Щоб збільшити загальність коду, створено функції, які, по типу пристрою, повертають величину відповідного його параметру, наприклад:
Для абстрагування конкретної апаратури створено ряд крихітних функцій:
Пін спочатку повинен працювати на вивід, а потім -- на ввід. Для переключення використано повну переініціалізацію -- на жаль, HAL не має відповідних функцій, безпосередня маніпуляція регістрами знизить портабельність бібліотеки, а швидкодія тут не критична. Але таке рішення все рівно видається мені відверто грубим...
Читання одного імпульсу просте, користується активним очікуванням:
Зверніть увагу на макрос STDIO_DEBUG, який вмикає вивід додаткової відладочної інформації.
З її допомогою здійснюється читання одного біта:
Нарешті, читання показів сенсора:
В рядках 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 | //! Мінімальний час між вимірами, мілісекунди #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; } } |
Ось і все. Сподіваюся, я її колись таки перепишу нормально, а поки --
Дякую за увагу!
Дяки-дяки!!! Дуже класний бложек))
ВідповістиВидалитиКоли буде наступна серія?)))
Дякую! :=)
ВидалитиНа жаль, останнім часом жахливо бракувало сил і часу щось писати. Зовсім скоро будуть нові. Правда, поки на зовсім іншу тему.
йой як круто! Дуже класний блог)
ВідповістиВидалитиКоли наступний пост?))
Дякую! :=)
ВидалитиНа жаль, останнім часом жахливо бракувало сил і часу щось писати. Зовсім скоро будуть нові. Правда, поки на зовсім іншу тему -- вимірювання часу виконання коду і все таке. :=)