GY-2561とESP-WROOM-02を使って照度を測る

概要

デジタル光センサーのTSL2561を載せた、GY-2561というブレイクアウトボードとESP-WROOM-02を使って、照度計を作ってみる。WROOM-02でwebサーバーを動かし、PCやスマホのブラウザからアクセスして照度を表示できるようにする。

TSL2561は、TAOS社(現在はams社)が開発した “luminosity sensor” で、直訳すれば光度センサーということだろうか。光度と照度の違いはよく分からないが、光に照らされているセンサー面の明るさを数値化してくれるデバイスということだろう。

テータシートを読んでいておもしろいと思ったのは、2つのフォトダイオードと2つの積分型A/Dコンバータが入っていて、一方は広帯域用、一方は赤外光用とのこと。2つのチャネルで同時に数値化を行い、広帯域側の値から赤外光の寄与分を除去することで、人間の感覚に近い照度が得られるとのこと。データシートはこちらのメーカーサイトからダウンロードできる。I2Cインタフェースを備えているので、ArduinoやESP-WROOM-02との接続は簡単である。

GY-2561

TSL2561を載せたブレイクアウトボードを取り寄せたところ、GY-2561という製品が届いた。アマゾンで2個入りのパッケージが295円、例によって広東省からの送料込だった。今回はポチっとしてから届くまでに25日間ほどかかったが、まあ仕方ないだろう。
先日使ってみた温度・気圧センサーのGY-BMP280とよく似ている。それにしても、製造しているメーカーも分からなければ、届くまで製品名も分からない。福袋的な要素もあって面白みもあるのだが。

外観

GY-2561 表側
GY-2561 表側
GY-2561 裏側
GY-2561 裏側

ボードと一緒にパッケージされている5本足のピンヘッダをハンダ付けした後で撮影。
ボード自体の大きさは20mm × 14mm程度。表側の中央左寄りにTSL2561FNが載っていて、右側には3.3V出力のLDOとコンデンサ、プルアップ抵抗などが実装されている。表側には、L H と書かれたジャンパエリアがあるが、I2Cのアドレスを変更するときに使うようである。

回路図

例によって頼りになる資料が少ないので、配線パターンに基づいて参考用の回路図を起こしてみた(実際と異なるかもしれません)。

GY-2561等価回路
GY-2561等価回路

TSL2561の電源電圧は2.7~3.6Vで、動作時の消費電流が0.6mA maxである。小さなLDOは+3.3V 200mA出力のXC6206P332だろう。わざわざLDOが載っているのは、+5V動作のマイコンと一緒に使うことを意図したものだと思うが、+5V系のシリアル線(SCL, SDA) を直結すると絶対最大定格を超えてしまう。I2Cのレベルコンバータを挟むか、直結するならば+3.3Vのマイコンを使う必要があるだろう。今回は+3.3V電源のWROOM-02と使うので直結可能。そうなると、LDOは余計なだけである。1枚あたり150円もしないものなので、そのうちLDOを外してVINとVOUTを直結してしまおうかと思っている。今回は、XCP6206P332の低負荷時のドロップアウトが0.35V以下ということで、そのまま使っても問題ないと判断した。

コンデンサが4つ載っているが、LDOの前後が0.1μFで、LDO近傍のもう一つが1~10μFといったところだろうか。TSL2561のVDD-GND間のコンデンサ( C4 )は、データシートによれば0.1μFが推奨されている。
また、今回入手したボードでは、ジャンパエリアがオープンだったので、I2Cアドレスは0x39と思われる。図に記したように、H側またはL側にジャンパしておくことで、アドレスの変更も可能。

INT端子はマイコンの割込み要因(レベル変化)として使うことが意図されている。内部A/Dコンバータによる積分が完了した時点でアサートしたり、検出した照度がある範囲を逸脱した場合にアサートするといったことが設定可能になっている。INT端子はオープンドレイン出力なので、使うときはプルアップしておく必要がある。

全体の回路

ESP-WROOM-02側はいつも通りに秋月電子のAE-ESP-WROOM02を用いた。GY-2561ともにブレッドボード上の三端子レギュレータ (TA48033S) から+3.3Vを供給する。スケッチやhtmlファイルを投入するときには、TXDとRXDにUSB-シリアルコンバータのAE-FT231Xを接続した。

I2C接続なので、GY-2561のSCLをGPIO5に、SDAをGPIO4に接続している。INT端子はGPIO13に接続し、10kΩでプルアップした。esp8266の場合、GPIO1~15を外部割込み要因として使うことができる。

wroom02-gy2561-1
wroom02-gy2561-1
WROOM-02 + GY-2561
WROOM-02 + GY-2561

ふつうのブレッドボードに載せるとこんな具合になった。センサーにあたる光を遮らないように、ブレッドボード用のワイヤを使って配線した。電源端子は秋月のDCジャックDIP化キットを用いていて、その先にはDCプラグ-USBケーブルを介してスマホ充電用のモバイルバッテリを接続している。

スケッチなど

全体としては、ESP8266 Core for Arduino のライブラリにある、ESP8266WebServer クラスを用いており、TSL2561から最新の照度を得る機能、ブラウザからのリクエストに応じてその時点での照度を応答する機能、ブラウザで照度表示を行うhtmlファイル (ESP8266のSPIFFS内に格納) から構成している。このライブラリに付属のFSBrowserというサンプルを参考にした。

TSL2561とのインタフェース

TSL2561とのインタフェースをとる部分についてはC++のクラスとして独立させた。このファイル (i2c_tsl2561.h) をメインのスケッチと同じフォルダにおいておけば、Arduino IDEにタブとして表示されるようになる。

#include <stdint.h>
#include "Wire.h"

// i2c_tsl2561.h
// 測定時の積分時間は常に402msecとする(INTEG = 10B, scale = 1)
class tsl2561
{
  byte i2c_address;
  byte i2c_status = 0;
  
  byte device_id = 0;
  uint16_t data_ch0, data_ch1;
  double lux_value;
  bool gain16 = false; // false のときx1 , trueのときx16
  enum {reg_control = 0, reg_timing = 1, reg_thr_L = 2, reg_thr_H = 4, reg_int = 6, reg_id = 0x0a, reg_data0 = 0x0c, reg_data1 = 0x0e };
  enum {cmd_clear_int = 0xc0 };

public:
  tsl2561(byte addr = 0x39) {
    i2c_address = addr;
    Wire.begin();
    data_ch0 = 0;
    data_ch1 = 0;
    device_id = 0;
    lux_value = 0.0;
  }

  const byte get_device_id() { return device_id; }
  const double get_lux_value() { return lux_value; }
  bool init() {
    device_id = i2c_read8(reg_id | 0x80);
    if ((device_id & 0xf0) != 0x50)  // TSL2561FNが載っている。
      return false;
    return true;    
  }

  void start(bool use_interrupt = true, bool high_gain = false) {
    gain16 = high_gain;
    i2c_write_reg(reg_timing | 0x80, gain16 ? 0x12 : 0x2);  // timing . GAIN=x1, integrate time = 402msec. (scale = 1).
    i2c_write_reg(reg_int | 0x80, use_interrupt ? 0x10 : 0);  // level interrupt. on every conversion completion. OR disable.
    i2c_write_reg(reg_control | 0x80, 3);  // power on.
    lux_value = 0;
  }

  void stop() {
    i2c_write_reg(reg_control | 0x80, 0);
  }

  double get_data() {
    data_ch0 =  i2c_read16_swab(reg_data0 | 0x80);
    data_ch1 =  i2c_read16_swab(reg_data1 | 0x80);
    calc_lux();
    return lux_value;
  }

// データシートに書いてあるlux値の算出方法をコード化。
// INTEG = 402msec
// パッケージによってプログラム内の定数が変わることに注意。FNパッケージ用を記述した。
// 
  void calc_lux() {
    if (!data_ch0 || !data_ch1) {
      lux_value = 0.0;
      return;
    }
    double ch0 = (double)data_ch0 * (gain16 ? 1.0 : 16.0);
    double ch1 = (double)data_ch1 * (gain16 ? 1.0 : 16.0);
    double ratio = ch1 / ch0;
    if (ratio <= 0.125)
      lux_value = 0.0304 - 0.0272 * ratio;
    else if (ratio > 0.125 and ratio <= 0.250)
      lux_value = 0.0325 - 0.0440 * ratio;          
    else if (ratio > 0.250 and ratio <= 0.375)
      lux_value = 0.0351 - 0.0544 * ratio;          
    else if (ratio > 0.375 and ratio <= 0.5)
      lux_value = 0.0381 - 0.0624 * ratio;          
    else if (ratio > 0.50 && ratio <= 0.61)
      lux_value = 0.0224 - 0.031 * ratio;
    else if (ratio > 0.61 && ratio <= 0.80)
      lux_value = 0.0128 - 0.0153 * ratio;
    else if (ratio > 0.80 && ratio <= 1.30)
      lux_value = 0.00146 - 0.00112 * ratio;
    else
      lux_value = 0.0;
    lux_value *= ch0;      
  }

  byte clear_interrupt() {
    Wire.beginTransmission(i2c_address);
    Wire.write(cmd_clear_int);
    i2c_status =  Wire.endTransmission();
    return i2c_status;
  }

  byte i2c_write_reg(byte reg, byte data) {
    Wire.beginTransmission(i2c_address);
    Wire.write(reg);
    Wire.write(data);
    i2c_status =  Wire.endTransmission();
    return i2c_status;
  }

// read 16bit data from addr/pointer. 1st LSB, 2ns MSB.
  uint16_t i2c_read16_swab(byte pointer) {
    Wire.beginTransmission(i2c_address);
    Wire.write(pointer);
    i2c_status = Wire.endTransmission();
    Wire.requestFrom(i2c_address,  (byte)2);
    uint16_t lsb = (uint16_t)Wire.read();
    uint16_t msb = (uint16_t)(Wire.read() << 8);
    return msb | lsb;
  }

// read 8bit data from pointer.
  uint8_t i2c_read8(byte pointer) {
    Wire.beginTransmission(i2c_address);
    Wire.write(pointer);
    i2c_status = Wire.endTransmission();
    Wire.requestFrom(i2c_address,  (byte)1);
    return (uint8_t)Wire.read();
  }
};

おもなメソッドは以下のとおり。

コンストラクタ tsl2561(byte addr = 0x39)

インスタンスの作成時にI2Cのスレーブ側アドレスを指定できるようにした。省略時は0x39となる。コンストラクタ内でWireライブラリの初期化を行っている。

bool init()

TSL2561のデバイスIDを問合せ、意図したデバイスが載っていることを確認する。パッケージやリビジョンによって得られる値が変わる。今回は、FNパッケージを対象とした。

void start(bool use_interrupt = true, bool high_gain = false)

動作の設定と、変換の開始を指示する。
変換時間は402msecとした。use_interruptがtrueの場合、変換が終了するごとにINT端子がLレベルとなる (明示的にクリアするまでLレベルを維持する)。また、high_gainがtrueの場合、照度を検出するゲインが16倍となる。使った感じ、強い光を扱うならばゲインはx1の方がよいようだ。

コントロールレジスタ (reg_control) に対して 3を与えると変換動作が開始する。デバイスは、変換が終了すると2つ変換結果(広帯域側と赤外光側)をデータレジスタ (reg_data0およびreg_data1) に格納し、次の変換動作を開始する。このため、変換動作中であっても直前の変換結果をデータレジスタから読み出すことができる。

void calc_lux()

データシートに書いてある内容にしたがって、2つのデータレジスタから得た内容から照度 (ルクス値) を得る。遠慮なく浮動小数点を使うことにした。

byte clear_interrupt()

start()時に割込みありにし、実際にマイコン側で割込みを使う場合、割込みハンドラ内でこのメソッドを呼び出し、INT端子を非L(ハイインピーダンス)に戻す必要がある。

このクラスを使う側は、インスタンスを作った後、init()でデバイスを確認し、start() で変換を開始する。その後は、

  • 402msec以上のインターバルで値を読み出す。
  • 割込みが発生するごとに値を読み出す。

のいずれかを行うことを意図して書いた。

メインのスケッチ (esp_tsl2561_Server1.ino)

100行ほどなので、こちらも載せることにした。

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include "Ticker.h"
#include "i2c_tsl2561.h"

#define START_MSG "\n" + String(__FILE__) + " start."
#define RED_LED 15

const char* ssid = "ssid";
const char* password = "password";
ESP8266WebServer server(80);

void die(int msec, const char* cp) {
  if (cp)
    Serial.println(cp);
  for(;;) {
    digitalWrite(RED_LED, 1);
    delay(msec);
    digitalWrite(RED_LED, 0);
    delay(msec);
  }
}

String getContentType(String filename){
  if (filename.endsWith(".html")) 
    return "text/html";
  return "text/plain";
}

bool handleFileRead(String path) {
  Serial.println("handleFileRead: " + path);
  if(path.endsWith("/")) path += "index.html";
  String contentType = getContentType(path);
  if (SPIFFS.exists(path)){
    File file = SPIFFS.open(path, "r");
    size_t sent = server.streamFile(file, contentType);
    file.close();
    return true;
  }
  return false;
}

tsl2561 tsl;
float current_lx = 0.0;
byte led_state = 0;

void ICACHE_RAM_ATTR tsl_func() {
  tsl.clear_interrupt();
  led_state ^= 1;
  digitalWrite(RED_LED, led_state);
  current_lx = (float)tsl.get_data();
}

Ticker ticker;
void ticker_func() {
  led_state ^= 1;
  digitalWrite(RED_LED, led_state);

  current_lx = (float)tsl.get_data();
}

void handleGetData() {
  int v = current_lx * 10.0;
  String data = String(v);
  server.send(200, "text/plain", data);
}

void setup(void){
  delay(20);
  Serial.begin(115200);
  Serial.println(START_MSG);

  bool f = tsl.init();
  Serial.print("TSL2561 device id = ");    
  Serial.println(tsl.get_device_id(), HEX);
  if (!f)  
    die(200, "tsl init() failed");

  SPIFFS.begin();
  
  pinMode(RED_LED, OUTPUT);
  digitalWrite(RED_LED, 0);
  WiFi.softAP(ssid, password);
  WiFi.mode(WIFI_AP);
  IPAddress ip = WiFi.softAPIP();
  Serial.println("");
  Serial.println(ssid + String(" starts...(") + ip.toString() + ")"); 
  server.on("/getlux", handleGetData);
  server.onNotFound([](){
    if(!handleFileRead(server.uri()))
      server.send(404, "text/plain", "FileNotFound");
  });

  WiFi.printDiag(Serial);
  server.begin();
  Serial.println("HTTP server started");
  delay(50);

#if 0  
  attachInterrupt(13, tsl_func, ONLOW);
  tsl.start(true);
#else
  ticker.attach_ms(500, ticker_func );
  tsl.start(false);
#endif
}

void loop(void){
  server.handleClient();
}

ESP8266WebServerクラスを使った典型的なWEBサーバーのコードに、tsl2561オブジェクトを追加し、初期化や変換動作、検出したルクス値を得るためのuriの追加などを行っている。

setup()の最後の方で、#if 0~#else~#endifのブロックがあるが、tsl2561の変換完了割込みを使う場合と、Tickerライブラリによるポーリングでデータを得る場合の両方を試してみた。おのおのに対応するハンドラが、tsl_func() ticker_func() になっている。いずれのハンドラもtsl2561から得られる最新値を変数current_lxに代入している。ブラウザからのデータ要求 /getlux リクエストがくると、current_lx の内容をレスポンスBODYとして応答する。

index.html

WROOM-02のSPIFFS内には、ブラウザで数値を表示するためのhtmlファイルを1つだけ入れた。

<HTML>
<head>
<meta chartset="utf-8"/>
<script>

window.onload = function(){  start_polling(); }

var cur_max = 0.0;
function clear_options() {
	update_options(0.0);
	return false;
}

function update_options(value) {
	if (value == 0.0 || value > cur_max){
		cur_max = value;
		disp_value('text_max', value);
	}
}

function start_polling() {
	setTimeout( function(){ http_request('/getlux'); }, 500);
}

function http_request(uri) {
	var request = new XMLHttpRequest();
	request.onload = function(){
		if (request.readyState == 4) {
			if (request.status != 200) {
				alert('http_request() error http status = ' + request.status);
			} else {
				value = 0.0;
				v = request.responseText
				if (v != null)
					value = parseFloat(v) / 10.0;
				disp_value('text', value);
				update_options(value);
				start_polling();
			}
		}
	};
	request.onerror = function() {
		alert('http_request failed. ' + request.statusText); 
	};
	request.open('GET', uri, true);
	request.timeout = 5000;
	request.send(null);
}

function disp_value(id, value) {
	var text = document.getElementById(id);
	text.innerHTML = value;
	return;
}

</script>
<body>
<h1>TSL2561 LUX value</h1>
<div style='text-align: right; padding: 0 2em 0 0; font-size:60px;'>
<span id='text' style='font-size: 192px;'>0.0</span><br/>
<div>max: <span id='text_max'>0.0</span>
<button onclick="return clear_options();" style="font-size: 24px;">clear</button>
</div>
</div>
</body>
</html>

javaScriptのsetTimeout()を使って、500msecごとに /getlux リクエストを送り、得られた内容を<span id=’text’> 内に表示するだけ。httpリクエストは XMLHttpRequest() を非同期で使って投げているが、同期モードではESP8266をリセットしたときなど、WiFiアクセスポイントが消滅してしまうから具合が悪い。

使ってみる

WROOM-02 + GY-2561
WROOM-02 + GY-2561

ブラウザにルクス値を表示できるようにした。スマホをWROOM02のsoftAPに接続し、IPアドレス 192.168.4.1 に対して”/”をリクエストすると、上記のような画面になる。静止画ではつまらないので動画をYoutubeにおいた。

最初はふだんの明るさで、およそ170ルクス前後。JISの照度基準からすると、作業には暗めだが居間としてはまずまず、ディスプレイを見るならこれくらい、といった明るさだろう。
次に、小物の撮影時に付けるLEDのテーブルライトをオンにすると900ルクス前後となった。手芸や裁縫といった細かい作業時には、これくらいの明るさが必要らしい。
最後に、カメラのLED照明の実験用に用意した高輝度白色LED(15°、並列4本、各25mA程度)をセンサーに近づけている。最高で22,000ルクスを超えているが、だいたい薄曇りの昼間くらいの感じか。

きょうのまとめ

GY-2561は150円もしないのに立派にはたらいてくる。大したものである。ただ、LDOを載せたのなら、せめてI2Cの2本のシリアル線にレベルコンバータを入れて欲しかった。

センサー関係の工作をしていていつも思うのは、果たして表示されている数値は本当に正しいのだろうか、という点。温度や湿度は、例えば時計にも表示されているし、気圧はアメダスを見れば分かる。それに対して照度の基準となるものがない。

今回の工作は、マクロレンズ用のLED照明を作るにあたって、LEDの配置構成や駆動回路を比較検討する際の、個人的な? うちうちの? 基準として作ってみることにしたものなので、別にJIS規格に則ったものと比べる必要はないのだが。