【2025 Make Blocks综合项目】基于STM32F103设计的多功能可扩展的手持热成像设备
该项目使用了STM32F103、MLX90640、BME280,实现了多功能可扩展的手持热成像设备的设计,它的主要功能为:热成像、环境监测、时间管理、电池电源管理、人机交互等功能。
标签
STM32F103
MLX90640
BME280
Make Blocks综合项目
手持热成像设备
无言的朝圣
更新2026-02-26
285

综合任务介绍

我们本次参加的活动是2025年度Make Block综合活动,最后通过长达半年的模块化设计实现一个完成的系统化的生态的系统,本次最终的目标是实现热成像设备的基础上融合多个主题内容,让最终的目标实现更加多元化的功能,并预留对应的接口可以进行扩展开发。本次活动共分为6期,融合了十二个主题内容,分别包括微控制器、人机接口、电源电池管理、传感器、数字接口、模拟接口、运动、照明等主题,接下来我们就会跟随这各期实现的内容进行综合的介绍。

分期回顾

以下是我们在本市2025年度make block活动中分期实现的目标情况:

第一期:主控部分+人机交互部分,项目地址:https://www.eetree.cn/project/4236

第二期:实时时钟部分+存储部分,项目地址:https://www.eetree.cn/project/4469

第三期:传感器与运动控制部分,项目地址:https://www.eetree.cn/project/4593

第四期:模拟数据处理和数据转换,项目地址:https://www.eetree.cn/project/4668

第五期:电池管理与电源管理,项目地址:https://www.eetree.cn/project/4742

第六期:通信与照明,项目地址:通过后可以主要观看

如果大家对单一功能感兴趣的可以单独进行交流学习。

综合项目介绍

我们本次的项目是基于stm32f103进行的设计,选择这款100pin单面机的主要原因是我们使用的TFT接口液晶采用的8位并行接口,正好契合这款单片机FSMC功能,可以实现更加快速的刷屏显示功能。在此基础上我们使用单片机的基本外设进行不同模块直接的驱动设计,最终实现了我们本次的项目目标。

整体项目框图


0

整体的设计思路是跟随活动的进展的主题变化进行不断的增加和调整,从最开始的基础部分不管添加新的模块,对于一些不太融入这个项目的内容我们是通过独立模块的形式进行的单期功能验证,并最终通过外部接口的方式融合综合项目中并进行调试。

基本功能与主题

主控单元设计对应的主题是”嵌入式微控制器“,这里我们选择的是市场上非常成熟的嵌入式产品——STM32F103VCT6,其具有丰富的接口能够让我们可以更好的应对后面的内容扩展,我们通过搭建外部晶振、复位电路、供电等部分实现单片机的基本运行,单片机系统的电路部分如下:


0

HMI(人机接口)主题也是第一期的分任务,其包含的项目内容包括TFT显示、多功能按键、多色LED显示,也包括后面的电源开关按键,融合到一起主要是按键作为输入,LED和TFT进行全面的状态反馈和内容显示,其中TFT液晶屏通过FPC座连接,可以进行背光控制:


0

按键部分包括多功能按键和开关机按键:


0

数字外设主题我们选择的是预留的功能,一个Flash模块,主要用于一些关键数据的掉电不丢失存储,使用SPI接口通信:


0

计时 & 时钟主题我们选择的是自带晶振的RTC——RX8130,当然了模块与模块之间也是有关联的,比如这款实时时钟的备电就来自电池:


0

传感器主题实际上和气体主题也是契合的,这里我们选择了本次使用的主要目标热成像传感器进行的设计,并且设计为了一个单独的模块,后续我们是通过转接实现的基本控制:


0

但是在最后的实现的项目中我们还加入了环境检测的传感器——BME280,让项目的功能更加完善:


0

运动控制的主题在本项目中依然是预留功能,这里我们加入的是一个国产的三轴传感器,还是非常有性价比的:


0

模拟信号处理的主题我们主要的体现实在电池电量的采集,是将电池电压通过分压的方式,通过单片机的ADC继续采集:


0

电池管理和电源管理的主题我们融合到一起进行介绍,电池采用的是锂电池进行供电,使用电池可以让我们的项目设备进行手持使用,摆脱地域的控制,应用环境会更多,不过有了锂电池就会出现没电的情况,就需要接入锂电池充电系统,这里选择的是TPB4056A进行充电,然后接入开关控制系统后,在经过LDO的稳压实现系统的供电:


0

照明的主题我们也是通过独立模块的方式进行的设计,实现了一个5串LED的点亮验证,实际上是一个升压模块去驱动LED,最后我们在独立模块的基础上设计了一个新的灯板集成到了项目中:


0

通信主题实际上通过扩展的不同的外设接口也是满足要求,USB、串口、SPI、I2C等等

最终通过各个主题之间的结合实现了以下功能:

热成像功能;

环境监测功能;

手持功能,锂电池供电与USB供电;

时间显示和设定功能;

显示功能与基本的人机交互;

功率LED灯亮度控制;

丰富的外设接口;

留的三轴与存储器,可进行页面自适应旋转控制。

物料清单

主要器件

简介

STM32F103VCT6

STM32F103VCT6是意法半导体的高性能32位微控制器,基于ARM Cortex-M3内核,主频高达72MHz,具备256KB Flash和48KB SRAM,支持多种通信接口和定时器,适用于工业控制、消费电子和汽车电子等领域。

MLX90640

MLX90640是一款小巧的32x24红外热成像传感器,能快速生成热图像,支持I²C通信,适用于安防、工业检测和智能家居等场景。

RX8130

RX8130CE是一款超低功耗实时时钟芯片,集成高精度晶振和主备电切换功能,支持I²C通信,专为智能手表等便携设备设计,确保时间精准且续航持久。

W25Q80

W25Q80是华邦电子推出的8M-bit串行闪存芯片,支持标准/双/四线SPI接口,工作电压2.7V-3.6V,工业级温度范围(-40°C至85°C),功耗低且封装小巧,适合存储需求。

BME280

BME280是博世的一款高精度环境传感器,能同时测量温度、湿度和气压,支持I²C和SPI接口,小巧低功耗,适合物联网、智能家居和穿戴设备等应用。

TPB4056A

TPB4056A是一款具有成本效益的高集成度线性充电器,适用于单电池锂离子或锂离子聚合物电池。该设备支持从USB端口或交流适配器充电。具有过压保护的高输入电压范围支持低成本的非调节适配器。

AP5724WG

AP5724WG 是由Diodes Incorporated公司生产的一款升压型LED驱动芯片,采用了高效的升压转换技术,广泛应用于各种LED驱动场合,特别是在需要高电压输出且具备高效率的系统中。该芯片能够支持较大输入电压范围,从2.8V到5.5V,适用于多种电池驱动的应用场景,如便携式设备和汽车照明系统等。

原理图与PCB

我们最终完成整个项目绘制了6块PCB(中间过程的就不算了),其中包括:

主控板:实现基本外设控制和交互内容,还包括i一些板载的传感器内容,原理图与PCB如下:


0


0

以独立模块形式存在的热成像传感器板,原理图和PCB如下:


0


0

以独立模块形式存在的LED升压驱动,原理图和PCB如下:


0


0

软件调试

软件流程图


0

主要代码介绍

按键的采集与处理我们是通过外部中断加定时器的方式实现的基本控制和防抖,最后会反馈到按键状态(加,减,确认,返回):

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == SW_A_Pin)
{
KeyEXIT_Handle(&key_A);
}
else if(GPIO_Pin == SW_B_Pin)
{
KeyEXIT_Handle(&key_B);
}
else if(GPIO_Pin == SW_OK_Pin)
{
KeyEXIT_Handle(&key_Ok);
}
else if(GPIO_Pin == SW_P_Pin)
{
KeyEXIT_Handle(&key_Power);
}
}

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM5)
{
HAL_IncTick();

KeyTimer_AntiShake(&key_Ok,READ_KEY_OK);
KeyTimer_AntiShake(&key_A,READ_KEY_A);
KeyTimer_AntiShake(&key_B,READ_KEY_B);
KeyTimer_AntiShake(&key_Power,READ_KEY_P);

}
}

void App_KEY_Judge(void)
{
if(key_Ok.ShortPress_flag == 1)//短按OK键
{
KeyState.Confirm = 1;
key_Ok.ShortPress_flag = 0;

}

if(key_A.ShortPress_flag == 1)//短按A键
{
KeyState.Less = 1;
key_A.ShortPress_flag = 0;

}

if(key_B.ShortPress_flag == 1)//短按B键
{
KeyState.Add = 1;
key_B.ShortPress_flag = 0;

}

if(key_Power.ShortPress_flag == 1)//短按电源键(返回)
{
KeyState.Return = 1;
key_Power.ShortPress_flag = 0;

}


if(key_Ok.LongPress_flag == 1)//长按OK键
{
key_Ok.LongPress_flag = 0;
switch(Menu.ing)
{
case SetTime_Interface:
Menu.Request = SetMenu_Interface;
SetMenu_unit.Request = 1;

RX8130_SetTime();
memcpy(&Time_Show,&Time_Set,7);

SetTime_unit.Request = 0;
SetTimeInterface_unit_cut(SetTime_unit.ing,SetTime_unit.Request);
SetTime_unit.ing = SetTime_unit.Request;

RTCSetState_unit.Request = 0;
RTCSetState_unit_cut(RTCSetState_unit.ing,RTCSetState_unit.Request);
RTCSetState_unit.ing = RTCSetState_unit.Request;
break;

default:
KeyState.Return = 1;
break;
}
}

if(key_Power.LongPress_flag == 1)//长按电源键
{
DeviceOff();
key_Power.LongPress_flag = 0;

}
KEYState_Handle();
}

页面助力是我们最主要的交互方式之一,我们通过FSMC继续的8位并口的控制,可以直接关联控制,向地址写数据就会自动传输:

#define TFT_COMM_ADD    *((volatile uint8_t *)0x60000000)
#define TFT_DATA_ADD *((volatile uint8_t *)0x60010000)
#define TFT_WR_COMM(cmd) {TFT_COMM_ADD = cmd;}
#define TFT_WR_DATA(data) {TFT_DATA_ADD = data;}

TFT的控制实现了初始化、写16字符、32字符等内容,包括一些中文内容,都通过取模的方式实现了,这一部分内容比较大,大家可以到代码中具体查看。

传感器的驱动也是非常重要的一部分,比如90640的驱动,这里我们通过IO口模拟IIC的方式进行的驱动,相关驱动文件可以通过官网获取,当时还是需要进行以下转化和接口适配,这里会有很多报错的地方,主意啊首次驱动传感器要等待一段时间让传感器达到平衡,接口代码如下;


uint8_t MLX90640_Read(uint16_t Saddress,uint16_t nWords,uint16_t *ndata)
{
IIC4_MasterUS_RecvBytes(nWords,ndata,MLX90640_SlaveAddress,Saddress);
return 0;
}

uint8_t MLX90640_Write(uint16_t address,uint16_t Word)
{
IIC4_MasterUS_SendBytes(1,&Word,MLX90640_SlaveAddress,address);
return 0;
}

void MLX90640_Delay_ms(uint32_t nms)
{
HAL_Delay(nms);
}

获得的温度数据还要进行页面刷新展示,还能切换显示模式:

uint16_t GrayToPseColor(uint16_t TemData,uint8_t converMethod)
{
uint8_t colorR,colorG,colorB;
uint8_t grayValue;
uint16_t color = 0;
if(TemData<=650)
grayValue = 0;
else if(TemData > 650 && TemData < 800)
{
grayValue = (((TemData-650)<<8)/150);
}
else if(TemData >= 800)
{
grayValue = 255;
}


switch(converMethod)
{
case 1:
colorR=abs(0-grayValue);
colorG=abs(127-grayValue);
colorB=abs(255-grayValue);

color = (color|(colorR>>3))<<6;
color = (color|(colorG>>2))<<5;
color = (color|(colorB>>3));
break;

case 2:
if((grayValue<=63))
{
colorR=0;
colorG=0;
colorB=(uint8_t)((float)grayValue/64*255+0.5);
}
else if((grayValue>=64) && (grayValue<=127))
{
colorR=0;
colorG=(uint8_t)((float)((grayValue-64)/64*255));
colorB=(uint8_t)((float)((127-grayValue)/64*255));
}
else if((grayValue>=128) && (grayValue<=191))
{
colorR=(uint8_t)((float)((grayValue-128)/64*255));
colorG=255;
colorB=0;
}
else if((grayValue>=192))
{
colorR=255;
colorG=(uint8_t)((float)((255-grayValue)/64*255));
colorB=0;
}

color = (color|(colorR>>3))<<6;
color = (color|(colorG>>2))<<5;
color = (color|(colorB>>3));
break;
case 3:
color = camColors[grayValue];
break;
default:break;
}
return color;
}

BME280的驱动也是比较需要注意的,我们在驱动的时候需要获取很多的校验数据,然后通过大量的计算来获得准确的温湿度大气压力数据:


void BME280_Init(void)
{
uint8_t dig_E4,dig_E5,dig_E6;
BME280_WriteByte(0xe0,0xb6);
BME280.Result.ID = BME280_ReadByte(0xD0);
if(BME280.Result.ID == 0x60)
{
BME280.Result.Existflag = 1;
}

BME280_WriteByte(0xf4, 0xfc);

BME280_WriteByte(0xf2, 0x01);
BME280_WriteByte(0xf5, 0x70);

BME280_WriteByte(0xf4, 0xff);


BME280.CalParam.dig_T1 = BME280_ReadWord(0x88);
BME280.CalParam.dig_T2 = BME280_ReadWord(0x8A);
BME280.CalParam.dig_T3 = BME280_ReadWord(0x8C);
BME280.CalParam.dig_P1 = BME280_ReadWord(0x8E);
BME280.CalParam.dig_P2 = BME280_ReadWord(0x90);
BME280.CalParam.dig_P3 = BME280_ReadWord(0x92);
BME280.CalParam.dig_P4 = BME280_ReadWord(0x94);
BME280.CalParam.dig_P5 = BME280_ReadWord(0x96);
BME280.CalParam.dig_P6 = BME280_ReadWord(0x98);
BME280.CalParam.dig_P7 = BME280_ReadWord(0x9A);
BME280.CalParam.dig_P8 = BME280_ReadWord(0x9C);
BME280.CalParam.dig_P9 = BME280_ReadWord(0x9E);

BME280.CalParam.dig_H1 = BME280_ReadByte(0xA1);
BME280.CalParam.dig_H2 = BME280_ReadWord(0xE1);
BME280.CalParam.dig_H3 = BME280_ReadByte(0xE3);

dig_E4 = BME280_ReadByte(0xE4);
dig_E5 = BME280_ReadByte(0xE5);
dig_E6 = BME280_ReadByte(0xE6);

BME280.CalParam.dig_H4 = ((BME280.CalParam.dig_H4|dig_E4)<<4)|(dig_E5&0x0f);
BME280.CalParam.dig_H5 = ((BME280.CalParam.dig_H5|dig_E6)<<4)|((dig_E5&0xf0)>>4);
BME280.CalParam.dig_H6 = BME280_ReadByte(0xE7);

BME280_delay_ms(200);
}

void BME280Calculation(void)
{

int32_t t_fine;
double var1, var2,var_H,p;

var1 = (((double)BME280.Result.data_temperature) / 16384.0 -
((double)BME280.CalParam.dig_T1) / 1024.0) * ((double)BME280.CalParam.dig_T2);
var2 = ((((double)BME280.Result.data_temperature) / 131072.0 -
((double)BME280.CalParam.dig_T1) / 8192.0) * (((double)BME280.Result.data_temperature) / 131072.0 -
((double)BME280.CalParam.dig_T1) / 8192.0)) * ((double)BME280.CalParam.dig_T3);

t_fine = (int32_t)(var1 + var2);
BME280.Result.temperature = (uint16_t)(t_fine/512+450);//分辨率0.1
if(BME280.Result.temperature >1300)
BME280.Result.temperature = 1300;


var1 = ((double)t_fine / 2.0) - 64000.0;
var2 = var1 * var1 * ((double)BME280.CalParam.dig_P6) / 32768.0;
var2 = var2 + var1 * ((double)BME280.CalParam.dig_P5) * 2.0;
var2 = (var2 / 4.0) + (((double)BME280.CalParam.dig_P4) * 65536.0);
var1 = (((double)BME280.CalParam.dig_P3) * var1 * var1 / 524288.0 + ((double)BME280.CalParam.dig_P2) * var1) / 524288.0;
var1 = (1.0 + var1 / 32768.0) * ((double)BME280.CalParam.dig_P1);
if(0.0 == var1)
{
BME280.Result.pressure = 0; // avoid exception caused by division by zero
}
else
{
p = 1048576.0 - (double)BME280.Result.data_pressure;
p = (p - (var2 / 4096.0)) * 6250.0 / var1;
var1 = ((double)BME280.CalParam.dig_P9) * p * p / 2147483648.0;
var2 = p * ((double)BME280.CalParam.dig_P8) / 32768.0;
p = p + (var1 + var2 + ((double)BME280.CalParam.dig_P7)) / 16.0;

BME280.Result.pressure = (uint16_t)(p/100);//分辨率百帕
}

var_H = (((double)t_fine) - 76800.00);
var_H = (BME280.Result.data_humidity -
(((double)BME280.CalParam.dig_H4) * 64.0 +
((double)BME280.CalParam.dig_H5) / 16384.0 * var_H)) *
(((double)BME280.CalParam.dig_H2) / 65536.0 *
(1.0 + ((double)BME280.CalParam.dig_H6) / 67108864.0 * var_H *
(1.0 + ((double)BME280.CalParam.dig_H3) / 67108864.0 * var_H)));
var_H = var_H * (1.0 - ((double)BME280.CalParam.dig_H1) * var_H / 524288.0);

if(var_H > 100.0)
{
var_H = 100.0;
}
else if(var_H < 0.0)
{
var_H = 0.0;
}

BME280.Result.humidity = (uint16_t)(var_H*10);//分辨率0.1

}

void App_BME280Read(void)
{
BME280.Result.data_pressure = BME280_ReadThreeByte(BME280_PRESS_ADDR);
BME280.Result.data_temperature = BME280_ReadThreeByte(BME280_TEMP_ADDR);

BME280.Result.data_humidity = BME280_ReadByte(BME280_HUM_ADDR);
BME280.Result.data_humidity = (BME280.Result.data_humidity<<8)|BME280_ReadByte(BME280_HUM_ADDR+1);

BME280Calculation();
}

其他代码内容建议大家去往期调试内容或者附件代码中去查看。

效果展示

本次进行一些基本功能的展示(静态图片或者动图的方式),完整的展示过程可以通过视频继续查看。

热成像功能:


0

环境采集以及时间显示和供电状态显示:


0

按键与显示交互:

总结

在历经了这么长时间的设计过程之后我们终于实现了最终的项目目标,其实在整体的一个设计开始之前,我们就对这个目标实现的基本功能进行了一下设想,和咱们的主题进行了一些深度的关联,来确保我们本次的设计最终能够更小型化集成度高一点。实际上,我们的设计包括硬件和后面的一个软件调试都是比较耗费时间和精力的,硬件设计方面我们可以参考一些数据手册中的典型电路进行设计,软件的驱动官方一般也会提供参考的一个代码,但是在最后的一个集成系统设计的时候,还是需要我们来自行设计软件结构,这一方面实际上就会比较耗费调试时间,本次我的设计都是使用的裸机开发,所以每一方面都可能会遇到问题进行优化调试,尤其涉及到UI交互方面。不过本次的一个全方位的学习设计锻炼,也是对个人的一次技能的重大的提升,也期待下一期活动的到来,相信会给工程师们带来不一样的体验!

附件下载
Thermal_STM32F103.zip
Sensor-90640.kicad_pcb
Sensor-90640.kicad_sch
Thermal-1.3.kicad_pcb
Thermal-1.3.kicad_sch
升压LED驱动-AP5724WG.kicad_pcb
升压LED驱动-AP5724WG.kicad_sch
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号