拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于ESP32与BW16的低成本脑电采集与实时显示原型搭建

基于ESP32与BW16的低成本脑电采集与实时显示原型搭建

做这个项目的起因其实挺简单:实验室里那台传统脑电设备又贵又笨,光是给受试者戴电极帽、连接一堆线缆就要折腾半小时,更别提做可穿戴或者移动场景下的验证了。我手头刚好有一块BW16模组、一块ESP32-CYD带屏开发板,就想试试能不能用这两块加起来不到几十块钱的硬件,搭一条从脑电采集、无线传输到屏幕波形绘制和网页实时展示的完整原型链路。做完之后发现这条路不仅走得通,效果还超出预期,所以把整个搭建过程和踩过的坑整理出来,给做生物电信号采集、可穿戴设备原型验证或者单纯想在嵌入式设备上玩实时信号处理的朋友一个参考。

这条链路说白了就三步:BW16负责无线数据转发,ESP32-CYD负责接收、显示和提供网页服务,中间用TCP或WebSocket把数据串起来。真正难的不是硬件连接,而是信号质量和实时性怎么在这种低成本方案里做平衡。下面按我的实际搭建顺序来拆解整个项目。

1. 项目整体设计与架构思路

1.1 为什么选BW16和ESP32-CYD这个组合

先说选型逻辑。脑电原型链路里最核心的诉求有两个:一是数据要能实时传,二是接收端要能快速可视化。BW16这颗芯片集成Wi-Fi和蓝牙双模,支持Arduino开发环境,更重要的是它带了一个硬核的Cortex-M4F浮点MCU,这意味着我可以在前端做一部分简单的滤波处理,而不是把裸数据一股脑全丢出去。ESP32-CYD则是国内开源圈很常用的一块带屏开发板,板载2.4英寸LCD屏幕,主控是ESP32,同样带Wi-Fi,性能和生态都非常成熟。

这个组合的巧妙之处在于:BW16作为佩戴端的采集转发节点,可以放在离电极很近的位置,减少模拟信号长距离传输的干扰;ESP32-CYD作为接收端,既有屏幕能画波形,又能顺手开一个WebSocket服务端,让浏览器直接连过来看数据。如果换成蓝牙方案,延迟和连接稳定性都不如Wi-Fi直接;如果换成纯有线方案,又失去了可穿戴验证的意义。

1.2 完整信号链路拆解

整个系统的数据流可以分成四段:脑电电极采集原始信号,模拟前端做放大和初步滤波,BW16完成数字化和无线发送,ESP32-CYD接收后同时推送到屏幕和网页客户端。

我用的是单通道干电极方案,参考电极放在耳垂位置,信号经过仪表放大器后进入BW16的ADC采样。采样率设在250Hz,分辨率16位,这个配置对于原型验证来说足够,既能看清Alpha波和Beta波的基本形态,又不会产生太大的无线传输压力。实测下来,250Hz采样率下单通道数据流大约每秒500字节,做以太网帧封装之后对Wi-Fi带宽的占用可以忽略不计。

1.3 为什么把显示做成“屏幕+网页”双通道

只做屏幕显示其实更简单,但屏幕存在两个问题:一是刷新率受限于SPI接口和屏幕分辨率,二是波形一旦滚起来,很难做长时间的观察和分析。网页端则没有这些限制,浏览器里的Canvas可以自由缩放、暂停、加标尺,还能同时开多个客户端观看。

所以我在ESP32-CYD上同时挂了两个输出:板载屏幕用TFT_eSPI库驱动,画实时滚动的波形;Wi-Fi侧开一个WebSocket服务端,浏览器连接后按帧推送数据。这样既满足了现场演示的需求,又保留了后续做算法调试和数据分析的扩展空间。

2. 脑电去噪:原型链路里最容易被低估的一环

2.1 脑电信号为什么这么难处理

说实话,搭建硬件链路只花了两天,但把脑电信号处理好花了整整一周。脑电信号的典型幅度只有1到100微伏,而周围环境的电磁干扰随便就是毫伏级别,差了好几个数量级。更麻烦的是,人体本身还有50Hz工频干扰、肌肉电伪迹、电极接触噪声和基线漂移这四类主要污染源。

最开始我踩过一个典型的坑:直接把BW16的ADC输入接到电极上,屏幕上显示的完全是一条带毛刺的直线,什么都看不出来。原因很简单,没有放大和滤波的脑电信号就像在嘈杂的菜市场里听一个人轻声说话,信息完全被噪声淹没了。后来我加了一级前置仪表放大电路,把信号放大了1000倍左右,再用二阶贝塞尔低通滤波器和50Hz陷波器处理,才终于在屏幕上看到像样的波形。

2.2 嵌入式环境下的滤波方案选型

去噪方案我试过三种:纯软件数字滤波、模拟RC滤波加数字滤波混合、以及自适应滤波。纯软件方案最灵活,但160MHz的ESP32和150MHz的BW16跑复杂算法时会占用较多CPU周期,影响数据处理的稳定性。自适应滤波(比如LMS算法)理论上效果最好,能在参考噪声源存在的情况下实时追踪干扰,但对于原型项目来说实现复杂度偏高。

最后我采用的是“模拟前端粗滤+数字精细去噪”的组合方案。模拟端用一个二阶贝塞尔低通把截止频率设在100Hz以下,滤掉大部分高频噪声;数字端在BW16里实现一个50Hz陷波器和一个25Hz低通滤波器。经过这两级处理后,静止闭眼状态下的Alpha节律已经能比较清晰地分辨出来。

// BW16 上的数字陷波器实现(直接IIR结构) // 采样率250Hz,陷波频率50Hz,带宽约2Hz float notchFilter(float input) { static float x1 = 0, x2 = 0, y1 = 0, y2 = 0; float r = 0.997f; float cos_w = 1.414f; // 对应50Hz/250Hz的归一化频率 float output = input + 2 * r * cos_w * y1 - r * r * y2 + x2 - 2 * cos_w * x1; output = output / (1 + 2 * r * cos_w - r * r); // 更新延迟线 x2 = x1; x1 = input; y2 = y1; y1 = output; return output; }

2.3 去噪效果的验证方法

滤波做完之后不能只看“感觉干净了”,得有一个客观的验证手段。我用了两种方式:第一种是让受试者闭上眼睛放松两分钟,然后看频域里8到12Hz的Alpha峰是否明显抬升;第二种是让受试者做几个快速的眨眼动作,看滤波后的波形能否捕捉到眨眼带来的瞬时高幅值脉冲。

实测下来,这套滤波方案能把50Hz工频干扰抑制到原来的十分之一左右,Alpha波的识别成功率大概在百分之七八十。对于原型项目来说这个水平已经够用了,后续如果要做更严谨的算法研究,就得换更高精度的模拟前端和更复杂的空间滤波方案。

3. 硬件搭建与核心环节实现

3.1 从脑电电极到BW16的硬件连接

我先说清楚,这里用的是干电极方式,不是传统的湿电极导电膏方案。湿电极信号质量好,但使用麻烦,不适合快速验证。干电极虽然接触阻抗高,但只要用弹性头带固定好,配合前置高输入阻抗放大器,也能拿到可用的信号。

硬件连接上,我用了两片ADS1292的简化版方案,实际项目里其实只需要一个单通道仪表放大器,型号选的是AD8232,因为它内部集成了右腿驱动电路和导联脱落检测,特别适合做心电和脑电的原型验证。AD8232的输出接到BW16的ADC引脚(GPIO4),参考地在电极板的公共端。

需要特别注意的一点是:整个模拟前端和BW16之间必须用屏蔽线连接,屏蔽层接地,否则无线发射电路的高频辐射会直接耦合进模拟信号里。我第一次飞线测试时没注意,屏幕上全是细密的高频噪声,最后换了三芯屏蔽电缆才解决。

模块引脚BW16引脚说明
AD8232输出OUTPUTGPIO4/ADC模拟信号输入
AD8232参考REF3V3参考电压
AD8232地GNDGND共同接地
屏蔽层屏蔽GND防射频干扰

3.2 BW16 上的采集与无线发送实现

BW16的软件开发我一开始用的是官方提供的Arduino兼容包,后来发现直接写SDK效率更高,主要是因为Arduino封装层对ADC连续采样和Wi-Fi并发支持不够好,数据一聊多了就容易卡顿。

采集端代码的逻辑很简单:ADC触发采样,数据进入DMA缓冲区,每凑够50个采样点(对应0.2秒的数据窗口)就打包成一个数据帧,通过Wi-Fi发送给ESP32-CYD。数据帧格式我设计成了定长结构,方便接收端做解析和校验——4字节帧头、1字节通道号、100字节的采样数据、4字节时间戳和1字节校验和。

// BW16 数据帧打包伪代码 uint8_t packet[110]; uint8_t* p = packet; // 帧头 0xA5 0x5A *p++ = 0xA5; *p++ = 0x5A; // 通道号 *p++ = 0x01; // 50个采样点,每个2字节,小端序 for (int i = 0; i < 50; i++) { *p++ = (sample[i] & 0xFF); *p++ = ((sample[i] >> 8) & 0xFF); } // 时间戳(毫秒级) uint32_t ts = millis(); memcpy(p, &ts, 4); p += 4; // 简单校验和 uint8_t checksum = 0; for (int i = 0; i < 106; i++) { checksum += packet[i]; } *p = checksum; // 通过TCP发送 client.write(packet, 110);

这里我踩了一个特别典型的坑:BW16的ADC在连续采样模式下,如果不开启内部偏置校准,会出现约100到200毫伏的直流偏置,这个偏置直接叠加在脑电信号上,让后续的滤波算法失效。解决办法是每次启动采集前先做一次软件自校准:把输入短接到参考地采样100次求平均,然后在后续采样结果里减去这个偏移值。

3.3 ESP32-CYD 的屏幕波形绘制

ESP32-CYD板载的屏幕是240×320分辨率的TFT屏,主控是ST7789或ILI9341(不同批次有区别),驱动库我用的是TFT_eSPI,这个库在ESP32上的性能表现非常好,DMA刷屏能达到40帧每秒以上。

波形绘制的思路是这样:ESP32-CYD作为TCP服务器接收BW16发来的数据帧,解析后放入环形缓冲区。屏幕绘制时,取缓冲区里最新的240个采样点(正好对应屏幕宽度),映射到屏幕高度范围内逐点连线。为了减少闪烁,我先绘制到一个离屏缓冲区,填充完成后一次性推送到屏幕。

// ESP32-CYD 波形绘制核心逻辑 void drawWaveform() { // 清空缓冲区 sprite->fillSprite(TFT_BLACK); // 获取最新数据 int16_t data[240]; ringBuffer->readLatest(data, 240); // 计算缩放系数,动态适配信号幅度 float maxVal = 50.0f; // 预设显示范围 ±50μV int x_step = 1; for (int x = 0; x < 240; x++) { int y = 160 - (int)(data[x] / maxVal * 120.0f); if (x > 0) { sprite->drawLine(x - 1, prev_y, x, y, TFT_GREEN); } prev_y = y; } // 推送到屏幕 sprite->pushSprite(0, 0); }

屏幕显示的方案看起来直接,但有几个细节要注意:一是波形滑动速度要和采样率匹配,否则视觉上会有跳跃感;二是动态缩放很关键,否则闭眼时幅值增大但显示范围不变,波形会削顶;三是屏幕刷新频率没必要太高,实测30Hz以上人眼就分不出区别了,反而白白增加CPU负担。

3.4 Web 端实时波形的实现方式

网页部分比屏幕显示更有意思,因为浏览器端的图形绘制能力远超嵌入式屏幕,而且天然支持多客户端同时访问、数据记录和离线分析。

ESP32-CYD上我开了一个WebSocket服务端,监听8080端口。浏览器页面通过JavaScript建立WebSocket连接,接收二进制数据帧,解析后用Canvas绘制波形。网页上我还加了几个简单的交互控件:暂停/继续、手动增益调节、波形颜色切换和截图保存。

// 浏览器端 WebSocket 接收波形数据 const socket = new WebSocket('ws://192.168.1.100:8080/ws'); socket.binaryType = 'arraybuffer'; socket.onmessage = function(event) { const data = new DataView(event.data); if (data.getUint8(0) === 0xA5 && data.getUint8(1) === 0x5A) { const samples = []; for (let i = 3; i < 103; i += 2) { samples.push(data.getInt16(i, true) * 0.1); // 转换为μV } drawSamples(samples); } }; function drawSamples(samples) { ctx.beginPath(); for (let i = 0; i < samples.length; i++) { const x = (index + i) % width; const y = height / 2 - samples[i] * gain; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); }

Web端去噪是我额外加的一个功能,浏览器里可以做实时FFT显示频谱,方便直观看到Alpha波和Beta波的频段能量分布。说实话,浏览器端的JavaScript浮点运算性能不差,跑FFT完全够用,作为原型阶段的快速验证工具再合适不过。

4. 常见问题与排查技巧实录

4.1 无线传输不稳定、数据掉帧

这个项目里最常遇到的问题就是无线链路不靠谱。我排查了一圈,发现有几个典型的诱因:BW16和ESP32-CYD距离太远、两者之间有金属遮挡、以及Wi-Fi信道被其他热点占用。解决办法是先用手机热点做基准测试,把路由器和AP模式固定下来,同时把ESP32-CYD设置成5GHz频段的接入点(BW16和ESP32-CYD都支持双频),减少和2.4GHz民用频段的冲突。

数据掉帧还有一个隐藏原因:TCP传输本身是有序且可靠的,但如果接收端处理速度跟不上发送端,内核缓冲区满了之后就会丢包。我最后的处理是增大ESP32-CYD的Socket接收缓冲,并且让BW16每满100帧做一次ACK握手。如果连续三次没收到ACK就主动降低采样率,从250Hz降到200Hz继续传,保证链路稳定优先。

4.2 屏幕上波形出现周期性毛刺

这个现象特别让人抓狂,波形整体正常,但每隔一段时间就冒出来一个尖峰。后来我用逻辑分析仪看了ESP32-CYD的解码时序才发现,毛刺出现的时间点恰好是WB16发送数据后的瞬间。原因很明确:Wi-Fi射频发射瞬间会产生一个较强的电磁脉冲,通过电源线耦合进模拟前端,经过1000倍放大后直接变成了毫伏级的尖峰。

解决办法是在模拟前端和BW16之间加了LC滤波隔离,同时给BW16供电加了一颗100μF的钽电容做储能缓冲。另外值得多说一句:ADC采样时刻要和Wi-Fi发包时刻避开,用软件把采样安排在两次发送之间,效果立竿见影。

现象原因解决方案
周期性尖峰Wi-Fi射频脉冲干扰模拟前端加屏蔽罩+Ferrite磁珠分离地线
屏幕波形缓慢上漂模拟前端直流偏置未有效抑制增加高通滤波或软件基线校准
网页端数据断流TCP连接超时无重连逻辑心跳包+自动重连机制
蓝牙/Wi-Fi相互干扰双模同时占用天线锁定Wi-Fi模式,禁用蓝牙共存

4.3 脑电信号在屏幕上看起来“太平滑”

这是一个容易误判成硬件故障、其实是滤波过度的问题。我之前为了让波形好看,把低通滤波器的截止频率调得太低,导致高频的Beta波成分被砍掉,波形变得又钝又平滑。后来在网页端加了FFT频谱显示,才发现高频成分整个消失了。

正确的做法是让滤波尽量保守:低通截止频率不要低于60Hz,陷波器只针对50Hz窄带,剩下那些细小的毛刺留给显示端的平滑算法处理。毕竟原型验证的目的是看信号特征,不是生产医疗设备。

4.4 传输协议设计中的粘包与粘帧处理

最后聊一下数据帧解析的问题。TCP是流式协议,本身没有消息边界,所以BW16发过来的数据帧在ESP32-CYD收到的可能不是一帧一个包,而是两帧粘在一起或者半帧到达。如果直接按固定长度切包,大概率会切错位置,导致整条数据流错乱。

我的处理方法是状态机解析:接收字节流逐字节推进,先找0xA5和0x5A的帧头,然后读到110字节后校验和,校验通过就提取一帧数据;如果校验失败,回到找帧头的状态重新开始。这样即使中间丢了一个字节,也能在下一个帧头处重新同步,不会伤及整条链路。

// 状态机解析数据帧 enum FrameState { WAIT_HEADER1, WAIT_HEADER2, READ_DATA, VERIFY }; void processByte(uint8_t byte) { switch (state) { case WAIT_HEADER1: if (byte == 0xA5) state = WAIT_HEADER2; break; case WAIT_HEADER2: if (byte == 0x5A) { state = READ_DATA; crc = 0xA5 + 0x5A; } else state = WAIT_HEADER1; break; case READ_DATA: buffer[bufIndex++] = byte; crc += byte; if (bufIndex >= 106) state = VERIFY; break; case VERIFY: if (byte == (crc & 0xFF)) { // 一帧有效数据,送入处理流程 processFrame(buffer); } state = WAIT_HEADER1; bufIndex = 0; break; } }

4.5 机缘巧合下的“意外发现”

其实做这个项目还有一个意外收获:当采样率降到100Hz左右的时候,屏幕和网页上的波形反而更“稳”,原因是脑电本身的有效带宽就在0.5到100Hz之间,高采样率下混入的高频噪声被显示端重新采样时会产生混叠效应。后来我把采样率定在250Hz,但在发送前做了抗混叠滤波,问题就解决了。

另外一个隐藏的坑是浮点数精度。BW16和ESP32的ADC都是16位,但如果你在两端用了浮点数来做归一化或缩放,多次累加误差会在画波形时显示成肉眼可见的“台阶”。解决办法是全程用整数运算,只在最终画点时转一次浮点,效果会好很多。

5. 项目复盘与后续扩展可能

这个项目做下来,最大的体会是:脑电原型链路的技术门槛不在于硬件连接,而在于对信号链路的整体理解和噪声预算分配。很多人一开始盯着屏幕看波形“怎么才能更干净”,但真正有用的思维方式是从每个环节去算噪声贡献量——电极接触阻抗带来的热噪声是固定的,模拟放大器的输入噪声是可以查数据手册的,ADC量化噪声是可以算出来的。把这些加起来,你会发现可用的信号裕量其实非常有限,每一步都要省着花。

后续我考虑在这个链路上再加两个方向:一个是在ESP32-CYD上跑一个简单的注意力指数算法,把脑电特征值转换成Bluetooth到手机端显示,这样就能做一个真无线可穿戴注目仪的原型;另一个方向是加一个SD卡存储模块,把原始脑电数据和事件标记一起录下来,方便离线跑更复杂的分类模型。说实话,这个项目一开始只是个“能不能搭起来”的验证,但做完之后,我意识到它的价值远不止于此——这是一个低成本、高可定制、同时覆盖采集、传输、信号处理和可视化的完整参考框架,对任何一个想做脑电相关应用的人来说,都值得在这条链路上反复打磨。

最后再分享一个小技巧:如果你也想复现这个链路,建议先把模拟前端单独接示波器,用函数发生器输入一个10μV、8Hz的正弦波来验证信号链放大倍率和滤波响应,确认没问题之后再接BW16做无线传输。一步步拆开验证,比整体联调出了问题再回头排查要省事太多。

返回列表