
先声明这是一次原型级尝试不是医疗参考。我自己做这条链路的动力很简单想在家里拿到可以实时看的脑电波形同时不想被一根 USB 线拴在电脑前。于是就有了这套组合——NeuroSky TGAM 脑电模块采集信号BW16瑞昱 RTL8720DN 模组做串口转 Wi-Fi 的搬运工ESP32-CYD 这块带 2.8 寸屏的开发板负责画波形顺带把数据推给浏览器。最终跑起来的效果是电极贴在额头上浏览器里有一条实时滚动的波形专注度、冥想度随状态跳动屏幕端也一样。这个标题听上去有点绕其实链路很短脑电模块 → 串口 → BW16 → Wi-Fi → ESP32-CYD → 屏幕/网页。真正值钱的部分不是任何一块硬件而是把四段数据路通时那些“为什么这么做”的选择。接下来我先把设计思路拆开再讲每个环节的具体实现和踩坑记录。1. 链路设计一套 EEG 原型系统先解决哪三个问题1.1 原始终端为什么要“无线化”医疗级别的脑电设备大多是多导联、放大器、主机、屏幕一体电脑通过线缆连接放大器。那种方案稳定但只适合躺在实验室里。我做这个原型的时候第一个需求就是“人不能坐在电脑前”。如果只是想验证脑电模块能不能工作直接拿 USB-TTL 转串口就能看到数据不需要无线可一旦你想把它固定到头带、帽子或者可穿戴原型里那根线就成了最大的束缚。无线化的直接收益是活动范围变大但代价是链路复杂度上升。原来一个串口助手就能搞定的事现在要拆成“采集端 → 无线桥接 → 显示端”三段每段都有自己的坑。更现实的问题是实验室里的无线环境并不总是干净的2.4G Wi-Fi、蓝牙、USB 3.0 辐射、甚至无线充电座的高频开关噪声都会在链条上叠加影响。所以我在设计时定的原则是每段单独可验证而不是等全部焊完再一口气排错。1.2 数据流与角色分工谁该做什么谁不该做什么整条链路的数据流是固定的额头和耳垂的电极信号进入 TGAM 脑电模块TGAM 以 57600 bps 的串口输出帧数据BW16 收到完整数据帧后通过 Wi-Fi 以 TCP 方式推给 ESP32-CYDESP32-CYD 一边在 ILI9341 屏幕上实时画波形一边在局域网里开一个 WebSocket 服务端口浏览器连上后就能看到同样的数据。这里最关键的设计决定是为什么不让 ESP32-CYD 直接接管采集端原因是职责分离。CYD 这块板子虽然有 Wi-Fi但它同时要驱动屏幕、处理触摸、跑 WebSocket 服务如果再把串口接收和组帧逻辑塞进去一旦出现花屏、掉线、数据错乱很难判断是哪一段出了问题。BW16 的职责非常单一串口读、Wi-Fi 发。它挂了你只需要盯住串口这头CYD 挂了你只需要盯住网络那头。对于原型阶段的调试来说这个拆分省了大量时间。2. 硬件选型TGAM、BW16、ESP32-CYD 各自的作用和取舍2.1 脑电模块为什么选 TGAM 而不是 OpenBCI 或 Muse脑电采集模块的选择范围其实很小。OpenBCI 是真正的高密度可扩展方案但价格高而且它默认走蓝牙或者 SD 卡想接进自定义链路还得再写一层协议Muse 是成品头带数据格式封闭想拿到原始脑电没那么直接。TGAM 是 NeuroSky 的入门级单导联芯片最大优势是把“原始脑电波形”和“专注度/冥想度”直接通过串口吐出来不需要自己做模拟前端和放大滤波正适合验证整条链路。TGAM 的输出是专有协议但网上资料很全。原始脑电采样率 512 Hz每个数据包里固定有 128 个采样点这意味着每包大约 250 ms。它还同时输出信号质量值、专注度Attention和冥想度Meditation。我做原型时最看重的就是信号质量值——它像一个“连接状态指示器”电极没贴好时数值会冲到 200很直观。作为对照我也列一下为什么不考虑其他方案方案优点缺点结论TGAM 单导联模块串口直出原始波形、成本低、协议公开单导联无法做空间定位适合作为链路原型OpenBCI多通道、开源、可扩展成本高、接入自定义链路复杂等原型跑通后再考虑成品 EEG 头带集成度高、佩戴方便数据封闭难以自定义显示不适合学习链路细节这里也顺带说一个容易被误会的点很多人看到 EEG 相关文章就期待做“源定位、最小范数估计”这类高级分析。严格来说最小范数估计需要多通道脑电数据、头模型和电极位置信息单导联模块做不了真正的源定位。我的建议是先把单导联的完整数据链路跑通后面要升级再换多通道采集软件框架可以复用。2.2 BW16 为什么适合当“搬运工”BW16 是瑞昱 RTL8720DN 的模组支持 2.4G Wi-Fi 和 BLE 5.0开发环境走 Arduino 的 Ameba 板卡包。选择它而不是更常见的 ESP8266我是有实际理由的。第一BW16 的 RAM 比 ESP8266 宽裕不少。脑电数据是持续不断的流Wi-Fi 协议栈处理网络事件时会打断串口接收如果 RAM 紧张、缓冲区开不够大高速串口下很容易丢字节。第二BW16 的串口电平是 3.3V和 TGAM 模块一致不用额外加电平转换。第三它支持同时开启 AP 和 STA 模式调试时可以直接让手机连到 BW16 自建的热点上看数据不依赖路由器。当然它不是没有缺点。BW16 的 Arduino 生态比 ESP32 小遇到问题能搜到的参考资料少而且板子在市场上有很多变体引脚引出不一样。我的做法是买回来后先看原理图确认 UART 引脚映射再决定把 Serial1 接到哪个口别想当然用默认配置。2.3 ESP32-CYD自带屏幕的开发板解决了什么ESP32-CYD 是 ESP32-2432S028 开发板的俗称板上集成了 2.8 寸 320x240 的 ILI9341 屏幕、XPT2046 触摸芯片、TF 卡槽、音频功放背面还能焊电池座。对于这个项目来说它最合适的点就是“自带屏幕”——你不需要额外买 OLED 或者 TFT 屏再布线直接就能在屏幕上画脑电波形。CYD 最常见的坑是版本混乱。市面有红色板、蓝色板、黑色板背光引脚、触摸芯片、PSRAM 配置都不一样。我手上的红板用 TFT_eSPI 库时官方示例里有现成的ESP32_2432S028R配置直接选出来能用但如果买的是蓝板触摸芯片可能是 FT6236引脚配置就得重新对。所以拿到板子的第一件事不是接线而是先查清楚你这一版的具体配置。3. 数据与协议设计串口解包、Wi-Fi 转发、屏幕绘制的工程细节3.1 读懂 TGAM 的串口数据帧TGAM 的帧结构不算复杂但如果你把它当成普通串口数据流去读很容易串位。协议格式是帧头0xAA 0xAA第三字节是 payload 长度payload 里按“数据类型数据值”排列最后一个字节是校验和。常见的几个字段0x02信号质量0~200越小越好0x04专注度0~1000x05冥想度0~1000x80原始脑电数据小端序有符号 16 位一包固定 128 个点。解析时要做一个状态机而不是简单读取缓冲区。核心逻辑是先找到0xAA 0xAA然后读 payload 长度逐字节解析 payload最后做校验。校验算法是 payload 所有字节累加取低 8 位后再用0x00减去这个累加和得到期望校验字节。如果对不上就放弃这一包回到找帧头的状态。我在 BW16 上写了解析代码的骨架大致是这个样子boolean feedTGAM(byte b) { static int state 0; static int len 0; static int idx 0; static byte payload[256]; static byte sum 0; switch (state) { case 0: if (b 0xAA) state 1; break; case 1: if (b 0xAA) state 2; else state 0; break; case 2: len b; idx 0; sum 0; state 3; break; case 3: payload[idx] b; sum b; if (idx len - 1) state 4; break; case 4: if (((0x00 - sum) 0xFF) b) { // 收到完整一包解析 payload parseTGAM(payload, len - 1); } state 0; break; } return false; }这段代码没有处理 payload 超长的情况实际用的时候要把payload缓冲区分界检查加上不然弱信号下的垃圾数据会导致越界。我当时就因为这个翻过车。3.2 无线转发为什么是“TCP 帧序号”而不是裸 UDP脑电数据显示对延迟不是特别敏感但对数据完整性有要求。最开始的版本我用 UDP理由很简单延迟小、代码少。结果在 Wi-Fi 环境稍微拥挤一点的情况下偶尔会丢一个包。脑电原始数据是连续信号丢一个点波形上就是一个跳变虽然肉眼看不太出来但后面要是做 FFT 频谱分析丢点会把频谱搞脏。所以我最后切到了 TCP。BW16 端把 TGAM 的原始帧原样放进 TCP 流里ESP32-CYD 端按帧解析。为了能发现中间是否漏帧我在每个帧头前加了一个 16 位序号。接收端会检查相邻包的序号是否连续不连续就计一次丢帧。这样做的好处是你可以明确知道链路是不是稳定而不是等波形不对劲了再去猜。TCP 的另一个细节是“包式发送”。千万别每收到一个字节就立刻WiFiClient.write()那样会让 Wi-Fi 协议栈频繁做小包传输效率低还容易阻塞串口接收。正确做法是解析完一整个 TGAM 帧128 点一包之后把序号和数据一起拼成一个大缓冲区再一次性发出去。我在 BW16 上实测这种方式能稳定跑到 512 Hz 采样率CPU 占用也不高。3.3 屏幕绘制关键在增量刷新而不是全屏重画ESP32-CYD 的屏幕是 320x240如果每 250 ms 来一包数据就清屏重绘画面会闪到没法看。正确做法是维护一个屏幕上宽的环形缓冲每次新数据到来时只把最新一列画出来同时把整屏左移一点。具体到脑电波形还有一个容易被忽略的细节直接把 512 Hz 的点连成线是不可行的因为一屏显示几秒数据时一列像素里塞了太多采样点。我的做法是每个像素列取该列范围内的最小值和最大值然后画一条竖线。这样既保留了波形的大致形态又不会出现快速抖动的信号被“抹平”成三角形的情况。TFT_eSPI 库里的核心绘制代码很短本质就是for (int col 0; col 320; col) { int data getMinMaxFromBuffer(col, col 1); int y map(data, -1000, 1000, 0, 240); tft.drawLine(col, y - 2, col, y 2, TFT_GREEN); }当然真机运行不能每列都调用drawLine性能不够需要配合 DMA 或者区域刷新。但原理就是这个原理用竖线代表一小段时间窗口内的幅度范围而不是逐点连线。4. 实操全记录从接线到跑通整条数据流4.1 硬件接线与供电别让噪声从源头进来先看接线表。TGAM 模块上最关键的几个接口VCC接 3.3V 电源GND接地EEG接额前电极REF接参考电极通常贴耳垂或者前额参考点TX接 BW16 的 UART RX。这里最容易犯错的是电源。TGAM 这类模拟前端对电源纹波非常敏感直接拿 USB 口的 5V 降压供电波形上会出现明显的工频干扰和开关噪声。我用两节 5 号电池做了独立电源实测波形干净很多。顺便说一句别在原型阶段用无线充电给脑电模块供电——无线充电本身的高频开关噪声会直接耦合进电极线波形直接糊掉。这在实验室里已经验证过了踩坑踩得很深刻。另一个必须强调的点是共地。TGAM、BW16、ESP32-CYD 三块板子即使通过 Wi-Fi 通信它们的 GND 也必须连到同一个参考点。如果不共地串口数据会出现莫名其妙的错位帧头也找不到。我第一次测试时只给 TGAM 和 BW16 共地CYD 单独供电结果 TCP 连接偶尔断开数据也断断续续后来把三块板子的地统一接好才稳定。4.2 BW16 固件串口接收和 Wi-Fi 发送的分工BW16 的开发环境是 Arduino IDE 加 Ameba 板卡包选好板子后就能用标准的Serial1做数据口。我建议把默认的串口日志和这路数据口分开不要用同一个串口打印调试信息又收脑电数据否则日志字符会混进数据流里。代码逻辑不复杂setup()里初始化Serial1为 57600初始化 Wi-Fi 连接loop()里持续读Serial1喂给 TGAM 解析状态机完整解出一帧后拼上序号通过 TCP 发给 ESP32-CYD。Wi-Fi 认证方面我一开始想连实验室的无线校园网但那种网络通常需要网页认证设备没有浏览器很难通过认证。所以干脆让 BW16 开 AP 模式手机和 ESP32-CYD 都连它自建的无线热点。如果你想用现有路由器就用 STA 模式连普通家用 Wi-Fi然后在路由器后台加一个静态 IP 绑定方便 CYD 端固定地址访问。BW16 端的代码大致是#include WiFi.h WiFiClient client; void setup() { Serial1.begin(57600); WiFi.begin(BrainNet, yourpassword); while (WiFi.status() ! WL_CONNECTED) { delay(100); } client.connect(192.168.4.2, 8266); } void loop() { while (Serial1.available()) { feedTGAM(Serial1.read()); } }注意feedTGAM解出完整帧后要立刻把帧数据送进一个发送缓冲区再在loop末尾统一client.write()。这样串口接收不会被网络阻塞卡死。4.3 链路验证三步确认“真的通了”很多同学第一次跑这种链路最怕的是不知道问题出在哪。我的经验是分三步验证第一步先用 USB-TTL 直连 TGAM在电脑串口助手上确认能看到0xAA 0xAA开头的帧。如果这步都不对后面全白搭。TGAM 不上电极时照样会输出帧只是信号质量值比较高正好可以用来验证协议解析。第二步让 BW16 连接电脑做 TCP 服务端电脑上用 TCP 调试助手接收看能不能收到完整帧。这一步验证的是“串口转 Wi-Fi”这段。我强烈建议在这步就把帧序号、校验和检查做完整而不是等接到 CYD 再调。第三步接通 ESP32-CYD让它建立 TCP 服务端BW16 主动连过来。CYD 屏幕上先打印收到的包计数如果包计数持续增加说明链路已经通了再开始画波形。这三个门槛按顺序过出问题很容易定位在哪一段。5. 网页可视化把 512Hz 脑电数据搬进浏览器5.1 为什么需要一个 WebSocket 转发层浏览器有 WebSocket API没有裸 TCP 的能力所以不能让浏览器直接连 BW16。我的做法是让 ESP32-CYD 跑一个 WebSocket 服务端监听例如 81 端口。BW16 推过来的 TCP 数据由 CYD 解析后通过 WebSocket 转发给浏览器。这样一来CYD 同时扮演三个角色TCP 接收端、屏幕绘制、WebSocket 服务端。听起来有点忙但对 ESP32 来说压力不大。需要注意的是一次别推太多数据我每次推 128 个采样点的一帧附带专注度、冥想度、信号质量和序号。浏览器端用二进制帧接收比一个个 JSON 解析高效得多。网页端的地址很简单就是ws://192.168.4.2:81。如果你不想在浏览器里直接连也可以用别人写好的 TCP 调试工具连同一个端口来验证转发是否正常。5.2 前端绘制的清屏与动态范围问题浏览器画波形也是同样的逻辑不要逐点画而是维护一个 Canvas 缓冲区按像素列做 min/max 映射。我用的代码是长这样ws.onmessage function(e) { let blob e.data; let buf new DataView(blob); let samplesPerFrame 128; for (let i 0; i samplesPerFrame; i) { dataBuffer.push(buf.getInt16(4 i * 2, true)); } }; function draw() { ctx.clearRect(0, 0, W, H); for (let col 0; col W; col) { let slice dataBuffer.slice(col * 2, col * 2 2); let min Math.min(...slice); let max Math.max(...slice); ctx.fillRect(col, map(min), 1, Math.max(1, map(max) - map(min))); } requestAnimationFrame(draw); }这段代码里有三个小坑要提。第一脑电的幅值动态范围不是固定的。一开始我固定映射 ±1000结果眨眼时一个尖峰就把整条波形压扁了。后来我加了一个“动态范围跟踪”根据最近一屏数据的最大值自动调整纵轴但限制单次调节幅度避免波形一直在缩放。第二眨眼产生的伪迹非常大一个眨眼尖峰可能比正常 α 波高 5 到 10 倍。如果不做限幅视觉上就是一条竖线刷过去。我用中值滤波做了一次平滑同时保留原始波形作为薄灰线方便后续观察。第三浏览器端的requestAnimationFrame和 512 Hz 数据本身是不同步的。帧率 60 Hz但脑电 512 Hz 意味着每秒有 512 个点。我的处理是每次draw()时只 draw 当前已到达的数据不强制每帧推进固定像素数。实测下来波形移动是丝滑的没有出现跳帧感。6. 排错实录七个我实际踩过的坑6.1 串口乱码、帧错位、数据对不上如果串口调试助手偶尔能看到AA AA但 payload 长度很离谱优先怀疑三件事波特率、共地、电平。TGAM 老模块的波特率是 57600但有些二手模块可能被改过配置你可以用 USB-TTL 直连电脑把波特率从 9600 到 115200 各试一遍看哪个能稳定收到合法帧。共地问题前面说过三块板子的 GND 必须连到一起。电平问题一般是 TGAM 和 BW16 的 UART 输出电压不匹配好在两者都是 3.3V只要别插到 5V 的 Arduino 上就行。帧错位再往后就是校验和问题。我建议在解析状态机里坚决执行“校验失败就丢弃整包”的策略不要尝试“补字节”。重新对齐帧头比修复半个帧便宜得多。6.2 无线环境差导致掉包和延迟尖峰Wi-Fi 是 2.4G和蓝牙、微波炉、USB 3.0 都会抢信道。实测延迟尖峰出现在 Wi-Fi 拥挤的时候TCP 重传会让数据突然停顿几十毫秒。我在这条链路里做不了硬件级别的确定性传输但有一个工程手段很有效给每一帧加序号显示端发现跳号就画一个小的“丢帧标记”而不是让波形错位。这个场景其实和工业“时间敏感网络无线领域”讨论的问题本质一样无线介质的时延抖动不可控应用层必须容忍乱序和延迟做的是缓冲而不是实时硬同步。对家用原型来说缓冲 1 到 2 帧约 500 ms足够平滑。如果你的实验环境有无线认证portal 认证问题设备连不上 Wi-Fi那最省事的方法是让 BW16 开 AP 热点或者用一台旧路由器做无线桥接把有线网络转成干净的独立无线网段。我试过用家用路由器做桥接比在模组里处理认证逻辑省心得多。6.3 工频干扰、射频噪声、电极接触不良波形上如果出现规律的 50 Hz 或 100 Hz 纹波第一反应是电源和电极线。电源改成电池供电能解决一半问题另一半是电极线屏蔽。耳夹线换成屏蔽线外层接地干扰明显下降。另外Wi-Fi 天线别放在电极线附近射频信号会直接耦合进模拟前端。我实际测试时把天线挪到离电极 10 cm 以上波形干净了不止一点。信号质量值居高不下、波形像毛刺一样乱跳那多半是电极接触问题。额头要擦一下减少油脂耳夹要夹紧必要时涂一点儿导电膏。TGAM 支持干电极但接触阻抗依然很关键。CYD 屏幕偶尔花屏或者刷一半就停大概率是 TFT_eSPI 的引脚配置不对。红板用官方ESP32_2432S028R配置通常能直接跑但如果你看到背光不亮先查背光引脚是不是被定义成 GPIO21 或者 GPIO27 了。还有如果不需要触摸建议直接关闭触摸初始化不然 XPT2046 会和屏幕抢 SPI 总线导致波形刷新变慢。现象可能原因处理办法串口帧头乱跳波特率错、共地不良、电平不匹配直连串口助手逐档试波特率统一 GND波形有规律纹波电源纹波、工频干扰电池供电屏蔽电极线偶尔跳帧Wi-Fi 丢包、TCP 重传加帧序号接受少量丢帧并标记眨眼尖峰过大动态范围未跟踪用中值滤波和自适应映射CYD 花屏TFT 引脚配置错误换官方 CYD 配置核对背光引脚TGAM 信号质量高电极接触不良、射频干扰清洁皮肤、夹紧耳夹、天线远离6.4 从原型到下一步单导联的天花板在哪里我把这条链路跑通之后最大的感慨是脑电原型开发里 80% 的时间不是在调算法而是在跟链路和噪声搏斗。你只要把每一跳的数据都验证干净、把电源弄好、把帧序号加上后面做特征提取会顺利很多。单导联模块的最大限制是无法做空间上的源定位。如果你想做更深入的脑电源定位、最小范数估计至少需要 8 通道以上的同步采集以及对应的头模型和电极位置信息。我的计划是保留这套 BW16 转发框架把 TGAM 换成多通道采集板后端的数据缓冲、显示、WebSocket 推送逻辑都可以直接复用。这也是我把协议解析和显示拆开的原因——后面换采集端只动最前面一段代码其余部分不动。最后分享一个小技巧调试这类多段无线链路时每完成一段验证就记一次日志包括时间、丢帧数、信号质量。等所有段联调完成对比日志能快速定位到最后一次改动引入的问题。这套方法帮我省掉了至少半天排查时间也希望对你有所帮助。