ARTICLE DETAIL

资讯详情

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

BW16与ESP32-CYD搭建无线EEG原型链路:从脑电模块到屏幕网页

BW16与ESP32-CYD搭建无线EEG原型链路:从脑电模块到屏幕网页 从脑电模块到屏幕和网页我用 BW16 与 ESP32-CYD 搭建了一条无线 EEG 原型链路玩嵌入式的人多少都有过这种念头把生物电信号搬到自己的屏幕上看看。EEG脑电一直是很吸引人的方向但门槛往往卡在“怎么把模块输出的数据真正变成能看、能分析的东西”。市面上不少脑电模块本身不复杂串口一接就能出数据麻烦的是后续链路要无线、要实时、要可视化最好还能在手机浏览器里看波形。我这次直接用手头的 BW16 和 ESP32-CYD 搭了一条完整链路从脑电模块的 TTL 串口输出经过 BW16 无线转发再到 ESP32-CYD 的屏幕实时绘制波形同时把数据推送到局域网内的网页端显示。整套东西跑起来之后基本就是一个小型 EEG 原型工作台适合做信号采集验证、算法调试或者单纯拿来折腾可视化。这篇就把整条链路的选型思路、关键代码逻辑、实测踩坑记录都捋一遍。如果你手里正好有类似的模块或者打算低成本搭一套生物电采集可视化系统可以按着这个路子走。1. 链路拓扑与选型逻辑1.1 为什么不用 ESP32 直接接脑电模块很多人第一反应是既然 ESP32 自带 WiFi为什么不直接让 ESP32 读脑电模块的串口然后一边显示一边推网页我最初也是这么想的但实际搭起来会发现一个尴尬问题ESP32 的串口外设和 WiFi 协议栈在中断、任务调度上的竞争会直接造成采集端的数据丢包。脑电模块一般以 256Hz 或 512Hz 的频率持续往外吐数据每包就几十字节本身不算高负载但只要 WiFi 在传数据串口读取就有概率被“饿”住轻则丢帧重则整个缓冲溢出。把采集和传输拆成两个节点BW16 专职读串口、无线转发ESP32-CYD 专职接收、绘制和提供网页服务逻辑上更干净调试起来也省心。另一个原因是参考电压和隔离问题脑电信号是微伏级别的微弱信号前端模块的电源纹波对信号质量影响很大把采集端和显示端的供电分开反而更容易处理噪声。BW16 是 Realtek RTL8720DN 方案的板子带 WiFi 和 BLE 5串口、SPI、PWM 都齐全做采集转发非常合适。1.2 三节点链路采集、传输、显示整条链路分成三段采集端脑电模块通过干电极采头皮信号内部完成放大、滤波通过 UART TTL 输出原始脑电波形数据和信号质量参数。我用的是单通道模块协议是 NeuroSky TGAM 那一类市面上很多国产模块也兼容这套串口协议选它的主要原因是成本低、上手快不需要自己搭模拟前端。传输端BW16 接脑电模块的 TX 引脚以 57600 波特率读取数据包解析出原始波形值然后打包成自定义帧通过 WiFi 以 TCP 方式发送给局域网内的 ESP32-CYD。BW16 在这里的角色是“采集网关”只负责转发不做复杂处理。显示与服务端ESP32-CYD 开发板ESP32-2432S028R带 2.4 寸电阻触摸屏监听局域网端口接收数据在屏幕上滚动绘制脑电波形同时运行一个 WebSocket 服务把实时数据推给浏览器页面手机、电脑打开同一个局域网内的网页就能看到波形。数据流总结下来就是脑电模块 → UART → BW16 → WiFi/TCP → ESP32-CYD → 屏幕显示 WebSocket → 网页显示。1.3 各节点的接线与供电规划接线部分没有太多悬念。脑电模块的 TX 接 BW16 的 RX模块的 RX 接 BW16 的 TX 用于配置指令下发地线必须共地。供电我踩过一个坑脑电模块和 BW16 共用同一个 5V 电源适配器时波形上会出现明显的周期性干扰后来把脑电模块改为锂电池独立供电BW16 用 3.7V 的 LDO 单独供电波形一下子干净很多。ESP32-CYD 直接用 USB 供电即可它的屏幕背光功耗不小建议用足 5V/2A 的适配器别用电脑 USB 口凑合。整个节点的 IP 规划也很简单三者连接同一个路由器BW16 作为 TCP 客户端主动连接 ESP32-CYD 的监听端口。为什么不用 UDPUDP 丢包在 2.4GHz 环境下其实挺频繁的脑电波形一个点丢了波形上就是一个缺口看着很难受TCP 虽然有点重传开销但数据量本身很小实测完全扛得住。2. EEG 前端与信号准备2.1 单通道脑电模块怎么选市面上的 EEG 前端模块大概分几类集成模拟前端芯片自己做比如 ADS1299、成品模块比如 OpenBCI 系列、消费级单通道模块TGAM 及其兼容品。对原型验证来说我推荐第三种。原因是它内部已经完成了仪表放大、带通滤波和 ADCMCU 这边只需解析串口即可。模块一般会输出三种信息信号质量Signal Quality、原始波形Raw Wave和经过算法处理后的专注度/放松度数值。我的最终目的是看原始波形所以主要抓 Raw Wave 字段。选模块的时候要注意一个参数输出采样率。TGAM 兼容模块的标准输出是 512Hz有的精简版只有 128Hz。128Hz 对普通脑电频段0.5Hz 到 40Hz 左右也够用但波形会明显“粗糙”做后续算法分析时分辨率不够。我实测下来更推荐 512Hz 的型号多出来的点可以在软件里做平均降噪。2.2 串口协议拆解0xAA 0xAA 开头的那串字节这类模块的串口协议是固定格式。每包数据以 0xAA 0xAA 开头后面跟一个字节表示 payload 长度然后是各个数据字段。RAW 波形字段的完整结构是小端 16 位值高字节在前。很多第一次接触的人会在这里翻车——直接把两个字节按大端拼接出来的波形是一堆正负跳变的尖峰。正确做法是先读两个字节按高字节左移 8 位或上低字节然后根据符号位判断正负。注意噪声标记位模块把 bit 当作噪声标志时实际有效波形只有 14 位精度。我在解析时直接把它当普通波形用了波形上偶尔出现毛刺排查半天才发现是头部两个 bit 不能参与数值计算。还有个值得留意的地方模块刚上电或电极接触不良时信号质量字段会长期处于低值原始数据也会一片混乱。最好在代码里加一个简单判断信号质量低于阈值时不转发原始波形只上报状态否则后面的显示端看到的全是噪声。2.3 串口连 BW16 的速率与缓冲问题脑电模块串口默认波特率一般是 57600也有 9600 的配置模式两者别搞混。BW16 的 UART 缓冲只有几十字节如果读取不及时就会溢出。我一开始直接用 Arduino 的 Serial.readBytes 阻塞读取结果 WiFi 一旦重连串口缓冲直接爆掉。后来改成中断接收 环形缓冲区的方案串口中断把字节写入环形队列主循环周期性从队列里解析帧。这样即使 WiFi 短暂卡顿数据也不至于丢得太狠。要注意环形缓冲区大小至少能容纳几十包数据我设了 256 字节实测 512Hz 采样率的 20 包数据约 40ms不会溢出。2.4 去噪为什么必须提前考虑EEG 去噪是个绕不开的话题。原始波形里混合着大量干扰50Hz 工频、低频基线漂移、肌电尖峰、电极移动伪迹。如果你没有在链路里预留去噪环节显示出来的波形基本没法看。我的做法是分层处理前端脑电模块自带硬件滤波能把信号限制在 0.5Hz 到 100Hz 左右。BW16 转发端不做滤波只做简单的高通直流偏置抑制。ESP32-CYD 显示端软件实现 50Hz 陷波和基线矫正。网页端再做一级移动平均平滑。这样处理的初衷是让每一层负担均衡不在 MCU 上堆太多浮点运算避免拖慢 WiFi 服务。3. BW16 无线转发实现细节3.1 BW16 的 Arduino 开发环境配置BW16RTL8720DN在 Arduino IDE 里的支持已经比较成熟板卡管理器添加 Realtek Ameba 的 JSON 地址就能安装。需要注意BW16 有两块板卡配置AmebaD RTL8720DN 是使用的型号烧录时按住板上的烧录键再插 USB波特率默认 1502000别用默认的 115200否则会烧录失败。WiFi 连接代码和 ESP32 类似但库函数略有差异AMebaIoT 库提供的 WiFi 类名是WiFiTCP 客户端用的是TCPClient。别直接用 ESP32 的WiFiClient编译会报错。BW16 较坑的一点是内存很小RAM 只有 512KB但可用堆空间不到 200KB。我的转发程序加上 WiFi 协议栈之后内存占用大概 40KB问题不大但如果要加滤波算法就得掂量一下浮点数组的大小。3.2 数据帧格式设计与防粘包转发端要设计一套干净的帧格式。我的方案是固定长度帧帧头 0xA5紧接着 1 字节数据长度然后是 12 字节的原始波形数据6 个采样点每点 2 字节最后 1 字节校验和。这样接收端只需要按长度缓冲区接收不需要逐字节分析。校验和采用简单累加和可以过滤掉大部分 WiFi 传输错误。实际上 TCP 本身有校验但链路中可能经过路由器转发数据被 TCP 层丢弃前确实有可能出现过坏包加了校验和更稳妥。粘包问题也要注意脑电模块 512Hz 速率下BW16 每约 11.7ms 就要发一包而 TCP 的 Nagle 算法会把多个小包合并成一个 TCP 段发送。如果你直接按 TCP 段接收会发现一次 recv 拿到了好几帧。解决办法有两个一是关闭 Nagle 算法writeNoDelay二是接收端严格按帧头 长度解析。我两个都做了实测更推荐第二种因为 Nagle 算法在局域网内延迟并不明显逐帧解析更可靠。3.3 断线重连与稳定性优化WiFi 模块最烦人的就是断线。我实测 BW16 在长时间传输后偶尔会进入“假死”状态TCP 连接一断就不再重连。解决思路是在主循环里周期性检查client.connected()的状态如果返回 false就释放之前的连接对象重新建立 TCP 连接同时打印重连日志。实测连续跑 6 小时后自动重连过 3 次每次都恢复得很快数据断档不超过 1 秒。另一个稳定性优化是降低对数输出频率。BW16 的 Serial 输出如果太频繁会挤占 CPU 时间片导致串口中断延迟增大进而丢数据。我把调试日志集中到一个标志位控制默认运行状态不输出只在重连或配置阶段打印。3.4 转发端的代码骨架BW16 端的代码大致是这样一个流程#include WiFi.h #include TCPClient.h const char* ssid your_wifi; const char* pass your_password; const char* host 192.168.1.100; // ESP32-CYD 的 IP const uint16_t port 8080; TCPClient client; RingBuffer uartBuf; // 自定义环形缓冲区 void setup() { Serial.begin(57600); // 接脑电模块 Serial2.begin(115200); // 调试日志 WiFi.begin(ssid, pass); while (WiFi.status() ! WL_CONNECTED) delay(20); client.connect(host, port); } void loop() { if (!client.connected()) { client.stop(); delay(50); client.connect(host, port); return; } // 从环形缓冲区解析完整帧并转发 if (uartBuf.available() 16) { uint8_t frame[16]; uartBuf.readBytes(frame, 16); client.write(frame, 16); } }大概示意实际完整代码会多一点但核心就是“读串口、查连接、发数据”三件事。需要提醒的是 BW16 的TCPClient::connected()在某些固件版本下有已知 bug连接刚建立时可能返回 false所以重连之后要延时几百毫秒再执行发送否则会误判断线。4. ESP32-CYD 接收与屏幕绘制4.1 CYD 这块板子到底怎么样ESP32-CYDCheap Yellow Display就是淘宝上常见的 ESP32-2432S028RESP32-WROOM-32 模组加 2.4 寸 ILI9341 屏幕、XPT2046 电阻触摸板载 MicroUSB、音频接口和 TF 卡槽。最大优势是便宜且自带屏做数据可视化非常合适。显示驱动用的是 LovyanGFX 库驱动效率和兼容性比 Adafruit GFX 好不少尤其在做高速波形滚动时优势明显。CYD 的屏幕刷新能力上限大概在 30fps 左右EEG 波形每秒钟 512 个点如果全部画出来一屏只能显示约 0.3 秒的数据。实际上脑电波形频率上限也就是 40Hz采样率 512Hz 纯属数据量冗余。我的做法是在 CYD 端做抽帧显示每 4 个原始点取一个最大值和一个最小值然后在屏幕上画竖线连接既保留波形包络信息又避免过度绘制导致闪烁。4.2 局域网 TCP 监听解析ESP32 端监听 8080 端口接收 BW16 的数据。代码上直接用WiFiServer库即可但要注意绑定端口后接收循环里不要用障碍式读取否则 WebSocket 服务会被卡死。我采用server.available()返回新的客户端然后把客户端数据缓存到队列中主循环里同时处理 TCP 数据、屏幕刷新和 WebSocket 推送。解析逻辑和 BW16 发的帧严格对应读帧头、读长度、读数据、算校验。校验失败直接丢弃整个帧。这里有个实际问题如果接了两个客户端比如 TCP 来了一个WebSocket 来了一个要区分数据来源避免把 WebSocket 客户端的握手数据当脑电数据解析。我的办法是给 TCP 客户端一个独立的文件描述符判断WebSocket 客户端统一用另一个类管理。4.3 波形绘制滚动窗口、自动增益与网格屏幕绘制这块我踩了几个值得说的坑。第一波形滚动要用“整体左移 右边缘画新点”的方式而不是每帧清屏重画。LovyanGFX的pushImage可以直接把屏幕内容左移然后只在最右侧画新数据实测刷新率能到 25fps性能比整体重绘高一个量级。第二自动增益问题。脑电原始波形值大概是 ±150 左右模块内部 ADC 有硬件放大但不同电极接触质量、不同人的头皮阻抗差异很大固定缩放比例会出现波形太小或顶出屏幕的情况。我加了一个简单自动增益维护一个滑动窗口最大值目标是把窗口内最大值映射到屏幕高度的 80%每次更新增益后平滑调整避免跳变。第三叠加网格和标注信息。屏幕底部画时间刻度右上方显示实时信号质量值和采样率方便判断链路状态。网格线用虚线绘制颜色用深灰避免和波形本身混淆。波形颜色我用亮绿色在黄色屏幕背光下对比度很高。4.4 软件去噪50Hz 陷波与基线矫正CYD 上跑滤波算法说实话计算量比想象中小。50Hz 陷波滤波器我用双二阶 IIR 实现公式直接按标准的 Direct Form I 结构写// 采样率 512Hz陷波 50HzQ30 float b0 1.0f, b1 -1.99407f, b2 1.0f; float a1 -1.99407f, a2 0.99404f; float x1 0, x2 0, y1 0, y2 0; float filter(float in) { float out b0 * in b1 * x1 b2 * x2 - a1 * y1 - a2 * y2; x2 x1; x1 in; y2 y1; y1 out; return out; }参数按采样率算出来的PCB 上没有实际测试工频干扰之前未必知道是 50Hz 还是 60Hz国内一般用 50Hz。滤波后波形上明显的锯齿状工频干扰几乎消失但高频的肌电尖峰和低频基线漂移还在。基线漂移我用一阶高通滤波器处理截止频率大概 0.5Hz这样波形会稳定在屏幕中央不会慢慢往上漂。我特别提醒一点滤波器的系数精度很重要直接用单精度浮点算如果系数没有及时更新可能在特定频段产生共振反而放大噪声。写滤波器的过程中最好先在电脑上用 MATLAB 或 Python 模拟一遍确认系数稳定再移植到板上。4.5 显示与转发的并行处理CYD 同时做接收、滤波、绘制、WebSocket 推送四件事的调度顺序有讲究。我按优先级排接收数据处理 滤波计算 屏幕绘制 WebSocket 推送。因为 WiFi 中断和数据接收是最不能丢的滤波计算是确定性操作绘制可以丢帧WebSocket 推送如果暂时卡顿浏览器端会积压缓冲影响实时性但不会丢数据。实测下来把 WebSocket 推送放在主循环的末尾客户端很少出现明显延迟。如果感觉 WebSocket 缓冲积压严重可以在前端做“按时间戳丢弃旧数据”只保留最近 1 秒的数据保证画面流畅。5. 从屏幕到网页浏览器里的 EEG 可视化5.1 为什么非要加一个网页端屏幕显示模式适合现场调试但对远程观察和数据分析来说不够。网页端一旦跑通你就可以坐在桌子对面用手机或者另一台电脑独立查看波形完全不占用 CYD 的物理屏幕。更重要的是浏览器里可以做更复杂的处理——比如 Canvas 绘制、FFT 频谱、记录数据文件这些在 MCU 上做起来费劲。我的方案是把 CYD 当成一个小型 WebSocket 服务器浏览器订阅数据流所有处理逻辑放在网页的 JavaScript 里。5.2 CYD 上的 WebSocket 服务实现ESP32 上做 WebSocket 一般直接用WebSocketsServer库。CYD 的 WiFi 连接建立后调用webSocket.begin()然后在webSocket.loop()里周期处理握手和消息。数据推送时把已经解析好的脑电波形打包成 JSON 数组发送给所有连接的客户端。JSON 格式的字段我设计成{ t: 123456, q: 76, s: 512, d: [12, 23, -4, -10, ...] }t是时间戳q是信号质量s是采样率d是原始波形的数组一次推送 10 个采样点。这样浏览器端每收到一条消息就追加 10 个点到画布上刷新率取决于数据频率。JSON 解析的注意点是ESP32 上发送 JSON 字符串要小心内存碎片我建议用DynamicJsonDocument设一个固定大小的缓冲区d数组不要超过 16 个整数否则内存不够会解析失败。更好的做法是直接发二进制帧浏览器端用ArrayBuffer解析效率高得多但代码复杂度也高一截。5.3 前端 Canvas 波形渲染与平滑网页端我用原生 Canvas 2D API 绘制。核心逻辑是一个滚动缓冲区维护最近约 3 秒的波形数据然后按屏幕宽度等比例映射到画布坐标。为了避免浏览器端绘制频率过高导致 CPU 占用爆表我用requestAnimationFrame控制刷新每秒最多更新 30 帧数据来了就存入缓冲区绘制时只取缓冲区尾部数据。网页端做了一层移动平均平滑窗口大小为 5因为浏览器端做滤波几乎零成本稍微平滑一下视觉观感提升很大。如果要在网页端做频谱显示可以直接用分段 FFT比如每 256 个点做一次在 Canvas 上画出频域柱状图这个效果展示起来非常酷。前端代码结构大致是!DOCTYPE html html head meta charsetUTF-8 titleEEG Wave Monitor/title /head body canvas idwave width800 height300/canvas script const ws new WebSocket(ws://192.168.1.100:81/); ws.onmessage (e) { const data JSON.parse(e.data); // 处理数据并绘制 }; /script /body /html5.4 手机浏览器联动实测移动端和桌面端本质上没有区别同一套 WebSocket 协议都能连上。只是手机屏幕尺寸小需要自适应画布大小我直接用了 CSSwidth: 100%配合 Canvas 的resize事件。实测手机连接后延迟大约 80ms 到 150ms主要开销在 WebSocket 打包和浏览器渲染肉眼看起来几乎没有滞后。如果是在公网上访问就得考虑 NAT 穿透和路由器端口映射的问题这个和项目本身关系不大了局域网内够用。6. 实测过程中的常见问题与排查技巧6.1 问题速查表现象可能原因处理方法波形全是高频毛刺工频干扰或电极接触不良加 50Hz 陷波重新佩戴电极检查地线基线缓慢上下漂移电极与皮肤相对运动加高通滤波0.5Hz贴紧电极波形整体偏小信号质量低或增益不够检查电极阻抗增大自动增益上限屏幕波形偶发缺口TCP 丢包或 BW16 缓冲溢出检查 WiFi 信号强度增大 BW16 环形缓冲区WebSocket 网页白屏浏览器跨域限制在 CYD 端添加 CORS 头或改为同源访问CYD 重启供电不足换 5V/2A 适配器减小屏幕背光亮度BW16 烧录失败波特率错误或没进入烧录模式使用 1502000 波特率先按住烧录键再插 USB6.2 串口噪声与电源纹波的坑这是整个项目里让我花时间最多的地方。脑电模块的传感器输出是微伏级别电源上的 100Hz 纹波会被放大后混进信号。排查时用示波器挂了模块电源发现接 USB 时纹波大约 50mV 峰峰值这已经足够干扰信号。换成锂电独立供电后纹波几乎降到零波形立刻干净了。如果你手头没有专门的 EEG 模块用普通运放电路的话电源处理更是重中之重建议所有模拟电路部分都用 LDO 加 π 型滤波。6.3 2.4GHz 干扰与无线稳定性在办公室环境里2.4GHz 频段干扰非常严重蓝牙、无线鼠标、手机热点的信道互相挤占。BW16 和路由器距离稍远的时候TCP 重传明显增多数据延迟从几十毫秒跳到 300 毫秒以上。我最后的解决办法是把路由器切换到 5GHz 频段同时让 BW16 固定在 2.4GHz 低信道比如 1 信道降低共存干扰ESP32-CYD 同样连接到 2.4GHz。如果路由器不支持双频至少把周围 WiFi 设备的信道错开。实测切换后丢包率从 5% 降到 0.1% 以下。6.4 干电极接触阻抗的教条干电极的优势是方便缺点是对接触状态非常敏感。头油多、头发厚、电极没按紧信号质量就会骤降。我的经验是佩戴前用酒精棉擦一下额头对应位置的皮肤等酒精干了再贴电极这个小动作能把信号质量从 30% 直接提到 90% 以上。另外模块附带的参考电极和接地电极也别偷懒必须都接触皮肤否则共模干扰会直接灌进来。6.5 数据记录与离线分析建议链路跑通之后很自然会想保存数据做离线分析。我的做法是在网页端加一个“录制”按钮用 JavaScript 每收到一条消息就把数据追加到 Blob点击停止后下载 CSV 文件。CSV 的格式很简单时间戳、信号质量、采样值。这样做的优势是不占用 ESP32 的 Flash数据分析完全在电脑端完成。如果想在 ESP32 端记录CYD 有 TF 卡槽可以定时把数据写入 SD 卡但写卡时的延迟会影响实时绘制建议用独立核心或降低采集节流。整个系统搭完之后心里最大的感受是好工具不是一次到位的而是边用边调出来的。最开始屏幕波形锯齿明显加陷波后平滑了但基线漂移又漏出来了把高通滤波器调好之后又发现无线偶尔断线折腾完重连逻辑又在网页端加了平滑。每一步都是在实际跑数据、盯波形、翻代码的过程中逐步完善的。如果你打算复刻这套链路我的建议是先跑通最小闭环模块—BW16—CYD 屏幕再逐步加入网页端和滤波算法别指望一步到位。后面我打算在这个基础上加一个简单的专注度算法把 eSense 数值映射成一个 LED 呼吸灯效果还可以顺便做一个实时 FFT 频谱图让这套原型从“能看”变成“能用”。
返回列表