做 EEG 原型最讨厌的就是那一束线传感器到主控、主控到电脑、中间还拖着电源线人稍微一动数据就全花了。我当时想把脑电模块的数据真正“甩”到空中去于是用手头两块板子搭了一条无线链路——BW16 负责从脑电模块读原始信号通过 WiFi 发出去ESP32-CYD 负责接收、在屏幕上画波形同时开一个网页让浏览器也能看到实时曲线。这条链路不算什么高精尖工程但把细节掰开看确实能帮不少做可穿戴原型的朋友少走弯路。如果你手里正好有便宜的脑电模块和一两块带 WiFi 的开发板跟着这条思路走完全能搭出一套属于自己的无线 EEG 采集前端。1. 为什么要无线化这条链路解决的核心问题1.1 有线 EEG 原型的几个痛点大部分入门级的脑电模块比如常见的单通道 NeuroSky 类模块都是通过串口直接往电脑发数据的。这听起来很简单实际做原型的时候问题一堆。首先是你得有一根很长的 USB 线或者串口线拖着实验者坐在电脑前面基本不敢乱动稍微转头、起身线缆一扯电极就松了脑电信号瞬间变成一堆毛刺。更麻烦的是电脑本身是个巨大的噪声源开关电源、USB 供电的地环路噪声经常让 ADS1299 这类精密模拟前端都压不住更别说消费级模块了。其次是数据展示问题。串口数据到了电脑之后如果你想边采集边看波形需要写上位机或者用 Processing、Python 的绘图库。调试过程中频繁开关串口数据丢失一点也不容易察觉到。你真正想做的可能是把模块放在帽子里、别在腰间、戴在头上然后在一个独立小屏幕上看看此刻的波形或者用手机浏览器远程瞄一眼。这些场景有线方案全都做不到。还有一个被低估的问题串口线会限制你后续做闭环实验。比如你想用脑电信号控制一个小风扇或者 LED 阵列如果数据必须先跑到电脑再发指令整套系统延迟大、部署笨重。真正的可穿戴原型应该是一个“能自己跑通”的设备至少要在本地完成数据采集、显示和基本的阈值判断。1.2 “本地屏 局域网网页”双通道方案的定位所以我当时给自己定的目标是做一个“无线 EEG 数据采集中转站”。脑电模块通过串口把数据交给 BW16BW16 用 WiFi UDP 把数据包广播到局域网ESP32-CYD 收到数据后一块 2.8 寸屏幕实时显示波形同时在这个板子上挂一个微型 Web 服务器浏览器通过 WebSocket 拉取同样的数据流。这样实验时我不需要开电脑只需盯着 ESP32-CYD 上的小屏如果想把波形录下来或者做进一步分析用笔记本打开网页就能看到数据。这个定位很适合原型阶段。它不追求医疗级的多通道精度也不做复杂的算法处理核心是把“数据采集 - 无线传输 - 多端显示”这条链路彻底打通。有了这条链路后面你换更好的脑电模块、加滤波算法、做源定位分析甚至把它改成蓝牙版本都是水到渠成的事。2. 硬件选型与数据流设计2.1 脑电模块选择与数据协议市面上能用 USB 转串口读原始数据的模块并不算多大多数消费级产品只给你一个封装好的“专注度”数值没法看到原始脑电波形。我当时选模块的原则很简单必须能直接输出原始 EEG 波形Raw Wave且串口数据格式公开可解析。于是选了一个和 NeuroSky TGAM 类似、通过 UART 输出 512Hz 原始波数据的单通道模块。这类模块的串口参数通常是 57600 波特率、8 位数据、无校验、1 位停止位。一包数据大致结构是帧头0xAA 0xAA接着一个字节表示负载长度然后是信号质量值、8 个频段能量参数Delta/Theta/Alpha 等、两字节的原始 EEG 值小端序最后是校验和。原始值是 16 位有符号数范围大概在-32768到32767之间但实际有用的摆幅通常只有几百到几千需要根据电极贴得紧不紧来动态缩放。你可能不需要纠结具体是哪家芯片只要记住一点你的脑电模块只要能输出串口原始数据后面这套链路就完全通用。如果模块只输出“专注度”这类组合指标也可以接到链路上只是波形显示会变成柱状图或者数值。2.2 为什么选 BW16 当无线发送端先说我为什么不用 ESP32 直接采集。ESP32 当然也能读串口、发 WiFi但它一个板子干完所有事看似更简单实际会陷入“既要采集又要在屏幕画波形又要跑 HTTP 服务”的泥潭。多任务一多无线 buffering 稍微卡一下波形就卡成 PPT。所以我坚持把“无线发送”这个职责单独拆给 BW16。BW16 这颗芯片是瑞昱的 RTL8720DN双频 WiFi2.4G/5G BLE 5.0价格却非常便宜甚至比很多 ESP8266 模组还便宜。它最突出的优点是功耗控制比 ESP 系列好对电池供电的可穿戴场景更友好。另一个优点是串口资源够用我可以把日志串口和通信串口分开调试起来不会互相干扰。当时我选择 BW16 的第三个原因其实有点现实模组自带 PCB 天线不需要额外焊接外置天线在原型板上好摆放。当然用 ESP32 当发送端也不是不行。如果你手头正好有 ESP32 而且不打算显示波形完全可以把它做成一个“串口转 WiFi UDP”的透明透传盒子。我这套方案的灵魂是“分工明确”采集发送归 BW16显示归 ESP32-CYD互不抢资源。对比项BW16 方案ESP32 方案待机功耗更低适合电池可穿戴较高便携性略差双频 WiFi支持 5G干扰少多数仅 2.4G串口数量足够拆分日志与数据也足够但占引脚多开发环境Arduino 兼容有坑Arduino/ESP-IDF 均成熟2.3 为什么接收端是 ESP32-CYDESP32-CYD 在国内有个更直白的名字黄屏开发板ESP32-2432S028R。它本质上是一个 ESP32 模块加一块 2.8 寸 ILI9341 触摸屏的整合板公板上甚至直接给你焊好了 TF 卡槽、RGB LED 和几个按键。这个板子最离谱的地方是便宜但给的东西非常多做显示类原型非常合适。选 CYD 的另一个原因是屏幕驱动非常成熟。TFT_eSPI 这个库对 ILI9341 的支持很完善画曲线、局部刷新都很方便。而且这块板子的供电路径比较清晰5V 输入经过板载稳压管给 ESP32 和屏幕供电驱动电路不会和后端 WiFi 模块抢电。我后来在原型中直接把 CYD 插在面包板旁边的排针上用它自带的 USB 口供电就够跑整套系统。CYD 跑网页服务的角色其实很关键。你不需要另外准备树莓派或者电脑来出网页直接在 CYD 上用 ESPAsyncWebServer 挂一个 WebSocket就能把实时波形推给局域网里任意一个浏览器。这大大降低了整个原型的硬件清单。2.4 整体链路与自定义数据帧格式把方案拆开看数据流是这样走的脑电模块以 57600 波特率把原始数据包发给 BW16 的串口。BW16 在收到整包数据后做轻量校验和平滑重新封装成自定义 UDP 帧。BW16 通过 WiFi 将 UDP 帧发往局域网内的指定 IP/端口即 ESP32-CYD。ESP32-CYD 的 UDP 套接字收到帧后把原始 EEG 值写入环形缓冲区。CYD 主循环每 20ms 取一批数据用 TFT 屏幕绘制实时波形。同时 WebSocket 服务器每 50ms 向已连接的网页客户端推送一批数据点浏览器用 Canvas 绘制波形。这里我强烈建议不要直接把模块原封不动的包发给接收端。原串口包虽然包含频段能量但结构散、校验弱直接转发到局域网容易因一两个误码导致波形跳变。我在自定义帧里加了一个序号字段来检测丢包每包携带的样本数也固定方便接收端做对齐。自定义 UDP 帧格式很简单字节偏移字段长度说明0帧头1固定0xA51-2包序号2小端自增用于检测丢包3-6时间戳4毫秒时间戳便于网页端 x 轴对齐7样本数1当前包内含 EEG 点数固定 328-71原始数据6432 个 int16 小端排列72校验和1前面所有字节累加取低 8 位一个包总共 73 字节。EEG 模块是 512Hz 采样32 个点一包意味着每秒约 16 包有效数据吞吐量只有几百字节每秒局域网传输毫无压力。3. 硬件接线与信号完整性3.1 引脚连接与电平匹配脑电模块到 BW16 的接线不能想当然。很多脑电模块虽然是 3.3V 逻辑但有些评估板自带 5V 电平转换或者直接输出 5V 串口信号。如果直接把 5V 信号接到 BW16 的 RX 引脚轻则数据全是乱码重则把 GPIO 烧掉。我的建议是先查模块手册再用万用表量一下空闲状态下的 TX 电平确认是 3.3V 再连接。典型的连接方式如下以我手上的模块为例设备端引脚连接到脑电模块TXBW16 的 UART RX例如 PA1脑电模块GNDBW16 的 GND脑电模块VCC3.3V 或 5V按模块要求BW16TX调试日志USB 串口模块 RX仅调试用注意脑电模块和 BW16 必须共地。如果不共地串口信号的电平参考点不一致偶尔能收几个字节但大多数时候会被噪声和电压差干扰成废包。我踩过几次坑之后干脆把所有模块的 GND 都汇总到一个公共焊盘上实测数据稳定很多。BW16 和 ESP32-CYD 之间没有直接物理连线它们是通过 WiFi 通信的。实际项目里很多人问“要不要把 BW16 的 TX 接到 ESP32 的 RX”答案是不需要除非你想加一条有线备份链路。3.2 电源、地线与抗干扰脑电信号是微伏级信号最怕的是 50Hz 工频干扰和电源纹波。我一开始图省事用同一个 5V USB 充电器同时给脑电模块、BW16、CYD 供电结果屏幕上直接出现了明显的 50Hz 波浪。后来我做了两件事一是把脑电模块单独用一节 18650 电池供电经过 3.3V LDO二是把电极线全部用屏蔽线屏蔽层单端接地。还有一个容易忽略的细节ESP32-CYD 的屏幕 PWM 背光会产生高频开关噪声。当 CYD 和脑电模块距离太近、共用一个电源轨时背光噪声会通过电源耦合到脑电采集端。解决办法是让 CYD 和脑电模块之间至少隔 20cm或者给脑电模块的电源入口加一个 10uF 钽电容和 100nF 陶瓷电容去耦。我最后在实际测试中是这样的布局脑电模块和电极线放在一个小盒子里盒子靠近头顶BW16 用杜邦线挂在脑电模块旁边CYD 放在桌面上当显示器。整个系统用一节手机充电宝供电充电宝的 USB 线同时给 CYD 和 BW16 供电脑电模块用独立的 18650 电池。这一套下来波形虽然比不上实验室设备但至少能清楚分辨出眨眼伪迹和 Alpha 节律的起伏。4. 软件实现发送端到接收端4.1 BW16 端串口解析与 UDP 转发BW16 在 Arduino 环境下开发安装好 RTL8720 的支持包之后写法和 ESP32 很接近。但有一个坑BW16 的常规 Arduino 核心暴露给用户的串口资源有限不能默认用Serial1去接设备。我实际是在初始化里把日志输出和通信串口分开用模块自带的 UART 引脚去读脑电数据日志仍然走 USB 虚拟串口。这里给一段核心逻辑作为示意#include WiFi.h #include WiFiUdp.h const char* ssid your_ap_ssid; const char* password your_ap_password; IPAddress remoteIP(192, 168, 1, 100); // ESP32-CYD 的固定 IP const uint16_t remotePort 8888; WiFiUDP Udp; // 串口接收缓存 uint8_t rxBuf[64]; uint8_t rxLen 0; uint16_t seq 0; void parseAndSendEEGFrame(uint8_t* data, uint8_t len) { // 这里简单示意直接取原始数据中的第 15-16 字节作为原始 EEG 值 int16_t eegSample (int16_t)((data[15] 8) | data[16]); uint8_t pkt[73]; pkt[0] 0xA5; pkt[1] seq 0xFF; pkt[2] (seq 8) 0xFF; uint32_t ts millis(); memcpy(pkt[3], ts, 4); pkt[7] 1; // 调试用这里只发一个点 memcpy(pkt[8], eegSample, 2); uint8_t sum 0; for (int i 0; i 10; i) sum pkt[i]; pkt[10] sum; Udp.beginPacket(remoteIP, remotePort); Udp.write(pkt, 11); Udp.endPacket(); seq; } void loop() { while (Serial.available()) { rxBuf[rxLen] Serial.read(); if (rxBuf[0] ! 0xAA) { rxLen 1; continue; } if (rxLen 3 rxBuf[1] ! 0xAA) { rxLen 1; continue; } if (rxLen 4 rxBuf[2] 2) { rxLen 1; continue; } // 这里为了演示简化成固定长度判断 if (rxLen 18) { parseAndSendEEGFrame(rxBuf, rxLen); rxLen 0; } } }真实项目里我会把解析函数写的更健壮比如检查校验和并且一次包积累足够 32 个点才发送。但上面这段代码的核心思路已经能说明问题把串口字节流变成一个个带序号的 UDP 包剩下的交给接收端。有一点要注意BW16 的 WiFibegin之后要等待连接成功不能立刻发 UDP否则前几包会丢。我在 setup 里加了一个超时循环最多等 10 秒。4.2 ESP32-CYD 端接收与双缓冲波形绘制CYD 这边的代码要多一点但也可以拆成几块。一是 UDP 接收解析二是环形缓冲区三是屏幕绘制四是 WebSocket 服务。我建议把它写成四个独立模块避免全部塞在loop()里。环形缓冲区是整个接收端的核心。我用了一个uint16_t数组存最近 512 个采样点类似一个滚动窗口。UDP 回调只要负责把新数据写进环主循环画波形时再统一读这样能避免 WiFi 中断和 TFT 刷新互相抢任务。代码骨架如下#include WiFi.h #include WiFiUdp.h #include TFT_eSPI.h #include ESPAsyncWebServer.h #include AsyncTCP.h TFT_eSPI tft TFT_eSPI(); WiFiUDP udp; AsyncWebServer server(80); AsyncWebSocket ws(/ws); #define BUF_SIZE 512 int16_t sampleBuf[BUF_SIZE]; volatile int writeIndex 0; void onUdpPacket(WiFiUDP udp) { uint8_t pkt[73]; int len udp.parsePacket(); if (len 0 || len 73) return; udp.read(pkt, len); if (pkt[0] ! 0xA5) return; uint8_t sampleCount pkt[7]; for (int i 0; i sampleCount i 32; i) { int16_t s; memcpy(s, pkt[8 i * 2], 2); sampleBuf[writeIndex] s; writeIndex (writeIndex 1) % BUF_SIZE; } } void setup() { WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) delay(200); udp.begin(8888); udp.onPacket(onUdpPacket); // websocket 和屏幕初始化省略 }屏幕绘制我强烈建议不要用fillScreen(黑色)再逐点画线那样会闪得厉害。正确做法是维护一个离线画布TFT_eSprite每次只在画布上画最新的一帧曲线然后通过pushSprite(0, 0)一次性推送到屏幕。屏幕分辨率是 320 像素宽所以我在每轮绘制时把 512 个采样点映射到 320 个 x 坐标取均值或最大值再乘以缩放系数映射到 180 像素高的绘图区域。波形缩放是另一个容易出错的地方。EEG 原始波动的幅度在不同人、不同电极位置下差异极大固定增益会导致有的人波形平直线有的人直接超出屏幕。我在代码里做了“自动增益”每次刷新时统计当前窗口内最大最小值的差然后把这个差缩放到绘图区高度同时记录一个缩放因子方便在触屏上手动微调。4.3 网页端实时显示网页部分使用了 ESPAsyncWebServer 库。这个库和 AsyncTCP 配合在 ESP32 上兼容性比较好。服务端主要做两件事GET/返回一个包含 Canvas 的 HTML 页面WebSocket/ws周期推送二进制波形数据。我写页面时尽量精简只保留一个波形画布和几个状态文字。浏览器端逻辑如下canvas ideegCanvas width600 height240/canvas script const canvas document.getElementById(eegCanvas); const ctx canvas.getContext(2d); const ws new WebSocket(ws:// location.host /ws); let points []; ws.binaryType arraybuffer; ws.onmessage (e) { const view new Int16Array(e.data); points points.concat(Array.from(view)).slice(-512); drawWave(); }; function drawWave() { ctx.clearRect(0, 0, 600, 240); // 缩放后逐段画线 } /script网页端其实不需要跟着每一帧都推送我在 ESP32 端做了降频每 50ms 只发送 32 个采样点约 256 字节。浏览器收到数据后直接拼进本地数组画图时再做一次下采样保证 60fps 的流畅度。实测中局域网里手机浏览器和电脑浏览器都能在 100ms 内收到数据视觉上基本感觉不到延迟。这里要提醒一句ESPAsyncWebServer 的 WebSocket 是异步的不要在 WebSocket 回调里做耗时操作更不要在回调里去刷新 TFT。我就犯过这个错误结果 CYD 一接浏览器屏幕画面就卡住。后来我把所有波形推送数据都在主循环里提前打包好WebSocket 回调只负责发出去。4.4 传输可靠性与丢包处理UDP 在局域网内丢包率很低但并不是零。ESP32 和 BW16 都连同一个路由器时如果中间隔了几堵墙或者旁边有微波炉在疯狂干扰 2.4GHz偶尔就会丢一个包。我在接收端做了丢包检测每收到一个新包时检查序号和上一个包的序号差。如果差值大于 1就说明中间丢了几个包我会在环形缓冲区里补一段线性插值避免波形图出现断裂。这里多提一句WiFi 默认工作在 2.4GHz 时干扰确实多。BW16 支持 5GHz如果有条件把 ESP32-CYD 和 BW16 都连到 5GHz SSID 上UDP 丢包几乎肉眼不可见。CYD 本身支持 2.4G 和 5G 双频吗并不是所有 CYD 都带 5G 射频实际要看板载模块。我的 CYD 版本只支持 2.4GHz所以后来只好给项目单独开了一个 5GHz 热点给 BW16 用CYD 仍然连 2.4GHz测试效果也还行。5. 调试记录与排坑速查表5.1 串口波特率不匹配导致满屏乱码第一次上电我发现 BW16 收到的 EEG 数据在串口监视器里全是乱码像是0xFF 0x00 0xAA这样的字节流。当时第一反应以为模块坏了最后才发现是脑电模块默认 57600而我 BW16 上初始化写成 115200。这个问题在接任何旧模块时都会遇到排查思路很直接先把脑电模块 TX 单独接到 USB 串口工具上用各种常见波特率扫一遍确认能解出AA AA帧头再对接 BW16。5.2 UDP 丢包严重怎么排查当电脑浏览器上看到波形频繁断裂先用ping 192.168.1.100 -t看局域网延迟。正常情况下延迟应该稳定在 1ms 到 3ms。如果延迟抖动很大说明 WiFi 环境不行。这时候可以先把 CYD 放到路由器旁边测试如果丢包明显减少就是射频距离/遮挡问题如果仍然丢包检查是否开了省电模式很多 WiFi 芯片的省电模式会让 UDP 缓冲被延迟发送。5.3 屏幕闪烁与刷新撕裂CYD 的 TFT_eSPI 默认 SPI 频率可以调到 40MHz太高反而容易出横纹。我把SPI_FREQUENCY设成 27MHz 后屏幕稳定多了。绘制波形时使用TFT_eSprite离屏绘制能有效避免闪烁。另外不要在drawWave()里频繁调用fillScreen正确做法是在离屏画布上先用背景色覆盖整个绘图区再重新画曲线最后一次pushSprite。5.4 WebSocket 网页端卡顿浏览器卡顿不一定是后端不行很可能是 JavaScript 端每帧画太多点。我一开始把 512 个点全画Canvas 每次重绘开销太大。后来改成在drawWave()里只画 300 个点并且只在requestAnimationFrame里调用一旦积压数据就丢弃旧点保证 UI 线程不阻塞。实测下来手机浏览器也能跑到 30fps 以上。这里整理一张速查表方便以后回查现象可能原因解决方案串口乱码波特率不匹配用 USB 串口工具扫描确认模块波特率波形断裂UDP 丢包检查 WiFi 距离、关闭省电、启用 5G屏幕闪烁全屏重绘使用 TFT_eSprite 离屏绘制网页卡顿JS 绘制点数太多降频发送、降低绘制点数50Hz 波浪明显电源共地/电极线屏蔽差单独供电屏蔽线单端接地数据只有一半模块信号质量问题重新贴电极检查参考电极接触5.5 一个容易被忽略的坑BW16 的固件默认波特率很多 BW16 开发板出厂固件默认波特率是 115200用于 AT 指令。如果你在 Arduino 里刷了自己写的程序它仍然会继承 bootloader 里的某些设置有时会导致第一次串口打印乱码。解决办法是在setup()里多延迟 500ms然后清空串口缓冲区再初始化日志串口。这不是大问题但第一次遇到时容易以为程序没刷进去。6. 从原型到应用数据能用来做什么6.1 脑电数据的基本预处理这条链路已经帮你解决了“把脑电数据弄到屏幕和网页”的问题但屏幕上显示的原始波形其实还很难直接使用。原始 EEG 里混着大量噪声包括眨眼、肌电、工频干扰和电极移动伪迹。想要进一步分析先把数据离线存下来然后做带通滤波比如 0.5Hz 到 40Hz 的 FIR 带通滤波能去掉一部分直流漂移和高频肌电干扰。滤波可以在 PC 上做也可以在 ESP32 端做但要清楚 ESP32 的算力有限。我在原型里只在接收端做了一个简单的 50Hz 陷波效果一般。真正精细的预处理还是把 UDP 数据包保存成 CSV再丢给 Python 做。整条链路提供了非常干净的数据接口这是它最大的价值。6.2 从波形显示到源定位的可能性如果你将来升级成多通道 EEG 模块比如 8 通道或者 32 通道这条无线链路的架构完全可以直接扩展。只需要把 BW16 的数据帧改成多通道打包ESP32-CYD 端按通道号解析即可。数据量上32 通道、512Hz、每样本 2 字节也就是每秒 32KBWiFi 依然毫无压力。多通道数据收集到一定程度就可以做更高级的分析比如脑电源定位。源定位常用的最小范数估计MNE需要非常干净的 EEG 信号、精确的电极位置和头模型不是消费级模块能直接搞定的。但如果只是把自己的单通道数据并到几个固定电极位置上去做粗略的注意力趋势分析这套系统已经够用了。我后来把保存下来的原始数据离线跑了一遍带通滤波和功率谱分析Alpha 频段的变化趋势能看出来这对接下来的闭环实验很有帮助。7. 一些实际操作体会如果你准备照着这个思路搭我给三个建议。第一一定先把串口数据协议跑通再搞 WiFi别同时上两大块难题否则排查起来很崩溃。第二BW16 和 ESP32-CYD 的 IP 最好在路由器里做 DHCP 静态绑定否则每次重启地址变了网页书签和数据接收都要重配。第三脑电模块的电极贴片非常影响信号质量调试阶段先锻炼自己保持安静等波形稳定了再测试动作伪迹和眨眼干扰。我自己在搭完这套系统之后又做了两个小改动一个是在 ESP32-CYD 上增加了一个串口日志翻页功能方便现场回看最近一分钟的波形另一个是把网页里的 Canvas 换成 WebGL 折线绘制实测浏览器渲染更流畅。这些都不是必须的但如果你打算长期用这套链路界面体验会好很多。说到底这条从脑电模块到屏幕再到网页的无线原型链路并没有多深的理论门槛更多的是一步步把硬件接线、数据协议、无线传输、前端展示这些工程细节磨扎实。做完之后你会明显感觉到做 EEG 相关原型不再需要被线缆绑在工位上了这才是无线化最大的意义。