首页 / 资讯中心 / 文章详情

低成本无线EEG原型链路:从脑电采集到网页实时显示

低成本无线EEG原型链路:从脑电采集到网页实时显示 ★ FEATURED ARTICLE
脑电模块的数据只要超过一米基本就是一团噪声但凡在实验室里接过脑电的都知道排线、屏蔽、地环路全是坑。所以当我想把“脑电采集—传输—展示”这一整套链路做成原型时第一反应就是接线越短越好数据越早上天越好。方向定了之后硬件选择就很清楚了——用一块低功耗 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主频最高 166MHz512KB SRAM1MB Flash。看一眼资源就知道跑一个 57600 波特率的串口读取任务加上 WiFi 协议栈很轻松几乎没有内存压力。真正让我选它的原因是双频 WiFi2.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[i1] 0xAA) { int plen data[i2]; 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[i3]; attention data[i4]; meditation data[i5]; 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、18WR 接 GPIO22RD 接 GPIO27CS 接 GPIO14DC 接 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 合并成一个 SSIDESP32 连接的时候可能被分配到 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 采集通道同步起来再用最小范数估计一类的方法去反演脑内活跃源。那一步才是真正进入脑机接口深水区的开始但眼下这条原型链路已经足够撑起你对脑电数据流动的完整概念了。
阅读完成 · 觉得有帮助?
咨询建站