一氧化碳, 液化石油氣瓦斯洩漏:

在臺灣, 常常可以聽到用瓦斯熱水器燒熱水洗澡, 因為通風不良最後一氧化碳中毒的新聞. 輕則送醫急救, 重則直接往生了. 另外一個常聽到是液化石油氣, 瓦斯洩漏. 雖然廠商已刻意在瓦斯裡面添加惡臭的成分, 讓洩漏時大家能聞到味道而有所警覺. 但還是很容易因為輕微洩漏, 無法十分確定而失去警覺, 最後因一點火花點燃造成嚴重的瓦斯氣爆而釀成嚴重的傷亡.

要能避免這兩種情況, 最好的方式就是購買一氧化碳偵測警報器, 跟瓦斯偵測警報器. 來偵測空氣中是否有不正常的一氧化碳跟瓦斯的濃度. 當濃度高達某種程度時, 立刻發出警報, 提醒周圍的人員開始應變, 就可以避免這樣的悲劇重複發生.

 

新竹電子材料店:

偶而做做電子勞作, 是筆者課餘下班後的消遣跟活動, 因為住的是新竹這個生活圈, 所以偶爾會去清華大學對面的電子材料行逛逛跟補補貨! 話說有一天, 一如往常逛逛新竹 NOVA 後就順便去電子材料行晃晃. 有注意到貨架上有 MQ 系列的一些氣體感測模組正在販售. 很早很早就想買個一氧化碳偵測器了, 覺得家裡應該會需要. 但是看到動輒數千元的售價, 實在讓人猶豫跟裹足不前. 現在看到 MQ 系列的氣體感測模組, 真是讓人眼睛一亮, 約百來元台幣非常可親的售價, 於是興起了一個念頭, 何不利用 Arduino, 自己來做一個一氧化碳偵測器呢? 既便宜又實惠, 還可以學習一下電子電路感測器技術的咩.

最後在感測器貨架前駐足良久, 東挑西選的, 在眾多 MQ 系列的氣體感測器中, 挑中 MQ-7 一氧化碳感測模組 跟 MQ-2 多重可燃氣體感測模組, 來打算製作一個可以偵測一氧化碳濃度跟液化石油氣瓦斯洩漏的雙重偵測警報器.

 

MQ 系列氣體感測元件:

這一系列的氣體感測元件是以二氧化錫為氣體感測基礎所製作的元件. 二氧化錫 SnO2 是一種 N-Type 的金屬氧化物半導體, 晶體結構為正方晶系, 能隙為 3.6eV, 薄膜折射率約為 2.

此類氣體感測器內通常裝置一加熱器, 將二氧化錫加熱至 200 - 300 C 左右的攝氏溫度. 感測器上裝置細孔不鏽鋼網, 除快速傳熱外兼防止對待測可燃性氣體造成可能的氣爆危險. 在此溫度下, 二氧化錫會吸附空氣中的氧原子, 形成氧的負離子吸附, 這會造成二氧化錫半導體中(N-type半導體, 多數載子為電子) 電子自由載子數目的降低, 導電率的下降. (電阻率上升)

當二氧化錫吸附還原性氣體 (如:液化石油氣, 天然氣, 氫氣, 一氧化碳, 有機溶劑蒸氣... etc.) 時, 原來吸附的氧分子脫離, 而由還原性氣體以正離子的狀態吸附在二氧化錫半導體表面. 吸附的氧分子脫離需要放出電子, 還原性氣體以正離子狀態吸附也會放出電子. 這使得二氧化錫半導體內的自由載子數目瞬間增加, 造成導電率的上升. (電阻率的下降)

透過偵測二氧化錫導電率的改變(或 電阻率, 總電阻的改變)所造成電路上的電壓改變, 就可以得知二氧化錫自由載子數目的改變, 這些改變來自於所吸附的還原性氣體跟其氧原子所貢獻的自由電子, 所以進而可以得知所吸附的還原性氣體的氣體濃度.

 

MQ 系列氣體感測電路:

底下為廠商所建議的 MQ 系列的標準感測電路

左側為標準 MQ 系列感測器的接腳. 通常會有 A, B, H 總共 6根接腳.  H 的接腳共有兩根, 接在內部的加熱器, 通常需要提供規定的額定電壓 (正常 5V) 產生固定的的電流提供內部加熱器運作, 讓感測器內部達到所設計的固定工作溫度. (200C -300C)

A, B 的接腳各兩根, 兩根 A-A 是一樣的, 同樣兩根 B-B 也是一樣的, 為了穩定性, 所以設計出兩根一樣的接腳提供外部連接. 而 A-B 間有一定的阻抗, 如前面所描述的感測原理, 當待測氣體進入感測器被二氧化錫吸附時, 會產生額外的自由電子進入二氧化錫半導體造成 A-B 間的阻抗下降. 所以 A-B 間的阻抗下降程度即是所感應到的待測氣體氣體濃度的改變程度.

定義感測器 A-B 間的阻抗為 Rs, 所以下圖右邊廠商所建議的感測電路, 由簡單的電路電壓分壓定律 Vout = Vc * RL / (Rs + RL)

當 Rs 因為感測到待測氣體濃度而降低時, Vout 會升高. 藉由感測 Vout 的電壓改變, 透過實驗跟標準濃度比對, 可以標定出 Vout 跟 待測氣體濃度的關係, 進而得知當時所感測到的真實氣體濃度.

MQ Sensor.png

附記:

Vout = Vc * RL / (Rs + RL), 一些官方的MQ 感測器 Data Sheet 是以 VL 符號來定義 Vout, 所以 VL = Vc * RL / (Rs + RL)

將 Rs 提出, 這個公式等價於不同版本 Data Sheet 所描述的 Rs = (Vc / VL - 1) * RL

 

Preheat (Burn-In) 時間:

這個 MQ 系列的感應器, 採用二氧化錫 SiO2 來感測運作, 這個物質相當穩定, 廠商宣稱正常24小時不間斷的運作下, 使用壽命可以五年以上. 不過很有趣的是, 全新的感測器一開始工作時並不穩定, 需要經過一段時間 (一天到兩天) 於工作溫度 (200C - 300C) 下的燒機 (Burn-In) 動作. 感測器才會逐漸穩定, 氣體的偵測跟讀值才會開始逐漸穩定跟準確.

原廠的 Data Sheet 把這個燒機 Burn-In 的過程稱之為 Preheat, 不同感測器的 Data Sheet 會記載描述其所對應型號感測器所需的燒機 Burn-In 時間.

例如: MQ-2 的 Preheat 時間是至少 24 小時, 也就是說全新的感測器需要通電讓內部的加熱器加熱起碼 24小時以上, 才可以開始穩定使用. 而 MQ-7 的 Preheat 時間就更久了, 起碼 48小時以上.

 

MQ-2 多重可燃氣體 (液化氣/丙烷/氫氣) 感測模組:

這個氣體傳感器對液化氧、丁烷、丙烷、甲烷、酒精、氫氣的靈敏度高,對天然氣和其它可燃蒸汽的檢測也很理想,可檢測多種可燃性氣體. 售價約 NT$60 - NT$180 台幣,是一款適合多種應用的低成本傳感器。

來看一下它的規格書吧!

MQ-2 SPEC01-2.png

 

摘要一下規格書的要點:

1. 感測器電壓 Vc, 標準測試電壓為 5V +/- 0.1V.

2. 必須提供 VH = 5.0V +/- 0.1V 的標準電壓給加熱器, 給感測器內部加熱用! 交流直流皆可. 加熱器的阻抗為 33 ohm, 消耗功率約 800mW 以內.  (所以加熱器電流額定值約為  = 5V / 33ohm = 152mA )

3. 氣體感測濃度能力依不同氣體規格不同, 約在 100 - 5,000 ppm 間. 感測器阻抗 Rs 於 3K - 30K ohm 間變動.

4. Preheat 時間超過 24 hours, 新 sensor 需燒機 Burn-In 超過 24小時才會穩定, 此時才是可用的.

 

特性曲線:

底下為正規化後的特性曲線, 於溫度 20C, 相對濕度 65%, 背景氧濃度 21%, RL = 5K ohm 下所量測得到的曲線. R0為 1,000 ppm 的氫氣在乾淨空氣下所量到的 Rs

橫軸是各氣體的濃度, 縱軸是正規化的沒單位的比值, 就感測器在這個氣體濃度下的 Rs 的阻值除以這個感測器在 1000 ppm 下的氫氣濃度下感測器所呈現的阻值 R0. (所以 1000 ppm 下的氫氣, Rs/R0 = 1, 當作此縱軸正規化的單位. 或者也可以說縱軸的單位是 R0)

這個 Chart 有兩個要點:

1. 在對數座標下, 感測器正規化的阻抗 Rs/R0 和氣體濃度的關係近似是線性的: y' = -a x' + b,  y'=對數座標下的正規化阻抗 Rs/R0, x'=對數座標下的氣體濃度ppm, a=此氣體線性曲線(對數座標下)的斜率常數, b=此氣體線性曲線(對數座標下)的截距常數, 一如預期的, 氣體濃度越濃, 感測器的阻抗越低. 不同氣體, 特性曲線的常數 a, b 不一樣!

2. 乾淨空氣 air 下, 感測器阻抗不會變化, 永遠是 10 (就是10個 R0的意思), 所以存在這個關係 Rs-Air = 10 * R0. 或者 R0 = Rs-Air / 10, 也就是說在乾淨空氣下量測到感測器的阻抗 Rs-Air, 給它除以 10 就是這張 Chart 縱軸的阻抗單位 R0

 

MQ-2 SPEC02-2.png

 

 

另外一個重要的特性曲線, 可能是個大噩耗, MQ 系列的感測器, 對溫度/濕度是敏感的. 其特性曲線如下!

這下要哭哭了,  Rs/R0 的變化, 在不同的溫度濕度下, 可以從 0.8 變化到 1.7 如此巨大的差距. 所以假如要做精密量測, 需要考慮量測當時的溫度濕度才行, 要再多加個溫度跟濕度的即時量測來做修正. (還真麻煩勒, 抱頭!)

底下為 1,000 ppm 的氫氣在不同溫度濕度下量測的結果!  R0 定義為 33%相對濕度, 20C 溫度下的 Rs值. 可以看到, 相同氣體濃度下, 溫度跟濕度越高阻抗會下降.

MQ-2 SPEC03-2.png

 

 

MQ-7 一氧化碳感測模組:

這個氣體傳感器對主要用來偵測一氧化碳, 對一氧化碳氣體的靈敏度高,可檢測多種含一氧化碳的氣體. 售價約 NT$100 - NT$200 台幣,是一款適合多種應用的低成本傳感器.

來看一下它的規格書吧!

MQ-7 SPEC01-2.png

 

摘要一下規格書的要點:

1. 感測器電壓 Vc, 標準測試電壓為 5V +/- 0.1V

2. 必須提供兩種電壓 high/low: 5V/1.4V +/- 0.1V 給感測器內部的加熱器加熱! 交流直流皆可.  (所以加熱器電流額定值約為 5V / 33ohm = 152mA, 1.4V / 33ohm = 42.4mA) 如下圖所示, 加熱器在 High: 5V 高溫的時候需要維持 60sec, 主要功能是 Sensor 逸氣, 將 Sensor 內部氣體趕走. 雖然此時也可以偵測到一氧化碳CO的濃度, 但此時訊號會不太一樣! 加熱器在 Low: 1.4V 低溫的時候需要維持 90sec, 此時才是正常偵測是否有無一氧化碳氣體, 此時的訊號是最準確可信的!

3. 氣體感測濃度能力為 20 - 2,000 ppm. 感測器阻抗 Rs 於 2 - 20K ohm 間變動.

4. Preheat 時間超過 48 hours, 新 sensor 需燒機 Burn-In 超過 48小時才會穩定, 此時才是可用的.

 

MQ-7 SPEC04-2.png

 

 

特性曲線:

底下為正規化後的特性曲線, 於溫度 20C, 相對濕度 65%, 背景氧濃度 21%, RL = 10K ohm 下所量測得到的曲線. 注意, R0 為一氧化碳氣體在 100pmm 下所得到的 Rs 阻抗!

MQ-7 SPEC02-2.png

 

 

另外一個 Rs/R0 對溫度/濕度的特性曲線.

底下為 100 ppm 的一氧化碳在不同溫度濕度下量測的結果!  R0 定義為 33%相對濕度, 20C 溫度下的 Rs值. 可以看到, 相同氣體濃度下, 溫度跟濕度越高阻抗會下降.

MQ-7 SPEC03-2.png

 

 

 

 

根據這些知識, 來沙盤推演一下, 進一步推導可能的濃度公式吧!

1. 首先, 假定 x' = 濃度ppm, y' = Rs/R0, 我們知道它們在對數座標下遵守線性方程 y' = -a x' + b, 其中 a, b 為常數, a 為斜率, b 為截距

2. x', y' 為對數座標, x, y 為正常座標. 所以 x'=log x, y'=log y

3. 可以推得 x=濃度ppm 跟 y=Rs/R0 須遵守下面的方程式: y = 10^b x^(-a), or Rs/R0 = 10^b ppm^(-a)

formula01.png

最終濃度關係式

formula02.png

 

4. 我們來以廠商所提供的 MQ-7 濃度曲線, 用 Excel 來 Curve Fitting 一下, 把 a, b 常數求出來吧!

MQ7 Fitting Chart.png

a, b 常數如下

MQ7 constants.png

 

5. 如法炮製, 把 MQ-2 廠商所提供的所有氣體濃度曲線, 用 Excel 來 Curve Fitting 一下, 把 a, b 常數求出來吧! Chart 因為曲線較多, 就不附上了!

MQ2 constants.png

 

6. 繼續延伸囉, 把電路感測器的阻抗 Rs 跟我們會量測到的 Vout 電壓, 根據分壓定律放進來囉!

formula03.png

全部加進來

formula04.png

移項簡化, 得到濃度x=ppm 跟 Vout 的關係式囉!

formula05.png

我們的 Arduino, 類比至數位轉換器是 10 bits 的, 所以解析度 2^10 = 1024, 假設 Vout 以 Arduino ADC 讀入的數位讀值為 Din, 所以滿足下面關係式

formula06.png

丟進去, 得到最終的結果,

formula07.png

這裡的 R0是未知的, 但由原廠的數據來看, 在清淨空氣下量測, 感測器的阻抗R-air 大約會是 10 R0, 所以最終公式如下

formula08.png

要建構這個公式, 底下的參數都必須要知道:

1. 氣體常數 a, b

2. 電路裡的 RL

3. 須在乾淨空氣下, 把感測器的阻抗 R-Air 的數值先量測出來!

 

前面都是理論篇, 後面開始實做囉!

先秀一下我買到的感測器模組!

 

MQ-2 多重可燃氣體 (液化氣/丙烷/氫氣) 感測模組:

正面照片

IMG_2292.png

背面照片

IMG_2293.png

 

這個模組設想周到, 裡面有個電壓比較器, 所以可以不透過任何複雜微處理器的處理, 只要氣體濃度達到某個程度, 亦即當類比電壓Vout達到某個程度觸動電壓比較器於 DOUT 送出 +5V 的邏輯真的訊號, 直接看是要驅動蜂鳴器, 或是後續的邏輯閘或是計時器電路來觸動警報, 隨君號令!

假如要複雜一點, 就是透過 AOUT 這個接腳, 它的輸出就是 Vout 的電壓, 電壓的大小就是偵測到的氣體濃度. 0.1 V ~ 0.3V (相對無污染),最高濃度時電壓4V左右. 這裡我們的設計就是利用這隻接腳來跟 Arduino 的五個 10bits 的類比輸入(類比數位轉換器) 相結合. 將電壓 0-5V 轉成 0 - 1024 的濃度數值提供我們的程式運用.

接腳定義

GND: 接地

DOUT: 數位 digital I/O輸出, +5V 為 ture, 代表氣體濃度過高觸發警報. 0V 為 false, 代表正常. 可以調整電路板上的可變電阻來決定警報觸發的上限值.

AOUT: 0-5V 類比輸出, 對應感測到的氣體濃度.

VCC: 電源輸入, +5V

可惜呀, 因為這是現成的模組, 裡面的詳細電路未知. 根本不知電路裡的 RL 的數值為何? 所以前面根據 MQ-2 的廠商規格書所發展出來的絕對氣體濃度 ppm 的公式, 因為不知內部模組的實際電路跟 RL 參數跟感測器在不同狀態下 Rs, 所以無法驗證我們根據規格書所推導出的公式的參數正確性跟做任何的修正跟使用. 那些推導算是給大家參考啦! 下次真的要發展精密濃度量測的話, 要直接買感測器, 不要買模組, 需要根據規格書的記載來設計搭配的感測控制電路, 不要用現成的. 這樣所設計出來的 RL 才能確定, 也才能驗證不同氣體濃度下感測器 Rs 的表現, 廠商規格書上氣體濃度特性曲線的正確性, 然後進一步使用跟修正前面所發展出來的濃度公式計算氣體的真實濃度.

所以這裡後面最後實作出來的成品只會在三位 7段LED顯示器 上秀出 ADC 轉換器所轉換出來的氣體濃度數值, 但這樣的數值所代表的真實濃度為何? 我們就放棄囉. 反正大概的特性是 Vout = 0.1 V - 0.3V 時代表 乾淨且相對無污染. Vout = 4V, 代表最高濃度.

 

MQ-7 一氧化碳感測模組:

正面照片

IMG_2290.png

背面照片

IMG_2291.png

 

這個模組, 我覺得有點搞笑噎!

仔細端詳電路板, 這上面的電路設計應該是和 MQ-2 的電路類似的! 但假如你有詳讀過 MQ-7 的技術文件, 上面講得很清楚. 感測器內部的加熱器需要有兩個不同的工作週期, 第一個是 5V High 高溫加熱, 維持 60秒的氣體逸氣; 第二個是 1.4V Low 的低溫加熱, 維持 90秒的 CO感測. 兩者工作週期須交互穿插.

這樣的電路板, 沒看到任何計時器在上面, 所以不可能是照技術文件上所描述的, 以兩個不同電壓跟時間週期來加熱加熱器. 應該是以 MQ-2 的方式持續以 5V 來加熱, 然後透過感測器電阻的改變造成輸出電壓的改變來偵測氣體濃度!

以單一 5V High 的方式加熱感測器, 技術文件描述這其實是完全錯誤的方式呀! 哎! 模組都買下去了, 要叫我將感測器解焊下來, 重改電路是讓我有點無言, 只好就將錯就錯囉! (攤手) 下次再來做一個完全遵照原廠設計的版本吧!

 

沒有雙週期, 就簡單了, 一樣跟 MQ-2 的設計跟接腳 

AOUT: 0-5V 類比輸出, 對應感測到的一氧化碳濃度.

DOUT: 數位 digital I/O輸出, +5V 為 ture, 代表一氧化碳氣體濃度過高觸發警報. 0V 為 false, 代表正常. 可以調整電路板上的可變電阻來決定警報觸發的上限值.

GND: 接地

VCC: 電源輸入, +5V

 

跟 MQ-2 同樣的理由, 這裡後面最後實作出來的成品只會在三位 7段LED顯示器 上秀出 ADC 轉換器所轉換出來的氣體濃度數值, 但這樣的數值所代表的真實濃度為何? 我們就放棄囉. 反正大概的特性是 Vout = 0.1 V - 0.3V 時代表 乾淨且相對無污染. Vout = 4V, 代表最高濃度.

 

實際的成品, 一氧化碳及可燃瓦斯氣體暨煙霧雙偵測警報器.

 

先來個成品的照片吧!

發現空白的 Arduino Shield 跟麵包板在一起的搭配, 真的是絕配耶! 其實也不用強求一定要完整的動到烙鐵跟銲錫啊! 就把它當作樂高玩具, 直接使用 Arduino 空白 Shield 上面所附贈的迷你麵包板來配線, 採用杜邦接頭的短線也蠻方便的, 配線也蠻牢固的啦, 又兼顧了彈性. 真的要好好推廣這樣的組合喲!

 

所以就直接用有麵包版的 Arduino Shield 來實作囉! 完成品如下!

下面那層是方便好用 CP值高的 Arduino UNO. 為了防止短路, 有買了一個塑膠底套座套在 Arduino UNO 的下方. 上面那層就是有附上一小塊麵包版的 Arduino Shield, 所有零件跟配線就在這塊小麵包板上囉.

這張側面圖可以看到 MQ-2, 跟對面橘色的 MQ-7. 中間是三段七段LED顯示器, 跟三個限流電阻. 配線採用杜邦接頭直接在麵包板上配線.

IMG_2294.png

 

另外一邊看過來的側面圖, 可以看到 Shield 板子上內建兩個按鈕, 左邊是跟 Arduino 相連結的 reset 按鈕, 按下去會對 Arduino CPU 做 reset. 右邊那顆是內建預留給使用者使用的. 這裡拿來連接到 Arduino 的 digital I/O pin2 中斷使用. 按下去後會產生中斷訊號提供程式來切換 Gas (MQ-2) 跟 CO (MQ-7) 這兩個感測器的讀值顯示! 可以看到兩個按鈕間有個 LED, 上面有焊上一根杜邦接頭的藍色導線, 這個就是按鈕的中斷訊號線, 最後接到對面的 Arduino I/O pin2

IMG_2295.png

 

從另外一側看過來, 因為要發聲響來警示氣體濃度的強度, 所以中間黑色那顆是壓電晶體的微型喇叭! 

IMG_2296.png

 

功能介紹:

1. 一氧化碳偵測器 MQ-7 跟可燃氣體瓦斯煙霧偵測器 MQ-2 雙氣體偵測器裝備偵測.

2. MQ 系列的感測器是需要暖機的, 等待裡面的加熱器溫度穩定. 所以開機後設計 180秒暖機時間倒數. 倒數完畢後才會開始顯示讀值跟濃度警報. 

3. 一個三段七段 LED 顯示器, 透過 Arduino Shield 上的按鈕可以隨意切換跟即時顯示 由 MQ-7所傳回的一氧化碳濃度程度的讀值或是由 MQ-2所傳回的可燃氣體瓦斯煙霧濃度程度的讀值.

4. 即時警報, 警報聲音的大小跟頻率會隨著不同濃度而改變. 洩漏氣體濃度越濃, 警報聲音越大聲跟越急促. 使用者不用隨時盯著三段七段 LED 顯示器, 可以透過耳朵所聽到的聲響, 立即感受或察覺目前所發生的瓦斯或一氧化碳洩漏的嚴重程度.

5. Smart I/O, 接上電腦 USB Port後, 隨時透過 USB 以文字傳送更詳細的資訊供電腦端的程式使用.

 

執行起來的影片如下, 要說明一下, 這個影片在暖機 180秒的時候, 秒數是正數的, 會從 0秒 數到 180秒後開始工作. 後來覺得這樣有點蠢, 所以後面的程式碼已經改成倒數的. 開機暖機時, 會從 180秒慢慢倒數到0秒後開始運作. 影片就懶得重新拍攝了!

 

電路規劃:

這個電路跟部分顯示程式, 其實就是以前面筆者所發展的三位七段LED 顯示器的電路為主體, 電路是一模一樣的. 請移駕詳閱我的那篇 BLOG. 這裡只是利用沒用到的 Arduino Analog 介面, 選兩個 ADC (Analog to Digital Converter) channel 來做 MQ-2, MQ-7 的 Aout 類比電壓的讀取, 再利用一個 digital I/O pin 來控制喇叭發出警報聲響, 這樣一切就搞定囉!

LPG CO gas detector circuit.png

 

 

程式解說:

1. 3位7段 LED 顯示器

3位7段 LED 顯示器控制方面, 硬體接腳跟控制程式都跟這篇 BLOG 一模一樣. 字型表添加了這6個字 'G', 'A', 's', 'C', 'o', ' ', 用在當使用者按了按鈕來切換兩個不同感測器的顯示時, 會先顯示一下提示字元 'GAs' 或是 'Co', 來提醒使用者, 現在顯示的讀值是 Gas 瓦斯 (MQ-2)呢? 還是 Co 一氧化碳 (MQ-7).

const byte segs_data[17]={B01111110, B00110000, B01101101, B01111001, B00110011, B01011011, B01011111, B01110000, B01111111, B01110011, B00000001, B01011110, B01110111, B01011011, B01001110, B00011101, B0} ;

然後 prep_Char(digit, index, dot), 用來準備所要顯示的字元, digit 是三位7段 LED 顯示器的位置, 由左算起位置為 digit =1, digit=2, digit=3, 字形表的索引位置為 index, 要不要顯示小數點為 dot = true or false.

prep_LED(num), 用來準備所要顯示的整數數字 num

lightup_beep(), 程式裡用來點亮3位7段 LED 顯示器並觸動壓電喇叭造出特定聲音的函式. 程式裡必須不斷的呼叫這個函式, 維持住 三個LED恆亮的錯覺跟產生特定頻率的聲響. 

 

2. 感測器 MQ-2, MQ-7 的處理

很直接, 直接用 analogRead 讀取 ADC 所傳回來 0 - 1024 間代表氣體濃度訊號的數值,

  CO_Val = compensate(analogRead(CO_In),CO_scalar,CO_offset);
  Gas_Val = compensate(analogRead(Gas_In),Gas_scalar,Gas_offset);

 

有稍微測試了一下, 設定了底下兩個警告或是警報的濃度訊號

const int CO_warning_lim = 80;
const int CO_alarm_lim = 600;
const int Gas_warning_lim = 200;
const int Gas_alarm_lim = 500;

設計當感測器讀值小於 warming_lim 時, 一概視為正常. 不會發出任何警報, 此時 濃度 level 視為 零,  level = 0

當感測器讀值介於 warming_lim 跟 alarm_lim 之間時, 此時 濃度 level 根據它們在這兩個之間的比例, 切成 10等分, level = 1 - 10 (利用 map 函式來做這樣的對應分配的動作)

當感測器讀值大於 alarm_lim 之間時, 視為濃度 level 最大, level = 11

  int CO_level; CO 濃度 level
  int Gas_level; Gas 濃度 level
  
  if (CO_Val <= CO_warning_lim) CO_level = 0; 小於 
warming_lim 時, 濃度 level=0
  else if ((CO_Val > CO_warning_lim) && (CO_Val < CO_alarm_lim)) CO_level = map(CO_Val,CO_warning_lim,CO_alarm_lim,0,10); 利用map()切成10等分給濃度level
  else CO_level = 11;  濃度大於 alarm_lim, 濃度 level=11
  
  if (Gas_Val <= Gas_warning_lim) Gas_level = 0; 小於 
warming_lim 時, 濃度 level=0
  else if ((Gas_Val > Gas_warning_lim) && (Gas_Val < Gas_alarm_lim)) Gas_level = map(Gas_Val,Gas_warning_lim,Gas_alarm_lim,0,10); 利用map()切成10等分給濃度level
  else Gas_level = 11; 濃度大於 alarm_lim, 濃度 level=11

 

3. 警報聲音的產生

透過 beep(freq, cycle) 函式來產生警報聲音, 每呼叫一次就會以頻率 freq 觸動喇叭 cycle 次數.

先用 ttt = 340000 / freq 算出觸動喇叭後需要等待的週期(方波), 然後用個 do-loop 執行 cycle 次數. 這樣就可以產生一個特定頻率的聲音

void beep(unsigned long freq,unsigned int cycle)
{
  unsigned long ttt = 340000 / freq;
   for (unsigned int i=0;i<cycle;i++)
   {
     digitalWrite(speaker_out,HIGH);
     delayMicroseconds(ttt); 
     digitalWrite(speaker_out,LOW);
     delayMicroseconds(ttt);      
   }
}

聲音的頻率由兩個氣體的濃度的最高那個來決定, 濃度愈高, 聲音的頻率愈低, 人耳的關係對中頻的聲音較高頻感受為強! 所以當聲音頻率從高頻轉到中頻, 人耳感受會從弱慢慢轉強. 這樣就不用看 7段LED顯示器就可以由頻率的變化知道氣體目前洩漏的嚴重性.

聲音頻率的高低由 beep_level 來決定, beep_level 等於兩個氣體濃度 level, 濃度最濃的那一個.

  if (CO_level<Gas_level) beep_level = Gas_level;
  else beep_level = CO_level;

 

聲音頻率的高低由 beep_level 來決定, 當 beep_level = 0 則不發出聲音. beep_level > 0, 一樣利用 map()函式把 beep_level = 0 - 11 之間的數值, 對應到頻率 beep_freq 從 50000 - 1000

void lightup_beep()
{
...    
    unsigned long beep_freq = map(beep_level,0,11,50000,1000);
    if (beep_level>0) beep(beep_freq,5);
  }
}

 

4. 主程式迴圈, 中斷處理.

兩個重要的狀態變數 prev_state 跟 main_state, 當其為 0 表示七段LED顯示器顯示 Co 濃度, 當其為 1 表示七段LED顯示器顯示 Gas 濃度. 當有人按了按鈕會觸發中斷, main_state 會被改變. 檢查 prev_state 跟 main_state 的差異, 就可以察覺有人按了按鈕, 這時候要顯示 'Co' 或是 'Gas' 提醒字元於七段LED顯示器.

byte prev_state = 0;
volatile byte main_state = 0;
// 0 = Co, 1 = Gas

按鈕按下去後, 會觸發中斷跳到底下的中斷函式 sw_sense(), 判斷一下這次的中斷的時間跟上次的中斷的時間是不是差距有 250ms 以上, 有的話肯定是真的按鈕被真的按下去了, 而不是因為按鈕因為彈跳現象所造成的重複假象, 所以將 main_state 在 0 = Co, 1 = Gas 間切換.

void sw_sense()
{
  if (millis() - time > 250)
  {
    time=millis();
    main_state ++ ;
    if (main_state==2) main_state=0;
  }
}

另外一個重要常數是 time_interval, 程式每等到 time_interval 所定義的 ms 秒數後才會讀取一次偵測器的濃度讀值. 這裡是 1000 ms, 所以每秒一次.

unsigned long time_interval = 1000; 

 

主程式迴圈介紹如下,

 

void loop() {
...  
  CO_Val = compensate(analogRead(CO_In),CO_scalar,CO_offset);  讀Co感測器濃度訊號
  Gas_Val = compensate(analogRead(Gas_In),Gas_scalar,Gas_offset); 讀Gas感測器濃度訊號


...  透過 USB 序列埠傳給電腦, 假如有接電腦的話.  
...  濃度跟 level 的處理
...  透過 USB 序列埠傳給電腦, 假如有接電腦的話.

  unsigned long target_time = millis() + time_interval; 每 time_interval 後離開迴圈, 讀取感測器一次
  
  while (millis() < target_time)  7段LED顯示及喇叭發聲迴圈
  {
   if (prev_state!=main_state) 兩個不相等,代表有人剛按了按鈕產生了中斷,main_state被改變了,所以要顯示一下'Gas'或'Co'
   {
     switch(main_state) 根據 main_state, 顯示 'Gas' 或是 'Co' 於七段LED顯示器
     {
       case 0 : prep_Char(0,14,false);
                prep_Char(1,15,false);
                prep_Char(2,16,false);
                 break;
       case 1 : prep_Char(0,11,false);
                prep_Char(1,12,false);
                prep_Char(2,13,false);             
     }
     prev_state = main_state; 更新 prev_state 跟 main_state 一樣.
     for (int i=0;i<50;i++) lightup_beep();
   }
   else
   {
    switch(main_state) 根據 main_state, 決定是顯示 Co讀值還是 Gas讀值
    {
     case 0 : prep_LED(CO_Val);
               break;
     case 1 : prep_LED(Gas_Val);          
    }
   }
   
   lightup_beep();
  } 

}

 

 

 

完整程式列表:

 

//
// dual sensors MQ7:CO, MQ2: gas detector
// version: 0.5
//
// Frank Lin  2018.3.31
//


// 2016.3.12 V0.3 
// 

// 2017.2.13 V0.4
// add statistic data report, calibration constant

// 2018.3.31 v0.5
// add warmup time

 

 

// MQ-7 pin definations

#define CO_In A0
#define Gas_In A1
#define speaker_out A2


// 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

// time interval (mS) for measurement
unsigned long time_interval = 1000; 
unsigned long warmup_time = 180000;


// beep control
int beep_level;

// data structure 8bits = dp aa bb cc dd ee ff gg
// 0 to 9, '-', 'G', 'A', 's', 'C', 'o', ''
const byte segs_data[17]={B01111110, B00110000, B01101101, B01111001, B00110011, B01011011, B01011111, B01110000, B01111111, B01110011, B00000001, B01011110, B01110111, B01011011, B01001110, B00011101, B0} ;
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};

// calibration constants
const float CO_scalar=1;
const float CO_offset=0;
const float Gas_scalar=1;
const float Gas_offset=0;

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

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 prep_LED(int num)
{
  boolean neg = false;
  if (num<0) neg = true;
  num=abs(num);
  prep_Char(2,num % 10, true);
  num /=10;
  if (num==0) seg7[1] = 0;
  else prep_Char(1,num % 10, false);
  num /=10;
  if (neg) prep_Char(0,10, false);
  else 
  {
    if (num==0) seg7[0] = 0;
    else prep_Char(0,num % 10, false);
  }
}

void lightup_beep()
{
   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);
    
    unsigned long beep_freq = map(beep_level,0,11,50000,1000);
    if (beep_level>0) beep(beep_freq,5);
  }
}

// beep

void beep(unsigned long freq,unsigned int cycle)
{
  unsigned long ttt = 340000 / freq;
   for (unsigned int i=0;i<cycle;i++)
   {
     digitalWrite(speaker_out,HIGH);
     delayMicroseconds(ttt); 
     digitalWrite(speaker_out,LOW);
     delayMicroseconds(ttt);      
   }
}


// interrupt service

byte prev_state = 0;
volatile byte main_state = 0;
// 0 = Co, 1 = Gas

unsigned long time;

void sw_sense()
{
  if (millis() - time > 250)
  {
    time=millis();
    main_state ++ ;
    if (main_state==2) main_state=0;
  }
}


void setup() {
  // setup I/O
  pinMode(speaker_out, OUTPUT);
  
  // 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);
 
  // attach ISR
  attachInterrupt(0,sw_sense,FALLING);
 
  
  // setup serial port
  Serial.begin(9600);
  
  // startup time
  unsigned long startup_time = millis();
  
  Serial.print(" Warming Up...");
  while (millis() - startup_time < warmup_time)
  {
    prep_LED((int)((warmup_time - millis() + startup_time)/1000));
    beep_level=0;
    lightup_beep();
  }
  prep_Char(0,14,false);
  prep_Char(1,15,false);
  prep_Char(2,16,false);
  for (int i=0;i<50;i++) lightup_beep();
    
}


int CO_Val;
int Gas_Val;
const int CO_warning_lim = 80;
const int CO_alarm_lim = 600;
const int Gas_warning_lim = 200;
const int Gas_alarm_lim = 500;

// beep control

 

void loop() {
  // main for measurement test
  
  int CO_level;
  int Gas_level;
  
  CO_Val = compensate(analogRead(CO_In),CO_scalar,CO_offset);
  Gas_Val = compensate(analogRead(Gas_In),Gas_scalar,Gas_offset);
  
  Serial.print("CO Concentration: ");
  Serial.print(CO_Val);
  Serial.print(" Gas Concentration: ");
  Serial.print(Gas_Val);
  
  if (CO_Val <= CO_warning_lim) CO_level = 0;
  else if ((CO_Val > CO_warning_lim) && (CO_Val < CO_alarm_lim)) CO_level = map(CO_Val,CO_warning_lim,CO_alarm_lim,0,10);
  else CO_level = 11;
  
  if (Gas_Val <= Gas_warning_lim) Gas_level = 0;
  else if ((Gas_Val > Gas_warning_lim) && (Gas_Val < Gas_alarm_lim)) Gas_level = map(Gas_Val,Gas_warning_lim,Gas_alarm_lim,0,10);
  else Gas_level = 11;
 
  
  if (CO_level<Gas_level) beep_level = Gas_level;
  else beep_level = CO_level;
    
   Serial.print(" CO_Lvl:");
   Serial.print(CO_level);
   Serial.print(" Gas_Lvl:");
   Serial.println(Gas_level);

  unsigned long target_time = millis() + time_interval; 
  
  while (millis() < target_time)
  {
   if (prev_state!=main_state)
   {
     switch(main_state)
     {
       case 0 : prep_Char(0,14,false);
                prep_Char(1,15,false);
                prep_Char(2,16,false);
                 break;
       case 1 : prep_Char(0,11,false);
                prep_Char(1,12,false);
                prep_Char(2,13,false);             
     }
     prev_state = main_state;
     for (int i=0;i<50;i++) lightup_beep();
   }
   else
   {
    switch(main_state)
    {
     case 0 : prep_LED(CO_Val);
               break;
     case 1 : prep_LED(Gas_Val);          
    }
   }
   
   lightup_beep();
  } 

}

 

xxx

arrow
arrow

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