前言

曾經為了搭便車, 而坐上筆者所駕駛的小車的同事跟朋友們. 一坐上筆者的車子, 總會注意到駕駛座排檔頭前方有個方形上面焊了電子零件奇怪的小東西, 其中有個顯目的紅色三位七段LED顯示器, 正顯示跳動著耀眼的紅色數字. 而下方各種顏色的一排LED燈號, 有個燈正亮著, 偶而會從這個燈跳到隔壁的燈. 總不免好奇的問一下, 這是個什麼東西呀!

這時候, 我總會輕描淡寫的說, 哦! 這是個露點溫度計, 溫度跟濕度計三合一的東東. 我自己做的喲!

你自己做的?? 真的?? 好厲害喲! 啥是露點溫度啊??

(然後, 聽到人稱讚後就人飄飄然, 暗爽在心裡呀... 哈哈...)

IMG_4232.png

 

露點溫度, 溫度, 相對濕度

大家一定聽過溫度, 相對濕度. 但一定沒聽過露點溫度對不對? 那, 什麼是露點溫度呢? 

露點溫度, 顧名思義, 就是空氣中的水氣, 開始凝結結露時的溫度!

它的物理, 也是很直覺的! 大家知道, 空氣中是可以容納一定的水氣分子的! 在固定壓力下空氣中這個容納水氣分子的數目跟溫度有關係. 溫度愈高, 可以容納的水氣分子就越多, 溫度越低, 可以容納的水氣分子就越少. 這有點像食鹽(水氣分子)溶解在水溶液(空氣)裡面的情況, 只是這時候溶液不是水了, 而是空氣. 而溶質不是食鹽分子了, 而是水氣分子囉. 溫度越高, 食鹽溶解在水溶液的溶解度越高, 相對的也一樣, 溫度越高, 水氣分子可以"溶解"在空氣中的量就越多. 兩者可以容納食鹽(或水氣分子)的量, 都是溫度的一個函數.

而這個水氣分子目前"溶解"在空氣中, 跟它的一個最大的溶解量(飽和的量)的一個相對比例, 就稱之為「相對濕度」.

例如相對濕度 80%, 就代表目前空氣中的水氣分子距離完全飽和已經佔有 80%的量, 再20%就完全飽和了.

相對濕度 100%, 就代表目前空氣中的水氣分子已經完全完全的飽和了, 無法再容納更多的水氣分子. 再更多的水氣分子進來的話, 就像過飽和的食鹽水中的食鹽會從溶液中析出一樣, 水氣分子會從空氣析出變成水滴(霧, 或結露). 而這時候的空氣溫度稱之為當時的露點溫度.

相對濕度100%, 空氣開始結露的溫度叫做露點溫度. 剛剛又說, 這個空氣中最大能容納水氣分子的量(最大溶解的量, 最大飽和的量)是溫度的函數, 溫度越高可以最大可以容納的量就越多. 所以露點溫度可以當作水氣分子能容納在空氣中的一種最大容納量(最大溶解度)的度量指標. 露點溫度越高, 代表當時空氣中可以容納的水氣越多(溶解度高). 露點溫度越低, 代表當時空氣中可以容納的水氣越少(溶解度低).

所以露點溫度雖然是物理單位是"溫度", 但是氣象學上是把它當作一種"絕對濕度", "絕對水氣量"的指標. 當一個地方的露點溫度高, 就代表它的絕對的水氣量比較多. 當一個地方露點溫度低, 就代表它的絕對水氣量比較低. 這已經跳脫單獨"溫度"物理量意義的範疇囉. 這也可以彌補我們熟悉的相對濕度是個相對值, 無法真確告訴你真實水氣多寡的缺陷.

(例如冬天裡因為溫度低, 所以相對濕度高並不代表比較濕. 夏天裡溫度高, 水氣多, 很熱的時候相對濕度不見得比較高. 相較相對濕度, 露點溫度是比較合適的比較水氣多寡的方式)

也因為露點溫度比相對濕度更能表示人體對空氣中水氣的感受, 維基百科裡把人類對不同露點溫度的感受分類如下, 共8種程度:

Dew point Human perception Relative humidity at 32 °C (90 °F)
Over 26 °C Over 80 °F Severely high, even deadly for asthma related illnesses 73% and higher
24–26 °C 75–80 °F Extremely uncomfortable, fairly oppressive 62–72%
21–24 °C 70–74 °F Very humid, quite uncomfortable 52–61%
18–21 °C 65–69 °F Somewhat uncomfortable for most people at upper edge 44–51%
16–18 °C 60–64 °F OK for most, but all perceive the humidity at upper edge 37–43%
13–16 °C 55–59 °F Comfortable 31–36%
10–12 °C 50–54 °F Very comfortable 26–30%
Under 10 °C Under 50 °F A bit dry for some 25% and lower

可以看到, 在高露點的時候, 一般人都會感到不適. 因為高露點伴隨著溫度高, 人體容易出汗. 而高露點時相對濕度高, 人體的汗不易蒸發排熱. 人體易因此過熱而感到悶熱異常而非常的不舒服.

低露點的時候, 氣溫跟相對濕度都會較低, 因而會感覺更為舒適.

一般而言, 露點在16C - 21C 會開始感覺不舒服. 超過 21C 就會開始感覺悶熱異常. 超過 26C 就有機會中暑囉! 而低於 10C 會覺得太乾燥!

 

來做個露點溫度計吧!

只要知道溫度跟濕度, 就可以大略計算出相當準確的露點溫度囉. 我們來利用 Arduino 平台, 透過溫度濕度的雙感測器來做個露點溫度計吧!

IMG_0282.png

IMG_0284.png

IMG_0285.png

 

這個露點溫度計, 規劃出來的功能如下

1. 即使不接上電腦, 也能夠即時地顯示露點溫度. 只顯示露點溫度太浪費了, 當然也希望能顯示當時的溫度跟濕度囉. 為了小型化跟各種環境的閱讀性, 只用一個三位七段顯示器來顯示資訊. 再透過按鈕開關提供使用者隨時切換不同的讀值模式喲! (三種讀值模式: 露點溫度模式, 濕度模式, 溫度模式)

三位七段顯示器, 顯示 [露點溫度]/[濕度]/[溫度]

pic1.png

按鈕開關, 切換三種顯示模式.

pic3.png

 

2. 當接上電腦, 希望能當個 Smart I/O 能夠報出更多更詳細的統計數據給電腦端運用囉. 為了方便記錄時間, 固定每10秒回報一次即時的露點溫度, 溫度, 濕度. 跟舒適狀態, 也能夠自動計算每分鐘的平均值及每分鐘的改變率.

terminal.png

 

3. 要有露點溫度舒適程度的狀態顯示燈. 透過維基百科的定義, 我們知道可以根據露點溫度範圍分成八種不同狀態, 這八個狀態可以顯示出人體對悶熱及舒適程度的感受. 這個露點溫度計可以用點亮 8個 LED 燈號的其中一個來顯示現在位於何種狀態. 也可以將這狀態, 透過電腦顯示對應的狀態文字訊息.

LED 狀態如下圖, 依序由左至右, 中暑/悶熱/.../舒適/舒適/乾燥. 請參照前文表格的八種條件.

pic2.png

4. 希望能自動記憶最後的顯示狀態模式, 例如現在顯示的是濕度模式, 最後即使拔掉電源再接上電源, 還是從濕度顯示開始. 這樣就不用一直每次開機都要重頭按鈕切換囉.

5. 選用大小相當迷你的 Arduino Uno 當開發版, 最後的電路就實作在空白的 Arduino Shield 上. Arduino Uno 的大小剛好, 手機 PDA 般的大小, 還可以用 USB 行動電源供電. 形成一個迷你的露點溫度濕度溫度計. 也可以輕易地放在車上使用.

 

實際運作的情況

 

Arduino Shield:

Arduino 很受歡迎, 所以也被開發出各種的應用板來跟它搭配. 這樣的應用板一般被稱之為 Shield. 例如, 想要做網路通訊應用, 有 Ethernet Shield, 插上去就可以有乙太網路 Ethernet 可以使用. 要做wifi, 資料紀錄, ... etc. 也都有對應的不同的Shield 以供運用. 其中也有完全空白的 Shield, 方便使用者把完成的電路焊在 Shield 上就可以直接使用囉!

這裡就要利用這種完全空白的 Shield 來完成我們最後的成品. 這種空白的 Shield, 非常的貼心, 上面除了一個 Reset 按鈕外, 還提供預留了一個空白的按鈕. 這裡我們就要利用這個空白的按鈕來當作我們的模式切換按鈕. 使用者可以按這個按鈕, 隨時在 [露點溫度], [濕度], [溫度] 三個LED顯示模式間切換. 

空白的 Arduino Shield, 可在上面焊接自己所要的電路.

IMG_0174.png

 

這個露點溫度計, 筆者是先在麵包板上測試整個電路, 最後可行後才正式焊接整個電路到 Arduino 空白的 Shield 板上.

整個電路跟零件和配線在 Shield 板上的安排跟焊接, 是筆者很隨意的選擇, 就看圖說故事, 沒有太多的規劃. 零件焊上去之後, 才開始一條條的配線. 這需要很大的耐心, 配線一條條的慢慢焊接. 沒想到經過幾小時的努力後, 居然一次就成功了耶! 真是超級開心的! (真是太佩服我自己了!) 這真的是大人的樂高玩具啊, 簡直是純手工打造, 在製作手工餅乾的嘛! ^_____^ v

露點溫度計 Shield 板背面複雜的接線情況! :)

IMG_0283.png

 

露點溫度計算,

露點溫度為溫度, 相對濕度, 大氣壓力的函數. 壓力的因素影響比較小, 所以可以近似看為溫度跟相對濕度的函數. 公式如下: (取自維基百科)

dew pt formula.png

其中 b = 17.67, c = 243.5

 

在 Arduino 的軟體開發環境中實作後的程式碼如下, dewTmp() 函式用來計算露點溫度. 需要兩個參數: RH相對濕度 (%), Tmp溫度. (Degree C)

float dewTmp(float RH, float Tmp)
{
  const float b=17.67;
  const float c=243.5;
  float gamma=log(RH/100)+b*Tmp/(c+Tmp);
  return c*gamma/(b-gamma);
}

 

DHT 溫度, 濕度雙感測器

不知道怎麼搞的, 關於溫度跟濕度的量測, Arduino 官方似乎特別喜歡 DHT 系列的感測器. 所以我們也跟著用囉. 這一系列的溫濕感測器, 一共有兩種: DHT11 跟 DHT22. 它們的特色是, 算是一種 smart I/O, 所以不用費心在量測的控制上, 他們直接會把量測的結果以數位的格式從序列埠送出來. 3V ~ 5V 間的電源皆可使用, 量測時最高消耗 2.5mA 的電流.

DHT11 算是低價的感測器準確度較低, DHT22 價格就貴許多, 但準確度較高. 這裡筆者要強烈建議使用 DHT22, 不要使用 DHT11. 因為真的準確度太差了. 曾經為了便宜買過好幾顆, 結果每顆量出來的數值都天差地遠, 都不知道要相信誰的, 最後一律直接使用 DHT22, 準確多了! 量測的範圍也大!

 

DHT11:

1. 價格非常的便宜 (< NT$100)

2. 濕度適用範圍: 20% - 80%, 誤差 5%

3. 溫度適用範圍: 0C - 50C, 誤差 +/-2C

4. 取樣頻率每秒不可超過一次.

 

DHT22:

1. 價格略高 (NT$150 ~ NT$250)

2. 濕度適用範圍: 0% - 100%, 誤差 2% ~ 5%

3. 溫度適用範圍: -40C - 125C, 誤差 +/-0.5C

4. 速度較慢, 取樣頻率每2秒不可超過一次.

 

實體圖示: 左邊 DHT11, 右邊 DHT22

DHT11  DHT22.png

 

DHT 溫度, 濕度雙感測器 pin 腳

DHT感測器實體有四個 pin腳 

pin1 接 Vcc, pin4 接地,

pin2 為序列埠傳送資料, 當接線小於 2m 時, 原廠建議要有個 5K 上拉 (pull-up) 電阻. 建議直接買現成的模組, 這些都會直接焊好, 會方便許多.

dht pins.png

 

DHT 溫度, 濕度雙感測器的控制

這裡就顯現出 Arduino 平台的優點了, 因為官方已經將驅動 DHT 感測器的程式碼寫好囉! 只要載入就可以立即使用.

Arduino 的官方函式庫跟這個程式庫都是以物件導向技術的方式所撰寫的. 雖然筆者個人是認為啦, 這種單晶片系統, 用物件導向的環境真的是有點多此一舉. 因為真的, 記憶體如此受限下, 不可能寫出非常龐大的程式. 這樣的情況下用物件的軟體技術, 真的是有點殺雞用牛刀!

不過, 官方都已經實作了這樣的環境出來了, 所以還是直接用了吧! :)

 

首先利用下列指令載入官方的DHT 程式庫

#include "DHT.h"

 

程式庫載入後, 可以利用 DHT 類別宣告產生 DHT 物件, 這個物件產生宣告需要兩個參數,一個是這個 DHT 感測器控制物件所對應到的串列溝通的腳位, 另一個是感測器種類. ('DHT11' 或 'DHT22', 因為 DHT11跟 DHT22所傳出來的資料格式不一樣, 所以要指名現在用的是何種感測器所使用的格式!)

例如 DHT dht(16, DHT22);  這樣的宣告會產生一個 dht 物件, 控制溝通腳位在 pin 16, 感測器種類是 DHT22

然後, 這裡就顯示出物件技術的好處了! 假如你有第二個 DHT 感測器要控制, 就再用 DHT 宣告第二個DHT控制物件, 有幾個, 就宣告幾個. 直接就可以控制讀值囉, 非常非常的方便! Arduino Uno 有20個 digital I/O port, 所以理論上最多可以控制 20 個 DHT 感測器.

DHT dht1(0, DHT22)

DHT dht2(1, DHT22)

...

DHT dht20(19, DHT22)

DHT類別裡面有兩個物件方法, 用來跟 DHT感測器溝通, 來獲取所量測到的溫度跟濕度: readTemperature() 跟 readHumidity()

要在dht物件裡面呼叫這兩個方法, 寫法是: dht.readTemperature() 跟 dht.readHumidity()

它們傳回來的值是浮點數的攝氏溫度跟濕度, 而當它們傳值回來是 Nan 的時候(可以用 isnan()函式來檢查), 就代表感測器數值的讀取遇到了問題.

readTemperature() 預設傳回值是攝氏溫度(Celsius), 假如要傳回華氏溫度(Fahrenheit) 的話, 只要裡面加個 true 的引數 readTemperature(true), 這時候傳回值會自動轉換成華氏溫度.

文件中有說明, 溫度跟濕度的讀取, 需要花費 250mS 的時間. 然後這是個很慢的感測器, DHT11 需要間隔個1秒以上, DHT22 需要間隔2秒以上.

 

整個讀取溫濕度的程式碼如下所示,

宣告一個 dht 物件

DHT dht

先從 DHT 感測器讀取濕度/溫度, 然後用 compensate() 函式修正可能的誤差, 先暫存到 hum0, temp0. 再透過 isnan() 來檢查一下是否有任何讀取錯誤, 假如沒有的話就放入 hum, tmp 給後面的程式碼使用囉.

  // read humidity
  float hum0 = compensate(dht.readHumidity(),RH_scalar,RH_offset);
  
  // Read temperature as Celsius (the default)
  float tmp0 = compensate(dht.readTemperature(),T_scalar,T_offset);
 

  // Check if any reads failed
  if (isnan(hum0) || isnan(tmp0)) 
  {
    Serial.println("Failed to read from DHT22 sensor!");
  }
  else
  {
    hum = hum0;
    tmp = tmp0;
  }

 

compensate() 校正函式修正的原理

在讀取一個類比線性感測器時, 因為完美的電路是不可能的, 雖然關係式是線性的,  但往往會有些類比誤差. 常見的兩個誤差源是電路有了些偏差量 (offset) 跟線性電路的斜率 (scalar) 有些小差異. 這時候通常有兩種作法, 一種是電路的修正, 利用運算放大器電路上設計一個偏差DC電流的調整來做偏差量 (offset) 的補償調整, 跟一個放大電路放大倍率的微調整電阻, 來做斜率(scalar)的補償調整.

另外一種更簡便的做法就是利用軟體來修正囉, 只要簡單的利用 y = ax + b 這樣簡單的線性方程, 例如溫度讀值 T, 透過 T' = scalar * T + offset, 透過 scalar 跟 offset 這兩個矯正參數, 將原始讀值修正到更精確沒有誤差的讀值 T'

scalar and offset.png

利用這樣的原理, 這裡實作了一個這樣的校正函數 compensate() 給溫度跟濕度使用.

float compensate(float raw, float scalar, float offset)
{
  return(scalar*raw + offset);
}

 

溫度濕度校正常數如下, 預留未來校正的可能性.

// calibration constants for DHT sensor
const float T_scalar=1;
const float T_offset=0;
const float RH_scalar=1;
const float RH_offset=0;

 

按鈕開關跟中斷的使用

因為三位七段顯示器一次只能顯示三位數. 我們希望能用這個三位七段顯示器顯示露點溫度, 濕度, 跟溫度.

Shield 上剛好有個按鈕是預留給使用者來做各種用途的, 這裡就利用這個按鈕來偵測使用者是否有按了按鈕, 有的話就依序切換這三個量露點溫度, 濕度, 跟溫度的顯示.

 

按鈕開關電路

電路有兩種, 一種是平常邏輯 High, 按了開關變成邏輯 Low, 放掉開關又恢復邏輯 High. 另外一種是完全倒過來, 平常邏輯 Low, 按了開關變成邏輯 High, 放掉開關又恢復邏輯 Low.

電路如下

I. 平常是邏輯 High, 按了開關變邏輯 Low.

電路上必須加上一個10K的電阻往上接上電源, 所以也叫上拉 (pull-up) 電阻. (這個電阻阻值愈低電路欲穩定, 越不易受雜訊干擾, 但越耗電!) 

這裡這個露點溫度計採用這個設計, 平常是邏輯 High, 按了按鈕會變成邏輯 Low. 且偵測電路電位是否掉落 (Falling) 來觸發中斷, 通知軟體動作.

sw2.png

 

II. 平常是邏輯 Low, 按了開關變邏輯 High.

電路上必須加上一個10K的電阻往下接地, 所以叫下拉 (pull-down) 電阻.

這個電阻阻值愈低電路欲穩定, 越不易受雜訊干擾, 但越耗電!

sw1.png

 

按鈕開關的偵測

因為平常程式已經很忙, 要不斷的掃描七段顯示器做數字的顯示輸出, 假如要它去不斷的檢查偵測(pooling)是否有使用者按了開關並作出反應的話, 這樣真的太麻煩了.

對於那種無法預期, 有隨機性的那種服務, 最好的方式是利用 CPU 的中斷服務來達成. 所以這裡就設計利用 Atmega 微處理器的外部中斷訊號 INT0 來達到這樣的目的.

 

Arduino Uno 外部中斷 INT0 的訊號, 位於 pin2, 所以定義一下腳位

// key switch for interruption
#define swPin 2

 

再來定義中斷服務常式 sw_sense(),

當你按下按鈕, 因為這個按鈕會造成電位往下改變, 從 High 變成 Low (FALLING), 這時候觸發硬體 INT0 中斷, 中斷服務常式 sw_sense() 就會被執行.

 

防止彈跳 (Debouncing)

大家都知道, 當你按了開關的按鈕時, 瞬間會有彈跳 (Bouncing) 的情況 (在物理的力學上, 這種現象叫 damping). 這時候狀態是不穩定的, 然後容易讀到錯誤的訊號. 這時候會把一次按鈕的訊號誤判成好幾次. 所以軟體在判斷開關觸動時, 需加入防止彈跳 (Debouncing) 的程式碼, 否則會誤判. 一般而言, 就是開關觸發後, 需等待一段時間, 等待彈跳 (bouncing) 現象消失, 開關完全穩定後再次讀取開關狀態進行確認.

bouncing.png

 

這個 sw_sense() 函式有考慮這樣的情況.

中斷一發生(按鈕被按), 程式會檢查是否跟上次按鈕被按的時間 (unsigned long time;) 兩者是否間隔超過了 250 mS, 有的話才認為這是真的訊號, 否則就認為是假的訊號不做任何動作.

這可以防止使用者明明只按了一次按鈕, 但因為電路的彈跳 (bouncing) 現象, 讓中斷訊號瞬間發生了兩次, 結果程式誤以為使用者按了兩次或三次按鈕. (實際上只有一次啊!)

sw_sense() 做的事情很簡單, 判斷開關沒有因為彈跳 (bouncing) 現象誤按多次後, 把主要的狀態變數 main_state 依序在 '1' (顯示露點溫度), '2' (顯示濕度), '3' (顯示溫度) 間變化.

然後為了怕 main_state 所記載的現在顯示狀態因為沒電後消失, 所以最後也順便寫入 EEPROM 裡面. 提供下次開機時讀取使用.

(所以當你切換到顯示濕度時, 下次開機還是顯示濕度, 不會老是從露點溫度開始)

 

// 0 = dew, 1 = RH, 2 = Temp

unsigned long time;

void sw_sense()
{
  if (millis() - time > 250)
  {
    time=millis();
    main_state ++ ;
    if (main_state==3) main_state=0;
    EEPROM.write(EEP_state_addr, main_state);
  }
}

 

連結中斷服務常式至硬體中斷 INT0, 觸發中斷訊號所需的狀態是 FALLING

// attach ISR
attachInterrupt(0,sw_sense,FALLING);

 

 

三位七段LED顯示

這裡基本原理的更詳細解說請看我的另一篇 blog, 這裡直接說成果了!

首先是字型定義表, 因為使用者按按鈕開關時, 假如只是顯示數字, 使用者很容易搞混, 現在的數字到底是露點溫度? 還是實際的溫度??

所以有需要當切換不同的顯示模式時, 要先顯示幾秒的提示字元, 來提醒現在所處的顯示模式.

例如, 顯示 'd' 'E' 't' 來表示現在顯示的是露點溫度, 顯示 'H' 'U' 't' 來表示現在顯示的是濕度, 顯示 ‘t' 'E' 'P' 來表示現在顯示的是溫度.

所以字型表需要額外的定義, 'd', 'E', 't', 'H', 'U', 'p' 這六個字元.

// data structure 8bits = dp aa bb cc dd ee ff gg
// 0 to 9, '-', 'd', 'E', 't', 'H', 'U', 'p'
const byte segs_data[17]={B01111110, B00110000, B01101101, B01111001, B00110011, B01011011, B01011111, B01110000, B01111111,

B01110011, B00000001, B00111101, B01001111, B00001111, B00110111, B00111110, B01100111} ;

 

再來, CPU 需要不斷的掃描三個七段LED, 維持著視覺暫留的現象, 讓你以為三個位數是同時亮的. 所以這裡定義了顯示陣列 byte seg7[3]={0,0,0}; 來儲存目前要顯示的字形資料.

lightup() 函式, 會從這個 seg7[3] 顯示陣列取值, 來顯示字形! 所以程式的主迴圈裡面, 要不斷的維持呼叫 lightup() 函式, 來掃描並維持三位七段顯示器的顯示工作.

prep_Char(byte digit, byte index, boolean dot) 函式, 會透過字型資料陣列取值, 取出所想要顯示的字型資料, 放入顯示陣列 seg7[3] 中, 以提供 lightup() 函式顯示用.

prep_LED(int num) 函式則更高階一點了, 會將要顯示的數字轉換成對應的字形資料(透過prep_Char()函式取值) 放入顯示陣列 seg7[3] 中, 以提供 lightup() 函式顯示用.

 

露點溫度舒適程度狀態顯示燈

這個舒適程度的狀態顯示需要八個 LED, 這有點多. 因為我們已經用掉大概十幾個 digital I/O埠囉, 再加上這八個, 會超過 Arduino Uno 最多 20個 digital I/O 埠的限制.

所以這裡採用一顆 74HC595 8位元的移位暫存器 Shift Register 來擴充我們的 Digital I/O埠

這個移位暫存器可以有 8 個 Digital I/O 埠, 分別由裡面的八個位元暫存器的狀態來決定. 使用者可以透過序列訊號的輸入, 來改變這八個位元暫存器的狀態. 整個 IC 的腳位如下圖.

74HC595.png

最重要的是 pin14 腳位 SER, 跟 pin11 腳位 SRCLK 以及 pin12 腳位 RCLK

SER 就是8 bits 序列訊號的資料輸入, SRCLK 為這8 bits 序列訊號資料輸入時所需要的 clock 時脈訊號(正緣觸發). 當SRCLK時脈訊號由Low到High的時候(正緣觸發), 這時候SER的邏輯訊號會被推入對應的暫存器.

例如, 假如送入序列訊號 10110001 於 SER腳位, 配合同步的時脈訊號於 SRCLK腳位. 最後八個 digital I/O 的訊號會是

Out0: High, Out1: Low, Out2: High, Out3: High, Out4: Low, Out5: Low, Out6: Low, Out7: High

 

pin12 腳位 RCLK, 其實是個 latch 鎖定用的腳位, 當它訊號為 High 時, 表示八個 digital I/O 的輸出是鎖定的. 這時候無法做任何的輸入動作改變狀態.

所以要輸入前, 須將這個腳位訊號變成 Low, 等輸入完畢後再將這個腳位訊號變成 High, 所有8個 digital I/O 埠的輸出就會改變並鎖定.

 

在 Arduino 裡面, 控制這個 IC 很容易啦! 這裡就發揮出 Arduino 的威力了, 已經有現成的函式, 只要直接呼叫就OK囉!

內建函式 shiftOut() 可以很方便地達成控制這個 IC 的目的, 這個函式需要四個參數. shiftOut(DataOutPin, ClockOutPin, BitsOrder, Data)

DataOutPin 是資料輸出的接腳位置; ClockOutPin 是時脈輸出的接腳位置; Data 是所要傳出的八位元資料; BitsOrder 指名解讀 Data bits 的方向,有兩種選擇: 'MSBFIRST' 表示從高位元往低位元解讀. 'LSBFIRST' 表示從低位元往高位元解讀.

這個函式會根據你給的 data, 跟跟你指定解讀的位元方向拆解 data 裡的每個 bits. 然後一邊從 ClockOutPin 送出時脈訊號, 一邊從 DataOutPin 送出所拆解的每一個 Data bits.

程式裡 light_indicator() 函式就是利用這個重要的 shiftOut() 函式來運作控制 74HC595. 先將74HC595的 RCLK (latch) 鎖定訊號解開, 再利用 shiftOut()送入新資料, 完畢後再重新鎖定(latch) 74HC595 IC 的八個digital I/O 埠. 

void light_indicator(byte state)
{
  digitalWrite(latchPin,LOW);
  shiftOut(dataPin, clockPin, LSBFIRST, state);
  digitalWrite(latchPin, HIGH);
}

程式利用下列程式碼來輸出點亮 露點溫度狀態指示燈.

先假設要點亮的是最右的那個燈號 byte ds = B00000001; dewS 是要點亮的位置號碼 (由右往左數), 根據這個號碼, 依序的將燈號往左移, 最後就是所要的正確位置了.

byte ds = B00000001;
  for (byte i=0;i<dewS;i++) ds<<=1;
  light_indicator(ds);

 

整個完整接腳電路如下所示!

circuit2.png

 

附上初期, 用麵包板測試的實際狀況,

 

 

原始完整程式碼列表

//
// Dewpoint meter by DHT22
//
// Frank Lin 2017.01.08
// Version: 1.3 final version
// 

// 2015.11.25 V1.0   
// first version, for DHT11
//
// 2016.01.31 V1.1   
// sync with internal timer, add push button, change sensor to more accurate DHT22
//
// 2016.02.03 V1.2 
// add new features for some statistical data calculations
//
// 2017.01.08 V1.3 
// add new feature, use EEPROM to store state variable, to memorize the state after power-off
//

#include "DHT.h"
#include <EEPROM.h>

#define DHTPIN 16             // digital i/o for reading DHT22 output
#define DHTTYPE DHT22   // sensor type: DHT22

// 7segments LED output pins definations
#define digit1 13
#define digit2 10
#define digit3 9
#define aa 12
#define bb 8
#define cc 6
#define dd 4
#define ee 3
#define ff 11
#define gg 7
#define dp 5

// key switch for interruption
#define swPin 2

// 8 bits shift register
#define dataPin 17
#define latchPin 18
#define clockPin 19


// version const
const byte vers=13;

// time interval (mS) for measurement
unsigned long time_interval = 10000; 


// calibration constants for DHT sensor
const float T_scalar=1;
const float T_offset=0;
const float RH_scalar=1;
const float RH_offset=0;

float compensate(float raw, float scalar, float offset)
{
  return(scalar*raw + offset);
}


DHT dht(DHTPIN, DHTTYPE);

// data structure 8bits = dp aa bb cc dd ee ff gg
// 0 to 9, '-', 'd', 'E', 't', 'H', 'U', 'p'
const byte segs_data[17]={B01111110, B00110000, B01101101, B01111001, B00110011, B01011011, B01011111, B01110000, 
B01111111, B01110011, B00000001, B00111101, B01001111, B00001111, B00110111, B00111110, B01100111} ;
const byte seg_pins[8]={gg,ff,ee,dd,cc,bb,aa,dp};
const byte digit_pins[3]={digit1,digit2,digit3};
byte seg7[3]={0,0,0};


// interrupt service

const byte EEP_state_addr = 0;
byte prev_state = 0;
volatile byte main_state = 0;
// 0 = dew, 1 = RH, 2 = Temp

unsigned long time;

void sw_sense()
{
  if (millis() - time > 250)
  {
    time=millis();
    main_state ++ ;
    if (main_state==3) main_state=0;
    EEPROM.write(EEP_state_addr, main_state);
  }
}

 

void prep_Char(byte digit, byte index, boolean dot)
{
  // digit = 0, 1, 2
  // index = 0..10
  
  seg7[digit] = segs_data[index];
  if (dot) seg7[digit] |= B10000000;
 
}


void lightup()
{
   for (byte i=0;i<3;i++)
  {
    byte segments = seg7[i];
    if (segments==0) continue;
    // select digit
    for ( byte j=0; j<3; j++)
    {   
      // common anode, set output to HIGH to select digit what you want, the other digits must be set to LOW
      // common cathode, set output to LOW to select digit what you want, the other digits must be set to HIGH
      
      if (j==i)   digitalWrite(digit_pins[j], HIGH); 
      else digitalWrite(digit_pins[j], LOW);
    }
    
    // acording char data, light each segments on digit
    for ( byte j=0; j<8; j++)
    {
      // common anode, set output to LOW to light up LED, HIGH to off LED
      // common cathode, set output to HIGH to light up LED, LOW to off LED

      if ((segments & B00000001) == 1)   digitalWrite(seg_pins[j], LOW);
      else  digitalWrite(seg_pins[j], HIGH);
      segments >>=1;
    }   
    delay(5);
  }
}

void prep_LED(int num)
{
  boolean neg = false;
  if (num<0) neg = true;
  num=abs(num);
  prep_Char(2,num % 10, false);
  num /=10;
  prep_Char(1,num % 10, true);
  num /=10;
  if (neg) prep_Char(0,10, false);
  else prep_Char(0,num % 10, false);
}

float dewTmp(float RH, float Tmp)
{
  const float b=17.67;
  const float c=243.5;
  float gamma=log(RH/100)+b*Tmp/(c+Tmp);
  return c*gamma/(b-gamma);
}

void light_indicator(byte state)
{
  digitalWrite(latchPin,LOW);
  shiftOut(dataPin, clockPin, LSBFIRST, state);
  digitalWrite(latchPin, HIGH);
}

byte dewState(float dew)
{
  if (dew > 26 ) return 7;
  else
   if (dew > 24 ) return 6;
   else
     if (dew > 21 ) return 5;
     else
       if (dew > 18 ) return 4;
       else
         if (dew > 16 ) return 3;
         else
           if (dew > 13 ) return 2;
           else
             if (dew > 10 ) return 1;
             else return 0;
}

void printState(byte state)
{
   switch(state)
   {
     case 7 : Serial.println(" --> extremely high, deadly for asthma related illnesses!!");
               break;
     case 6 : Serial.println(" --> Very very humid, extremely uncomfortable!!");
               break;
     case 5 : Serial.println(" --> Very humid, quite uncomfortable!!");
               break;
     case 4 : Serial.println(" --> humid, Somewhat uncomfortable for most people at upper edge!!");
               break;
     case 3 : Serial.println(" --> OK for most, but all perceive the humidity at upper edge!!");
               break;
     case 2 : Serial.println(" --> Comfortable!!");
               break;
     case 1 : Serial.println(" --> Very Comfortable!!");
               break;
     case 0 : Serial.println(" --> A bit dry for some!!");
   }
             
}

void setup() 
{
  // DHT sensor will take 250 mS to read temperature or humidity
  time_interval -= 250*2;
  
  // setup 7seg LED pins mode
  pinMode(digit1, OUTPUT);
  pinMode(digit2, OUTPUT);
  pinMode(digit3, OUTPUT);
  pinMode(aa, OUTPUT);
  pinMode(bb, OUTPUT);
  pinMode(cc, OUTPUT);
  pinMode(dd, OUTPUT);
  pinMode(ee, OUTPUT);
  pinMode(ff, OUTPUT);
  pinMode(gg, OUTPUT);
  pinMode(dp, OUTPUT);
  pinMode(dataPin, OUTPUT);
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  
  // attach ISR
  attachInterrupt(0,sw_sense,FALLING);
  
  // restore main_state from EEPROM
  main_state = EEPROM.read(EEP_state_addr);
  if (main_state<0||main_state>2) main_state=0;
 

  // setup serial port
  Serial.begin(9600);
  Serial.println("DHT22 dew point meter!");
  
  // setup dht sensor routine
  dht.begin();
}


// statistical variables
unsigned int m_count=0;
unsigned int h_count=0;
float avgtmp_min;
float p_avgtmp_min;
float avgtmp_hour;
float p_avgtmp_hour;

float avghum_min;
float p_avghum_min;
float avghum_hour;
float p_avghum_hour;

float hum;
float tmp;
float dew;

void loop() 
{
  // read humidity
  float hum0 = compensate(dht.readHumidity(),RH_scalar,RH_offset);
  
  // Read temperature as Celsius (the default)
  float tmp0 = compensate(dht.readTemperature(),T_scalar,T_offset);
 

  // Check if any reads failed
  if (isnan(hum0) || isnan(tmp0)) 
  {
    Serial.println("Failed to read from DHT22 sensor!");
  }
  else
  {
    hum = hum0;
    tmp = tmp0;
  }
  
  m_count++;
  h_count++;
  avghum_min = (avghum_min*(m_count-1)+hum)/m_count;
  avgtmp_min = (avgtmp_min*(m_count-1)+tmp)/m_count;
  avghum_hour = (avghum_hour*(h_count-1)+hum)/h_count;
  avgtmp_hour = (avgtmp_hour*(h_count-1)+tmp)/h_count;
    
   if (m_count==6) 
  { 
    m_count=0;
    p_avghum_min = avghum_min;
    p_avgtmp_min = avgtmp_min;
  }
   if (h_count==360) 
  { 
    h_count=0;
    p_avghum_hour = avghum_hour;
    p_avgtmp_hour = avgtmp_hour;
  }
   
  
  // Calculate Dewpoint
  dew = dewTmp(hum,tmp);

  Serial.print("Humidity: ");
  Serial.print(hum);
  Serial.print("% , ");
  
  Serial.print("Temperature: ");
  Serial.print(tmp);
  Serial.print("C , ");
  
  Serial.print("DewPoint Temperature: ");
  Serial.print(dew);
  Serial.print("C  ");
  
  Serial.print("State: ");
  Serial.print(main_state);
    
  byte dewS = dewState(dew);
  
  Serial.print(" Dew Status: ");
  Serial.print(dewS);
  printState(dewS);
  
  Serial.print("Prev_Hum_min_avg: ");
  Serial.print(p_avghum_min);
  Serial.print("%  ");
  
  Serial.print("Hum_ROR/min: ");
  Serial.print((avghum_min - p_avghum_min));
  Serial.print("%  ");
  
  Serial.print("Prev_Temp_min_avg: ");
  Serial.print(p_avgtmp_min);
  Serial.print("C  ");
  
  Serial.print("Temp_ROR/min: ");
  Serial.print((avgtmp_min - p_avgtmp_min));
  Serial.println("C  ");
  
  Serial.println();
  
  byte ds = B00000001;
  for (byte i=0;i<dewS;i++) ds<<=1;
  light_indicator(ds);
   
  unsigned long target_time = millis() + time_interval; 
  while (millis() < target_time)
  {
      if (prev_state!=main_state)
   {
       switch(main_state)
     {
       // display 'd', 'E', 't' - dew point temperature
       case 0 : prep_Char(0,11,false);
                prep_Char(1,12,false);
                prep_Char(2,13,false);
                 break;
       // display 'H', 'U', 't' - humidity
       case 1 : prep_Char(0,14,false);
                prep_Char(1,15,false);
                prep_Char(2,13,false);
                 break;
       // display 't', 'E', 'p' - temperature
       case 2 : prep_Char(0,13,false);
                prep_Char(1,12,false);
                prep_Char(2,16,false);           
     }
     prev_state = main_state;
     for (int i=0;i<50;i++) lightup();
   }
   else
   {
    switch(main_state)
    {
     case 0 : prep_LED((int)(dew*10.0+0.5));
               break;
     case 1 : prep_LED((int)(hum*10.0+0.5));
               break;
     case 2 : prep_LED((int)(tmp*10.0+0.5));
               
    }
   }
   
   lightup();
  }
 }

xx

 

 

arrow
arrow

    ohiyooo2 發表在 痞客邦 留言(0) 人氣()