1. 项目缘起与整体链路设计
脑电采集这件事,早年给人的印象就是一堆线缆加一台笨重放大器,被采集的人只能老老实实坐在椅子上。这几年消费级 EEG 模块越来越小,成本也压到了个人开发者能承受的区间,于是"把脑电数据无线传出来、在屏幕和网页上实时看到波形"就成了一个很自然的折腾方向。我手上正好有一块 BW16 模块和一块 ESP32-CYD(就是那种自带 2.4 寸彩屏的开发板),就想着把这条链路搭起来:脑电模块负责采集,BW16 负责无线透传,ESP32-CYD 负责接收、显示,同时再推一份数据到网页端。
这条链路的核心关键词是BW16、ESP32-CYD、EEG、BLE、UART。简单说,脑电模块通过 UART 把采样数据吐给 BW16,BW16 用 BLE 把数据广播/透传出去,ESP32-CYD 作为接收端拿到数据后,一边在本地屏幕上画波形,一边通过它自己的 Wi-Fi 把数据转发到网页。整套东西解决的问题是:让脑电数据摆脱线缆束缚,同时提供"本地屏幕 + 远程网页"两种观察方式,方便调试和演示。
适合谁来参考?如果你手上有消费级脑电模块、玩过 Arduino 或 ESP-IDF、对 BLE 和串口通信有基本概念,那这篇内容基本可以照着复现。如果你是完全的新手,也不用慌,我会把 UART 协议、BLE 连接、数据解析这些环节拆开讲清楚,尽量做到"看完能动手"。
先说清楚一个前提:脑电信号本身非常微弱,通常在微伏级别,而且极易被工频干扰、肌电、眼动污染。所以这条链路里,数据链路的稳定性比采样精度更值得先关注——因为如果无线链路丢包、串口波特率对不上、BLE 连接频繁断开,你后面做再多的去噪算法都是白搭。我踩过的第一个坑就是:一开始只盯着波形好不好看,结果发现是串口丢包导致的波形断裂,白白怀疑了半天电极。
整条链路我画成文字版是这样的:
- 脑电模块(采集 + 模数转换)→ UART 串口 → BW16(BLE 透传)
- BW16 → BLE 无线 → ESP32-CYD(BLE 客户端)
- ESP32-CYD → 本地 TFT 屏幕绘制波形
- ESP32-CYD → Wi-Fi → 网页端(WebSocket 或 HTTP 轮询)
这个设计里,BW16 和 ESP32-CYD 各自承担了不同角色:BW16 是"无线串口",把 UART 数据原封不动搬到 BLE 上;ESP32-CYD 是"数据枢纽",既做本地显示又做网络转发。为什么不让 BW16 直接连 Wi-Fi 推网页?因为 BW16 的定位更偏向 BLE 透传,而 ESP32-CYD 自带屏幕和 Wi-Fi,做枢纽更合适。这个分工是我试过几种组合后定下来的,后面会详细说。
2. 硬件选型与核心模块解析
2.1 BW16 为什么适合做 BLE 透传
BW16 是基于 RTL8720DN 的模块,双频 Wi-Fi 加 BLE 5.0,社区里常被拿来当"无线串口"用。它的优势在于固件生态相对成熟,AT 指令集能直接配置 BLE 透传模式,不需要你从零写 BLE 协议栈。对于脑电这种"数据流持续、包不大、要求低延迟"的场景,BLE 透传比自定义 GATT 服务省事得多。
我选它的理由很直接:脑电模块输出的是连续字节流,我只需要一个"管道"把它搬到空中,不需要在 BW16 上做任何解析。BW16 的 AT 透传模式正好干这个。相比之下,如果用一个纯 BLE 芯片自己写服务,光是处理连接参数、MTU、通知使能就要花不少时间,对原型验证来说不划算。
不过要注意,BW16 的 AT 固件版本差异挺大,不同批次的默认波特率、指令集细节可能不一样。我手上这块默认 UART 波特率是 115200,但有的版本是 9600。上手第一件事就是用串口助手确认它能正常回 AT,并查清当前波特率,否则后面全是玄学问题。
2.2 ESP32-CYD 的角色与屏幕优势
ESP32-CYD 的"CYD"是"Cheap Yellow Display"的缩写,典型配置是 ESP32-WROOM 加一块 2.4 寸 ILI9341 SPI 屏,还带触摸(部分版本)。它最大的价值是把屏幕、主控、Wi-Fi/BLE 集成在一块板子上,省去了单独接屏的布线麻烦。对脑电波形显示来说,本地屏幕能让你在不打开电脑的情况下快速判断信号质量,这个体验比纯网页端好太多。
ESP32-CYD 在这条链路里同时干三件事:作为 BLE 客户端接收 BW16 的数据、驱动 TFT 屏画实时波形、通过 Wi-Fi 把数据转发给网页。三件事并行,对任务调度有要求,所以后面我会用 FreeRTOS 的任务划分来拆开处理,避免 BLE 回调和屏幕刷新互相阻塞。
2.3 脑电模块与 UART 接口
脑电模块这块,市面上常见的消费级方案输出格式大多是"包头 + 数据 + 校验"的二进制帧,波特率常见 57600 或 115200。我用的模块输出的是固定长度帧,每帧包含若干通道的采样值。这里的关键是先搞清楚模块的帧格式:包头字节是什么、每帧多少字节、采样值是大端还是小端、有没有校验位。这些信息通常在模块的数据手册里,如果没有手册,就得用串口助手抓原始数据自己分析。
UART 本身是异步串行通信,靠起始位和停止位同步,不需要时钟线。它的时序是:空闲高电平 → 起始位拉低 → 8 位数据(LSB 先发)→ 可选校验位 → 停止位拉高。理解这个时序对排查问题很有帮助,比如波特率不匹配时,你会看到满屏乱码,本质就是采样点错位。热词里提到的 16550 是经典的 UART 控制器标准,STM32 的 HAL 库、ESP32 的 UART 驱动本质上都是对这类硬件的封装,理解了底层时序,用哪家的库都不慌。
2.4 关键参数对照
| 环节 | 模块 | 关键参数 | 我的取值 |
|---|---|---|---|
| 采集输出 | 脑电模块 | 波特率 / 帧长 | 115200 / 固定帧 |
| 无线透传 | BW16 | BLE 透传 / 波特率 | AT 透传 / 115200 |
| 接收显示 | ESP32-CYD | BLE 客户端 / SPI 屏 | GATT 通知 / ILI9341 |
| 网络转发 | ESP32-CYD | Wi-Fi / 协议 | WebSocket |
这张表是我实际跑通后的配置,你可以作为起点,但波特率一定要以你手上模块的实际值为准,不要照抄。
3. 核心细节解析与实操要点
3.1 UART 数据帧的解析思路
脑电模块吐出来的数据不是给人看的,是二进制流。解析的核心是"找包头、定帧长、取数据、验校验"。我用的模块帧结构大致是:1 字节包头(比如 0xAA)、1 字节长度、N 字节数据、1 字节校验和。解析时用一个环形缓冲区接收,然后逐字节扫描找包头,找到后判断缓冲区里是否够一帧,够了就取出来校验,校验通过才交给上层。
这里有个容易忽略的点:串口数据是流式的,可能一帧被拆成两次收到,也可能一次收到一帧半。所以不能用"收到一次就读一帧"的写法,必须用缓冲区累积。我一开始就是直接在串口回调里解析,结果数据一快就丢帧,后来改成环形缓冲区加状态机才稳定。
状态机大概是这几个状态:等待包头 → 读取长度 → 读取数据 → 校验。每个状态只处理一个字节,收到完整帧后触发一次"帧就绪"事件。这种写法对任何 UART 协议都通用,热词里那些"uart 传输通信时序""uart 串口通信"讲的就是这套底层逻辑。
3.2 BLE 透传的连接与通知机制
BW16 配成 BLE 透传后,它对外表现为一个 BLE 从设备,ESP32-CYD 作为主设备去连接它。连接建立后,数据通过 GATT 通知(Notify)推送给主设备。这里的关键参数是MTU 和连接间隔。MTU 决定单次能传多少字节,默认 23 字节,实际有效载荷 20 字节;连接间隔决定数据传输的频率,单位是 1.25ms。
脑电数据量大,如果 MTU 太小,一帧数据要拆成好几个通知包,延迟和丢包风险都上升。我的做法是协商更大的 MTU(比如 247),这样一帧能塞进一个通知包。连接间隔我设成 15ms 左右,兼顾延迟和功耗。这些参数在 ESP32 侧通过esp_ble_gatt_set_local_mtu和连接参数更新来设置。
注意:BLE 连接参数不是你想设多少就多少,主从双方要协商,最终以双方都支持的范围为准。如果从设备(BW16)不支持你请求的参数,它会拒绝或给一个折中值。
3.3 屏幕波形绘制的性能取舍
ESP32-CYD 的屏幕是 SPI 接口,刷屏速度有限。如果每收到一个采样点就重绘整屏,帧率会惨不忍睹。我的做法是只重绘变化区域:维护一个波形缓冲区,新数据进来后,只擦除并重绘波形移动的那一列或那几列。这样刷屏量大幅下降,波形滚动也流畅。
另一个技巧是降采样显示。脑电采样率可能几百 Hz,但屏幕宽度只有 320 像素,没必要每个点都画。我按屏幕宽度做抽取,比如每 4 个采样点取一个显示,视觉上完全够用,还省了 CPU。这个取舍在嵌入式显示里很常见,核心思想是"显示是给人看的,不是给示波器看的"。
3.4 网页端的数据通道选择
网页端我一开始用 HTTP 轮询,ESP32-CYD 起一个简单的 HTTP 服务,网页定时请求最新数据。但轮询延迟高、请求频繁,体验一般。后来换成 WebSocket,ESP32-CYD 作为服务端,网页连上后数据主动推送,延迟明显降低。
WebSocket 在 ESP32 上有现成库(比如esp_websocket_client或 Arduino 的WebSocketsServer),配置不复杂。数据格式我用的是简单的文本协议,每个采样点用逗号分隔,网页端用 JavaScript 解析后画到 Canvas 上。为什么不用二进制?因为原型阶段可读性比带宽更重要,文本方便调试,等稳定了再换二进制也不迟。
4. 实操过程与核心环节实现
4.1 第一步:确认脑电模块的串口输出
动手第一件事不是写代码,是用 USB 转串口工具把脑电模块的输出抓到电脑上看。热词里提到的 FT231X、FT232R 都是常见的 USB 转 UART 芯片,装好驱动后,用串口助手以模块标称的波特率打开,看能不能收到数据。
如果收到的是乱码,先怀疑波特率不对,逐个试常见值(9600、57600、115200)。如果完全没数据,检查接线:模块 TX 接转换器 RX,模块 RX 接转换器 TX,共地。这一步确认了,后面才有基础。我当时抓到的数据是一串十六进制,对照手册确认了包头是 0xAA,帧长 33 字节,心里就有底了。
4.2 第二步:配置 BW16 进入 BLE 透传模式
BW16 用 AT 指令配置。典型流程是:先发AT确认通信正常,然后设置串口波特率与脑电模块一致,再配置 BLE 名称、透传模式、广播参数。具体指令因固件版本而异,但思路一致:让 BW16 上电后自动进入透传,脑电模块的数据从 UART 进来,直接从 BLE 出去。
配置时我建议把参数写进 BW16 的 flash,这样断电重启后不用重新配。配置完成后,用手机上的 BLE 调试 App 搜一下,能看到 BW16 广播的名字,连上后订阅通知,应该能看到脑电数据流。这一步是很好的中间验证点,能确认"采集到无线"这段链路是通的。
4.3 第三步:ESP32-CYD 作为 BLE 客户端接收
ESP32-CYD 这边用 ESP-IDF 或 Arduino 都行,我用的是 Arduino 框架,BLE 库用NimBLE,因为它比原生 BLE 库省内存。核心流程是:扫描 → 找到 BW16 的设备名 → 连接 → 发现服务 → 找到通知特征 → 使能通知 → 在回调里收数据。
收数据的回调里,我把字节流喂给前面说的环形缓冲区加状态机,解析出完整帧后,把采样值放进一个队列,交给显示任务和网络任务消费。这里用队列是为了解耦:BLE 回调只管收,显示和网络各自按自己的节奏取,互不阻塞。
// 伪代码示意:BLE 通知回调里只做入队 void onNotify(BLERemoteCharacteristic* chr, uint8_t* data, size_t len) { for (size_t i = 0; i < len; i++) { ringBuffer.push(data[i]); // 喂给环形缓冲区 } // 状态机在另一个任务里跑,解析出帧后 push 到队列 }4.4 第四步:本地屏幕绘制波形
显示任务从队列取采样值,更新波形缓冲区,然后重绘变化区域。我用的是 TFT_eSPI 库,它支持局部刷新,配合前面说的降采样,帧率能稳定在可接受范围。波形缓冲区我做成环形,新数据覆盖最旧的数据,屏幕上就是一条向左滚动的波形。
这里有个细节:屏幕刷新和 BLE 接收如果放在同一个任务里,BLE 一忙屏幕就卡。所以必须分任务。我用 FreeRTOS 起了三个任务:BLE 接收任务、显示任务、网络任务,通过队列通信。任务优先级上,BLE 接收最高,显示次之,网络最低,保证数据不丢。
4.5 第五步:Wi-Fi 转发到网页
网络任务从队列取数据,通过 WebSocket 推给网页。ESP32-CYD 先连 Wi-Fi,然后起 WebSocket 服务端。网页端用一段简单的 HTML + JavaScript,连上 WebSocket 后接收数据,用 Canvas 画波形。
// 网页端示意 const ws = new WebSocket('ws://192.168.x.x:81'); ws.onmessage = (evt) => { const values = evt.data.split(',').map(Number); drawWaveform(values); // 画到 Canvas };网页端的好处是可以多设备同时看,手机、电脑都能打开,适合演示。而且网页端可以做更复杂的处理,比如加个简单的滤波、显示频谱,这些在 ESP32 上算力不够,放浏览器里就轻松了。
4.6 关键参数计算示例
以采样率 250Hz、单通道 2 字节为例,数据率是 250 × 2 = 500 字节/秒。加上包头校验,约 550 字节/秒。BLE 在连接间隔 15ms、每包 20 字节有效载荷下,理论吞吐约 20 / 0.015 ≈ 1333 字节/秒,够用。但如果 MTU 协商到 247,单包有效载荷约 244 字节,吞吐大幅提升,余量更足。这就是为什么我强调要协商大 MTU——算一下就知道,默认 MTU 下多通道高采样率会顶到天花板。
5. 常见问题与排查技巧实录
5.1 波形断裂或丢帧
这是最常见的现象。排查顺序:先看串口端有没有丢(用串口助手对比),再看 BLE 有没有丢(看通知回调频率),最后看显示任务有没有来不及消费队列。我遇到的一次是队列太小,显示任务一慢就溢出,把队列加大就好了。另一次是 BLE 连接间隔太长,数据积压,缩短间隔解决。
5.2 BLE 连不上或频繁断开
先确认 BW16 是否在广播,用手机 App 搜一下。如果搜不到,可能是 BW16 没进透传模式或供电不足。如果能搜到但连不上,检查 ESP32 侧的服务 UUID 和特征 UUID 是否匹配。频繁断开通常是连接参数太激进或信号干扰,把连接间隔放宽一点,或者让两个模块离近些。
5.3 串口乱码
九成是波特率不匹配。剩下的一成是接线错误(TX/RX 没交叉)或电平不匹配(3.3V 对 5V)。BW16 和脑电模块都是 3.3V 电平,直连没问题,但如果中间接了 5V 的转换器,就要注意电平转换。
5.4 网页端延迟高
如果是 HTTP 轮询,换成 WebSocket。如果已经是 WebSocket 还延迟高,检查是不是在 ESP32 侧做了太多处理,把重活放到网页端。另外 Wi-Fi 信号差也会导致延迟,尽量让 ESP32-CYD 离路由器近些。
5.5 问题速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 波形断裂 | 队列溢出 / 连接间隔长 | 加大队列 / 缩短间隔 |
| 连不上 BLE | 未广播 / UUID 不匹配 | 手机搜设备 / 核对 UUID |
| 串口乱码 | 波特率 / 接线 | 试波特率 / 检查 TX-RX |
| 网页延迟 | 轮询 / 信号差 | 换 WebSocket / 靠近路由 |
| 屏幕卡顿 | 任务未分离 | 拆 FreeRTOS 任务 |
5.6 几条踩坑心得
第一,先通链路再谈算法。我见过太多人一上来就研究 EEG 去噪、源定位最小范数估计,结果数据链路都不稳,算法再好也是空中楼阁。第二,每个环节都要有独立的验证手段:串口用串口助手验,BLE 用手机 App 验,网页用浏览器控制台验。这样出问题时能快速定位是哪一段。第三,参数不要照抄,波特率、MTU、连接间隔都要根据自己硬件实测调整。
6. 链路扩展与个人体会
这条链路跑通后,能扩展的方向不少。比如在 ESP32-CYD 上加个 SD 卡,把数据存下来做离线分析;或者在网页端加实时滤波和频谱显示,把浏览器当成简易分析工具;再或者把 BLE 换成 Wi-Fi 直连,进一步降延迟。热词里提到的 EEG 去噪、源定位这些,都可以在数据稳定之后,放到 PC 或网页端去做,不必压在嵌入式端。
我个人在实际操作中的体会是:原型阶段最值钱的不是功能多,而是链路稳、可观测。屏幕和网页这两个观察窗口,一个负责即时判断,一个负责详细分析,配合起来调试效率高很多。BW16 加 ESP32-CYD 这个组合,成本不高,资料也够,适合拿来练手无线数据链路。真正难的不是把数据传出去,而是传出去之后你信不信它——所以每个环节的验证手段,比代码本身更重要。