做 EEG 原型最讨厌的就是那一束线:传感器到主控、主控到电脑、中间还拖着电源线,人稍微一动数据就全花了。我当时想把脑电模块的数据真正“甩”到空中去,于是用手头两块板子搭了一条无线链路——BW16 负责从脑电模块读原始信号,通过 WiFi 发出去;ESP32-CYD 负责接收、在屏幕上画波形,同时开一个网页,让浏览器也能看到实时曲线。这条链路不算什么高精尖工程,但把细节掰开看,确实能帮不少做可穿戴原型的朋友少走弯路。如果你手里正好有便宜的脑电模块和一两块带 WiFi 的开发板,跟着这条思路走,完全能搭出一套属于自己的无线 EEG 采集前端。
1. 为什么要无线化:这条链路解决的核心问题
1.1 有线 EEG 原型的几个痛点
大部分入门级的脑电模块,比如常见的单通道 NeuroSky 类模块,都是通过串口直接往电脑发数据的。这听起来很简单,实际做原型的时候问题一堆。首先是你得有一根很长的 USB 线或者串口线拖着,实验者坐在电脑前面基本不敢乱动,稍微转头、起身,线缆一扯,电极就松了,脑电信号瞬间变成一堆毛刺。更麻烦的是,电脑本身是个巨大的噪声源,开关电源、USB 供电的地环路噪声经常让 ADS1299 这类精密模拟前端都压不住,更别说消费级模块了。
其次是数据展示问题。串口数据到了电脑之后,如果你想边采集边看波形,需要写上位机或者用 Processing、Python 的绘图库。调试过程中频繁开关串口,数据丢失一点也不容易察觉到。你真正想做的可能是把模块放在帽子里、别在腰间、戴在头上,然后在一个独立小屏幕上看看此刻的波形,或者用手机浏览器远程瞄一眼。这些场景,有线方案全都做不到。
还有一个被低估的问题:串口线会限制你后续做闭环实验。比如你想用脑电信号控制一个小风扇或者 LED 阵列,如果数据必须先跑到电脑再发指令,整套系统延迟大、部署笨重。真正的可穿戴原型应该是一个“能自己跑通”的设备,至少要在本地完成数据采集、显示和基本的阈值判断。
1.2 “本地屏 + 局域网网页”双通道方案的定位
所以我当时给自己定的目标是:做一个“无线 EEG 数据采集中转站”。脑电模块通过串口把数据交给 BW16,BW16 用 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,双频 WiFi(2.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-CYD
ESP32-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 | 固定0xA5 |
| 1-2 | 包序号 | 2 | 小端,自增,用于检测丢包 |
| 3-6 | 时间戳 | 4 | 毫秒时间戳,便于网页端 x 轴对齐 |
| 7 | 样本数 | 1 | 当前包内含 EEG 点数,固定 32 |
| 8-71 | 原始数据 | 64 | 32 个 int16 小端排列 |
| 72 | 校验和 | 1 | 前面所有字节累加取低 8 位 |
一个包总共 73 字节。EEG 模块是 512Hz 采样,32 个点一包意味着每秒约 16 包,有效数据吞吐量只有几百字节每秒,局域网传输毫无压力。
3. 硬件接线与信号完整性
3.1 引脚连接与电平匹配
脑电模块到 BW16 的接线不能想当然。很多脑电模块虽然是 3.3V 逻辑,但有些评估板自带 5V 电平转换或者直接输出 5V 串口信号。如果直接把 5V 信号接到 BW16 的 RX 引脚,轻则数据全是乱码,重则把 GPIO 烧掉。我的建议是先查模块手册,再用万用表量一下空闲状态下的 TX 电平,确认是 3.3V 再连接。
典型的连接方式如下(以我手上的模块为例):
| 设备端 | 引脚 | 连接到 |
|---|---|---|
| 脑电模块 | TX | BW16 的 UART RX(例如 PA1) |
| 脑电模块 | GND | BW16 的 GND |
| 脑电模块 | VCC | 3.3V 或 5V,按模块要求 |
| BW16 | TX(调试日志) | 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 id="eegCanvas" width="600" height="240"></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 字节,也就是每秒 32KB,WiFi 依然毫无压力。
多通道数据收集到一定程度,就可以做更高级的分析,比如脑电源定位。源定位常用的最小范数估计(MNE)需要非常干净的 EEG 信号、精确的电极位置和头模型,不是消费级模块能直接搞定的。但如果只是把自己的单通道数据并到几个固定电极位置上去做粗略的注意力趋势分析,这套系统已经够用了。我后来把保存下来的原始数据离线跑了一遍带通滤波和功率谱分析,Alpha 频段的变化趋势能看出来,这对接下来的闭环实验很有帮助。
7. 一些实际操作体会
如果你准备照着这个思路搭,我给三个建议。第一,一定先把串口数据协议跑通再搞 WiFi,别同时上两大块难题,否则排查起来很崩溃。第二,BW16 和 ESP32-CYD 的 IP 最好在路由器里做 DHCP 静态绑定,否则每次重启地址变了,网页书签和数据接收都要重配。第三,脑电模块的电极贴片非常影响信号质量,调试阶段先锻炼自己保持安静,等波形稳定了再测试动作伪迹和眨眼干扰。
我自己在搭完这套系统之后又做了两个小改动:一个是在 ESP32-CYD 上增加了一个串口日志翻页功能,方便现场回看最近一分钟的波形;另一个是把网页里的 Canvas 换成 WebGL 折线绘制,实测浏览器渲染更流畅。这些都不是必须的,但如果你打算长期用这套链路,界面体验会好很多。
说到底,这条从脑电模块到屏幕再到网页的无线原型链路,并没有多深的理论门槛,更多的是一步步把硬件接线、数据协议、无线传输、前端展示这些工程细节磨扎实。做完之后你会明显感觉到,做 EEG 相关原型不再需要被线缆绑在工位上了,这才是无线化最大的意义。