基于MLX90640的红外热成像仪

基于MLX90640的红外热成像仪

1. 引言

红外热成像技术近年来在工业检测、医疗诊断、消防救援和自动化领域得到了广泛应用。作为一种非接触式温度测量和图像生成技术,红外热成像仪能够实时捕获物体表面的温度分布,并以可视化的形式直观呈现。这种特性使其在环境复杂、温度变化敏感的场景中具有不可替代的优势。

MLX90640是一款高性能的红外热成像传感器,采用 32×24 的像素阵列,能够在较大的视场角下提供精确的热图数据。相比于传统的单点红外温度传感器,MLX90640 具有数据量大、分辨率适中和体积小巧的优点,非常适合便携式热成像设备的开发,同时,MLX90640 更加经济且易于集成,适合便携式设备开发以及资源受限场景下的应用。

本项目以STM32F407微控制器为核心,结合MLX90640红外热成像传感器和 LVGL 图形库,设计并实现了一款便携式红外热成像仪,为红外热成像仪的低成本开发提供了一种可行的实现路径。同时以舵机为例展示了本项目的可扩展性,也为进一步的功能扩展,如添加数据存储、无线传输或AI温度分析等功能提供了技术基础。下面我们将以实验平台与模块介绍、硬件连接、技术要求、系统架构、软件设计、实验效果和结论七个方面阐述基于MLX90640的红外成像仪设计与实现。最后,我们将给出在设计过程中所涉及到的开源资料以及本项目的开源代码和资料。

2. 实验平台与模块介绍

本实验以MLX90640红外热成像传感器为核心,结合STM32F407微控制器和LVGL图形界面库,实现实时红外热成像的采集、处理与显示。实验平台主要由以下模块构成:

2.1 核心处理单元:STM32F407

STM32F407 是基于 ARM Cortex-M4 内核的微控制器,具有以下特点:

  • 主频高达 168 MHz,支持复杂计算和高速数据处理。
  • 1MB Flash 和 192KB RAM,适合大规模数据存储和处理。
  • 提供丰富的外设接口(I2C、SPI、UART 等),便于与 MLX90640 和显示设备通信。
  • 具有 DMA 控制器和浮点运算单元,适合处理 MLX90640 输出的大量数据。

2.2 红外传感模块:MLX90640

MLX90640 是一款高分辨率的红外热成像传感器,适用于远距离非接触温度测量,主要特性如下:

  • 分辨率:32 × 24 像素,覆盖视场角为 55°× 35° 或 110°× 75°。
  • 测量范围:-40°C 至 300°C,精度 ±1°C(典型)。
  • 输出数据帧率高达 64 Hz,支持实时热成像。
  • 通过 I2C 接口与主控芯片通信,传输红外图像数据。
MLX90640

2.3 图形界面:LVGL

LVGL(Light and Versatile Graphics Library)是一个开源的嵌入式图形界面库,用于开发低资源占用的用户界面:
- 提供丰富的 UI 控件,支持多种交互方式(触摸、按键等)。
- 具有高效的内存管理和动画功能,适合嵌入式显示设备。
- 可运行在 STM32F407 上,与 TFT LCD 显示屏结合,实现热成像的实时图形化显示。

LVGL

2.4 显示屏与外设模块

  • TFT LCD 屏幕:用于显示热成像图和温度信息。使用4.3英寸的屏幕,分辨率为800 x 480,支持SPI通信。
  • 触摸屏模块:用于调整显示设置、温度信息。
  • 舵机模块:选用SG90舵机,3.3V供电,用于模拟瞄准操作,范围为0-90度,工作扭矩2KG。
  • 电源模块:提供稳定的3.3V和5V电压,用于驱动 STM32 和 MLX90640。
SG90

3. 硬件连接

  • STM32F407和LCD显示屏: 通过 SPI 或并口连接,使用 DMA 提高传输效率。
  • MLX90640和STM32F407: 通过串口连接,I2C驱动已集成至传感器中。
  • SG90和STM32F407: 使用PWM驱动,改变频率以改变角度。
  • 电源供电: 使用电源模块为 STM32 和 MLX90640 提供稳定的电压。

4. 技术要求

4.1 传感器数据处理

  • 数据采集: 实现MLX90640的I2C通信、实现串口通信,获取原始红外数据。
  • 温度解码: 根据传感器数据格式,计算对应的温度值矩阵。
  • 数据统计: 根据温度数据,绘制直方图,计算平均温度、方差等数据。

4.2 界面开发

  • 基于LVGL的用户界面
    • 显示实时热成像图。
    • 显示最高温和最低温数值,并标记。
    • 绘制温度直方图。
    • 绘制最高温度点。
  • 交互功能: 通过触摸屏切换模式和显示参数。

5. 系统架构

5.1 硬件架构

硬件架构
  • STM32F407: 作为核心处理单元,负责数据采集、处理和显示。
  • MLX90640: 作为红外热成像传感器,负责采集红外热成像数据。
  • TFT LCD: 用于显示热成像图和温度信息。
  • 触摸屏: 用于调整显示设置、温度信息。
  • SG90: 用于模拟瞄准操作,将最高温度点对准中心。
  • 电源模块: 为 STM32 和 MLX90640 提供稳定的电压。

5.2 软件架构

软件架构
  • LVGL: 用于绘制用户界面,显示热成像图、温度信息和直方图。
  • 数据采集与处理: 通过串口接收 MLX90640 输出的原始温度数据帧,解析数据并生成温度矩阵。
  • 数据处理与显示: 将温度矩阵映射为热图像素数据,通过LVGL绘制热图到TFT LCD上,显示温度统计信息、直方图、舵机瞄准示意图等UI。
  • 舵机驱动: 根据最高温度点的坐标映射为舵机角度,设置定时任务,驱动舵机瞄准。

5.3 运行流程

5.3.1 数据采集流程

  1. STM32 初始化UART5串口,与 MLX90640 建立通信。
  2. 采集 MLX90640 输出的原始温度数据帧。
  3. 校准和处理数据,生成温度矩阵。

5.3.2 数据处理与显示流程

  1. 温度矩阵经过映射生成热图像素数据。
  2. 使用双缓冲机制,通过LVGL将热图绘制到TFT LCD上。
  3. 绘制温度统计信息、直方图、舵机瞄准示意图等UI。
  4. 根据用户输入调整显示参数。

5.3.3 舵机驱动流程

  1. 等待温度数据接收完毕,生成温度矩阵。
  2. 将坐标映射为舵机角度。
  3. 设置定时任务,驱动舵机瞄准。

6. 软件设计

6.1 数据采集与处理

6.1.1 数据采集

使用STM32F407的UART5模块与MLX90640进行通信,通过I2C协议获取红外热成像数据。

通信协议:
串口:

  1. 串口通信参数(默认波特率 115200,数据位 8,停止位 1,无校验)
  • 波特率 9600,数据位 8,停止位 1,无校验。
  • 波特率 115200,数据位 8,停止位 1,无校验。
  • 波特率 460800,数据位 8,停止位 1,无校验。
  1. 数据帧格式
  • 数据帧长度 1544 字节,包括 1536 字节的温度数据。
  • 数据帧格式:
    • 帧头 2 字节:0x5A 0x5A。
    • 数据量 2 字节:数据量低 8 位,数据量高 8 位。
    • 温度数据 1536 字节:32 × 24 像素,每个像素 2 字节,低位在前。
    • 自身温度 2 字节:低位在前。
    • 帧尾 2 字节:校验和,低位在前。

字节代表含义:

  • byte0~1: 帧头
  • byte2~3: 该帧数据量=byte3 * 256 + byte2
  • byte4~1539
    温度点阵,目标物体768个点的温度,每两个字节为一个温度,该温度是实际温度的100倍;
    例如:点 1 的温度 = (byte5 * 256 + byte4) / 100
    点 768 的温度 = (byte1539 * 256 + byte1538) / 100
  • byte1540~1541
    MLX90640 自身温度的100 倍。也可当作环境温度。
    TA = (byte1541 * 256 + byte1540) / 100
  • byte1542~1543: 帧尾,前771个字的累加和,保留16bit。

在STM32F407中,通过UART5接收数据,然后解析数据帧,提取温度数据,于是在串口中断中进行数据处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void UART5_IRQHandler(void)
{
if (USART_GetITStatus(UART5, USART_IT_RXNE) != RESET) // 若接收数据寄存器满
{
USART_ClearITPendingBit(UART5, USART_IT_RXNE);
Uart5.Rxbuf[Uart5.RXlenth] = USART_ReceiveData(UART5);

switch (Uart5.RXlenth)
{
case 0:
if(Uart5.Rxbuf[0] != 0x5a)
{
Uart5.RXlenth = 0;
}else{
Uart5.RXlenth++;
}
break;
case 1:
if(Uart5.Rxbuf[1] != 0x5a)
{
Uart5.RXlenth = 0;
}else{
Uart5.RXlenth++;
}
break;
case 2:
if(Uart5.Rxbuf[2] != 0x02)
{
Uart5.RXlenth = 0;
}else{
Uart5.RXlenth++;
}
break;
case 3:
if(Uart5.Rxbuf[3] != 0x06)
{
Uart5.RXlenth = 0;
}else{
Uart5.RXlenth++;
}
break;
default:
Uart5.RXlenth++;
if(Uart5.RXlenth == 1544)
{
if(is_update){
for(int i = 0; i < 1544; i++)
{
mlx90640_buf[i] = Uart5.Rxbuf[i];
}
if(disp_finish_flag){ // 显示完成
update_buffer(&mlx90640_buf[4]); // 更新缓冲区
disp_finish_flag = 0;
}
}
Uart5.ReceiveFinish = 1;
state = 1;
test < Uart5.RXlenth ? test = Uart5.RXlenth : test;
Uart5.RXlenth = 0;
}
break;
}
}
}
首先判断帧头是否正确,校验帧头,然后判断数据量是否正确,经过计算数据量为1544,转换为16进制为0x0602,若校验失败则舍弃数据,重新开始接收。接收完毕后,将数据存入缓冲区mlx90640_buf,将mlx90640_buf传入update_buffer函数更新显示缓冲区,以便后续显示,最后将接收完成标志位置1,进入数据处理状态。

  1. 温度解码
温度矩阵

数据放大倍数:
100,即实际温度为数据除以100。

数据解析:

  • 数据帧中,每两个字节表示一个温度值,低位在前。

  • 温度值为实际温度的100倍,需除以100得到实际温度。

  • 例如一帧数据
    5A5A-0206-6E0E-690E-5A0E-XXXX-050E-8D0E-D540
    Byte0~ Byte1---0x5A0x5A 表示帧头;
    Byte2~ Byte3---0x0206 表示数据量=0x06*256+0x02=1538 个温度数据(包括目标数据和MLX90640 自身温度数据)
    Byte4~ Byte1539---表示上图中768 个点的温度数据,输出顺序一次为(Col 1,Row 1)->(Col 32,Row 1)->(Col 1,Row 2)->(Col 32,Row 2)->(Col 1,Row XX)->(Col 32,Row XX)->(Col 1,Row 24)->(Col 32,Row 24)

  • 例子
    一帧数据 5A5A-0206-6E0E-690E-5A0E-XXXX-050E-8D0E-D540

    \[ \begin{align*} T_{Col1, Row1} = \frac{0x0E*256+0x6E}{100} = 36.94 \\ T_{Col2, Row1} = \frac{0x0E*256+0x69}{100} = 36.89 \\ \dots \\ T_{Col32, Row24} = \frac{0x0E*256+0x50}{100} = 36.64 \end{align*} \]

    Byte1540---Byte1541表示MLX90640自身温度数据。

    \[ TA = \frac{0x0E*256+0x8D}{100} = 37.25 \]

    Byte1542---Byte1543表示前771个字的累加和, 每个字为16bit。
    字 1=0x5A5A
    字2=0x0602(即数据量)
    字 3=0x0E6E(即点1的温度数据)
    ...
    字 770=0x0E05(即点768 的温度数据)
    字 771=0x0E8D(即MLX90640 的温度数据)

    校验和=字1+字2+字3+字XX+字700+字771=Byte1543*256+Byte1542

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
uint8_t Check(uint8_t *data)
{
uint16_t sum=0,length=0,i=0;
uint16_t temp=0;
length=((uint16_t )mlx90640_buf[3]<<8)|mlx90640_buf[2]+6;
if(length>1544)//超过上传数据
return 0;
for(i=0; i<length-2; i=i+2)
{
temp=((uint16_t )mlx90640_buf[i+1]<<8)|mlx90640_buf[i];
sum+=temp;
}
temp=((uint16_t )mlx90640_buf[i+1]<<8)|mlx90640_buf[i];
if(sum==temp)
{
// memcpy(data,mlx90640_buf,length);
for(i=0; i<length; i++)
{
data[i]=mlx90640_buf[i];
// printf("%x", data[i]);
}
return 1;
}
else
return 0;
}

我们在代码中定义了一个Check函数,用于校验数据帧的正确性,校验和为前771个字的累加和,若校验和与最后两个字节相等,则校验成功,返回1,否则返回0,在串口数据接收完成后,在处理数据之前调用Check函数进行校验。

1
2
temp=((int16_t)data_buf[i*2+1+4]<<8|data_buf[i*2+4]);
Temperature=(float)temp * 0.01f;

之后我们将数据解析为实际温度,即将两个字节的数据转换为实际温度,如上所示,将两个字节的数据转换为16位整型,然后乘以0.01即可得到实际温度,通过这种方式解析数据,我们可以得到一个32*24的温度矩阵。

  1. 指令

发给模块的指令为四字节:
波特率设置指令:

  • 9600 设置指令-----------0xA5+0x15+0x01+0xBB
  • 115200 设置指令---------0xA5+0x15+0x02+0xBC
  • 460800 设置指令---------0xA5+0x15+0x03+0xBD

模块更新频率设置指令:

  • 0.5hz 设置指令---------0xA5+0x25+0x00+0xCA
  • 1hz 设置指令-----------0xA5+0x25+0x01+0xCB
  • 2hz 设置指令-----------0xA5+0x25+0x02+0xCC
  • 4hz 设置指令-----------0xA5+0x25+0x03+0xCD
  • 8hz 设置指令-----------0xA5+0x25+0x04+0xCE

自动/查询设置指令:

  • 查询输出数据指令-------0xA5+0x35+0x01+0xDB
  • 自动输出数据指令-------0xA5+0x35+0x02+0Xdc
  • 发射率设置指令: 0xA5+0x45+0xXX+sum(8bit校验和)

保存设置指令:

  • 保存设置指令-------------0xA5+0x65+0x01+0x0B
  • 保存设置指令:表示将当前的波特率设置、模块更新频率设置、自动/查询和发射率设置保存到flash中,重启后按照保存的设置运行。

I2C模式, 把模块PS接GND或者SET点焊接即可。

I2C驱动代码太长,这里不再赘述,可以在github这里这里查看源码。

6.1.2 数据处理

温度统计

我们可以通过统计温度矩阵中的数据,得到一些有用的信息,如最高温度、最低温度等,这里我们主要关注以下几个数据:

  • 最高温度:统计温度矩阵中的最大值。
  • 最低温度:统计温度矩阵中的最小值。
  • 平均温度:统计温度矩阵中的平均值。
  • 温度方差:统计温度矩阵中的方差。
  • 温度分布:绘制温度直方图,显示温度分布情况。

最高温度、最低温度很容易统计,只需要遍历温度矩阵,找到最大值和最小值即可。为了提高效率,我们可以在数据接收完成后,在计算温度时直接统计最高温度和最低温度,然后在显示时直接显示,这样就不再需要重新遍历温度矩阵。

通常情况下,平均温度和方差的计算需要遍历温度矩阵,计算平均值和方差。这里我们可以使用增量法计算平均值和方差,这样可以减少遍历次数,一边遍历一边计算,提高效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for(int i=0; i<768;)
{
// 增量法计算均值和方差
temp=((int16_t)data_buf[i*2+1+4]<<8|data_buf[i*2+4]);
Temperature=(float)temp * 0.01f;
delta = Temperature - mean_temp;
mean_temp += delta / (i + 1);
variance_temp += delta * (Temperature - mean_temp);

if(Temperature>max_temp){
max_temp_x = i % 32;
max_temp_y = i / 32;
max_temp=Temperature;
}
if(Temperature<min_temp){
min_temp_x = i % 32;
min_temp_y = i / 32;
min_temp=Temperature;
}
i++;
// 将竖行的数据加起来
chart_cnt[i%32] += Temperature;
}
variance_temp /= 768;

我们定义了chart_cnt数组,用于存储每一列的温度值,然后在遍历温度矩阵的过程中,将每一列的温度值加起来,这样就可以得到每一列的温度总和,然后在显示直方图时,只需要将chart_cnt数组中的数据绘制出来即可。

但是屏幕分辨率有限,我们无法将所有的温度值都显示出来,所以我们需要对温度值进行映射,将温度值映射到屏幕上,这样就可以显示出温度分布情况,也就是对温度值进行归一化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for(uint8_t i = 0;i<32;i++){
if(max_chart_cnt < chart_cnt[i]){
max_chart_cnt = chart_cnt[i];
}
if(min_chart_cnt > chart_cnt[i]){
min_chart_cnt = chart_cnt[i];
}
}
// reset
for(uint8_t i = 0;i<32;i++){
chart_data[i] = myMap(chart_cnt[i], min_chart_cnt, max_chart_cnt, 0, 90);
if(chart_max_value < chart_data[i]){
chart_max_value = chart_data[i];
// chart_max_x = 10 + i * 310 / CHART_SIZE;
}
if(chart_min_value > chart_data[i]){
chart_min_value = chart_data[i];
}
chart_cnt[i] = 0;
}

我们先找到chart_cnt数组中的最大值和最小值,然后将chart_cnt数组中的数据映射到0-90之间,这样就可以将温度值映射到屏幕上,然后在显示时,只需要将chart_data数组中的数据绘制出来即可。

其中myMap函数为映射函数,用于将数据映射到指定范围内,如下所示:

1
2
3
4
float myMap(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

这样我们就可以得到温度分布的直方图,显示温度分布情况。

热成像显示

我们可以通过LVGL库绘制热成像图,将温度矩阵中的数据映射到屏幕上,这样就可以显示出热成像图。

1
2
3
4
5
6
7
void update_buffer(uint8_t* src){
if(updating_buffer_flag == 0){
memcpy(buffer_a, src, SRC_WIDTH * SRC_HEIGHT * 2);
}else{
memcpy(buffer_b, src, SRC_WIDTH * SRC_HEIGHT * 2);
}
}

这里引入了双缓冲机制,我们定义两个缓冲区buffer_a和buffer_b,一个用于显示,一个用于更新,这样可以避免显示时的闪烁问题。将显示和更新解耦,通过disp_finish_flag和updating_buffer_flag两个flag来标记当前状态,完成绘制操作后,将更新缓冲区中的数据拷贝到显示缓冲区中,然后再绘制显示缓冲区中的数据,这样就可以实现无闪烁的显示效果。为了提高显示效率,我们不进行拷贝操作,而是交换缓冲区,直接更新显示缓冲区的指针,这样就可以直接绘制更新缓冲区中的数据。

double_buffer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static lv_img_dsc_t dynamic_img_dsc = {
.header.always_zero = 0,
.header.w = SRC_WIDTH,
.header.h = SRC_HEIGHT,
.header.cf = LV_IMG_CF_TRUE_COLOR,
.data = (const uint8_t*)buffer_b, // 初始使用 buffer_b
.data_size = SRC_WIDTH * SRC_HEIGHT * 2,
}; // 图片描述符
void create_dynamic_image(lv_obj_t* parent){
lv_obj_t* img = lv_img_create(parent);
lv_img_set_src(img, &dynamic_img_dsc);
lv_img_set_zoom(img, 256 * 8);
lv_obj_align(img, LV_ALIGN_TOP_LEFT, 144, 115);
}
void swap_buffers(lv_obj_t* img){
if(updating_buffer_flag == 0){
dynamic_img_dsc.data = (const uint8_t*)buffer_b;
}else{
dynamic_img_dsc.data = (const uint8_t*)buffer_a;
}
lv_img_set_src(img, &dynamic_img_dsc);
// 更新图像源,并刷新
lv_obj_invalidate(img);

updating_buffer_flag = !updating_buffer_flag;
}

定义一个dynamic_img_dsc结构体,用于描述图片,在create_dynamic_image函数中创建图片对象,然后在swap_buffers函数中交换缓冲区,更新图片源,然后刷新图片对象,实现热成像图的实时显示。

瞄准角度计算

我们可以通过舵机模块,实现瞄准功能,将最高温度点对准中心,这样就可以实现瞄准功能。

计算瞄准角度也很简单,只需要将最高温度点的坐标映射到舵机的角度范围内即可。

这里使用的是SG90舵机,工作范围为0-90度,所以我们需要将最高温度点的坐标映射到0-90度之间。

1
2
SetServoAngle(0, 90 * (float)chart_max_x / 320);
SetServoAngle(1, 45 * (float)chart_max_y / 240);

其中SetServoAngle函数用于设置舵机的角度,第一个参数为舵机的编号,第二个参数为舵机的角度,而chart_max_x和chart_max_y为最高温度点的坐标,将其映射到0-90度之间,然后设置舵机的角度即可。

6.2 图形界面设计

我们使用LVGL库绘制图形界面,实现热成像图的显示、温度统计信息的显示、温度直方图的显示、舵机瞄准示意图的显示等功能。

6.2.1 界面布局

这里的LCD显示屏为4.3英寸,分辨率为800 x 480,我们可以将界面分为几个部分:

  • 热成像图:显示实时热成像图,位于左上角,大小为320 x 240。
  • 温度直方图:显示温度分布情况,位于屏幕左下角,大小为320 x 240。
  • 温度信息:显示最高温度、最低温度、平均温度、方差等信息,位于屏幕右下角。
  • 舵机瞄准:显示舵机瞄准示意图,位于屏幕右上角,大小为320 x 240,使用两直线表示舵机的角度。

再设置两个菜单按钮,用于切换显示模式,控制显示参数。

示意图如下:

layout

6.2.2 控件设计

简单确定了界面布局后,我们就可以开始设计控件了,这里我们主要使用以下控件:

  • 图片控件:用于显示热成像图和舵机瞄准示意图。
  • 图表控件:用于显示温度直方图。
  • 文本控件:用于显示温度信息。
  • 按键控件:用于切换显示模式。
1
2
3
4
5
6
void create_dynamic_image(lv_obj_t* parent){
lv_obj_t* img = lv_img_create(parent);
lv_img_set_src(img, &dynamic_img_dsc);
lv_img_set_zoom(img, 256 * 8);
lv_obj_align(img, LV_ALIGN_TOP_LEFT, 144, 115);
}

其中dynamic_img_dsc上文已经定义,用于描述图片,然后在create_dynamic_image函数中创建图片对象,用于显示热成像图,使用lv_img_set_src函数设置图片源,lv_img_set_zoom函数设置图片缩放比例,最后使用lv_obj_align函数设置图片位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void create_image_chart(){
chart = lv_chart_create(lv_scr_act());
lv_chart_set_type(chart, LV_CHART_TYPE_BAR);
lv_chart_set_point_count(chart, CHART_SIZE);
lv_obj_set_style_pad_column(chart, 2, 0);
lv_obj_set_size(chart, 320, 200);
lv_obj_set_style_bg_color(chart, lv_color_hex(0xE6E6FA), LV_PART_MAIN);
//lv_obj_align(chart, LV_ALIGN_BOTTOM_LEFT, 0, 0);
lv_obj_set_pos(chart, 0, 240);
ser = lv_chart_add_series(chart, lv_color_hex(0xff0000), LV_CHART_AXIS_PRIMARY_Y);
lv_obj_add_event_cb(chart, chart_draw_event_cb, LV_EVENT_DRAW_PART_BEGIN, ser);
for(uint16_t i = 0; i < CHART_SIZE; i++) {
lv_chart_set_next_value(chart, ser, 0);
}
lv_timer_create(update_chart_task, UPDATE_TIME, chart);
}

使用lv_chart_create函数创建图片空间,简单设置了一些属性,如图表类型、点数、背景颜色等,然后使用lv_chart_add_series函数添加一个数据系列,通过lv_chart_set_next_value函数设置数据,这里我们通过lv_timer_create函数创建一个定时器,定时更新图表数据。

为了实现温度直方图的颜色渐变效果,我们需要在lv_obj_add_event_cb函数中添加一个事件回调函数,用于绘制图表,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
static void chart_draw_event_cb(lv_event_t * e){
lv_obj_draw_part_dsc_t* chart_dsc = lv_event_get_draw_part_dsc(e);
/* 检查绘制的部分是否是柱状条 */
if (chart_dsc->part == LV_PART_ITEMS) {
uint16_t index = chart_dsc->id; // 获取当前柱子的索引
lv_coord_t value = chart_dsc->value; // 获取当前柱子的值
/* 根据值设置不同颜色 */
// uint16_t ratio = value * 255 / 100;
// chart_dsc->rect_dsc->bg_color = lv_color_make(ratio, 0, (255-ratio)/2);
chart_dsc->rect_dsc->bg_color = map_temperature_to_color(value, chart_min_value, chart_max_value);
}
}

这样就能根据温度值设置不同的颜色,更容易区分温度分布情况。

chart

同时,我们在update_chart_task的回调函数中更新动画效果:

  • 通过箭头动画,指示温度直方图的最高温度点位置。
  • 通过直线动画,指示舵机的瞄准角度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void update_pointer_anim(uint16_t x_d, uint16_t t_d){
lv_anim_set_values(&pointer_anim, lv_obj_get_x(pointer_label), x_d);
lv_anim_set_time(&pointer_anim, t_d);
lv_anim_start(&pointer_anim);
}
static void update_line_x_anim(uint16_t x_d, uint16_t t_d){
lv_anim_set_values(&pointer_line_x_anim, line_x_draw[0].x, x_d);
lv_anim_set_time(&pointer_line_x_anim, t_d);
lv_anim_start(&pointer_line_x_anim);
}
static void update_line_y_anim(uint16_t y_d, uint16_t t_d){
lv_anim_set_values(&pointer_line_y_anim, line_y_draw[0].y, y_d);
lv_anim_set_time(&pointer_line_y_anim, t_d);
lv_anim_start(&pointer_line_y_anim);
}

创建动画的过程也很简单,只需要设置动画的起始值和结束值,然后设置动画的时间,最后启动动画即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void create_pointer_anim(){
pointer_label = lv_label_create(lv_scr_act());
lv_label_set_text(pointer_label, "1");
lv_obj_set_pos(pointer_label, 0, 440);
lv_anim_init(&pointer_anim);
lv_anim_set_var(&pointer_anim, pointer_label);
lv_anim_set_values(&pointer_anim, lv_obj_get_x(pointer_label), 0);
lv_anim_set_time(&pointer_anim, UPDATE_TIME);
lv_anim_set_exec_cb(&pointer_anim, anim_x_cb);
lv_anim_set_path_cb(&pointer_anim, lv_anim_path_overshoot);
// lv_anim_start(&anim);
pointer_line_x = lv_line_create(lv_scr_act());
pointer_line_y = lv_line_create(lv_scr_act());
lv_line_set_points(pointer_line_x, line_x_draw, 2);
lv_line_set_points(pointer_line_y, line_y_draw, 2);

lv_anim_init(&pointer_line_x_anim);
lv_anim_init(&pointer_line_y_anim);
lv_anim_set_var(&pointer_line_x_anim, pointer_line_x);
lv_anim_set_var(&pointer_line_y_anim, pointer_line_y);
lv_anim_set_values(&pointer_line_x_anim, line_x_draw[0].x, 320+320);
lv_anim_set_values(&pointer_line_y_anim, line_y_draw[0].y, 240);
lv_anim_set_time(&pointer_line_x_anim, 250);
lv_anim_set_time(&pointer_line_y_anim, 250);
lv_anim_set_exec_cb(&pointer_line_x_anim, anim_line_x_cb);
lv_anim_set_exec_cb(&pointer_line_y_anim, anim_line_y_cb);
// // lv_anim_set_path_cb(&pointer_line_x_anim, lv_anim_path_overshoot);
// lv_anim_start(&pointer_line_x_anim);
// lv_anim_start(&pointer_line_y_anim);
}

主要关注create_pointer_anim函数,我们创建了一个label控件,用于显示箭头动画,然后创建了两个line控件,用于显示直线动画,然后创建了两个动画,分别用于箭头动画和直线动画,设置动画的起始值和结束值、动画完成的时间,最后启动动画即可。

为了方便数据观察,我们定义了两个按键控件,用于切换显示模式,控制显示参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lv_obj_t* button_is_update = lv_btn_create(lv_scr_act());
lv_obj_align(button_is_update, LV_ALIGN_TOP_RIGHT, 0, 0);
lv_obj_add_flag(button_is_update, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_size(button_is_update, 100, 70);
lv_obj_t* label_is_update = lv_label_create(button_is_update);
lv_label_set_text(label_is_update, "snapshot");
lv_obj_align(label_is_update, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(button_is_update, snap_stream_event, LV_EVENT_CLICKED, NULL);
lv_obj_t* button_show_info = lv_btn_create(lv_scr_act());
lv_obj_align(button_show_info, LV_ALIGN_TOP_RIGHT, 0, 90);
lv_obj_add_flag(button_show_info, LV_OBJ_FLAG_CHECKABLE);
lv_obj_set_size(button_show_info, 100, 70);
lv_obj_t* label_show_info = lv_label_create(button_show_info);
lv_label_set_text(label_show_info, "info");
lv_obj_align(label_show_info, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(button_show_info, is_show_info_event, LV_EVENT_CLICKED, NULL);

其回调函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void snap_stream_event(lv_event_t *event)
{
lv_obj_t *btn = lv_event_get_target(event); // 获得调用这个回调函数的对象
if (event->code == LV_EVENT_CLICKED)
{
lv_obj_t *label = lv_obj_get_child(btn, NULL); // 获取第1个子对象(我们在设计时,已安排了它的第1个子对象是一个label对象)
is_update = !is_update;
if(is_update){
lv_label_set_text_fmt(label, "snapshot");
}else{
lv_label_set_text_fmt(label, "streaming");
}
}
}
static void is_show_info_event(lv_event_t *event)
{
lv_obj_t *btn = lv_event_get_target(event); // 获得调用这个回调函数的对象

if (event->code == LV_EVENT_CLICKED)
{
if(!is_info_show){
hide_labels();
is_info_show = 1;
}else{
show_labels();
is_info_show = 0;
}
}
}
snapshot和streaming用于切换显示模式,info用于显示温度信息,通过这两个按键控件,我们可以方便地切换显示模式,控制显示参数。在snapshot模式下,我们暂停图像流,只显示当前图像,方便观察数据,而在streaming模式下,我们显示图像流,实时显示数据。

至此,我们就完成了大部分图形界面的设计,实现了热成像图的显示、温度直方图的显示、舵机瞄准示意图的显示等功能。最后的温度统计信息的显示,我们可以通过lv_label_create函数创建label控件,然后通过lv_label_set_text函数设置文本内容,实时更新数据即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 独立的标签
label_max_min_temp = lv_label_create(lv_scr_act());
lv_label_set_text(label_max_min_temp, "initial");
lv_obj_set_pos(label_max_min_temp, 320+10, 240+10);
label_max_min_temp_xy = lv_label_create(lv_scr_act());
lv_label_set_text(label_max_min_temp_xy, "initial");
lv_obj_set_pos(label_max_min_temp_xy, 320+10, 240+40);
label_servo_angle = lv_label_create(lv_scr_act());
lv_label_set_text(label_servo_angle, "initial");
lv_obj_set_pos(label_servo_angle, 320+10, 240+70);
label_mean_variance = lv_label_create(lv_scr_act());
lv_label_set_text(label_mean_variance, "initial");
lv_obj_set_pos(label_mean_variance, 320+10, 240+110);
label_time_stamp = lv_label_create(lv_scr_act());
lv_label_set_text(label_time_stamp, "initial");
lv_obj_set_pos(label_time_stamp, 220+400, 240+10);

更新数据时,只需要调用lv_label_set_text函数设置文本内容即可。

1
2
3
4
5
6
7
8
9
10
sprintf(output_max_min_temp, "max T: %.2f \tmin T: %.2f", max_temp, min_temp);
sprintf(output_max_min_temp_xy, "max x: %d y: %d \tmin x: %d y: %d", max_temp_x, max_temp_y, min_temp_x, min_temp_y);
sprintf(output_servo_angle, "servo 0: %d \tdegree\nservo 1: %d \tdegree", (int)(90 * (float)max_temp_x / 32), (int)(45 * (float)max_temp_y / 24));
sprintf(output_mean_variance, "mean: %.2f \tvariance: %.2f", mean_temp, variance_temp);
sprintf(output_time_stamp, "time: %d s %d ms", time_s, time_ms);
lv_label_set_text(label_max_min_temp, output_max_min_temp);
lv_label_set_text(label_max_min_temp_xy, output_max_min_temp_xy);
lv_label_set_text(label_servo_angle, output_servo_angle);
lv_label_set_text(label_mean_variance, output_mean_variance);
lv_label_set_text(label_time_stamp, output_time_stamp);

完成了图形界面的设计后,我们就可以实现热成像图的显示、温度统计信息的显示、温度直方图的显示、舵机瞄准示意图的显示等功能。

interface

6.3 舵机驱动

上面我们已经实现了舵机的瞄准功能,这里我们简单介绍一下舵机的驱动,驱动非常简单,只需要通过PWM信号控制舵机的角度即可。

可以得到舵机的PWM信号的频率为50Hz,我们使用Timer1初始化舵机的PWM信号,然后通过设置占空比来控制舵机的角度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void SetServoAngle(uint8_t id, uint8_t angle){
uint16_t pulse = 1500;
if(id == 0){
if(angle > 90){
angle = 90;
}else if(angle < 0){
angle = 0;
}
pulse += 11 * angle;
TIM_SetCompare1(TIM1, pulse);
}else if(id == 1){
if(angle > 45){
angle = 45;
}else if(angle < 0){
angle = 0;
}
pulse += 11 * (45-angle);
TIM_SetCompare1(TIM10, pulse); // 修改比较值,修改占空比
}
}

定时更新舵机的角度即可。

7. 实验效果

stm32
camera

上图为实验效果,我们可以看到,通过MLX90640红外热成像模块,我们可以实时显示热成像图,显示温度直方图,显示温度统计信息等,成功实现了一个简单的红外热成像仪。

视频文件有点大,可以在这里查看。

8. 结论

本项目基于MLX90640的红外热成像仪系统利用STM32F407作为核心控制单元,通过硬件与软件的深度结合,实现了高效的温度数据采集、处理和实时可视化展示。本系统采用模块化设计方法,将硬件抽象、数据处理、图形显示和系统控制等功能分层实现,不仅提高了代码的可读性和维护性,也为未来功能扩展提供了坚实基础。

本系统具有广泛的应用场景,包括工业设备的温度监测、医疗诊断中的热图成像、以及环境监测中的目标检测等,提供了一种低成本的红外热成像解决方案。

但受限于硬件性能和算法复杂度,本系统在实时性和精度上仍有待提高,图像刷新率较低,温度分辨率较低,需要进一步优化算法和硬件设计,提高系统性能。

9. 参考资料

[1] MLX90640数据手册
[2] 官方驱动代码
[3] 类似项目
[5] LVGL官方文档
[6] 本项目源码
[7] 演示视频


基于MLX90640的红外热成像仪
https://symcreg.github.io/2024/12/29/基于MLX90640的红外热成像仪/
作者
sam
发布于
2024年12月29日
许可协议