DHT11 & AVR & USB - влажность под контролем

 

Отступление (в качестве вступления)

Кажется, я разучился писать. Не вообще, а в частности, здесь, в блоге. 🙂
Наши сайты уже давно и самоотверженно поддерживает Альбина, я же занимаюсь чистейшим удовольствием - делаю железки и пишу программы. И ничего о них никому из широкой публики не рассказываю. ))

Но вот, и в моём раю разразился гром,- во время отладки очередной маленькой железки (она будет представлена на сайте в открытом виде).
Нечаянно по питанию подал ей напряжение больше 30В, её прошибло и, через программатор, повышенное напряжение оказалось на USB моего ноута. В итоге, моментальной смертью умер винчестер со всеми моими трудами. Особо крутой облом оказался, когда на SVNе не обнаружилось комитов за последние полтора месяца (видимо, что-то сломалось после очередного обновления Visual SVN сервера или Tortoise SVN клиента). Сейчас вот жду, пока придет такой же винтик от китайцев, чтобы перекинуть его электронику на трупика... и, благодаря освободившемуся времени, занимаюсь общественно полезными работами.

Моя жена является умеренной последовательницей доктора Комаровского, и прислушивается ко многим его советам. В своих выступлениях он частенько делает акцент на необходимости поддержания довольно высокой относительной влажности (около 50%) и невысокой температуры, чтобы избегать респираторных заболеваний. С температурой всё просто и понятно, а вот с влажностью... Гигрометры и психрометры в быту есть разве что у очень больших ценителей такой специфической измерительной техники, к коим мы не относились. Впрочем, обратив пристальное внимание на прилавки магазинов бытовой техники и всякой китайской электронной мелочевки, я быстро выяснил, что измерителей влажности и температуры - хоть пруд пруди, и мой интерес к этой задаче моментально исчез. Было сформулировано предложение просто купить готовый гаджет и не изобретать велосипед.
На это женой был дан решительный отпор, мол, стыдно  мне должно быть: что я за электронщик такой, что буду семейный бюджет разматывать, вместо посидеть пару выходных дней и сделать доброе дело - надо ведь и про людей не забывать, чтобы на сайте можно было рассказать о доступной для повторения в домашних условиях и полезной штуковине! Тем более, что у нас уже давно завалялся когда-то купленный за компанию с прочей мелочевкой китайский цифровой датчик влажности и температуры DHT11. Я согласился, и пообещал сделать. После этого разговора прошло года два (жена уточнила - всего лишь один)... Но, однозначно, меньше трех. ))

Сбылось! Столь долгожданный измеритель влажности и температуры собран, работает, и поверг мою жену в состояние полнейшего счастья (секунд на 30 - это точно). О нем и будет сегодняшний рассказ.

Наверное, вы уже успели заметить немного фотографий (они кликабельны). На них - общий вид сверху, вид со стороны DHT11, вид с нижней стороны платы и натюрморты с мультиметром в режиме измерения коэффициента заполнения по дискретным выходам H и T.

На индикаторе показания влажности и температуры сменяются через настраиваемый промежуток времени (от 1 до 10 секунд). Первый символ во время отображения температуры - "t.", для влажности - "h.". Точка у третьего символа индицирует состояние дискретного выхода LEVEL, предназначенного для управления внешним оборудованием.

Технические характеристики

Диапазон измерения температуры: от 0 до +50 С с погрешностью +/-2 С.
Диапазон измерения относительной влажности: от 20 до 90 % с погрешностью +/-5 %.
Эти параметры определяются возможностями датчика DHT11. Если установить вместо него датчик DHT22 (они взаимозаменяемы), точность результатов будет существенно лучше (за цену примерно в 3 раза выше). По нашим личным впечатлениям, точноcти DHT11 для комнатного применения предостаточно.
Питание - от USB (можно подключить к компьютеру или любой зарядке для телефонов и планшетов с USB гнездом), или от любого другого источника питания постоянного тока с выходным напряжением от 5 до 20 В.
Индикация - светодиодный семисегментный трехсимвольный индикатор, отображение значений влажности и температуры поочередное.
Выходы для систем автоматики и телеметрии:
-два канала (для влажности и температуры) с широтно-модулированным сигналом прямоугольной формы частотой 10 Гц и коэффициентом заполнения от 0 до 100%. Значение коэффициента заполнения соответствует числовому значению измеренной величины;
-дискретный сигнальный выход для управления исполнительными устройствами, логика и параметры срабатывания по температуре и влажности настраиваются через прилагаемую к проекту утилиту.
Интерфейс к ПК - USB 1.0, HID устройство. Драйвер для Windows не требуется, работает в Windows XP и более новых. Позволяет считывать текущие результаты измерений и осуществлять настройку устройства.
Настройки:
- калибровка смещения температуры;
- время удержания показаний на индикаторе;
- пороги срабатывания дискретного сигнального выхода LEVEL.

Схема измерителя влажности и температуры

Схема предельно простая (по картинкам нужно кликать, чтобы посмотреть в более крупном размере). Содержит микроконтроллер ATMEGA48, светодиодный семисегментный индикатор, датчик влажности и температуры DHT11, три транзисторных дискретных выхода, линейный стабилизатор питания на 3.3В, клеммники для линий автоматики и разъем ЮСБ кабеля. В конце статьи есть ссылка на PDF со схемой.
Почему для выходов влажности и температуры для систем автоматики применен широтно-модулированный дискретный сигнал, а не аналоговый? Причина проста: выход по напряжению очень подвержен воздействию помех, токовый выход - потребляет относительно большой ток (до 20 мА) и превращает в тепло. Здесь же дополнительное тепло крайне нежелательно, поскольку будет искажать показания DHT11. Преобразовать ШИМ сигнал в аналоговый, при необходимости, можно с помощью интегрирующей цепочки. А для мало-мальски нормального телеметрического контроллера с дискретными входами ШИМ сигнал и так идеально подходит.

П/П измерителя влажности - вид сверху П/П измерителя влажности - вид снизу

Печатная плата (разработчик - Альбина) двусторонняя, под ЛУТ вряд ли особо подойдет из-за маленьких переходных отверстий. Размер - 45 х45 мм. Её вид сверху и снизу - на картинках, гербер файлы для заказа в производство - в прилагаемом к статье архиве (ищите ссылку в самом конце).

После монтажа, наверняка, захочется помыть плату от канифоли растворителем. Делать это следует очень осторожно, чтобы растворитель и грязь не попали внутрь датчика DHT11, поскольку это, с очень большой вероятностью, повредит его.

Встраиваемое ПО (прошивка)

Примерно половину флеша занимает программная реализация USB интерфейса, для которой задействована замечательная библиотека V-USB от Objective Development. Она же налагает серьезные ограничения на использование прерываний и структуру кода. Чтобы всё было хорошо, нельзя использовать прерывания с длинным кодом внутри, да и обрабатывать состояния этой библиотеки нужно предельно быстро. Иначе ваше USB устройство будет глючить и периодически отваливаться. Оставшаяся половина флеш-памяти занята чтением показаний влажности и температуры из DHT11, отображением их на индикаторе, формированием дискретных выходных сигналов и обработкой команд от компьютера. 

Прошивка написана и собирается под AtmelStudio 7 .  Рекомендую использовать именно её для написания прошивок под атмеловские контроллеры - это лучшая среда разработки из всех, мною виденных, предоставляемая бесплатно производителем микроконтроллеров . По сути, это Microsoft Visual Studio (великолепная среда для разработки ПО) с интегрированным (бесплатно!) Visual Assist и GCC тулчэйном. Последние версии С++ компилятора для AVR просто восхитительны - они генерят чудесный, замечательно оптимизированный выхлоп, даже при относительно расслабленном стиле написания С/С++ кода. После череды проектов для STM32, где только настройка периферии под новую железку съедает рабочий день и десяток килобайт флеша, написать прошивку для AVR, да еще в Atmel Sudio - это 100% удовольствие, лучик света из милого 8-битного прошлого. 🙂 Пользователи Arduino в моем миропонимании не укладываются в принципе. Как можно для подобных простейших штук еще какие-то программно-аппаратные костыли по собственной воле юзать? Ну да ладно...

Ссылку на исходники ищите в конце статьи, здесь же рассмотрим только самое интересное - код для чтения показаний DHT11. Найти примеры кода для чтения DHT вовсе несложно, но у нас есть особое требование - обработка обмена с DHT должна быть фоновой, поскольку абсолютный приоритет процессорного времени принадлежит коду библиотеки V-USB. То есть мы не можем написать что-то вроде result = DHT_Read(), подождать сколько надо до окончания обмена, и получить результат. Если мы так поступим, в один прекрасный момент, который наступит очень скоро, Windows нас не простит и скажет, что наше USB устройство не работает. Да и вообще, синхронный код работы с внешним оборудованием - это совершенно неподходящее решение для систем, реализующих несколько функций, выполняющихся параллельно (типа "многозадачных").

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// ======================= DHT DATA EXCHANGE START ======================
 
void inline Int1On(){
  if(EIFR & (1 << INTF1)) EIFR |= (1 << INTF1); // reset interrupt flag
  EIMSK |= (1 << INT1); // turn interrupt ON
}
 
void inline Int1Off(){
  EIMSK &= ~(1 << INT1);// deny this interrupt
  if(EIFR & (1 << INTF1)) EIFR |= (1 << INTF1); // reset interrupt flag
}
 
enum eDhtState{e_none, e_start, e_waitfordata, e_fail_timeout, e_fail_crc, e_finished};
uchar Tcnt0Val(0); // value stored in the interrupt
class cDHT{
  cTimeout ExchTimeout;
  uchar raw_data[5]; // 40 bit of DHT answer
  uchar bit_counter;
  uchar b_was_handshake;
 
 public:
  signed char Toffset; // user calibration value
  uchar H, T; // results
  eDhtState CurState;
  cDHT():ExchTimeout(20), Toffset(0), H(0), T(0), CurState(e_none){}
 
  // call it to update H and T values
  void StartRead(){
    bit_counter = 0;
    b_was_handshake = 0;
    // clear data buffer
    for(uchar i = 0; i < sizeof raw_data; i++) raw_data[i] = 0;
    // prepare timeouter
    ExchTimeout.Reset();
    //set IO to the output
    DDRD |= 1 << DHT_DATA_PD;
    // set low level
    PORTD &= ~(1 << DHT_DATA_PD);
    CurState = e_start;
  }
 
  void OnInt1(){
    if(PIND & (1 << DHT_DATA_PD)){
      // high level - nothing to do
      return;
    }
    else{ // low level
      // it was high level
      uchar cur_t = Tcnt0Val >> 1; // TCNT 0 resolution is 0.5 us
      // wait for the handshake (first 80 us high level)
      if(!b_was_handshake){
        if(cur_t > 70) b_was_handshake = 1;
        return; // skip all until handshake
      }
 
      // analyze current duration
      // 26-28 us -> 0
      // 70 us -> 1
      if(cur_t < 80){ // skip all durations longer than 80 us
        uchar bit = cur_t < 40 ? 0 : 1;
        if(bit && bit_counter < 40){ // write only 1, because buffer was prepared with 0
          uchar byte_ind = bit_counter >> 3;
          uchar bit_ind = 7 - (bit_counter - (byte_ind << 3)); // high bit is first
          // write next bit into the raw result data
          raw_data[byte_ind] |= 1 << bit_ind;
        }
        bit_counter++;
      }
    }
  }
 
  void Handler(){
    if(Tcnt0Val){ // if this val != 0 it means that INT1 interrupt happened
      OnInt1();
      Tcnt0Val = 0;
      return;
    }
    if(CurState == e_start && ExchTimeout()){
      // 20 ms low level on IO finished
      ExchTimeout.Reset();
      //set IO pin to input
      DDRD &= ~(1 << DHT_DATA_PD);
      CurState = e_waitfordata;
      // reset microseconds timer
      TCNT0 = 0;
      // turn on interrupt processing
      Int1On();
    }
    else if(CurState == e_waitfordata && ExchTimeout()){
      // turn off INT1
      Int1Off();
      if(bit_counter < 40){
        // exchange failed with timeout
        CurState = e_fail_timeout;
      }
      else{
         // we got a result
         // check CRC
         uchar crc = raw_data[0] + raw_data[1] + raw_data[2] + raw_data[3];
         if(crc == raw_data[4]){ // CRC ok
           H = raw_data[0];
           T = raw_data[2] + Toffset;
           CurState = e_finished;
         }
         else{
           // CRC error
           CurState = e_fail_crc;
         }
      }
    }
  }
 
}DHT;
 
ISR(INT1_vect)//INT 1 for DHT data IO pin PD3
{
  // make hear as little work as we can, because long interrupt processing will destroy USB normal work
  Tcnt0Val = TCNT0;
  TCNT0 = 0;
}
 
// ======================= DHT DATA EXCHANGE END ======================
// ======================= DHT DATA EXCHANGE START ======================

void inline Int1On(){
  if(EIFR & (1 << INTF1)) EIFR |= (1 << INTF1); // reset interrupt flag
  EIMSK |= (1 << INT1); // turn interrupt ON
}

void inline Int1Off(){
  EIMSK &= ~(1 << INT1);// deny this interrupt
  if(EIFR & (1 << INTF1)) EIFR |= (1 << INTF1); // reset interrupt flag
}

enum eDhtState{e_none, e_start, e_waitfordata, e_fail_timeout, e_fail_crc, e_finished};
uchar Tcnt0Val(0); // value stored in the interrupt
class cDHT{
  cTimeout ExchTimeout;
  uchar raw_data[5]; // 40 bit of DHT answer
  uchar bit_counter;
  uchar b_was_handshake;
 
 public:
  signed char Toffset; // user calibration value
  uchar H, T; // results
  eDhtState CurState;
  cDHT():ExchTimeout(20), Toffset(0), H(0), T(0), CurState(e_none){}
 
  // call it to update H and T values
  void StartRead(){
    bit_counter = 0;
    b_was_handshake = 0;
    // clear data buffer
    for(uchar i = 0; i < sizeof raw_data; i++) raw_data[i] = 0;
    // prepare timeouter
    ExchTimeout.Reset();
    //set IO to the output
    DDRD |= 1 << DHT_DATA_PD;
    // set low level
    PORTD &= ~(1 << DHT_DATA_PD);
    CurState = e_start;
  }
 
  void OnInt1(){
    if(PIND & (1 << DHT_DATA_PD)){
      // high level - nothing to do
      return;
    }
    else{ // low level
      // it was high level
      uchar cur_t = Tcnt0Val >> 1; // TCNT 0 resolution is 0.5 us
      // wait for the handshake (first 80 us high level)
      if(!b_was_handshake){
        if(cur_t > 70) b_was_handshake = 1;
        return; // skip all until handshake
      }
 
      // analyze current duration
      // 26-28 us -> 0
      // 70 us -> 1
      if(cur_t < 80){ // skip all durations longer than 80 us
        uchar bit = cur_t < 40 ? 0 : 1;
        if(bit && bit_counter < 40){ // write only 1, because buffer was prepared with 0
          uchar byte_ind = bit_counter >> 3;
          uchar bit_ind = 7 - (bit_counter - (byte_ind << 3)); // high bit is first
          // write next bit into the raw result data
          raw_data[byte_ind] |= 1 << bit_ind;
        }
        bit_counter++;
      }
    }
  }
 
  void Handler(){
    if(Tcnt0Val){ // if this val != 0 it means that INT1 interrupt happened
      OnInt1();
      Tcnt0Val = 0;
      return;
    }
    if(CurState == e_start && ExchTimeout()){
      // 20 ms low level on IO finished
      ExchTimeout.Reset();
      //set IO pin to input
      DDRD &= ~(1 << DHT_DATA_PD);
      CurState = e_waitfordata;
      // reset microseconds timer
      TCNT0 = 0;
      // turn on interrupt processing
      Int1On();
    }
    else if(CurState == e_waitfordata && ExchTimeout()){
      // turn off INT1
      Int1Off();
      if(bit_counter < 40){
        // exchange failed with timeout
        CurState = e_fail_timeout;
      }
      else{
         // we got a result
         // check CRC
         uchar crc = raw_data[0] + raw_data[1] + raw_data[2] + raw_data[3];
         if(crc == raw_data[4]){ // CRC ok
           H = raw_data[0];
           T = raw_data[2] + Toffset;
           CurState = e_finished;
         }
         else{
           // CRC error
           CurState = e_fail_crc;
         }
      }
    }
  }

}DHT;

ISR(INT1_vect)//INT 1 for DHT data IO pin PD3
{
  // make hear as little work as we can, because long interrupt processing will destroy USB normal work
  Tcnt0Val = TCNT0;
  TCNT0 = 0;
}

// ======================= DHT DATA EXCHANGE END ======================

Исключительно для удобства представления кода (поскольку инкапсуляция, наследование и полиморфизм здесь не очень актуальны) обработка датчика оформлена в виде класса cDHT (огромнейшее спасибо разработчикам компилятора за человеческий С++), он же имеет единственный статический экземпляр DHT, инстанциированный сразу же после кода класса.
Для запуска чтения данных из датчика DHT11 нужно периодически вызывать метод  StartRead() из главного цикла программы, что и происходит каждые полсекунды.
Вся обработка сосредоточена в методе Handler(), который вызывается в главном цикле программы настолько часто, насколько это возможно. В случае, если было прерывание по линии данных датчика (это прерывание по любому фронту INT1), то метод Handler вызовет метод OnInt1(), в котором происходит декодирование длительностей импульсов от датчика в биты данных. Если устройство ответило за время, не превышающее 20 мс, количество бит не менее 40 и сходится контрольная сумма, то происходит обновление свойств T и H, представляющих измеренные значения температуры и относительной влажности. Узнать, чем сейчас занимается объект и с каким успехом, позволяет свойство CurState. Для температуры предусмотрена корректировка значения путем сложения со свойством Toffset, значение которого сохраняется в EEPROM и устанавливается с помощью команды от компьютера. Это позволяет откалибровать наш "показометр" по температуре по эталонному термометру до +/- 1 градуса. Калибровки влажности нету, в виду отсутствия в домашнем хозяйстве требуемых измерительных приборов. 🙂
Обработчик прерывания ISR(INT1_vect) состоит всего из двух строчек - мы запоминаем значение таймера TCNT0 и сбрасываем его в 0. TCNT0 настроен на непрерывный счет с тактированием частотой 2МГц, что дает нам разрешение в 0.5 мкс. Он является для нас опорным таймером для измерения длительностей импульсов от датчика влажности и температуры.

Далее мы просто читаем значения свойств DHT.T и DHT.H и применяем по своему усмотрению, а именно: выводим на светодиодный дисплей, превращаем в заполнение на дискретных выходах T и H, сравниваем с уставками для управления выходом LEVEL, сообщаем компьютеру, когда он об этом попросит.

Прошивать микроконтроллер - как обычно. Если раньше этого делать не приходилось, советую собрать программатор ucGoZilla - работает очень достойно, эмулирует STK500.

Не забудьте запрограммировать флаги: использовать внешний кварц с частотой выше 8МГц; не делить частоту на 8; выход тактовой частоты не нужен. Остальные - по вашему вкусу и опыту. Главное -не отключайте программирование по SPI! 😉

Программа для настройки

Для настройки и проверки нашего устройства я написал небольшую утилиту DHT11 Monitor. Она автоматически обнаруживает подключение HID устройства DHT11 Sens, позволяет на экране компьютера увидеть текущие значения температуры и влажности, а также настроить всё, что в этом устройстве вообще настраивается. Утилиту предлагаю только в готовом к употреблению виде, без исходников, но использовать её можете как вам угодно. Просто извлеките из архива и запустите исполняемый файл DHT_Mon.exe.

Интересная (на перспективу) возможность - дискретный выход LEVEL для автоматического управления внешними устройствами. Его логика работы определяется четырьмя числовыми значениями: температура включения, температура выключения, влажность включения, влажность выключения. Еще есть флаг, позволяющий инвертировать выходной сигнал. Условия по влажности и температуре должны выполняться одновременно для активации выхода, при чем, возможен контроль внешнего устройства как по сценарию "обогреватель", так и "холодильник"; как "увлажнитель", так и "осушитель". Например, если температура включения ниже температуры выключения, то это - нагреватель. Если наоборот - "холодильник". Если температура включения равна температуре выключения (то же самое с влажностью) - выход LEVEL никогда не будет активирован. По сути, эти пары значений описывают параметры петли гистерезиса, которая устраивает нас в нашей системе автоматического регулирования.

Ссылки на скачивание

Принципиальная схема измерителя влажности и температуры
Гербер файлы для производства печатной платы
Прошивка с исходным кодом для МК ATMega48
Утилита настройки измерителя DHT11 Monitor

Библиотека V-USB от Objective Development

 

2 комментария к записи DHT11 & AVR & USB - влажность под контролем

  • Константин пишет

    Классная и лаконичная вещь! Еще бы добавить в схему возможность индикации отрицательных температур, и было бы вообще замечтательно) Даже лишний разряд восьмисегментника не нужен, только повесить на одну из свободных ног дополнительный светодиод, который будет индицировать знак "минус". Именно то, что мне нужно для гаража. Попробую покурить исходник на предмет доработки. Одна проблема, я с Сями на Вы, только asm со словарём разумею)

    • Альбина пишет

      Добрый день, Константин. Рады, что пригодилась схемка. Индикации отрицательных температур нет по простой причине - датчик DHT11 их не измеряет. Если нужны отрицательные температуры, можно использовать датчик DHT22 (он дороже). Изначально прибор задумывался исключительно как "комнатный", поэтому такие компоненты и использовались. Покопайтесь в исходниках, программа написана максимально просто. А Си, как по мне, проще asm. Но, дело опыта, конечно :-). Удачи Вам! Обращайтесь!

Добавить комментарий для Альбина Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *