25、传感器系统的高速采集与医疗传感应用

传感器系统的高速采集与医疗传感应用

1. 高速数据采集

1.1 高速采集需求与ESP32能力

在某些情况下,我们需要以高于每秒几百个样本的速率来观察和生成信号,例如分析RS - 232等通信通道或观察声学信号。幸运的是,ESP32能够通过直接内存访问(DMA)模式,将样本从ADC直接传输到内存,而无需使用CPU,实现每秒超过200,000个样本的采样速率。它利用了ESP32芯片上通常用于数字传输音乐的I2S(Inter - IC Sound)支持。此外,ESP还具有在相同频率范围内的正弦和矩形信号内置频率发生器。

1.2 高速模式配置代码

为了将这些功能添加到数据采集系统中,我们可以使用以下基于ESP32示例HiFreq ADC的代码片段来配置高速模式:

#include <driver/i2s.h>
bool i2s_adc_enabled=false;
void i2sInit(uint32_t i2s_sample_rate){
    i2s_config_t i2s_config = {
      .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX
      | I2S_MODE_ADC_BUILT_IN),
      .sample_rate = i2s_sample_rate,
      .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
      .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
      .communication_format = I2S_COMM_FORMAT_STAND_I2S,
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
      .dma_buf_count = 8,
      .dma_buf_len = 1024,
      // 1024 is maximum length
      .use_apll = true,
      // needed for low frequencies
      .tx_desc_auto_clear = false,
      .fixed_mclk = 0
    };
    if(ESP_OK != i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL)){
        Serial.print("Error installing I2S.");
    }
    if(ESP_OK != i2s_set_adc_mode(ADC_UNIT_1, ADC1_CHANNEL_4)){ // pin 32
        Serial.print("Error setting up ADC.");
    }
    if(ESP_OK != adc1_config_channel_atten(ADC1_CHANNEL_4, ADC_ATTEN_DB_11)){
        Serial.print("Error setting up ADC attenuation.");
    }
    if(ESP_OK != i2s_adc_enable(I2S_NUM_0)){
        Serial.print("Error enabling ADC.");
    }
    Serial.print("I2S ADC setup ok\n");
}

1.3 正弦频率发生器代码

正弦频率发生器也使用ESP32上的I2S硬件。以下是相关代码:

#include <driver/dac.h>
#include <soc/sens_reg.h>
#include "soc/rtc.h"
float dac25freq=0;
int dac25scale=0;
// output full scale
void cwDACinit(float freq, int scale, int offset) { //...cwDACinit
    if (abs(freq) < 1e-3) {dac_output_disable(DAC_CHANNEL_1); return;}
    int frequency_step=max(1.0,floor(0.5+0.95*65536.0*freq
    /RTC_FAST_CLK_FREQ_APPROX));
    SET_PERI_REG_MASK(SENS_SAR_DAC_CTRL1_REG, SENS_SW_TONE_EN);
    SET_PERI_REG_MASK(SENS_SAR_DAC_CTRL2_REG, SENS_DAC_CW_EN1_M);
    SET_PERI_REG_BITS(SENS_SAR_DAC_CTRL2_REG, SENS_DAC_INV1, 2,
    SENS_DAC_INV1_S);
    SET_PERI_REG_BITS(SENS_SAR_DAC_CTRL1_REG, SENS_SW_FSTEP,
    frequency_step, SENS_SW_FSTEP_S);
    SET_PERI_REG_BITS(SENS_SAR_DAC_CTRL2_REG, SENS_DAC_SCALE1,
    scale, SENS_DAC_SCALE1_S);
    SET_PERI_REG_BITS(SENS_SAR_DAC_CTRL2_REG, SENS_DAC_DC1,
    offset, SENS_DAC_DC1_S);
    dac_output_enable(DAC_CHANNEL_1);
}

1.4 网页功能更新

在esp32 - daq.html文件中,我们更新了 webSocketEvent() 函数,以处理JSON编码命令的响应。特别是, START 命令现在需要区分慢速或高速模式下的采集启动。我们约定,正的采样周期值定义慢速采集模式下的采样速度,而负值则通过调用 i2sInit() 初始化高速采集。

if (strstr(cmd,"START")) {
    sendMSG("INFO","ESP32: Received Start command");
    sample_period=val;
    if (sample_period > 0) {
        SampleSlow.attach_ms(sample_period,sampleslow_action);
        Serial.print("sample_period = "); Serial.println(sample_period);
    } else {
        if (i2s_adc_enabled) {
            i2s_adc_disable(I2S_NUM_0); i2s_driver_uninstall(I2S_NUM_0);}
        uint32_t i2s_sample_rate=(uint32_t) abs(sample_period);
        i2sInit(i2s_sample_rate);
        i2s_adc_enabled=true;
    }
} else if (strstr(cmd,"STOP")) {
    if (sample_period>0) {
        sendMSG("INFO","ESP32: Received Stop command");
        SampleSlow.detach();
    } else {
        if (ESP_OK != i2s_adc_disable(I2S_NUM_0)) {
            Serial.printf("Error disabling ADC.");
        }
        if (ESP_OK != i2s_driver_uninstall(I2S_NUM_0)) {
            Serial.printf("Error uninstalling I2S driver.");
        }
        i2s_adc_enabled=false;
        if (dac25freq>0) {cwDACinit(dac25freq,dac25scale,0);}
    }
} else if (strstr(cmd,"SETDAC")) {
    Serial.printf("DAC25 frequency = %d\n",val);
    if (val < 0) {dac25scale=-val-1;} else {dac25freq=val;}
    cwDACinit(dac25freq,dac25scale,0);
}

1.5 矩形信号发生器配置

我们通过在 setup() 函数的末尾添加以下代码,将矩形信号发生器配置为在引脚27上连续输出1 kHz信号:

ledcSetup(0, 1000, 2); ledcAttachPin(27, 0); ledcWrite(0, 2);

1.6 数据采集与处理

loop() 函数的末尾,我们添加代码以使用 i2s_read() 采集数据,并重新排列 buffer[] 中的样本。

if (i2s_adc_enabled) {
    size_t bytes_read;
    uint16_t buffer[1024], swap; //={0};
    i2s_read(I2S_NUM_0, &buffer, sizeof(buffer), &bytes_read, 15);
    for (int j=0; j<bytes_read/2; j+=2) {
        // swap left and right channel
        swap=buffer[j+1]; buffer[j+1]=buffer[j]; buffer[j]=swap;
    }
    doc.to<JsonObject>();
    // clear DynamicJsonDocument doc
    for (int j=0;j<bytes_read/2;j++){doc["WF0"][j]=(buffer[j]&0x0FFF)>>3;}
    serializeJson(doc,out);
    webSocket.sendTXT(websock_num,out,strlen(out));
    delay(2000);
}

1.7 浏览器控制功能

我们通过在esp32 - daq.html文件中添加代码,从浏览器控制ESP32的新功能。例如,为采样速度的 SELECT 菜单添加一个 OPTGROUP ,以向ESP32发送负值频率,用于配置高速ADC。

<OPTGROUP label="Hi - speed I2S ADC">
    <OPTION value="-1000">1 kSamples/s</OPTION>
    <OPTION value="-10000">10 kSamples/s</OPTION>
    <OPTION value="-20000">20 kSamples/s</OPTION>
    <OPTION value="-50000">50 kSamples/s</OPTION>
    <OPTION value="-100000">100 kSamples/s</OPTION>
    <OPTION value="-200000">200 kSamples/s</OPTION>
    <OPTION value="-277777">277.777 kSamples/s</OPTION>
    <OPTION value="-500000">500 kSamples/s</OPTION>
    <OPTION value="-1000000">1 MSamples/s</OPTION>
</OPTGROUP>
</SELECT>

同时,我们使用另一个 SELECT 菜单控制正弦频率发生器。

<SELECT onchange="setDacFrequency(this.value);">
    <OPTGROUP label="DAC frequency">
        <OPTION value="0">DAC25 OFF</OPTION>
        <OPTION value="2000">DAC25 = 2 kHz</OPTION>
        <OPTION value="5000">DAC25 = 5 kHz</OPTION>
        <OPTION value="10000">DAC25 = 10 kHz</OPTION>
        <OPTION value="20000">DAC25 = 20 kHz</OPTION>
        <OPTION value="50000">DAC25 = 50 kHz</OPTION>
        <OPTION value="100000">DAC25 = 100 kHz</OPTION>
        <OPTION value="200000">DAC25 = 200 kHz</OPTION>
    </OPTGROUP>
    <OPTGROUP label="DAC output scale">
        <OPTION value="-1">1/1 scale</OPTION>
        <OPTION value="-2">˜1/2 scale</OPTION>
        <OPTION value="-3">˜1/4 scale</OPTION>
        <OPTION value="-4">˜1/6 scale</OPTION>
    </OPTGROUP>
</SELECT>

1.8 系统测试

我们通过在引脚25上生成2 kHz正弦信号,以200 kS/s的速率对其进行采样,并在图中显示波形,验证了系统的功能。同时,我们还可以使用该系统分析TSOP2238红外接收器的信号。

1.9 项目思路

  • 添加功能以在下降或上升沿触发采集。
  • 修改代码以从引脚33读取快速ADC。
  • 添加菜单以选择引脚27上矩形信号发生器的频率和脉冲宽度。
  • 加倍提供给 i2s_read() 函数的 buffer[] 数组的大小,并记录更长的波形。
  • 添加放大器以测量并显示麦克风的小电压。
  • 讨论在DAC输出端添加作为线路缓冲器的运算放大器,在ADC输入端添加跨阻放大器,以测量组件的阻抗。

2. 医疗传感应用

2.1 医疗传感系统概述

我们使用NodeMCU测量心电图(ECG)、血液中的氧饱和度水平和生物阻抗,并使用Web界面显示测量结果。此外,我们还添加了电路来模拟人体的阻抗和心电图,并提供将测量值保存到NodeMCU内部闪存磁盘并稍后重放的选项。

2.2 硬件组成

  • AD8232心电图传感器 :通过电缆连接三个电极,将测量的小电压进行放大和清理,并将模拟电压输出到NodeMCU的模拟输入引脚A0。
  • MAX30102脉冲血氧仪 :通过比较手指尖端的红色和红外光反射来测量血氧饱和度(SPO2),通过I2C线连接到NodeMCU的引脚D1和D2。
  • AD5933生物阻抗网络分析仪 :用于测量连接到模拟人体阻抗的电路(Cole模型)的两个电极之间的阻抗,通过I2C线与NodeMCU接口。

2.3 代码实现

2.3.1 初始化部分
// MediSense, V. Ziemann, 221106
const char* ssid = "messnetz";
const char* password = "zxcvZXCV";
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <WebSocketsServer.h>
#include <ArduinoJson.h>
#include <FS.h>
#include <SPI.h>
ESP8266WebServer server2(80);
// port 80
WebSocketsServer webSocket = WebSocketsServer(81);
// port 81
#include <Ticker.h>
Ticker SampleSlow,ECGticker;
#include <DFRobot_MAX30102.h>
DFRobot_MAX30102 Sensor;
#include "I2Crw.h"
#include "AD5933.h"
int16_t Ival[512],Qval[512]; float mag50=1, phase50=0;
volatile uint8_t websock_num=0,info_available=0,output_ready=0;
int mmode=0,sample_period=100,samples[3],icounter=0,recording=0;
int ringbuf[50],ibuf=0,ped=0;
// pedestal subtraction for oximeter
char info_buffer[80];
// for websock messages
char out[300];
// space for websocket messages
DynamicJsonDocument doc(300);
float Rcal=10.0;
// calibration resistor
uint16_t ecgtrace[200],ecg_counter=0;;
#define CS D8
// chip select for SPI - DAC
2.3.2 回调函数
  • cardio_action() :读取与AD8232相关的IO端口信号,并将其放入 samples[] 数组。
void cardio_action() {
    samples[0]=analogRead(0)/2;
    // adjust scale
    samples[1]=100*digitalRead(D3)+50*digitalRead(D4);
    samples[2]=0;
    output_ready=1;
}
  • oximeter_action() :读取MAX30102的信号,确定原始信号的基座,并将减去基座并适当缩放的信号放入 samples[] 数组。
void oximeter_action() {
    float ir=0;
    if (mmode==3) {ir=(float)Sensor.getIR();
    } else if (mmode==4) {ir=(float)Sensor.getRed();}
    ringbuf[ibuf]=ir; ibuf++; if (ibuf>=50) {ibuf=0;}
    ped=ringbuf[0]; for (int k=1;k<50;k++) {ped=min(ped,ringbuf[k]);}
    samples[0]=100+(ir-ped)/4.0;
    // adjust scale
    icounter=icounter+1;
    if (icounter==200){samples[1]=30000; icounter=0;} else {samples[1]=0;}
    samples[2]=0;
    output_ready=1;
}
  • impedance_action() :编程AD5933产生50 kHz信号,读取I和Q信号,将其转换为幅度和相位,并将适当缩放的值放入 samples[] 数组。
void impedance_action() {
    AD5933_sweep2(50e3,0.01,0,mode,pga,Ival,Qval);
    float mag=sqrt(Ival[0]*Ival[0]+Qval[0]*Qval[0]);
    float phase=atan2(Qval[0],Ival[0])*180/3.1415926;
    samples[0]=Rcal*(mag50/mag-1.0)*512.0;
    // samples[0]=128+128*log10(mag50/mag); // log scale
    samples[1]=256+phase-phase50;
    samples[2]=0;
    output_ready=1;
}
2.3.3 其他函数
  • set_dac() :将值传递给MCP4921 DAC。
void set_dac(uint16_t val) {
    val|=(B0011 << 12);
    digitalWrite(CS,LOW);
    SPI.transfer(highByte(val));
    SPI.transfer(lowByte(val));
    digitalWrite(CS,HIGH);
}
  • ecg_action() :不断更新DAC,使用存储在 ecgtrace[] 数组中的心电图轨迹值。
void ecg_action() {
    set_dac(ecgtrace[ecg_counter]);
    ecg_counter++;
    if (ecg_counter==200) {ecg_counter=0;}
}
  • send_samples() :将 samples[] 数组格式化为JSON文档并通过WebSocket接口发送到网页显示。
void send_samples(int samples[]) {
    for (int k=0;k<3;k++) {doc["ADC"][k]=samples[k];}
    serializeJson(doc,out); webSocket.sendTXT(websock_num,out,strlen(out));
}
  • sendMSG() :发送其他WebSocket消息。
void sendMSG(char *nam, const char *msg) {
    (void) sprintf(info_buffer,"{\"%s\":\"%s\"}",nam,msg);
    if (strstr(msg,"!")==msg) {
        webSocket.sendTXT(websock_num,info_buffer,strlen(info_buffer));
    } else {
        info_available=1;
    }
}
  • webSocketEvent() :处理WebSocket消息,根据 mmode 变量执行不同的操作。
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload,
size_t length) {
    Serial.printf("webSocketEvent(%d, %d, ...)\r\n", num, type);
    websock_num=num;
    switch(type) {
        case WStype_DISCONNECTED:
            Serial.printf("[%u] Disconnected!\r\n", num);
            break;
        case WStype_CONNECTED:
            {
                IPAddress ip = webSocket.remoteIP(num);
                Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\r\n",
                num, ip[0], ip[1], ip[2], ip[3], payload);
            }
            sendMSG("INFO","ESP: Successfully connected");
            break;
        case WStype_TEXT:
            {
                Serial.printf("[%u] get Text: %s\r\n", num, payload);
                DynamicJsonDocument root(300);
                //........parse JSON
                deserializeJson(root,payload);
                const char *cmd = root["cmd"];
                const int val = root["val"];
                if (strstr(cmd,"START")) {
                    sendMSG("INFO","ESP: Received Start command");
                    sample_period=val;
                    Serial.print("sample_period = "); Serial.println(sample_period);
                    if (mmode==0) {
                        SampleSlow.attach_ms(sample_period,cardio_action);
                    } else if (mmode==3 || mmode==4) {
                        sample_period=max(50,sample_period);
                        SampleSlow.attach_ms(sample_period,oximeter_action);
                    } else if (mmode==8) {
                        SampleSlow.attach_ms(sample_period,impedance_action);
                    }
                } else if (strstr(cmd,"STOP")) {
                    sendMSG("INFO","ESP: Received Stop command");
                    SampleSlow.detach();
                } else if (strstr(cmd,"MMODE")) {
                    mmode=val;
                    if (mmode==3 & mmode==4) {
                        if (!Sensor.begin()){sendMSG("INFO","ESP: No MAX30102");mmode=-1;
                        } else {
                            Sensor.sensorConfiguration(60,SAMPLEAVG_8,MODE_MULTILED,
                            SAMPLERATE_400,PULSEWIDTH_411,ADCRANGE_16384);
                        }
                    } else if (mmode==5) {
                        if (!Sensor.begin()){sendMSG("INFO","ESP: No MAX30102");mmode=-1;
                        } else {
                            sendMSG("INFO","ESP: MAX30102 is up and running");
                            Sensor.sensorConfiguration(50,SAMPLEAVG_4,MODE_MULTILED,
                            SAMPLERATE_100,PULSEWIDTH_411,ADCRANGE_16384);
                        }
                    } else if (mmode==200) {
                        recording=1; sendMSG("INFO","ESP: Recording ON");
                    } else if (mmode==201) {
                        recording=0; sendMSG("INFO","ESP: Recording OFF");
                    } else if (mmode==300) { // Start ECG output
                        ECGticker.attach_ms(5,ecg_action);
                        sendMSG("INFO","ESP: Starting ECG output");
                    } else if (mmode==301) { // Stop ECG output
                        ECGticker.detach();
                        set_dac(0);
                        sendMSG("INFO","ESP: Stopping ECG output");
                    }
                } else {
                    Serial.println("Unknown command");
                    sendMSG("INFO","ESP: Unknown command received");
                }
            }
    }
}
2.3.4 setup()函数

初始化IO引脚,连接到WLAN,启动WebSocket服务器,初始化SPIFFS文件系统,初始化MAX30102和AD5933的频率范围,并加载校准值和心电图轨迹。

void setup() {
    pinMode(D3,INPUT);
    pinMode(D4,INPUT);
    pinMode(D0,OUTPUT);
    digitalWrite(D0,HIGH); // LED on NodeMCU
    Serial.begin(115200);
    delay(1000);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {delay(500); Serial.print(".");}
    Serial.print("\nConnected to ");
    Serial.print(ssid);
    Serial.print(" with IP address: "); Serial.println(WiFi.localIP());
    webSocket.begin();
    webSocket.onEvent(webSocketEvent);
    if (!SPIFFS.begin()) {
        Serial.println("ERROR: no SPIFFS filesystem found");
    } else {
        server2.begin();
        server2.serveStatic("/", SPIFFS, "/medisense.html");
        server2.onNotFound(handle_notfound);
        Serial.print("SPIFFS file system OK and server started on port 80");
        listdir();
    }
    while (!Sensor.begin()) {Serial.println("No MAX30102"); delay(1000);}
    Sensor.sensorConfiguration(60,SAMPLEAVG_8,MODE_MULTILED,SAMPLERATE_200,
    PULSEWIDTH_411,ADCRANGE_16384);
    freq=1e4; finc=200; npts=511;
    // AD5933 frequency range
    File f=SPIFFS.open("/calib50kHz.dat","r");
    // 50kHz calibration data
    if (f) {
        float re,im; char line [80];
        f.readStringUntil(’\n’).toCharArray(line,80);
        sscanf(line," %g %g %g %g",&re,&im,&mag50,&phase50);
        f.close();
    }
    f=SPIFFS.open("/ecg.dat","r");
    if (f) {
        uint16_t v; char line[20];
        for (int k=0;k<200;k++) {
            f.readStringUntil(’\n’).toCharArray(line,80);
            sscanf(line,"%d",&v);// Serial.println(v);
            ecgtrace[k]=v;
        }
        f.close();
    }
    pinMode(CS,OUTPUT); digitalWrite(CS,HIGH);
    // init SPI - CS
    SPI.begin(); SPI.setBitOrder(MSBFIRST);
}
2.3.5 loop()函数

处理HTTP服务器和WebSocket请求,根据 info_available output_ready 变量发送消息和数据,根据 mmode 变量执行不同的操作。

void loop() {
    server2.handleClient();
    // http server
    webSocket.loop();
    // websocket
    if (info_available==1) {
        info_available=0;
        webSocket.sendTXT(websock_num,info_buffer,strlen(info_buffer));
    }
    if (output_ready==1) {
        output_ready=0;
        if (recording==0) {
            send_samples(samples);
        } else if (recording==1) {
            // record data on local file system
            File f=SPIFFS.open("/data.txt","a");
            // open for append
            if (!f) {
                Serial.println("Unable To Open file");
            } else {
                f.print(samples[0]); f.print("\t"); f.println(samples[1]);
                f.close();
            }
            sendMSG("INFO","ESP: Recording...");
        }
    }
    if (mmode==5) {
        // SPO2 rate displayed on status line
        char msg[50];
        int32_t SPO2,HR;
        // values
        int8_t SPO2_OK,HR_OK;
        // data OK?
        Sensor.heartrateAndOxygenSaturation(&SPO2,&SPO2_OK,&HR,&HR_OK);
        sprintf(msg,"!ESP: SPO2,HR = %d, %d (%d,%d)",SPO2,HR,SPO2_OK,HR_OK);
        sendMSG("INFO",msg);
    } else if (mmode==6) {
        // AD5933 calibrate
        digitalWrite(D0,LOW);
        //turns LED on
        sendMSG("INFO","!ESP calibrating...");
        AD5933_sweep(freq,finc,npts,mode,pga,Ival,Qval);
        sendMSG("INFO","ESP: AD5933 Calibration complete");
        digitalWrite(D0,HIGH);
        //turns LED off
        File f=SPIFFS.open("/calib.dat","w");
        for (int k=0;k<npts+1;k++) {
            f.print(Ival[k]); f.print("\t"); f.print(Qval[k]);
            f.print("\t"); f.print(sqrt(Ival[k]*Ival[k]+Qval[k]*Qval[k]));
            f.print("\t"); f.println(atan2(Qval[k],Ival[k])*180/3.1415926);
        }
        f.close();
        AD5933_sweep(50e3,0.01,0,mode,pga,Ival,Qval); // calibrate at 50 kHz
        f=SPIFFS.open("/calib50kHz.dat","w");
        f.print(Ival[0]); f.print("\t"); f.print(Qval[0]);
        f.print("\t"); f.print(sqrt(Ival[0]*Ival[0]+Qval[0]*Qval[0]));
        f.print("\t"); f.println(atan2(Qval[0],Ival[0])*180/3.1415926);
        f.close();
        mmode=-1;
    } else if (mmode==7) {
        // AD5933 frequency sweep
        char line[80];
        float re,im,ab,ph;
        sendMSG("INFO","!ESP sweeping...");
        digitalWrite(D0,LOW);
        //turns LED on
        AD5933_sweep(freq,finc,npts,mode,pga,Ival,Qval);
        digitalWrite(D0,HIGH);
        //turns LED on
        sendMSG("INFO","!ESP: AD5933 Sweep complete");
        File f=SPIFFS.open("/calib.dat","r");
        for (int k=0;k<npts+1;k++) {
            float mag=sqrt(Ival[k]*Ival[k]+Qval[k]*Qval[k]);
            float phase=atan2(Qval[k],Ival[k])*180/3.1415926;
            f.readStringUntil(’\n’).toCharArray(line,80);
            sscanf(line," %g %g %g %g",&re,&im,&ab,&ph);
            samples[0]=Rcal*(ab/mag-1.0)*512.0;
            // samples[0]=128+128*log10(ab/mag);
            samples[1]=256+25.6*(phase-ph);
            samples[2]=0;
            send_samples(samples); send_samples(samples);
            Serial.print(ab/mag); Serial.print("\t"); Serial.print(phase-ph);
            Serial.print("\t"); Serial.println(samples[0]);
        }
        f.close();
        mmode=-1;
    } else if (mmode==100) {
        // Directory listing
        mmode=-1;
        listdir();
    } else if (mmode==101) {
        // Remove data file
        mmode=-1;
        SPIFFS.remove("/data.txt");
        sendMSG("INFO","ESP: Removing file /data.txt");
    } else if (mmode==202) {
        // replay data from local file
        Serial.println("Replaying data");
        File f=SPIFFS.open("/data.txt","r");
        // open for read
        if (!f) {
            Serial.println("Cannot open file /data.txt");
            sendMSG("INFO","ESP: cannot open file /data.txt");
        } else {
            Serial.print("File size = "); Serial.println(f.size());
            char msg[80]; sprintf(msg,"ESP: File size of /data.txt = %d",f.size());
            sendMSG("INFO",msg);
            char line[80];
            samples[2]=0;
            while (f.position()<f.size()) {
                f.readStringUntil(’\n’).toCharArray(line,80);
                sscanf(line," %d %d",&samples[0],&samples[1]);
                send_samples(samples);
            }
        }
        f.close();
        mmode=-1;
    }
    yield();
}

2.4 浏览器控制

在medisense.html文件中,我们添加了发送JSON格式的 MMODE 消息的功能,以设置NodeMCU上的 mmode 变量。

<SELECT onchange="sendSpecialCommand(this.value);">
    <OPTION value="0">ECG live</OPTION>
    <OPTION value="3">Oximeter IR live</OPTION>
    <OPTION value="4">Oximeter Red live</OPTION>
    <OPTION value="5">Oximeter SPO2 live</OPTION>
    <OPTION value="6">AD5933 Calibrate</OPTION>
    <OPTION value="7">AD5933 Frequency sweep</OPTION>
    <OPTION value="8">AD5933 at 50kHz</OPTION>
    <OPTION value="100">List directory</OPTION>
    <OPTION value="101">Remove data file</OPTION>
    <OPTION value="200">Recording ON</OPTION>
    <OPTION value="201">Recording OFF</OPTION>
    <OPTION value="202">Replay recording</OPTION>
    <OPTION value="300">Start ECG output</OPTION>
    <OPTION value="301">Stop ECG output</OPTION>
</SELECT>
function sendSpecialCommand(v) {
    toStatus("Special Command " + v + " ");
    websock.send(JSON.stringify({ "cmd" : "MMODE", "val" : v }));
}

2.5 系统运行

将代码上传到NodeMCU,将medisense.html文件放入草图的数据子目录,并使用Arduino IDE的Tools菜单中的ESP8266 Sketch Data Upload命令将其上传到NodeMCU。系统运行后,我们可以通过浏览器访问NodeMCU的网页,选择不同的模式进行测量和记录。

2.6 项目思路

  • 了解心电图特征与心脏活动的关系。
  • 在手指和MAX30102脉冲血氧仪之间放置彩色但大部分透明的薄膜,观察其对原始信号和SPO2值的影响。
  • 使用基本电路理论分析Cole模型。
  • 了解四端阻抗测量与两端阻抗测量的差异和优势。
  • 参考相关资料,为AD5933构建更好的模拟前端。
  • 为了减少原始校准数据中的噪声,对校准数据拟合二次多项式,并在后续使用。
  • 添加MLX90614温度计,进行无接触式人体温度测量。
  • 测量香蕉两端的阻抗,并尝试不同类型的电极。
  • 确定一块木头的阻抗,并观察其在木头变湿时的变化。
  • 将软件移植到ESP32。
  • 将AD5933连接到ESP - 01控制器,使系统便携。
  • 将MAX30102和MLX90614连接到ESP - 01控制器。
  • 讨论如何将AD8232与不暴露内置ADC到外部引脚的ESP - 01控制器接口,并构建电路。

3. 高速采集与医疗传感技术点分析

3.1 高速采集技术要点

技术点 说明
I2S模式 ESP32通过I2S模式实现高速采样,直接将ADC样本传输到内存,无需CPU干预,提高采样速率。
频率发生器 正弦和矩形频率发生器利用ESP32的I2S硬件,通过直接写寄存器的方式配置信号频率和输出比例。
网页控制 在网页中通过JSON编码命令控制采集模式,区分慢速和高速采集,以及频率发生器的开关和参数设置。
数据处理 采集的数据需要进行通道交换和格式转换,以适应显示需求,并通过WebSocket发送到浏览器。

3.2 医疗传感技术要点

技术点 说明
传感器选择 选用AD8232测量心电图,MAX30102测量血氧饱和度,AD5933测量生物阻抗,各传感器通过不同接口与NodeMCU连接。
代码结构 代码分为初始化、回调函数、主函数等部分,通过Ticker定时采集数据,根据不同的 mmode 变量执行不同的操作。
文件系统 使用SPIFFS文件系统存储校准数据、心电图轨迹和测量记录,方便数据的保存和重放。
浏览器控制 在网页中添加选择菜单,通过发送JSON格式的 MMODE 消息控制NodeMCU的不同功能。

3.3 技术流程图

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B{模式选择}:::decision
    B -->|高速采集| C(初始化I2S):::process
    C --> D(采集数据):::process
    D --> E(数据处理):::process
    E --> F(发送数据到浏览器):::process
    B -->|医疗传感| G(初始化传感器):::process
    G --> H(定时采集数据):::process
    H --> I{mmode判断}:::decision
    I -->|不同模式| J(执行相应操作):::process
    J --> K(保存或发送数据):::process
    F --> L([结束]):::startend
    K --> L

4. 总结与展望

4.1 总结

本文介绍了高速数据采集和医疗传感两个方面的应用。在高速采集部分,利用ESP32的I2S模式实现了每秒超过200,000个样本的采样速率,并通过网页控制实现了慢速和高速采集的切换,以及正弦和矩形频率发生器的配置。在医疗传感部分,使用NodeMCU连接AD8232、MAX30102和AD5933传感器,测量心电图、血氧饱和度和生物阻抗,并通过网页显示测量结果,同时支持数据的保存和重放。

4.2 展望

  • 功能扩展 :可以进一步添加更多的传感器,如加速度计、陀螺仪等,实现更多的测量功能。
  • 算法优化 :对采集的数据进行更深入的分析和处理,如使用机器学习算法进行心电图的自动诊断、血氧饱和度的精确计算等。
  • 系统集成 :将高速采集和医疗传感系统集成到一个更完整的平台中,实现远程监测和数据共享。
  • 硬件升级 :考虑使用性能更强大的微控制器或开发板,提高系统的处理能力和稳定性。

4.3 项目实践建议

  • 逐步实现 :对于初学者来说,可以先从简单的功能开始实现,如高速采集或单个医疗传感器的测量,逐步熟悉系统的架构和代码。
  • 调试和测试 :在开发过程中,要进行充分的调试和测试,确保系统的稳定性和准确性。可以使用示波器、逻辑分析仪等工具辅助调试。
  • 参考资料 :参考相关的文档和示例代码,如ESP32、NodeMCU的官方文档,传感器的数据手册等,获取更多的技术支持。
  • 团队合作 :如果项目规模较大,可以考虑团队合作,分工完成不同的模块,提高开发效率。

4.4 常见问题及解决方法

问题 解决方法
高速采集数据丢失 检查I2S配置和DMA缓冲区大小,适当增加缓冲区长度,调整采样速率。
传感器数据不准确 检查传感器的连接和配置,进行校准操作,确保传感器正常工作。
网页控制无响应 检查网络连接和WebSocket通信,确保消息发送和接收正常。
文件系统操作失败 检查SPIFFS文件系统的初始化和权限,确保文件读写操作正确。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值