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

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

TWE-Lite DIP と AM2302 をつなぐ

中華温湿度センサー AM2302 と TWE-Lite DIP をつないでみた。
いやー、これはちょっと難しかったです。

問題点としては、AM2302 の 1-wire インタフェースと TWE-LiteDIP の相性の悪さというか、(今も100%成功しているわけではないんだけど)仕様外の仕様に悩まされました。

基本的アイデアはこう。

TWE-Lite は基本イベント駆動で動いているので、1-wire インタフェースのネガティブエッジで割り込みをかけ、次の割り込みまでのクロック数でビットが0か1かを判定する。クロックとして、今回は TickTimer を使用した。AM2302 は PORT_INPUT4 に接続した。

まず、TickTimer の上限値がいくつに設定されているのか、100μsec は何 Tick なのか測定する必要があると思い、以下のようなコードを初期化時に追加した。

volatile uint32 last = u32AHI_TickTimerRead();
volatile uint32 current = last;

while ((current = u32AHI_TickTimerRead()) >= last) {
	vWait(1);
last = current;
}

sAppData.sIOData_now.u32AM2302TickWater = last;
last = u32AHI_TickTimerRead();

しかし、このコードは動かない。理由は初期化コードが呼ばれた時はまだ TickTimer が初期化されていないようなのである。
仕方がないので、E_STATE_APP_AM2302_MEASURE というステートを作り、この中で上記コードを回すように修正した。ついでに110マイクロ秒が何 Tick に当たるかも計測しておく。

void vProcessEvCoreSlp(tsEvent *pEv, teEvent eEvent, uint32 u32evarg) {
	switch (pEv->eState) {
...
	case E_STATE_APP_AM2302_MEASURE:
	_C {
			if (eEvent == E_EVENT_NEW_STATE) {
			/* 上記のコード */
			sAppData.sIOData_now.u32AM2302TickWater = last;
			last = u32AHI_TickTimerRead();
			vWait(110);
			sAppData.sIOData_now.u32AM2302TickCount100 = u32AHI_TickTimerRead() - last;
			ToCoNet_Event_SetState(pEv, E_STATE_RUNNING);
		}
	}
	break;
...

モード7の場合は起きているときは常時計測することになるので、E_STATE_RUNNING においては直ちに計測を開始する。

			vPortSetLo(PORT_INPUT4);
			vPortAsOutput(PORT_INPUT4);
			ToCoNet_Event_SetState(pEv, E_STATE_APP_AM2302_START);

状態 E_STATE_APP_AM2302_START では2msec 間 1-wire をドライブして AM2302 を起動する。その後入力モードに再設定して4msec待つ。(最大100マイクロ秒×40ビットなので)

	case E_STATE_APP_AM2302_START:
		if (PRSEV_u32TickFrNewState(pEv) > 2) {
			// 2msec 経過したらスクラッチパッドを初期化して Hi に戻す
			AM2302ReadCount = 0;
			AM2302Value[0] = AM2302Value[1] = 0;
			AM2302Queue.start = AM2302Queue.end = 0;
			sAppData.sIOData_now.u32RxLastTick = u32AHI_TickTimerRead();

			// エッジトリガを設定
			vAHI_DioInterruptEnable(1 << PORT_INPUT4, 0);
			vAHI_DioInterruptEdge(0, 1 << PORT_INPUT4); // 立下りエッジを設定

			ToCoNet_Event_SetState(pEv, E_STATE_APP_AM2302_RECEIVING);
			vPortAsInput(PORT_INPUT4);
		}
		break;

	case E_STATE_APP_AM2302_RECEIVING:
		if (PRSEV_u32TickFrNewState(pEv) > 4) {
			vAM2302ReadAndSend();
			ToCoNet_Event_SetState(pEv, E_STATE_WAIT_TX);
		}
		break;

図にするとこんな感じ。
f:id:minosys:20141124105626p:plain
注意点は開始ビットと応答ビットを拾うので全体では42ビットあり、先頭の2ビットを無視する必要がある点。

トリガを検出したらその時点での TickTimer を保存する。Timer0 や TickTimer など他の割り込み影響でビット判定処理が間に合わない状況を想定し、ここではキューに入れることにした。入りきれない場合は取りこぼす仕様。

PUBLIC uint8 cbToCoNet_u8HwInt(uint32 u32DeviceId, uint32 u32ItemBitmap) {
	uint8 u8handled = FALSE;

	switch (u32DeviceId) {
	case E_AHI_DEVICE_SYSCTRL:
		_C {
			uint32 TickCount = u32AHI_TickTimerRead();
			if (u32ItemBitmap & (1 << PORT_INPUT4)) {
				// キューに現在の Tick Timer を設定する
				if ((AM2302Queue.start + 1) % AM2302QUEUE_SIZE != AM2302Queue.end) {
					AM2302Queue.u32TickCount[AM2302Queue.start++] = TickCount;
					if (AM2302Queue.start >= AM2302QUEUE_SIZE) {
						AM2302Queue.start = 0;
					}
				}
			}
		}
		break;

割り込みがあると後で cbToCoNet_vHwEvent() が呼ばれるので、ここでビット判定を行う。

		// 立下りエッジ間隔からビットを判定する
		while (AM2302Queue.end != AM2302Queue.start) {
			uint32 last = sAppData.sIOData_now.u32AM2303lastTickTimer;
			uint32 current = AM2302Queue.u32TickCount[AM2302Queue.end++];
			if (AM2302Queue.end >= AM2302QUEUE_SIZE) {
				AM2302Queue.end = 0;
			}

			// 1ビット左シフト
			AM2302Value[0] <<= 1;
			if (AM2302Value[1] & 0x80000000) {
				AM2302Value[0] |= 1;
			}
			AM2302Value[1] <<= 1;

			// 前回との差分をとる
			if (last < current) {
				last = current - last;
			} else {
				last = current - last + sAppData.sIOData_now.u32AM2302TickWater;
			}
//			if (last >= sAppData.sIOData_now.u32AM2302TickCount100) {
			if (last >= 0x600) {
				// 100usec より長かったらビット 1 と判定
				AM2302Value[1] |= 1;
			}

//			AM2302Value[0] = last >> 24;
//			AM2302Value[1] = last << 8;

			// 現在の Tick Timer 値を保存
			sAppData.sIOData_now.u32AM2303lastTickTimer = current;
			AM2302ReadCount++;
		}

とここで再び問題が発生。ビット0なら100マイクロ秒以内に波形が1に戻る仕様のはずなのだが、戻る気配なし。中華センサーは5V未満で使用すると通信時間が伸びるようだ。トライアンドエラーで調べたところ、0x600より大きければ1を送っているようである。

ここまでくれば、いつものように温度・湿度情報を取り出して親機に送るのみである。

static void vAM2302ReadAndSend(void) {
	if (AM2302ReadCount != 42) {
		// 42ビット入力できなかったら "不存在" ビットを立てる
		sAppData.sIOData_now.u8AM2302status = 1; // no presence
		sAppData.sIOData_now.u16AM2302temp = (uint16)AM2302ReadCount;
		sAppData.sIOData_now.u16AM2302hum = (uint16)sAppData.sIOData_now.u32AM2302TickCount100;
	} else {
		uint8 data[5];
		data[0] = AM2302Value[0] & 255;
		data[1] = (AM2302Value[1] >> 24) & 255;
		data[2] = (AM2302Value[1] >> 16) & 255;
		data[3] = (AM2302Value[1] >> 8) & 255;
		data[4] = AM2302Value[1] & 255;

		// check sum を計算
		if ((uint8)(data[0] + data[1] + data[2] + data[3]) != data[4]) {
			sAppData.sIOData_now.u8AM2302status = data[4]; // parity error
		} else {
			sAppData.sIOData_now.u8AM2302status = 0;
		}
		sAppData.sIOData_now.u16AM2302hum = (data[0] << 8) | data[1];
		sAppData.sIOData_now.u16AM2302temp = (data[2] << 8) | data[3];
	}
	// 親機に対してメッセージを送る
	sAppData.sIOData_now.i16TxCbId = i16TransmitAM2302Data(LOGICAL_ID_PARENT);
}

static int16 i16TransmitAM2302Data(uint8 u8AddrDst) {
	uint8 auOutBuf[32];
	uint8 *p = &auOutBuf[0], *q = p;

	S_OCTET(u8AddrDst);
	S_OCTET(SERCMD_ID_MINOSYS_DATA);
	S_OCTET(SERCMD_SUBID_MINOSYS_AM2302);
	S_OCTET(5);
	S_OCTET(sAppData.sIOData_now.u8AM2302status);

	S_BE_WORD(sAppData.sIOData_now.u16AM2302temp);
	S_BE_WORD(sAppData.sIOData_now.u16AM2302hum);

	return i16TransmitSerMsg(p, q - p, ToCoNet_u32GetSerial(),
			sAppData.u8AppLogicalId, p[0], FALSE,
			sAppData.u8UartReqNum++);
}

しばしばチェックサムエラーが発生するが、値は連続しているようである。