もくじ
→https://qiita.com/tera1707/items/4fda73d86eded283ec4f
WinIoT on ラズパイでのI2C通信関連
やりたいこと・やったこと
電子工作で、ラズパイ3にWindows IoT Coreを入れて、サーマルカメラをつなげて、いわゆるサーモグラフィを作ってみたい。
さっと調べたところ、一番安くて(amazonで7000円くらい)手に入りやすそうな「MLX90640」を使おうと思うが、そのサーマルカメラがI2C接続のようなので、以前、9軸センサで練習したI2Cを生かせそう。
と思い立って作ったのが下記のようなもの。作るうえでいろいろ調べた事、やったことをメモしておく。
■動画
https://youtu.be/cPm-WGY2Y8c
■コード一式
https://github.com/tera1707/ThermalCamera
使った機材
- RaspberryPi3
- ラズパイ用小型モニタQuimat 3.5インチ
- サーマルカメラMLX90640
全体の流れ
- 回路作成
- サーマルカメラとのI2C通信実装
- VisualStudio2019ソリューションの設定
- I2C通信のデータ送受信の準備
- 初期化処理
- EEPROM読み出し
- サーマル生データを取得
- 生データから温度に変換
- 温度データからサーマル画像作成
回路作成
下図のような回路をつくる。
実際の配線
電源は、作成中、デバッグ中はコンセント~USBで。実際動かすときはモバイルバッテリーでの予定。
サーマルカメラとのI2C通信実装
以前実験した9軸センサのI2Cをもとに、通信部分の実装を行う。
VisualStudio2019ソリューションの設定
9軸センサのI2Cの方で同じ作業をしているので、そちら参照。
I2C通信のデータ送受信の準備
データシートによると、このサーマルカメラは、2バイトを一単位としてデータのやり取りを行う。
9軸センサ(MPU-9150)の場合は、1バイト単位でデータをやり取りしていたので、そこが違う。
9軸センサの場合は、下記のようにしていた。
// 書き込み:1バイト目に書き込みたいレジスタアドレス、2バイト目に書く内容を載せて送信
WriteBuf = new byte[] { 0x6B, 0x00 };
I2CAccel.Write(WriteBuf);
// 読み込み:1バイト目に読み込みたいレジスタアドレスを載せておくると、
// そのアドレスを先頭にした、指定バイト数分のデータが返ってくる
// (バイト数の指定は、ReadBufの配列数で行う(下記の場合はnew byte[1]なので1バイト))
WriteBuf = new byte[] { 0x75 };
ReadBuf = new byte[1];
I2CAccel.WriteRead(WriteBuf, ReadBuf);
今回のサーマルカメラでは、下記のようなメソッドを作って、ushortでアドレス指定、データ指定を行うようにした。
// 書き込み
private void WriteRegisterData(ushort writeAddr, ushort data)
{
// 書き込むデータ作成(最初の2バイトが書き込み先アドレス、その後の2バイトがそこに書き込むデータ)
var writeByteData = new byte[]
{
(byte)(writeAddr / 0x100), (byte)(writeAddr % 0x100), // 書き込み先アドレス
(byte)(data / 0x100), (byte)(data % 0x100), // 書き込みデータ
};
// 書き込み実施
I2CThermalCamera.Write(writeByteData);
}
// 読み込み
private ushort[] ReadRegisterData(ushort readAddr, int NumberOfData)
{
// 返すデータ(受信したbyteデータをushortに直したもの)
ushort[] ret = new ushort[NumberOfData];
// アドレスを上位/下位に分解
var destAddr = new byte[] { (byte)(readAddr / 0x100), (byte)(readAddr % 0x100) };
// 受信用バッファを確保(このサーマルカメラのレジスタは1つで2バイト)
var readBuf = new byte[NumberOfData * 2];
// 読み込み実施
I2CThermalCamera.WriteRead(destAddr, readBuf);
// 読み込んだbyteデータをushortに直す
for (int i = 0; i < NumberOfData; i++)
{
ret[i] = (ushort)(readBuf[2 * i] * 0x100 + readBuf[2 * i + 1]);
}
return ret;
}
初期化処理
下記のような流れで初期化を行う。
(というか、I2cDevice.FromIdAsync();
まではWinIotのI2C通信の準備)
public async Task InitThermalCamera()
{
// すべてのI2Cデバイスを取得するためのセレクタ文字列を取得
string aqs = I2cDevice.GetDeviceSelector(I2cDeviceName);
DeviceInformationCollection dis = null;
try
{
// セレクタ文字列を使ってI2Cコントローラデバイスを取得
dis = await DeviceInformation.FindAllAsync(aqs);
if (dis.Count == 0)
{
Debug.WriteLine("I2Cコントローラデバイスが見つかりませんでした");
return;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
// I2Cアドレスを指定して、デフォルトのI2C設定を作成する
var settings = new I2cConnectionSettings(ThermalCameraI2CAddress);
// バス速度を設定(FastMode:400 kHz)(指定しないと、標準設定(StandardMode:100kHz)になる)
settings.BusSpeed = I2cBusSpeed.FastMode;
// 取得したI2Cデバイスと作成した設定で、I2cDeviceのインスタンスを作成
I2CThermalCamera = await I2cDevice.FromIdAsync(dis[0].Id, settings);
if (I2CThermalCamera == null)
{
Debug.WriteLine(string.Format("スレーブアドレス {0} の I2C コントローラー {1} はほかのアプリで使用されています。他のアプリで使用されていないか、確認してください。", settings.SlaveAddress, dis[0].Id));
return;
}
// サーマルカメラの設定
try
{
// コントロールレジスタを取得
var ctrreg = ReadRegisterData(0x800D, 1).FirstOrDefault();
// リフレッシュレートを変更する(現在のコントロールレジスタを読み出して、そいつに対して変更を実施)
var ctrregset = (ushort)(ctrreg | 0x0300);
WriteRegisterData(0x800D, ctrregset);
}
catch (Exception ex)
{
Debug.WriteLine("デバイスとの通信に失敗しました。: " + ex.Message);
return;
}
// EEPROM読み出し
this.MLX90640_DumpEE();
}
ここで「リフレッシュレートを変更」しているが、今回は32Hzを使用した。(最速の64Hzにしなかったことに特に理由なし。)
試したところ、リフレッシュレートの設定によって、下のデータ取得のところで出てくる「isReady」フラグ(0x8000のbit3)がONになるまでの時間が変わってくるので注意。
※以降の内容について
ここから下は、ほぼサンプルプログラムを基に作成しています。
⇒https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example
サンプルはC言語で描かれていますので、それをもとに、C#の処理を作成しました。
また、EEPROMの個別の中身や生データから温度に変換する計算など、データシートをじっくり読めばわかるのかもしれませんが、今回はあまり理解せずに(というか難しくてすぐに理解できなかった)サンプルを基に作らせて頂いてます。
EEPROM読み出し
上の初期化の中の一番下、下記の部分。
// EEPROM読み出し
this.MLX90640_DumpEE();
EEPROMに保存されているパラメータが、読み出したサーマルデータの生データを温度の値に変換する際に使われるので、EEPROMから各種パラメータを読みだして、所定のクラスに格納しておく。(ここではParamsMLX90640
クラスにいれた。)
データシートより、EEPROMはレジスタ0x2400番地から0x273F番地。
※EEPROMの読み出し項目は非常に多数あるので、コードは、githubのこちらを参照。
サーマル(生)データを取得 ~ 生データを温度データに変換
まずは、I2Cでサーマルの生データを取得する。
生データは温度データではないので、EEPROMから読み出したパラメータを使って温度に変換が必要。
データ取得は、データシートの「Measurement Flow」の項の流れに沿った処理を行う。
public double[] GetTemperatureData()
{
for (int i = 0; i < 2; i++)
{
byte isReady = 0;
while (isReady == 0)
{
// ステータスレジスタ取得
isReady = (byte)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0008);
}
//// ステータスレジスタ書き込み(MeasurementStartをON)
WriteRegisterData(0x8000, 0x0030);
// アIRデータ取得
// 0x0400~0x06FF:IRデータ
// 0x0700~0x070F:Ta_Vbe、CP.GAIN
// 0x0720~Ta_PATA,CP,VddPix
var frameDataS = ReadRegisterData(0x0400, FrameDataLength);
// ステータスレジスタ読み出し(SubPage番号)
//ReadRegisterData(0x8000, 1);
StatusRegister = (ushort)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0001);
// コントロールレジスタ読み出し
ControlRegister = ReadRegisterData(0x800D, 1).FirstOrDefault();
/////////////////////////
// データ読み出し終了、データから温度への変換計算実施
/////////////////////////
var ta = this.MLX90640_GetTa(frameDataS, CamParameters);
double tr = ta - 8;
double[] ret = new double[FrameDataLength];
// 生データを温度データに変換
MLX90640_CalculateTo(frameDataS, CamParameters, 0.95, tr, ret);
for (int l = 0; l < frameDataS.Length; l++)
{
if (ret[l] > 0.0)
{
TotalFrameData[l] = ret[l];
}
}
}
return TotalFrameData;
}
生データ取得
メソッドMLX90640_CalculateTo()
より上は、生データ取得処理。
生データを取る際、for分で2回回している。
データを採る際、「Subpage」という番号も一緒にとるのだが、これで全体のどの部分のデータが取れているかがわかる。
具体的には、データは下記の図のようなイメージで2回とって1画面分のデータとなる。
ステータスレジスタの設定により2パターンの取れ方があるが、今回は上のパターン(横一行分のデータが1行飛ばしで取れるパターン)を使った。つまり、subPageが0のデータは奇数行目のデータ、subPageが1のデータが1のデータは偶数行目のデータとなっている。
温度データに変換
そのsubPageの値と、取れてきた1行飛ばしのデータは、生データを温度データに変換するメソッドMLX90640_CalculateTo()
の中で使っている。(中の計算ロジックは難しいのであまり見ず。)
変換した温度データを、最終的に配列に格納。(ここではTotalFrameData[]
)
この配列の中身が、サーマルカメラに映った画面(32*24)の1ピクセルごとの温度の値となる。
温度データからサーマル画像作成
温度データを画面に表示するための値にさらに変換する。
画面に温度を表す点を打つ方法は、こちらの以前の記事を参照。
private async Task<BitmapImage> DoubleToRaindowColor(double[] totalFrameData)//temp:温度の値
{
int width = 32;
int height = 24;
byte[] data = new byte[width * height * 4];
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
// 指定の温度下限~上限の値を、0.0~1.0の値に変換する
var v = TemperatureTo0to1Double(totalFrameData[i + j * width]);
// 0.0~1.0の値を、虹色を表すバイト列に変換する
var c = ColorScaleBCGYR(v);
data[4 * (i + j * width)] = c.Item4; // Blue
data[4 * (i + j * width) + 1] = c.Item3; // Green
data[4 * (i + j * width) + 2] = c.Item2; // Red
data[4 * (i + j * width) + 3] = c.Item1; // alpha
}
}
// サーマル画像を作成
WriteableBitmap bitmap = new WriteableBitmap(width, height);
InMemoryRandomAccessStream inMRAS = new InMemoryRandomAccessStream();
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, inMRAS);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96.0, 96.0, data);
await encoder.FlushAsync();
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.SetSource(inMRAS);
return bitmapImage;
}
上記の中の、指定の温度下限~上限の値を、0.0~1.0の値に変換するメソッドColorScaleBCGYR()
は、こちらのサイトを参考にさせていただいています。ありがとうございます。
完成
これで作成したbitmapを画面に表示したら、サーモグラフィの完成。
意外となめらかに動いているが、サーマルカメラの解像度が3224と結構低いので、モザイク状に見える。最初、解像度、一桁間違えてないか?320240の間違いでは?と思ったが、32*24であってた。
サーマルカメラは、割と解像度が低いものらしい。(高いものには、もっと高解像度のものもあるが、手が出ない)
コード一式
ここまでで挙げてきた初期化やらデータ取得やらのメソッド以外にも、画面だったりC++のサンプルをもとに作ったEEPROM読み込み機能やらが多数ある。下記に一式置いているので参照ください。
■191116 追記
コンセントから離れても動かせるよう、携帯の乾電池式充電器に接続。
あとサーマルカメラもブランブランならないよう、手抜きだが形だけユニバーサル基盤にくっつけて一旦のできあがり。
電気電子やらメカのプロの方からしたら怒られそうだが、個人的には手作り感満載で、落としたら一撃死しそうな感じがたまらなく良い。
↓↓↓敬礼する、メガネをかけた嫁
※真ん中の四角の中の、3*3マスの平均温度を画面下に表示しているのだが、
体温が異常に低く見えてる。放射率?とかパラメータ調整必要なのかも。
参考
公式ページ
https://shop.pimoroni.com/products/mlx90640-thermal-camera-breakout
データシート
https://cdn.sparkfun.com/assets/7/b/f/2/d/MLX90640-Datasheet-Melexis.pdf
値の大きさをサーモグラフィのような色に変換する
https://qiita.com/krsak/items/94fad1d3fffa997cb651
サンプルプログラム(arduino)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/tree/master/Firmware
通信手順(どういうデータが取れるか、とか通信のお作法の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_API.cpp
通信ドライバ(データの送り方の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_I2C_Driver.cpp