ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESP32驱动MAX30102心率血氧传感器:I2C配置与FIFO读取实战

ESP32驱动MAX30102心率血氧传感器:I2C配置与FIFO读取实战 先说句实在话这模块我前前后后帮人调过不少次光“为什么我读出来全是0”“为什么数值在天上飞”这两个问题就够写一篇长文了。MAX30102听起来唬人实际上就是个集成了红光LED、红外LED、光电二极管和信号链的“数位脉搏波传感器”。它走I2C接口ESP32要驱动它本质上就三件事把I2C打通、把FIFO数据读出来、把红光/红外两路波形变成心率和血氧数值。这三件事拆开讲每一步都不神秘但每一步都有细节很多教程只给代码不解释“为什么这么配”结果你抄完代码还是一头雾水。这篇文章我尽量按“从原理到落地”的顺序写适合手里有ESP32开发板和一块MAX30102模块、想自己做个手环或者健康监测小项目的朋友。不夸张地说只要你照着我下面的思路走哪怕你之前没碰过传感器驱动也能在一个晚上把这套东西跑起来。先说明白MAX30102測的是光电容积脉搏波不是医疗设备做个学习Demo、毕设、个人健康参考完全没问题但别拿它当诊断依据。1. 先搞明白MAX30102到底在干什么1.1 为什么一颗芯片里要塞两个LED人体组织对光的吸收是变化的血液流动、脉搏搏动会导致血管容积周期性变化这个变化会反映在透射或反射回来的光强上这就是光电容积描记英文缩写叫PPG。MAX30102内部布局很有意思它把红光LED、红外LED、光电检测器、ADC、数字滤波和FIFO全塞进了一个小封装里你在模块背面能看到一个金属窗口手指轻轻按上去光线进入皮肤后一部分被血液吸收另一部分散射回来光电二极管把这部分光强变成电信号。问题来了为什么用两个LED因为血红蛋白有两种主要状态——氧合血红蛋白和脱氧血红蛋白它对红光约660nm和红外光约880nm的吸收率不一样。红光对脱氧血红蛋白更敏感红外光对氧合血红蛋白更敏感两路信号一起采取个比值就能估算血氧饱和度。这就是MAX30102要把两种LED放在同一个传感器里的根本原因单色光只能测心率双色光才能算血氧。有人会问手指直接按上去就能测对MAX30102支持反射式测量不需要把手指夹得发白只要模块的光窗贴合皮肤能遮住外部光就行。这里有个常见误区模块和手指之间不能有间隙指甲盖、老茧、冬天皮肤干裂都会严重影响信号质量。我实测下来食指和中指的效果最好按压力度以“刚好遮光、不觉得压迫”为准按太紧反而会让毛细血管血液被挤出波形幅度变小。注意MAX30102和MAX30105外观几乎一样寄存器也基本兼容但MAX30105多了一个绿光LED设计给更多应用场景用的。你手里那板子到底是不是MAX30102用下面代码读一下PART_ID寄存器就知道了MAX30102对应值通常是0x15这个验证步骤很值得做。1.2 I2C接口和内部FIFO是怎么配合的通信层面MAX30102就是一个标准I2C从设备7位地址是0x57。ESP32做主控按地址向寄存器写配置、从FIFO数据寄存器连续读数据。真正让它比普通ADC传感器好用的点是内部带了一个可存储32个采样点的FIFO。这个设计很巧妙传感器自身以固定采样率采集数据数据先存到FIFO里主控有空才来批量取走不用每采一个点就响应一次中断对I2C时序要求大大降低。FIFO的读写逻辑是芯片内部有一个写指针FIFO_WR_PTR和一个读指针FIFO_RD_PTR每次采样完成数据写入写指针指向的位置写指针自动加一主控从FIFO_DATA寄存器连续读取数据时读指针跟着走。你只要比较这两个指针就知道FIFO里攒了几个新样本。配置好FIFO_ROLLOVER_EN以后如果主控没及时读走数据新数据会覆盖旧数据避免读指针追上写指针导致堵塞。具体到每个样本的格式一个采样点由红光24位、红外24位两部分组成但我通常按18位来处理。MAX30102内部ADC的分辨率可以在配置里选择常见的是18位也就是说一个通道的有效数据落在24位数据的高18位里读取后右移或掩码就行。FIFO里连续存放一个样本6字节顺序是红光高字节、中字节、低字节接着红外高字节、中字节、低字节。读的时候直接I2C连续读6字节拼成两个整型这就是原始数据了。2. 硬件接线与供电避坑2.1 引脚连接ESP32与MAX30102的接法ESP32的I2C引脚可以任意指定这里用默认的GPIO21SDA和GPIO22SCL方便和大多数ESP32开发板的丝印对应。MAX30102模块一般引出六个引脚VIN、GND、SCL、SDA、INT、PWDN。PWDN是硬件掉电控制引脚不使用时接高电平可以让芯片进入睡眠正常运行时悬空或者接低都行INT是中断输出引脚可接可不接我自己习惯不接全程用FIFO指针轮询省一根线。MAX30102引脚ESP32引脚说明VIN3V3必须3.3V不能接5VGNDGND共地SCLGPIO22I2C时钟SDAGPIO21I2C数据INT不接或GPIO15中断脚实验可不接PWDN不接或GND接地即正常工作接线只有四根线的时候我会额外提一句上拉电阻模块板载默认有I2C上拉电阻常见的是2.2kΩ或4.7kΩ直接用没问题。如果你手气不好买到那种超便宜的裸芯片转接板没有上拉I2C会时好时坏表现为扫描不到设备或偶尔掉线。这时自己在SCL和SDA上各加一个4.7kΩ电阻到3.3V即可这是个很容易忽略的坑。2.2 供电“三不要”第一不要接5V。MAX30102的VIN上限是5.5V听起来接5V没毛病但绝大多数模块先经过一个LDO稳压到3.3V如果模块设计得粗糙LDO的压差余量不足5V供电时发热不说上电瞬间电流尖峰还可能把传感器内部搞挂。我见过好几块板子就是被5V烧掉的老老实实接3.3V最稳。第二不要跟电机、舵机、继电器共用一个电源。这个传感器对电源纹波非常敏感因为PPG信号本身是叠在直流分量上的微小交流变化幅度可能只有几十到几百LSB电源上一丁点噪声都会淹没脉搏波。你如果同时驱动舵机串口打印的波形会像地震仪记录一样完全没法看。正确做法是给ESP32和MAX30102分开供电或者至少保证传感器电源上有足够容量的滤波电容。我用独立LDO给传感器供电以后波形干净了两个量级。第三不要用ESP32的ADC去采样传感器输出。这是很多人一开始走弯路的地方看到模块有模拟量输出就想接ESP32的ADC。实际上MAX30102已经把光信号量化成I2C数字输出了你完全不需要碰ADC引脚。而且ESP32的ADC精度在低电压区间本来就一般加上零点几伏的偏移直接采模拟信号只会让你怀疑人生。选MAX30102就是看中它内部自带完整信号链这正是你们常搜到的“ESP32 ADC缺陷”问题的最优规避方案。3. 寄存器配置与FIFO读取3.1 初始化顺序寄存器不能乱写MAX30102的寄存器不复杂但讲究一个顺序。我建议按下面几步来后一步以前一步为前提第一步软复位。往MODE_CONFIG寄存器写0x40芯片复位所有寄存器恢复默认值然后等至少100毫秒。很多新手跳过了这一步直接在残留的配置上操作后面的现象会很莫名其妙。第二步配置FIFO。FIFO_CONFIG寄存器里bit5是FIFO_ROLLOVER_EN置1允许FIFO满后覆盖旧数据bit4到bit2是SMP_AVE也就是采样平均次数设置成对应“4次平均”的编码值相当于硬件层面做了个简易滤波能有效减少随机噪声bit1到bit0是FIFO_A_FULL中断阈值实验时设成默认值就行。整个寄存器我习惯直接写0x28。第三步配置工作模式和测量参数。MODE_CONFIG写0x03表示SpO2模式也就是红LED和红外LED同时点亮、交替采样。SPO2_CONFIG寄存器里bit5到bit4是脉冲宽度也就是LED每次点亮的时长脉宽决定了ADC积分时间和有效采样率bit2到bit0是ADC量程数值越大量程越宽。代码里写0x27表示脉宽411微秒、ADC量程8192nA这对大多数手指测试场景是稳定起手的配置。第四步设置LED电流。LED1_PA控制红光LED电流LED2_PA控制红外LED电流。寄存器值越大电流越大比如0x24对应大约36mA0xFF接近最大。初次调试建议从小电流开始比如0x20到0x2F如果波形太弱再往上加。电流太大不仅耗电还容易让光电二极管饱和波形顶部被削平。3.2 数据读取代码实现初始化完成后主循环只需要隔一段时间检查写指针和读指针如果FIFO里有新数据就连续读出来。下面这段是寄存器级操作的封装不依赖任何第三方传感器库你把它和后面的完整代码拼在一起就能跑。#include Wire.h #define MAX30102_ADDR 0x57 // 关键寄存器地址 #define REG_INTR_STATUS_1 0x00 #define REG_INTR_STATUS_2 0x01 #define REG_INTR_ENABLE_1 0x02 #define REG_INTR_ENABLE_2 0x03 #define REG_FIFO_WR_PTR 0x04 #define REG_OVF_COUNTER 0x05 #define REG_FIFO_RD_PTR 0x06 #define REG_FIFO_DATA 0x07 #define REG_FIFO_CONFIG 0x08 #define REG_MODE_CONFIG 0x09 #define REG_SPO2_CONFIG 0x0A #define REG_LED1_PA 0x0C #define REG_LED2_PA 0x0D #define REG_PART_ID 0x23 bool readReg8(uint8_t reg, uint8_t val) { Wire.beginTransmission(MAX30102_ADDR); Wire.write(reg); if (Wire.endTransmission(false) ! 0) return false; if (Wire.requestFrom(MAX30102_ADDR, 1) 1) return false; val Wire.read(); return true; } bool writeReg8(uint8_t reg, uint8_t val) { Wire.beginTransmission(MAX30102_ADDR); Wire.write(reg); Wire.write(val); return Wire.endTransmission() 0; } bool readFIFO(uint32_t red, uint32_t ir) { Wire.beginTransmission(MAX30102_ADDR); Wire.write(REG_FIFO_DATA); if (Wire.endTransmission(false) ! 0) return false; if (Wire.requestFrom(MAX30102_ADDR, 6) 6) return false; uint8_t buf[6]; for (int i 0; i 6; i) buf[i] Wire.read(); red ((uint32_t)buf[0] 16) | ((uint32_t)buf[1] 8) | buf[2]; ir ((uint32_t)buf[3] 16) | ((uint32_t)buf[4] 8) | buf[5]; red 0x3FFFF; // 取低18位有效数据 ir 0x3FFFF; return true; }读FIFO这段代码里有个细节值得说endTransmission(false)表示发送完寄存器地址后不发送停止条件紧接着发重复起始信号去读数据。这个操作能让传感器保持在I2C总线上、连续返回6字节数据比分两次访问更可靠。如果你用的是某些非标准Wire库要注意这个false参数是否被支持。4. 心率与血氧算法拆解4.1 心率计算峰值间隔法拿到红光或红外的一串原始数据后第一步是分离交流分量和直流分量。PPG信号可以粗略看作原始信号 直流分量 交流分量 噪声。直流分量主要来自皮肤、骨骼、组织等非脉动部分的吸收数值很大且变化缓慢交流分量来自动脉血容积的搏动变化幅度小但包含心率信息。最简单的直流估计就是用滑动平均或者指数移动平均dc dc * 0.98 sample * 0.02这个系数对应的时间常数取决于采样率实测下来对于100Hz的采样率很合适。然后ac sample - dc交流分量就出来了。心率检测有很多算法从简单到复杂是阈值峰值法、自适应阈值峰值法、频域FFT法、小波变换。对于ESP32这种资源不算宽裕但也不太紧张的MCU自适应阈值峰值法性价比最高。核心逻辑是维护一个滑动窗口里的最大值和最小值计算出动态阈值比如阈值为最小值 0.3 * (最大值 - 最小值)。当交流波形从低于阈值上升到高于阈值时记一次“疑似心跳”记录这次心跳的时间戳两次心跳的间隔就是瞬时心率周期心率 60000 / 间隔毫秒。这里有个很关键的心率范围约束正常人心率不会低于30BPM也不会高于240BPM换算成间隔就是250毫秒到2000毫秒。如果两次“心跳”间隔小于250毫秒很可能是噪声触发的误检直接丢弃如果大于2000毫秒那更可能是信号丢失也不用更新心率。这个思路简单粗暴但比不做约束的版本稳健得多实测在静止状态下误差可以控制在±3BPM以内。提示测量原理决定了对运动伪影极其敏感。走路时手臂摆动、说话时面部肌肉牵拉、手指在传感器上轻微移动都会产生比脉搏波还大的干扰信号。想让心率算得稳测量期间尽量保持身体静止这是所有家用穿戴设备共同面临的物理难题软件算法只能缓解不能根除。4.2 血氧计算AC/DC比值法血氧饱和度估算依赖红光和红外两路信号的一个无量纲比值R。公式是R (AC_red / DC_red) / (AC_ir / DC_ir)其中AC_red和AC_ir是红光、红外信号的交流分量幅度DC_red和DC_ir是对应的直流分量。R值越大说明红光吸收相对红外吸收变强血氧越低。R和SpO2的关系不是线性在全范围内但在常见的高血氧区间85%到100%可以用一个经验公式拟合比如SpO2 110 - 25 * R或者更精确一些的多项式拟合。实际工程里有很多查表法就是预先测量大量志愿者的R值和真实血氧值建立映射表然后查表插值。这里给出的简化公式只适合学习和演示要用于严谨场景建议参考Maxim官方文档里的SpO2查表。计算AC幅度不用真去做傅里叶变换工程上会用一个滑动窗口内的标准差估算交流分量或者用窗口内最大值减最小值作为峰峰值。我习惯用标准差因为它更抗单个异常点干扰。每采集一个样本就维护一个环形缓冲区存最近5秒的红光和红外数据5秒窗口足够覆盖至少4次心跳能稳定算出平均幅度。窗口长度也是个讲究事太短算出的R值波动大太长则反应慢实时性差。5秒是手环级别产品的常见折中值。5. 完整可编译代码5.1 程序结构说明我写的这个完整例程分为三部分I2C寄存器读写、传感器初始化、数据采集与算法。算法部分保留了最简单的自适应阈值法没有引入复杂的浮点矩阵运算ESP32跑起来轻轻松松在普通Arduino Uno上也能跑只是内存紧一点。代码里我加了详细的注释配合前面的原理部分你可以按需修改LED电流、采样平均次数和窗口大小。运行时串口输出格式是逗号分隔的四列红光原始值、红外原始值、心率、血氧。你可以打开Arduino IDE的串口绘图器把第一列和第二列一起画出来能看到清晰的周期性脉搏波那感觉还挺有成就感的。5.2 完整代码#include Wire.h // MAX30102 寄存器映射 #define MAX30102_ADDR 0x57 #define REG_INTR_STATUS_1 0x00 #define REG_INTR_STATUS_2 0x01 #define REG_INTR_ENABLE_1 0x02 #define REG_INTR_ENABLE_2 0x03 #define REG_FIFO_WR_PTR 0x04 #define REG_OVF_COUNTER 0x05 #define REG_FIFO_RD_PTR 0x06 #define REG_FIFO_DATA 0x07 #define REG_FIFO_CONFIG 0x08 #define REG_MODE_CONFIG 0x09 #define REG_SPO2_CONFIG 0x0A #define REG_LED1_PA 0x0C #define REG_LED2_PA 0x0D #define REG_PART_ID 0x23 // 算法参数 #define WINDOW_SIZE 125 // 5秒窗口约25Hz样本速率 #define SAMPLE_RATE_HZ 25.0f // 与FIFO配置里的4次平均对应 bool readReg8(uint8_t reg, uint8_t val) { Wire.beginTransmission(MAX30102_ADDR); Wire.write(reg); if (Wire.endTransmission(false) ! 0) return false; if (Wire.requestFrom(MAX30102_ADDR, 1) 1) return false; val Wire.read(); return true; } bool writeReg8(uint8_t reg, uint8_t val) { Wire.beginTransmission(MAX30102_ADDR); Wire.write(reg); Wire.write(val); return Wire.endTransmission() 0; } bool readFIFO(uint32_t red, uint32_t ir) { Wire.beginTransmission(MAX30102_ADDR); Wire.write(REG_FIFO_DATA); if (Wire.endTransmission(false) ! 0) return false; if (Wire.requestFrom(MAX30102_ADDR, 6) 6) return false; uint8_t buf[6]; for (int i 0; i 6; i) buf[i] Wire.read(); red (((uint32_t)buf[0] 16) | ((uint32_t)buf[1] 8) | buf[2]) 0x3FFFF; ir (((uint32_t)buf[3] 16) | ((uint32_t)buf[4] 8) | buf[5]) 0x3FFFF; return true; } bool initMAX30102() { uint8_t partId 0; if (!readReg8(REG_PART_ID, partId)) return false; Serial.printf(PART_ID 0x%02X\n, partId); writeReg8(REG_MODE_CONFIG, 0x40); // 软复位 delay(100); writeReg8(REG_FIFO_CONFIG, 0x28); // 平均4次FIFO满覆盖 writeReg8(REG_MODE_CONFIG, 0x03); // SpO2模式红光红外同时开 writeReg8(REG_SPO2_CONFIG, 0x27); // 脉宽411usADC量程8192nA writeReg8(REG_LED1_PA, 0x24); // 红光LED电流约36mA writeReg8(REG_LED2_PA, 0x24); // 红外LED电流约36mA return true; } // 全局变量 float dcRed 512.0f, dcIr 512.0f; float acRedPeak 0.0f, acIrPeak 0.0f; float lastBeatTime 0.0f; float bpm 0.0f; float spo2 0.0f; float redBuf[WINDOW_SIZE]; float irBuf[WINDOW_SIZE]; int bufIndex 0; void setup() { Serial.begin(115200); delay(500); Wire.begin(21, 22); // ESP32 I2CSDA21, SCL22 Wire.setClock(400000); if (!initMAX30102()) { Serial.println(MAX30102 init failed, check wiring!); while (1) delay(1000); } Serial.println(MAX30102 ready.); } void loop() { uint8_t wrPtr 0, rdPtr 0; readReg8(REG_FIFO_WR_PTR, wrPtr); readReg8(REG_FIFO_RD_PTR, rdPtr); int samplesToRead (wrPtr - rdPtr 32) % 32; if (samplesToRead 0) return; for (int i 0; i samplesToRead; i) { uint32_t redRaw, irRaw; if (!readFIFO(redRaw, irRaw)) continue; // 直流分量用指数移动平均估计 dcRed dcRed * 0.98f (float)redRaw * 0.02f; dcIr dcIr * 0.98f (float)irRaw * 0.02f; float acRed (float)redRaw - dcRed; float acIr (float)irRaw - dcIr; // 更新交流峰值用于自适应心率阈值 acRedPeak max(acRedPeak, fabs(acRed * 0.6f) 30.0f); acRedPeak acRedPeak * 0.995f; // 心跳检测上升沿超过阈值 static bool aboveThreshold false; if (acRed acRedPeak * 0.3f !aboveThreshold) { aboveThreshold true; float now millis() / 1000.0f; float interval now - lastBeatTime; lastBeatTime now; if (interval 0.25f interval 2.0f) { bpm 60.0f / interval; } } if (acRed acRedPeak * 0.1f) { aboveThreshold false; } // 填充血氧计算窗口 redBuf[bufIndex] (float)redRaw; irBuf[bufIndex] (float)irRaw; bufIndex (bufIndex 1) % WINDOW_SIZE; // 窗口满时计算一次血氧 if (bufIndex 0) { float sumRed 0, sumIr 0; float sumRedSq 0, sumIrSq 0; for (int j 0; j WINDOW_SIZE; j) { sumRed redBuf[j]; sumIr irBuf[j]; } float meanRed sumRed / WINDOW_SIZE; float meanIr sumIr / WINDOW_SIZE; for (int j 0; j WINDOW_SIZE; j) { sumRedSq sq(redBuf[j] - meanRed); sumIrSq sq(irBuf[j] - meanIr); } float stdRed sqrt(sumRedSq / WINDOW_SIZE); float stdIr sqrt(sumIrSq / WINDOW_SIZE); float ratio (stdRed / meanRed) / (stdIr / meanIr); spo2 110.0f - 25.0f * ratio; spo2 constrain(spo2, 80.0f, 100.0f); } Serial.printf(%d,%d,%.1f,%.1f\n, redRaw, irRaw, bpm, spo2); } delay(5); }如果你的串口打印速度太快导致显示卡顿可以把delay(5)调大一点或者只在检测到心跳时打印一行。我实测这个板子在默认配置下FIFO里大概每40毫秒会攒出1个新样本主循环读写速度完全跟得上。6. 常见问题速查与调试心得6.1 问题速查表现象可能原因排查方法I2C扫描不到0x57设备供电不对、SDA/SCL接反、模块上拉缺失万用表量3.3V和GND交换SDA/SCL再扫描能扫描到设备但读PART_ID失败接触不良、线材过长、I2C总线电容太大缩短杜邦线降低I2C时钟到100kHz原始值恒定不变且偏大或偏小传感器没对准手指、环境光直射、LED电流过小手指完全覆盖光窗关掉强光调大LED电流原始值有波形但心率算出来乱跳按下力度不稳、手指晃动、阈值不合适保持静止观察串口绘图器里的波形幅度再调峰值系数血氧数值长期低于90%或高于100%R值计算窗口不足、手指按压太紧、公式简化误差增大窗口到10秒放松手指多测几次取平均ESP32和传感器之间通信时好时坏面包板接触不良、上拉电阻缺失、电源纹波建议直接插拔检查引脚必要时焊接固定6.2 调试顺序建议我调试这种传感器的固定流程分享出来可能帮大家少走弯路。第一步先刷一个最简单的I2C扫描程序确认总线地址确实是0x57这一步能排除80%的硬件问题。第二步读PART_ID验证芯片型号顺便确认寄存器读写没问题。第三步不跑任何算法只打印红光和红外原始值用手指按住传感器看串口绘图器里的波形是否像脉搏。这一步最重要如果原始数据就是一条平线后面算法写得再花哨也没用。第四步确认波形形状正常、幅度稳定了再上心率和血氧算法。这样哪怕结果不对你也能快速定位是硬件问题还是算法问题。串口绘图器是个神奇的调试工具我强烈建议你养成看波形的习惯。原始红光值应该在一个较大的直流值可能是几百到几千上下周期性浮动频率大概每秒1到2次波形幅度波动个几十到几百。如果你看到的是方波、尖刺、或者高频乱抖先从供电和手指放置这两件事查起别急着改算法。6.3 几个容易忽略的经验关于LED电流我多说一句。很多人觉得电流越大信号越强就直接怼到0xFF结果波形顶端削平ADC饱和反而算不出血氧。我的经验是从0x20开始串口绘图器观察波形幅度如果幅度只有几个到十几个LSB再逐步往上加每次加0x08左右。找到一个“波形圆润、峰谷清晰、不削顶”的电流值就固定下来不要迷信最大值。另外MAX30102内部还有一个温度传感器PART_ID旁边就是TEMP_INTR寄存器。如果你做的是穿戴设备体温是个有价值的关联信息读出来很方便。代码里可以加一个简单的函数往TEMP_CONFIG写触发位等转换完成后读TEMP_INTR整数部分和TEMP_FRAC小数部分拼成带一位小数的温度值。这个算是扩展功能不影响主流程有兴趣可以自己加上。7. 最后的经验之谈我自己最开始调这个模块的时候犯过一个特别低级的错误把模块的VIN接到了ESP32的5V引脚上结果传感器发烫读完寄存器全是0xFF我还以为是接线问题折腾了一晚上。后来换了3.3V供电一切正常。所以第一步检查供电真的不是废话。另外不同商家卖的MAX30102模块质量参差不齐有的模块光窗玻璃用得很差有的焊盘有虚焊现象都是波形很弱或者完全没波形。如果你连续换了两块模块都还是同一现象建议先怀疑自己手里的第三块再多检查一遍接线。说回算法层面我这套自适应阈值峰值法属于“能用但不算聪明”的方案。你要做毕设或者个人项目它足够你交差和演示但如果你是认真要做产品级心率血氧建议去看Maxim官方的算法库和选型指南他们花了大量人力资源在运动伪影消除和低灌注场景优化上这个东西不是几十行代码能完全复现的。不过理解基础的PPG信号处理逻辑永远是深入下去的第一步。实测下来这个方案在ESP32上跑得非常轻松CPU占用率几乎可以忽略不计。如果你还想在这个项目上加个OLED显示屏实时画波形、或者加一个BLE模块把数据推送到手机AppESP32的资源完全够用。关于蓝牙和WiFi能不能同时用的问题我实测过BLE传输小数据量时和WiFi共存是没问题的只要你不搞又开网页服务器又大流量蓝牙传输这种极端情况。项目空间很大玩起来也很有意思后面可以慢慢扩展。
返回列表