半熟前端

軟體工程師 / 台灣人 / 在前端的路上一邊探索其他領域的可能性

IoT

用 Arduino 與 ESP32 實作空氣品質監測應用(1)- 感測器介紹

前言

最近真的很喜歡各種 IoT 的應用,買了很多 Arduino 跟各種感測器,光是手上有的 Arduino 就有 Arduino Uno、Arduino Mega2560、Arduino nano * 3。想說有空就來玩玩,看能不能做出一些有趣的應用,順便趁著這個機會複習一下高中的電子學還有各種硬體的運作。

剛好公司舉辦了社內黑客松,對於主題沒有太多限制,就用了這段期間實作了之前就很想做的應用,「空氣品質監測器」。雖說是空氣品質,不過實作出來後也只有二氧化碳濃度跟溫濕度監測,如果有感測器的話要另外加上也不是太大的問題就是了。

會有這個想法主要是因為二氧化碳的濃度真的會影響人的思考跟產能,相信大家都有關在會議室裡結果突然就覺得頭昏、思緒混亂的情形吧。有很大的可能性就是因為二氧化碳濃度過高導致頭昏。如果換氣不佳加上密閉空間,二氧化碳濃度上升速度非常快,幾分鐘內就到 2000 多 ppm 了。

除了實作本身以外,因為我實在不喜歡套件套一套然後就完成的感覺,所以我會盡量涉略多一點細節,才不會有種「啊套件套完了,但發生什麼事都不知道」的空虛感。(雖然實作還是用套件,但至少用得比較心安理得XD)

本系列文會涵蓋以下的主題:

  1. 感測器介紹篇 - DHT11 與 MH-Z14A
  2. 資料溝通篇 - UART
  3. Arduino 踩雷篇:會講解在實作時遇到的問題,很多時候不是雷,是對 Arduino 跟硬體不熟悉
  4. WiFi 篇:為了省下 Debug 的時間,我額外購買了 ESP32 開發版,本身已經含有 WiFi 跟藍芽功能
  5. MQTT 篇:為了把資料傳給其他設備,使用了 MQTT 這個輕薄短小的通訊協定
  6. Grafana / Web 篇:資料存在資料庫當然要用炫炮的方式顯示出來啊!這裡使用了 Grafana + Promethus 以及 Svelte 來顯示資料。

架構

為了方便說明,架構上大概長得像是這樣:

Infrastructure

  • Arduino Uno 傳送指令給 CO2 感測器,CO2 感測器回傳資料給 Arduino,兩者透過 UART 溝通

  • Arduino Uno 傳送 CO2 資料給 ESP32(也是用 UART)

  • 透過 DHT11 得到溫濕度資料,再傳送給 ESP32

  • ESP32 將溫濕度資料以及 CO2 資料透過 WiFi 傳給 MQTT Borker

  • 訂閱事件的應用有兩個:分析伺服器以及 App 伺服器。

    • 分析伺服器負責將資料傳給 Promethus,再由 Grafana 呈現資料
    • App 伺服器接收資料後存到資料庫,並提供 API 給外部使用
  • 如果二氧化碳超過一定濃度,則送出一個 slack notification

整體上大致長這樣,接下來回答一些可能出現的問題:

1. 為什麼要額外用 Arduino Uno?這樣看下來 ESP32 一塊版子就可以解決?

當初的想法是 ESP32 就當作中介者的感覺,讓整體的架構變得乾淨一點,不過實作完成後似乎沒有必要,唯一要注意的一點是有些 Arduino 內建的 Library 在 ESP32 是沒辦法使用的,像是這次實作用到的 SoftwareSerial(一個可以讓你把 Digital Pin 拿來當成 tx, rx 接腳用的 Library,在之後的文章會續談),沒有這個 Library 的話我們只能用內建的 HardwareSerial,最多就只能進行一個 UART 溝通。

另外一點就是看起來比較潮嘛,沒有用到 Arduino 就不酷了。

2. DHT11 為什麼要接在 ESP32 而不是 Arduino Uno?

前面有提到如果用在 Arduino Uno 上,就要另外用 UART 的方式將資料送去 ESP32,為了省麻煩就直接在 ESP32 做了,剛好 Library 本身也有支援 ESP32。要作在 Arduino Uno 上當然也是可以。

3. 為什麼選用 MQTT?

這個通訊協定輕薄短小,相較於其他協定比較沒有那麼多 overhead,所以很適合 IoT 這種 CPU 跑不快、記憶體不多的場景,當然可用性就沒有那麼好。

4. 為什麼資料庫要開兩個?

眼尖的讀者應該會發現分析用的資料庫跟 App 的資料庫是另外開的,第一是我比較不熟 Prometheus,如果自己下 query 拿資料可能比較會花上比較久時間;另外一點就是 Postgres 的 SQL 語法跟 node.js 有比較完整的整合,寫起來也比較方便,之後或許會在玩玩 Prometheus 的 query!

成果

在網路上比較有名的空氣監測的產品似乎是 AWAIR,除了精美的 UI、檢測溫濕度和二氧化碳之外,還可以檢測化學物質跟 PM2.5。不過價格不菲,一個要價 149149 美金,相當於4,470 新台幣。相比起來這次的實作總共為:

  • DHT11 溫濕度感測器:約 60 元
  • MH-Z14A CO2 感測器:約 800 元
  • ESP32 開發版:約 200 元
  • Arduino Uno:約 800 元(原廠,若用 nano 代替會便宜更多)

總計:$1,860 元

當然外觀也是很簡陋啦 XD,不過稍微改一下程式碼應該也可以整合很多應用,例如搭配 Google Home 或是寫個簡單的手機 App 等等。

電路

稍微整理一下勉強可以塞進盒子吧

Grafana

Web 界面

UI 很簡陋,這邊只是作一個示範。

gauge

感測器介紹:DHT11 與 MH-Z14A

MH-Z14A CO2 感測器

為了拿到二氧化碳的濃度,首先我們需要感測器。在網路上找了一下,要得到二氧化碳的濃度,可以用一個叫做 NDIR(Non Dispersive Infra Red,非分散式紅外線)的方法感測。

它的原理是利用氣體本身會吸收特定波長的紅外線的特性,進而計算氣體的濃度。像是二氧化碳的吸收紅外線波長的範圍就是 4.26μm,所以就可以針對這段波長作計算得出濃度。計算的公式不是我們這次要關注的事情先跳過,值得一提的是這個感測器的使用方式。

在網路上找了一下發現中文的資源很少,在這邊大概說明一下,可以搭配 Datasheet 一起來看,網路上的 datasheet 來源很多,這個是目前看到現在最詳細的。

從這份 datasheet 當中可以發現幾件事情:

  1. MH-Z14A 本身透過 UART 進行通信,傳遞指令碼後會得到回應。指令除了感測二氧化碳濃度外,還有歸零調整、調整感測範圍等等。

    指令碼 功能(原文 / 中譯) 備註
    0x86 Gas concentration / 感測二氧化碳濃度
    0x87 Calibrate zero point value / 歸零校正 這個感測器有三個歸零校正方法,傳指令是其中一個
    0x99 Calibrate span point value / 修改二氧化碳偵測範圍
    0x79 Start/stop auto-calibration function of zero point value / 使用(或停止)自動校正功能 在出廠時,這個感測器預設會開啟自動校正
  2. MH-Z14A 輸出支援三種類型(超方便 XD),分別是 UARTPWM 以及 Analog。可以自行選擇想要的輸出來獲得二氧化碳濃度,這次的實作選擇了 UART,因為方便並且不需要再做額外的數字轉換。

DHT11 感測器

DHT11 是一個溫濕度感測器。低功耗、構造簡單,只有 3 個 pin,只要分別接上 Vcc 以及 GND 就可以開始運作。這裡比較特別的是 DHT11 傳輸資料的方式,因為 DHT11 只有一個腳位當作 data bus,透過一個腳位要如何傳遞溫濕度的資料呢?

答案是時間。事實上在硬體當中很多資料的溝通都是仰賴精準的時間控制。例如我想要拿取 8bit 的資料,可以每隔 2ms 當作一個單位,這樣每隔 2ms 讀取一次,讀取 16ms 就可以拿到 8bit 的資料。

不過要做到這件事還要考慮幾件事情:

  • 對方何時開始傳送資料?也就是說我的 2ms 要從什麼時候開始計算?
  • 傳輸過程中如果出現錯誤(如環境、暫時短路、資料丟包),應該如何處理?

這些其實都寫在 datasheet 當中,讓我們一起來瞧瞧。

Single-bus data format is used for communication and synchronization between MCU and DHT11 sensor. One communication process is about 4ms. Data consists of decimal and integral parts. A complete data transmission is 40bit, and the sensor sends higher data bit first. Data format: 8bit integral RH data + 8bit decimal RH data + 8bit integral T data + 8bit decimal T data + 8bit check sum. If the data transmission is right, the check-sum should be the last 8bit of "8bit integral RH data + 8bit decimal RH data + 8bit integral T data + 8bit decimal T data"

根據 datasheet 的描述,每次的通訊過程都會花費 4ms 的時間,其中資料的組成為 8bit 相對濕度(整數位)+ 8bit 相對濕度(小數位)+ 8bit 溫度(整數位)+ 8bit 溫度(小數位)+ 8bit checksum

這邊的 checksum 回答了第二個問題,計算的方式就是將全部資料加總後取最後 8bit。(humidity + humidityDec + temp + tempDec != parity

這樣一來如果計算結果不等於 checksum,我們就可以知道傳送過程中有問題。當然也有可能傳輸過程中造成的問題剛好導致 checksum 相同,這種情況也只能認了。(如果是在正常的環境下機率應該不大)

DHT11 如何傳送資料?

為了回答第一個問題,我們可以參考 Datasheet 當中的圖示,裡頭有說明整個通訊的過程

Screenshot from 2020-07-21 21-34-22

  1. 在開始感測溫度時(Arduino 向 DHT11 要資料),首先先送一個 HIGH -> LOW 的信號,並維持至少 18ms,然後再將信號改為 LOW -> HIGH。在 Arduino 當中可以用 delay()digitalWrite 這兩個函數做到。 p.s: 這裡我不太明白為什麼要那麼長的時間讓 DHT11 偵測這個訊號,還請硬體高手們指教
  2. 這時 MCU 過了 20us 會將信號從 HIGH 拉回 LOW。(這邊要記得將 Arduino 腳位改成 INPUT,可以用 pinMode(PIN, INPUT) 達成)
  3. 這時信號應該會是 LOW,並且等待 80us 後會變成 HIGH。然後再過 80us,再把信號變成 LOW。
  4. 開始傳遞資料(接續下一張)

Screenshot from 2020-07-21 21-44-40

之後終於開始傳資料了!datasheet 說明了要如何分別 0 和 1。如圖所示,如果電壓保持高電位並維持 26-28us 之間,代表回應的資料為 0,如果電壓保持在高電位 70us,代表回應的資料為 1。每個 bit 之間都會將高電位轉為低電位並持續 50us 後再轉成高電位。這麼作的用意應該是清楚分隔每個 bit 之間的間距,讓 Arduino 讀取時比較不會出現誤判的情形。

要計算這個時間,可以透過 Arduino micros() 來完成,簡單的實作像是這樣:

void blockUntil(int state)
{
  while (digitalRead(PIN) != state)
  {
  }
}

void main() {
  auto timeS = micros();
  blockUntil(LOW);
  auto timeE = micros();
  auto result = timeE - timeS > 60 ? 1 : 0;
}

這邊用了 60us 而非 datasheet 上描述的 50us 是因為如果時間剛好的話,有時讀取上會有些錯誤。接下來是我的猜測,還望大大們指教。

我在想是否是因為當我們跑程式碼的時候,因為會編譯成機器代碼,所以導致 CPU 在執行時也會耗掉一些 instruction cycle,而累積起來的結果就讓這邊的 50us 不準確?不過如果是這樣的話,應該是將 50us 調成更小而不是更大才對,所以我在想另外一種可能性就是 DHT11 就是慢。

整個過程完成後應該就可以拿到資料了!雖然這次的實作是用 Library 完成,不過有機會的話會用另外一篇文章來介紹要如何不用 Library 來完成整個功能。其實只要願意花心思閱讀 datasheet 的話,整個實作應該不會太難才對。

總結一下

今天我們介紹了要完成本次應用所需要的感測器、用途、帶大家閱覽了 datasheet,現在大家應該對這兩個感測器是如何傳輸資料的有點概念了吧!下一篇我們會講解硬體與硬體之間的常見溝通管道 - UART,並且分享在實作時遇到的問題以及最後的解決方法(其實就是套 Library 啦!)