☰
从零搭建无线EEG原型:BW16+ESP32-CYD实时波形显示与网页推送
2026/10/2 1:29:53 网站建设 项目流程

如果你做过脑电采集的原型实验,大概率经历过这种尴尬:电极已经贴好,被测试者坐在椅子上不敢乱动,而为了看波形,你得歪着头去够桌上的屏幕,或者把笔记本架在旁边。线一多,人站起坐下都得小心翼翼。这个痛点直接促成了我这条链路:用一个脑电前端模块把人头皮上的微弱信号变成数字流,交给 BW16,通过 Wi-Fi 送出去;接收端用一块 ESP32-CYD,既在本地 TFT 屏上画实时波形,又顺带开一个网页,让手机和电脑能同时看到数据。整套链路跑通后,脑电数据从"必须插线"变成了"随手可看"。这篇文章就记录我从零搭这条无线 EEG 原型链路的全过程,包括硬件选型、通信协议设计、屏幕绘制、网页推送,以及中途踩过的那些坑。如果你正在做 BCI/脑电硬件原型、生物电采集,或者只是想在 ESP32 上做实时波形显示,这篇的思路可以直接复用。

1. 为什么非要无线:这条原型链路想解决的问题

1.1 有线方案的三个痛点

最初我用的是最朴素的做法:脑电模块通过 USB 转串口直接连电脑,在电脑上开一个串口绘图工具看波形。看起来简单,实际上问题很多。

第一个痛点是实验场景被固定死。脑电采集本身就需要被测试者保持相对放松的状态,但一根 USB 线把人和电脑绑在一起,稍微离远一点就得加延长线,延长线一加,干扰和压降都是麻烦。第二个痛点是数据转发路径单一。电脑作为唯一接收端,其他人想看实时波形,只能挤在屏幕前面,或者我再开一套远程桌面,延迟和画质都不理想。第三个痛点是布线本身会引入工频干扰。虽然是数字信号,但线材在长距离传输时更像一根天线,50Hz 及其谐波很容易串进来,后期滤波要多做不少工作。

无线方案正好把这三个痛点一起解决。发送端跟着人走,接收端可以放在任意位置;网页推送天然支持多人同时查看;短距离无线传输省掉了那根长线,干扰源少了一个。对原型验证来说,这已经足够划算。

1.2 一条链路到底要拆成几个环节

我在动手之前先把链路拆成了四段,每一段的职责都很单一:

  • 采集段:脑电前端模块,负责把电极上的差分信号放大、滤波、模数转换,输出数字采样值。
  • 发送段:BW16,监听脑电模块的串口输出,把采样值按协议组帧,通过 Wi-Fi UDP 发送。
  • 接收显示段:ESP32-CYD,接收 UDP 数据,在 2.8 寸 TFT 屏幕上绘制滚动波形。
  • 网页推送段:同一个 ESP32-CYD 上跑 WebServer 和 WebSocket,把同样的数据推给浏览器。

这里有一个容易被忽略的设计决策:为什么接收端不做成纯网关,而是直接带屏幕?我的理由是 ESP32-CYD 本身就有完整的 ESP32 算力和屏幕,它在接收的同时把数据显示出来,调试时不需要额外开电脑;网页服务只是它顺手多干的一份活。如果以后想升级成集中式网关,把这个 ESP32 换成树莓派或者直接让电脑接收,协议不变,成本很低。

1.3 原型系统和医疗级系统的边界

我必须先把话说在前面:这条链路做的是原型验证和数据传输演示,不是医疗级脑电设备。医疗级 EEG 系统在电极阻抗、共模抑制比、电气安全、数据完整性和校准方面有一整套标准,原型链路完全达不到。它的价值在于把"采集→传输→显示→远程访问"这条数据管道跑通,让后续算法开发有真实数据可用。带着这个边界去做,就不会在选型上过度纠结。

2. 硬件选型:为什么是 BW16 和 ESP32-CYD

2.1 BW16 作为发送端的理由

BW16 的核心是 Realtek RTL8720DN,双核 MCU,标称主频在 100MHz 级别,片上 SRAM 512KB 左右。它最亮眼的配置是除了支持 802.11 a/b/g/n 的双频 Wi-Fi(2.4G/5G)之外,还带 BLE5.0,这两样是同时共存的。对于发送端来说,这个配置非常契合:数据用 Wi-Fi 走,控制通道以后可以走 BLE,不需要外挂两套射频。

在 Arduino 生态里,BW16 可以在官方的 Ameba 开发板包里直接使用,整体开发体验和 ESP8266 很接近,有 WiFi 库、UDP 库、硬件串口,基本就是换个平台写一样的逻辑。价格上 BW16 也很便宜,带排针的模块十几块钱左右,做原型完全不用心疼。

对比一下其他候选:

方案优势不足结论
BW16双频 WiFi + BLE5.0,Arduino 支持,便宜资料相对少,社区小适合做发送端
nRF52840BLE 功耗低,生态成熟走 BLE 吞吐有限,做音频级数据流吃力更适合低采样率控制
ESP8266资料多,价格低只有 2.4G,没有 BLE,IO 少够用但扩展性弱
ESP32性能强,资料多只有 2.4G,功耗比 BW16 略高适合做接收端

最终让我选 BW16 的其实是那个双频选项。虽然我在这个项目里最终用的还是 2.4G,因为 ESP32-CYD 的 Wi-Fi 只支持 2.4G,但发送端留有 5G 能力,以后如果接收端换成树莓派或者电脑棒,我可以直接在 5G 频段上跑,避开家里 2.4G 大量 IoT 设备的互相干扰。

2.2 ESP32-CYD:接收端和显示端的二合一

ESP32-CYD 是市面上一类开发板的常见叫法,全称大概是 ESP32-2432S028R,也叫 Cheap Yellow Display。它的核心是一块 ESP32-WROOM-32 模组,双核 240MHz,带 2.4GHz Wi-Fi 和 BLE,板载一块 2.8 寸 240×320 的 ILI9341 TFT 屏,还带 XPT2046 电阻触摸、MicroSD 卡槽、RGB LED 和串口下载电路。整块板子带屏幕才四五十元,确实是"便宜又黄"。

接收端用 ESP32-CYD 的好处是少接一堆线。普通 ESP32 开发板要外挂 TFT 屏、触摸板,光接线就有十几根,接触不良的概率直线上升。CYD 把屏和主控焊在一起,引脚由 PCB 走线固定好,我只用关注逻辑不用关注物理连接。TFT_eSPI 库对 CYD 有现成的支持,改一下 User_Setup 就能跑。

2.3 整条链路的信号流和数据率估算

在写代码之前,我习惯先把数据率算一遍,避免后期被性能问题打脸。

假设脑电模块输出单通道 16 位有符号采样值,采样率 250Hz(原型足够覆盖到 100Hz 脑电频段,留出奈奎斯特余量)。那么裸数据率是:

  • 250 样本/秒 × 2 字节/样本 = 500 字节/秒,约 4kbps。

如果走批量 UDP 发送,我设计每 20 个采样帧打一个包,每秒只发 12.5 个包,包头开销按每帧 12 字节头部计算,总数据率大概是:

  • 20 帧 × (12 + 2) 字节 = 280 字节/包,实际发送 12.5 包/秒 ≈ 3.5KB/s ≈ 28kbps。

这个量级对 Wi-Fi 来说连零头都算不上。所以这条链路的瓶颈从来不是带宽,而是延迟抖动和丢包。后续就算升级到 8 通道、24 位 ADC,250Hz 采样,总数据率也就 8×250×3×12.5 ≈ 75kbps,BW16 依然毫无压力。这也是我在协议设计时重点盯丢包和时序,而不是压缩算法的原因。

3. 通信协议设计:先定规矩再写代码

3.1 为什么数据帧必须有头部、序号和 CRC

很多人在做无线传感器原型时会犯一个错误:直接把采样值拼成字符串通过 UDP 发出去,接收端按行解析。这种方案调试很快,但一旦开始丢包、粘包,你什么都查不出来。

我吸取了之前的教训,老老实实定义了二进制帧格式,所有整数统一大端序:

字段长度说明
帧头2 字节0xA5 0x5A,用于同步
帧类型1 字节0x01 数据帧,0x02 心跳帧
通道数1 字节当前 0x01,预留后续
序号2 字节单调递增,检测丢包
时间戳4 字节发送端 millis(),单位 ms
采样值2/3/4 字节按通道数和 ADC 位数扩展
CRC162 字节对前面所有字节计算

帧头的作用是接收端找同步起点,因为 UDP 虽然不粘包,但你一旦批量发送,接收端必须能在缓冲区里逐帧切割。序号是最关键的设计,接收端看到序号跳变,就能立刻知道丢了几个采样点,这样才能在绘图时决定是断开还是补插。CRC16 则是为了拦截 Wi-Fi 传输过程中偶发的 bit 翻转,脑电这种微弱信号,一个错误的采样值在图上就是一根刺。

3.2 UDP 批量发送:吞吐、延迟和丢包的三角权衡

传输层我选了 UDP 而不是 TCP。理由有两个。

第一,脑电是实时流数据,旧样本的可靠重传没有意义。TCP 遇到丢包时会重传和阻塞,导致后续数据在接收端排队,延迟从 10ms 飘到几百ms,这个抖动对波形显示和后续频谱分析都是灾难。UDP 丢就丢了,下一包会带着新数据继续来,实时性优先。

第二,UDP 可以批量打包。我实测过单帧单包、每秒 250 个 UDP 小包的发送方式,在普通家用路由器上丢包率能到 12% 左右。为什么?路由器对单位时间内的包数量有限制,小包尤其吃亏。改成 20 帧一包、每秒 12.5 包之后,丢包率基本可以忽略不计。批量发送的代价是一条采样点最多延迟 80ms(20/250),对原型显示完全可接受。如果你的实验对延迟敏感,可以调小批大小,比如 10 帧一包,延迟降一半,丢包率略升一点,这个参数值得专门测。

3.3 时间戳必须以发送端为准

这是整个协议里最容易被忽视、但影响最大的设计点。Wi-Fi 传输延迟是抖动的,接收端收到包的时刻并不能代表采样发生的时刻。如果接收端用本地 millis() 来当波形的 x 轴时间,你会看到波形在时间轴上忽快忽慢,周期性每 100ms 顿一下——那是路由器的 Beacon 帧在捣乱。

正确做法是发送端记录采样时刻,我这里用 BW16 的 millis(),放进帧里。接收端画图时,x 轴一律用帧内时间戳,丢弃本地接收时刻。这样波形在视觉上就稳定了,即使 WiFi 延迟在 10ms 到 100ms 之间抖动,波形内部的时间关系是原始采样时刻的真实关系。后续如果要送 MNE-Python 做分析,这个时间戳也能直接转成采样时间轴。

4. BW16 端实现:串口收、无线发

4.1 接线和电平检查

脑电模块和 BW16 之间我用的是 UART。脑电模块的 TX 接 BW16 的 RX,地线必须共地,波特率先在模块文档里确认,常见的脑电前端模块有 115200 和 921600 两种,我家这块是 115200。需要注意的是,如果脑电模块是 5V 逻辑电平,而 BW16 是 3.3V IO,TX 线上最好加一个分压电阻,不然长期跑容易烧 BW16 的 RX 引脚。如果模块本身是 3.3V 逻辑,直接接就行。

接线顺序我习惯先接地,再接信号线,最后接电源。脑电这类高阻抗信号源对电源纹波敏感,发送端供电尽量用 LDO 而不是 DCDC,我实测用手机充电头加劣质 DCDC 时,波形底噪明显变大。

4.2 固件逻辑和核心代码

BW16 端固件逻辑其实很薄:从串口读脑电模块的采样值,攒够 20 帧打包发 UDP。不要在发送端做滤波、FFT 之类的事情,RTL8720DN 虽然能跑,但会挤占接收采样的时间,反而增加时间戳抖动。

伪代码长这样:

#include <WiFi.h> #include <WiFiUdp.h> const char* ssid = "你的热点"; const char* pass = "你的密码"; IPAddress server(192, 168, 1, 50); // ESP32-CYD 的固定 IP const uint16_t port = 8266; WiFiUDP udp; uint8_t pktBuf[20 * 14]; // 20帧 × (14字节/帧) uint16_t seq = 0; void buildFrame(uint8_t* b, uint16_t sample) { b[0] = 0xA5; b[1] = 0x5A; b[2] = 0x01; // 数据帧 b[3] = 0x01; // 单通道 b[4] = seq >> 8; b[5] = seq & 0xFF; uint32_t ms = millis(); b[6] = ms >> 24; b[7] = ms >> 16; b[8] = ms >> 8; b[9] = ms; b[10] = sample >> 8; b[11] = sample; uint16_t crc = crc16(b, 12); b[12] = crc >> 8; b[13] = crc & 0xFF; seq++; } void loop() { int n = 0; while (n < 20) { if (Serial.available() >= 2) { uint16_t sample = Serial.read() << 8 | Serial.read(); buildFrame(&pktBuf[n * 14], sample); n++; } } udp.beginPacket(server, port); udp.write(pktBuf, n * 14); udp.endPacket(); }

实际工程里当然要处理串口断帧、缓冲区溢出这些边缘情况,但核心逻辑就是串口读两个字节、拼成采样值、组帧、攒批、发送。这里有个细节:我实测发现Serial.read()一次读一个字节,在 115200 波特率下,250Hz 采样意味着大约每 4ms 来一个采样值,CPU 完全跟得上,但如果以后升级到 8 通道,建议改成 DMA 或中断攒缓冲,避免丢串口数据。

4.3 发送端的实测表现

我把 BW16 放在房间一角,ESP32-CYD 放在三米外的桌上,中间隔了一堵非承重墙。测试结果:

  • RSSI 稳定在 -55dBm 左右,发送端固定 IP,没有重连情况。
  • 丢包率在批量发送模式下几乎为 0,连续跑 30 分钟只有一个序号跳变。
  • 端到端延迟(从 BW16 打时间戳到 ESP32-CYD 解析出帧)大约 5~20ms,波动主要来自路由器调度。

这个表现对原型链路来说足够稳。BW16 的弱点是 5GHz 频段穿墙能力弱,但这跟 ESP32-CYD 只支持 2.4GHz 没关系,反正这条链路默认走 2.4G。

5. ESP32-CYD 端:屏幕实时波形绘制

5.1 TFT_eSPI 的配置要点

ESP32-CYD 这块板子的屏是 ILI9341,走 SPI 接口。TFT_eSPI 库需要在User_Setup.h里配置引脚,不同批次的 CYD 引脚可能有差异,我手上这块的配置如下,供参考:

#define ILI9341_DRIVER #define TFT_WIDTH 240 #define TFT_HEIGHT 320 #define TFT_MISO 19 #define TFT_MOSI 23 #define TFT_SCLK 18 #define TFT_CS 5 #define TFT_DC 21 #define TFT_RST 16 #define TFT_BL 14 #define SPI_FREQUENCY 27000000

还有一个容易漏的:CYD 板载了 RGB LED 和 SD 卡槽,这几个外设和 TFT 共用 SPI 总线或 GPIO,如果你没初始化它们,默认拉高拉低都可能干扰显示。我直接不初始化 SD 卡,并把不用的 GPIO 设为 INPUT_PULLUP,能明显减少开机阶段的画面闪烁。

SPI 频率我试过 27MHz 和 40MHz,40MHz 在部分批次 CYD 上会出现横条纹,最后稳定在 27MHz。ESPs 的 SPI 刷 240×320 全屏在 27MHz 下大约 80ms,做实时波形显示够用,但如果你要跑每秒 30 帧动画,就得考虑局部刷新的优化。

5.2 滚动波形绘制策略:增量画列而不是全屏重绘

刚接上的时候,我图省事,每收到一个采样值就全屏清一次再画整条波形。效果就是屏幕疯狂闪烁,一秒钟能闪出残影,根本没法看。

正确的做法是增量绘制:屏幕宽度是 240 像素,我把最近 240 个采样值维护成一个环形缓冲区,每来一个新值,只擦掉最旧一列、画上新一列。SPI 刷一列也就几毫秒,刷新率轻松超过 30fps。

核心逻辑简写如下:

#define SCREEN_W 240 #define SCREEN_H 320 int16_t waveBuf[SCREEN_W]; int wavePos = 0; void plotSample(uint16_t sampleRaw) { int16_t y = map(sampleRaw, 0, 4095, 60, 280); // 按你的 ADC 量程调整 int oldX = wavePos; int newX = (wavePos + 1) % SCREEN_W; // 擦掉旧列,注意别把网格线一起擦掉 tft.drawFastVLine(oldX, 60, 220, TFT_BLACK); // 画新列 tft.drawPixel(newX, y, TFT_RED); // 可选:从上一列连一条线过来,让波形连续 int oldY = waveBuf[oldX]; tft.drawLine(oldX, oldY, newX, y, TFT_RED); waveBuf[newX] = y; wavePos = newX; }

Map 函数是 Arduino 自带的,做线性映射。我这里把 0~4095 的原始值映射到屏幕竖直方向的 60~280 像素,等价于留出上下边距写标题栏。波形是连续的还是点状的,取决于你的 EEG 采样率:250Hz 在 240 像素宽的屏幕上大概每 0.96 秒滚一屏,点与点之间间隔约 4ms,连续画线看起来更舒服。

5.3 屏幕上的状态信息和触摸操控

光画波形还不够,我在屏的顶部留了一条状态栏,显示当前 RSSI、丢包率、采样序号、以及与发送端的时间戳差值。这四个值来自接收端解析帧时的统计,不需要额外通信。丢包率用滑动窗口统计最近 1000 帧的序号跳变数,画在屏幕上可以直观看到链路质量。

触摸屏我加了三块区域按钮:暂停/继续、放大/缩小、清除波形。XPT2046 的触摸读取用 TFT_eSPI 的getTouch(),注意电阻触摸需要校准,我在启动时让用户点屏幕四角,存下校准参数到 NVS。暂停功能很有用,当你想细看某一段波形时暂停滚动,数据仍然在后台接收,只是不画而已。

6. 网页端:WebSocket 实时显示与远程监控

6.1 ESP32 同时跑 WebServer 和 UDP 接收

ESP32-CYD 在接收 UDP 的同时,还起了两个服务:一个 HTTP WebServer 跑在 80 端口,提供静态网页;一个 WebSocket 服务跑在 /ws 路径,负责推送实时数据。ESP32 双核 240MHz,这点并发完全扛得住。

网页端我坚持用 WebSocket 而不是 SSE 或者轮询。WebSocket 是双向全双工,而且可以直接传二进制帧,正好匹配我定义的 0xA5 0x5A 帧格式。浏览器收到二进制帧后,用 DataView 解析,和 C 端解析逻辑一致,相当于同一个协议两端复用。

需要注意一个细节:ESP32 的 WebServer 库和 WebSocketsServer 库可能会抢 WiFi 事件处理,我建议 WebSocket 服务放在一个单独的任务里,或者至少在loop()里先webSocket.loop()再处理 UDP,避免 WebSocket 的 ping/pong 超时把客户端踢掉。

6.2 浏览器端解析和绘制

网页部分我写了一个单页 HTML,里面用 canvas 画波形。关键代码如下:

const ws = new WebSocket(`ws://${location.host}/ws`); ws.binaryType = 'arraybuffer'; let latestSample = null; ws.onmessage = (e) => { const dv = new DataView(e.data); // 假设一个包就是多个帧拼在一起 for (let i = 0; i + 14 <= dv.byteLength; i += 14) { if (dv.getUint8(i) !== 0xa5 || dv.getUint8(i + 1) !== 0x5a) continue; latestSample = dv.getInt16(i + 10, false); // 大端序 } }; function drawLoop() { requestAnimationFrame(drawLoop); if (latestSample === null) return; const y = mapRange(latestSample, 0, 4095, canvas.height * 0.2, canvas.height * 0.8); pushToCanvas(y); }

这里有个关键技巧:onmessage里只保留最新的采样值,drawLoop每帧界面刷新时取一次。WebSocket 推送频率是 250Hz,而浏览器 canvas 刷新上限是 60fps,如果每来一个采样就画一次,canvas 队列会积压,延迟越积越大。用"最新值覆盖"的方式,视觉上最多丢一两个中间点,但延迟始终稳定在几十毫秒以内。这就是典型的实时流处理里的"latest-wins"策略。

6.3 多人查看和数据落盘

网页的好处是手机、平板、电脑都能同时打开。我在 ESP32 的 WebSocket 服务器里维护一个客户端列表,每收到一个 UDP 包就广播给所有连接的客户端。广播前先检查每个客户端的发送缓冲区长度,如果超过阈值就跳过该客户端,防止一个慢设备拖垮整个服务器。

网页端我还加了一个"开始录制"按钮。点击后,浏览器把所有通过 WebSocket 收到的采样值按时间戳缓存起来,结束时导出一个 CSV 文件。CSV 第一列是发送端时间戳,第二列是原始采样值,后续可以直接喂给 Python 脚本做频域分析或最小范数估计。注意浏览器页签切到后台时 JS 定时器会被节流,录制期间尽量保持页签在前台。如果是严肃的数据采集,我还是建议在 ESP32-CYD 上接 SD 卡落盘,后面会说到。

7. 调试实测:几个让人头大的坑和完整排查过程

7.1 波形变"梳子":UDP 丢包的排查链路

第一次跑通时,屏幕上波形每隔一段就出现一个很大的竖直跳变,看起来像梳子齿。很多人第一反应是脑电模块出了问题,或者是电源干扰。我的排查链路是这样的:

第一步,先看脑电模块直接接串口时波形是否正常。用 USB 转串口连模块,电脑端画图,波形正常。这说明问题出在无线链路段。

第二步,在 ESP32-CYD 端打印接收到的序号,统计跳变。我加了一个临时调试代码,把最近 100 个包的序号差值打出来,发现几乎每 3~5 个包就有一个缺口,丢包率大概在 8%~12%。

第三步,查 RSSI 和 ping。RSSI -55dBm,信号没问题;ping BW16 丢包率为 0。这说明丢包不是物理层的,而是路由器在转发高频小包时的队列丢弃。

第四步,就是我前面说的批量发送改造。把单帧单包改成 20 帧一包后,丢包率掉到 0.1% 以下,波形立刻顺滑。这个问题的根因不是信号弱,而是包速率太高。

7.2 屏幕刷新卡顿和残影

第二个坑是屏幕刷新率的优化。硬件链路的根因是TFT_eSPI默认会做全屏 DMA 刷新,而我只画了一条线,却让 GPU 把整块屏都刷了。改成drawFastVLine局部擦除之后,刷新从 80ms 降到 1ms 左右,肉眼完全感觉不到刷新延迟。

另外,canvas 绘图中我踩了一个小坑:TFT_eSPI默认坐标是左上角为 (0,0),y 轴向下,而脑电波形习惯上是负值在下方。我的映射函数里用了map(raw, 0, 4095, 280, 60),把 0 映射到屏幕下方,4095 映射到屏幕上方。如果你写成 60 到 280 的升序,波形显示就是镜像的,别问我怎么知道的。

7.3 WebSocket 积压导致网页延迟越来越大

网页端出现的问题是:打开页面半小时后,波形明显"迟到",手机页面上看到的波形比屏幕上慢了好几秒。我一开始以为是 ESP32 卡了,后来在浏览器里打印latestSample的更新时间戳,发现数据一直在更新,但绘制延迟在增长。

原因就是我在 6.2 节说的:onmessage里每个采样点都调用canvas绘制函数,浏览器帧循环来不及消费,canvas 内部缓冲越来越多。改成"消息只存最新值,绘制循环每帧取一次"之后,延迟稳定在 100ms 内。这是做任何实时 Web 可视化都应该记住的经验。

7.4 看似玄学的时间戳抖动问题

还有一个很难察觉的问题:波形虽然画出来了,但用示波器看采样间隔,有时候两个采样点之间差了 40ms,有时候只有 5ms。这个问题如果不做频谱分析根本发现不了。

根因在 3.3 节已经说过:WiFi 的 Beacon 帧每 100ms 广播一次,发送期间 BW16 的 UDP 发送会被挤占,导致采样点到达接收端的时间不均匀。我验证的方法是在接收端打印帧内时间戳与本地接收时间的差值,看到每 100ms 有一个明显跳变,然后改用帧内时间戳画图,问题消失。这里的关键不是修掉 WiFi 抖动(做不到),而是让显示和记录都基于采样时刻的时间戳,把传输抖动从信号路径中隔离出去。

8. 向前一步:从原型到 EEG 源定位与最小范数估计

8.1 单通道只是数据管道,不是完整 EEG 系统

这条链路现在跑的是单通道脑电,它证明的是"采集→无线→显示→网页"的数据管道是通的。但如果你关注 EEG 源定位(source localization),单通道远远不够。源定位的目标是根据头皮表面的电位分布,反推出大脑内部神经源的位置和强度。头皮上有多少个测点,通常就需要多少个通道,标准做法是 10-20 系统的 19 通道、64 通道,甚至高密度 128 通道。单通道只能看到额叶或枕叶某一点的信号,无法提供空间分辨力。

所以这条原型链路的真正价值,是为后续多通道系统打好传输和可视化基础。协议里的"通道数"字段我现在只填 0x01,但帧格式已经预留了;BW16 的吞吐能力跑 8 通道 24 位 250Hz 也绰绰有余。真正需要换掉的硬件是前端采集模块:从单通道模组换成一枚 ADS1299 这样的 8 通道 24 位生物电采集芯片,后面接一个多路复用器,电极按 10-20 系统摆放,链路其余部分不用大改。

8.2 最小范数估计需要什么数据

如果你想把这条路走到底,做 EEG 源定位,网上搜"EEG 源定位 最小范数估计"基本都会指向 MNE-Python。最小范数估计(Minimum Norm Estimate, MNE)是分布式源重构里最常用的方法之一,它不是一个能直接吃 UART 数据的算法,而是一整套数据管线。

它需要三样东西:

第一,多通道脑电数据,采样率和通道数必须满足空间采样的基本要求,通道数太少源定位结果不稳定。第二,头模型和电极位置。被测试者的头部几何模型通常用模板头模(如 ICBM152),电极坐标要按 10-20 系统换算成 MRI 空间坐标。第三,预处理后的干净数据段。基线漂移、眼电伪迹、工频干扰都要先处理,否则噪声会让最小范数估计解出假源。

MNE 的数学核心可以简化理解成求解一个线性逆问题:设头皮电位为 M,源空间电流为 J,正向模型(volume conductor)为矩阵 G,那么 M = G J + noise,最小范数估计就是在约束 J 的能量尽量小的前提下,解 J = argmin ||M - G J||² + λ||J||²,其中 λ 是正则化参数。实际使用的 MNE-Python 会做得更精细,比如用协方差矩阵加权、对不同源深度做归一化。但前提是数据质量,你给它一堆带 WiFi 丢包的脏数据,算出来的源位置就是纸上谈兵。

8.3 从原型到正式数据采集的建议

我个人的建议是:如果你想用这套链路做 EEG 源定位,至少要做三件事再开始。

第一,在 ESP32-CYD 上加 SD 卡记录。把 UDP 收到的原始帧按高效二进制格式存到 microSD,而不是靠浏览器缓存导 CSV。WiFi 链路适合实时监控,但正式分析的数据必须在采集端实时落盘,避免传输丢包污染数据。我设计了帧序号,落盘后可以用脚本按序号补齐或剔除异常段。

第二,在采集端加数字滤波确认数据质量。BW16 上只做轻量级工频陷波(50/60Hz)和直流基线去除,不做重处理,因为重处理会挤占采样时间。真正的滤波放在数据回放阶段,用 MNE-Python 的filter()一步到位。

第三,先把采样率和通道数盯死。250Hz 对常规脑电源定位偏低,建议至少 500Hz 起步,因为源定位对高频瞬态成分比较敏感;通道数至少 16,不然头表空间采样不足,MNE 的结果可信度很低。BW16 和 ESP32-CYD 在这一层的角色不变:BW16 负责把多通道数据完整送达,ESP32-CYD 负责可视化与落盘。

我个人在实际操作中的体会是,这类原型链路的成败往往不在硬件性能,而在你把"实时性"和"可靠性"这对矛盾想清楚了没有:实时显示用 UDP 批量发送和 latest-wins,数据落盘用帧序号和时间戳兜底,两边互不干扰。还有就是波形显示类的项目,一定要把时间戳的归属关系想明白,谁记录采样时刻,谁负责画图,这个归属错了,后面所有分析都是错。如果你也正在搭类似的无线脑电原型,可以从这条单通道链路起步,跑通数据管道后再逐步加通道,每一步都有清晰的验证标准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询