
1. 项目缘起从开源硬件到健康监测的跨界尝试最近在整理手头的开发板翻出了这块WizFi360-EVB-Mini。这块板子我当初买来主要是为了测试WizFi360这个Wi-Fi模块的它集成了ESP32-D0WD的核心自带天线还引出了丰富的GPIO算是个挺不错的物联网开发入门板。但说实话如果只是用它来点个灯、连个网总觉得有点大材小用也缺乏点新意。正好那段时间我对可穿戴健康设备产生了兴趣特别是心率血氧监测。市面上成熟的方案很多像MAX30102这类传感器模块价格便宜资料也全。我就琢磨着能不能用这块吃灰的WizFi360-EVB-Mini结合一个脉搏血氧传感器自己搭建一个能联网的、可以远程查看数据的简易血氧仪呢这个想法听起来有点“跨界”但仔细一想逻辑是通的传感器采集生理信号开发板上的MCUESP32负责处理数据并计算出心率BPM和血氧饱和度SpO2数值然后通过板载的Wi-Fi模块将数据发送到云端或手机App上。目标明确了技术栈也就清晰了硬件端是WizFi360-EVB-Mini 脉搏血氧传感器我选择了常见的MAX30102软件和云端部分我决定用Blynk平台。Blynk的优势在于它极大地简化了物联网设备的App开发和云端连接你几乎不需要写任何手机端的代码就能通过拖拽组件的方式创建一个功能完整的监控仪表盘。这对于快速验证想法、构建原型来说效率非常高。所以这个项目的全貌就是利用WizFi360-EVB-Mini开发板驱动MAX30102传感器实时测量使用者的心率和血氧饱和度并通过Wi-Fi将数据上传至Blynk物联网平台最终在手机App上以图表和数值的形式直观展示。下面我就把从硬件连接到算法调试再到Blynk集成的完整过程以及其中踩过的坑和总结的经验毫无保留地分享出来。2. 核心硬件选型与连接为什么是它们工欲善其事必先利其器。硬件是项目的基石选型和连接方式直接决定了后续开发的难易度和最终效果的稳定性。2.1 主控板WizFi360-EVB-Mini的独特价值为什么选择WizFi360-EVB-Mini而不是更常见的NodeMCU或ESP32开发板这背后有几个实际的考量。首先集成度与可靠性。WizFi360-EVB-Mini的核心是WizFi360模块它本身就是一个基于ESP32-D0WD的SiP系统级封装方案。这意味着Wi-Fi射频电路、天线匹配、晶振等都已经由原厂优化并集成在模块内部了。对于射频设计经验不多的开发者比如我来说使用这种模块能极大避免因天线设计不当导致的信号弱、连接不稳定等问题。板子将模块的引脚引出并集成了USB转串口芯片CH340、复位按键、Boot按键和用户按键到手即用非常方便。其次引脚布局与电源。这块板子的GPIO引脚以2.54mm排针形式引出兼容面包板。更重要的是它提供了一个稳定的3.3V输出引脚而MAX30102传感器的工作电压正好是3.3V。这就省去了额外的电平转换或稳压电路直接用杜邦线连接即可简化了硬件连接。最后开发环境统一。虽然板载的是WizFi360模块但其核心是ESP32因此完全兼容Arduino IDE下的ESP32开发框架。你可以像使用任何一款ESP32开发板一样对它进行编程无需学习新的SDK生态资源丰富。2.2 传感器MAX30102的工作原理与局限心率血氧监测的核心是光电传感器。MAX30102是一款集成了红光660nm和红外光880nmLED、光电探测器、环境光抑制电路以及数字处理器的芯片。它的工作原理基于光电容积脉搏波描记法PPG。简单来说当光线照射到皮肤通常是指尖或耳垂时一部分会被组织吸收另一部分会反射回来。血液中的血红蛋白对红光和红外光的吸收率不同并且随着心脏的搏动血管的容积会发生周期性变化从而导致反射光强度也发生周期性变化。MAX30102通过交替点亮红光和红外光LED并测量对应的反射光强度得到两路PPG信号。通过分析这两路信号的交流分量AC对应脉搏波动和直流分量DC对应组织、骨骼等静态吸收运用特定的算法就能估算出心率和血氧饱和度。选择MAX30102的原因很直接它是最成熟、最易得的开源方案。有大量现成的Arduino库支持降低了算法入门门槛。但它也有明显的局限对测量姿势和外部光线非常敏感。手指必须紧贴传感器不能有晃动环境光尤其是日光的泄漏会严重干扰信号。因此它更适合做原理验证和静态测量与商业级指夹式血氧仪的精度和抗干扰能力有差距。在项目开始前必须对这个预期有清醒的认识。2.3 硬件连接图与要点连接非常简单只需要4根线电源、地、I2C时钟、I2C数据。以下是具体的连接方式WizFi360-EVB-Mini 引脚MAX30102 模块引脚说明3.3VVIN供电务必接3.3V接5V会损坏传感器GNDGND共地。GPIO 22SCLI2C时钟线。ESP32的默认I2C引脚可以是多个这里选择22。GPIO 21SDAI2C数据线。ESP32的默认I2C引脚选择21。注意有些MAX30102模块上可能还有INT中断引脚和SCK/SDI兼容SPI引脚。对于基础的心率血氧读取我们只使用I2C接口因此只需连接上述4根线。INT引脚可以不接。连接完成后硬件部分就准备好了。建议先将传感器模块用胶带或橡皮筋临时固定在指尖确保发光面和接收面正对皮肤并尽可能遮蔽环境光。一个简易的遮光罩比如用黑色电工胶带缠绕能显著提升信号质量。3. 开发环境搭建与基础代码调试硬件连好了接下来就是让它们“活”起来。这一步的关键是建立正确的开发环境并验证传感器是否能被正常驱动。3.1 Arduino IDE环境配置虽然WizFi360有自家的开发套件但为了最大化利用ESP32的生态我选择在Arduino IDE中进行开发。具体步骤如下安装ESP32开发板支持打开Arduino IDE依次点击“文件” - “首选项”在“附加开发板管理器网址”中输入https://espressif.github.io/arduino-esp32/package_esp32_index.json。然后打开“工具” - “开发板” - “开发板管理器”搜索“esp32”安装由Espressif Systems提供的版本。选择正确的开发板安装完成后在“工具” - “开发板”中选择“ESP32 Arduino”。由于WizFi360-EVB-Mini没有直接对应的板型我们需要选择一个引脚定义相近的通用板。通常选择“ESP32 Dev Module”即可。关键是要在后续的代码中正确定义我们实际使用的I2C引脚GPIO21和GPIO22。安装必要的库我们需要两个核心库。MAX30102库用于驱动传感器。在“项目” - “加载库” - “管理库”中搜索“MAX30105”。这里有个小坑最流行的库是“SparkFun MAX3010x Pulse and Proximity Sensor Library”但它主要针对MAX30105。MAX30102与其引脚兼容寄存器也高度相似这个库通常也能用于MAX30102并且包含了心率血氧算法。因此我们安装这个“SparkFun MAX3010x”库。Blynk库用于连接Blynk云。同样在库管理中搜索并安装“Blynk”。3.2 传感器基础功能测试在集成复杂算法和网络功能前必须先确保传感器硬件和I2C通信是正常的。我写了一个最简单的测试脚本目的是读取传感器的原始红光和红外光数据并通过串口打印出来。#include Wire.h #include “SparkFun_MAX3010x.h” // 安装的库 MAX30105 particleSensor; void setup() { Serial.begin(115200); Serial.println(“MAX30102 基础测试”); // 初始化I2C指定SDA21, SCL22 Wire.begin(21, 22); // 初始化传感器 if (particleSensor.begin(Wire, I2C_SPEED_FAST) false) { Serial.println(“MAX30102未找到。检查连接/电源。”); while (1); } // 配置传感器采样率、脉冲宽度、LED亮度 // 这些参数影响功耗和数据质量需要根据实际情况调整 byte ledBrightness 0x1F; // 选项0关 0xFF最大 byte sampleAverage 4; // 采样平均1 2 4 8 16 32 byte ledMode 2; // 模式1仅红光 2红光红外 3红光红外绿光 int sampleRate 400; // 采样率(Hz)50 100 200 400 800 1000 1600 3200 int pulseWidth 411; // 脉冲宽度(微秒)69 118 215 411 int adcRange 4096; // ADC范围2048 4096 8192 16384 particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange); } void loop() { // 检查是否有新的传感器数据可用 if (particleSensor.available()) { // 读取原始值 long redValue particleSensor.getRed(); long irValue particleSensor.getIR(); // 通过串口输出 Serial.print(“R”); Serial.print(redValue); Serial.print(“ IR”); Serial.println(irValue); // 丢弃已处理的数据准备下一次读取 particleSensor.nextSample(); } }将代码上传到开发板打开串口监视器波特率115200把手指轻轻放在传感器上。你应该能看到R和IR的数值开始变化。当手指按压时数值会显著下降因为更多光被血液吸收当手指离开时数值会回升。如果能看到这种规律性的变化恭喜你硬件连接和基础驱动成功了如果一直显示0或者固定值请检查电源必须是3.3V、I2C连线SDA/SCL是否接反以及库的兼容性。4. 心率与血氧算法解析从原始数据到生命体征拿到了原始的光信号数据这只是万里长征第一步。如何从这一串看似杂乱无章的数字中提取出规律的心跳节律和血氧百分比才是真正的挑战。SparkFun的库自带了一个算法示例heartRate和spo2计算但直接使用往往效果不佳。我们需要理解其原理并进行针对性的调优。4.1 心率BPM检测的核心寻找PPG信号的峰值心率算法的目标是计算每分钟心跳次数BPM。在PPG信号中每一次心跳对应着一个波形峰值或谷值。因此核心就是实时、准确地检测出这些峰值并计算峰值之间的时间间隔。库中常用的是一种基于“阈值”和“变化率”的简单峰值检测算法其流程大致如下信号预处理原始信号包含高频噪声如电源噪声、运动伪影和低频基线漂移。通常先进行带通滤波例如0.5Hz - 5Hz只保留与心率相关的频率成分。库函数内部可能已经做了简单的平均或滤波。寻找波峰遍历滤波后的数据点如果一个数据点同时满足以下条件则被标记为潜在的波峰它的值高于其前后若干个数据点的值局部最大值。它的值超过一个动态调整的阈值。计算心率记录下两个有效波峰之间的时间差peakInterval单位毫秒。心率BPM 60000 / peakInterval。为了结果稳定通常会计算最近几次心跳间隔的平均值。实操中的关键调参点采样率sampleRate我设置为400Hz这对于心率检测绰绰有余。更高的采样率能捕捉更细致的波形但数据量也更大。脉冲宽度pulseWidth和LED亮度ledBrightness这两者共同决定了入射光的强度和传感器的信噪比。光线太弱信号幅度小不易检测光线太强可能导致信号饱和ADC值达到上限且耗电增加。需要通过实验找到一个平衡点使手指放置时红外信号的AC波动幅度在几百到几千个ADC计数之间为宜。阈值调整算法内部的阈值如果不能自适应信号强度在信号弱时可能漏检信号强时可能误检。有时需要根据信号的平均值动态调整阈值。4.2 血氧饱和度SpO2计算红光与红外光的比率血氧饱和度的计算原理基于一个关键的观察氧合血红蛋白HbO2和还原血红蛋白Hb对红光和红外光的吸收率不同。具体来说HbO2对红外光吸收更多而对红光吸收较少Hb则相反。在脉搏波动的过程中动脉血液容积周期性变化引起了光吸收量的周期性变化。我们分别计算红光和红外光PPG信号的交流分量AC和直流分量DC。血氧饱和度与这两个比值的函数关系近似为R (Red_AC / Red_DC) / (IR_AC / IR_DC)SpO2 A - B * R其中A和B是经验常数需要通过校准确定库中的spo2计算函数就是基于这个原理。它会在检测到心率的同时计算最近一段时间窗口内红光和红外光信号的AC/DC比值然后套用公式得出SpO2值。影响SpO2精度的主要因素信号质量这是最重要的前提。如果PPG信号本身噪声大、波形畸形计算出的AC/DC比值就不可靠。因此确保良好的测量姿势和遮光是基础。常数A和B的校准库中使用的A、B常数通常是-45.06和30.354是针对特定传感器和光学路径的典型值。不同批次的传感器、不同的测量部位指尖、耳垂、甚至不同肤色的人都需要不同的校准常数。这就是为什么自制设备的SpO2读数往往只能作为趋势参考而不能作为医疗依据。要进行粗略校准可以找一个已知准确的医用血氧仪进行同步测量通过线性回归来拟合自己的A、B值。运动伪影手指的微小晃动会被传感器误认为是脉搏信号严重干扰计算结果。算法中需要加入运动检测和信号质量评估在运动时暂停或提示测量无效。在我的项目中我直接使用了库函数提供的getHeartRate()和getSpO2()但会同时输出一个“信心指数”——比如心率值是否在合理的生理范围内40-180 BPM最近几次读数是否稳定。如果波动太大则视为无效数据不发送给Blynk。5. Blynk平台集成构建手机端监控仪表盘当算法能够在串口输出相对稳定的BPM和SpO2数值后下一步就是让数据“上网”在手机App上可视化。Blynk平台完美地解决了这个问题。5.1 Blynk项目创建与设备认证创建Blynk账户与项目在Blynk官网注册或使用手机App注册。在App内点击“New Project”给项目起名如“Pulse Oximeter”选择硬件设备为“ESP32”连接类型为“Wi-Fi”。创建成功后你会收到一封包含Auth Token的邮件。这个Token是你的设备连接Blynk云的唯一凭证至关重要需要写入代码中。设计手机端界面在Blynk App的项目编辑界面你可以从右侧控件箱拖拽组件。对于这个项目我添加了两个Value Display控件分别用于显示实时心率和血氧数值。可以设置数据流Data Stream为虚拟引脚例如V1对应心率V2对应血氧并定义数值范围、单位等。两个Chart控件一个用于显示心率随时间变化的曲线图另一个显示血氧曲线。同样需要绑定到对应的虚拟引脚。一个Super Chart控件可选可以同时在一个图表中绘制心率和血氧两条曲线方便对比观察。一个Labeled Value控件用于显示设备状态如Wi-Fi连接状态、信号强度等。一个Button控件用于手动触发一次测量或重置数据。设计界面非常直观就像搭积木一样。关键在于为每个需要接收数据的控件正确设置其关联的“虚拟引脚”V0, V1, V2...。这些虚拟引脚号将在Arduino代码中用到。5.2 Arduino代码与Blynk的融合现在我们需要将之前的传感器读取代码与Blynk的通信逻辑整合起来。核心思路是在主循环中持续读取传感器数据并计算生命体征然后以固定的时间间隔例如每2秒或当数据有效更新时调用Blynk的接口将数据发送到对应的虚拟引脚。#define BLYNK_PRINT Serial // 在串口打印Blynk调试信息 #include WiFi.h #include WiFiClient.h #include BlynkSimpleEsp32.h #include Wire.h #include “SparkFun_MAX3010x.h” // 你的Blynk Auth Token char auth[] “YourAuthTokenHere”; // 你的Wi-Fi凭证 char ssid[] “YourWiFiSSID”; char pass[] “YourWiFiPassword”; MAX30105 particleSensor; // 定义心率、血氧变量 float heartRate 0; int spo2 0; long lastBeatTime 0; // 用于计算心率 bool fingerDetected false; // Blynk虚拟引脚定义 #define BLYNK_HR_PIN V1 #define BLYNK_SPO2_PIN V2 #define BLYNK_STATUS_PIN V3 void setup() { Serial.begin(115200); Blynk.begin(auth, ssid, pass); // 连接Wi-Fi和Blynk Wire.begin(21, 22); // 初始化I2C if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) { Serial.println(“传感器初始化失败”); while (1); } // 配置传感器参数同前 particleSensor.setup(0x1F, 4, 2, 400, 411, 4096); particleSensor.enableDIETEMPRDY(); // 启用温度传感器可选用于补偿 } void loop() { Blynk.run(); // 必须持续运行处理Blynk通信和事件 // 传感器数据读取与处理逻辑 if (particleSensor.available()) { long redValue particleSensor.getRed(); long irValue particleSensor.getIR(); // 简单的指尖检测红外值低于某个阈值认为有手指放置 if (irValue 50000 irValue 1000) { fingerDetected true; // 调用库函数更新心率和血氧计算 // 注意这些函数内部会维护状态需要持续调用 heartRate particleSensor.getHeartRate(); spo2 particleSensor.getSpO2(); // 数据有效性检查 if (heartRate 40 heartRate 180) { Blynk.virtualWrite(BLYNK_HR_PIN, heartRate); // 发送心率到V1 } if (spo2 70 spo2 100) { Blynk.virtualWrite(BLYNK_SPO2_PIN, spo2); // 发送血氧到V2 } // 在串口打印调试信息 Serial.print(“HR: “); Serial.print(heartRate); Serial.print(“ | SpO2: “); Serial.println(spo2); } else { fingerDetected false; // 手指离开可以发送默认值或提示信息 Blynk.virtualWrite(BLYNK_STATUS_PIN, “No Finger”); } particleSensor.nextSample(); } // 每隔一段时间发送一次设备状态非阻塞方式 static unsigned long lastStatusSend 0; if (millis() - lastStatusSend 10000) { // 每10秒发送一次 lastStatusSend millis(); String status fingerDetected ? “Measuring” : “Ready”; status “ | RSSI:” String(WiFi.RSSI()); Blynk.virtualWrite(BLYNK_STATUS_PIN, status); } }这段代码是一个高度简化的框架。在实际项目中你需要处理更多细节比如心率/血氧算法的稳定输出、去抖动处理、网络断开重连、低功耗考虑如果是电池供电等。5.3 数据可视化与远程监控代码上传并运行后如果一切正常WizFi360-EVB-Mini会连接到你的Wi-Fi并上线Blynk云。打开手机上的Blynk App进入你创建的项目就能看到实时跳动的心率和血氧数值以及绘制的历史曲线图。Blynk的高级玩法通知报警你可以设置“通知Notification”控件。当心率超过某个阈值如120 BPM或血氧低于某个阈值如95%时Blynk服务器会自动向你的手机推送报警通知。数据导出Blynk可以将虚拟引脚的历史数据以CSV格式导出方便你进行更深入的分析。多设备共享你可以将项目分享给家人让他们在自己的Blynk App上也能看到你的实时数据需注意隐私。至此一个基于WizFi360-EVB-Mini和Blynk的联网脉搏血氧仪原型就基本完成了。它实现了从信号采集、数据处理、无线传输到手机可视化的完整链路。6. 实测中的挑战与优化策略项目做出来和项目能用、好用之间往往隔着无数个需要填平的坑。在实测这个血氧仪原型的过程中我遇到了几个典型问题并摸索出一些优化策略。6.1 信号稳定性与抗干扰处理最初版本在手指静止时读数还算稳定但稍有移动数据就会剧烈跳动甚至失效。这主要是运动伪影和环境光干扰造成的。优化措施机械固定与遮光这是成本最低且最有效的改进。我3D打印了一个小指套将MAX30102模块嵌入其中确保传感器与指尖贴合紧密且完全遮光。你也可以用黑色热缩管或不透光的胶带包裹传感器和指尖接触部分。软件滤波在算法层面增加滤波强度。除了传感器库自带的平均我在Arduino代码中对最终计算出的heartRate和spo2值进行了滑动平均滤波。例如维护一个长度为5的数组存储最近5次的有效读数输出其平均值。这能平滑掉一些随机跳动。#define FILTER_SIZE 5 float hrBuffer[FILTER_SIZE]; int hrIndex 0; float getFilteredHR(float newHR) { hrBuffer[hrIndex] newHR; hrIndex (hrIndex 1) % FILTER_SIZE; float sum 0; int count 0; for (int i 0; i FILTER_SIZE; i) { if (hrBuffer[i] 0) { // 忽略无效值 sum hrBuffer[i]; count; } } return (count 0) ? (sum / count) : 0; }信号质量评估在发送数据到Blynk之前增加一个“信号质量”判断。例如计算最近一段时间内红外信号AC分量的幅度最大值-最小值。如果幅度太小信号弱或变化过于剧烈可能是在运动则判定本次读数不可信不更新显示并在状态栏提示“信号弱请保持静止”。6.2 功耗与续航考量WizFi360-EVB-Mini通过USB供电很方便但如果想做成便携设备就需要考虑电池供电下的功耗。主要耗电大户MAX30102传感器LED亮度是功耗关键。在能满足信号质量的前提下尽量降低ledBrightness。此外可以间歇性测量比如每10秒测量5秒其余时间让传感器进入休眠模式如果库函数支持。ESP32与Wi-Fi持续保持Wi-Fi连接和发送数据非常耗电。Blynk的Blynk.run()需要持续调用以维持心跳连接。优化策略使用Blynk的BLYNK_CONNECTED()和BLYNK_DISCONNECTED()事件在连接稳定后可以尝试让ESP32进入“轻睡眠”模式定时唤醒发送数据。或者更激进一点只在需要查看数据时通过手机App发送一个指令唤醒设备进行测量测量完成后自动休眠。这需要更复杂的代码和Blynk Widget的配合如使用Button控件发送指令。6.3 Blynk连接稳定性与断线重连在网络环境不佳时设备可能会与Blynk服务器断开连接。默认的Blynk.begin()在setup中只尝试一次连接。增强连接健壮性 在loop()中除了Blynk.run()还需要检查连接状态并在断开时尝试重连。void loop() { if (Blynk.connected()) { Blynk.run(); // ... 你的主业务逻辑 } else { Serial.println(“Blynk连接断开尝试重连...”); // 这里可以加入一个简单的重连逻辑避免频繁重试刷屏 static unsigned long lastReconnectAttempt 0; if (millis() - lastReconnectAttempt 5000) { // 每5秒尝试一次 lastReconnectAttempt millis(); if (Blynk.connect()) { Serial.println(“重连成功”); } } } // ... 其他代码 }同时确保你的Wi-Fi路由器信号覆盖良好并且ESP32的天线区域板载PCB天线没有被金属物体遮挡。7. 项目总结与扩展思考回顾整个项目从一块闲置的开发板出发到最终在手机上看到自己实时的心跳曲线这个过程充满了探索和调试的乐趣。WizFi360-EVB-Mini作为主控其开箱即用的Wi-Fi能力和兼容Arduino生态的特性大大降低了物联网原型开发的门槛。MAX30102传感器虽然精度有限但作为学习和原理验证的工具已经足够。Blynk平台则完美地填补了设备与用户交互之间的鸿沟让你无需关注复杂的App开发或服务器搭建。这个项目的价值不仅仅在于做出了一个能用的设备更在于它清晰地展示了一个典型的物联网健康监测设备的完整技术栈传感层MAX30102- 处理与连接层ESP32/Wi-Fi- 云平台与交互层Blynk。每一个环节都有可深入挖掘和优化的地方。可能的扩展方向精度提升尝试更先进的PPG信号处理算法如利用傅里叶变换FFT进行频域分析来提取心率这比时域的峰值检测更抗运动干扰。也可以尝试融合加速度计数据进行运动伪影消除。本地显示与存储增加一块OLED屏幕在设备端直接显示数值不依赖手机。加入SD卡模块将历史数据存储在本地实现离线记录。多参数监测MAX30102还能测量体温通过内置的温度传感器。可以将其一并上传到Blynk实现体温、心率、血氧的一体化监测。私有化部署如果你对数据隐私有更高要求或者想完全掌控后端可以用Blynk的开源服务器版本Blynk.Local搭建在自己的树莓派或NAS上实现数据的完全私有化。低功耗优化如前所述通过深度睡眠、间歇测量、降低发射功率等方式打造一个真正的便携式、长续航设备。最后必须再次强调安全与责任。这个自制设备的所有读数都绝对不能用于医疗诊断。它的结果受传感器性能、算法、校准、测量环境和个人差异的影响只能作为个人健康趋势的参考和科技爱好者的学习工具。在涉及健康的关键决策时请务必使用经过认证的医疗设备。