- 2024-11-17
-
加入了学习《【2024 DigiKey创意大赛】+智慧焊接工作台》,观看 智慧焊接工作台
- 2024-11-02
-
发表了主题帖:
【DigiKey创意大赛】便携生命探测仪07+作品提交
便携生命探测仪
作者:sipower
一、作品简介
现在越来越多业余驴友喜欢户外冒险,但是随之而来的就是遇到危险的情况时有发生,因此我想到做一个便携生命探测仪,在发生危险时,能帮助救援人员方便找到遇险者。此方案主要采用热敏式图像传感器MLX90640ESF-BAB-000-TU检测人体红外信号,采用BME680气体,湿度,压力,温度传感器评估板记录环境信息,配合一个带屏幕的评估板,显示热成像图片。再额外添加一个心电采集模块,当发现遇险者后,能立即给遇险者采集心电图和心率并在屏幕上显示,方便救援人员判断生命体征状态,好制定下一步救援计划。下图是最终的作品照片,我用纸壳做成一个手枪形态的样子。
本次作品从得捷电子采购的器件:
序号
型号
名称
用途
1
MLX90640ESF
热敏式图像传感器
检测人体红外信号
2
3660
BME680评估板
采集气体,湿度,压力,温度参数
3
DFR1075
ESP32-C6评估板
转换心电数据格式
4
JB2835AWT-W
白色发光管
用于探测仪照明
本次作品自备的器件:
序号
型号
名称
用途
1
DFR0975
ESP32-S3评估板
主控板实现数据处理和现实功能
2
MSP3520
3.5英寸480*320分辨率触摸屏
显示热成像图像和心电波形
3
LH001-91
心电评估板
采集心电数据
二、系统框图
整个系统框图如下图。
如上图所示,ESP32-S3评估板作为主控板,通过SPI接口连接一片480*320分辨率的触摸屏,用来显示各个检测数据、图像、心电波形。通过串口连接一片ESP32-C6评估板。ESP32-C6负责通过串口从LH001-91心电评估板接收字符串数据,然后选择心电波形、心率、导联脱落数据转换成浮点数,再从另外一个串口发给主控板。通过I2C接口连接热敏式图像传感器MLX90640ESF-BAB-000-TU检测人体红外信号,转换成热成像实时图片刷新到显示屏。通过I2C接口连接BME680气体,湿度,压力,温度传感器评估板记录环境信息,并显示在屏幕上。
三、各部分功能说明
所有板子,传感器,屏幕等器件焊接好,并打胶加固后的效果如下图。
热成像传感器MLX90640
该传感器分辨率为32H x 24V阵列,封装是TO-39,本次大赛两个必选料之一。焊接好的图片如下。和主控板之间采用I2C接口。该传感器软件驱动我这里参考Adafruit_MLX90640的库,再结合显示屏特点进行了调色板处理,通过差值将32*24点阵放大到320*240点阵,可以在屏幕上均匀显示,祛除颗粒感。
主控板
选的是以前做评测攒下的一块ESP32-S3开发板,由DFRobot设计的FireBeetle-ESP32-S3,如下图。该评估板提供非常丰富的IO接口,可以满足大部分DIY的需求。这个板卡的程序我在VScode+PlatformIO平台开发,使用体验非常好,强烈推荐没用过的同行们尝试一下。
显示屏
选了一块480*320的TFT,如下图,也是以前做评测攒下的。该屏幕采用SPI接口,带电阻式触摸屏,使用该屏幕可以省掉按键,配合开源显示驱动库TFT_eSPI,可以非常方便的实现各种显示功能。这里需要特别说明一下,由于该屏幕使用的驱动IC ILI9488是18bit数据,TFT_eSPI是不支持DMA的,我这使用了一些技巧,最终实现DMA写屏,使得热成像刷新率显著提高,具体的细节可以参考我分享的第三贴。
BME680传感器
该传感器具有SPI和I2C两种接口,由于我用的这个ESP32-S3开发板引出的SPI口接屏了并且SPI占IO口比较多,我择使用I2C接口连接BME680小板,最开始为了省事我把它和MLX90640挂在同一个I2C总线上了。但是我发现一个问题,就是每次读取BME680时候,热成像刷新都会卡顿一下,体验不好,原因就是共用总线造成的。好在ESP32-S3有多个I2C,并且还有一个巨牛掰的GPIO交换矩阵,可以让外设使用任意IO口。于是我从引脚分配表上找了2个闲置的IO口A4和A5配置成了I2C总线,完美解决卡顿问题。采集数据显示如下图。
心电采集部分
本次作品为了实现心电采集,我选用了领慧立芯推出的医疗级模拟前端心电采集方案。具体实现采用的是领慧立芯的心电采集评估板,该评估板基于LH001-91设计。
LH001-91是专门针对心电信号采集而开发的医疗级模拟前端,集成了24位Σ-Δ ADC,可编程增益放大器,右腿驱动,导电脱落监测等功能。更详细资料见如下链接:
http://www.legendsemi.com/hyyy_2/4.html
下图是我申请到的LH001-91的评估板,主要由AFE(LH001-91),通用MCU和两侧的三个电极组成。
为了降低主控端的软件复杂度,我还选用了一块ESP32-C6开发板做心电数据解析,该开发板由DFRobot设计研发,型号是FireBeetle 2 ESP32-C6。如下图所示。
FireBeetle 2 ESP32-C6采用较新的RISC-V内核,在官方VScode+PlatformIO平台还不支持Arduino架构,最后采用ArduinoIDE做的程序开发。详细程序见作品源代码。
在本次作品中主要是用到了ESP32-C6的两个串口做协议转换。原理框图如下图。
我在PC端用了一个能显示串口数据波形的软件,ESP32-C6负责从LH001-91评估板接收字符串数据,然后转换成上位机软件能识别的协议,再从另外一个串口发出去。最后由主控板接收数据并显示在屏幕上。心电显示效果如下图。
照明部分
考虑到这个探测仪会在黑暗环境下工作,我在作品最前端加了2个高亮白光LED发光管。在最终的手枪状扳机处粘了一个按键,可以控制发光管的亮灭。局部照片如下图。
四、作品源码
ESP32-S3程序采用VScode+PlatformIO平台开发。
ESP32-C6程序采用ArduinoIDE开发。
便携生命探测仪源代码
https://download.eeworld.com.cn/detail/sipower/634850
五、作品功能演示视频
[localvideo]ec5c9ffb1ee98e05de790da3423d6055[/localvideo]
六、项目总结
感谢EEWorld和得捷电子提供的这次机会,让我体验了热敏式图像传感器MLX90640和环境检测传感器BME680。然后做出这个有意义的作品。在开发过程中,感觉到ESP32单片机生态系统很完善,不但有非常好用的Arduino平台,还有更强大的VScode+PlatformIO开发平台,让程序开发更加高效稳健。
下面链接是本次作品开发过程中的经验分享贴,本文中没有详细介绍的内容,在下面帖子中都能找到。
https://bbs.eeworld.com.cn/thread-1290207-1-1.html
https://bbs.eeworld.com.cn/thread-1291434-1-1.html
https://bbs.eeworld.com.cn/thread-1292207-1-1.html
https://bbs.eeworld.com.cn/thread-1294411-1-1.html
https://bbs.eeworld.com.cn/thread-1294434-1-1.html
https://bbs.eeworld.com.cn/thread-1296592-1-1.html
七、其他
无。
-
发表了日志:
【DigiKey创意大赛】便携生命探测仪07+作品提交
- 2024-10-30
-
上传了资料:
【DigiKey创意大赛】便携生命探测仪源代码
- 2024-10-28
-
加入了学习《 【2024 DigiKey创意大赛】 《智能起居室环境控制台》任务报告汇总》,观看 【2024 DigiKey创意大赛】 《智能起居室环境控制台》任务报告汇总
-
回复了主题帖:
【DigiKey创意大赛】便携生命探测仪06+各模块整合联调
wangerxian 发表于 2024-10-24 16:13
嗯嗯,单导只能看心率,我以前问过医生,单导其实看不出什么东西。
感觉您也是专业人士哈
- 2024-10-24
-
回复了主题帖:
【DigiKey创意大赛】家庭共享智能药盒05+基于TouchGFX的UI程序设计
界面简洁实用
-
回复了主题帖:
【DigiKey创意大赛】便携生命探测仪06+各模块整合联调
wangerxian 发表于 2024-10-24 09:02
那确实,采样率不够,心电波形就会有点问题。
对于单导的心电,主要还是看心律,计算心律不齐、室早房早啥的,看形态的少些。
-
回复了主题帖:
【DigiKey创意大赛】便携生命探测仪06+各模块整合联调
wangerxian 发表于 2024-10-21 15:32
我感觉那个心电图可以画的频率低一些,要不显示的不是很好看。
是的,这个采样率低,处理的时候还丢点了,心电波就显得比较密集,要是想显示清楚,需要增加采样点和MCU处理能力,目前选的这个板板改起来比较费劲,当前这个波形主要是看心律情况。
- 2024-10-20
-
发表了主题帖:
【DigiKey创意大赛】便携生命探测仪06+各模块整合联调
[localvideo]d3ebd2761ecd76c87286f8e09232ebab[/localvideo]
上一帖介绍了如何获取心电数据并解析协议,最终显示在电脑上,实现了心电采集功能。本帖介绍如何把所有模块整合到一起,实现预期的作品功能。
一、预期功能
做一个便携生命探测仪,在发生危险时,能帮助救援人员方便找到遇险者。此方案主要采用热敏式图像传感器MLX90640ESF-BAB-000-TU检测人体红外信号,采用BME680气体,湿度,压力,温度传感器评估板记录环境信息,配合一个带屏幕的评估板,显示热成像图片。再额外添加一个心电采集模块,当发现遇险者后,能立即给遇险者采集心电图和心率并在屏幕上显示,方便救援人员判断生命体征状态,好制定下一步救援计划。
二、系统框图
整个系统框图如下图。
如上图所示,ESP32-S3评估板作为主控板,通过SPI接口连接一片480*320分辨率的触摸屏,用来显示各个检测数据、图像、心电波形。通过串口连接一片ESP32-C6评估板。ESP32-C6负责通过串口从LH001-91心电评估板接收字符串数据,然后选择心电波形、心率、导联脱落数据转换成浮点数,再从另外一个串口发给主控板。通过I2C接口连接热敏式图像传感器MLX90640ESF-BAB-000-TU检测人体红外信号,转换成热成像实时图片刷新到显示屏。通过I2C接口连接BME680气体,湿度,压力,温度传感器评估板记录环境信息,并显示在屏幕上。
三、硬件电路焊接
在之前帖子中已经详细介绍ESP32-S3主控板连接的各个传感器的焊接调试过程,在本贴中只展示一下ESP32-C6的两个串口焊接情况。如下图是焊接好的串口导线局部特写。
所有板子,传感器,屏幕等器件焊接好,并打胶加固后的效果如下图。
最终作品我计划使用硬纸板做个手枪型外壳,把这些模块全部套起来,方便使用。
四、软件编写
上一贴中介绍了如何将心电数据打包发送,此次不再赘述。此处介绍一下心电数据接收、解包过程。
为了不停从串口收数,我单开了一个任务,好在ESP32-S3有两个CPU内核,我让这个任务单独运行在另外一个内核上,防止影响屏幕刷新和热成像数据转换。
接收到的数据先是按照包头解包,然后通过数组转浮点数函数提取出来心电波形、心率、导联脱落三个独立的浮点数。其中心率和导联脱落只需简单判断一下合法性,就可以用于显示。但是心电波形是小数表达方式,需要映射成0-239范围内的整数,而且为了波形能占满全部波形区域,每显示一屏,还需要从新计算一下极值范围,保证波形不太小或太大。实际转换代码如下。
代码1:
#include <Arduino.h>
#include <ecg.h>
#include <lcd.h>
uint8_t ECG_data = 0;
uint16_t ECG_hr = 0;
bool ECG_lead = true;//导联脱落指示,true代表正常未脱落
bool ECG_flag = true;//心电数据更新指示,true代表更新了
float ecg_data = 0;
float ecg_data_max = 0;
float ecg_data_min = 0;
float ecg_data_max_temp = 0;
float ecg_data_min_temp = 0;
uint16_t ecg_data_num = 0;
float ecg_hr = 0;
float ecg_lead = 0;
uint8_t Rx_data = 0;
uint8_t Rx_buff[16] = {0};// only use 12 bytes
// float to 0,239
int map_ecg(float in, float a, float b)
{
if (in < a)
return 0;
if (in > b)
return 239;
if(b == a)
b = a + 1;
return (int)((in - a) * 239 / (b - a));
}
//函数说明:将4字节数据转成单精度浮点数据并存入指定地址
//附加说明:用户无需直接操作此函数
//target:目标单精度数据
//buf:待写入数组
//beg:指定从数组第几个元素开始写入
//函数无返回
void Byte2Float(float *target,unsigned char *buf,unsigned char beg)
{
unsigned char *point;
point = (unsigned char*)target; //得到float的地址
point[0] = buf[beg];
point[1] = buf[beg+1];
point[2] = buf[beg+2];
point[3] = buf[beg+3];
}
void task_ecg(void *ptr)
{
int i = 0;
Serial0.begin(1500000); //pin 43/44 to C6-ECG
Serial.println("uart0 init ok");
while (true)
{
if (Serial0.available() > 0)
{
// 读取串口数据
Rx_data = Serial0.read();
// Serial.println(Rx_data);
if('$' == Rx_data)//如果是包头
{
Rx_data = Serial0.read(Rx_buff,12);//连续读取12个字节
if(12 == Rx_data)
{
Byte2Float(&ecg_data,Rx_buff,0);
Byte2Float(&ecg_hr,Rx_buff,4);
Byte2Float(&ecg_lead,Rx_buff,8);
////////////////////
if(ecg_lead == 0)
{
ECG_lead = true;
}
else
{
ECG_lead = false;
}
/////////////////
if(ecg_hr != 0)
{
ECG_hr = int(ecg_hr);
}
//////////////////////
if(ecg_data_max_temp < ecg_data)
ecg_data_max_temp = ecg_data;
if(ecg_data_min_temp > ecg_data)
ecg_data_min_temp = ecg_data;
ecg_data_num++;
if(ecg_data_num > 480)
{
ecg_data_num = 0;
/////
ecg_data_max = ecg_data_max_temp;
ecg_data_min = ecg_data_min_temp;
ecg_data_max_temp = ecg_data;
ecg_data_min_temp = ecg_data;
// Serial.println(ecg_data_max);
// Serial.println(ecg_data_min);
}
ECG_data = map_ecg(ecg_data, ecg_data_min, ecg_data_max);
///////////////
ECG_flag = true;
}
}
}
vTaskDelay(1);
// ECG_flag = true;
// ECG_data = random(0, 239);
// ECG_hr = random(60, 120);
// if (ECG_hr > 110)
// ECG_lead = true;
// else
// ECG_lead = false;
}
vTaskDelete(NULL);
}
代码调试好后,整个系统只需要一个充电宝供电就能独立运行,下图是运行效果图。
实际操作体验效果见开头视频。
截止到此就基本完成了本次作品设计,后续整理材料编写作品文档和制作总结视频。
-
发表了日志:
【DigiKey创意大赛】便携生命探测仪06+各模块整合联调
- 2024-09-26
-
回复了主题帖:
【DigiKey创意大赛】便携生命探测仪05+心电数据解析并在PC显示
chejm 发表于 2024-9-25 13:29
楼主分享的内容确实值得学习,希望有机会能亲自动手实验一下啊
那就申请板子动起来
-
回复了主题帖:
【DigiKey创意大赛】便携生命探测仪05+心电数据解析并在PC显示
lansebuluo 发表于 2024-9-24 08:23
别光顾着开发新东西,也要适时锻炼身体,看这肉...........
年纪大了,练不动了
-
回复了主题帖:
【DigiKey创意大赛】便携生命探测仪05+心电数据解析并在PC显示
wangerxian 发表于 2024-9-23 20:55
3导联应该只能出一个心电数据吧?
是的,只有单导数据
-
回复了主题帖:
【DigiKey创意大赛】便携生命探测仪04+驱动BME680并设计UI
Jacktang 发表于 2024-9-23 07:29
就是每次读取BME680时候,热成像刷新都会卡顿一下,这个看来是确实总线的问题
是滴,换个I2C口就好了,还好ESP32-S3的I2C口多
- 2024-09-21
-
发表了日志:
【DigiKey创意大赛】便携生命探测仪05+心电数据解析并在PC显示
-
发表了主题帖:
【DigiKey创意大赛】便携生命探测仪05+心电数据解析并在PC显示
[localvideo]6d91483f079ec907fa9ab1ad8b63ebaa[/localvideo]
上一帖介绍了驱动BME680并将检测结果显示在屏幕上,然后设计一个操作交互界面,实现用户操作功能。本帖介绍如何获取心电数据并解析协议,最终显示在电脑上。
一、方案和开发板的选择
本次作品为了实现心电采集,我选用了领慧立芯推出的医疗级模拟前端心电采集方案。具体实现采用的是领慧立芯的心电采集评估板,该评估板基于LH001-91设计。
LH001-91是专门针对心电信号采集而开发的医疗级模拟前端,集成了24位Σ-Δ ADC,可编程增益放大器,150Hz带内噪声3μVpp,包括右腿驱动,导电脱落监测等功能的AFE。产品的性能指标优越,其中输入参考噪声2.9 μVpp ,共模抑制比 117dB,内部参考温漂16ppm/℃;静态功耗0.1μW,各项核心指标均达到国际品牌的水平,甚至整体性能优于国际厂商同类型产品,并得到国内医疗客户的专业评测,是目前国内极少能够通过医疗测试的国产芯片。使国内高精度信号链产品有望进入医疗领域,结束被欧美品牌垄断的局面。
更详细资料见如下链接:
http://www.legendsemi.com/hyyy_2/4.html
下图是我申请到的LH001-91的评估板,主要由AFE(LH001-91),通用MCU和两侧的三个电极组成。
领慧立芯同时提供全套的软件演示方案,下图是基于领慧立芯的评估板获得的心电图实时波形图,心电特征明显,满足医疗级应用。
为了降低主控端的软件复杂度,我还选用了一块ESP32-C6开发板做心电数据解析,该开发板由DFRobot设计研发,型号是FireBeetle 2 ESP32-C6。如下图所示。
FireBeetle 2 ESP32-C6是一款基于ESP32-C6芯片设计的低功耗物联网主控板,适用于智能家居项目。ESP32-C6支持Wi-Fi 6、Bluetooth 5、Zigbee 3.0、Thread 1.3通讯协议,可接入多种通讯协议的物联网网络。FireBeetle 2 ESP32-C6支持Type-C、5V DC、太阳能供电,部署时有更多的供电方式选择。
在本次作品中主要是用到了ESP32-C6的两个串口做协议转换。原理框图如下图。
我在PC端用了一个能显示串口数据波形的软件,ESP32-C6负责从LH001-91评估板接收字符串数据,然后转换成上位机软件能识别的协议,再从另外一个串口发出去。
二、硬件电路连接
LH001-91评估板只有一个type-c接口,采集心电用的板子上的焊盘做电极。为了能跟ESP32-C6板连接,需要将电源和串口飞线出来。为了方便采集心电,也需要引出导联线接口。
电源和串口飞线:电源部分直接从USB的5V网络焊盘上引出电源和接地。串口是从板子上的CH340E芯片TX和RX直接飞线出来,为了防止TX线输出信号线与导致烧IO口,我把PCB走线割断串联了一个100欧姆的电阻。最终将飞线出来的接头汇总到一个PH端子上,并用热熔胶固定好。
心电电极飞线:板子上有留有三个电极的焊盘测试点,我找到一个耳机插头形式的导联线,只需要在板上焊接一个耳机插座即可,这个比较容易,焊好后用热熔胶固定。最终的成品如下图。
软件编写
首先是分析LH001-91评估板的程序可知,最终输出的每个采样点数据是一个字符串,如下图所示。
这个字符串中包含心电原始数据,滤波后数据,心率,导联状态等信息,最后以回车换行结尾。数据格式有整型和浮点两种,解析的时候可以都转换成浮点数。实际转换代码如下。
代码1:
#include <Arduino.h>
// 定义一个足够大的数组来存储浮点数
float dataArray[10]; // 假设我们知道数组的最大长度
int dataArrayIndex = 0;
String data = "";
void setup() {
// 初始化串口通信
Serial0.begin(1500000); //pin 16/17 to ECG
Serial1.begin(1500000); //pin 4/5 to PC or Main board
// reserve 200 bytes for the inputString:
data.reserve(200);
}
void loop() {
// 检查是否有数据可读
data = "";
if (Serial0.available() > 0) {
// 读取串口数据
data = Serial0.readStringUntil('\n');
// 移除字符串末尾的换行符
data.trim();
// 使用 split 方法按照逗号拆分字符串
int lastIndex = 0;
for (int i = 0; i < data.length(); i++) {
if (data[i] == ',') {
// 将子字符串转换为浮点数并存储在数组中
String numberString = data.substring(lastIndex, i);
dataArray[dataArrayIndex++] = numberString.toFloat();
lastIndex = i + 1;
}
}
// 处理最后一个元素
String lastNumberString = data.substring(lastIndex);
dataArray[dataArrayIndex++] = lastNumberString.toFloat();
// 重置索引,准备发送数据
dataArrayIndex = 0;
if (0 != dataArray[6]) {
// 将浮点数数组转换回字符串,以逗号分隔,并添加回车符
for (int i = 0; i < 10; i++) {
Serial1.print(dataArray[9 - i]);
if (i < 9) {
Serial1.print(",");
} else {
Serial1.println();
}
}
}
}
}
设计此代码我取了个巧,直接把需求写清楚发给Kimi,然后Kimi会给出一份参考代码,我在参考代码上修改成我需要的形式,省去不少脑细胞。下图是我截取的转换后数据,程序转换效果不错。
然后按照上位机软件的协议要求修改发送代码,上位机显示只需要滤波后的波形数据,心率,导联信息,其他的可以都舍弃掉。改完的代码如下。
代码2:
#include <Arduino.h>
// 定义一个足够大的数组来存储浮点数
float dataArray[10]; // 假设我们知道数组的最大长度
int dataArrayIndex = 0;
String data = "";
unsigned char DataScope_OutPut_Buffer[15] = {0}; //串口发送缓冲区
//函数说明:将单精度浮点数据转成4字节数据并存入指定地址
//附加说明:用户无需直接操作此函数
//target:目标单精度数据
//buf:待写入数组
//beg:指定从数组第几个元素开始写入
//函数无返回
void Float2Byte(float *target,unsigned char *buf,unsigned char beg)
{
unsigned char *point;
point = (unsigned char*)target; //得到float的地址
buf[beg] = point[0];
buf[beg+1] = point[1];
buf[beg+2] = point[2];
buf[beg+3] = point[3];
}
void setup() {
// 初始化串口通信
Serial0.begin(1500000); //pin 16/17 to ECG
Serial1.begin(256000); //pin 4/5 to PC or Main board
// reserve 200 bytes for the inputString:
data.reserve(200);
}
int fps_num = 0;//丢点计数
void loop() {
// 检查是否有数据可读
data = "";
if (Serial0.available() > 0) {
// 读取串口数据
data = Serial0.readStringUntil('\n');
// 移除字符串末尾的换行符
data.trim();
// 使用 split 方法按照逗号拆分字符串
int lastIndex = 0;
for (int i = 0; i < data.length(); i++) {
if (data[i] == ',') {
// 将子字符串转换为浮点数并存储在数组中
String numberString = data.substring(lastIndex, i);
dataArray[dataArrayIndex++] = numberString.toFloat();
lastIndex = i + 1;
}
}
// 处理最后一个元素
String lastNumberString = data.substring(lastIndex);
dataArray[dataArrayIndex++] = lastNumberString.toFloat();
// 重置索引,准备发送数据
dataArrayIndex = 0;
if (fps_num > 8)
{
//丢点处理,防止屏幕刷新过快
DataScope_OutPut_Buffer[0] = '$'; //帧头
Float2Byte(&dataArray[5],DataScope_OutPut_Buffer,1);
Float2Byte(&dataArray[6],DataScope_OutPut_Buffer,5);
Float2Byte(&dataArray[7],DataScope_OutPut_Buffer,9);
DataScope_OutPut_Buffer[13] = 13;
// for (int i = 0; i < 14; i++) {
Serial1.write(DataScope_OutPut_Buffer,14);
// }
fps_num = 0;
}
fps_num ++;
// if (0 != dataArray[6]) {
// // 将浮点数数组转换回字符串,以逗号分隔,并添加回车符
// for (int i = 0; i < 10; i++) {
// Serial1.print(dataArray[9 - i]);
// if (i < 9) {
// Serial1.print(",");
// } else {
// Serial1.println();
// }
// }
// }
}
}
手头没有心电信号模拟仪,只好亲自上阵,电极片贴在胸口,如下图。
期间经过多次调试修改错误,最终实现在PC端显示心电波形。具体效果见开头视频。
数据解析没问题后,下一步就是将这部分整合到主控端,就基本完成本次作品设计了。
-
发表了日志:
【DigiKey创意大赛】便携生命探测仪04+驱动BME680并设计UI
-
发表了主题帖:
【DigiKey创意大赛】便携生命探测仪04+驱动BME680并设计UI
[localvideo]916a2794f14a971a06346587b393da09[/localvideo]
上一帖介绍了如何使用DMA提高热成像显示帧率,本帖先介绍驱动BME680并将检测结果显示在屏幕上,然后设计一个操作交互界面,实现用户操作功能。
一、驱动BME680并显示在屏幕上
BME680具有SPI和I2C两种接口,由于我用的这个ESP32-S3开发板引出的SPI口接屏了并且SPI占IO口比较多,我优先选择使用I2C接口连接BME680小板,为了省事我把它和MLX90640挂在同一个I2C总线上了。如下图。
然后在PlatformIO中安装Adafruit BME680 Library,如下图。
直接使用默认例程测试,Adafruit造好的轮子基本不用费心,刷进板子就OK。测试代码如下。
代码1:
/***************************************************************************
This is a library for the BME680 gas, humidity, temperature & pressure sensor
Designed specifically to work with the Adafruit BME680 Breakout
----> http://www.adafruit.com/products/3660
These sensors use I2C or SPI to communicate, 2 or 4 pins are required
to interface.
Adafruit invests time and resources providing this open source code,
please support Adafruit and open-source hardware by purchasing products
from Adafruit!
Written by Limor Fried & Kevin Townsend for Adafruit Industries.
BSD license, all text above must be included in any redistribution
***************************************************************************/
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#define BME_SCK 13
#define BME_MISO 12
#define BME_MOSI 11
#define BME_CS 10
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BME680 bme; // I2C
//Adafruit_BME680 bme(BME_CS); // hardware SPI
//Adafruit_BME680 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK);
void setup() {
Serial.begin(9600);
while (!Serial);
Serial.println(F("BME680 test"));
if (!bme.begin()) {
Serial.println("Could not find a valid BME680 sensor, check wiring!");
while (1);
}
// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms
}
void loop() {
if (! bme.performReading()) {
Serial.println("Failed to perform reading :(");
return;
}
Serial.print("Temperature = ");
Serial.print(bme.temperature);
Serial.println(" *C");
Serial.print("Pressure = ");
Serial.print(bme.pressure / 100.0);
Serial.println(" hPa");
Serial.print("Humidity = ");
Serial.print(bme.humidity);
Serial.println(" %");
Serial.print("Gas = ");
Serial.print(bme.gas_resistance / 1000.0);
Serial.println(" KOhms");
Serial.print("Approx. Altitude = ");
Serial.print(bme.readAltitude(SEALEVELPRESSURE_HPA));
Serial.println(" m");
Serial.println();
delay(2000);
}
然后考虑怎么移植到现有的程序中。我在热成像图像右边留白的区域就是为BME680数据准备的。在右下角放置一个按钮用来切换屏幕功能。留白的显示效果如下图。
程序设计上,专门为BME680建立一个独立任务,设置成每2秒读一次数据。显示任务判断数据变化就更新屏幕,实际显示效果如下图。
虽然整个屏幕显示实现了,但是我发现一个问题,就是每次读取BME680时候,热成像刷新都会卡顿一下,体验不好,原因就是共用总线造成的。好在ESP32-S3有多个I2C,并且还有一个巨牛掰的GPIO交换矩阵,可以让外设使用任意IO口。于是我从引脚分配表上找了2个闲置的IO口A4和A5配置成了I2C总线,如下图。
在程序初始化中也适当修改,选择I2C1,直接调用即可,代码如下。
代码2:
#define SEALEVELPRESSURE_HPA (1019.25)
Adafruit_BME680 bme(&Wire1); // I2C
void Bme680Init(void)
{
Serial.println(F("BME680 async test"));
// I2C init
Wire1.begin(BME_SDA, BME_SCL);
// bme.Adafruit_BME680(Wire1);
if (!bme.begin())
{
Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
while (1)
;
}
// Set up oversampling and filter initialization
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms
}
以上都弄好后,屏幕显示就流畅起来了,看着虽然简陋些,但是没啥问题。下一步就是设计人机交互。
二、设计操作交互界面
我的预期功能是这样的,本作品具备两个屏幕界面,第一屏界面显示热成像图像和环境信息,用于寻找体温高于环境温度的被施救人员,上面已经做完了。第二屏界面显示心电图,目的是在发现生命体后可以立即给被发现者做一个快速心电检测,帮助救援人员根据心电数据制定下一步的施救方案。
设计的心电图采集界面如下图所示。
整个屏幕上部分分配480x240点阵的区域用于显示心电波形,左下分两行文字,一行显示导联脱落信息,一行显示心率。右下角按钮用来切换两个界面。
心电数据预计从串口接收,这里我新建了一个单独的任务,暂时用于产生随机数模拟心电波形、心率、导联脱落信息。通过模拟数据与显示任务对接,可以仿真心电采集界面的各个功能。仿真数据产生代码如下。
代码3:
#include <Arduino.h>
#include <ecg.h>
#include <lcd.h>
uint8_t ECG_data = 0;
uint16_t ECG_hr = 0;
bool ECG_lead = true;
bool ECG_flag = true;
float ecg_data = 0;
float ecg_hr = 0;
float ecg_lead = 0;
void task_ecg(void *ptr)
{
int i = 0;
while (true)
{
vTaskDelay(4);
ECG_flag = true;
ECG_data = random(0, 239);
ECG_hr = random(60, 120);
if (ECG_hr > 110)
ECG_lead = true;
else
ECG_lead = false;
}
vTaskDelete(NULL);
}
TFT_eSPI库本身支持触摸功能,可以直接调用API接口函数实现触摸输入事件解析,通过按钮锁定逻辑,每点击一次右下角按钮,切换一次界面,按钮文字也随之更改,具体代码如下。
代码4:
void LcdScreenSwitch(void)
{
if (screen_lock == SCREEN_CAM)
{
lock_mxl = true;//禁能热成像采集
screen_lock = SCREEN_ECG;
tft.fillScreen(TFT_BLACK);
LcdEcgBar();
// Serial.println("ECG");
}
else
{
screen_lock = SCREEN_CAM;
tft.fillScreen(TFT_BLACK);
LcdColorBar();
Bme680Display();
lock_mxl = false;//使能热成像采集
// Serial.println("CAM");
}
}
bool Key_Short = false;
void LcdGetTouch(void)
{
uint16_t x = 0, y = 0; // To store the touch coordinates
// Pressed will be set true is there is a valid touch on the screen
bool pressed = tft.getTouch(&x, &y);
// Serial.println(pressed);
// Draw a white spot at the detected coordinates
if (pressed)
{
if ((x > 350) && (y > 260))
{
if (Key_Short == false)
{
Key_Short = true;
LcdScreenSwitch(); // 切换屏幕
}
}
else
{
Key_Short = false;
}
}
else
{
Key_Short = false;
}
}
至此用户交互界面基本设计完成,具体演示效果见开头视频。接下来要搞定心电数据的采集和处理。
- 2024-09-01
-
发表了主题帖:
【DigiKey创意大赛】便携生命探测仪03+ILI9488驱动DMA实现和简单界面
【DigiKey创意大赛】便携生命探测仪03+ILI9488驱动DMA实现和简单界面
[localvideo]90ee46c917f5858a22844e0dedbc1f89[/localvideo]
上一帖介绍了热成像相机软硬件开发环境搭建,并根据查到的一些资料初步实现了热成像基本功能。但是由于我使用的是以前别的测评留下的屏幕,在刷屏时不能使用DMA导致显示帧率很慢,留下一个问题待解决。参见本帖最后。
https://bbs.eeworld.com.cn/thread-1291434-1-1.html
本来想着再买个支持SPI 16bit DMA的屏,或者换其他带屏幕的平台开发板。但是作为DIY老鸟,本着电子垃圾能用就绝不浪费的原则,我对开源显示驱动库TFT_eSPI进行详细研究。虽然该库作者Bodmer在GitHub上回复说,即使勉强使用DMA,但是16bit转18bit的CPU开销会抵消掉DMA带来的优势,但是我这个热成像相机的应用和普通的显示不一样,这个需要用到伪彩色,也就是从调色板选部分颜色就行,不用显示全部16bit色彩范围,这就给我留出来操作空间了。
这里说的伪彩色就是选一组渐变色来表示被测物不同的温度,常见的渐变色如下图。
我这里参考Adafruit_MLX90640的库,设置了256色渐变色作为温度标识,效果接近上图第二个。Adafruit_MLX90640的库提供的是16bit调色板,我需要先转换成18bit调色板,考虑ILI9488在DMA传输时是按照字节送数据,我把每个颜色存成3个字节,RGB顺序按照ILI9488的时序来,就能在DMA自动执行数据传送时不用倒腾字节顺序。时序图如下。
然后就是参照TFT_eSPI库重新写一个8bit的DMA送数据函数,如下图。
最后就是显示的时候,先设置显示窗口,再调用这个函数。其中要注意的是,DMA每次传输不能超过64KB字节,我这里为了省内存,把一帧数据分成了16份,远远小于64KB,代码如下图。
经过这样更改后,显示文字和图片还是采用TFT_eSPI库的接口函数,显示热成像图片使用新做的这个函数,既保证了兼容,还提高了效率。实测效果还不错,CPU纯跑刷屏任务可以到每秒十多帧,完全满足热成像的速度了。实际演示效果见开头视频。
显示搞定后,我做了一个简单的界面,如下图。
热成像占据左边大部分屏幕。上面是320*240点阵的热成像动态图,中间是调色板温度范围指示条。最下面显示最高温度。右半边目前留白,后面计划添加另外一个传感器数据。
本次设计采用的是Adafruit_MLX90640的库,具体代码如下。
#ifndef MXL_H
#define MXL_H
#include <Arduino.h>
#include <lcd.h>
#define MINTEMP 26
#define MAXTEMP 35
#define MLX_MIRROR 0 // Set 1 when the camera is facing the screen
#define FILTER_ENABLE 1 // 滤波器使能
#define INTERPOLATION_ENABLE 1 // 差值使能
extern uint8_t CamBuffer[CAM_W * CAM_H]; // 存储量化后的8位像素温度数据
extern bool lock; // 简单的锁,防止拷贝温度数据的时候对内存的访问冲突
void task_mlx(void *ptr);
#endif // MXL_H
#include <Arduino.h>
#include <mxl.h>
#include <Wire.h>
#include <Adafruit_MLX90640.h>
#include <lcd.h>
#define I2C_SCL SCL
#define I2C_SDA SDA
#define MLX_I2C_ADDR 0x33
Adafruit_MLX90640 mlx;
float frame[32 * 24]; // buffer for full frame of temperatures
float temp_frame[32 * 24];
uint16_t inter_p[320 * 240];
void MxlInit(void)
{
// I2C init
Wire.begin(I2C_SDA, I2C_SCL);
byte error, address;
Wire.beginTransmission(MLX_I2C_ADDR);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print(F("I2C device found at address 0x"));
Serial.print(MLX_I2C_ADDR, HEX);
Serial.println(F(" !"));
}
else if (error == 4)
{
Serial.print(F("Unknown error at address 0x"));
Serial.println(MLX_I2C_ADDR, HEX);
}
Serial.println(F("Adafruit MLX90640 Simple Test"));
if (!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire))
{
Serial.println(F("MLX90640 not found!"));
while (1)
delay(10);
}
// mlx.setMode(MLX90640_INTERLEAVED);
mlx.setMode(MLX90640_CHESS);
mlx.setResolution(MLX90640_ADC_18BIT);
mlx90640_resolution_t res = mlx.getResolution();
mlx.setRefreshRate(MLX90640_16_HZ);
mlx90640_refreshrate_t rate = mlx.getRefreshRate();
Wire.setClock(800000); // max 1 MHz
// image init
for (int i = 0; i < 320 * 240; i++)
{
inter_p[i] = 255;
}
Serial.println("image init ok");
// frame init
for (int i = 0; i < 32 * 24; i++)
{
temp_frame[i] = MINTEMP;
}
Serial.println("frame init ok");
Serial.println(F("All init over."));
}
// 消抖并翻转
// Filter temperature data and change camera direction
void filter_frame(float *in, float *out)
{
if (MLX_MIRROR == 1)
{
for (int i = 0; i < 32 * 24; i++)
{
if (FILTER_ENABLE == 1)
out[i] = (out[i] + in[i]) / 2;
else
out[i] = in[i];
}
}
else
{
for (int i = 0; i < 24; i++)
for (int j = 0; j < 32; j++)
{
if (FILTER_ENABLE == 1)
out[32 * i + 31 - j] = (out[32 * i + 31 - j] + in[32 * i + j]) / 2;
else
out[32 * i + 31 - j] = in[32 * i + j];
}
}
}
// float to 0,255
int map_f(float in, float a, float b)
{
if (in < a)
return 0;
if (in > b)
return 255;
return (int)((in - a) * 255 / (b - a));
}
// 32*24插值10倍到320*240
// Transform 32*24 to 320 * 240 pixel
void interpolation(float *data, uint16_t *out)
{
for (uint8_t h = 0; h < 24; h++)
{
for (uint8_t w = 0; w < 32; w++)
{
out[h * 10 * 320 + w * 10] = map_f(data[h * 32 + w], MINTEMP, MAXTEMP);
}
}
for (int h = 0; h < 240; h += 10)
{
for (int w = 1; w < 310; w += 10)
{
for (int i = 0; i < 9; i++)
{
out[h * 320 + w + i] = (out[h * 320 + w - 1] * (9 - i) + out[h * 320 + w + 9] * (i + 1)) / 10;
}
}
for (int i = 0; i < 9; i++)
{
out[h * 320 + 311 + i] = out[h * 320 + 310];
}
}
for (int w = 0; w < 320; w++)
{
for (int h = 1; h < 230; h += 10)
{
for (int i = 0; i < 9; i++)
{
out[(h + i) * 320 + w] = (out[(h - 1) * 320 + w] * (9 - i) + out[(h + 9) * 320 + w] * (i + 1)) / 10;
}
}
for (int i = 0; i < 9; i++)
{
out[(231 + i) * 320 + w] = out[230 * 320 + w];
}
}
}
// Quick sort
void qusort(float s[], int start, int end) // 自定义函数 qusort()
{
int i, j; // 定义变量为基本整型
i = start; // 将每组首个元素赋给i
j = end; // 将每组末尾元素赋给j
s[0] = s[start]; // 设置基准值
while (i < j)
{
while (i < j && s[0] < s[j])
j--; // 位置左移
if (i < j)
{
s[i] = s[j]; // 将s[j]放到s[i]的位置上
i++; // 位置右移
}
while (i < j && s[i] <= s[0])
i++; // 位置左移
if (i < j)
{
s[j] = s[i]; // 将大于基准值的s[j]放到s[i]位置
j--; // 位置左移
}
}
s[i] = s[0]; // 将基准值放入指定位置
if (start < i)
qusort(s, start, j - 1); // 对分割出的部分递归调用qusort()函数
if (i < end)
qusort(s, j + 1, end);
}
// The camera allows up to four non-consecutive bad points with a value of nan.
void bug_fix(float *frame)
{
for (int i = 0; i < 768 - 1; i++)
{
if (isnan(frame[i]))
frame[i] = frame[i + 1];
}
}
uint32_t runtime = 0;
int fps = 0;
float max_temp = 0.0;
int record_index = 0;
// 热成像读取多任务
void task_mlx(void *ptr)
{
int i = 0;
MxlInit();
while (true)
{
// 获取一帧
// Get a frame
if (mlx.getFrame(frame) != 0)
{
Serial.println(F("Get frame failed"));
return;
}
bug_fix(frame);
// 和上次结果平均,滤波
// Filter temperature data
filter_frame(frame, temp_frame);
// 快排
qusort(frame, 0, 32 * 24 - 1);
max_temp = frame[767];
MaxTemp = max_temp;
// Serial.println(max_temp);
if (INTERPOLATION_ENABLE == 1)
{
// 温度矩阵转换图像矩阵,将32*24插值到320*240
// Display with 320*240 pixel
interpolation(temp_frame, inter_p);
lock = true;
i = 0;
while (i < 76800)
{
CamBuffer[i] = inter_p[i];
i++;
}
lock = false;
}
else
{
// Display with 32*24 pixel
for (uint8_t h = 0; h < 24; h++)
{
for (uint8_t w = 0; w < 32; w++)
{
uint8_t colorIndex = map_f(temp_frame[h * 32 + w], MINTEMP, MAXTEMP);
for (uint8_t y = 0; y < 10; y++)
{
for (uint8_t x = 0; x < 10; x++)
{
CamBuffer[(h * 10 + y) * 320 + w * 10 + x] = colorIndex;
}
}
}
}
}
}
vTaskDelete(NULL);
}
目前还有一个问题,就是屏幕显示的温度图像有纵向干扰条纹,我用了不同库读取数据都存在,初步怀疑是传感器的问题,但是手头只有这一个宝贝,还没办法验证,等我再查查资料,看有没有解决办法。
下一步添加另外一个传感器Adafruit BME680的读取和显示。传感器如下图。