2015年10月7日 星期三

★ Arduino 光敏電阻測試

做完 Arduino 的溫溼度量測數據經 ESP8266 WiFi 模組上傳到物連網伺服器 Thingspeak 後, 我想氣候測量還有甚麼呢? 應該還有三個可以記錄的 :
  1. 亮度
  2. 風向 & 風速
  3. 雨量
這樣就可以形成一個小型測候站了. 這裡面我覺得雨量是比較麻煩的, 風向風速應該不難, 而亮度則是最簡單的, 因為只要用簡單又便宜的光敏電阻 (LDR, Light Dependent Resistor, 因以硫化鎘 CdS 製成, 故通常稱光敏電阻為 CdS) 即可測量. 它是一種可變電阻, 其電阻值依光線之強度而變化, 光度亮時電阻值變低, 暗時電阻值變高. 因此可以利用光敏電阻上壓降的變化來探知光度的強弱.

我參考了下列書籍的範例進行測試 :
  1. 葉難 : Arduino 範例分析與實作設計, 博碩出版, 4-1 節
  2. 楊明豐 : Arduino 最佳入門與應用, 碁峰出版, 8-3-4 節
  3. 孫駿榮等 : Arduino 一試就上手第二版, 碁峰出版, 4-6 節 
  4. 趙英傑, Arduino 互動設計入門超圖解第二版, 6-2 節 
  5. Michael Margolis, Arduino Cookbook, 歐萊禮出版, 6-2 節
亮度量測的原理是使用電阻分壓法, 即將一電阻 R 與光敏電阻 Rcds 串聯, 量取中間的分壓壓降 Vcds (通常是量取光敏電阻上的壓降), 然後將其接至 Arduino 的類比輸入腳量測光敏電阻之壓降 :

Vcds=Vcc*Rcds/(R+Rcds)

串聯電阻的值依光敏電阻大小而定, 若是較大顆的光敏電阻, 其電阻值較低, 則串聯電阻用 1K 即可; 若為一般較小顆的光敏電阻, 則用 4.7K 或 10K 串聯電阻, 電路圖如下所示 :


有些書的接法與上圖中的電阻上下相反 (如 "Arduino Cookbook" 與 "Arduino 範例分析與實作設計"), 即 10K 串聯電阻在下, 因此是量測串聯電阻的壓降. 這樣讀取值就會與亮度成正比, 因光度越亮, 在上的光敏電阻值越小, 壓降也越小, 接地的串聯電阻壓降則越大. 上圖則是越亮讀取值越小.

Arduino 的類比輸入 (Nano 是 A0~A7) 雖名為類比, 事實上輸入信號會被 Arduino 內建的類比至數位轉換電路轉成 0~1023 的數位值, 若 Vcc=5V, 則當輸入也是 5V 時 (全暗, 光敏電阻值最高), 用 analogRead() 讀取時將得到 1023. 由於串聯電阻多少會有一些壓降, 因此要出現 1023 的值必須光敏電阻在全暗環境下, 此時其電阻為 Mega 級, 會吃掉幾乎全部 5V 壓降.

範例 1 :

int value;  //類比輸入讀取值
int LED=13;
void setup() {
  pinMode(LED, OUTPUT);  //設定 DIO Pin 13 為 LED 輸出
  Serial.begin(9600);
  }

void loop() {
  value=analogRead(A0);  //參數也可用 0
  Serial.println(value);
  delay(1000);  //每秒讀取一次
  }

此範例會在序列埠監視視窗顯示所讀取到的光敏電阻壓降值 (0~1023), 每秒讀取一次.  在 "Arduino" 一試就上手這本書裡提到, 由於感測器會受到環境雜訊的干擾, 經過內建 AD 轉換後的 0~1023 數值有時會突然劇烈變動, 通常使用平均值法來處理這些雜訊, 使其對最後之數值輸出影響降低, 例如使用一個 10 次的迴圈連加量測值, 再除以 10 作為輸出值.

比較簡單的方式是採用濾波方式 (積分器), 只取前一次量測值與本次量測值做比例相加後得到輸出值, 如下列範例所示 :

範例 2 :

int value;  //類比輸入讀取值
int LED=13;
void setup() {
  pinMode(LED, OUTPUT);  //設定 DIO Pin 13 為 LED 輸出
  Serial.begin(9600);
  }

void loop() {
  value=analogRead(A0);  //參數也可用 0
  float new_value=0.3*value + 0.7*analogRead(A0);
  Serial.println(floor(new_value));
  delay(1000);  //每秒讀取一次
  }

此處輸出值採前次佔 3 成, 本次佔 7 成方式處理, 這樣數值變化經過積分器濾波後變化就不會那麼劇烈了.

另外, 在 "Arduino Cookbook" 這本書中則介紹了讓光度以 PWM 方式控制 Arduino LED 的閃爍頻率 :

範例 3 :

int value;  //類比輸入讀取值
int LED=13;
void setup() {
  pinMode(LED, OUTPUT);  //設定 DIO Pin 13 為 LED 輸出
  }

void loop() {
  value=analogRead(A0);
  digitalWrite(LED, HIGH);  //用量測值控制亮滅時間, 光度越強, 閃爍越快
  delay(value);
  digitalWrite(LED, LOW);
  delay(value);
  }

以上面的電路圖來說, 此程式光度越亮, 閃爍的頻率就越快. 如下面影片所示 :


光敏電阻最常應用在路燈自動開關上, 當白天太陽升起, 就將路燈關閉; 到了傍晚就開啟. 但是從光敏電阻讀取的數據, 其臨界值到底要用多少作為開啟與關閉的依據? 這要用 Arduino 實際做光度試驗後來決定, 例如大於 700 以上表示天色已暗, 則可用 if 來判斷 :

範例 4 :

int value;  //類比輸入讀取值
int LED=13;
void setup() {
  pinMode(LED, OUTPUT);  //設定 DIO Pin 13 為 LED 輸出
  }

void loop() {
  value=analogRead(A0);
  if  (value > 700) {
    digitalWrite(LED, HIGH);  //路燈開啟
    }
  else {
    digitalWrite(LED, LOW);  //路燈關閉
    }
  }

這裡只是點亮板上的 LED, 只要稍作修改, 改成輸出到控制繼電器的 DIO 腳, 就能真正控制路燈了. 但上面這程式只以一個臨界值做判斷, 當天色變暗剛好在臨界值附近變動時, 路燈就會有閃爍現象, 這可以用兩個臨界值來修正, 如下範例所示 :

範例 5 :

int value;  //類比輸入讀取值
int LED=13;
void setup() {
  pinMode(LED, OUTPUT);  //設定 DIO Pin 13 為 LED 輸出
  }

void loop() {
  value=analogRead(A0);
  if (value > 700) {
    digitalWrite(LED, HIGH);  //路燈開啟
    }
  else if  (value < 600) {
    digitalWrite(LED, LOW);   //路燈關閉
    }
  }

當量測值低於 700 時路燈仍會亮著, 直到低於 600 才會關閉; 若又彈回 600 以上仍是關閉, 直到高於 700 才會開啟. 總之, 雜訊必須每次都大於 100 才會產生閃爍情況, 機率就大大降低了. 設置寬度為 100 當緩衝區就可以消除閃爍情況了.

另外, 因為讀取到的電壓值是 0~1023 的數位化後的數值, 要如何反求光敏電阻上的壓降呢? 可以除以1023 再乘以 5 即得 :

Vcds=5*float(analogRead(A0))/1023

而光敏電阻值也可以從上面的分壓公式反求 :

Rcds=R*Vcds/(Vcc-Vcds)

另外值得一提的是, 在 "Arduino Cookbook" 與 "Arduino 最佳入門與應用" 這兩本書中介紹了一個 map() 函數, 可以把某個值域映射到另一個值域, 例如想將 0~1023 的數值映射到 0~100%, 就可以用 map(0,1023, 0, 100) 來完成.

綜合以上計算式, 我將上面範例 1 修改為下列範例 6 :

範例 6 :

int value;  //類比輸入讀取值
int LED=13;

void setup() {
  pinMode(LED, OUTPUT);  //設定 DIO Pin 13 為 LED 輸出
  Serial.begin(9600);
  }

void loop() {
  value=analogRead(A0);  //參數也可用 0
  float Vcds=5*float(value)/1023;  //光敏電阻壓降
  float Rcds=10000*Vcds/(5-Vcds);  //光敏電阻值
  int percent=map(value,0,1023,0,100);
  Serial.print("value=");
  Serial.print(value);
  Serial.print("(");
  Serial.print(percent);
  Serial.print("%)");
  Serial.print("\t");
  Serial.print("Vcds=");
  Serial.print(Vcds);
  Serial.print("\t");
  Serial.print("Rcds=");
  Serial.println(Rcds);
  delay(1000);  //每秒讀取一次
  }

序列埠輸出擷取如下 :

value=465(45%) Vcds=2.27 Rcds=8333.33
value=465(45%) Vcds=2.27 Rcds=8333.33
value=465(45%) Vcds=2.27 Rcds=8333.33
value=465(45%) Vcds=2.27 Rcds=8333.33
value=204(19%) Vcds=1.00 Rcds=2490.84
value=200(19%) Vcds=0.98 Rcds=2430.13
value=200(19%) Vcds=0.98 Rcds=2430.13
value=199(19%) Vcds=0.97 Rcds=2415.05
value=537(52%) Vcds=2.62 Rcds=11049.38
value=642(62%) Vcds=3.14 Rcds=16850.40
value=636(62%) Vcds=3.11 Rcds=16434.11

這裡百分比越大表示亮度越暗, 100% 就是全暗. 如果要倒過來, 就把 percent 用 100 去減即可 :

int luminance=100-map(value,0,1023,0,100); 

我把這個亮度也加入 Thingspeak 物聯網氣候觀測程式, 名稱從 DHT11 改為 Atmosphere, 標題也改為氣候觀測資料, 參考前篇 :



程式則改為 :

#include <SoftwareSerial.h>         //載入軟體串列埠函式庫
#include <DHT.h>                    //載入 DHT11 函式庫
#define DHTPIN 2                    //定義 DIO 腳 2 為 DHT11 輸入
#define DHTTYPE DHT11               //定義 DHT 型態為 DHT11

DHT dht(DHTPIN, DHTTYPE);           //初始化 DHT11 感測器
int LED=13;                         //宣告 DIO 腳 13 為 LED 輸出
String api_key="NO5N8C7T2KINFCQE";  //Thingspeak API Write Key
SoftwareSerial sSerial(10,11);      //設定軟體串列埠腳位 RX, TX為 DIO 腳 10, 11

void setup() {             
  pinMode(LED, OUTPUT);             //設定 DIO Pin 13 為 LED 輸出
  Serial.begin(9600);                //啟始硬體串列埠 (除錯用)
  sSerial.begin(9600);              //啟始軟體串列埠 (與 ESP8266 介接用)
  dht.begin();                      //啟始 DHT11 溫濕度感測器
  sSerial.println("AT+RST");        //軟體串列埠傳送 AT 指令重啟 ESP8266
  }

void loop() {
  int cds=analogRead(A0);  //讀取光敏電阻壓降數位化數據 0~1023
  int l=100-map(cds,0,1023,0,100); //亮度轉成 %
  float h=dht.readHumidity();         //讀取濕度
  float c=dht.readTemperature();      //讀取攝氏溫度
  float f=dht.readTemperature(true);  //讀取華氏溫度
  //有任何一個是 NAN 就不往下執行資料傳送
  if (isnan(l) || isnan(h) || isnan(c) || isnan(f)) {
    Serial.println("Failed to read from Atmosphere sensors!");
    return;
    }
  blink_led(1000,500); //進入資料傳送程序 : LED 閃爍一次
  //製作參數字串 
  String param="&field1=" + (String)c + "&field2=" + (String)f +
               "&field3=" + (String)h + "&field4=" + (String)l;

  //與 Thingspeak 主機建立 TCP 連線 (用 api.thingspeak.com 亦可)
  String cmd="AT+CIPSTART=\"TCP\",\"184.106.153.149\",80";
  sSerial.println(cmd); //向 ESP8266 傳送 TCP 連線之 AT 指令

  //偵測 TCP 連線是否成功
  if (sSerial.find("Error")) {         
    Serial.println("AT+CIPSTART error!");
    return;  //連線失敗跳出目前迴圈 (不做後續傳送作業)
    }
  Serial.println(cmd);  //輸出 AT 指令於監控視窗

  //製作 GET 字串
  String GET="GET /update?api_key=" + api_key + param + "\r\n\r\n";
  cmd="AT+CIPSEND=" + String(GET.length()); //傳送 GET 字串長度之 AT 指令
  sSerial.println(cmd);  //告知 ESP8266 即將傳送之 GET 字串長度
  Serial.println(cmd); //輸出 AT 指令於監控視窗

  //檢查 ESP8266 是否回應
  if (sSerial.find(">")) {  //若收到 ESP8266 的回應標頭結束字元
    sSerial.print(GET);  //向 ESP8266 傳送 GET 字串內容
    Serial.println(GET);  //顯示 GET 字串內容於監控視窗   
    }
  else {  //沒有收到 ESP8266 回應
    sSerial.println("AT+CIPCLOSE");  //關閉 TCP 連線
    Serial.println("AT+CIPCLOSE");    //顯示連線關閉訊息於監控視窗
    }
  delay(16000);  //延遲 16 秒 (因 Thingspeak 每次更新須隔 15 秒)
  }

void blink_led(int on, int off) {  //LED 閃爍函式
  digitalWrite(LED, HIGH);
  delay(on);
  digitalWrite(LED, LOW);
  delay(off);
  }

亮度的量測數據圖如下 :


觀察 Thingspeak 上收集的數據後, 我發現亮度在 10% 以下就算是昏暗了, 可以點亮路燈; 20% 以上大約是東方魚肚白程度, 可以關閉路燈. 公開的資料觀察連結如下 :

# https://api.thingspeak.com/channels/47985/feed.json?key=PWEAKSDX8X8QP0V5

OK, 大功告成!


9 則留言:

  1. 你為什麼不把arduino uno 也丟掉
    只用8266就好
    真是見樹不見林

    回覆刪除
  2. 嗯, 之前的文章我有提到過, ESP8266 潛力很大, 確實可以取代 Arduino, 但是因為我主要是想把 Arduino 摸熟後再集中心力於 ESP8266, 畢竟 Arduino 是創客最愛的 MCU 啊! 很多範例都是用 Arduino 做的. 而且最便宜的 ESP-01 模組只有一個 GPIO2 可用, 只能連接單一感測器. ESP8266 目前只是我用來聯網的工具而已. 不過 UNO 這麼大顆, 確實已經被我冰起來囉, 我現在都只用 NANO 做實驗. 謝謝您的高見.

    回覆刪除
  3. 想請問老師 如果我要將三個光敏電阻也就是Arduino顯示的數據 利用Nodemcu上傳到thingspeak 因為網路上沒有相關資料 所以我利用濕度得下去改 請問哪邊有誤呢 或者這樣是不可行的 謝謝


    #include

    String apiWritekey = "IUI14WAO2THGE8XU"; // replace with your THINGSPEAK WRITEAPI key here
    const char* ssid = "302"; // your wifi SSID name
    const char* password = "0" ;// wifi pasword

    const char* server = "api.thingspeak.com";
    float resolution=3.3/1023;// 3.3 is the supply volt & 1023 is max analog read value
    WiFiClient client;

    void setup() {
    Serial.begin(115200);
    WiFi.disconnect();
    delay(10);
    WiFi.begin(ssid, password);

    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    }
    Serial.println("");
    Serial.print("NodeMcu connected to wifi...");
    Serial.println(ssid);
    Serial.println();
    }

    void loop() {
    float cmd = analogRead(A0) ;
    if (client.connect(server,80))
    {
    String tsData = apiWritekey;
    tsData +="&field1=";
    tsData += String(cmd);
    tsData += "\r\n\r\n";

    client.print("POST /update HTTP/1.1\n");
    client.print("Host: api.thingspeak.com\n");
    client.print("Connection: close\n");
    client.print("X-THINGSPEAKAPIKEY: "+apiWritekey+"\n");
    client.print("Content-Type: application/x-www-form-urlencoded\n");
    client.print("Content-Length: ");
    client.print(tsData.length());
    client.print("\n\n"); // the 2 carriage returns indicate closing of Header fields & starting of data
    client.print(tsData);

    Serial.print("Temperature: ");
    Serial.print(cmd);
    Serial.println("uploaded to Thingspeak server....");
    }
    client.stop();

    Serial.println("Waiting to upload next reading...");
    Serial.println();
    // thingspeak needs minimum 15 sec delay between updates
    delay(15000);
    }

    回覆刪除
  4. 出現甚麼錯誤呢? 我目前無暇作測試. thingspeak 上傳三個變數是沒問題的, 就是 field1, field2, filed3 而已.

    回覆刪除
  5. 不好意思我想請問老師,
    我寫了一個偵測土壤濕度想用MQTT存到thingspeak。
    可是遇到一個問題
    就是只要我使用到WiFi.begin 這樣我偵測到的值都會有問題會是什麼問題呢?
    我是用esp32

    回覆刪除
  6. MQTT message : field1=25&field2=62&field3=1366.00
    MQTT message : field1=25&field2=62&field3=4095.00

    前兩個是 DHT11的溫溼度 後面一個是土壤濕度檢測的值

    兩段的差異只有我有沒有把WiFi.begin註解掉

    回覆刪除
  7. 抱歉, 不太清楚您的問題, 可以 POST 較完整的程式碼嗎?

    回覆刪除
  8. 範例6中,計算光敏電阻值的公式
    float Rcds=10000*Vcds/(5-Vcds); //光敏電阻值

    分子分母是不是反了呢?
    應該是:float Rcds=10000*(5-Vcds)/Vcds;

    從串聯分壓公式來看I=V1/R1=V2/R2
    R1=R2*V1/V2=R2*V1/(Vt-V1)

    此外,原公式計算的結果,亮反而電阻值高、暗反而電阻值低,也不符合光敏電阻的特性

    回覆刪除
  9. 從串聯分壓公式來看沒有錯
    但為什麼亮反而電阻值高、暗反而電阻值低@@

    回覆刪除