【DigiKey好物畅享】Melexis MLX90640 简易热成像仪

本次你晒单,我买单活动另一个所选物料为来自Melexis的MLX90640热敏式图像传感器,其最大的功能特点便是红外热像,可以通过它来访问环境中的热状况。-40到85℃的工作温度范围,使其可以部署在复杂的工业环境中。同时MLX90640可以测量的温度范围为-40到300℃,精度1℃,免校准,具备IIC接口,外围电路简单,非常适合安装到嵌入式系统中。
本次设计主要使用来自Seeed的Wio Terminal开发板和来自Melexis的MLX90640红外热成像传感器来实现一个简易热成像仪,通过调整MLX90640的方向,使其对准不同物体来测量温度,同时在Wio Terminal的显示屏上显示热像图。
设计中主要用到的硬件设备包括Wio Terminal开发板、MLX90640红外热成像传感器模块(外围硬件简单,需要注意在IIC总线上添加上拉电阻,因为Wio Terminal硬件上没有,此处选择手焊方式)。具体实物如下图。

硬件设计上,MLX90640红外热成像传感器模块通过排针引出对应的引脚,并在IIC引脚添加上拉电阻,接着通过杜邦线转换为Grove接口。由于Wio Terminal开发板板载一个用于IIC的Grove接口,此处直接用Grove连接线直连即可。关于MLX90640红外热成像传感器模块的详细原理图,可以参考SparkFun对应的模块(SKU为SEN-14844)。

软件设计上,MLX90640红外热成像传感器模块与Wio Terminal开发板通过IIC总线通信,同时Wio Terminal开发板板载一个2.4寸的显示屏(使用SPI通信)。为了使用方便,引入Seeed_Arduino_MLX9064X和Seeed_GFX两个库,在Arduino IDE中安装这两个库后即可使用。下面为具体代码。

#include <Wire.h>
#include “MLX90640_API.h”
#include “MLX9064X_I2C_Driver.h”
#include <TFT_eSPI.h>

#define debug Serial
#define TA_SHIFT 8 //Default shift for MLX90640 in open air

TFT_eSPI tft = TFT_eSPI();
TFT_eSprite Display = TFT_eSprite(&tft);

const byte MLX90640_address = 0x33; //Default 7-bit unshifted address of the MLX90640
float mlx90640To[768];
uint16_t eeMLX90640[832];
uint16_t mlx90640Frame[834];
paramsMLX90640 mlx90640;

uint16_t MinTemp = 25;
uint16_t MaxTemp = 38;

// variables for interpolated colors
byte red, green, blue;

// variables for row/column interpolation
byte i, j, row, col;
float a, b, c, d, ii;
//byte aLow, aHigh;

// size of a display “pixel”
byte BoxWidth = 3;
byte BoxHeight = 3;

//int x, y;
char buf[20];

// variable to toggle the display grid
int ShowGrid = -1;

// array for the interpolated array
float HDTemp[6400];
//float HDTemp = 19.92;

void setup() {
Wire.begin();
Wire.setClock(400000); //Increase I2C clock speed to 400kHz

debug.begin(115200); //Fast debug as possible
while (!debug); //Wait for user to open terminal

if (isConnected() == false) {
    debug.println("MLX9064x not detected at default I2C address. Please check wiring. Freezing.");
    while (1);
}

//Get device parameters - We only have to do this once
int status;
status = MLX90640_DumpEE(MLX90640_address, eeMLX90640);
if (status != 0) {
    debug.println("Failed to load system parameters");
}

status = MLX90640_ExtractParameters(eeMLX90640, &mlx90640);
if (status != 0) {
    debug.println("Parameter extraction failed");
}

//Once params are extracted, we can release eeMLX90640 array

//MLX90640_SetRefreshRate(MLX90640_address, 0x02); //Set rate to 2Hz
MLX90640_SetRefreshRate(MLX90640_address, 0x03); //Set rate to 4Hz
//MLX90640_SetRefreshRate(MLX90640_address, 0x07); //Set rate to 64H

tft.begin();
tft.setRotation(3);
tft.fillScreen(TFT_BLACK);
Display.createSprite(220, 220);
//Display.createSprite(TFT_HEIGHT, TFT_WIDTH);
Display.fillSprite(TFT_BLACK); 

// get the cutoff points for the color interpolation routines
// note this function called when the temp scale is changed
Getabcd();

// draw a legend with the scale that matches the sensors max and min
DrawLegend(); 

}

void loop() {
Display.fillRect(10, 10, 220, 220, TFT_WHITE);
for (byte x = 0 ; x < 2 ; x++) {

    int status = MLX90640_GetFrameData(MLX90640_address, mlx90640Frame);

    float vdd = MLX90640_GetVdd(mlx90640Frame, &mlx90640);
    float Ta = MLX90640_GetTa(mlx90640Frame, &mlx90640);

    float tr = Ta - TA_SHIFT; //Reflected temperature based on the sensor ambient temperature
    float emissivity = 0.95;

    MLX90640_CalculateTo(mlx90640Frame, &mlx90640, emissivity, tr, mlx90640To);
}

// for (int x = 0 ; x < 768 ; x++) {
//     //if(x % 8 == 0) debug.println();
//     debug.print(mlx90640To[x], 2);
//     debug.print(",");
// }
// debug.println("");

interpolate_image(mlx90640To,24,32,HDTemp,80,80);

//display the 80 x 80 array
DisplayGradient();

//Crosshair in the middle of the screen
Display.drawCircle(115, 115, 5, TFT_PURPLE);
Display.drawFastVLine(115, 105, 20, TFT_PURPLE);
Display.drawFastHLine(105, 115, 20, TFT_PURPLE);
//Displaying the temp at the middle of the Screen

//Push the Sprite to the screen
Display.pushSprite(0, 0);

tft.setRotation(3);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.drawFloat(HDTemp[35 * 80 + 35], 2, 90, 20); 

}

//Returns true if the MLX90640 is detected on the I2C bus
boolean isConnected() {
Wire.beginTransmission((uint8_t)MLX90640_address);

if (Wire.endTransmission() != 0) {
    return (false);    //Sensor did not ACK
}
return (true);

}

void DisplayGradient() {

//tft.setRotation(3);

// rip through 70 rows
for (row = 0; row < 70; row ++) {

// fast way to draw a non-flicker grid--just make every 10 MLX90641To 2x2 as opposed to 3x3
// drawing lines after the grid will just flicker too much
if (ShowGrid < 0) {
  BoxWidth = 3;
}
else {
  if ((row % 10 == 9) ) {
    BoxWidth = 2;
  }
  else {
    BoxWidth = 3;
  }
}
// then rip through each 70 cols
for (col = 0; col < 70; col++) {

  // fast way to draw a non-flicker grid--just make every 10 MLX90641To 2x2 as opposed to 3x3
  if (ShowGrid < 0) {
    BoxHeight = 3;
  }
  else {
    if ( (col % 10 == 9)) {
      BoxHeight = 2;
    }
    else {
      BoxHeight = 3;
    }
  }

  // finally we can draw each the 70 x 70 points, note the call to get interpolated color
  Display.fillRect((row * 3) + 15, (col * 3) + 15, BoxWidth, BoxHeight, GetColor(HDTemp[row * 80 + col]));

}

}
}
// my fast yet effective color interpolation routine
uint16_t GetColor(float val) {

/*
pass in value and figure out R G B
several published ways to do this I basically graphed R G B and developed simple linear equations
again a 5-6-5 color display will not need accurate temp to R G B color calculation

equations based on
http://web-tech.ga-usa.com/2012/05/creating-a-custom-hot-to-cold-temperature-color-gradient-for-use-with-rrdtool/index.html

*/

red = constrain(255.0 / (c - b) * val - ((b * 255.0) / (c - b)), 0, 255);

if ((val > MinTemp) & (val < a)) {
green = constrain(255.0 / (a - MinTemp) * val - (255.0 * MinTemp) / (a - MinTemp), 0, 255);
}
else if ((val >= a) & (val <= c)) {
green = 255;
}
else if (val > c) {
green = constrain(255.0 / (c - d) * val - (d * 255.0) / (c - d), 0, 255);
}
else if ((val > d) | (val < a)) {
green = 0;
}

if (val <= b) {
blue = constrain(255.0 / (a - b) * val - (255.0 * b) / (a - b), 0, 255);
}
else if ((val > b) & (val <= d)) {
blue = 0;
}
else if (val > d) {
blue = constrain(240.0 / (MaxTemp - d) * val - (d * 240.0) / (MaxTemp - d), 0, 240);
}

// use the displays color mapping function to get 5-6-5 color palet (R=5 bits, G=6 bits, B-5 bits)
return Display.color565(red, green, blue);

}

// function to get the cutoff points in the temp vs RGB graph
void Getabcd() {

a = MinTemp + (MaxTemp - MinTemp) * 0.2121;
b = MinTemp + (MaxTemp - MinTemp) * 0.3182;
c = MinTemp + (MaxTemp - MinTemp) * 0.4242;
d = MinTemp + (MaxTemp - MinTemp) * 0.8182;

}
float get_point(float *p, uint8_t rows, uint8_t cols, int8_t x, int8_t y)
{
if (x < 0)
{
x = 0;
}
if (y < 0)
{
y = 0;
}
if (x >= cols)
{
x = cols - 1;
}
if (y >= rows)
{
y = rows - 1;
}
return p[y * cols + x];
}

void set_point(float *p, uint8_t rows, uint8_t cols, int8_t x, int8_t y, float f)
{
if ((x < 0) || (x >= cols))
{
return;
}
if ((y < 0) || (y >= rows))
{
return;
}
p[y * cols + x] = f;
}

// src is a grid src_rows * src_cols
// dest is a pre-allocated grid, dest_rows*dest_cols
void interpolate_image(float *src, uint8_t src_rows, uint8_t src_cols,
float *dest, uint8_t dest_rows, uint8_t dest_cols)
{
float mu_x = (src_cols - 1.0) / (dest_cols - 1.0);
float mu_y = (src_rows - 1.0) / (dest_rows - 1.0);

float adj_2d[16]; // matrix for storing adjacents

for (uint8_t y_idx = 0; y_idx < dest_rows; y_idx++)
{
    for (uint8_t x_idx = 0; x_idx < dest_cols; x_idx++)
    {
        float x = x_idx * mu_x;
        float y = y_idx * mu_y;
        get_adjacents_2d(src, adj_2d, src_rows, src_cols, x, y);

        float frac_x = x - (int)x; // we only need the ~delta~ between the points
        float frac_y = y - (int)y; // we only need the ~delta~ between the points
        float out = bicubicInterpolate(adj_2d, frac_x, frac_y);
        set_point(dest, dest_rows, dest_cols, x_idx, y_idx, out);
    }
}

}

// p is a list of 4 points, 2 to the left, 2 to the right
float cubicInterpolate(float p, float x)
{
float r = p[1] + (0.5 * x * (p[2] - p[0] + x * (2.0 * p[0] - 5.0 * p[1] + 4.0 * p[2] - p[3] + x * (3.0 * (p[1] - p[2]) + p[3] - p[0]))));
return r;
}

// p is a 16-point 4x4 array of the 2 rows & columns left/right/above/below
float bicubicInterpolate(float p, float x, float y)
{
float arr[4] = {0, 0, 0, 0};
arr[0] = cubicInterpolate(p + 0, x);
arr[1] = cubicInterpolate(p + 4, x);
arr[2] = cubicInterpolate(p + 8, x);
arr[3] = cubicInterpolate(p + 12, x);
return cubicInterpolate(arr, y);
}

// src is rows*cols and dest is a 4-point array passed in already allocated!
void get_adjacents_1d(float *src, float *dest, uint8_t rows, uint8_t cols, int8_t x, int8_t y)
{
// pick two items to the left
dest[0] = get_point(src, rows, cols, x - 1, y);
dest[1] = get_point(src, rows, cols, x, y);
// pick two items to the right
dest[2] = get_point(src, rows, cols, x + 1, y);
dest[3] = get_point(src, rows, cols, x + 2, y);
}

// src is rows*cols and dest is a 16-point array passed in already allocated!
void get_adjacents_2d(float *src, float *dest, uint8_t rows, uint8_t cols, int8_t x, int8_t y)
{
float arr[4];
for (int8_t delta_y = -1; delta_y < 3; delta_y++)
{ // -1, 0, 1, 2
float *row = dest + 4 * (delta_y + 1); // index into each chunk of 4
for (int8_t delta_x = -1; delta_x < 3; delta_x++)
{ // -1, 0, 1, 2
row[delta_x + 1] = get_point(src, rows, cols, x + delta_x, y + delta_y);
}
}
}

// function to draw a legend
void DrawLegend() {

//color legend with max and min text
j = 0;

float inc = (MaxTemp - MinTemp ) / 160.0;

for (ii = MinTemp; ii < MaxTemp; ii += inc) {
tft.drawFastHLine(260, 200 - j++, 30, GetColor(ii));
}

tft.setTextSize(2);
tft.setCursor(245, 20);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
sprintf(buf, “%2d/%2d”, MaxTemp, (int) (MaxTemp * 1.12) + 32);
tft.print(buf);

tft.setTextSize(2);
tft.setCursor(245, 210);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
sprintf(buf, “%2d/%2d”, MinTemp, (int) (MinTemp * 1.12) + 32);
tft.print(buf);

}

特别需要注意的是,Wio Terminal开发板的RAM为192KB,当在Setup函数中调用Display.createSprite()创建画布时,参数可以为320240,但此时一屏所占的内存约为150KB(显示模式为RGB565),再加上存储一帧热成像数据所占的内存为32244=3KB,用于插值处理的数组所占的内存为64004=25KB,此时会非常容易爆内存,导致显示不正常。因此设计中要考虑适当减小画布尺寸,例子中采用220*220的画布。下面为具体的运行效果。可以看到当MLX90640对准手掌上时,屏幕上的热像图会有变化并显示相应温度。

总结:mlx90640红外热成像传感器拥有一个32*24的成像阵列,同时视场角有110°和55°的版本,此外体积小巧和硬件电路简单的特点,相信一定会受到更多开发者的喜欢。