【DigiKey好物畅享】Melexis MLX90640 热成像仪开发实现记录

本文主要介绍MLX90640红外热成像传感器的原理特性、硬件设计方法、在mcu平台下的驱动移植流程以及数据读取与液晶屏可视化显示。MLX90640作为一款32×24像素的红外阵列传感器,能够实现非接触式温度测量和热成像功能。

一、MLX90640传感器介绍:

MLX90640是Melexis公司推出的一款32×24像素红外阵列传感器,能够在-40℃至300℃范围内实现非接触式温度测量。与传统的单点红外传感器不同,MLX90640通过阵列式设计,可以同时获取768个点的温度数据,从而构建出被测物体的热分布图像。

官方资料地址:远红外热传感器阵列(32x24 RES)I Melexis

该传感器的主要技术参数如下:

分辨率:32×24像素,共768个测温点;

视场角(FOV):两种视场角可选:55°*30°和110°*75°;

测温范围:-40℃至300℃;

刷新率:可编程刷新速率为0.5Hz至64Hz共8种档位;

通信接口:I2C,支持标准模式(100kHz)和快速模式(400kHz/1MHz)。

MLX90640的内部结构包含以下几个关键部分:

红外传感阵列:由32×24个热电堆探测器组成,每个探测器对应一个像素点。当红外辐射照射到探测器时,会产生微弱的电压信号,该信号与被测物体的温度相关。

EEPROM存储器:容量为8Kbit,用于存储出厂校准数据和用户可配置参数。每个传感器都经过工厂单独校准,校准参数包括:每个像素的灵敏度补偿系数,环境温度补偿参数,电源电压监测校准值,默认配置寄存器值。

RAM存储器:用于存储ADC转换后的原始测量数据。整个RAM区域分为两个页面:Page0和Page1,每个页面包含384个像素的数据。

传感器有两种数据更新模式:(1)棋盘模式(默认),每个像素间隔排列-像素交错模式。标准的MLX90640 是在棋盘模式下校准的,因此传感器在棋盘模式下具有更好的噪声 滤除性能。为了得到最好的结果,建议使用默认的棋盘模式。 (2)电视交错模式,( 隔行排列-行交错模式)。

二、硬件设计

下面是我设计的基于MLX90640的测量显示模块。板载LCD和MLX90640传感器,使用arduino接口。

最终成品实物如下:

三、软件编程设计

接下来调试MLX90640热成像传感器软件,将传感器温度点阵图像显示到LCD屏上。之前试过MLX90642型号,这次就可以直接套用。

首先下载MLX90640的软件库。驱动程序可从下面网页下载: GitHub - melexis/mlx90640-library: MLX90640 library functions · GitHub

将下载的mlx90640库加入工程,然后编写I2C接口驱动。下面是我基于MCXN236写的I2C驱动。

typedef struct mlx_handle

{

#if 1

ARM_DRIVER_I2C *i2c_driver;

volatile uint32_t i2c_event;

volatile bool i2c_event_received;

#else

mlx_i2c_send_func_t I2C_SendFunc;

mlx_i2c_receive_func_t I2C_ReceiveFunc;

#endif

} mlx_handle_t;

mlx_handle_t mlx90640_handle;

/* FUNCTION ************************************************************************************************************

*

* Function Name : LPI2C5_InitPins

* Description : Configures pin routing and optionally pin electrical features.

*

* END ****************************************************************************************************************/

void LPI2C5_InitPins(void)

{

/* Enables the clock for PORT4: Enables clock */

CLOCK_EnableClock(kCLOCK_Port1);

const port_pin_config_t port1_16_pinP1_config = {/* Internal pull-up resistor is enabled */

.pullSelect = kPORT_PullUp,

/* Low internal pull resistor value is selected. */

.pullValueSelect = kPORT_LowPullResistor,

/* Fast slew rate is configured */

.slewRate = kPORT_FastSlewRate,

/* Passive input filter is disabled */

.passiveFilterEnable = kPORT_PassiveFilterDisable,

/* Open drain output is disabled */

.openDrainEnable = kPORT_OpenDrainDisable,

/* Low drive strength is configured */

.driveStrength = kPORT_LowDriveStrength,

/* Pin is configured as FC2_P0 */

.mux = kPORT_MuxAlt2,

/* Digital input enabled */

.inputBuffer = kPORT_InputBufferEnable,

/* Digital input is not inverted */

.invertInput = kPORT_InputNormal,

/* Pin Control Register fields [15:0] are not locked */

.lockRegister = kPORT_UnlockRegister};

/* PORT1_16 is configured as FC5_P0 */

PORT_SetPinConfig(PORT1, 16U, &port1_16_pinP1_config);

const port_pin_config_t port1_17_pinP2_config = {/* Internal pull-up resistor is enabled */

.pullSelect = kPORT_PullUp,

/* Low internal pull resistor value is selected. */

.pullValueSelect = kPORT_LowPullResistor,

/* Fast slew rate is configured */

.slewRate = kPORT_FastSlewRate,

/* Passive input filter is disabled */

.passiveFilterEnable = kPORT_PassiveFilterDisable,

/* Open drain output is disabled */

.openDrainEnable = kPORT_OpenDrainDisable,

/* Low drive strength is configured */

.driveStrength = kPORT_LowDriveStrength,

/* Pin is configured as FC2_P1 */

.mux = kPORT_MuxAlt2,

/* Digital input enabled */

.inputBuffer = kPORT_InputBufferEnable,

/* Digital input is not inverted */

.invertInput = kPORT_InputNormal,

/* Pin Control Register fields [15:0] are not locked */

.lockRegister = kPORT_UnlockRegister};

/* PORT1_17 is configured as FC5_P1 */

PORT_SetPinConfig(PORT1, 17U, &port1_17_pinP2_config);

}

void LPI2C5_DeinitPins(void)

{

}

uint32_t LPI2C5_GetFreq()

{

return CLOCK_GetLPFlexCommClkFreq(5u);

}

static uint32_t MLX90642_WaitEvent(mlx_handle_t *handle)

{

uint32_t i2c_event;

while (!(handle->i2c_event_received)) ;

i2c_event = handle->i2c_event;

handle->i2c_event_received = false;

return i2c_event;

}

void MLX90642_I2C_MasterSignalEvent(uint32_t event)

{

mlx90640_handle.i2c_event = event;

mlx90640_handle.i2c_event_received = true;

}

void MLX90640_I2CInit(void)

{

LPI2C5_InitPins();

/* attach FRO 12M to FLEXCOMM5 */

CLOCK_SetClkDiv(kCLOCK_DivFlexcom5Clk, 1u);

CLOCK_AttachClk(kFRO12M_to_FLEXCOMM5);

Driver_I2C5.Uninitialize();

Driver_I2C5.Initialize(MLX90642_I2C_MasterSignalEvent);

Driver_I2C5.PowerControl(ARM_POWER_FULL);

Driver_I2C5.Control(ARM_I2C_BUS_SPEED, ARM_I2C_BUS_SPEED_FAST_PLUS);

mlx90640_handle.i2c_driver = &Driver_I2C5;

mlx90640_handle.i2c_event = 0;

mlx90640_handle.i2c_event_received = 0;

return ;

}

int MLX90640_I2CGeneralReset(void)

{

return MLX90640_NO_ERROR;

}

int MLX90640_I2CRead(uint8_t slaveAddr, uint16_t startAddress, uint16_t nMemAddressRead, uint16_t *rData)

{

status_t status = 0;

uint8_t i2c_buf[4];

uint32_t i,temp;

if (!(mlx90640_handle.i2c_driver))

{

return kStatus_InvalidArgument;

}

i2c_buf[0] = startAddress >> 8;

i2c_buf[1] = startAddress & 0xff;

if (mlx90640_handle.i2c_driver->MasterTransmit(slaveAddr, i2c_buf, 2, true) != ARM_DRIVER_OK)

{

status = -1;

}

else if (MLX90642_WaitEvent(&mlx90640_handle) != ARM_I2C_EVENT_TRANSFER_DONE)

{

status = -1;

}

else if (mlx90640_handle.i2c_driver->MasterReceive(slaveAddr, (uint8_t *)rData, nMemAddressRead*2, false) != ARM_DRIVER_OK)

{

status = -1;

}

else if (MLX90642_WaitEvent(&mlx90640_handle) != ARM_I2C_EVENT_TRANSFER_DONE)

{

status = -1;

}

for(i=0;i<nMemAddressRead;i++)

{

temp = (uint32_t) rData[i] | (rData[i]<<16);

rData[i] = (temp >> 8) & 0xffff;

}

return status;

}

int MLX90640_I2CWrite(uint8_t slaveAddr,uint16_t writeAddress, uint16_t data)

{

status_t status = 0;

uint8_t i2c_buf[4];

if (!(mlx90640_handle.i2c_driver))

{

return kStatus_InvalidArgument;

}

i2c_buf[0] = writeAddress >> 8;

i2c_buf[1] = writeAddress & 0xff;

i2c_buf[2] = data >> 8;

i2c_buf[3] = data & 0xff;

if (mlx90640_handle.i2c_driver->MasterTransmit(slaveAddr, i2c_buf, 4, true) != ARM_DRIVER_OK)

{

status = -1;

}

else if (MLX90642_WaitEvent(&mlx90640_handle) != ARM_I2C_EVENT_TRANSFER_DONE)

{

status = -1;

}

return status;

}

再就是在main中调用API函数初始化MLX90640,然后读取温度数据并在液晶屏上显示温度伪彩色图。

MLX90640_I2CInit();

// 从传感器EEPROM转储校准数据到 eeData 缓冲区

if (MLX90640_DumpEE(MLX90640_ADDRESS, eeData) != 0) {

PRINTF(“error\n”);

while(1);

}

// 从原始校准数据中提取参数,存入 mlx90640 结构体

if (MLX90640_ExtractParameters(eeData, &mlx90640) != 0) {

PRINTF(“error\n”);

while(1);

}

// 设置传感器刷新率,例如8Hz

MLX90640_SetRefreshRate(MLX90640_ADDRESS, 0x05); // 0x05 对应 16Hz

// 设置传感器工作模式为棋盘格模式 (推荐,精度更高) [citation:10]

// MLX90640_SetChessMode(MLX90640_ADDRESS); // 通常模式在初始化时已默认配置

While(1)

{

if(show_timeout == 0)

{

show_timeout = 100;

// 不断轮询,直到获取到一帧新的完整数据

// MLX90640_GetFrameData 会从传感器RAM中读取最新的一帧原始数据到 frameData

// 返回值表示当前是新帧的哪个子页 (0 或 1)

subpage = MLX90640_GetFrameData(MLX90640_ADDRESS, (uint16_t *)frameData);

// 检查返回值是否有效 (通常只需等待两个子页都更新一次)

if (subpage < 0)

{

// 读取失败,可能I2C通信有问题,稍等后重试

show_timeout = 10;

}else

{

// — . 计算环境温度 (Ta) 和 所有像素温度 (To) —

// 从刚获取的 frameData 中提取传感器自身的环境温度 [citation:10]

float ta = MLX90640_GetTa((uint16_t *)frameData, &mlx90640);

// 核心计算函数:使用原始数据、校准参数、发射率和环境温度,计算各像素温度

// 结果存入 pixels 数组 [citation:3][citation:8][citation:10]

MLX90640_CalculateTo((uint16_t *)frameData, &mlx90640, EMISSIVITY, ta, pixels);

if(frameData[833])

{

// — . 数据处理和显示 —

// 至此,pixels[0] 到 pixels[767] 包含了768个点的摄氏温度值

// 你可以在这里进行伪彩色处理、插值放大、LCD显示或通过串口发送到PC上位机 [citation:3]

temp_max=0x7fff,temp_min=0x7fff;

for(i=0;i<24;i++)

{

expand_dot_matrix(i,pixels,outimage);

lcd_draw_image(0,i*10,320,10,(uint8_t *)outimage);

}

sprintf((char *)outimage,"Min: %d.%2dC ",temp_min/100,temp_min%100);

lcd_disp_str_at(0,0,(const char *)outimage);

sprintf((char *)outimage,"Max: %d.%2dC ",temp_max/100,temp_max%100);

lcd_disp_str_at(120,0,(const char *)outimage);

}

}

}

}

对于温度扩展伪彩色图算法是之前使用MLX90642差不多的算法,将32*24点阵扩展到320*240显示。算法代码如下:

//分辨率扩展插值

// 0 1 2 3 4

// 5 6 7 8 9

//10 11 12 13 14

//15 16 17 18 19

//20 21 22 23 24

const uint16_t PointRateTable[25][10] =

{

{4,6,0,6,9,0,0,0,0}, //0

{2,8,0,3,12,0,0,0,0},

{0,10,0,0,15,0,0,0,0},

{0,8,2,0,12,3,0,0,0},

{0,6,4,0,9,6,0,0,0},

{2,3,0,8,12,0,0,0,0}, //5

{1,4,0,4,16,0,0,0,0},

{0,5,0,0,20,0,0,0,0},

{0,4,1,0,16,4,0,0,0},

{0,3,2,0,12,8,0,0,0},

{0,0,0,10,15,0,0,0,0}, //10

{0,0,0,5,20,0,0,0,0},

{0,0,0,0,25,0,0,0,0},

{0,0,0,0,20,5,0,0,0},

{0,0,0,0,15,10,0,0,0},

{0,0,0,8,12,0,2,3,0}, //15

{0,0,0,4,16,0,1,4,0},

{0,0,0,0,20,0,0,5,0},

{0,0,0,0,16,4,0,4,1},

{0,0,0,0,12,8,0,3,2},

{0,0,0,6,9,0,4,6,0}, //20

{0,0,0,3,12,0,2,8,0},

{0,0,0,0,15,0,0,10,0},

{0,0,0,0,12,3,0,8,2},

{0,0,0,0,9,6,0,6,4},

};

int16_t temp_max=0x7fff,temp_min=0x7fff;

int16_t temp_color_index = 0;

uint32_t in_matrix[10];

//32*24扩展160*120

//row 0-23 24行

//input 32*24x2 byte

//output 32*5*5x2 一行颜色点

void expand_dot_matrix(int16_t row, float *input,uint16_t *output)

{

int16_t x,i,j,n,index;

uint32_t temp;

for(x=0;x<MLX_WIDTH;x++)

{

//获取扩展点周围共9个点数值

n=0;

for(i=row-1;i<=row+1;i++)

{

if((i<0)||(i>=MLX_HEIGHT))

{

in_matrix[n++] = *(input + row * MLX_WIDTH + x) * 100;

in_matrix[n++] = *(input + row * MLX_WIDTH + x) * 100;

in_matrix[n++] = *(input + row * MLX_WIDTH + x) * 100;

}else

{

for(j=x-1;j<=x+1;j++)

{

if((j<0)||(j>=MLX_WIDTH))

{

in_matrix[n++] = *(input + row * MLX_WIDTH + x) * 100;

}else{

in_matrix[n++] = *(input + i * MLX_WIDTH + j) * 100;

}

}

}

}

//==========================================

//计算最低和最高温度

temp = *(input + row * MLX_WIDTH + x) * 100;

if(temp_max == 0x7fff) temp_max = temp;

if(temp_max < temp) temp_max=temp;

if(temp_min == 0x7fff) temp_min = temp;

if(temp_min > temp) temp_min=temp;

//每个点扩展5*5点

for(n=0;n<5;n++)

{

for(i=0;i<5;i++)

{

temp = 0;

for(j=0;j<9;j++)

{

temp += in_matrix[j] * PointRateTable[n*5+i][j];

}

// temp /= 25;

// index = n*MLX_WIDTH*5 + x*5 + i;

// *(output + index) = temp;

////////////////////////////////////////

//处理温度数据,转换成颜色点

temp /= (25*100*(40+10*temp_color_index)/255); //(0-40度范围0-255)

if(temp > 255) temp = 255;

//灰度转换

//temp = RGB(temp,temp,temp) | (RGB(temp,temp,temp) << 16); //交换颜色高低字节

//temp >>= 8;

//彩色转换

temp = Point2ColorTable[temp] | ((uint32_t)Point2ColorTable[temp] << 16); //交换颜色高低字节

temp >>= 8;

//======================================

//填充每个点数值颜色,每个点为2*2个点颜色

index = n*MLX_WIDTH*10 + x*5 + i;

*(output + 2*index) = temp&0xffff;

*(output + 2*index + 1) = temp&0xffff;

*(output + 2*index + MLX_WIDTH*10) = temp&0xffff;

*(output + 2*index + MLX_WIDTH*10+1) = temp&0xffff;

}

}

}

}

最终编译下载后就可以看到温度彩色图了。效果如下:

四、总结

非常有幸参加得捷的畅享好物活动,本次活动主要看到有MLX90640红外传感器,之前玩过MLX90642型号,觉得这个热成像传感器很不错,非常适合DIY一个热成像仪。这次调试这个mlx90640的时候发现与mlx90642还是有一些区别。首先是mlx90640的I2C地址是0x33,不是mlx90642的0x66。导致在使用时没注意卡住了很久,还以为器件坏了。后来仔细查看文档才发现地址弄错,这才搞定了传感器数据的读取。参考之前弄mlx90642的经验,后面很快就实现了传感器的热成像图像显示了。还有就是这个mlx90640型号的32*24点阵温度帧格式不是直接一次性读取的,而是棋盘模式(默认),每个像素间隔排列-像素交错模式,需要2次更新才能读取所有点阵数据,这个使用时要注意。