看到自己头顶脑电模块的原始波形,通过 WiFi 飞到自己手里那块小黄板屏幕上,又同步推到电脑浏览器的标签页里,那一刻确实挺有成就感。断断续续折腾了将近一个月,我把“从脑电模块到屏幕和网页”这条无线 EEG 原型链路完整打通了,中间两个主角分别是 BW16(RTL8720DN 模组)和 ESP32-CYD 带屏开发板。这篇文章不是讲神经科学,更不是讲医疗诊断,只讲硬件链路怎么搭建、数据怎么传输、代码怎么组织、坑又在哪里。如果你手上也有一块串口输出的脑电模块(不管是 TGAM 方案的还是自研的 ADS 方案),想把它变成屏幕上滚动的波形、变成一个能在浏览器里刷新的实时数据流,那这篇应该对你有用。
整个项目的核心其实就一句话:把脑电模块当成一个“串口传感器”,用一个低功耗 WiFi 模组把数据读进来,再以无线方式送给带屏的开发板做可视化。难点不在 EEG 本身,而在“两个板子之间的无线链路怎么设计得稳、怎么排查问题”。
1. 项目全貌:这条无线 EEG 链路到底干了什么
1.1 最小闭环:从脑电模块到屏幕和网页
我在设计这个原型的时候,给自己定的目标很朴素:脑电模块不用插线,就能把实时波形显示在 ESP32-CYD 的 TFT 屏幕上,同时同一份数据还要能推送到浏览器里。整个链路的拓扑大概是这样的:
- 脑电模块通过串口(TTL 3.3V)输出采样数据和统计特征;
- BW16 作为采集端,负责读串口、解析协议、把数据打包成 UDP 包发到局域网;
- ESP32-CYD 作为展示端,接收 UDP 包,在屏幕上画波形;
- 同一块 CYD 再运行一个轻量 WebSocket 服务,把数据广播给局域网里的浏览器。
换句话说,这条链路一共有四个环节:采集、传输、屏幕渲染、网页渲染。你可能会问,为什么要把一个看起来很小的事拆成这么多段?因为只有拆开,每一段的职责才清楚,排查问题时才能逐段定位。EEG 信号本身是微伏级别的模拟信号,模块内部已经做了放大和滤波,到了串口这一层,它就是一堆有格式的数字包,跟一个温湿度传感器没本质区别。
我当时给朋友打过一个比方:脑电模块是麦克风,BW16 是无线麦克风的发射端,ESP32-CYD 是接收音箱,而浏览器就是第二只音箱。这个类比虽然粗糙,但把链路关系说清楚了。对做原型来说,最重要的不是“脑电”两个字,而是数据链路的鲁棒性——麦克风声音断断续续没法用,EEG 波形断断续续同样没法看。
1.2 为什么拆成两个板子:BW16 与 ESP32-CYD 的选型逻辑
有人看完标题可能会问:ESP32-CYD 本身就是一块带 WiFi 的开发板,为什么不直接把脑电模块接到 CYD 上,一个板子全搞定?
这个问题我当时也认真想过。第一个原因是物理形态:脑电模块是要戴在头上或者贴近皮肤使用的,而 CYD 是一块带屏幕的板子,既大又重,放在头上完全不现实。采集端和展示端天然就应该是分离的。第二个原因是干扰:ESP32-CYD 的 TFT 屏幕、SD 卡、背光 PWM 都是噪声源,紧挨着脑电模块的模拟前端会严重影响信号质量。第三个原因是工程解耦:采集端要低功耗、小体积、长时间运行;展示端要算力足够、有屏、方便交互,两者的需求完全不同,硬凑在一块板子上只会两头都别扭。
选 BW16 而不是 ESP32 做采集端,主要是看中它的低功耗和足够的串口资源。BW16 用的瑞昱 RTL8720DN,支持 2.4GHz WiFi 和 BLE,主频不低,但跑这种串口转发任务绰绰有余,而且体积很小,以后想做成纽扣电池供电的穿戴端,功耗底子比 ESP32 好不少。选 ESP32-CYD 则纯粹是因为它“便宜大碗还带屏”,市价二十来块就有 2.4 寸 TFT、WiFi、蓝牙、SD 卡槽,跑一个 UDP 接收任务加一个 WebSocket 服务器完全没压力。两块板子加起来成本很低,做原型不用心疼。
当然也有反面考虑。BW16 的 Arduino 生态没有 ESP32 那么成熟,踩坑时需要多查东西;CYD 的屏幕驱动配置也比较折腾。但这些都是“麻烦”而不是“瓶颈”,作为原型链路验收是完全可以接受的。
1.3 硬件清单与大致预算
我实际用到的硬件清单如下,给想复刻的朋友一个参考。价格随渠道和行情会有浮动,但整体上这套方案成本不高:
| 器件 | 型号/说明 | 大致价格 |
|---|---|---|
| 脑电模块 | TGAM 方案串口模块,输出原始波和 eSense | 120-200 元 |
| BW16 模组 | RTL8720DN,带板载天线或 IPEX 天线 | 10-20 元 |
| ESP32-CYD | ESP32-2432S028R,2.4/2.8 寸 TFT 带触摸 | 25-40 元 |
| 电源 | 两路 3.3V LDO,或直接用充电宝 + 降压模块 | 10-20 元 |
| 连接线 | 杜邦线、排针、焊锡 | 10 元以内 |
如果你手里已经有脑电模块,这块成本可以只算后面三项,总计不到 80 元。没有脑电模块的话,用任意串口输出数字协议的传感器先顶上也行,把链路打通之后再换脑电模块,原理完全一样。
另外提醒一下,脑电模块的电极和佩戴方式直接决定信号质量。我一开始用的是普通金属夹子电极,不涂导电膏,波形噪声非常大;后来换成医用 Ag/AgCl 电极片,配合导电膏,信号才勉强能看。这个问题我会在后面“信号质量”章节详细讲。
2. 链路设计:脑电数据怎么被搬上天
2.1 EEG 信号的数据特征:1KB/s 的“小水管”
在写代码之前,一定要先算一笔带宽账。我用的脑电模块输出两路数据:一路是 512Hz 采样率的原始脑电波形,每个采样点是 16bit 有符号整数;另一路是每秒一次的统计特征,包括信号质量、专注度 eSense、冥想度 eSense。算一下就知道,原始波形数据量是:
512 次/秒 × 2 字节/次 = 1024 字节/秒
也就是 1KB/s,换算成比特率大约是 8kbps。这个数据量是什么概念?普通 Wi-Fi 的有效吞吐率少说也有几十 Mbps,1KB/s 连“一根水管”都算不上,只能算一滴水。所以这条无线链路完全不存在带宽瓶颈,真正的难点是实时性和稳定性:数据必须按时到达,不能一会儿快一会儿慢,更不能因为 TCP 重传导致画面突然跳到几秒前。
这个估算是整个项目的地基。正因为数据量这么小,我才有底气用 UDP 而不是 TCP,才有底气让 ESP32-CYD 同时干“接收 + 画屏 + 网页推送”三件事。做链路设计的人第一反应应该是估算数据量,而不是直接想用哪种协议、哪种硬件,这个顺序千万别搞反。
2.2 传输协议取舍:采集端用 UDP,展示端用 WebSocket
链路分成两段,每段的协议选择完全不同。
第一段是 BW16 到 ESP32-CYD,我选了UDP。原因很简单:脑电波形是连续采样的,偶尔丢一两个采样点,反映到屏幕上只是波形上出现一个极小的毛刺,下一帧马上补回来,人眼根本察觉不到。而如果走 TCP,一旦链路抖动触发重传,后面所有数据都要排队等这个迟到包,延迟会像滚雪球一样越滚越大,这在实时波形显示里是不可接受的。UDP 的“丢包不重传”特性在这个场景里反而是优点。
第二段是 ESP32-CYD 到浏览器,我用的是WebSocket。因为浏览器里的 JavaScript 没办法直接收 UDP,用 WebSocket 最自然,而且 WebSocket 是建立在 TCP 基础上的,局域网内丢包率极低,延迟稳定,完全符合浏览器端实时绘图的需求。这里要强调一点:CYD 自己就是一个 WebSocket 服务器,浏览器直接连它,不需要额外跑一台 PC 中转,这也是“到网页”这步最顺手的地方。
有的朋友可能会问,为什么不直接用 BLE(低功耗蓝牙)把脑电传给手机或电脑?BLE 当然可以做,但要维护 GATT 服务、处理连接管理和分包,开发量明显更大,而且手机端要写 App,网页端要处理 Web Bluetooth 协议的兼容性问题。在“快速验证原型”的目标下,WiFi 的局域网链路显然更省事。
2.3 自定义数据帧:给数据包加上“信封”
串口和 WiFi 传输的都是字节流,接收方必须知道“从哪开始读、读多长、怎么判断数据坏了”。脑电模块本身有自己的协议,比如常见的 TGAM 数据包以0xAA 0xAA开头,后面跟着长度、类型、负载和校验和。但 BW16 把数据转发给 ESP32-CYD 的时候,我额外包了一层自己的帧格式,相当于在原有协议外面又套了一个“信封”。
我自定义的帧结构如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 2 字节 | 0xB6 0x7E,用于同步 |
| 数据长度 | 1 字节 | 负载长度,最长 255 |
| 数据类型 | 1 字节 | 0x01原始波,0x02统计特征 |
| 序列号 | 2 字节 | 帧计数,用于检测丢包 |
| 负载 | N 字节 | 采样点或统计值 |
| CRC8 | 1 字节 | 对前面所有字节做校验 |
加序列号是我后来才想到的关键改进。最初我只看帧头,结果发现偶尔出现掉帧但校验和没报错的情况,因为单字节校验覆盖不住极端场景。加上 16bit 序列号之后,接收方一旦发现序列号跳变,立刻能算出丢了多少包,这在调 WiFi 稳定性时提供了关键证据。
CRC8 的代码网上很多,几行就能实现,这里不展开。需要提醒的是:帧头一定要设计成足够“罕见的组合”,不能全是0x00或0xFF,否则负载里一旦出现和帧头一样的字节,接收端就会误判同步位置。0xB6 0x7E是我随手定的,实测用下来没冲突,但如果你的负载数据比较特殊,建议用更长一点的同步头。
3. 采集端实操:BW16 的串口读取与 UDP 转发
3.1 接线:TX、RX、GND,千万别接反
硬件接线是整个项目里最容易出错的一步,尤其是第一次玩串口的人。脑电模块的 TTL 串口一般引出VCC、GND、TX、RX四个引脚,BW16 也一样。接线规则是“交叉互连”:模块的 TX 接 BW16 的 RX,模块的 RX 接 BW16 的 TX,两边 GND 必须接在一起。
这里有三条注意事项:
- 电压必须匹配。我的模块是 3.3V TTL,BW16 的 IO 也是 3.3V,可以直接连。如果你的模块是 5V 电平,最好串电阻分压或者用电平转换板,直接接大概率会把 BW16 烧了。
- GND 是命根子。收发双方如果没有共地,串口数据完全是一堆乱码,我一开始用 USB 供电时忘了把两边的 GND 接一块,看到的数据全是
0xFF,折腾了半天才反应过来。 - 先确认丝印再上电。不同厂家对 TX/RX 的丝印标注有时候很迷,有的标
TXD/RXD,有的直接标P2/P3,最好用万用表量一下或者看原理图,别凭感觉接。
BW16 的串口资源方面,我用的 UART1 接脑电模块,UART0 留给烧录和调试信息。这样程序里Serial1是脑电数据入口,Serial是日志出口,两边互不干扰。
3.2 开发环境与 WiFi 连接
BW16 用的开发环境是 Ameba Arduino SDK,在 Arduino IDE 的“开发板管理器”里添加瑞昱的板卡支持包即可,安装好后选择对应的 RTL8720 系列板卡。这个 SDK 的 API 风格和标准 Arduino 很接近,从 ESP32 迁移过来不会太痛苦,但从 Arduino AVR 迁移过来要注意:BW16 的Serial默认波特率、中断行为和一些底层库是有差异的。
WiFi 连接部分,代码其实很短:
#include <WiFi.h> #include <WiFiUdp.h> const char* ssid = "你的WiFi名"; const char* password = "你的WiFi密码"; WiFiUDP udp; void setup() { Serial.begin(115200); Serial1.begin(57600); // 接脑电模块,波特率以模块手册为准 WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(200); Serial.print("."); } Serial.print("\nConnected, IP: "); Serial.println(WiFi.localIP()); udp.begin(8888); // 本地端口,随意指定 }这里有几个细节值得说。第一,WiFi.begin在 BW16 上的连接速度比 ESP32 略慢,如果 RTOS 版本或 SDK 版本不同,可能表现为连不上或者要等好几秒,多试几个版本能找到最稳的。第二,脑电模块的波特率不一定是 57600,以你手上模块的 datasheet 为准,常见还有 115200,设置错了只能收到乱码。第三,udp.begin(8888)指的是 BW16 本地接收端口,但本方案中 BW16 只发不收,这个端口不绑定也行,保留是为了将来做反向控制。
3.3 脑电帧解析与打包发送
串口读数据不能直接在loop里一个字节一个字节地处理,因为 WiFi 协议栈偶尔会阻塞,单片机可能几百毫秒腾不出手,这段时间串口缓冲区的数据会堆积甚至溢出。我的做法是:开一个环形缓冲区,串口中断把字节填进去,主循环负责从缓冲区取数据做帧解析。这样即使 WiFi 阻塞一下,数据也不会立刻丢。
帧解析的核心逻辑是找帧头。TGAM 协议里帧头是0xAA 0xAA,但这两个字节也可能出现在负载里,所以必须配合长度字段和校验和来判定“真正的帧头”。解析出完整帧之后,提取原始波采样点或统计值,再封装成我自定义的 UDP 帧格式发送。核心发送代码如下:
void sendEEGFrame(uint8_t type, uint8_t* payload, uint8_t len) { uint8_t txBuffer[32]; txBuffer[0] = 0xB6; txBuffer[1] = 0x7E; txBuffer[2] = len; txBuffer[3] = type; txBuffer[4] = (uint8_t)(seq >> 8); txBuffer[5] = (uint8_t)(seq & 0xFF); memcpy(&txBuffer[6], payload, len); txBuffer[6 + len] = calcCRC8(txBuffer, 6 + len); seq++; udp.beginPacket(cydIP, 8888); udp.write(txBuffer, 6 + len + 1); udp.endPacket(); }cydIP是 ESP32-CYD 在局域网里的 IP 地址,我建议在程序里用一个常量写死,测试阶段比 mDNS 省心得多。发送策略上,我没有攒包,而是每解析到一个原始波采样点就立刻发一包。因为每包很小,UDP 的包间隔足够支撑 512Hz 的发送频率,实测下来 CYD 收到的数据流是均匀的。如果你把多个采样点塞进一包再发,省一点包头开销,但会增加接收端缓冲区的复杂度,对 1KB/s 的数据量来说完全没必要。
BW16 作为一个“串口到 WiFi”的桥,调试时有个好习惯:同时在Serial上打印关键计数,比如每秒发了多少包、平均包大小、出错次数。这些日志不占用业务链路,但能帮你快速定位“问题出在模块、串口还是 WiFi”。
4. 展示端实操:ESP32-CYD 的屏幕绘制与网页推送
4.1 TFT_eSPI 配置:CYD 的引脚避坑指南
ESP32-CYD 这块板子屏幕驱动是 ILI9341 控制器的 2.4/2.8 寸 TFT,但它的引脚定义比较“反人类”,跟标准 ESP32 开发板上的 SPI 引脚完全不一样。用 TFT_eSPI 库时,必须手动修改User_Setup.h,把引脚定义改成 CYD 的实际接线。
我实测下来的 CYD 屏幕引脚对照表如下:
| 信号 | GPIO |
|---|---|
| TFT_MISO | 12 |
| TFT_MOSI | 13 |
| TFT_SCLK | 14 |
| TFT_CS | 15 |
| TFT_DC | 2 |
| TFT_RST | 21 |
| TFT_BL | 5 |
User_Setup.h里最关键的部分是这样一段:
#define ILI9341_DRIVER #define TFT_CS 15 #define TFT_DC 2 #define TFT_RST 21 #define TFT_MISO 12 #define TFT_MOSI 13 #define TFT_SCLK 14 #define LOAD_GLCD #define LOAD_FONT2 #define SPI_FREQUENCY 27000000有一个坑必须单独讲:CYD 的屏幕背光默认是关闭的,很多人刷完代码发现屏是黑的,以为屏幕坏了。实际上要把 GPIO5 设为输出并拉高,背光才会亮。我习惯在setup最开始加两行:
pinMode(5, OUTPUT); digitalWrite(5, HIGH);另外,CYD 板载的触摸芯片(XPT2046)跟屏幕共用一部分 SPI 引脚,但本方案不需要触摸,不用初始化。如果你的开发板型号是 ESP32-2432S028R 或 ESP32-2432S024R,上述引脚定义通用,但也建议拿到板子后先看一眼丝印确认版本。
4.2 屏幕波形绘制
CYD 端收到 UDP 帧之后,经过 CRC 校验和帧协议解析,拿到原始波采样点,下一步就是把它画到屏幕上。因为 320 像素宽的屏幕在横向画 320 个采样点刚好是一屏,我直接在内存里维护一个长度为 320 的环形数组,每来一个新采样点就画一条竖线连接到前一个点,视觉效果就是一条连续滚动的波形。
核心绘制代码大概长这样:
int buf[320]; int bufIndex = 0; void drawWave(int value) { int x_prev = (bufIndex + 319) % 320; int x_next = bufIndex; int y_prev = map(buf[x_prev], -2000, 2000, 20, 220); int y_next = map(value, -2000, 2000, 20, 220); tft.drawLine(x_prev, y_prev, x_next, y_next, TFT_GREEN); tft.drawPixel(x_prev, y_prev, TFT_BLACK); // 清掉上一帧残留 buf[bufIndex] = value; bufIndex = (bufIndex + 1) % 320; }map函数把 -2000 到 2000 的原始波值映射到屏幕 Y 坐标,这个范围可以根据你的脑电模块实际输出幅度调整。我在调试时发现,原始波幅度经常只有正负几百,如果固定映射到 ±2000,波形会变成一条几乎水平的线,看起来很不明显;后来我把范围改成 ±800,波形细节就清楚多了。建议在代码里做成可调常量,方便现场改。
绘图过程有一个性能问题要注意:TFT_eSPI 默认每次调用 draw 都会往外设刷新,320 像素宽度的波形要画 320 个点,加上清旧点,刷新一屏大约需要几十毫秒。如果同时还要跑 WiFi 和 WebSocket,偶尔会出现波形卡顿。解决办法是降低 SPI 频率或者用局部刷新区域,也可以只画“新点替换旧点”而不是全屏重绘。实测下来,如果SPI_FREQUENCY用 27MHz,且只更新当前点,屏幕刷新率能稳定在 20fps 以上,肉眼看起来相当顺滑。
4.3 网页端实时显示:让浏览器当“第二块屏”
“屏幕”只覆盖了 CYD 这块本地屏,我还想在浏览器里看同一份数据,这样就实现了标题里说的“到网页”。做法是在 CYD 上同时跑一个 WebServer 和一个 WebSocketServer,浏览器访问 CYD 的 IP,拿到一个静态 HTML 页面,页面里的 JavaScript 用 Canvas 画波形,数据通过 WebSocket 实时推送。
CYD 上 WebSocket 初始化的核心代码大致如下:
#include <WebSocketsServer.h> WebSocketsServer wsServer(81); void setup() { // WiFi、屏幕初始化 ... wsServer.begin(); wsServer.onEvent(webSocketEvent); } void loop() { wsServer.loop(); // UDP 接收、屏幕绘制 ... }浏览器里的 HTML/JS 部分更简单,核心就两段逻辑:建立 WebSocket 连接,收到消息后把采样值压进数组并重绘 Canvas。这段代码在网上有很多现成模板,我这里就不贴完整页面了,只提醒几个关键点:
- 页面一定是从 CYD 上托管的,浏览器和 CYD 在同一个局域网,直接访问
http://192.168.x.x就行; - WebSocket 地址写成
ws://<CYD_IP>:81,注意是ws不是http; - Canvas 绘图的性能比 TFT 屏好很多,浏览器里可以轻松画 1000 个点的滚动队列,不需要做性能优化。
这里有个很自然的疑问:CYD 又要接收 UDP 又要画屏又要当 WebSocket 服务器,会不会卡死?实测下来完全不会。因为数据量只有 1KB/s,WebSocket 广播又是很小的数据帧,ESP32 的主频跑这些任务绰绰有余。唯一要注意的是 Arduino 的loop里不能有阻塞调用,比如delay(1000)之类的,一旦阻塞,UDP 接收和 WebSocket 握手都会受影响。
浏览器端的价值在于,它可以让你不用蹲在 CYD 屏幕前看数据,电脑上开着网页就能远程观察,而且还能叠加 FFT 频谱、数据记录等更复杂的功能,算是把这条链路的“上限”拉高了不少。
4.4 四步联调法:先大声喊,再无线喊
任何无线链路项目,我最忌讳的就是“一次全连上再调”。正确做法是分段验证,每一段都能独立证明“上一段的数据确实传到了这里”。我这次用了四步联调法,效果非常好。
第一步,脑电模块直连电脑。用 USB 转串口模块把脑电模块的 TX 接到电脑,打开串口助手,确认能看到原始的AA AA帧,并且信号质量字段是正常的。这一步如果过不了,后面全都不必做,因为源头就是坏的。
第二步,BW16 串口打印。把脑电模块接到 BW16,写一小段程序直接把Serial1收到的原始字节透传到Serial,电脑串口助手看看是不是和第一步看到的一样。这一步能确认 BW16 的硬件串口接线没问题。
第三步,CYD 串口打印 UDP 内容。给 CYD 写个临时程序,只收 UDP 包并把收到的字节打印到串口,用电脑看它收到的帧头、序列号是否连续。这一步能确认 WiFi 链路通了,而且能看出丢包率。
第四步,全链路显示。在 BW16 上把 UDP 发送和脑电模块解析同时打开,在 CYD 上把屏幕绘制和 WebSocket 同时打开,浏览器刷新网页,确认屏幕和网页都有波形滚动。这四步每一步都有明确的“通过标准”,不用猜,不用靠玄学。
我在第三步卡了最久。当时 CYD 串口一直打印不出数据,排查了半天才发现是 WiFi 连接用了加密方式不匹配,CYD 连路由器一直失败。这种问题在无线联调里非常典型,所以第三步一定要在 CYD 的启动日志里确认WiFi.localIP()打印出来的 IP 不是 0.0.0.0。
5. 问题排查与避坑实录
5.1 WiFi 断流、数据丢包与串口阻塞
做无线链路最怕的就是“跑十分钟之后开始断”。我实际遇到的一个典型问题是:BW16 长时间运行后,WiFi 连接还在,但 UDP 发送的包全部丢失,只有重启才能恢复。
后来分析发现,原因是 BW16 的 WiFi 协议栈在低数据速率长连接情况下,会进入某种省电状态,而我的代码一直没有正确唤醒它。解决办法是在 WiFi 初始化后显式禁用省电模式,并定期发送一个空 UDP 保活包。如果用的是 Arduino SDK,可以在WiFi.begin后调用WiFi.setSleepMode(WIFI_NONE_SLEEP)或类似 API,具体名称随 SDK 版本不同略有差异,可以搜一下你的 SDK 文档。
丢包判断不要靠感觉,要看序列号。我在每一帧里都带了 16bit 序列号,接收端可以统计连续收到的帧里序列号跳变的情况。实测稳定状态下,局域网内丢包率几乎为 0,但如果路由器信号弱,丢包率可能一下就跳到 5% 以上,波形会出现明显的“梳齿状”断裂。
串口阻塞问题集中在 BW16 上。如果你在loop里用delay(10)或者频繁调用 WiFi 相关函数,串口中断被屏蔽的时间一长,环形缓冲区就会溢出。我的经验是:环形缓冲区至少开 512 字节,并且主循环里每次只从缓冲区取一部分字节处理,不要一次把所有字节都取出来,避免长时间占用 CPU。
5.2 屏幕背光、花屏与刷新问题
屏幕相关的坑,第一个就是背光。如果你代码跑起来 CPU 在动、WiFi 也连上了,但屏幕黑漆漆的,99% 是 GPIO5 没有拉高。第二个坑是花屏。CYD 的花屏原因通常有三个:电源不稳、SPI 速率过高、接线接触不良。
我测试时发现,如果用充电宝给 CYD 供电,同时又把屏幕刷新率拉满,偶尔会出现屏幕顶部闪烁或者花条。后来用稳压电源供电,并且把SPI_FREQUENCY从 40MHz 降到 27MHz,花屏问题就消失了。TFT 的 SPI 速率不是你写多少就一定能跑多少,还要看杜邦线的质量和长度,越长的杜邦线,越要降低 SPI 速率。
第三个坑是刷新“残影”。屏幕上画波形时,如果不手动清除上一帧的旧点,波形会拖出很长的尾巴。我在绘制代码里用“先画新线,再擦旧点”的方式解决,实际效果比全屏清零更好,闪烁感也小很多。
5.3 脑电信号质量差:50Hz 工频和电极接触
链路通了之后,最大的敌人变成了信号质量本身。我第一次戴上电极看波形时,屏幕上基本是一条 50Hz 的粗毛刺,根本看不出 alpha 波和眨眼伪迹的区别。这里主要两个原因。
第一是工频干扰。50Hz 交流电会通过空间耦合进人体和导线,脑电信号本身才几微伏,工频干扰可能几百微伏,不做处理完全没法看。我的做法是:让所有设备用电池供电,彻底断开市电地回路,同时把脑电模块的参考电极贴在耳后或额头相对骨骼位置,干扰会明显下降。如果还是不行,就在软件里加一个 50Hz 陷波滤波器,这个在 CYD 端做也行,但会增加复杂度,能靠物理手段解决就先靠物理手段。
第二是电极接触。干电极不涂导电膏,阻抗动辄几百千欧,信号根本进不去。换成 Ag/AgCl 湿电极并涂导电膏之后,阻抗降到几十千欧,信号质量肉眼可见地提升。脑电模块输出的“信号质量”字段这时候特别有用——我把它打印在屏幕上,作为实时调试依据。
还有一个容易忽略的点:脑电模块和 BW16 之间不能有长线飞线。我给原型调试时用了一根 20cm 杜邦线连接模块和 BW16,结果串口数据没问题,但脑电模块的模拟前端被这条“天线”接收到的环境噪声搞得很惨。后来把线剪短到 10cm 以内,并且让模块和 BW16 尽量贴近,信噪比立刻改善。
5.4 常见问题速查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 串口全是 0xFF/乱码 | 未共地、波特率错、TX/RX 接反 | 共地、核对波特率、交换 TX/RX |
| BW16 连不上 WiFi | 加密方式不匹配、SSID 写错 | 检查日志、确认 AP 配置 |
| CYD 屏幕黑屏 | 背光未开启 | GPIO5 拉高 |
| 屏幕花屏 | 电源不稳、SPI 速率过高 | 用稳压电源,降低 SPI 频率 |
| 波形断开/跳变 | UDP 丢包 | 检查信号强度和序列号 |
| 波形全是细刺 | 工频干扰 | 电池供电、缩短导线、检查电极 |
| WebSocket 连不上 | 浏览器跨网段、CYD IP 错误 | 确认同一局域网、检查 IP |
| 运行半小时后无数据 | WiFi 省电模式 | 禁用省电,增加保活包 |
| 波形幅度太平 | 映射范围过大 | 调小 map 的 Y 轴范围 |
6. 还能怎么玩:后续扩展与个人体会
链路打通之后,最大的收获其实不是“能看脑电波形了”,而是你拥有了一条从任意串口传感器到屏幕和网页的完整通路。想换温湿度、心率、IMU,只改采集端的解析函数就可以了,传输和显示完全不用动。我后来把同一个框架接了一个心率模块,改了几行代码,屏幕上就开始跳脉搏波形了,整个过程不到半小时。
后续想在这个基础上继续扩展,我比较推荐几个方向:一是给 CYD 插上 SD 卡,把原始脑电数据落盘,做离线分析;二是在浏览器端加 FFT,把时域波形变成频域 α/β/θ 波段的能量条,这个对实时反馈项目非常有用;三是把 BW16 换成电池供电的小型穿戴形态,做成一个真正的“无线脑电采集头带”;四是把网页再做一层控制,比如通过 WebSocket 反向给采集端发命令,实现多通道采集的开始/停止。
我个人在反复调试中的体会是:做这类原型链路,别把“无线”想得太玄,它就是一根看不见的串口延长线。重点永远是数据格式、时序、校验和排查方法。只要把这四件事做好了,换了再花哨的传感器,对你来说也只是换一堆字节而已。最后分享一个小技巧:调试时在 CYD 屏幕上同时显示当前丢包率、信号质量和帧率,这三个数字比什么波形都重要,它们能在一瞬间告诉你链路健不健康。这条“从脑电模块到屏幕和网页”的路,我走了一遍,坑踩了不少,但走通之后,后面就是一片开阔地。