1. 项目缘起与整体链路设计
脑电采集这件事,早年给人的印象就是一堆线缆加一台笨重放大器,被试坐在屏蔽室里连头都不敢动。这几年消费级 EEG 模块越来越便宜,很多做嵌入式、做交互艺术、做睡眠监测的朋友都开始自己搭原型链路。但真正动手之后你会发现,最麻烦的往往不是电极贴得好不好,而是数据怎么从脑袋上那小块板子,稳定地送到你能看见的屏幕上。有线串口当然最省事,可被试一动线就扯,采集场景直接被限制死。
我这个项目的出发点很朴素:手上有一块常见的单通道/多通道脑电采集模块,输出走 UART;另有一块 BW16 模块,支持 BLE;还有一块 ESP32-CYD,也就是那种自带 2.4 寸彩屏和触摸的廉价开发板。我想搭一条从脑电模块到屏幕、再到网页的无线原型链路,让采集到的波形能实时显示在 CYD 的屏幕上,同时通过 Wi-Fi 推到浏览器里看。整条链路的关键词就是BW16、ESP32-CYD、EEG、BLE、UART这五个。
先说清楚这条链路解决什么问题。脑电模块负责模拟前端采集和数字化,它通常通过 UART 把采样点吐出来;BW16 在这里扮演的是无线透传桥的角色,把 UART 数据通过 BLE 发出去;ESP32-CYD 作为接收端和显示端,一边解析数据画波形,一边起一个轻量 Web 服务把数据推到网页。这样做的价值在于:被试可以相对自由地活动,屏幕端可以放在桌上实时看,网页端可以远程或者多设备查看。适合谁来参考?做可穿戴生理信号采集的、做 BCI 原型的、做交互装置的,以及单纯想练手 BLE + UART + Web 这套组合的嵌入式爱好者。
为什么选 BW16 而不是直接用 ESP32 做无线?这是很多人第一个会问的问题。BW16 基于 RTL8720DN,双频 Wi-Fi 加 BLE 5.0,它的 BLE 协议栈在透传场景下非常省心,AT 固件就能把 UART 数据直接映射到 BLE 特征值上,几乎不用写协议代码。而 ESP32-CYD 的优势在于它自带屏幕和触摸,省掉了一整套显示外设的接线和驱动调试。两者分工明确:BW16 管无线桥接,CYD 管接收、显示和 Web 服务。这种职责分离的设计,让每一端的调试都相对独立,出问题容易定位。
链路的数据流大致是这样:EEG 模块 UART TX 接 BW16 的 UART RX,BW16 把数据打包成 BLE 通知发出去;ESP32-CYD 作为 BLE 中心设备扫描并连接 BW16,订阅对应的通知特征值,收到数据后解析成采样点,一路送屏幕刷新,一路缓存后由 Web 服务推给浏览器。整条链路里,UART 负责板间短距传输,BLE 负责空中桥接,Wi-Fi 负责网页分发,三种通信方式各司其职。
提示:原型阶段不要一上来就追求高采样率。单通道 250Hz 已经足够看清 alpha 波和眨眼伪迹,先把链路跑通,再谈性能优化。
2. 核心模块选型与通信原理拆解
2.1 为什么是 BW16 做 BLE 桥
BW16 模块出厂一般带 AT 固件,支持 UART 转 BLE 的透传模式。它的核心价值在于把复杂的 BLE 协议栈封装成几条 AT 指令。你不需要自己去建 GATT 服务、定义特征值 UUID、处理连接参数更新,只要配置好串口波特率和 BLE 名称,模块上电后就能被手机或开发板扫描到,连上之后 UART 进来的数据会以通知形式发出去。
从原理上讲,BLE 的数据传输基于 GATT 协议。BW16 透传模式下,它会暴露一个服务,里面有一个用于写的特征值和一个用于通知的特征值。中心设备往写特征值写数据,BW16 从 UART 吐出来;UART 进来的数据,BW16 通过通知特征值推给中心设备。这个模型非常适合 EEG 这种单向、连续、小包的数据流。每个采样点或者一小段采样打包成一个 BLE 通知,中心设备收到就解析。
这里有个关键参数要理解:BLE 连接间隔。它决定了中心设备和外设之间多久通信一次,最小可以到 7.5ms,常见配置在 15ms 到 50ms 之间。连接间隔越小,延迟越低,但功耗越高。对于 EEG 实时显示,我一般把连接间隔设在 15ms 到 30ms,这样波形刷新看起来是连续的,不会一卡一卡。如果连接间隔设到 100ms 以上,你会明显看到波形是分段跳变的。
2.2 ESP32-CYD 的角色与屏幕优势
ESP32-CYD 全称是 ESP32 Cheap Yellow Display,板载 ESP32-WROOM-32、2.4 寸 ILI9341 SPI 屏、电阻触摸、SD 卡槽和若干扩展 IO。它最大的好处是屏幕和主控已经集成好,你不用再为接线和背光电路操心。对于这个项目,CYD 要同时干三件事:跑 BLE 中心设备协议栈、驱动屏幕刷新波形、起 Web 服务。ESP32 双核正好可以分工,一个核侧重 BLE 和数据处理,另一个核跑显示和网络。
屏幕刷新这块要注意,ILI9341 走 SPI,刷全屏比较慢。画波形不要每来一个点就全屏重绘,而是用局部刷新加滚动的策略。常见做法是维护一个环形缓冲区,新数据来了之后只重绘波形区域,或者用双缓冲。CYD 的屏幕分辨率 320x240,横屏用起来画波形刚好,横轴放时间,纵轴放幅度。
2.3 UART 与 BLE 的数据衔接
EEG 模块和 BW16 之间是 UART,这是最传统的异步串行通信。UART 没有时钟线,靠双方约定的波特率同步,所以波特率必须严格一致,否则收到的全是乱码。常见 EEG 模块输出波特率有 115200、460800、921600 等。采样率越高、通道越多,需要的波特率越高。单通道 250Hz、每个采样 3 字节的话,数据率大约 750 字节每秒,115200 绰绰有余;但如果是 8 通道 1000Hz,数据率就上到 24KB/s 以上,这时候 115200 就不够了,得往上提。
UART 帧结构是起始位、数据位、校验位、停止位。绝大多数模块用 8N1,也就是 8 数据位、无校验、1 停止位。接线时记住TX 接 RX、RX 接 TX、共地,这是新手最容易翻车的地方。BW16 的 UART 电平一般是 3.3V,EEG 模块如果也是 3.3V 就可以直连;如果模块是 5V 电平,中间要加电平转换,否则可能损坏 BW16。
BLE 这边,数据以通知形式发出,每个通知能带的有效载荷有限。默认 ATT MTU 是 23 字节,去掉 3 字节头,实际能带 20 字节。可以通过 MTU 协商提高到 247 字节甚至更大。对于 EEG 数据,我建议在 BW16 侧做简单的打包,比如把若干个采样点拼成一包再发,减少通知次数,提高吞吐效率。但打包也不能太大,否则单包丢失影响就大。
| 通信环节 | 协议 | 典型参数 | 主要作用 |
|---|---|---|---|
| EEG 模块到 BW16 | UART | 115200/460800, 8N1 | 板间短距数据传输 |
| BW16 到 CYD | BLE | 连接间隔 15-30ms, MTU 协商 | 空中无线桥接 |
| CYD 到浏览器 | Wi-Fi + HTTP/WebSocket | 2.4G, 端口 80 | 网页数据分发 |
3. 硬件连接与固件配置实操
3.1 硬件接线与供电检查
先把三块板子的供电理清楚。EEG 模块一般有自己的供电要求,有的是 3.3V,有的是 5V,务必查手册。BW16 通常 3.3V 供电,注意它的峰值电流可能到 200mA 以上,别用太细的线或者电流能力不足的 LDO。ESP32-CYD 可以直接 USB 供电,它板载了稳压。
接线顺序我建议分两步。第一步只连 EEG 模块和 BW16:EEG 的 TX 接 BW16 的 RX,EEG 的 RX 接 BW16 的 TX(如果只需要单向采集,RX 可以不接),两边共地。第二步把 BW16 和 CYD 分开,因为它们之间是 BLE 无线连接,不需要物理连线。这样调试的时候,你可以先用 USB 转串口工具接 BW16,确认它能正常收发,再上 CYD。
注意:BW16 的 UART 引脚定义不同批次可能不一样,上电前一定用万用表确认 TX/RX 位置,接反了虽然一般不会烧,但会让你怀疑人生。
3.2 BW16 的 AT 固件配置
BW16 上电后,通过 USB 转串口连到电脑,用串口助手发 AT 指令。先发AT确认模块响应,返回 OK 说明通信正常。然后配置 BLE 名称和透传参数,常见指令包括设置设备名、设置串口波特率、开启 BLE 透传。具体指令集随固件版本有差异,但思路一致:让模块上电自动进入透传模式,UART 数据直接映射到 BLE 通知。
配置波特率时,要和 EEG 模块的输出波特率一致。比如 EEG 模块是 115200,那 BW16 的 UART 也设 115200。设置完记得保存到 flash,否则断电就丢。有些固件支持AT+SAVE之类的指令,具体看手册。
配置完成后,用手机上的 BLE 调试工具扫描,应该能看到你设置的设备名。连上之后,订阅通知特征值,如果 EEG 模块正在输出数据,你应该能看到一串十六进制数在滚动。这一步是整个链路的关键验证点,只要手机能看到数据,说明 EEG 到 BW16 这段通了。
3.3 ESP32-CYD 端 BLE 中心设备实现
CYD 这边用 Arduino 框架或者 ESP-IDF 都行,我倾向 Arduino 因为生态成熟、库多。BLE 中心设备的核心流程是:初始化 BLE、扫描、按名称或 MAC 过滤目标设备、连接、发现服务、订阅通知、在回调里处理数据。
扫描阶段要注意,BW16 的广播可能不是一直持续,有些透传固件只在未连接时广播。所以 CYD 启动扫描后要给它一点时间。连接成功后,遍历服务列表,找到那个带通知属性的特征值,写 CCCD 描述符开启通知。之后数据就会通过回调函数进来。
回调里拿到的数据是原始字节,需要按 EEG 模块的协议解析。常见格式是每个采样点用 3 字节有符号数表示,高字节在前或后要看模块定义。解析出来的数值再送显示和网络模块。这里建议把解析逻辑单独封装成一个函数,方便后面换不同模块时只改这一处。
// BLE 通知回调的简化示意 void onNotify(BLERemoteCharacteristic* chr, uint8_t* data, size_t len, bool isNotify) { for (size_t i = 0; i + 2 < len; i += 3) { int32_t raw = (data[i] << 16) | (data[i+1] << 8) | data[i+2]; if (raw & 0x800000) raw |= 0xFF000000; // 符号扩展 eegBuffer.push(raw); } }3.4 屏幕波形刷新策略
CYD 的屏幕刷新是性能瓶颈。我的做法是维护一个宽度等于屏幕宽度的采样缓冲区,每收到一批新数据,就把缓冲区左移,新数据填到右边,然后只重绘波形所在的矩形区域。为了进一步提速,可以用TFT_eSPI库的 sprite 功能,先在内存里画好再一次性推屏。
波形纵轴要做自适应或者固定缩放。固定缩放简单,但信号幅度变化大时会出界;自适应缩放好看,但每次都要算最大最小值,增加计算量。原型阶段我建议先用固定缩放,把基线放在屏幕中间,幅度按经验设一个值,比如原始值除以 64 或者 128。等链路稳定了再考虑自适应。
刷新率不用追求和采样率一致。屏幕 30fps 看起来就很流畅了,所以可以每积累若干个采样点刷新一次,比如 250Hz 采样、每 8 个点刷一次,大约 31fps。这样既省 CPU 又省 SPI 带宽。
4. 网页端数据分发与可视化
4.1 CYD 上的轻量 Web 服务
ESP32 跑 Web 服务有成熟方案,用WebServer库或者ESPAsyncWebServer。前者简单,后者性能好但配置稍复杂。原型阶段我用同步的WebServer就够了。思路是:CYD 连上 Wi-Fi 后起一个 HTTP 服务,根路径返回一个内嵌了 JavaScript 的 HTML 页面,页面里用 WebSocket 或者定时轮询从 CYD 拿数据。
WebSocket 更实时,但 ESP32 上实现 WebSocket 服务端要额外库。如果只是看波形,定时轮询反而更简单可靠。页面每隔 50ms 发一个 fetch 请求,CYD 返回最近一段采样点的 JSON 数组,页面用 Canvas 画出来。50ms 对应 20fps,肉眼看波形已经够连续了。
数据接口设计上,我建议返回一个固定长度的数组,比如最近 256 个点。CYD 维护一个环形缓冲区,接口每次返回当前缓冲区快照。这样页面端不用关心采样率,只管画。
4.2 前端 Canvas 波形绘制
前端部分不需要框架,原生 Canvas 就够。拿到 JSON 数组后,遍历画折线。为了性能,用requestAnimationFrame控制重绘节奏,不要每次 fetch 回来就立刻画,而是把数据存起来,在下一帧统一画。
坐标映射很简单:横轴把数组索引映射到画布宽度,纵轴把数值映射到画布高度。数值范围可以固定,也可以根据数据动态调整。动态调整时加一点平滑,避免波形上下跳。颜色上,深色背景配亮绿色或青色线条,看起来最像示波器,也最护眼。
提示:浏览器标签页在后台时,定时器会被节流,fetch 频率会下降。如果你需要后台持续记录,最好在 CYD 侧做数据缓存或者落盘到 SD 卡。
4.3 端到端延迟的估算与优化
整条链路的延迟来自几部分:EEG 模块采集和缓冲、UART 传输、BW16 打包、BLE 空中传输、CYD 解析、屏幕刷新、网络传输、浏览器渲染。粗略估算,UART 传输几乎可以忽略,BLE 连接间隔 20ms 的话,空中延迟大概 20 到 40ms,屏幕刷新 30ms 左右,网络和渲染再加 50ms。整体端到端大概在 100 到 150ms 量级。
这个延迟对于看波形、做简单的神经反馈够用了,但如果你要做实时闭环控制,比如根据 alpha 波控制某个执行器,那就要尽量压缩。优化手段包括:减小 BLE 连接间隔、提高 MTU、减少打包层数、屏幕用局部刷新、网页用 WebSocket 替代轮询。每优化一项,延迟都能降一点,但要权衡稳定性和功耗。
| 延迟来源 | 典型值 | 优化手段 |
|---|---|---|
| BLE 空中传输 | 20-40ms | 减小连接间隔、协商大 MTU |
| 屏幕刷新 | 20-30ms | 局部刷新、sprite 双缓冲 |
| 网络与渲染 | 30-60ms | WebSocket、减少 DOM 操作 |
| 采集与缓冲 | 10-20ms | 减小模块内部缓冲 |
5. 常见问题与排查技巧实录
5.1 数据乱码与波特率排查
最常见的现象是串口收到一堆乱码。九成以上是波特率不匹配。排查顺序:先确认 EEG 模块手册标称的波特率,再确认 BW16 设置的波特率,两者必须完全一致。如果都一致还乱码,检查数据位、校验位、停止位是否匹配,绝大多数是 8N1,但个别模块会用 8E1 或者 7N1。
还有一种乱码是电平不匹配导致的。3.3V 和 5V 混接时,高电平识别可能出错,表现为偶尔乱码或者完全收不到。用示波器或者逻辑分析仪看一下 TX 线上的波形,一眼就能看出来。没有仪器的话,用 USB 转串口分别单独测 EEG 模块和 BW16,能快速定位是哪一端的问题。
5.2 BLE 连不上或频繁断开
BLE 连接问题通常有几个原因。一是设备名或 MAC 过滤写错,CYD 扫描时匹配不到。二是连接参数不合理,比如连接间隔设得太小,某些模块扛不住就断。三是供电不稳,BW16 在发射瞬间电流拉高,如果供电能力不足会复位。
排查时先用手机连 BW16,手机能稳定连上说明 BW16 没问题,问题在 CYD 侧。然后看 CYD 的串口日志,确认扫描到了哪些设备、连接在哪一步失败。如果连接后很快断开,试着把连接间隔调大,比如从 15ms 调到 30ms 或 50ms。如果还是断,检查 BW16 的供电,换一个电流能力更强的电源试试。
5.3 波形显示异常与数据解析错误
屏幕上波形不对,可能是解析错误,也可能是显示逻辑问题。先确认解析:把 CYD 收到的原始字节打印出来,和手机 BLE 调试工具收到的对比,如果一致说明 BLE 链路没问题,问题在解析。检查字节序、符号扩展、采样点对齐。EEG 数据经常是 24 位有符号数,符号扩展写错的话,负值会变成很大的正数,波形就会突然跳变。
显示逻辑方面,常见问题是缓冲区索引越界或者刷新区域算错。用固定的测试数据(比如正弦波)喂给显示函数,能快速判断是显示代码的问题还是数据的问题。这个方法我强烈推荐,用已知正确的数据隔离问题,比盲目改代码高效得多。
5.4 网页端收不到数据
网页空白或者不更新,先看 CYD 的串口日志,确认 Wi-Fi 连上了、Web 服务起来了。然后在电脑浏览器直接访问 CYD 的 IP,看能不能打开页面。如果页面能打开但数据不更新,打开浏览器开发者工具看 Network 面板,确认 fetch 请求有没有发出、返回了什么。常见问题是跨域或者返回格式不对,JSON 解析失败。
还有一个坑是 CYD 的 IP 地址变了。DHCP 分配的地址可能每次重启都不同,建议在路由器里给 CYD 绑定静态 IP,或者在代码里配置静态 IP。这样你收藏的网页地址就不会失效。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 串口乱码 | 波特率/电平不匹配 | 核对参数、测波形 |
| BLE 频繁断开 | 连接间隔小、供电不足 | 调大间隔、换电源 |
| 波形跳变 | 符号扩展错误 | 检查 24 位解析 |
| 网页不更新 | IP 变化、JSON 错误 | 绑静态 IP、看控制台 |
6. 实操心得与后续扩展方向
这个链路我前后搭了大概两周,踩的坑主要集中在 BLE 连接稳定性和数据解析上。最大的体会是:先把每一段单独验证通,再串起来。EEG 到 BW16 用手机验证,BW16 到 CYD 用固定测试数据验证,CYD 到网页用本地假数据验证。每段都确认没问题了,整条链路一次成功的概率就很高。反过来,如果一上来就全串起来调,出了问题你根本不知道是哪一段的锅。
另一个心得是关于采样率和数据率的平衡。原型阶段不要贪高采样率,250Hz 单通道足够验证整条链路。等链路稳了,再逐步提高采样率和通道数,同时观察 BLE 带宽和屏幕刷新能不能跟上。BLE 的实际吞吐受连接间隔和 MTU 影响很大,理论值和实测值差距不小,一定要实测。
后续扩展的话,有几个方向值得做。一是把数据落盘到 CYD 的 SD 卡,这样网页端只负责实时显示,历史数据可以离线分析。二是加简单的信号处理,比如在 CYD 上做滑动平均去噪,或者算一个频带能量,把原始波形和特征值一起显示。三是把网页端做成多设备可访问,手机、平板、电脑都能看,方便多人观察。四是如果要做闭环,可以把网页端的控制指令通过 WebSocket 回传给 CYD,再由 CYD 通过 BLE 发回 BW16,形成双向链路。
最后分享一个小技巧:BW16 和 CYD 之间的 BLE 连接,如果长时间不用,可以主动断开省电,需要时再重连。但重连需要时间,如果你要连续采集,就保持连接。连接稳定性方面,把 CYD 和 BW16 的距离控制在几米内,中间不要有太多金属和墙体,2.4G 频段本来就拥挤,环境干扰对 BLE 影响很明显。实测下来,同一房间内连接很稳,隔一堵墙就开始偶尔丢包,隔两堵墙基本就断了。所以这个原型更适合小范围、近距离的使用场景。