ソーラパネルで発電し、バッテリーに充電。充電した電気で夜間LEDを点灯する。天気が良ければずっとこれを繰り返す。そんな機器を作ろうと思っています。どこから始めようかと思ったのですが、パネルやバッテリー等の機器を評価できる測定器から始める事にしました。
製作したい機器の最終形態が分からないので測定器への要求も当然未定なのですが、とりあえず
- CPUはESP32を使用する。
- 特別な理由は無いのですが、今手持ちにこのCPUが有るから。
- 測定したデータを保存出来るようにする。
- ESP32は内部メモリにデータを保持出来ますが、測定値のように書き換える頻度が高い場合、内部メモリでは不利と思い今回はSDカードをサポートする事にしました。
- 測定時間は専用のRTCを使用する。
- ESP32はRTCを持っていますが、専用のRTCを使用してボタン電池でバックアップすれば、本体の電源を切っても時間は保持され便利です。
- 電源は12Vのバッテリーからも取れるようにする。
- ソーラパネル等の評価は外でも行う可能性が高い。
- その場合ESP32の電源をバッテリーからとれるようにすると便利
- 電圧測定用にADコンバーターを持つ
- ESP32はAD変換機能が有るのでそれを利用する。
- 温度湿度も測定出来るようにする。
- 使用する予定のRTCがI2Cなので、I2Cで動作する温度湿度を測定出来る素子をついでにサポートする。
以上をもとに、以下の回路を書いてみました。
- CPU
- 秋月電子さんで下記の部品を買って自分で半田付けしました。
- かなり難しかったです。完成品 を買った方が無難です。
- このESP32はUSB->シリアルの変換機能が有りません。PCとESP32をつなぐには変換機が必要です。
- ESP32のBoot,ENの回路も必要です。
- SDカード
- KKHMF SDカードスロットソケットリーダーモジュールArduino用 [並行輸入品]
- インターフェイスは、”SPI”です。
- 電源、CSを合わせて6本の線のみで接続出来ます。
- RTC
- RTC DS3231を使用しました。
- ”I2C”のSCL、SDA信号線が4.7Kオームでプルアップされています。
- この他にも、”I2C”の素子を付ける予定なのでこのプルアップ抵抗を外して使用しています。
- また、PowerLEDもバッテリ駆動時の消費電力を考えて外しました。(そんなに大きな電流では無いので外さなくても良いかも)
- バックアップのボタン電池には、充電可能はLIR2032では無くCR2032(充電タイプでは無い通常品)を使用しています。
- これはVccが3.3Vでボタン電池が充電されないと判断した為です。心配な場合は、ダイオードが抵抗を外して下さい。
- 電源は12Vのバッテリーから3.3Vを取る
- 低損失三端子レギュレーター 3.3V500mA NJU7223DL1 (4個入)
- 入力電圧は14Vまで。今回は入力が12VのバッテリですのでOK。
- 最大出力電流500mAがちょっと心配。
- スイッチでバッテリーと外部電源(3.3V)を切り替えて使用できる様になっています。
- 電圧測定用ADコンバーター
- ESP32は、0〜3.6Vを12ビットでAD変換します。
- 100Kと22Kの抵抗を直列につないだ間の電圧は、全体の22/(100+22)となります。ここの電圧を3.6Vとすると、全体は約20Vになります。
- つまりこの回路でMAX20Vまで測定出来事になります。
- AD変換器は3つ用意し、1つはバッテリー電圧測定用に固定しました。
- 温度湿度の測定
- AM2320を使うを使用しました。
- これは温度と湿度を測定出来る素子です。
- 今回は使用していませんが温度だけなら、ADT7410を使う等の使用を考慮して、”I2C”用のコネクタを4個用意しました。
- LED
- ESP32に電源が供給された時に点灯するもの。動作確認用に2個のLEDのを配線しています。
- これらのLEDはバッテリー駆動時の消費電力を考えてソケットに差し込んで取り外し出来る様にしています。
実際の配線は下記の様になりました。
選んだ基板が小さかったので縦に積み上げて実装しています。一番右が完成品です。
次回は制御用のソフトの説明です。ESP32はArduino IDEを使用してソフトを書きます。まずはメインのスケッチ。HTTPサーバを上げて、Web画面からハードを操作します。
#include "Arduino.h"
#include "SD.h"
#include <Wire.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include "AM2320.h"
#include <DS3231.h>
// SD Card select PIN
#define sd_ss 5
/*
#define sd_sck 18
#define sd_mosi 23
#define sd_miso 19
*/
// AD Conversion R
#define ad_r1 100
#define ad_r2 22
// AD Conversion IO
#define ad_00 36
#define ad_01 39
#define ad_02 34
// Pilot Lamp
#define Pilot_Lamp 13
// DS3231 Flg
#define every_s 0x0f //Alarm once per second
#define match_s 0x0e //Alarm when seconds match
#define match_ms 0x0c //Alarm when min, sec match
#define match_hms 0x08 //Alarm when hour, min, sec match
#define match_dhms 0x00 //Alarm when date, h, m, s match
#define match_whms 0x00 //Alarm when DoW, h, m, s match
#define alarm1 1
#define alarm2 2
int led_stat;
WebServer server(80);
const char *SSID = "XXXXXXXX";
const char *PASSWORD = "YYYYYYYY";
DS3231 Clock;
byte tm_data[10]; //Year,Month,Date,DoW,Hour,Minute,Secon
void setup()
{
pinMode(Pilot_Lamp, OUTPUT);
digitalWrite(Pilot_Lamp, LOW);
Serial.begin(115200);
delay(500);
Wire.begin();
SPI.begin();
SD.begin(sd_ss);
init_DS3231();
Serial.println("Connecting to WiFi");
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
delay(500);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
delay(1000);
// Try forever
while (WiFi.status() != WL_CONNECTED) {
Serial.println("...Connecting to WiFi");
delay(1000);
}
Serial.println("Connected");
Serial.println(SSID);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
if (MDNS.begin("esp32solar")) {
Serial.println("MDNS responder started");
}
server.on("/", handleRoot);
server.onNotFound(handleWebRequests);
server.begin();
Serial.println("HTTP server started");
digitalWrite(Pilot_Lamp, HIGH);
}
void loop()
{
server.handleClient();
}
void handleRoot() {
String buf,cmd;
int a,b,fl;
File dataFile;
float h_data[2];
uint8_t para[7];
fl=1;
cmd=server.argName(0);
switch(cmd.toInt())
{
case 1: // Update Data
break;
case 99: // Back or Cansel
break;
case 2: //Set TIME (send url data)
dataFile = SD.open("/Set_Time.html", FILE_READ);
server.streamFile(dataFile,"text/html");
dataFile.close();
fl=0;
break;
case 80: // Send Solar parameter
buf=get_current_time() + ",";
AM2320(h_data);
buf += ("Temp:" + String(h_data[1]) + "'c / Humi:" + String(h_data[0]) +"%,");
buf += (String(get_volt(ad_00)) + "v / " + String(get_volt(ad_01)) + "v / " + String(get_volt(ad_02)) + "v,");
server.send(200, "text/plain", buf);
fl=0;
break;
case 81: // Send RTC parameter
get_DS3231(tm_data);
buf = "";
for(a = 0; a < 7; a ++) buf += (String(tm_data[a]) + ",");
server.send(200, "text/plain", buf);
fl=0;
break;
case 100: // Set TIME
buf=server.arg("100");
b=0;
for(a=0; a<7; a++)
{
cmd="";
while( buf[b] != ',')
{
cmd += buf[b];
b ++;
}
tm_data[a]=cmd.toInt(); b ++;
}
set_DS3231(tm_data);
break;
}
if(fl)
{
dataFile = SD.open("/menu.html", FILE_READ);
server.streamFile(dataFile,"text/html");
dataFile.close();
}
}
void init_DS3231()
{
Clock.turnOffAlarm(alarm1); // Disables alarm 1 or 2 (default is 2 if Alarm != 1);
Clock.turnOffAlarm(alarm2); // Disables alarm 1 or 2 (default is 2 if Alarm != 1);
Clock.setClockMode(0);
}
//#define CLOCK_ADDRESS 0x68
void set_DS3231(byte* tm_data)
{
Clock.setYear(tm_data[0]); //Set the year (Last two digits of the year)
Clock.setMonth(tm_data[1]);
Clock.setDate(tm_data[2]);
Clock.setDoW(tm_data[3]); //Set the day of the week SUN=1 / SAT=7
Clock.setHour(tm_data[4]);
Clock.setMinute(tm_data[5]);
Clock.setSecond(tm_data[6]);
}
void get_DS3231(byte* tm_data)
{
bool Century=false;
bool h12;
bool PM;
tm_data[0] = Clock.getYear();
tm_data[1] = Clock.getMonth(Century);
tm_data[2] = Clock.getDate();
tm_data[3] = Clock.getDoW();
tm_data[4] = Clock.getHour(h12,PM);
tm_data[5] = Clock.getMinute();
tm_data[6] = Clock.getSecond();
}
String digit_2(byte num)
{
String str;
if(num < 10) str = ("0" + String(num));
else str = String(num);
return (str);
}
String get_current_time()
{
uint8_t a;
String buf;
String w_data[8]={"Sun","Mon","Tue","Wen","Thu","Fri","Sat"};
get_DS3231(tm_data);
buf=String(tm_data[0] + 2000);
buf += ("/" + digit_2(tm_data[1]));
buf += ("/" + digit_2(tm_data[2]));
buf += ("/" + w_data[tm_data[3] - 1] + "/");
for(a = 4; a < 7; a ++)
{
buf += digit_2(tm_data[a]);
if(a != 6) buf += ":";
}
return(buf);
}
void handleWebRequests()
{
String dataType = "text/plain";
String path;
File dataFile;
path = server.uri();
if(path.endsWith(".txt")) dataType = "text/plain";
else if(path.endsWith(".css")) dataType = "text/css";
else if(path.endsWith(".js")) dataType = "application/javascript";
else if(path.endsWith(".png")) dataType = "image/png";
else if(path.endsWith(".html")) dataType = "text/html";
else if(path.endsWith(".jpg")) dataType = "image/jpeg";
delay(5);
dataFile = SD.open(path.c_str(), "r");
server.streamFile(dataFile, dataType);
dataFile.close();
delay(5);
}
float get_volt(int no)
{
float d_vol;
d_vol = analogRead(no);
d_vol *= 3.6; d_vol /= 4095;
d_vol *= ((ad_r1 + ad_r2) / ad_r2);
d_vol *= 1.1;
return(d_vol);
}
- 7行:#include “AM2320.h” AM2320用のヘッダー
- 8行:#include <DS3231.h> DS3231用のヘッダー。RTC DS3231を参考にライブラリーをインクルードして下さい。
- 44行: ルーターのSSID
- 45行: ルーターのパスワード
- 83行: DNSを使ってサーバーのホスト名を、”esp32solar”としています。
下記の2つのファイルも本体と同じフォルダにセーブして下さい。
ファイル名:AM2320.cpp /AM2320本体スケッチ
#include "AM2320.h"
void AM2320(float * h_t_data)
{
uint8_t data[8];
int flg;
Wire.beginTransmission(AM2320_ADR);
Wire.endTransmission();
delay(10);
Wire.beginTransmission(AM2320_ADR);
Wire.write(0x03);
Wire.write(0x00);
Wire.write(0x04);
Wire.endTransmission();
delay(10);
Wire.requestFrom(AM2320_ADR,8);
if (Wire.available() >= 8)
{
for (uint8_t i=0; i<8; i++) data[i] = Wire.read();
h_t_data[0] = ((float)((data[2] << 8) | data[3]))/10;
flg = 1;
if(data[4] & 0x80) flg = -1;
data[4] &= 0x7f;
h_t_data[1] = ((float)((data[4] <<8 ) | data[5]))/10 * flg;
}
}
ファイル名:AM2320.h /AM2320用ヘッダーファイル。
#include <Wire.h>
// AM2320 I2C Address
#define AM2320_ADR 0x5c // FIX
//------------------------------------------------
// Get Humidity & Temperature
// float * h_t_data: Pointer to array of floating point variables
// 1st: Humidity 2nd:Temperature
//------------------------------------------------
void AM2320(float * h_t_data) ;
Web画面用HTMLは下記の3ファイルです。これらのファイルは、SDカードに保存して下さい。
ファイル名: menu.html /本体
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel='stylesheet' type='text/css' href='menu.css' >
<title>ESP32 Solar</title>
</head>
<body>
<center>
<div style='font-size:35px'><b><i><u>ESP32 Solar</u></i></b><br><br></div>
<div class="menu">
<div class="input-group">
<label>Time:</label>
<div id="0" >20</div>
</div>
<div class="input-group">
<label>Temp & Humi:</label>
<div id="1" >Temp: 20.5'C / Humi: 20.3%</div>
</div>
<div class="input-group">
<label>Voltage 0/1/2:</label>
<div id="2" >12.5V / 12.5V / 12.5V</div>
</div>
<form method='get'>
<button type='submit' name='1' >Update</button>
<button type='submit' name='2' >Set Time</button>
</form>
</div>
</center>
<script>
var para=['0','0','0'];
var url = "http://esp32solar.local/?80=";
var xhr = new XMLHttpRequest();
var a,b,str;
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = function()
{
if(xhr.readyState === 4 && xhr.status === 200)
{
console.log( xhr.responseText );
b=0;
for(a = 0; a < 3; a ++)
{
para[a]='';
while( xhr.responseText[b] != ',')
{
para[a] += xhr.responseText[b];
b ++;
}
b ++;
}
for(a = 0; a < 3; a ++) document.getElementById(String(a)).innerHTML = para[a];
}
}
</script>
</body>
</html>
ファイル名: Set_Time.html /DS3231の時間設定画面用HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel='stylesheet' type='text/css' href='menu.css' >
<title>ESP32 Solar</title>
</head>
<body>
<center>
<div style='font-size:35px'><b><i><u>Set Time</u></i></b><br><br></div>
<div class="menu">
<div class="input-group">
<label>Year:</label>
<div >
20<input type='number' max='99' min='0' id='100' class='inp_box' onchange='onChg_Set_Year()' />
</div>
</div>
<div class="input-group">
<label>Month/Day/Weekday:</label>
<div>
<input type='number' max='12' min='1' id='101' class='inp_box' />(M)
<input type='number' max='31' min='1' id='102' class='inp_box' />(D)
<select id="103" style="width:65px">
<option value='1' selected >Sun.</option>
<option value='2'>Mon.</option>
<option value='3'>Tue.</option>
<option value='4'>Wen.</option>
<option value='5'>Thu.</option>
<option value='6'>Fry.</option>
<option value='7'>Sat.</option>
</select>
</div>
</div>
<div class="input-group">
<label>Hour/Minute/Second:</label>
<div>
<input type='number' max='23' min='0' id='104' class='inp_box' />(H)
<input type='number' max='59' min='0' id='105' class='inp_box' />(M)
<input type='number' max='59' min='0' id='106' class='inp_box' />(S)
</div>
</div>
<form method='get' class="input-group">
<button type='submit' name='99' value='2' >Back</button>
<button type='submit' name='100' value='2' onclick='onBtn_Set_Time()' >Set</button>
</form>
</div>
</center>
<script>
function onChg_Set_Year() {
var a;
a=Number(document.getElementById("100").value);
if(a < 10) document.getElementById("100").value="0" + String(a);
}
function onBtn_Set_Time() {
var a;
var str="";
for(a=100; a<107; a ++) str += (document.getElementById(String(a)).value + ",");
document.getElementsByName('100')[0].value=str;
}
var para=['0','0','0','0','0','0','0'];
var url = "http://esp32solar.loacl/?80=";
var xhr = new XMLHttpRequest();
var a,b,str;
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = function()
{
if(xhr.readyState === 4 && xhr.status === 200)
{
console.log( xhr.responseText );
b=0;
for(a=0; a<7; a++)
{
para[a]='';
while( xhr.responseText[b] != ',')
{
para[a] += xhr.responseText[b];
b ++;
}
b ++;
}
if(Number(para[0]) < 10)
para[0] += ("0" + para[0]);
for(a=0; a<7; a++)
{
document.getElementById(String(a + 100)).value=para[a];
if(a == 3)
document.getElementById("103").selectedIndex=Number(para[a]);
}
}
onChg_Set_Year();
}
</script>
</body>
</html>
ファイル名: menu.css /CSSファイル
@charset "UTF-8";
body {
font-family: Arial,Helvetica,sans-serif;
background: #181818;
color: #EFEFEF;
font-size: 16px
}
.menu {
width: 420px;
background: #363636;
padding: 15px;
border-radius: 10px;
margin-top: -30px;
margin-right: 10px;
}
.input-group {
display: flex;
flex-wrap: nowrap;
line-height: 22px;
margin: 5px 0px;
}
.input-group label {
padding-right: 10px;
min-width: 38%;
text-align: left
}
button {
display: block;
margin: 10px;
line-height: 35px;
cursor: pointer;
color: #fff;
background: #228b22;
border-radius: 10px;
font-size: 16px;
width:200px;
}
.inp_box {
width:32px;
font-size: 16px;
text-align: right;
}
すべての準備が出来たら実行して下さい。パイロットLEDが点灯したらサーバーの準備完了です。PC ブラウザーに、”esp32solar.local”と入力すると、以下の画面が表示されます。
現在の時間と温度、湿度が表示されます。また電圧はバッテリーを使用しているので、0番のみ電圧を表示されています。
Updateボタンを押すと値が更新されます。またSet Timeボタンを押すと現在の時間の設定が出来ます。今回は、RTCをボタン電池でバックアップしているので本体の電源を落としてもRTCのデータは保存されます。