動機
暑いけど僕の部屋に信頼できる温度計がないので、部屋の隅に転がってた1wireの温度センサを温度計として使いたかった。
センサ
今回使うのはDS18B20という、1wireの温度センサではポピュラーなものです。秋月電子やストロベリーリナックスから素子を購入できます。また、ストリナではシース入りのタイプも購入できます(ただし、今回の方法ではシース入りに代表される、2線式は使用できません)。
素子単体
デジタル温度センサ(1wire)DS18B20+: センサ一般 秋月電子通商-電子部品・ネット通販
1-Wire温度センサ DS18B20 - DS18B20+ - ネット販売
シース入り(参考)
1-Wireステンレス防水温度センサ(1m) - ネット販売
1-Wireステンレス防水温度センサ(10m) - ネット販売
ゲートウェイ
プロトコル
1wireは負論理の2値パルス幅変調で通信します(リセットを除く)。これを実現するにはTTL UARTを使用します。
TTL UARTはデータがない時はバスがHighになり、送信する時は最初のbitが0、次の8bitは任意の値を送信し、最後のbitが1になります。
タイミングを適切に設定すれば、10 - 90%の範囲で分解能10%の負論理パルス幅変調が可能になります。またUARTは送信だけでなく、受信も可能なので、1wireには最適なバスです。
ただ、1wireはUARTのタイミングを満たさない可能性があります。UARTコントローラによっては1wireとして使えないものがあるかもしれません。
ハードウェア
今回はFT232モジュールを使用して1wireをUSBに接続します。以下の製品を使用しました。
FTDI USB・シリアル変換ケーブル(3.3V): 半導体 秋月電子通商-電子部品・ネット通販
1wireはTTL UARTで通信できるので、USBを一旦UARTに変換し、それを1wireとして利用しています。
1wireはプルアップ/オープンドレインで通信します。そのため送信ポートはオープンドレイン動作がほしいのですが、FT232はプッシュプル動作になります。プッシュプルをオープンドレインにするにはいくつかの方法がありますが、今回は簡易的にダイオードを使う方式を使用します。
ダイオードを使用した場合、ダイオードの順方向電圧により0V付近まで下がりきりませんが、今回は問題ありませんでした。
パラサイトパワー
1wireはパラサイトパワーという動作モードが有り、これにより給電の配線が不要になります。1wireの特徴の1つに、この2線ケーブルで通信できるという点があります。
しかし、DS18B20では温度変換時に消費電力が増え、パラサイトパワーでは足りなくなります。そのような場合はストロングプルアップという機能を使うのですが、これはタイミング要求が非常に厳しく、10マイクロ秒以内にストロングプルアップを有効にしなければいけません。この時間を保証するにはRTOSでもかなり大変で、Windows等では望むべくもありません。
ということで、今回はパラサイトパワー/ストロングプルアップは使用せず、3線式で使うことにします。
回路
USBとUARTの変換はFT232で行ってしまうので、UARTと1wireの変換にはダイオード1本と抵抗1本が有れば足ります。今回はそれに加えてそれぞれを接続するコネクタも使用しています。
回路はこのようになります。
左側がFT232、右側がDS18B20になりますが、1wire側はコネクタのピン配置とDS18B20のピン配置が異なるので注意してください。
今回は小さな基板にFT232の6ピンヘッダと1wireに3ピンJST-EHを取り付け、その隙間にダイオード1本とチップ抵抗1個を取り付けました。
プログラム
今回はDS18B20をひとつだけバスに接続し、温度計測のみの機能をもたせました。
DS18B20 ds18b20 = new DS18B20("COM3");
const double interval_seconds = 10;
double seconds = 0;
DateTime prev_time = DateTime.Now;
while (true)
{
DateTime now = DateTime.Now;
seconds += (now - prev_time).TotalSeconds;
prev_time = now;
if (seconds > interval_seconds)
{
seconds -= interval_seconds;
ds18b20.convert_temperature();
System.Threading.Thread.Sleep(800);
float deg_C;
ds18b20.read_temperature(out deg_C);
Console.WriteLine(now.ToString("yyyy/MM/dd HH:mm:ss.ff") + " " + deg_C.ToString("+00.0;-00.0; 00.0") + " degC");
}
int sleep = (int)((interval_seconds - seconds) * 750);
System.Threading.Thread.Sleep(sleep > 10 ? sleep : 10);
}
class One_wire
{
private static int baudRate_reset_pulse = 18750;
private static int baudRate_bit_TR = 100 * 1000;
private readonly SerialPort serial_port;
public class BusErrorException : Exception
{
}
public One_wire(String port_name)
{
serial_port = new SerialPort(port_name);
serial_port.ReadTimeout = 50;
}
public byte calc_CRC(byte[] data, int length = 0)
{
if (length == 0)
{
length = data.Length;
}
byte crcsr = 0;
for (int i = 0; i < length; i++)
{
byte dat = data[i];
for (int j = 0; j < 8; j++)
{
crcsr = ((dat ^ crcsr) & 1) != 0 ? (byte)(((crcsr ^ 0x18) >> 1) | 0x80) : (byte)(crcsr >> 1);
dat >>= 1;
}
}
return (crcsr);
}
protected void Open()
{
serial_port.Open();
}
protected void Close()
{
serial_port.Close();
}
protected bool Reset()
{
serial_port.BaudRate = baudRate_reset_pulse;
serial_port.Write(new byte[1] { 0x00 }, 0, 1);
try
{
serial_port.ReadByte();
}
catch
{
serial_port.BaudRate = baudRate_bit_TR;
throw (new BusErrorException());
}
bool result;
try
{
serial_port.ReadByte();
result = true;
}
catch (TimeoutException)
{
result = false;
}
serial_port.BaudRate = baudRate_bit_TR;
return (result);
}
protected void write(byte data, bool read_data)
{
serial_port.Write(new byte[]
{
(byte)((data >> 0 & 1) != 0 ? 0xFF : 0x00),
(byte)((data >> 1 & 1) != 0 ? 0xFF : 0x00),
(byte)((data >> 2 & 1) != 0 ? 0xFF : 0x00),
(byte)((data >> 3 & 1) != 0 ? 0xFF : 0x00),
(byte)((data >> 4 & 1) != 0 ? 0xFF : 0x00),
(byte)((data >> 5 & 1) != 0 ? 0xFF : 0x00),
(byte)((data >> 6 & 1) != 0 ? 0xFF : 0x00),
(byte)((data >> 7 & 1) != 0 ? 0xFF : 0x00),
}, 0, 8);
if (read_data)
{
byte[] buff = new byte[8];
try
{
buff[0] = (byte)serial_port.ReadByte();
buff[1] = (byte)serial_port.ReadByte();
buff[2] = (byte)serial_port.ReadByte();
buff[3] = (byte)serial_port.ReadByte();
buff[4] = (byte)serial_port.ReadByte();
buff[5] = (byte)serial_port.ReadByte();
buff[6] = (byte)serial_port.ReadByte();
buff[7] = (byte)serial_port.ReadByte();
}
catch (TimeoutException)
{
throw (new BusErrorException());
}
}
}
protected void read(byte[] datas, int start, int length)
{
for (int i = 0; i < length; i++)
{
write(0xFF, false);
}
for (int i = 0; i < length; i++)
{
byte[] buff = new byte[8];
try
{
buff[0] = (byte)serial_port.ReadByte();
buff[1] = (byte)serial_port.ReadByte();
buff[2] = (byte)serial_port.ReadByte();
buff[3] = (byte)serial_port.ReadByte();
buff[4] = (byte)serial_port.ReadByte();
buff[5] = (byte)serial_port.ReadByte();
buff[6] = (byte)serial_port.ReadByte();
buff[7] = (byte)serial_port.ReadByte();
}
catch (TimeoutException)
{
throw (new BusErrorException());
}
byte data = 0;
if (buff[0] == 0xFF) { data |= 0x01; }
if (buff[1] == 0xFF) { data |= 0x02; }
if (buff[2] == 0xFF) { data |= 0x04; }
if (buff[3] == 0xFF) { data |= 0x08; }
if (buff[4] == 0xFF) { data |= 0x10; }
if (buff[5] == 0xFF) { data |= 0x20; }
if (buff[6] == 0xFF) { data |= 0x40; }
if (buff[7] == 0xFF) { data |= 0x80; }
datas[i] = data;
}
}
}
class DS18B20 : One_wire
{
public DS18B20(String portName) : base(portName)
{
}
public bool convert_temperature()
{
Open();
bool flag = true;
flag = flag &&
Reset();
if (flag)
{
write(0xCC, true);
write(0x44, true);
}
Close();
return (flag);
}
public bool read_temperature(out float temperature_degree_celsius)
{
temperature_degree_celsius = float.NaN;
Open();
bool flag = true;
flag = flag &&
Reset();
if (flag)
{
write(0xCC, true);
write(0xBE, true);
byte[] buff = new byte[9];
read(buff, 0, 9);
flag = buff[8] == calc_CRC(buff, 8);
if (flag)
{
temperature_degree_celsius = (Int16)(buff[0] | buff[1] << 8) / 16.0f;
}
}
Close();
return (flag);
}
}
複数の1wireデバイスを使用する場合、ROMアドレスを探索するアルゴリズムや、ROMアドレスを指定して通信する機能が必要になります。
結果
このように出力されます。
2018/07/14 08:09:41.51 +28.4 degC
2018/07/14 08:09:51.51 +28.4 degC
2018/07/14 08:10:01.51 +28.4 degC
2018/07/14 08:10:11.51 +28.5 degC
2018/07/14 08:10:21.51 +28.5 degC
2018/07/14 08:10:31.51 +28.5 degC
2018/07/14 08:10:41.51 +28.5 degC
2018/07/14 08:10:51.51 +28.4 degC
2018/07/14 08:11:01.51 +28.5 degC
2018/07/14 08:11:11.51 +28.5 degC
2018/07/14 08:11:21.51 +28.5 degC
2018/07/14 08:11:31.51 +28.5 degC
2018/07/14 08:11:41.51 +28.4 degC
2018/07/14 08:11:51.51 +28.4 degC
2018/07/14 08:12:01.51 +28.5 degC
2018/07/14 08:12:11.51 +28.5 degC
12日深夜からの2日間、室温のデータを取ってみました。
13日は曇であまり気温が上がりませんでした。それでも30℃は超えていたようです。14日は晴れていたこともあり、一瞬ですが31.8℃程度まで上がっています。
ウチでは1週間前まで寒くてストーブを使っていました。熱衝撃がヤバイ。
応用
UARTはマイコンではかなりポピュラーな通信プロトコルです。大抵のマイコンには搭載されていると思います。今回はPC/C#でプログラムを作りましたが、同じ考え方でマイコンでも使用することが可能です。
例えばSTM32であればTXピンをオープンドレインで駆動することも可能ですが、その場合はダイオードを省略できます。ただ、STM32はボーレートの変更が少し大変ですが。。。
その他
今回試していて気がついたのですが、DS18B20はストロングプルアップが足りないと温度が高く出る傾向があるようです。デジタルセンサとは言え、内部はアナログ回路なので、電源が不安定だと結果も不安定になるようです。温度の計測ができないとか、エラーフラグが立つとか、そういうわかりやすいエラーではないので、注意する必要があります。
以前こんな事がありました。
マイコンで温度センサから1秒に1回程度、温度を読み出して表示するプログラムを作りました。それをマイコンに書き込み、実行してグラフにした所、実行後からどんどん温度が上がっていき、1.5 - 2℃ほど上昇して安定しました。
サンプリングレートを変更したプログラムを書き込んでも、同じような傾きで温度が変化していきます。ということは、温度計測を行った時の消費電力増加による発熱ではない、と判断できます。
温度センサはマイコンがリセットされていても常に給電されているので、自己発熱の影響は無いはずです。
この場合、温度センサに影響を与えるものは何でしょうか? この温度センサはなにを計測しているのでしょうか?
試しに、RTOSのIdle Hookでマイコンをスリープに入るようにした所、温度センサの上昇はなくなりました。つまり、マイコンの発熱を、5cmとか10cm離れた場所に配線した温度センサが検出していたことになります。
人間の目では温度は見えませんし、これだけ離れてるなら影響はないだろう、それもたかが168MHzのマイコンの発熱なんて……と思っていましたが、実際には大きな影響を受けていました。
温度センサに限りませんが、何らかのセンサで現象を計測する場合は「なんのために計測するのか」をはっきりとさせ、「何を計測しているのか」を意識することが必要です。
そしてグラフを見る前に自分でどのような形になるかを予想し、予想と違う場合はなぜそのような差が生まれるかを考える必要があります。予想通りのグラフになっても、自分が測りたい現象が本当にそれなのか、ということも意識する必要があります。