脑电模块的数据只要超过一米,基本就是一团噪声;但凡在实验室里接过脑电的都知道,排线、屏蔽、地环路全是坑。所以当我想把“脑电采集—传输—展示”这一整套链路做成原型时,第一反应就是:接线越短越好,数据越早上天越好。方向定了之后,硬件选择就很清楚了——用一块低功耗 WiFi 模块做采集端,把脑电数据从串口捞出来直接扔上无线;接收端用一块带彩色屏幕的开发板,一边在屏上画波形,一边把数据转发给浏览器。
这个项目说白了就是一条“从脑电模块到屏幕和网页的无线 EEG 原型链路”:脑电模块负责采集原始脑电信号并输出串口数据,BW16 模块负责读取、解析、打包并通过 WiFi 发送,之后数据落到 ESP32-CYD 的 TFT 屏幕上实时显示,同时 ESP32-CYD 内置一个 WebSocket 服务,把同一路数据推给网页端的 canvas 画布。整条链路板子成本不到两百块,却能验证脑电采集、无线传输、多端展示的所有关键环节。
这套东西特别适合两类人:一是嵌入式爱好者,想低成本体验一下生物信号采集和无线传输的配合;二是生物医学工程相关专业的学生,做课程设计或者毕设预研的时候,先用它跑通链路,后面再上多通道、上源定位算法。下面我把整个链路的选型思路、动手过程、踩坑实录全写出来,照着抄就能搭一套能跑的原型。
1. 链路设计与核心选型:为什么让 BW16 采集,让 CYD 显示
1.1 先把数据流走一遍
整条链路的物理连接和数据走向是这样的:脑电模块的串口 TX 接到 BW16 的 RX,脑电模块和 BW16 之间只隔几根杜邦线;BW16 解析出脑电特征值(专注度、冥想度、信号质量)之后,组一个简单的文本帧,用 UDP 广播到局域网固定端口;ESP32-CYD 在同一局域网里监听这个端口,收到数据后做两件事——在 ILI9341 屏幕上绘制滚动波形和数值,同时通过 WebSocket 把数据帧向所有连接的浏览器广播。浏览器端就拿到了一条实时推流的数据,画布上直接出曲线。
这样的分工有明显好处。采集端和接收端彻底分离:BW16 只负责“读和发”,代码量小、实时性强;ESP32-CYD 只负责“收和显”,可以把精力全花在 TFT 绘制和 WebServer 上。以后想换成多通道脑电模块,或者加 SD 卡存储,都只需要改单端,不用动整个架构。
1.2 BW16 凭什么当采集端
BW16 这颗芯片是 Realtek RTL8720DN,双核 Cortex-M3,主频最高 166MHz,512KB SRAM,1MB Flash。看一眼资源就知道,跑一个 57600 波特率的串口读取任务加上 WiFi 协议栈很轻松,几乎没有内存压力。真正让我选它的原因是双频 WiFi:2.4GHz 和 5GHz 都支持,在环境复杂的实验室里,5GHz 频段干扰要小很多,脑电数据本身又是低吞吐量的,双频优势能实打实地反映在丢包率上。
另外 BW16 的低功耗特性也适合可穿戴场景。脑电模块本来就是要贴在头上或者做成头环的,采集端功率越低,电池越小,后面做成品的时候才好交代。Arduino 生态也把这颗芯片收进来了,IDE 里装一个 Realtek Ameba 核心就能直接写代码,原生的 WiFi 库、WiFiUDP 库都有,基本是零门槛。
1.3 ESP32-CYD 为什么适合当接收端
ESP32-CYD 其实就是国内俗称的“黄色开发板”,全名 ESP32-2432S028,板载一颗 ESP32-WROOM-32,带一块 2.4 英寸 240x320 的 ILI9341 TFT 电阻触摸屏,旁边还有 SD 卡槽、LED、电位器。它最妙的地方是“有屏又有网”:同一颗 ESP32 既能驱动 TFT 画图,又能开 WebServer 和 WebSocketServer,正好把接收端和展示端合并成一个设备。
我最初也想用电脑直接当接收端,但转念一想,如果我在户外、没带电脑,只有手机,怎么快速看到数据?ESP32-CYD 托管一个网页就不存在这个问题了:CYD 自己连着路由器,手机或者电脑打开浏览器访问 CYD 的 IP,就能看到实时的脑电波形。这套方案把“接收显示”和“网页展示”拧在一起,少了一台专门跑服务的电脑,原型变得特别完整。
2. 核心细节解析:EEG 帧解析与无线协议设计
2.1 脑电模块的串口帧格式
我用的脑电模块是 TGAM 兼容款,也就是 NeuroSky ThinkGear 系的串口输出方案。这种模块输出 57600 波特率的串口数据流,数据是流式的,没有固定的“行”概念,里面是一串小包夹偶尔的大包。小包格式固定是 0xAA 0xAA 开头,第三个字节是负载长度,短包长度一般为 0x04,后面跟着三个有效数据字节和一个校验字节。
有效数据字节里最重要的是三个值:信号质量(0 代表接触好,200 代表电极基本脱落)、专注度、冥想度。注意第三个有效值在不同模块固件里可能被定义成眨眼强度,接上后用串口工具多观察一下就能分辨。校验算法很简单:把长度字节和三个有效数据字节加起来,取低 8 位的补码,也就是 0x100 减去累加和的低 8 位,如果等于最后一个校验字节,这帧就算通过了。
解析的时候千万不要按固定偏移去读,因为模块输出的包是连续流,可能从任意字节开始。正确做法是维护一个环形缓存,逐字节滑窗找 0xAA 0xAA 头,再按照长度字段去截完整包并校验。这个思路对所有“流式协议”都通用,不管是脑电模块还是其他传感器,先找头、再算长、最后校验,基本不会翻车。
2.2 为什么不用 JSON,一行文本帧就够
链路中间传的数据格式我一开始纠结过。嵌入式端传 JSON 看起来高大上,但 ESP32 和 BW16 都是单片机,解析 JSON 有额外开销,而且 JSON 字符串里逗号、引号、转义符一多,调试的时候肉眼排查特别难受。最后我定了一个极简文本帧:
EEG,75,40,0,1345一共五个字段,用逗号分隔:固定标识 EEG,然后是专注度、冥想度、信号质量、当前 ADC 原始值。整帧不到 25 字节,10Hz 的频率下算下来每秒只有 250 字节左右,WiFi 传输余量极大。更关键的是这个格式谁都能读:BW16 拼出来不费劲,ESP32-CYD 用strtok或者sscanf切一下就行,浏览器端 JavaScript 用split(",")一行处理,连抓包工具都能直接看懂。
2.3 无线通道选 UDP 还是 TCP
脑电数据是一路连续采样的流,不是事务型请求,所以我选了 UDP 而不是 TCP。选 UDP 的原因很直接:脑电帧每秒只有 10 帧左右,每一帧 20 多个字节,就算偶尔丢一个包,下一帧 100ms 后就到了,肉眼根本察觉不到;UDP 没有 TCP 的握手和 ACK 重传机制,延迟更低,而且实现起来简单到只要beginPacket、write、endPacket三步。
那 TCP 是不是完全没用?也不是。如果你后续要做脑电的离线分析,要求每个包都不丢、带序号可以恢复连续数据,那就该切到 TCP,甚至可以考虑在 BW16 上开一个 TCP Server,让采集端主动等连接。浏览器端不能直接收 UDP,所以网页显示要用 WebSocket,这是整个链路里唯一需要“翻译”协议的地方,我把这活儿交给了 ESP32-CYD,由它做 UDP 接收端和 WebSocket 发送端的桥接。
3. 实操过程:BW16 采集端固件从零搭起
3.1 开发环境与硬件接线
BW16 在 Arduino IDE 里的支持很成熟。打开 IDE 的首选项,在附加开发板管理地址里填上 Realtek Ameba 的 JSON 地址,然后在开发板管理器里搜索 Ameba,安装 “Realtek Ameba Boards (32-bits ARM Cortex-M3)”,选板子的时候找 AmebaD / RTL8720DN 对应的型号。装好之后就跟普通 ESP8266 开发一样写代码了。
硬件接线按数据流方向来:脑电模块的 TX 接 BW16 的 RX,脑电模块的 RX 接 BW16 的 TX,两根地线一定要共地。Ameba Arduino 核心把硬件串口 Serial1 的引脚映射放在板型文件里,不同卖家做的 BW16 小板丝印不一定一致,接之前一定仔细看模块原理图,别不看引脚就往上怼。我手上这块 Serial1 的 RX 和 TX 引在 PA18、PA19,但你的板子可能不同,拿万用表导通档照着丝印确认是最稳的。
3.2 串口读帧与校验实现
BW16 端的代码可以分为三块:串口收流、帧解析、WiFi 发送。串口部分用Serial1.begin(57600)设置波特率之后,在loop()里把字节读进一个缓冲区。帧解析我写成专门函数,逐字节滑窗找 AA AA 头,找到后读长度字节,做补码校验,校验通过就取出专注度、冥想度、信号质量三个值。
bool parseThinkGear(uint8_t* data, int len, int& attention, int& meditation, int& quality) { for (int i = 0; i < len - 5; i++) { if (data[i] == 0xAA && data[i+1] == 0xAA) { int plen = data[i+2]; if (plen != 4) continue; // 只处理短包 if (i + 3 + plen >= len) return false; // 等更多数据 uint8_t sum = plen; for (int k = i + 3; k < i + 3 + plen; k++) sum += data[k]; if ((uint8_t)(0x100 - sum) != data[i + 3 + plen]) continue; quality = data[i+3]; attention = data[i+4]; meditation = data[i+5]; return true; } } return false; }如果你的模块只出原始波形数据而不带注意力解读,解析逻辑也类似,只是大数据包的长度字节最高位会被置 1,代表后面跟的是 RAW 波形值。想验证解析对不对,可以在 BW16 上用Serial.println把三个数值打出来,和电脑上用串口助手抓到的原始帧对比一下。
3.3 WiFi 发送与断线重连
WiFi 部分走 UDP 广播。这样不需要在 BW16 里写死 ESP32-CYD 的 IP,同一局域网里所有设备只要监听同一个端口都能收到,调试时电脑也能直接用工具抓包。发送逻辑极简,把解析结果拼成一行文本帧,用WiFiUDP广播到 255.255.255.255:5005。
WiFiUDP udp; IPAddress broadcast(255, 255, 255, 255); void sendEEG(int att, int med, int q) { char tx[40]; snprintf(tx, sizeof(tx), "EEG,%d,%d,%d,%lu", att, med, q, millis() % 100000); udp.beginPacket(broadcast, 5005); udp.write((uint8_t*)tx, strlen(tx)); udp.endPacket(); }注意 BW16 也是单片机,WiFi 断线重连是必须处理的。实际跑的时候不一定断网,但路由器半夜重启一次、DHCP 租约到期重新获取 IP,都可能让 WiFi 静默失联。我在loop()里每隔几秒检查WiFi.status(),不等于WL_CONNECTED就重新WiFi.begin,配合一个 10 秒超时再复位 WiFi 模块,基本能做到断线后自动恢复,不用拔电重启。
4. 实操过程:CYD 屏幕端与网页端一起做
4.1 TFT_eSPI 配置与波形绘制
ESP32-CYD 的屏幕驱动态取决于你拿到的具体板子版本,大多数是 ILI9341,用 8 位并行接口挂在 ESP32 上。TFT_eSPI 库里已经内置了 ESP32-2432S028 这个板型的配置,当前比较常见的做法是直接使用 USER_SETUP_ID 75 对应的配置,关键是把 TFT 数据线 D0~D7 接到 GPIO32、33、26、25、21、20、19、18,WR 接 GPIO22,RD 接 GPIO27,CS 接 GPIO14,DC 接 GPIO2。每块 CYD 可能由于批次不同有细节差异,屏幕不亮先查背光引脚,再逐个核对数据线。
波形绘制我没有用复杂的双缓冲,而是直接维护一个长度为 240 环形数组,每收到一帧数据就往数组里填一个点,然后整行重画。这个方案在 10Hz 数据率下完全够用,毕竟每秒只重画 10 次,对 240x320 的屏幕来说压力不大。为了减小闪烁,重画前我用fillRect清掉上一帧波形区域,再用drawLine把相邻点连起来,实测看起来是一条平滑滚动的曲线,没有明显撕裂。
void drawWave() { tft.fillRect(0, 40, 240, 120, TFT_BLACK); for (int x = 0; x < 239; x++) { int y1 = 160 - rawBuf[x] * 2; int y2 = 160 - rawBuf[x + 1] * 2; tft.drawLine(x, y1, x + 1, y2, TFT_GREEN); } }如果原始波形幅度太大或太小,把乘的系数按实际数据范围调一下就行。专注度和冥想度这类数值直接画大字号显示在屏幕上方,比波形更直观。
4.2 用 WebSocket 把数据推给浏览器
CYD 端还有一个角色:WebSocket 服务器。我用 Arduino 生态里比较成熟的 WebSocketsServer 库,先server.on("/")返回一个内置 HTML 页面,再webSocket.begin()在 81 端口监听浏览器连接。UDP 收到 BW16 的数据帧之后,CYD 既画屏幕又webSocket.broadcastTXT向全部客户端广播同一行文本。
#include <WiFi.h> #include <WebServer.h> #include <WebSocketsServer.h> WebServer httpServer(80); WebSocketsServer wsServer(81); void processUDPPacket(char* line) { drawWave(); drawNumbers(line); wsServer.broadcastTXT(line); }浏览器那边不需要任何插件,原生 WebSocket 就能接。页面里用一个 canvas 画连续波形,onmessage回调里把逗号分隔的字段拆出来,数据推给setTimeout刷新的绘制函数。这里有个细节:WebSocket 默认不加消息头,文本帧按 UTF-8 来传,中文字符都没问题,但我们的帧全是 ASCII,所以怎么传都不会出乱码。
4.3 网页端波形交互体验调优
网页端的 canvas 绘制逻辑看着简单,实际有几个容易忽略的点。第一个是 canvas 的高度和波形缩放比例:如果脑电原始值波动范围是 0~255,直接映射到 300px 高的 canvas 会让波形看起来特别“满”;我建议做一次线性映射,把常见范围压缩到画布高度的 60% 左右,留出上下余量,曲线更舒展。第二个是不要用setInterval做绘制,而是用requestAnimationFrame配合一个数据缓冲,保证浏览器刷新率和数据到达率解耦,观感会平滑很多。
我还在网页上加了一个小的状态栏,用不同颜色显示信号质量:绿灯表示信号良好,黄灯表示电极接触一般,红灯就是脑电模块已经探头掉了。这个反馈在调试电极的时候特别有用,比在屏幕端看一串数字直观得多。
5. 调试实录与常见问题速查表
5.1 排查链路丢失的三个层面
整个链路最容易出的问题不是代码逻辑,而是信号根本没有正确到达。我把排查顺序总结成三层:物理层、协议层、应用层。
物理层先用电脑的串口助手直接监听脑电模块原始输出,确认有 0xAA 0xAA 头出现,并且数据不是全 0 也不是全 FF。接着看 BW16 串口解析是否成功,在 BW16 串口打印里输出解析后的数值,如果数值跳动正常,说明采集端没问题。协议层用电脑抓 UDP 包,能抓到 5005 端口的广播就说明 WiFi 发送正常;抓不到就查 BW16 的 SSID 和密码,查路由器是不是开了 AP 隔离。应用层再打开 CYD 的 TFT 和网页,两个界面能同时刷新就说明端到端通了。
我调试的时候习惯在链路的每一级都加一个状态标记:BW16 收到一帧打一个点,CYD 收到一个 UDP 包点一下屏上的 LED,网页收到一条 WebSocket 信息在状态栏刷新计数。这样节点间是哪一段断了,一眼就能定位。
5.2 几个跑项目时的典型坑
第一个坑是 5GHz 频段。ESP32-CYD 的 WiFi 只有 2.4GHz,而 BW16 是双频。很多新路由器默认把 2.4G 和 5G 合并成一个 SSID,ESP32 连接的时候可能被分配到 5GHz 频段导致连不上。最简单的处理办法是关掉“双频合一”,或者给 IoT 设备单独开一个 2.4GHz 的访客网络。我一开始没意识到这个问题,CYD 反复连接不上,折腾了很久。
第二个坑是串口波特率不匹配。TGAM 模块默认 57600,有些模块卖家会预配成 38400 甚至 115200,直接拿 BW16 去读就会解析失败。最好的做法是先接 USB 转 TTL 用串口助手看,确认波特率之后再写进 BW16 的代码里。
第三个坑是供电地环路。脑电信号是非常微弱的生物电信号,对工频干扰极其敏感。如果脑电模块和 WiFi 模块都用同一个 USB 充电器供电,电脑的开关电源会把 50Hz 干扰耦合进来,信号质量数值可能一直在 100 以上徘徊。我的经验是脑电模块端尽量用独立锂电池供电,主控和屏幕再用另一路电源,两块板子之间只连信号线和一根公共地,实测干扰明显下降。
第四个坑是屏幕刷新闪烁。CYD 的 TFT 通过 8 位并口驱动,虽然比 SPI 快,但如果 waveform 重绘时先整屏 fillRect 再逐点画,能看到轻微闪烁。这个问题的根源是屏幕没有帧缓冲,重绘中间画面被肉眼看到。我最后用“只擦出波形区域的上半部分、保留下半部分静态区”的方式缓解,同时对数值显示区域单独刷新,视觉效果好了很多。
5.3 常见问题速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| BW16 连不上 WiFi | 路由器开启 5GHz/2.4GHz 单频合一 | 关闭双频合一,使用独立 2.4GHz SSID |
| 脑电信号质量一直为 200 | 电极没贴紧、参考电极开路 | 重新贴电极,检查导电胶和地线 |
| CYD 屏幕不亮 | TFT 引脚配置错误、背光引脚未拉高 | 核对 TFT_eSPI 配置,检查背光控制引脚 |
| 网页连不上 WebSocket | 浏览器与 CYD 不在同一网段 | 检查路由器和防火墙,改用同一局域网 |
| 波形一条直线无变化 | 串口波特率不匹配或脑电模块断电 | 用串口助手确认原始数据是否正常 |
| 数据偶尔跳变 | WiFi 丢包,UDP 不可靠 | 在帧里加序号,前端做数据平滑或插值 |
写在最后
整套链路搭完之后,我最直观的感受是:一条无线 EEG 数据链路其实没有想象中复杂,关键在于把每一段的职责切清楚。脑电模块只输出串口流,BW16 管解析和上云,ESP32-CYD 指挥屏幕和网页,浏览器只管画图。每一段用最土的方式——文本帧、UDP、WebSocket——串起来,反而比花里胡哨的协议栈更稳、更好调。
如果玩完基本功想继续深入,最顺理成章的扩展方向是加存储:在 CYD 的 SD 卡槽上把原始脑电波形落盘,这样之后可以用 MATLAB 或者 Python 做离线分析,甚至可以顺着脑电源定位的思路,把两三个 BW16 采集通道同步起来,再用最小范数估计一类的方法去反演脑内活跃源。那一步才是真正进入脑机接口深水区的开始,但眼下这条原型链路,已经足够撑起你对脑电数据流动的完整概念了。