本文主要介绍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次更新才能读取所有点阵数据,这个使用时要注意。


