1435|7

574

帖子

11

TA的资源

一粒金砂(高级)

楼主
 

【DigiKey创意大赛】多通道微型气相色谱采集单元-作品提交 [复制链接]

 
作品名称:多通道微型气相色谱采集单元
作者:sunduoze
 
一、作品简介
 
该作品主要为微型气相色谱系统开发一套多通道数据采集单元。使用大赛推荐的EVAL-AD7606CFMCZ板卡作为信号采集的核心,整个系统使用PD供电并使用ESP32作为主控及其Wi-Fi实现了可兼容TCD、PID、DID 3种不同类型的同步或异步色谱采集并通过Wi-Fi将图谱数据上传到电脑等设备实现物联,方便用户实时查看气相色谱中检测器测量得到的图谱并可以辅助进行峰识别、数据导出等功能。此外还具有电压和电流辅助测量功能,通过OLED菜单切换显示ADC数据、通道校准、调节TCD电源等功能。

 

名词解释:
微型气相色谱:它是一种小型化、便携式的气相色谱(GC, 分离和分析混合物中成分的化学分析技术)系统,通过微型化的色谱柱和微型检测器,能够高效分离和检测气体混合物中的成分,广泛应用于快速、实时的气体分析领域。
热导检测器TCD气相色谱仪中常用的检测器之一,通过测量气体在检测器中传导热量的变化来检测和定量分离柱中的化合物,广泛用于分析气体混合物中的组分。
光离子化检测器PID气相色谱仪中的一种检测器,利用紫外光照射样品产生的离子电流来检测和定量分离柱中的挥发性有机化合物,特别适用于检测低浓度的挥发性有机物。
放电离子化检测器DID气相色谱仪中的一种检测器,主要利用放电产生等离子体间接将化合物电离产生的离子电流的方法来检测和定量分离柱中的化合物,可以检测几乎所有化合物。
 
 
二、系统框图
整个系统开发部分包含了采集单元的硬件(Detector DAS Board & Eval-AD7606C板)及其基于ESP32的嵌入式软件和电脑端软件(Data Acquisition System)。采集单元启动后通过Wi-Fi连接到路由器后启动TCP的Client端,电脑端通过上位机软件的server端输入IP后连接采集单元,采集单元将采集后的数据发送到上位机软件,上位机软件通过UI显示数据、图谱以及导出数据。

2.1硬件架构

 

整个系统在ADI的EVAL-AD7606CFMCZ 评估套件 | 亚德诺半导体 (analog.com)评估板的基础上开发控制、交互、物联以及检测器接口部分来实现整个系统的硬件部分。
整个系统使用ESP32作为控制核心,采用TYPE-C接口供电和烧录(及串口通讯),使用Type-C控制器与适配器进行cc通讯实现PD充电,切换为20V电压输入,通过使用螺旋编码按键、OLED、蜂鸣器进行简单的硬件端UI交互。使用数字电位器控制可调输出的高性能电压源Vs1为TCD供电,通过DC/DC降压到5.6V的Vs2为EVAL-AD7606C、PID及DID供电,AD7606C的6个通道分别采集TCD、PID、DID、Vs1、Vs2、VBUS(20V)信号或电压,剩余的2个通道预留为辅助电压测量(可配置最大为±20V的差分输入范围)和电流测量(0.1-9.8A)功能。

2.2嵌入式软件架构

由于其可以选择Arduino模式编程,并且有丰富的库可以方便调用,可大幅提高开发效率。实现了Wi-Fi的Client端收发数据、通过螺旋编码按键来控制OLED菜单切换,采集数据的校准、数值及曲线显示等功能。
嵌入式软件采用Platform IO+VSCODE对ESP32进行开发。ESP32具有2个核心,为了最大化利用,于是利用FreeRTOS来调度这两核心分别用于ADC实时数据采集和其他任务(包含Wi-Fi客户端、OLED菜单处理及显示、调试线程等)。
整体嵌入式软件基于ESP32平台下,运行Free RTOS系统来方便开发。主要包含了3层,应用层主要包含用于实现Wi-Fi client端数据的收发模块和OLED的交互模块;中间层包含了U8g2图形库、Wi-Fi Client库等来方便应用层与底层(LL、HAL、驱动层)交互;底层包含了AD7606C、AD5272、FUSB302等驱动库,I2C、SPI、GPIO等HAL库。

2.3上位机软件架构

上位机软件同样采用VSCODE,使用python进行开发,界面部分采用PyQt5实现,借助其成熟的轮子,300+行代码便实现了整个具有初步功能的应用开发。
使用PyQt5的图形用户界面的数据采集系统(DAS),用于实时数据可视化、控制和分析。软件架构包括以下组件:
DataReceiver 类:
监听指定 IP 地址和端口的传入数据。
发出带有接收数据的信号。
管理数据缓冲区的存储,并可选择将数据写入CSV文件。
PlotUpdater 类:
使用PyQtGraph实时绘图更新接收到的数据,并发送更新绘图和文本显示的信号。
SpectrumAnalysisThread 类:
在单独的线程中使用 'spectrum_analysis.py' 脚本执行频谱分析。
MainWindow 类:
使用PyQt5实现主GUI窗口。
集成了一个用于实时绘图的PlotWidget和一个用于显示数据的QTextEdit。
处理用户交互,如启动/停止数据服务器、清除图表和启动频谱分析。
使用单独的线程进行数据接收、绘图、频谱分析和打开Web浏览器以显示Dash应用。
Dash 集成:
代码包括一个用于频谱分析的Dash应用。
当用户点击“停止”按钮时,在单独的线程中启动Dash应用,并打开Web浏览器以可视化分析结果。
总体而言,该架构采用了模块化和多线程设计,将与数据接收、可视化和分析相关的关注点分离,以提高性能和响应能力。GUI集成了实时绘图和基于Dash的频谱分析,实现了全面的数据探索。
 
 
三、各部分功能说明

3.1主要硬件组件

3.1.1 控制器及通讯单元

ESP32是一款由乐鑫信息科技开发的低成本、高性能的微控制器芯片,主要用于物联网(IoT)应用。ESP32继承了其前身ESP8266的特点,并在性能、功耗以及功能上进行了改进。以下是ESP32的一些重要特性和介绍:
  1. 双核处理器:ESP32搭载了两个处理器核心,可分别用于应用处理和Wi-Fi网络协议栈的管理,提高了系统的整体性能和响应速度。
  2. Wi-Fi和蓝牙连接:ESP32支持2.4GHz的Wi-Fi连接,适用于无线网络通信。此外,它还集成了蓝牙4.2/5.0技术,可用于连接其他蓝牙设备或实现蓝牙Mesh网络。
  3. 丰富的接口:ESP32提供了多种通用输入输出接口(GPIO)、SPI、I2C、UART等,使其易于与其他硬件设备和传感器进行连接,满足各种应用场景的需求。
  4. 低功耗设计:ESP32在设计上注重功耗优化,支持多种低功耗模式,可在电池供电的应用中实现长时间运行。
  1. ADC模数转换器
该系统采用ADI的EVAL-AD7606CFMCZ 评估板来实现ADC功能,其中的AD7606C-18是18位、1 MSPS SAR-ADC(所有通道),具有 1MΩ 最小模拟输入阻抗 (RIN) 的输入缓冲器,可以免除去以往的前端电路设计,极大方便并行数据的采集。同时可以通过寄存器配置来切换ADC的模式(差分,单端)及量程,极大的简化Front-End设计成本并且简化了系统设计复杂度。
本设计中采用单线SPI与其通讯来节省pin脚占用,另外评估板上集成了ADR4525的B级基准源,2 ppm/°C的温漂性能,1.25μV p-p的低频输出噪声极大的降低整个系统误差以及噪声。
由于该芯片有8路采集功能,故为了将其通道全部利用,实际除了检测器信号外还预留了辅助的电压电流测量功能,系统不同检测器供电电压监控的功能,以下是实际通道的配置信息:
评估板跳线帽配置:

3.1.3 USB转串口(无需外围晶体管实现一键烧录ESPxx)

CH343是一个USB总线的转接芯片,实现USB转高速异步串口,同时支持115200bps及以下通讯波特率的自动识别和动态自适应,提供了常用的MODEM联络信号,用于为计算机扩展异步串口,或者将普通的串口设备或者MCU直接升级到USB总线。通过RTS pin和DTRTNOW pin连接ESPxx的EN脚和IO00脚,利用流控制来实现程序的自动烧录。

3.1.4 其他外设

1. OLED
该系统采用OLED(SSD1306)为辅助显示屏幕来方便用户功能的切换
2. 螺旋编码按键,该按键用于与OLED实现UI交互,极大的方便调试和使用

3.2 电源设计

3.2.1 电源需求

  • 系统中DID & PID均需要5.6V以上电源供电,AD7606C EVAL板也需要5V以上电源供电
  • TCD检测器需要18-20V电源进行供电
  • MCU、OLED等外设均需要3.3V电源供电

3.2.2 电源管理

系统通过Type-C接口,采用PD协议通过cc线与适配器通讯,输出20V电压为整个系统供电。通过buck电路降压到5.6V给DID与PID检测器以及AD7606C EVAL板供电;通过超低噪声和超高PSRR LDO及可编程电阻产生18-20V的可编程电压输出;使用LDO将5.6V转换成3.3V给MCU及周边外设供电。
  1. TYPE-C及PD
    该系统采用FUSB302B 通过较少的编程实现了 USB Type-C 检测,包括连接和定向。该设计中实现可高达 60 W 的电源电力输送。
  2. TCD用可调电源
    该电源采用ADI的LT3045超低噪声和PSRR的LDO来给检测器提供电源,由于实际需求(此处设计超过其典型值但未超过其绝对最大值),需要其具有小范围的电压调节功能,故采用AD5272来(最大20kOhm)的可调

20V转5.6V的降压Buck电路及5.6V转3.3V的LDO


3.2.3 电流采集(测量范围:0.1A – 9.8A)
电流采集部分采用了支持高共模电压的电流监视放大器INA290A3,通过Vishay Y14730R00500B0R的±5ppm/°C的5 mOhms ±0.1% 3W的精密电阻两端电压并实现100倍电压放大,送到ADC-CH8来进行采集(此处采用ADI的运放评估板的通用接口来与ADC板进行连接),理论上可以做到76.3uA的最小分辨能力(实际设计连接器pin1与ADI板的pin1不对应,ADC-CH8理应配置为0-5V单极性采样来做到~38uA的电流分辨能力)。

3.3 物理设计
3.3.1 内部连接
为了方便的使用板,并且重复开发,采用板卡堆叠的方式实现。实际使用SMB接头、插拔式连接器及cable、排针及排母实现连接。

3.3.2 外部接口
PID、DID均采用Type-C接口(这里没有做防呆,但PD适配器插入也不会损坏),TCD擦用XH-2.54的连接器与其连接。辅助为了方便的使用板,并且重复开发,采用板卡堆叠的方式实现。实际使用SMB接头、插拔式连接器及cable、排针及排母实现连接。

3.3.3 其他

 

四、作品源码
硬件资料:
嵌入式软件源码:
主要逻辑代码:(全部代码详见下载链接)
#include "task.h"

#include <WiFi.h>
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
#include "soc/rtc_wdt.h" // 设置看门狗应用

#include "PD_UFP.h"
#include "kalman_filter.h"
#include "event.h"
#include "rotary.h"
#include "beep.h"
#include "menu.h"

uint8_t *C_table[] = {c1, c2, c3, Lightning, c5, c6, c7};
uint8_t rotary_dir = false;
uint8_t volume = true;

int32_t adc_raw_data[ADC_ALL_CH];
float adc_disp_val[ADC_ALL_CH];

uint8_t conn_wifi = 0;
adc_calibration_ adc_cali;

char ssid[] = "xxxxx";		  // wifi名
char password[] = "xxxxx";	  // wifi密码
char ssid_bk[] = "xxxxx";	  // wifi名
char password_bk[] = "xxxxx"; // wifi密码

const IPAddress serverIP(192, 168, 1, 1); // 欲访问的服务端IP地址
uint16_t serverPort = 1234;				  // 服务端口号
uint64_t ChipMAC;
char ChipMAC_S[19] = {0};
char CompileTime[20];

KalmanFilter kf_disp(0.00f, 1.0f, 10.0f, 100.0f);
KalmanFilter kf_main_ui(0.00f, 1.0f, 100.0f, 20000.0f);

WiFiClient client; // 声明一个ESP32客户端对象,用于与服务器进行连接
AD7606C_Serial AD7606C_18(ADC_CONVST, ADC_BUSY);
U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE, /* clock=*/SCL, /* data=*/SDA); // ESP32 Thing, HW I2C with pin remapping
AD5272 digital_pot(POT_ADDR_NC);
PD_UFP_c PD_UFP;

uint8_t pd_init(void)
{
	uint8_t ret = 0;
	uint16_t timeout = 2000;
	PD_UFP.init(PD_POWER_OPTION_MAX_20V);
	Serial.printf("PD_UFP.init-ing\r\n");
	while (1)
	{
		PD_UFP.run();
		if (PD_UFP.is_power_ready())
		{
			if (PD_UFP.get_voltage() == PD_V(20.0) && PD_UFP.get_current() >= PD_A(1.5))
			{
				PD_UFP.set_output(1); // Turn on load switch
				Serial.printf("PD 20V ENABLE Sucess\r\n");
				SetSound(BootSound); // 播放音效
				break;
			}
			else
			{
				PD_UFP.set_output(0); // Turn off load switch
				Serial.printf("DISABLE\r\n");
				ret = 1;
				break;
			}
		}
		if (timeout-- <= 1)
		{
			Serial.printf("PD power not ready\r\n");
			ret = 2;
			break;
		}
		vTaskDelay(1);
	}
	return ret;
}

uint8_t wifi_init()
{
	uint8_t ret = 0;
	WiFi.mode(WIFI_STA);
	WiFi.setSleep(false); // 关闭STA模式下wifi休眠,提高响应速度
	WiFi.begin(ssid, password);
	uint8_t i = 100;
	while (WiFi.status() != WL_CONNECTED)
	{
		delay(200);
		Serial.print(".");
		if (i-- == 50)
		{
			Serial.printf("Connecting to %s\r\n", ssid_bk);
			WiFi.begin(ssid_bk, password_bk);
			Serial.println("WiFi connected");
		}
		if (i <= 1)
		{
			Serial.print("\r\nWifi connect failed\r\n");
			ret = 1;
			break;
		}
	}

	if (ret == 0)
	{
		if (i > 50)
		{
			Serial.printf("Connecting to %s\r\n", ssid);
		}
		Serial.println("IP address: ");
		Serial.println(WiFi.localIP());
		Serial.println("MAC address: ");
		Serial.println(WiFi.macAddress());
		Serial.println("DNS address: ");
		Serial.println(WiFi.dnsIP());
		Serial.println("Gateway address: ");
		Serial.println(WiFi.gatewayIP());
		Serial.println("Subnet mask: ");
		Serial.println(WiFi.subnetMask());
	}
	return ret;
}

void adc_init()
{
	adc_cali.adc_gain_ch[ADC_CH1] = 1.0f;
	adc_cali.adc_offset_ch[ADC_CH1] = -0.017699; //-0.017547;

	adc_cali.adc_gain_ch[ADC_CH2] = 0.99664f;	 // 未校准
	adc_cali.adc_offset_ch[ADC_CH2] = -0.017547; // 未校准

	adc_cali.adc_gain_ch[ADC_CH3] = 0.997993; // 0.990973;
	adc_cali.adc_offset_ch[ADC_CH3] = -0.01098;

	adc_cali.adc_gain_ch[ADC_CH4] = 1.000207f;
	adc_cali.adc_offset_ch[ADC_CH4] = -0.00011;

	adc_cali.adc_gain_ch[ADC_CH5] = 1.000132f;
	adc_cali.adc_offset_ch[ADC_CH5] = 0.0;

	adc_cali.adc_gain_ch[ADC_CH6] = 0.999882f;
	adc_cali.adc_offset_ch[ADC_CH6] = 0.0;

	adc_cali.adc_gain_ch[ADC_CH7] = 5.0f * 1.000188f;
	adc_cali.adc_offset_ch[ADC_CH7] = 0.0;

	adc_cali.adc_gain_ch[ADC_CH8] = -2.0f * 1.000188f; // 未校准增益 curr = volt / 100 / 0.005R
	adc_cali.adc_offset_ch[ADC_CH8] = 0.00605;

	AD7606C_18.config();
}

void oled_init()
{
	oled.begin();
	oled.setBusClock(4e5);
	oled.enableUTF8Print();
	oled.setFontDirection(0);
	oled.setFontPosTop();
	oled.setFont(u8g2_font_wqy12_t_gb2312);
	oled.setDrawColor(1);
	oled.setFontMode(1);
}

void ad527x_init()
{
	Wire.begin(SDA, SCL, 1e5);
	int ret = digital_pot.set_res_val(0);
	if (ret != 0) // check if data is sent successfully
		Serial.println("[Error]digital_pot init !");
	Wire.begin(SDA, SCL, 1e6);
}

void hardware_init(void)
{
	beep_init();
	WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // 关闭断电检测
	Serial.begin(5e5);
	Wire.begin(SDA, SCL, 4e5);
	if (pd_init() == 0)
	{
		pd_status = true;
	}
	// FilesSystemInit(); // 启动文件系统,并读取存档 **bug***
	Serial.printf("\r\n\r\nDetector DAS based on EVAL-AD7606CFMCZ !\r\n");
	oled_init();

	disp_boot_info();
	delay(500);
	enter_logo();

	if (digital_pot.init() != 0)
	{
		Serial.println("Cannot send data to the digital_pot.");
	}
	ad527x_init();
	rotary_init(); // 初始化编码器
	adc_init();
	// ble_phyphox_init();
	rtc_wdt_protect_off(); // 看门狗写保护关闭
	rtc_wdt_enable();
	rtc_wdt_set_time(RTC_WDT_STAGE0, 3000); // wdt timeout

	get_sin();
}

uint8_t sin_tab[SIN_TB_SIZE];
void get_sin(void)
{
	const uint16_t uPoints = SIN_TB_SIZE;
	float x, uAng;
	uAng = 360.000 / uPoints;
	for (int i = 0; i < uPoints; i++)
	{
		x = uAng * i;
		x = x * (3.14159 / 180); // 弧度=角度*(π/180)
		sin_tab[i] = (255 / 2) * sin(x) + (255 / 2);
		// printf("sin tab[%d]: %f\n", i, sin_tab[i]);
	}
}

void xTask_oled(void *xTask)
{
	static double rotary;
	static double rotary_hist;

	while (1)
	{
		sys_KeyProcess();
		// TimerEventLoop();
		rotary = sys_Counter_Get();
#ifdef ROTARY_DEBUG
		if (rotary != rotary_hist)
		{
			Serial.printf("rotary:%lf\n", rotary);
		}
#endif // ROTARY_DEBUG

		rotary_hist = rotary;
		// 刷新UI
		System_UI();
		vTaskDelay(1);
	}
}

/*
 *
 */
void xTask_dbgx(void *xTask)
{
	// Serial.print("core[");
	// Serial.print(xTaskGetAffinity(xTask));
	// Serial.printf("]xTask_dbg \r\n");
	while (1)
	{
		// Serial.printf("[%6d %6d %6d %6d][%6d %6d %6d %.6f]\r\n", adc_raw_data[0], adc_raw_data[1], adc_raw_data[2], adc_raw_data[3], adc_raw_data[4], adc_raw_data[5], adc_raw_data[6], C2V(adc_r_d_avg[ADC_CH8], PN5V00));
		vTaskDelay(100);
	}
}

void xTask_adcx(void *xTask)
{
	static uint16_t cnt;
	// Serial.print("core[");
	// Serial.print(xTaskGetAffinity(xTask));
	// Serial.printf("]xTask_adc \r\n");
	// Serial.print("tsk_adc: ");
	// Serial.println(millis());
	while (1)
	{
		AD7606C_18.fast_read(adc_raw_data);
		adc_disp_val[ADC_CH1] = (C2V(adc_raw_data[ADC_CH1], PN20V0) + adc_cali.adc_offset_ch[ADC_CH1]) * adc_cali.adc_gain_ch[ADC_CH1];
		adc_disp_val[ADC_CH2] = (C2V(adc_raw_data[ADC_CH2], PN20V0) + adc_cali.adc_offset_ch[ADC_CH2]) * adc_cali.adc_gain_ch[ADC_CH2];
		adc_disp_val[ADC_CH3] = (C2V(adc_raw_data[ADC_CH3], PN20V0) + adc_cali.adc_offset_ch[ADC_CH3]) * adc_cali.adc_gain_ch[ADC_CH3];
		adc_disp_val[ADC_CH4] = (C2V(adc_raw_data[ADC_CH4], PP5V00) + adc_cali.adc_offset_ch[ADC_CH4]) * adc_cali.adc_gain_ch[ADC_CH4];
		adc_disp_val[ADC_CH5] = (C2V(adc_raw_data[ADC_CH5], PP5V00) + adc_cali.adc_offset_ch[ADC_CH5]) * adc_cali.adc_gain_ch[ADC_CH5];
		adc_disp_val[ADC_CH6] = (C2V(adc_raw_data[ADC_CH6], PP10V0) + adc_cali.adc_offset_ch[ADC_CH6]) * adc_cali.adc_gain_ch[ADC_CH6];
		adc_disp_val[ADC_CH7] = (C2V(adc_raw_data[ADC_CH7], PP5V00) + adc_cali.adc_offset_ch[ADC_CH7]) * adc_cali.adc_gain_ch[ADC_CH7];
		adc_disp_val[ADC_CH8] = (C2V(adc_raw_data[ADC_CH8], PN5V00) + adc_cali.adc_offset_ch[ADC_CH8]) * adc_cali.adc_gain_ch[ADC_CH8];

		delayMicroseconds(730);
	}
}

void xTask_wifi(void *xTask)
{
	if (wifi_init() == 0)
	{
		wifi_status = true;
	}
	while (1)
	{
		// Serial.println("connect server -ing");
		if (client.connect(serverIP, serverPort)) // 连接目标地址1
		{
			Serial.println("connect success!");
			// client.print("Hello world!");					 // 向服务器发送数据
			while (client.connected() || client.available()) // 如果已连接或有收到的未读取的数据
			{
				conn_wifi = 1;
				client.printf("%2.6f, %2.6f, %2.6f, %2.6f, %2.6f, %2.6f, %2.6f, %2.6f\r\n",
							  adc_disp_val[ADC_CH1], adc_disp_val[ADC_CH2],
							  adc_disp_val[ADC_CH3], adc_disp_val[ADC_CH4],
							  adc_disp_val[ADC_CH5], adc_disp_val[ADC_CH6],
							  adc_disp_val[ADC_CH7], adc_disp_val[ADC_CH8]);
				vTaskDelay(100);
			}
			conn_wifi = 0;
			Serial.println("close clent");
			client.stop(); // 关闭客户端
		}
		else
		{
			conn_wifi = 0;
			// Serial.println("connect fail!");
			client.stop(); // 关闭客户端
		}
		vTaskDelay(1000);
	}
}

void ble_phyphox_init()
{
	PhyphoxBLE::start("Detector-DAS");
	PhyphoxBleExperiment MultiGraph;
	MultiGraph.setTitle("Detector-DAS");
	MultiGraph.setCategory("DigiKey创意大赛:多通道微型气相色谱采集单元");
	MultiGraph.setDescription("基于AD7606C-18的多通道检测器数据采集系统");
	PhyphoxBleExperiment::View firstView;
	firstView.setLabel("FirstView"); // Create a "view"
	PhyphoxBleExperiment::Graph detector_data_graph;
	PhyphoxBleExperiment::Graph::Subgraph pid_data;
	pid_data.setChannel(1, 2);
	pid_data.setColor("ff00ff");
	pid_data.setStyle(STYLE_LINES);
	pid_data.setLinewidth(2);
	detector_data_graph.addSubgraph(pid_data);
	PhyphoxBleExperiment::Graph::Subgraph did_data;
	did_data.setChannel(1, 3);
	did_data.setColor("0000ff");
	did_data.setStyle(STYLE_LINES);
	did_data.setLinewidth(2);
	detector_data_graph.addSubgraph(did_data);
	PhyphoxBleExperiment::Graph::Subgraph tcd_data;
	tcd_data.setChannel(1, 4);
	tcd_data.setColor("0ee0ff");
	tcd_data.setStyle(STYLE_LINES);
	tcd_data.setLinewidth(2);
	detector_data_graph.addSubgraph(tcd_data);

	firstView.addElement(detector_data_graph);
	MultiGraph.addView(firstView);
	PhyphoxBLE::addExperiment(MultiGraph);

	// PhyphoxBLE::printXML(&Serial);
}

float periodTime2 = 2.0; // in s
float generateSin2(float x)
{
	return 1.0 * sin(x * 2.0 * PI / periodTime2);
}
void xTask_blex(void *xTask)
{

	float currentTime = millis() / 1000.0;
	float sinus = generateSin2(currentTime);
	float cosinus = generateSin2(currentTime + 0.5 * periodTime2);
	PhyphoxBLE::write(currentTime, sinus, cosinus, cosinus);
	delay(100);
	// PhyphoxBLE::poll(); //Only required for the Arduino Nano 33 IoT, but it does no harm for other boards.
}

void xTask_blex2(void *xTask)
{
	float time;
	float data0, data3, data4;
	while (1)
	{
		time = millis() / 1000.0;
		// data0 = C2V(adc_r_d_avg[0], PN10V0);
		// data3 = C2V(adc_r_d_avg[3], PN10V0);
		// data4 = C2V(adc_r_d_avg[4], PN10V0);
		PhyphoxBLE::write(time, data0, data3, data4);
		vTaskDelay(100);
		PhyphoxBLE::poll(); // Only required for the Arduino Nano 33 IoT, but it does no harm for other boards.
	}
}

float map_f(float x, float in_min, float in_max, float out_min, float out_max)
{
	const float run = in_max - in_min;
	if (run == 0)
	{
		log_e("map(): Invalid input range, min == max");
		return -1; // AVR returns -1, SAM returns 0
	}
	const float rise = out_max - out_min;
	const float delta = x - in_min;
	return (delta * rise) / run + out_min;
}

void i2c_dev_scan()
{
	Wire.begin(SDA, SCL, 1e3);
	Serial.begin(5e5);
	uint8_t error, address;
	int nDevices;
	Serial.println("Scanning...");
	nDevices = 0;
	for (address = 1; address < 127; address++)
	{
		Wire.beginTransmission(address);
		error = Wire.endTransmission();
		if (error == 0)
		{
			Serial.print("I2C device found at address 0x");
			if (address < 16)
				Serial.print("0");
			Serial.print(address, HEX);
			Serial.println(" !");
			nDevices++;
		}
		else if (error == 4)
		{
			Serial.print("Unknow error at address 0x");
			if (address < 16)
				Serial.print("0");
			Serial.println(address, HEX);
		}
	}
	if (nDevices == 0)
		Serial.println("No I2C devices found\n");
	else
		Serial.println("done\n");
	delay(5000); // wait 5 seconds for next scan
}

 

上位机软件源码:
import sys
import socket
import subprocess
from PyQt5 import sip
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QTextEdit, QPushButton, QLabel, \
    QHBoxLayout, QLineEdit, QScrollArea
from PyQt5.QtCore import pyqtSignal, QObject, QThread, QTimer, QDateTime
import pyqtgraph as pg
import numpy as np
from dash import Dash, dcc, html, dash_table
import webbrowser
import time


class DataReceiver(QObject):
    data_received = pyqtSignal(list)

    def __init__(self, host, port):
        super().__init__()
        self.host = host
        self.port = port
        self.server_socket = None
        self.running = False
        self.data_buffer = []
        self.csv_file = None
        self.start_time = None

    def start_server(self):
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.bind((self.host, self.port))
        self.server_socket.listen(1)
        self.running = True
        self.data_buffer = []

        while self.running:
            client_socket, address = self.server_socket.accept()
            with client_socket:
                print('Connected by', address)
                buffer = ""
                while self.running:
                    data = client_socket.recv(1024).decode('utf-8')
                    if not data:
                        break

                    buffer += data
                    messages = buffer.split('\r\n')

                    for msg in messages[:-1]:
                        data_values = [float(value) for value in msg.split(',')]
                        self.data_received.emit(data_values)
                        self.data_buffer.append(data_values)

                        if self.csv_file is not None:
                            timestamp = (QDateTime.currentMSecsSinceEpoch() - self.start_time) / 1000.0
                            row = [f'{timestamp:.3f}'] + list(map(str, data_values))
                            self.csv_file.write(','.join(row) + '\n')

                    buffer = messages[-1]

    def stop_server(self):
        self.running = False
        if self.server_socket:
            self.server_socket.close()

        if self.csv_file:
            self.csv_file.close()
            self.csv_file = None

    def start_csv_file(self):
        timestamp = QDateTime.currentDateTime().toString("yyyyMMddhhmmss")
        filename = f"DAS_{timestamp}.csv"
        self.csv_file = open(filename, 'w')
        self.csv_file.write("Timestamp,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8\n")
        self.start_time = QDateTime.currentMSecsSinceEpoch()


class PlotUpdater(QObject):
    update_plot_signal = pyqtSignal(list)
    update_text_signal = pyqtSignal(str)

    def __init__(self, curve_dict, text_edit):
        super().__init__()
        self.curve_dict = curve_dict
        self.text_edit = text_edit

    def update_plot(self, data_values):
        self.update_plot_signal.emit(data_values)

    def update_text(self, text):
        self.update_text_signal.emit(text)


class SpectrumAnalysisThread(QThread):
    def run(self):
        try:
            subprocess.run(['python', '*.py'], timeout=60 * 1)  # Run for 1 minute (60 seconds)
        except subprocess.TimeoutExpired:
            print("Spectrum Analysis completed.")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.run_dash_flag = 0

        self.setWindowTitle('Data Acquisition System              by sunduoze 20231231')
        self.setFixedSize(1800, 1500)

        self.setGeometry(100, 100, 800, 600)

        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)

        self.layout = QVBoxLayout(self.central_widget)
        # 禁止拖动图表,并启用抗锯齿
        pg.setConfigOptions(background='k', foreground='y', leftButtonPan=False, antialias=False, useOpenGL=True,
                            useNumba=True)

        # 创建水平布局
        self.input_layout = QHBoxLayout()

        # 创建 IP 和端口输入框
        self.ip_port_input = QLineEdit(self)
        self.ip_port_input.setText("192.168.1.1:1234")
        self.input_layout.addWidget(self.ip_port_input)

        # 创建按钮
        self.start_button = QPushButton('Start', self)
        self.stop_button = QPushButton('Stop', self)
        self.clear_chart_button = QPushButton('Clear Chart', self)
        self.clear_data_button = QPushButton('Clear Data', self)

        # 将按钮添加到布局中
        self.input_layout.addWidget(self.start_button)
        self.input_layout.addWidget(self.stop_button)
        self.input_layout.addWidget(self.clear_chart_button)
        self.input_layout.addWidget(self.clear_data_button)

        # 将输入布局添加到主布局
        self.layout.addLayout(self.input_layout)

        # 创建图表布局
        self.plot_layout = QVBoxLayout()

        self.plot_widget = pg.PlotWidget()
        self.plot_layout.addWidget(self.plot_widget)

        self.plot_widget.setXRange(-200, 2000)  # 设置y轴范围
        self.plot_widget.showGrid(x=True, y=True)

        # 创建滚动区域
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        self.text_edit = QTextEdit()
        self.scroll_area.setWidget(self.text_edit)
        self.text_edit.setFixedWidth(1760)
        self.text_edit.setFixedHeight(500)

        self.scrollBar = self.scroll_area.verticalScrollBar()

        self.scrollBar.setValue(25)

        # 将滚动区域添加到图表布局
        self.plot_layout.addWidget(self.scroll_area)

        # 将图表布局添加到主布局
        self.layout.addLayout(self.plot_layout)

        # 创建一个水平布局
        self.labels_layout = QHBoxLayout()

        # 创建两个 QLabel 控件
        self.label1 = QLabel(self)
        self.label1.setText("Label 1")
        self.labels_layout.addWidget(self.label1)
        self.original_font = self.label1.font()  # 保存 label 的原始字体
        new_font = self.label1.font()
        new_font.setBold(True)
        new_font.setPointSize(new_font.pointSize() + 6)
        self.label1.setFont(new_font)

        self.label2 = QLabel(self)
        self.label2.setText("Label 2")
        self.labels_layout.addWidget(self.label2)
        new_font = self.label2.font()
        new_font.setBold(True)
        new_font.setPointSize(new_font.pointSize() + 6)
        self.label2.setFont(new_font)

        # 将 labels_layout 添加到主布局中
        self.layout.addLayout(self.labels_layout)

        # 创建曲线图
        self.curve_dict = {}
        self.curve_data = {}
        colors = ['#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#42d4f4', '#f032e6']
        channel_name = ['TCD', 'TCD_PS', 'AUX_VOLT', 'PID', 'DID', 'PID_PS', 'VBUS', 'AUX_CURR']
        widths = [3, 3, 3, 3, 3, 3, 3, 3]

        # 添加图例
        self.plot_widget.addLegend(labelTextColor="black", offset=(2, 2), labelTextSize='12pt',
                                   brush=pg.mkBrush(color='#E8f0f0'))

        for i in range(8):
            name = channel_name[i % len(channel_name)]
            pen = pg.mkPen(cosmetic=True, color=colors[i % len(colors)], width=widths[i % len(widths)])
            self.curve_dict[i] = self.plot_widget.plot(pen=pen, name=name, labelTextSize='16pt')
            self.curve_data[i] = np.array([])

        self.data_receiver = DataReceiver('', 0)
        self.plot_updater = PlotUpdater(self.curve_dict, self.text_edit)

        self.data_receiver.data_received.connect(self.plot_updater.update_plot)
        self.plot_updater.update_plot_signal.connect(self.update_plot)
        self.plot_updater.update_text_signal.connect(self.update_text)

        self.data_thread = QThread()
        self.plot_thread = QThread()
        self.start_dash_thread = QThread()
        self.open_broswer_thread = QThread()

        self.data_receiver.moveToThread(self.data_thread)
        self.plot_updater.moveToThread(self.plot_thread)

        self.data_thread.started.connect(self.data_receiver.start_server)
        self.plot_thread.started.connect(self.start_plotting)
        self.start_dash_thread.started.connect(self.run_dash)
        self.open_broswer_thread.started.connect(self.open_broswer)

        self.start_button.clicked.connect(self.start_server)
        self.stop_button.clicked.connect(self.stop_server)
        self.stop_button.clicked.connect(self.run_dash)
        self.stop_button.clicked.connect(self.open_broswer)

        self.clear_chart_button.clicked.connect(self.clear_chart)
        self.clear_data_button.clicked.connect(self.clear_data)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start(100)  # 设置定时器,每100毫秒更新一次绘图

    def start_plotting(self):
        for i in range(8):
            self.curve_dict[i].setDownsampling(auto=True, method='peak')
            self.curve_dict[i].setClipToView(True)

    def start_server(self):
        if not self.data_thread.isRunning():
            ip, port = self.ip_port_input.text().split(':')
            self.data_receiver.host = ip
            self.data_receiver.port = int(port)
            self.data_receiver.start_csv_file()
            self.data_thread.start()
            self.start_button.setStyleSheet("background-color: green")
            self.start_button.setEnabled(False)

    def stop_server(self):
        if self.data_thread.isRunning():
            self.data_receiver.stop_server()
            self.data_thread.quit()
            self.data_thread.wait()
            self.start_button.setStyleSheet("")
            self.start_button.setEnabled(True)


    def run_dash(self):
        print("run dash")
        # Start spectrum analysis in a separate thread
        app.run_server(debug=True, threaded=True)

    def open_broswer(self):
        # 等待应用启动完成,可以根据实际情况调整等待时间
        time.sleep(5)
        print("open broswer")
        # 你的Dash应用启动的URL
        url = "http://127.0.0.1:8050/"
        webbrowser.open(url, new=2)

    def clear_chart(self):
        for i in range(8):
            self.curve_data[i] = np.array([])
            self.curve_dict[i].setData(y=self.curve_data[i])

    def clear_data(self):
        self.text_edit.clear()

    def update_plot(self, data_values=None):
        try:
            if data_values is not None:
                if not isinstance(data_values, list) or len(data_values) != 8:
                    raise ValueError("Invalid data format")

                for i, value in enumerate(data_values):
                    if i >= 8:
                        break
                    if not isinstance(value, (int, float)):
                        raise ValueError(f"Invalid data type for value {i}: {type(value)}")

                    if len(self.curve_data[i]) >= 20000:
                        self.curve_data[i] = np.roll(self.curve_data[i], -1)
                        self.curve_data[i][-1] = value
                    else:
                        self.curve_data[i] = np.append(self.curve_data[i], value)

                    self.curve_dict[i].setData(y=self.curve_data[i])
                    self.label1.setText("VOLT:" + str(data_values[2]) + "V")
                    self.label2.setText("CURR:" + str(data_values[7]) + "A")

                # 滚动显示最新40行文本数据
                current_text = self.text_edit.toPlainText()
                new_text = '\n'.join([current_text] + [', '.join(map(str, data_values))])
                self.text_edit.setPlainText('\n'.join(new_text.splitlines()[-40:]))
                # self.scroll_area.verticalScrollBar().setValue(30)
                self.scrollBar.setValue(30)
                # print(self.scrollBar.maximum())

        except Exception as e:
            print(f"Error in update_plot: {e}")

    def update_text(self, text):
        # 滚动显示最新40行文本数据
        current_text = self.text_edit.toPlainText()
        new_text = '\n'.join([current_text, text])
        self.text_edit.setPlainText('\n'.join(new_text.splitlines()[-40:]))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

 

 
五、作品功能演示视频
 

 
六、项目总结
本次比赛,对于我而言时间甚是匆匆(11/14号报名,11/30号收到DigiKey的AD7606C评估板),工作之余不到45天的时间完成了整个系统的硬件、嵌入式软件、上位机软件的设计。硬件从系统设计到器件选型到电路设计、PCB外发制作、手工贴片及焊接、系统调试。嵌入式软件的开发,尤其是AD7606C的调试也是花了不少时间,但是借助于U8g2等等开源库及项目的参考如虎添翼般的大大加速我的开发。对于上位机软件,借助于强大的ChatGPT很好的给我提供不同的实现思路,跳出选择的盲区,大大简化了整个软件部分的开发,仅仅300+行代码便可以实现以往数不胜数的代码才能实现的UI。
该项目本为多通道气相色谱采集单元,但是目的远不如此,它是可以很方便的扩展为数据采集平台(辅助的电压测量及电流测量其目的就在于此),配合上位机软件,可以为广大工程师及爱好者提供一个开放的可自由发挥的平台,可以帮助大家更容易、更轻松的实现更多的创意。项目包含的PD供电部分并未采用集成好协议的器件,而是可以通过用户自己编程实现不同的协议,比如PPS、240w私有协议快充等等,大大提高了可扩展性。嵌入式软件中也预留了蓝牙部分的线程,后续稍稍修改代码便可以方便的通过手机BLE连接来控制。
 本次比赛中嵌入式软件和上位机软件全部采用开源的工具和平台,广大朋友们后续可以非常方便的使用和开发,借助于开源平台的库也可以大幅提高开发效率。整个系统的软硬件全套设计资料会全部开放到论坛和Github,希望对大家后续的学习和创意有所帮助。
最后还是非常感谢DigiKey和EEWORLD,组织了这场盛大的比赛,为众多电子爱好者和专业人士提供平台和机会展现大家的idea,源源不断的去创新创造。
 
 
七、其他
 
github硬件设计:
链接已隐藏,如需查看请登录或者注册

github嵌入式软件源码:
链接已隐藏,如需查看请登录或者注册
github上位机软件源码:
链接已隐藏,如需查看请登录或者注册
链接已隐藏,如需查看请登录或者注册
 
digikey_iot_contest_2023_多通道微型气相色谱采集单元.doc (21.89 MB, 下载次数: 5)

最新回复

数字电位器 用了哪个芯片?  详情 回复 发表于 2024-2-1 16:32
点赞 关注
 
 

回复
举报

7047

帖子

11

TA的资源

版主

沙发
 

非常牛的一个作品,期待大佬得个好名次!

点评

非常感谢!  详情 回复 发表于 2024-1-13 13:16
 
 
 

回复

574

帖子

11

TA的资源

一粒金砂(高级)

板凳
 
lugl4313820 发表于 2024-1-12 11:16 非常牛的一个作品,期待大佬得个好名次!

非常感谢!

 
 
 

回复

162

帖子

5

TA的资源

一粒金砂(中级)

4
 

哇塞,这个东西好牛

点评

一起来diy  详情 回复 发表于 2024-1-16 22:22
 
 
 

回复

574

帖子

11

TA的资源

一粒金砂(高级)

5
 
烟花绽放 发表于 2024-1-15 09:45 哇塞,这个东西好牛

一起来diy

点评

下载下来,学习下,  详情 回复 发表于 2024-1-17 09:32
 
 
 

回复

162

帖子

5

TA的资源

一粒金砂(中级)

6
 

下载下来,学习下,

 
 
 

回复

26

帖子

12

TA的资源

一粒金砂(中级)

7
 
数字电位器 用了哪个芯片?

点评

AD5272  详情 回复 发表于 2024-2-1 22:29
 
 
 

回复

574

帖子

11

TA的资源

一粒金砂(高级)

8
 
genvex 发表于 2024-2-1 16:32 数字电位器 用了哪个芯片?

AD5272

 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/7 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表