sipower

  • 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的读取和显示。传感器如下图。

统计信息

已有167人来访过

  • 芯积分:1476
  • 好友:2
  • 主题:82
  • 回复:198

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言