Help us understand the problem. What is going on with this article?

Raspberry pi 3でストロベリー・リナックス社製「TSL2561 照度センサ・モジュール メーカー品番:TSL2561」を使う(試行錯誤編)

More than 3 years have passed since last update.

Rasberry pi 3でストロベリー・リナックス社製の「TSL2561 照度センサ・モジュール (メーカー品番:TSL2561)」を使うための奮闘記録です。TSL2561チップを使ったセンサーの記事はいくつかありますが、うまく動かなかったり、python3だったりでした。ので、python2で試行錯誤してみました。

ちなみに...筆者はpythonを初めて半月、電子工作的なものは初めてで、ほとんど知識がありません...。そこら辺を念頭に置きつつ、参考になればと思います。

とりあえず使うために必要な部分のまとめはこちらです。

物理配線

届いたセンサをブレッドボードにのせるには、ピンヘッダのハンダ付けが必要です。
その後、同梱されていた紙を見つつ配線します。RaspiのGPIOピン構成は検索すればすぐに出てくるので、さして難しくありません。

センサ1pin(GND) -> Raspi 6番pin
センサ2pin(SDA) -> Raspi 3番pin
センサ3pin(SCL) -> Raspi 5番pin
センサ4pin(VDD) -> Raspi 1番pin

こんな感じです。
TSL2561.png

Raspberry piでI2Cを使えるようにする

I2Cが何ものかよくわかってないです。が、wikiによるとscsiみたいにいろんなデバイスをチェーンでつなげる規格みたいですね。とにかくこいつをraspbianで使えるように有効化します。

sshコンソールからOSのコンフィグメニューを出して設定します。

$ sudo raspi-config

"9 Advenced Options” -> “A7 I2C” の順にメニューを選択。
「Would you like the ARM I2C interface to be enabled?」と聞いてくるのでyesを選択。
「Would you like the I2C kernel module to be loaded by default?」と聞いてくるのでyesを選択。

次に、/boot/config.txtを以下のとおり編集します。

$ sudo vi /boot/config.txt
...以下の内容を追記
dtparam=i2c_arm=on

更に、/etc/modulesを以下のとおり編集します。

$ sudo vi /etc/modules
...以下の内容を追記
snd-bcm2835
i2c-dev

設定終了後、Raspyを再起動します。再起動後にlsmodコマンドでカーネルモジュールがロードされていることを確認します。

$ lsmod
...
i2c_dev                 6709  0 
snd_bcm2835            21342  0 
...

ツール類をインストールする

I2Cを使うためのコマンドや、pythonライブラリ(多分)をインストールします。
zsh
$ sudo apt-get install i2c-tools python-smbus

アドレス確認

配線が完了していて、ツール類のインストールまで終わったら、センサーモジュールが認識されているかコマンドで確認します。

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- 39 -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

アドレス0x39に認識されているようです。

...ここまでは、まあ順調でした。

とにかく試してみる

まずはこちらの記事を参考に試してみたところ、値は出るものの、明暗に連動していないようでした。
http://qiita.com/masato/items/1dd5bed82b19477b45d8

そこで、更に調べるとこちらの記事に行き着きましたが、python3になっています。
http://shimobayashi.hatenablog.com/entry/2015/07/27/001708

うーん、デフォルトのpython2ではアクセス出来ないのか?

pythonからI2Cにアクセスする

上記の記事や、gitのソースを読ませてもらうと、pythonからI2Cデバイスにアクセスするにはsmbusライブラリを使えば良さそうです。ただ使い方がよくわからないので、試行錯誤してみます。

ストロベリー・リナックスからの添付資料によると、

内部レジスタ0xA0に0x03を書き込むことでセンサが動作を開始します。内部アドレス0xACから2バイト読み込むとこれが可視光センサの明るさの生データ(16ビットで下位バイトが先)になります。内部アドレス0xAEから2バイト読み込むとこれが赤外線センサの生データ(16ビットで下位バイトが先)になります。

とあるります。...ふーむナンノコッチャ。

とにかく、RaspiはTSL2561を0x39で認識しているようなので、そこを起点に、0xA0、0xAC、0xAEを読んだり書いたりすればよいのだろうと当たりをつけましたが、問題はどうやるかです。

smbusで色々検索してみたところ、下記のサイトに行き当たりました。
http://raspberrypi.stackexchange.com/questions/8469/meaning-of-cmd-param-in-write-i2c-block-data
http://www.raspberry-projects.com/pi/programming-in-python/i2c-programming-in-python/using-the-i2c-interface-2

英語が苦手なので、サンプルソースを中心に斜め読みして、勘でトライアンドエラーを試みます。色々試した結果、下記のようにやると指定の内部アドレスを読んだり、書いたりできるようです。

bus     = smbus.SMBus(1)
bus.write_i2c_block_data(0x39, 0xA0, [0x03])
bus.read_i2c_block_data(0x39, 0xAC ,2)

最初のsmbus.SMBus(1)は、バスにアクセスするオブジェクトを作っているんですね、多分。'1'は、「sudo i2cdetect -y 1」の最後に指定した1と同じもののようで、/dev/配下にある、「i2c-X」ファイルのXを指定するらしいです。

データの読み取りはbus.read_i2c_block_dataメソッドを使います。

bus.read_i2c_block_data(I2Cアドレス, 内部アドレス, 読み込むデータバイト数)

データの書き込みはwrite_i2c_block_dataメソッドを使います。

write_i2c_block_data(I2Cアドレス, 内部アドレス, 書き込むデータ配列)

これでサンプルソースを書いたところなんかそれらしい値が出てきています。
手をかざすと反応して数値が変わります。行けそうな気がしてきた!

#!/usr/bin/python -u
# -*- coding: utf-8 -*-
import smbus
import time
bus = smbus.SMBus(1)
bus.write_i2c_block_data(0x39, 0x80, [0x03])

while True:
    data    = self.bus.read_i2c_block_data(self.address, 0xAC ,2)
    raw     = data[1] << 8 | data[0]
    print "環境光 : " + str(raw)

    data    = self.bus.read_i2c_block_data(self.address, 0xAE ,2)
    raw     = data[1] << 8 | data[0]
    print "赤外線 : " + str(raw)

    time.sleep(1.0)

メーカのデータシートを読む

生データは取得できるようなので、照度(Lux)の算出を試みます。再びストロベリー・リナックスからの添付資料によると、

メーカーのデータシートに可視光、赤外線センサの生データから照度(ルクス)に換算するアルゴリズムが掲載されていますので参考にしてください。

とあります。メーカーってどこだっ!、と思いながら色々ググッた結果、TAOS社のチップで、下記のURLのがデータシートのようです。

https://cdn-shop.adafruit.com/datasheets/TSL2561.pdf

カッハァァッ!!(吐血)
英語のドキュメントですか、戦うか、戦えるか、おびえる心よ〜....一体GWに何をやっているのだろうか、と自問自答しながらなんとか斜め読みします。
すると、23ページに「Calculating Lux」があって、gitのソースでも見かけた計算式を発見しました。

クラス化してみる1

なるほど、流れがわかったので、クラス化してみます。

#!/usr/bin/python -u
# -*- coding: utf-8 -*-

import smbus
import time

# Strawberry Linux社の「TSL2561 照度センサ・モジュール」から
# I2Cでデータを取得するクラス(その1)
# https://strawberry-linux.com/catalog/items?code=12561
# 2016-05-03 Boyaki Machine
class SL_TSL2561:
    def __init__(self, address, channel):
        self.address    = address
        self.channel    = channel
        self.bus        = smbus.SMBus(self.channel)
        self.bus.write_i2c_block_data(self.address, 0x80, [0x03])
        time.sleep(0.5)

    def getVisibleLightRawData(self):
        data    = self.bus.read_i2c_block_data(self.address, 0xAC ,2)
        raw     = data[1] << 8 | data[0]    # 16bitで下位バイトが先
        return raw

    def getInfraredRawData(self):
        data    = self.bus.read_i2c_block_data(self.address, 0xAE ,2)
        raw     = data[1] << 8 | data[0]    # 16bitで下位バイトが先
        return raw

    def getLux(self):
        # センサ生データの取得
        VLRD = getVisibleLightRawData()
        IRRD = getInfraredRawData()

        # 0の除算にならないように               
        if (float(VLRD) == 0):
            ratio = 9999
        else:
            ratio = (IRRD / float(VLRD))

        # Luxの算出
        if ((ratio >= 0) & (ratio <= 0.52)):
            lux = (0.0315 * VLRD) - (0.0593 * VLRD * (ratio**1.4))
        elif (ratio <= 0.65):
            lux = (0.0229 * VLRD) - (0.0291 * IRRD)
        elif (ratio <= 0.80):
            lux = (0.0157 * VLRD) - (0.018 * IRRD)
        elif (ratio <= 1.3):
            lux = (0.00338 * VLRD) - (0.0026 * IRRD)
        elif (ratio > 1.3):
            lux = 0

        return lux 

if __name__ == "__main__":
    sensor  = SL_TSL2561(0x39,1) 
    while True:
        print "Lux : " + str(sensor.getLux())
        time.sleep(1.0)

それにしても随分簡素に出来たが、gitのソースを読むともっと複雑で長いです。一体何が違うのだろう?

試行錯誤してみる

gitのソースを読んでいると、照度算出の前にGainとか、Scaleとかやたら出てきます。一体何のことだろうと調べ進めると、データシートの15ページ「Timing Register」に「high gain (16×)」の記述を発見、この辺りを丹念に読み直します。

どうもこのセンサは、「Gain」と「Integration Time」の組み合わせによって、センシングできる範囲を広げる仕様のようです。Height Gainにすると16倍の感度になるということでしょうか。また、Integration Timeをデフォルトの402msから、101ms、13.7msにすると、センシング値の積分時間が短くなって、感度が下がる(=出力される数値が下がる)ようです。露光時間が短くなるような感じでしょうか。

この情報を元に、GainとIntegration Timeを色々変更して、rawデータの出力を見ます。まずHigh Gainに設定すると、概ね16倍の値が得られることが分かりました。また、Integration Timeを101ms、13.7msにセットすると、概ね101/402、13.7/402の値が得られることが分かりました。

例えば5月日中の日向では、デフォルト状態(Gainが1、Integration Timeが402ms)だと、可視光の出力が65535(16bitの最大値)になってしまい、正しい値が得られません。このようなときは、Integration Timeを短く設定して、有効値を得る仕様のようです。

ただ、High Gainの時、ある程度の光量(日中窓際)がある状態だと、可視光/赤外線共に生データが5047固定になってしまう問題があり、解決できませんでした。低照度だと問題なく動いているのですが。

ともかく、設定により出力が変わることから、照度計算を行う前に生データをスケールする必要があるようです。ここでよくわからなかったのが、データシートの計算式がどの設定をベースに書かれているかです。gitのソースを見ると、Low Gainの時は生データを16倍しています。いくつかサンプルコードを書いて実測データを取得、下記のサイトと比較します。

http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q12117474762

結果、High Gainがベースではないかと結論づけました。データシートのどこかに書いてないのかしら...

クラス化してみる2

試行錯誤を元に、クラスを再構築します。Gainや、Integration Timeも変更できるようなメソッドを用意しました。

#!/usr/bin/python -u
# -*- coding: utf-8 -*-

import smbus
import time

# Strawberry Linux社の「TSL2561 照度センサ・モジュール」から
# I2Cでデータを取得するクラス
# https://strawberry-linux.com/catalog/items?code=12561
# 2016-05-03 Boyaki Machine
class SL_TSL2561:
    def __init__(self, address, channel):
        self.address    = address
        self.channel    = channel
        self.bus        = smbus.SMBus(self.channel)
        self.gain       = 0x00          # 0x00=normal, 0x10=×16
        self.integrationTime    = 0x02  # 0x02=402ms, 0x01=101ms, 0x00=13.7ms
        self.scale      = 1.0

        # センサ設定の初期化
        self.setLowGain()
        self.setIntegrationTime('default')

    def powerOn(self):
        self.bus.write_i2c_block_data(self.address, 0x80, [0x03])
        time.sleep(0.5)

    def powerOff(self):
        self.bus.write_i2c_block_data(self.address, 0x80, [0x00])

    # High Gainにセットする(16倍の感度?)
    def setHighGain(self):
        # High Gainにするとうまくrawデータが取れないことがある。
        # 要原因調査 ( 5047固定値になる )    
        self.gain   = 0x10
        data        = self.integrationTime | self.gain
        self.bus.write_i2c_block_data(self.address, 0x81, [data])
        self.calcScale()

    # Low Gain(default) にセットする 
    def setLowGain(self):
        self.gain   = 0x00
        data        = self.integrationTime | self.gain
        self.bus.write_i2c_block_data(self.address, 0x81, [data])
        self.calcScale()

    # 積分する時間の設定(1回のセンシングにかける時間?)
    # val = shor, middle, logn(default)
    def setIntegrationTime(self, val):
        if val=='short':
            self.integrationTime    = 0x00  # 13.7ms scale=0.034
        elif val=='middle':
            self.integrationTime    = 0x01  # 101ms  scale=0.252
        else:
            self.integrationTime    = 0x02  # defaultVal 402ms  scale=1.0
        data = self.integrationTime | self.gain
        self.bus.write_i2c_block_data(self.address, 0x81, [data])
        self.calcScale()

    def getVisibleLightRawData(self):
        data    = self.bus.read_i2c_block_data(self.address, 0xAC ,2)
        raw     = data[1] << 8 | data[0]    # 16bitで下位バイトが先
        return raw

    def getInfraredRawData(self):
        data    = self.bus.read_i2c_block_data(self.address, 0xAE ,2)
        raw     = data[1] << 8 | data[0]    # 16bitで下位バイトが先
        return raw

    def getRawData(self):
        data    = self.bus.read_i2c_block_data(self.address, 0xAC ,4)
        VL      = data[1] << 8 | data[0]    # 可視光 16bitで下位バイトが先
        IR      = data[3] << 8 | data[2]    # 赤外線 16bitで下位バイトが先
        return (VL,IR)

    def calcScale(self):
        _scale = 1.0
        # integrationTimeによるスケール
        if self.integrationTime == 0x01:    # middle
            _scale = _scale / 0.252
        elif self.integrationTime == 0x00:  # short
            _scale = _scale / 0.034

        # gainによるスケール
        if self.gain == 0x00 :              # gain 1
            _scale = _scale * 16.0

        self.scale = _scale

    def getLux(self):
        # センサ生データの取得
        raw  = self.getRawData()

        # 65535の時はエラー出力にする実装
        if raw[0] == 65535 or raw[1] == 65535:
            return "Range Over"

        # センサ設定により生データをスケールする
        VLRD = raw[0] * self.scale
        IRRD = raw[1] * self.scale

        # 0の除算にならないように               
        if (float(VLRD) == 0):
            ratio = 9999
        else:
            ratio = (IRRD / float(VLRD))

        # Luxの算出
        if ((ratio >= 0) & (ratio <= 0.52)):
            lux = (0.0315 * VLRD) - (0.0593 * VLRD * (ratio**1.4))
        elif (ratio <= 0.65):
            lux = (0.0229 * VLRD) - (0.0291 * IRRD)
        elif (ratio <= 0.80):
            lux = (0.0157 * VLRD) - (0.018 * IRRD)
        elif (ratio <= 1.3):
            lux = (0.00338 * VLRD) - (0.0026 * IRRD)
        elif (ratio > 1.3):
            lux = 0

        return lux 


if __name__ == "__main__":
    sensor  = SL_TSL2561(0x39,1) 
    sensor.powerOn()
    # sensor.setHighGain()
    sensor.setIntegrationTime('default')
    while True:
        print "Lux : " + str(sensor.getLux())
        time.sleep(1.0)

終わりに

上記クラスを使うとそれらしい値が得られるようにはなりました。ただ、High Gainの問題が解決できていません。そもそも実装が正しいか確認できません...

データシートのサンプルソースを見ると、内部アドレス(レジスタというのですかね)にアクセする時、0xAXだったり、0x8Xだったりします。「Register Set」の仕様を見ると0h〜Fhまでしか書いていないです。0xAなのか、0x8なのか、ソース見て推測すると、取得する値の型を指定しているのではないかと思うのですが、本当のところは分かりません。どちらのアドレスを指定しても、同じ値が得られることから、pythonがよしなに変換しているのではないかと思っています。誰かわかる方がいたら教えて下さい。

rawデータの値を適宜確認して、限界値に近づいてきたら、GainやIntegration Timeの設定を切り替えるようにすると、シームレスに広い範囲を測定できるかと思います。

お役に立てば幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away