1. 为什么我要做一台口袋里的心电监测设备心电监测这件事过去在我印象里一直属于医院专属。十二导联、导电膏、贴片电极、专业工作站一整套下来动辄几万块普通人根本碰不到。但如果你只是想知道自己静息状态下的心率节律是否规整、有没有明显的异常波形其实完全没必要上那么重的装备。MicroECG这个项目就是奔着这个目标去的用一块 ESP8266 加上几片廉价模拟前端做出一台能塞进口袋、成本压到几十块钱、还能把波形实时推到手机或电脑上看的单导联心电监测方案。我最初接触这个方向是因为身边有长辈偶尔会感觉心慌去医院做 Holter 又要预约又要背一整天机器体验很差。当时我就在想能不能自己搭一个低成本的采集端至少在日常状态下做个初步观察。查了一圈资料后发现心电信号本身并不神秘它在皮肤表面的幅度大概在 0.5mV 到 4mV 之间主要能量集中在 0.05Hz 到 100Hz 这个频段。只要前端能把共模干扰压下去、把信号放大到 ADC 能采的量级后面的事情就交给数字处理了。ESP8266 虽然是个 Wi-Fi 模块出身但它自带一个 10 位 ADC主频能跑到 80MHz 甚至 160MHz用来做低速生理信号采集和网络传输其实绰绰有余。这个方案适合谁呢我觉得有三类人值得动手试试。第一类是电子爱好者或者嵌入式初学者想找一个有实际意义、又能把模拟和数字串起来的练手项目第二类是做健康类产品原型的开发者需要一个低成本的信号采集验证平台第三类是对自身心率节律有日常观察需求、又不想频繁跑医院的人。需要提前说清楚的是这东西不是医疗器械不能用于诊断它的定位是学习、原型验证和日常趋势观察。把这句话放在最前面是因为我见过太多人拿着自制设备的数据去吓自己这一点后面还会反复强调。整个项目涉及的技术栈其实挺杂的模拟前端设计、ESP8266 的 ADC 采样与 Wi-Fi 传输、Arduino IDE 下的固件开发以及一个用 JavaScript 写的网页端波形显示。热搜词里同时出现了 ESP8266、Arduino IDE 和 JavaScript恰好对应了这个项目的三个核心环节——采集、固件、显示。下面我就按这个顺序把每个环节的设计思路、踩过的坑和可复现的操作一步步拆开讲。2. 整体方案设计与关键选型思路2.1 单导联为什么是低成本场景下的最优解心电监测按导联数分常见的有单导联、三导联、十二导联。导联越多能反映的心脏电活动空间信息越丰富临床上诊断价值越高。但导联数增加意味着电极数量增加、前端通道数增加、布线复杂度增加成本是成倍往上走的。对于 MicroECG 这种口袋级设备我的判断是单导联足够理由有三点。第一单导联只需要两个采集电极加一个参考电极佩戴位置灵活最常用的就是左右手各贴一片参考电极贴在同侧脚踝或者腹部。这种接法本质上测的是 I 导联方向的心电向量对于观察 P 波、QRS 波群、T 波的基本形态和节律已经够用。第二单通道模拟前端的器件数量少噪声源和失调源也少调试难度大幅下降。第三从数据传输角度看单通道以 250Hz 采样率、10 位精度计算每秒也就 250 个采样点ESP8266 通过 Wi-Fi 推流毫无压力。注意单导联无法定位心肌缺血的具体部位也无法做十二导联那种多壁面评估。如果你的目标是严肃的临床筛查这个方案不适用。2.2 ESP8266 当采集主控的利与弊选 ESP8266 而不是 STM32 或者专用 AFE 芯片是一个很现实的取舍。ESP8266 最大的优势是自带 Wi-Fi 和 TCP/IP 协议栈这意味着采集端可以直接把数据推到局域网里的服务器或者网页不需要额外的蓝牙模块或者串口转接。成本上一块 ESP8266 模组十几块钱比 STM32 加 Wi-Fi 模块的组合便宜不少。开发环境用 Arduino IDE库生态成熟上手快。但它的短板也很明显。ESP8266 内置的 ADC 只有一个通道10 位分辨率输入范围是 0 到 1.0V而且线性度和噪声表现都一般。更麻烦的是Wi-Fi 工作时射频部分会产生周期性干扰如果采样时序和射频活动没协调好波形上会出现规律性的尖刺。我在早期版本里就吃过这个亏后来通过调整采样窗口、在射频发送间隙集中采样才缓解。对比项ESP8266STM32F103专用 AFE如 AD8232单价约 10-15 元约 15-25 元约 30-60 元ADC 分辨率10 位12 位24 位含前端无线能力内置 Wi-Fi需外挂需外挂开发难度低中中适合场景原型验证、教学中端采集高精度医疗从这张表能看出来ESP8266 的定位就是低成本原型。如果你追求的是信号质量AD8232 这类专用前端加 STM32 的组合会更合适但如果你要的是快速验证、低成本、能联网ESP8266 是性价比最高的选择。2.3 模拟前端的基本架构心电信号从电极出来的时候非常微弱而且叠加了大量的共模干扰主要是 50Hz 工频干扰和人体作为天线拾取的各种电磁噪声。所以前端必须做三件事仪表放大、高通滤波、低通滤波。仪表放大我用的是经典的三运放结构核心是 AD620 或者更便宜的 INA128 这类仪表放大器芯片。它的作用是把差分信号放大同时抑制共模信号。增益设置上心电信号典型幅度 1mVESP8266 的 ADC 满量程 1.0V理论上需要 1000 倍增益才能充分利用量程。但实际中考虑到基线漂移和干扰我把第一级增益设在 100 倍左右后面再加一级可调增益总增益控制在 500 到 800 倍之间。高通滤波用来去掉电极极化产生的直流偏置和呼吸引起的基线漂移截止频率设在 0.05Hz 到 0.5Hz 之间。低通滤波用来抗混叠截止频率设在 100Hz 到 150Hz因为心电信号的主要能量在 100Hz 以下。这两级滤波用运放搭有源滤波器就能实现成本很低。2.4 数据链路与显示端的分工整个数据流是这样的电极采集 → 模拟前端放大滤波 → ESP8266 ADC 采样 → Wi-Fi 推送到局域网 → 浏览器网页接收并绘制波形。显示端我选择用 JavaScript 在浏览器里做而不是写一个原生 App原因是跨平台、零安装、调试方便。你只要在电脑或者手机上打开一个网页就能看到实时波形不需要装任何东西。网页端用 WebSocket 接收 ESP8266 推过来的数据然后用 Canvas 或者 SVG 绘制波形。JavaScript 在这里的角色很纯粹建立连接、解析数据、滚动绘制。热搜词里有一堆 JavaScript 相关的内容从基础语法到 DOM 操作都有说明很多人对用 JS 做可视化感兴趣。这个项目的显示端代码量不大核心就是一个环形缓冲区加一个定时重绘循环。3. 模拟前端与电极连接的实操细节3.1 电极选型与皮肤接触处理电极是整个链路里最容易被忽视、但对信号质量影响最大的环节。医用一次性 Ag/AgCl 电极效果最好但价格偏高且是一次性的。日常 DIY 我推荐用** reusable 的凝胶电极**或者退而求其次用镀银的金属片加导电膏。我试过直接用铜片贴皮肤结果极化电压漂得厉害波形根本没法看这个坑大家不要踩。皮肤处理也很关键。贴电极之前用酒精棉片擦拭皮肤去掉油脂和角质层能显著降低接触阻抗。如果皮肤比较干燥可以稍微用细砂纸轻轻打磨一下但别磨破。接触阻抗理想情况下要低于 10kΩ太高的话共模抑制会变差50Hz 干扰会直接淹没有用信号。电极位置方面单导联最常用的接法是负极LA贴左手腕内侧正极RA贴右手腕内侧参考电极RL贴右脚踝或者右下腹。这个接法对应标准 I 导联。如果你只是想看心率其实左右手加一个参考就够了。贴的时候注意电极方向别贴反了反了波形会倒置。3.2 仪表放大电路的搭建要点仪表放大器我选的是 AD620因为它外围元件少、增益设置简单只需要一个电阻就能设定增益。增益公式是 G 1 49.4kΩ / Rg。如果你想要 100 倍增益Rg 大约取 499Ω想要 500 倍Rg 取 99Ω 左右。我建议第一级先用 100 倍把 Rg 用精密电阻误差控制在 1% 以内。供电方面AD620 需要正负双电源典型是 ±5V 或者 ±9V。口袋设备里搞双电源有点麻烦我的做法是用一颗 9V 电池加一个虚拟地电路或者用升压芯片从 3.7V 锂电池升到 ±5V。虚拟地可以用两个等值电阻分压加一个缓冲运放来实现这样单电源就能模拟出双电源的效果。提示AD620 的 REF 引脚要接到虚拟地而不是真正的电源地否则输出会偏掉。这个细节我第一次搭的时候忽略了结果输出一直饱和。3.3 滤波器的参数计算与实现高通滤波器我用的是一阶有源高通截止频率 f 1 / (2πRC)。取 R 1MΩC 1μF算下来 f 约 0.16Hz正好落在心电信号下限附近。这个电容要用无极性电容电解电容的漏电流会影响基线。低通滤波器用的是二阶 Sallen-Key 结构截止频率设在 100Hz 左右。取 R 10kΩC 100nFf 1 / (2π × 10k × 100n) ≈ 159Hz。如果想更保守一点把电容换成 150nF截止频率降到 106Hz。Sallen-Key 的 Q 值通过电阻比例调节我一般取 Q 0.707也就是巴特沃斯响应通带最平坦。这里有个经验滤波器不要一次做太陡。有些人喜欢把低通截止频率压到 40Hz觉得这样噪声小但心电的 QRS 波群高频成分会被削掉R 波幅度会明显下降反而不好判断。100Hz 左右是个比较平衡的选择。3.4 电源与共模抑制的实战处理共模抑制比是心电前端最核心的指标之一。理想情况下仪表放大器的 CMRR 能到 100dB 以上但实际电路中电极阻抗不匹配、电阻误差、PCB 布局都会把 CMRR 拉低。我的经验是电极阻抗匹配比芯片本身更重要。两个采集电极的接触阻抗差如果超过 10%CMRR 会急剧恶化。电源方面如果用电池供电本身就比较干净。如果用 USB 供电一定要加 LC 滤波否则电脑电源的开关噪声会串进来。我在 USB 供电版本里加了一个共模扼流圈加两个 100μF 电容50Hz 干扰明显改善。另外人体本身会拾取空间中的工频干扰这个干扰是共模的理论上会被仪表放大器抑制。但如果你的电路没有良好的参考地共模干扰会转化成差模。所以参考电极一定要接好而且参考电极的接触阻抗也要尽量低。4. ESP8266 固件开发与采样时序控制4.1 Arduino IDE 环境配置与踩坑记录用 Arduino IDE 开发 ESP8266第一步是装开发板支持。打开首选项在附加开发板管理器网址里填入 ESP8266 的板级支持包地址然后在开发板管理器里搜索 esp8266 并安装。这个过程看起来简单但热搜词里出现了 a fatal esptool.py error occurred: failed to connect to esp8266: timed out waiting for packet header说明很多人卡在烧录这一步。这个报错的原因通常有三个一是串口驱动没装好CH340 或者 CP2102 的驱动要装对二是开发板型号选错了NodeMCU 和 Wemos D1 mini 的烧录参数不一样三是烧录时 GPIO0 没有拉低。我的建议是烧录前先把开发板型号、端口、烧录速度都确认一遍速度先降到 115200 试试稳定了再往上调。注意ESP8266 的 ADC 引脚是 A0输入范围 0 到 1.0V。如果你直接把 3.3V 的信号接上去会烧掉 ADC。前端输出一定要分压或者限幅到 1.0V 以内。4.2 ADC 采样率与定时器配置心电信号采样率不需要太高根据奈奎斯特采样定理信号带宽 100Hz采样率至少 200Hz。我实际用的是 250Hz留一点余量。ESP8266 上实现精确的 250Hz 采样不能用delay()因为delay()的精度受 Wi-Fi 任务影响很大。正确做法是用硬件定时器或者micros()做非阻塞轮询。我的做法是在loop()里用micros()判断时间差每 4000 微秒采一次也就是 250Hz。采样的时候先关掉 Wi-Fi 的中断采完再打开这样能避免射频干扰串进采样窗口。实测下来这样处理之后波形上的周期性尖刺基本消失了。unsigned long lastSample 0; const unsigned long sampleInterval 4000; // 250Hz void loop() { unsigned long now micros(); if (now - lastSample sampleInterval) { lastSample now; int raw analogRead(A0); // 存入缓冲区稍后发送 buffer[bufIndex] raw; if (bufIndex BUF_SIZE) { sendBuffer(); bufIndex 0; } } // 处理 Wi-Fi 和其他任务 server.handleClient(); }这段代码的核心是非阻塞采样。如果你用delay(4)实际间隔可能是 4ms 到 10ms 不等采样率会飘画出来的波形时间轴就不准了。4.3 Wi-Fi 连接与数据推流策略ESP8266 连 Wi-Fi 用WiFi.begin()连上之后可以开一个 TCP server 或者 WebSocket server。我选的是 WebSocket因为浏览器端原生支持不需要额外协议解析。用WebSocketsServer这个库几行代码就能把数据推出去。推流策略上我建议攒一批再发而不是每采一个点就发一次。因为每个 WebSocket 帧都有开销250Hz 逐点发的话网络负担很重。我的做法是攒 25 个点发一次也就是每 100ms 发一帧每帧 25 个采样值。这样既保证了实时性又降低了网络开销。数据格式上我用的是简单的文本格式每个采样值用逗号分隔一帧就是 512,515,520,... 这样的字符串。虽然二进制更省带宽但文本调试方便而且 250Hz 的数据量对 Wi-Fi 来说微不足道。4.4 采样数据的预处理与打包ESP8266 的 ADC 读出来是 0 到 1023 的整数对应 0 到 1.0V。但实际信号有基线漂移直接画出来会上下跑。我在固件里做了一个简单的高通滤波用一阶 IIR 实现y[n] x[n] - x[n-1] 0.995 * y[n-1]。这个滤波器的截止频率大概在 0.2Hz 左右能有效去掉基线漂移同时保留心电波形。打包的时候我把滤波后的值转成整数范围控制在 0 到 1023。如果要做更精细的处理可以在固件里做 50Hz 陷波但 ESP8266 的算力有限我建议把复杂处理放到网页端做固件只负责采集和透传。5. JavaScript 网页端波形显示的实现5.1 WebSocket 连接与数据接收网页端的第一步是建立 WebSocket 连接。ESP8266 的 IP 地址在串口里会打印出来网页里直接连这个地址就行。代码很简单const ws new WebSocket(ws://192.168.1.100:81); ws.onmessage function(event) { const values event.data.split(,).map(Number); values.forEach(v dataBuffer.push(v)); if (dataBuffer.length MAX_POINTS) { dataBuffer.splice(0, dataBuffer.length - MAX_POINTS); } };这里用了一个数组当环形缓冲区超过最大点数就把老的丢掉。MAX_POINTS 根据屏幕宽度和显示时长来定我一般设 1250也就是 5 秒的数据。5.2 Canvas 绘制与滚动刷新绘制用 Canvas 2D 就够了不需要 WebGL。核心是一个requestAnimationFrame循环每帧清空画布然后把缓冲区里的数据画成折线。为了让它看起来像实时滚动我每帧只画最新的 N 个点并且把 X 轴映射到画布宽度。function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); const step canvas.width / MAX_POINTS; for (let i 0; i dataBuffer.length; i) { const x i * step; const y canvas.height - (dataBuffer[i] / 1023) * canvas.height; if (i 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle #00ff00; ctx.lineWidth 1.5; ctx.stroke(); requestAnimationFrame(draw); }这段代码里Y 轴做了翻转因为 Canvas 的坐标原点在左上角而心电波形习惯上正方向朝上。颜色用绿色是模仿传统监护仪的风格看起来比较顺眼。5.3 心率计算与波形特征提取光看波形还不够最好能实时算出心率。心率的计算思路是检测 R 波峰值然后算相邻 R 波之间的时间间隔。R 波检测最简单的办法是阈值法设定一个动态阈值当信号从下方穿过阈值时认为检测到一个 R 波。let lastPeakTime 0; let heartRate 0; function detectRPeak(value, timestamp) { const threshold 600; // 根据实际信号调整 if (value threshold timestamp - lastPeakTime 300) { if (lastPeakTime 0) { const interval timestamp - lastPeakTime; heartRate 60000 / interval; } lastPeakTime timestamp; } }300ms 的不应期是为了防止同一个 R 波被重复检测。阈值 600 是经验值实际要根据信号幅度调整。更 robust 的做法是用自适应阈值比如取最近 2 秒信号最大值的 60%。热搜词里提到了 ecg的几种特殊波形这里顺便说一下。正常心电波形由 P 波、QRS 波群、T 波组成。P 波代表心房除极幅度小、圆钝QRS 波群代表心室除极幅度大、尖锐T 波代表心室复极幅度中等、较宽。如果 R-R 间期不规整可能提示节律异常如果 QRS 波群变宽可能提示传导问题。但这些判断需要专业知识自制设备的数据只能作为参考不能作为诊断依据。5.4 页面交互与移动端适配网页端我还加了几个交互功能暂停/继续、调整增益、调整时间轴长度。这些用简单的按钮和滑块就能实现。移动端适配方面Canvas 的尺寸要根据屏幕宽度动态设置用window.innerWidth和devicePixelRatio来处理高清屏。热搜词里有一堆 JavaScript 基础内容从数据类型到循环语句到正则表达式说明很多人在系统学习 JS。这个项目的网页端代码量不大但涵盖了 WebSocket、Canvas、事件处理、数组操作等实用知识点拿来练手很合适。6. 常见问题排查与实战避坑指南6.1 波形噪声大、50Hz 干扰明显怎么办这是最常见的问题。排查顺序我建议从后往前先看电极接触再看前端电路最后看电源。电极接触阻抗高是最常见的原因用酒精擦拭皮肤、换新电极、确保电极贴紧往往能解决大半问题。如果电极没问题检查仪表放大器的 CMRR看看两个输入端的电阻是否匹配。电源方面如果用 USB 供电换电池供电试试如果电池供电下噪声消失那就是电源问题加 LC 滤波。还有一种情况是波形上有规律的尖刺间隔和 Wi-Fi 发送周期一致。这是射频干扰串进了模拟前端。解决办法是把模拟前端和 ESP8266 物理隔开或者在前端输出加 RC 低通截止频率设在 1kHz 左右。6.2 烧录失败与串口连接问题failed to connect to esp8266: timed out waiting for packet header 这个报错我遇到过很多次。除了前面说的驱动和型号问题还有一个容易被忽略的点有些 ESP8266 开发板的自动复位电路设计不好烧录时需要手动按住 FLASH 键再按 RST 键。如果自动烧录一直失败试试手动进入烧录模式。串口监视器乱码通常是波特率不对ESP8266 默认是 74880 或者 115200试试这两个。如果串口完全没输出检查 TX/RX 有没有接反以及开发板有没有正常供电。6.3 波形基线漂移与饱和基线漂移一般是电极极化或者呼吸引起的。高通滤波器能解决大部分问题但如果漂移太严重可能是电极质量太差或者接触不稳定。饱和则是增益太大信号超出了 ADC 量程。解决办法是降低增益或者在前端加一个自动增益控制。我一般把总增益设在 500 倍左右留足够的余量。6.4 网络延迟与数据丢包Wi-Fi 推流偶尔会丢包表现为波形上有断点。如果丢包不严重可以在网页端做插值补点。如果丢包严重检查路由器负载和信号强度。ESP8266 的 Wi-Fi 天线是板载的信号弱的时候可以换个位置或者用带外置天线的模组。问题现象可能原因排查方法解决措施波形噪声大电极接触差测接触阻抗清洁皮肤、换电极50Hz 干扰共模抑制不足检查电阻匹配加共模扼流圈规律尖刺射频干扰对比 Wi-Fi 开关隔离前端、加低通烧录失败驱动/型号/模式检查端口和型号手动进烧录模式基线漂移电极极化观察漂移周期加高通滤波波形饱和增益过大看 ADC 是否顶到 1023降低增益数据丢包网络不稳ping 延迟优化路由器位置6.5 几个我踩过的坑和独家经验第一个坑是忽略了 ADC 的输入范围。ESP8266 的 ADC 只能接受 0 到 1.0V我一开始直接把前端输出接上去结果信号一超过 1V 就削顶波形全是平的。后来加了一个分压电阻把信号压到 0.8V 以内才正常。第二个坑是采样和 Wi-Fi 抢资源。ESP8266 是单核Wi-Fi 协议栈会占用大量 CPU 时间。如果采样放在中断里Wi-Fi 任务会被打断导致连接不稳定。我的做法是把采样放在主循环里用非阻塞方式让 Wi-Fi 任务有足够时间运行。第三个坑是网页端缓冲区管理。一开始我用数组push和shift数据量大了之后性能急剧下降。后来改成固定长度的环形缓冲区用索引覆盖性能稳定多了。第四个坑是电极贴反。有一次我把正负极贴反了波形整个倒过来R 波朝下差点以为心脏出了问题。后来养成习惯贴之前先确认电极方向并且在网页端加了一个翻转波形的按钮方便快速纠正。7. 这个方案还能怎么扩展MicroECG 作为一个基础平台后续可以往几个方向扩展。一是加 SD 卡本地存储做长时间记录相当于一个简易 Holter。二是加蓝牙模块把数据推到手机 App摆脱 Wi-Fi 的局域网限制。三是加第二路采集通道做双导联能提供更多空间信息。四是把前端换成专用 AFE 芯片提升信号质量往更严肃的方向走。软件端也可以做很多事。比如加一个简单的 QRS 检测算法自动标记每次心跳加一个心率变异性分析看 R-R 间期的波动加一个数据导出功能把波形存成 CSV方便后续分析。这些功能用 JavaScript 都能实现而且不需要改动硬件。我个人在实际操作中的体会是这个项目最大的价值不在于做出一个多精确的设备而在于它把模拟前端、嵌入式固件、网络传输、前端可视化串成了一条完整的链路。你会在调试过程中真正理解共模抑制、采样定理、非阻塞编程这些概念这些经验比单纯看教程要深刻得多。最后再分享一个小技巧调试前端的时候可以用信号发生器输入一个 1mV、10Hz 的正弦波来模拟心电信号这样比直接用人体测试更容易定位问题也更安全。
