徒然なる日々を送るソフトウェアデベロッパーの記録(2)

技術上思ったことや感じたことを気ままに記録していくブログです。さくらから移設しました。

TWE-Lite DIP で温湿度センサーを作る

部屋から少し離れた場所(外とか)での温湿度を計測したいというニーズがあり、ならば無線通信だろうということで、最近はやりらしい TWE-Lite DIP に手を出してみた。

温湿度センサーとして使ったのは秋月電子通商で売っていた SHT11 ベースのチップである。
ぐぐってみると製作記事も多く、とてもオーソドックスな IC らしい。
その割には I2C っぽいのに I2C じゃないなど少し苦労したので、メモする。

やりたいことは20秒間隔で温湿度を測り、ラズパイに飛ばす。
ラズパイではやってきたデータを加工して json フォーマットに変換する。
その後は作っていないが、例えば Dropbox に保存して他からみられるようにする予定。

まず、TWE-Lite 側だが、I2C ではないので当然独自に作る必要がある。今回は DO4 を SCLK に、DI4 を SDA に接続した。

SHT11へのアクセスを行うために、いくつかの関数を追加する。まとめて SH11.h として定義した。
というわけで SH11.h の中身。(SH11.h とか SH11.c とかなっているのはタイポです...)

/*
 * SH11.h
 *
 *  Created on: 2014/10/24
 *      Author: Minosys
 */

#ifndef SH11_H_
#define SH11_H_

#include <serial.h>
#include <fprintf.h>

#include "config.h"
#include "common.h"

#include "Version.h"

#define SH11_CMD_TEMP (0x03)
#define SH11_CMD_HUM  (0x05)
#define SH11_CMD_STATUS (0x07)

// SH11 が存在するか確認する
bool_t SH11_init(void);

// SH11 にコマンドを送る
// TRUE が返ってきた場合はデバイスが存在する
// FALSE の場合はデバイスがいなくなったかエラーが発生した
bool_t SH11_send(uint8 cmd);

// 温度・湿度測定終了なら TRUE, そうでなければ FALSE
bool_t SH11_ready(void);

// SH11 からデータを読み出す(16ビット)
uint16 SH11_read(void);

// SH11 からデータを読み出す(8ビット)
uint8 SH11_read1(bool_t noAck);

// SH11 をリセットする
void SH11_reset(void);

#endif /* SH11_H_ */

最初に SH11_init() を呼ぶとつながっているかどうかを調査する。
つぎに SH11_send() でコマンドを送出し、一定時間経過後(今回はざっくり500msecとした)SH11_ready() を呼ぶ。TRUE が返ってきたらデータが用意できているので SH11_read() または SH11_read1() を呼び出してデータを取得する。この繰り返しで定期的に温湿度を取得できる。

さて、SH11.c の中身だが、まだあまり整理されておらず恥ずかしいのだが、一応載せておく。

/*
 * SH11.c
 *
 *  Created on: 2014/10/24
 *      Author: Minosys
 */

#include <jendefs.h>
#include <AppHardwareApi.h>
#include "SH11.h"
#include "utils.h"

#define SH11_WAIT (20)

/**********************************************************************************
 * 注意!! INPUT4 は絶対に Hi にしないこと!
 * 間違って Hi にすると TWE Light が壊れる可能性がある
 * @return
 */
static void SH11_start(void);

// SH11 デバイスのイニシャライズ
// 最初に1回だけ呼び出すこと
bool_t SH11_init(void) {
	// 11msec 待つ
	vWait(11000);

	// ステータスレジスタを読んでみる
	if (!SH11_send(SH11_CMD_STATUS)) {
		return FALSE;
	}
	SH11_read1(TRUE); // STATUS レジスタ
	return TRUE;
}

// SH11 バスをリセットする
void SH11_reset(void) {
	int i;

	// ポートを再定義
	vPortAsOutput(PORT_OUT4);
	vPortAsInput(PORT_INPUT4);

	// すでに pullup されているので不要
	vPortDisablePullup(PORT_INPUT4);
	vPortSetLo(PORT_INPUT4);

	// SCLK を Lo に、SDA を Hi にする
	// 念のため20CLK投げてみる
	vPortSetLo(PORT_OUT4);
	for (i = 0; i < 20; ++i) {
		vWait(SH11_WAIT);
		vPortSetHi(PORT_OUT4);
		vWait(SH11_WAIT);
		vPortSetLo(PORT_OUT4);
	}
	vWait(SH11_WAIT);
}

// コマンドを送った後、デバイスが ready 状態か判定する
bool_t SH11_ready(void) {
	int i;
	// 1msec 待ってみる
	for (i = 0; i < 100; ++i) {
		if (bPortRead(PORT_INPUT4)) return TRUE;
		vWait(SH11_WAIT);
	}
	return FALSE;
}

// コマンドを送る
bool_t SH11_send(uint8 cmd) {
	int i;
	bool_t ready;
	SH11_reset();

	// SDA が Hi であることを確認する
	if (bPortRead(PORT_INPUT4)) return FALSE;

	SH11_start();
	for (i = 0; i < 8; ++i) {
		if (cmd & 0x80) {
			vPortAsInput(PORT_INPUT4);
		} else {
			vPortAsOutput(PORT_INPUT4);
			vPortSetLo(PORT_INPUT4);
		}
		cmd <<= 1;
		vWait(SH11_WAIT);
		vPortSetHi(PORT_OUT4);
		vWait(SH11_WAIT);
		vPortSetLo(PORT_OUT4);
		vWait(SH11_WAIT);
	}
	vPortAsInput(PORT_INPUT4);

	// ACK を待つ
	vWait(SH11_WAIT);
	vPortSetHi(PORT_OUT4);
	ready = bPortRead(PORT_INPUT4);
	vWait(SH11_WAIT);
	vPortSetLo(PORT_OUT4);
	vWait(SH11_WAIT);
	return ready;
}

// 温度・湿度データを読み込む
uint16 SH11_read(void) {
	uint16 msb = SH11_read1(FALSE);
	uint16 lsb = SH11_read1(TRUE);
	return (msb << 8) | lsb;
}

// スタートビットを送る
static void SH11_start(void) {
	vWait(SH11_WAIT);
	vPortSetLo(PORT_OUT4);
	vPortAsInput(PORT_INPUT4);
	vPortSetLo(PORT_INPUT4);
	vWait(SH11_WAIT);
	vPortSetHi(PORT_OUT4);
	vWait(SH11_WAIT);
	vPortAsOutput(PORT_INPUT4);
	vPortSetLo(PORT_INPUT4);
	vWait(SH11_WAIT);
	vPortSetLo(PORT_OUT4);
	vWait(SH11_WAIT);
	vPortSetHi(PORT_OUT4);
	vWait(SH11_WAIT);
	vPortAsInput(PORT_INPUT4);
	vWait(SH11_WAIT);
	vPortSetLo(PORT_OUT4);
	vWait(SH11_WAIT);
}

// 1バイト読み込む; MSB ファースト
uint8 SH11_read1(bool_t noAck) {
	int i;
	uint8 cc = 0;
	vPortSetLo(PORT_OUT4);
	vPortAsInput(PORT_INPUT4);
	vWait(SH11_WAIT);
	for (i = 0; i < 8; ++i) {
		vPortSetHi(PORT_OUT4);
		vWait(SH11_WAIT);
		cc <<= 1;
		if (!bPortRead(PORT_INPUT4)) {
			cc |= 1;
		}
		vPortSetLo(PORT_OUT4);
		vWait(SH11_WAIT);
	}
	// ACK を返す
	if (!noAck) {
		vPortAsOutput(PORT_INPUT4);
		vPortSetLo(PORT_INPUT4);
		vWait(SH11_WAIT);
	}
	vPortSetHi(PORT_OUT4);
	vWait(SH11_WAIT);
	vPortSetLo(PORT_OUT4);
	vWait(SH11_WAIT);
	vPortAsInput(PORT_INPUT4);
	return cc;
}

TWE-Lite に使われているマイコンは各ポートを入出力どちらにもできるらしいので、PORT_INPUT4 を出力ポートとしても使う。ここで気を付けなければならないのは、うっかり 1 を出力しているときにSHT11が0を出力すると回路がショートしてしまう点だ。これを防ぐために出力は0専用、1を出力したいときはハイ・インピーダンス状態にする。
9ビット目のACK出力プログラムが正しく動かなくて苦労していた。

あとは SDK についてくる App_Twelite サンプルで E_STATE_RUNNING 状態のときの出力を SHT11 用にアレンジすればいいだけだ。

			// 500msec 毎に SH11 の情報を読み出す
			bool_t bCond = FALSE;
			if (((sAppData.u32CtTimer0 & 31) == 31)) {
				switch (sAppData.sIOData_now.u8sh11 & 7) {
				case 0: // 温度計測を開始する
					bCond = FALSE;
					if (--sAppData.sIOData_now.u16sh11fetch == 0) {
						sAppData.sIOData_now.u16sh11fetch = SH11_EVALUATION_PERIOD;
						bCond = TRUE;
					}
					if (bCond) {
						if (sAppData.sIOData_now.u8sh11 & 0x80) {
							sAppData.sIOData_now.u16temp = sAppData.sIOData_now.u16hum = 0;
							if (SH11_send(SH11_CMD_STATUS)) {
								sAppData.sIOData_now.u8status = SH11_read1(TRUE);
							} else {
								sAppData.sIOData_now.u8status = 0x7f;
							}
							if (SH11_send(SH11_CMD_TEMP)) {
								sAppData.sIOData_now.u8sh11 = (sAppData.sIOData_now.u8sh11 & ~0x7) | 1;
								bCond = FALSE;
							} else {
								sAppData.sIOData_now.u16temp = 0xfffc;
							}
						} else {
							sAppData.sIOData_now.u8status = 0xff;
							sAppData.sIOData_now.u16temp = sAppData.sIOData_now.u16hum = 0xffff;
						}
					}
					break;
				case 1: // 温度を読み取る/湿度計測を開始する
					if (SH11_ready()) {
						sAppData.sIOData_now.u16temp = SH11_read();
						sAppData.sIOData_now.u8sh11 = (sAppData.sIOData_now.u8sh11 & ~0x7) | 2;
						if (!SH11_send(SH11_CMD_HUM)) {
							sAppData.sIOData_now.u16hum = 0xfffd;
							bCond = TRUE;
						}
					} else {
						SH11_reset();
						sAppData.sIOData_now.u16temp = 0xfffe;
						bCond = TRUE;
					}
					break;
				case 2: // 湿度を読み取る
					if (SH11_ready()) {
						sAppData.sIOData_now.u16hum = SH11_read();
					} else {
						sAppData.sIOData_now.u16hum = 0xfffe;
					}
					bCond = TRUE;
					break;
				default:
					bCond = TRUE;
				}
			}
			if (bCond) {
				// 親機に対してメッセージを送る
				sAppData.sIOData_now.i16TxCbId = i16TransmitSH11Data(LOGICAL_ID_PARENT);
				sAppData.sIOData_now.u8sh11 = (sAppData.sIOData_now.u8sh11 & ~0x7) | 0;
			}
		}
		break;

今回はサブステートマシンを組んでみた。パワーダウンモードの時も同様に書き加える。(ほぼ同内容なので省略)出力フォーマットはシリアルアウトプットの中に以下のバイト列を埋め込んだ。
[宛先=00固定] 82 01 06 [SHT11 の存在ビット][statusレジスタ][温度読み出し値][湿度読み出し値][CRC8] CR+LF
SHT11存在ビットは最上位ビットに用意した。存在すれば1、存在しなければ0となる。
TWE-Lite DIP 親機(今回は ToCoStick を使用)からは
:3C8201068200...
のような16進数文字列が出力される。

これをラズパイに処理させるのだが、今回は初めて python を使った。(一緒に買った「TWE-Lite ではじめるカンタン電子工作」という本が python を使っていたので)

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

import json, datetime, struct, binascii, serial

def parseSH11(data):
        # 先頭の : を取り除く
        if data[0] != ":":
                return False
        data = data[1:]

        # バイトデータに変換する
        ss = struct.Struct(">BBBBBBHHB")
        data = binascii.unhexlify(data.rstrip())
        parsed = ss.unpack(data);

        # 温度データを温度に変換
        temp = -39.6 + 0.01 * parsed[6]

        # 湿度データを湿度に変換
        hum = -2.0468 + 0.0367 * parsed[7] + -1.5955e-6 * parsed[7] * parsed[7]
        hum = (temp - 25.0) * (0.01 + 0.00008 * parsed[7]) + hum;

        # 現在時刻
        now = datetime.datetime.now()
        current = now.strftime("%Y-%m-%d %H:%M:%S")

        # 結果を返す
        result = {
                "type" : "sht11",
                "datetime" : current,
                "from" : parsed[0],
                "presence" : parsed[4],
                "status" : parsed[5],
                "temperature" : temp,
                "humidity" : hum
        }
        return result;

# /dev/ttyUSB0 を開く
s = serial.Serial(port = "/dev/ttyUSB0", baudrate = 115200)

while True:
        # 1行読み取る
        data = s.readline()

        # 解釈する
        parsed = parseSH11(data)

        # 書き込みファイルを開く
        if parsed != False:
                f = open("/tmp/sht11.txt", "a")
                f.write(json.dumps(parsed, sort_keys=True))
                f.write("\n")
                f.close()
else:
        True

# COM を閉じる
s.close()

プログラムを通すと、以下の json フォーマットで出力される。
{"datetime": "2014-10-25 22:52:07", "from": 60, "humidity": 78.4553415245, "presence": 130, "status": 0, "temperature": 21.5, "type": "sht11"}
from は送信元の論理 ID。
単3アルカリ乾電池を入れて常時起動モードで1日上げっぱなしにしているが、大丈夫のようだ。
(完成品では CR2032 に変更する予定)

到達距離で苦労しがちな BlueTooth と違い、ZigBee ベースの TWE-Lite は電波が強力のようだ。センサーからラズパイまで20m近くあるが、何の問題もなく届いている。むしろ電力を弱くしないと混信&電池の問題がありそうだ。

次は K熱電対ベースの温度計を作ろうかな。(さっき部品は注文しました)