2016年11月3日 星期四

20W 太陽能板安裝測試 (一)

去年買的 20W 小型太陽能板最近準備安裝在鄉下的豬舍洗手間上面的平台, 過去三周假日空閒時就利用萬用角鐵製作了一個支架, 以便將太陽能板鎖在平台上. 由於之前買的蓄電池與充控被我拿去給風力發電機用了, 所以上周還跟露天賣家 psa0958 另外買了兩個 10A 充電控制器與一個 7AH 的蓄電池, 參考 :

# 購買充電控制器與 7AH 電池

這個小型太陽能板只是為了用來進行我的綠能初步實驗而已, 小小的 20W 並無多大實際用途. 我打算將其安裝在菜園旁的豬舍, 目的是晚上去豬舍上廁所時會自動感應照明, 或者到菜園倒廚餘時能有燈光照亮菜園一角. 去年曾向露天賣家iq1189 買了幾個 3W 12V LED 投射燈就是為了這個用途, 參考 :

# 關於小型風力發電機的負載與控制器
# MR16 3W LED燈泡 投射燈 成品 12V LED 杯燈 燈杯 崁燈 取代50W鹵素燈杯 買10送1 $48

當然要加上一個 PIR 紅外線移動偵測模組來感應是否有人接近. 另外因 LED 燈為 12V, 必須使用一個繼電器來隔離 Arduino 的 5V 系統, 而要將蓄電池的 12V 供電給 Arduino 也必須有一個 5V 的穩壓模組, 剛好去年跟露天賣家盼盼 (ccdogccdogkimo) 買的一批零件剛好可以派上用場 :

# 向露天賣家盼盼購買零件模組一批
# KIS3R33S DC-DC 降壓電源模組 輸入7V-24V 轉 5V USB輸出3A $75
# 1路 繼電器 模組 5V 支援 高低電位 觸發 帶 光耦隔離 Relay Arduino可 $34

除了照明與移動偵測功能外, 我也順便加上基本氣候偵測功能, 包括溫溼度與照度, 這只要一個 DHT11 與光敏電阻就可以了. 關於溫溼度與照度做法參考下列文章 :

Arduino 溫濕度感測器 DHT11 測試
以 Arduino+ESP8266 物聯網模組重作 DHT11 溫溼度實驗
Blynk 應用 (二) : 在手機顯示即時溫溼度與光度

整個電路架構如下圖所示, 充電控制器有三組輸入, 左方兩組分別接太陽能板與蓄電池, 最右邊為 12V 負載, 經 12V-5V 降壓模組供電給 Arduino 與 PIR, DHT11 等模組; 另外 12V 直接連到 LED 燈再接繼電器的 NO 端 :


在上面電路圖中 DHT11 旁邊有兩個元件沒畫出來 : VCC 與 Data 腳之間要跨接一個 10K 電阻 (顏色是棕黑橙); 另外 VCC 腳接一個 0.1uF (104) 電容到 GND. DHT11 的輸出則接至 Arduino 的 D5 腳. 光敏電阻與一個 10K 電阻串聯後中間接點接至 Arduino 的 D4, 而 Arduino 的 D6 輸出信號控制繼電器的常開接點 (NO), 當輸出 HIGH 時會閉合, 使 12V 3W LED 形成迴路而點亮.

硬體部分接線完成後, 先來測試以 PIR 模組偵測物體移動後透過繼電器控制 LED 明滅, 程式參考下面這篇 :

Arduino 測試 : PIR 紅外線移動偵測 (二)


測試 1 :

const byte pirPin=2; //PIR ouput connected to Arduino D2
const byte ledPin=13;
const byte relayPin=6;
int highDelay=10000; //high delay time (ms)
int triggerCount=0; //interrupt counter
int triggerLimit=3; //cieling of trigger Counter
volatile boolean state=LOW; //initial value of output

void setup() {
  pinMode(pirPin, INPUT);
  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  Serial.begin(9600);
  attachInterrupt(0, int0, RISING); //assign int0
  }

void loop() {
  Serial.println(triggerCount);
  if (state == HIGH) {
    digitalWrite(ledPin, HIGH); //set HIGH
    digitalWrite(relayPin, HIGH);
    delay(highDelay); //lasting HIGH output
    --triggerCount;
    if (triggerCount <= 0) {
      digitalWrite(ledPin, LOW); //HIGH timeout
      digitalWrite(relayPin, LOW);
      state=LOW; //reset state
      }
    }
  }

void int0() { //interrupt handler
  state=HIGH; //set state
  if (triggerCount <= triggerLimit) {++triggerCount;}
  }

此程式利用 PIR 輸出去觸發 Arduino 外部硬體中斷來控制 LED 明滅, 這裡使用一個隨每次觸發而增量的變數 triggerCount 來使持續移動時 LED 能繼續保持明亮, 而不是一明一滅切換. 但所記錄的累計觸發次數受到 triggerLimit 變數的限制而碰頂, 避免連續的觸發使 triggerCount 太大, 使得停止移動後須等很久才熄滅.

我在降壓模組串接一個電壓電流表模組, 測試發現 LED 燈熄滅時耗電約 50mA, 點亮時則大約 110mA, 可見此 3W LED 耗電約 50mA 左右 :




接下來要來實驗用光敏控制燈光的明滅, 模擬路燈在傍晚陽光漸暗時開燈, 早上晨光乍現時關閉的自動控制. 關於光敏電阻用法參考 :

# Arduino 光敏電阻測試


測試 2 :

const byte ledPin=13;
const byte relayPin=6;
const byte onLevel=20; //0 (dark)~100 (bright)
const byte offLevel=30; //0 (dark)~100 (bright)

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  }

void loop() {
  int cds=analogRead(A0); //get cds voltage (0~5V to 0~1023 dark)
  int Luminance=100-map(cds,0,1023,0,100); //map 0~1023(dark) to 0(dark)~100%
  Serial.print("Luminance=");
  Serial.println(Luminance);
  if (Luminance < onLevel) {
    digitalWrite(relayPin, HIGH);  //turn on
    digitalWrite(ledPin, HIGH);
    }
  else if  (Luminance > offLevel) {
    digitalWrite(relayPin, LOW);   //turn off
    digitalWrite(ledPin, LOW);
    }
  delay(100);
  }

此程式從 A0 讀入光敏電阻與 10K 電阻的分壓後, 利用 map() 函數將其由 0~1023 值域轉成 0~100 值域, 這樣得到的值就與光度成正比了, 即 0 為最暗; 100 為最亮. 這裡我們使用兩個臨界值 onLevel 與 offLevel 建立開與關動作的緩衝區, 當亮度低於 onLevel 時點亮 LED 燈; 當亮度高於 offLevel 時熄滅 LED 燈.

我在睡覺前將室內燈全部熄滅, 讓3W LED 燈亮整晚, 第二天早上發現燈還亮著, 電池電壓從 12.6V 降到 12.3V, 亮了 6 個小時壓降 0.3V. 當然, 為了避免電池過度放電 (我就是想知道電池能撐多久才做此實驗的啊), 電池必須透過充電控制器才接上負載 :


接下來我在上面測試 2 的基礎上添加物聯網功能, 將光度資訊紀錄在 ThingSpeak 網站上. 我使用自行焊接的 Arduino+Nano IOT 模組來連上 Internet, 參考 :

# 製作 Arduino Nano + ESP8266 物聯網模組 (四) : 完結篇

在之前的 ThingSpeak 實驗裡我已經設定好 field4 為紀錄照度的欄位, 參考 :

以 Arduino+ESP8266 物聯網模組重作 DHT11 溫溼度實驗

測試 3 :

#include <SoftwareSerial.h>
#define DEBUG true

const byte ledPin=13;
const byte relayPin=6;
const byte onLevel=20; //0(dark)~100(bright)
const byte offLevel=30; //0(dark)~100(bright)
const String ssid="EDIMAX-tony";
const String pwd="1234567890";
SoftwareSerial esp8266(7,8); //(RX,TX)
String api_key="NO5N8C7T2KINFCQE";  //Thingspeak API Write Key

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  }

void loop() {
  int cds=analogRead(A0); //get cds voltage (0~5V to 0~1023 dark)
  int Luminance=100-map(cds,0,1023,0,100); //map 0~1023(dark) to 0(dark)~100%
  Serial.print("Luminance=");
  Serial.println(Luminance);
  if (Luminance < onLevel) {
    digitalWrite(relayPin, HIGH);  //turn on
    digitalWrite(ledPin, HIGH);
    }
  else if  (Luminance > offLevel) {
    digitalWrite(relayPin, LOW);   //turn off
    digitalWrite(ledPin, LOW);
    }
  String data="GET /update?api_key=" + api_key + "&field4=" +
              (String)Luminance + "\r\n";
  Serial.println(data);
  toThingSpeak(data);
  delay(15000);   //ThingSpeak update rate limit : over 15 seconds
  }

void toThingSpeak(String data) {
  String cmd="AT+CIPSTART=\"TCP\",\"184.106.153.149\",80\r\n";
  sendData(cmd, 1000, DEBUG);
  cmd="AT+CIPSEND=" + (String)data.length() + "\r\n";
  sendData(cmd,1000,DEBUG);
  String res=sendData(data, 2000, DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("Unlink") == -1) { //if no auto unlink
    sendData("AT+CIPCLOSE\r\n",2000,DEBUG); //close session
    }
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),8000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

這裡我自訂了一個 roThingSpeak() 函數來更新 ThingSpeak 伺服器上的相關欄位. 注意, 迴圈週期是每 15 秒鐘更新一次, 根據 ThingSpeak 文件, 每一個資料通道 channel 的更新週期必須大於 15 秒, 參考 :

https://thingspeak.ubirch.com/docs/channels

"The open service via ThingSpeak.com has a rate limit of an update per channel every 15 seconds. This limit is so that the service can remain free and give everyone a high-level of service. The API source will also be made available on GitHub so that you can run this locally or via a shared web host provider. At that point you will be able to to tweak settings for your application requirements."

因為 ThingSpeak 最快允許 15 秒更新週期的限制, 此程式比上面測試 2 反應速度要慢, 我在室內測試時, 將室內燈關閉後, 要等 15 秒程式才會偵測到光度已經變暗, 才會將 3W LED 燈點亮.


序列埠監控視窗擷取訊息如下 :


AT+CIPSEND=48

> GET /update?api_key=NO5N8C7T2KINFCQE&field4=40

SEND OK

+IPD,5:27391
OK
AT+CIPCLOSE


OK
Unlink
Luminance=40
GET /update?api_key=NO5N8C7T2KINFCQE&field4=40

AT+CIPSTART="TCP","184.106.153.149",80

bB�鑭b禔S��"愃L�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+CIPCLOSE


ERROR
Luminance=41
GET /update?api_key=NO5N8C7T2KINFCQE&field4=41

AT+CIPSTART="TCP","184.106.153.149",80

no ip
AT+CIPSEND=48

link is not
GET /update?api_key=NO5N8C7T2KINFCQE&field4=41


ERROR
AT+CIPCLOSE


ERROR
Luminance=40
GET /update?api_key=NO5N8C7T2KINFCQE&field4=40

AT+CIPSTART="TCP","184.106.153.149",80

no ip
AT+CIPSEND=48

link is not
GET /update?api_key=NO5N8C7T2KINFCQE&field4=40


ERROR

從上面所擷取的訊息可知, 有時候 ESP8266 會莫名其妙 Reset, 但又不完全, 導致後面指令出現 ERROR 或 busy. 我在想, 是不是有辦法在出現這種現象時對 Arduino 進行暖開機呢? 有的, 參考下列文章 :

two ways to reset arduino in software

我採用了此文中的第二種方法, 在上面測試 3 的基礎上添加暖開機功能如下 :

測試 4 :


#include <SoftwareSerial.h>
#define DEBUG true

const byte ledPin=13;
const byte relayPin=6;
const byte onLevel=20; //0(dark)~100(bright)
const byte offLevel=30; //0(dark)~100(bright)
const String ssid="EDIMAX-tony";
const String pwd="1234567890";
SoftwareSerial esp8266(7,8); //(RX,TX)
String api_key="NO4N7C6T1KINFCQE";  //Thingspeak API Write Key

void(* resetFunc) (void)=0; //declare reset function at address 0

void setup() {
  Serial.begin(9600);
  esp8266.begin(9600);
  sendData(F("AT+RST\r\n"),2000,DEBUG); // reset ESP8266
  while (!connectWifi(ssid, pwd)) {
    Serial.println(F("Connecting WiFi ... failed"));
    delay(2000);
    }
  sendData(F("AT+GMR\r\n"),1000,DEBUG);
  delay(3000); //wait for wifi connection to get local ip
  sendData(F("AT+CIFSR\r\n"),1000,DEBUG); //get ip address
  pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  }

void loop() {
  int cds=analogRead(A0); //get cds voltage (0~5V to 0~1023 dark)
  int Luminance=100-map(cds,0,1023,0,100); //map 0~1023(dark) to 0(dark)~100%
  Serial.print("Luminance=");
  Serial.println(Luminance);
  if (Luminance < onLevel) {
    digitalWrite(relayPin, HIGH);  //turn on
    digitalWrite(ledPin, HIGH);
    }
  else if  (Luminance > offLevel) {
    digitalWrite(relayPin, LOW);   //turn off
    digitalWrite(ledPin, LOW);
    }
  String data="GET /update?api_key=" + api_key + "&field4=" +
              (String)Luminance + "\r\n";
  Serial.println(data);
  toThingSpeak(data);
  delay(15000);
  }

void toThingSpeak(String data) {
  String cmd="AT+CIPSTART=\"TCP\",\"184.106.153.149\",80\r\n";
  sendData(cmd, 1000, DEBUG);
  cmd="AT+CIPSEND=" + (String)data.length() + "\r\n";
  sendData(cmd,1000,DEBUG);
  String res=sendData(data, 2000, DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  Serial.println("===" + res + "===");
  if (res.indexOf("Unlink") == -1) { //if no auto unlink
    sendData("AT+CIPCLOSE\r\n",2000,DEBUG); //close session
    }
  if (res.indexOf("busy") != -1 || res.indexOf("ERROR") != -1) {
    Serial.println("*****busy/ERROR*****");
    resetFunc();
    }
  Serial.println(res.indexOf("busy"));
  Serial.println(res.indexOf("ERROR"));
  }

boolean connectWifi(String ssid, String pwd) {
  String res=sendData("AT+CWJAP=\"" + ssid + F("\",\"") + pwd + F("\"\r\n"),10000,DEBUG);
  res.replace("\r\n",""); //remove all line terminator
  if (res.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

String sendData(String command, const int timeout, boolean debug) {
  String response="";
  esp8266.print(command); // send the read character to the esp8266
  long int time=millis();
  while ((time+timeout) > millis()) {
    while(esp8266.available()) {
      // The esp has data so display its output to the serial window
      char c=esp8266.read(); // read the next character.
      response += c;
      }
    }
  if (debug) {Serial.print(response);}
  return response;
  }

上面藍色部分即為新加的 Arduino 暖開機程式碼, 它在位址 0 定義了一個 restFunc() 函數, 當 ESP8266 出現 ERROR 或 busy 時就呼叫它, 回到位址 0 執行, 相當於我們按下 Reset 鈕一樣.

序列埠監控視窗擷取訊息如下 :

GET /update?api_key=NO5N8C7T2KINFCQE&field4=45


ERROR
===GET /update?api_key=NO5N8C7T2KINFCQE&field4=45
ERROR===
ATAT+RST


OK
bB�鑭b禔S��"兀L�侒��餾�
[System Ready, Vendor:www.ai-thinker.com]
AT+CWJAP="EDIMAX-tony","1234567890"


OK
AT+GMR

0018000902

OK
AT+CIFSR

192.168.2.101

OK
Luminance=40
GET /update?api_key=NO5N8C7T2KINFCQE&field4=40

AT+CIPSTART="TCP","184.106.153.149",80


OK
Linked
AT+CIPSEND=48

> GET /update?api_key=NO5N8C7T2KINFCQE&field4=40

SEND OK

可見檢查 ESP8266 回應訊息發現有 ERROR 後, 成功地重啟 Arduino 了.


參考 :

# NodeMCU Tutorial 1:NodeMCU + DS18S20 + Thingspeak + MATLAB
# 葉難: Arduino一個好用的計時器程式庫


3 則留言 :

Unknown 提到...

您的Blog函蓋的幾個方向的研究我都有很興趣,幾乎快從頭看到尾。
我是在google Arduino時看到您的blog。
這一篇裡您提到「為了避免電池過度放電, 電池必須透過充電控制器才接上負載」,
我有點看不太懂。意思是負載是接在充電控制器上、電池也接在充電控制器上的意思嗎?

小狐狸事務所 提到...

感謝您, 如果直接將電池接負載, 若電池持續放電到低於終止電壓 10.5v 時會變成深度放電,這樣會使電池壽命減少. 充電控制器可以在電池接近終止電壓時切斷輸出以保護電池. 充電控制器有六個端點, 兩個是輸入端接能板, 另兩個接電池, 最後兩個是皆負載, 負載與電池是分開的.

Unknown 提到...

您的說明很仔細,我了解了~ 感謝。 ^^~