ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

低成本无线EEG原型链路:BW16与ESP32-CYD的BLE透传实战

低成本无线EEG原型链路:BW16与ESP32-CYD的BLE透传实战 1. 为什么我要折腾这条无线 EEG 原型链路脑电采集这件事门槛从来不在电极贴得好不好而在数据怎么从脑袋上那几片干电极稳稳当当地落到屏幕上、落到网页里。我最早做 EEG 原型的时候用的是有线方案电极帽接模拟前端前端出 UART 串口串口再转 USB 进电脑。线一多人就废了——被试稍微动一下工频干扰、运动伪迹全来了采到的信号一半是噪声。后来我下定决心把中间那段线砍掉改成无线链路于是就有了这套BW16 ESP32-CYD的组合。先说清楚这套东西是什么。BW16是一块基于 RTL8720DN 的 Wi-Fi 蓝牙双模模块支持 BLE 5.0价格便宜、体积小、功耗低非常适合做无线数据透传的发射端。ESP32-CYDCYD 是 Cheap Yellow Display 的缩写是一块自带 2.8 寸 TFT 触摸屏的 ESP32 开发板屏幕、主控、Wi-Fi/蓝牙全集成在一块板子上拿来当接收端和本地显示终端再合适不过。EEG就是脑电信号我这里用的是单通道或双通道的干电极模拟前端输出的是串口数据。BLE负责把数据从 BW16 无线发出去UART负责前端和 BW16 之间、以及 ESP32-CYD 内部各模块之间的有线通信。这条链路能干什么简单说它把脑电采集和数据可视化这两件事解耦了。采集端可以做成一个小盒子贴在被试身上接收端可以是一块带屏幕的板子放在桌上也可以是一个网页。数据从电极到屏幕的延迟我实测在 80~150ms 之间做实时波形显示、简单的注意力/放松度指标演示完全够用。适合谁来参考做脑机接口入门原型的学生、想快速验证 EEG 算法的工程师、以及手上有 BW16 和 CYD 这两块板子但不知道怎么把它们串起来玩的爱好者。如果你正在找一条低成本、可复现、不依赖昂贵商业设备的 EEG 原型链路这篇就是我从头踩到尾的记录。2. 整体链路设计与方案选型拆解2.1 为什么是前端 → BW16 → BLE → CYD → 屏幕/网页这条路径EEG 信号的本质是微伏级的低频差分信号带宽通常在 0.5~100Hz 之间采样率做到 250Hz 或 500Hz 就能覆盖大部分应用场景。这意味着数据率其实很低单通道 250Hz、24bit 采样算下来也就 6kbps 左右双通道 12kbps。这个数据率对 BLE 来说毫无压力BLE 5.0 在 1M PHY 下理论吞吐能到几百 kbps实际透传做到 20~50kbps 很轻松。那为什么不直接用 ESP32 自带的蓝牙非要中间加一个 BW16这是很多人会问的第一个问题。原因有三点。第一分工清晰。模拟前端出来的 UART 数据需要有人稳定地读、打包、发出去BW16 做这件事很纯粹它不需要驱动屏幕、不需要跑网页服务器任务单一出问题的概率低。第二功耗和体积。BW16 模块本身很小配合前端可以做成一个很小的采集端而 CYD 带屏幕体积大、功耗高不适合贴在人身上。第三调试方便。发射端和接收端分离之后我可以单独用手机 BLE 调试助手去抓 BW16 发出来的包确认数据格式对不对再去调 CYD 的接收逻辑问题定位快很多。提示如果你的前端本身就有 BLE比如某些集成式 EEG 模块那确实可以省掉 BW16。但大多数便宜的模拟前端只出 UART这时候 BW16 就是最省事的无线化方案。2.2 UART 这一层的关键参数怎么定前端和 BW16 之间走的是 UART这是整条链路里最容易出问题的一环。UART 是全双工异步串口双方必须约定好波特率、数据位、停止位、校验位这四个参数任何一个对不上收到的就是乱码。我用的前端默认输出是 115200 波特率、8 数据位、1 停止位、无校验也就是常说的 8N1BW16 这边也必须配成 8N1、115200。这里有个坑要提前说BW16 的 UART 引脚是 3.3V 电平前端如果是 5V 电平必须加电平转换否则轻则通信不稳重则烧引脚。我一开始图省事直接对接结果数据时不时丢包后来加了一片 TXS0108E 电平转换芯片才稳定下来。另外UART 的 TX 和 RX 一定要交叉接前端的 TX 接 BW16 的 RX前端的 RX 接 BW16 的 TX这个新手最容易接反。关于波特率的计算其实很简单波特率就是每秒传输的符号数8N1 模式下每个字节占 10 位1 起始位 8 数据位 1 停止位所以 115200 波特率下每秒最多传 11520 字节。我的数据帧是每通道 3 字节24bit 1 字节帧头双通道一帧 8 字节250Hz 采样就是每秒 2000 字节远低于上限留足了余量。如果你要提高到 500Hz 甚至 1000Hz 采样115200 依然够用但建议留出 30% 以上的带宽余量避免突发丢包。2.3 BLE 透传的角色分配与连接参数BLE 通信里有两个角色外设Peripheral和中心设备Central。外设负责广播、等待连接、提供服务中心设备负责扫描、发起连接、读写数据。在我的方案里BW16 做外设CYD 做中心设备。这样设计的原因是 BW16 资源少、任务轻做外设最合适CYD 要主动去连、要处理数据、要刷屏做中心设备符合它的定位。BLE 透传的核心是GATT 服务。BW16 需要建立一个自定义服务里面放一个特征值Characteristic并开启Notify属性。所谓 Notify就是外设可以主动把数据推给中心设备不需要中心设备轮询。这对 EEG 这种连续数据流是必须的否则靠 Read 轮询根本跟不上采样率。连接参数也很关键直接影响延迟和稳定性。主要看三个值连接间隔Connection Interval、从机延迟Slave Latency、监督超时Supervision Timeout。连接间隔决定了两次通信之间的最小时间BLE 规定范围是 7.5ms 到 4s。我设的是 15ms兼顾延迟和功耗。从机延迟设 0意思是外设每个连接事件都必须响应不跳过。监督超时设 2s超过这个时间没收到对方信号就认为断连。这三个参数在 CYD 侧发起连接时指定BW16 侧一般接受即可。参数我用的值可选范围说明连接间隔15ms7.5ms~4s越小延迟越低功耗越高从机延迟00~4990 表示不跳过任何连接事件监督超时2000ms100ms~32s需大于 (1延迟)×间隔×2MTU247 字节23~517越大单包数据越多MTU 这个参数值得单独说。BLE 默认 MTU 是 23 字节去掉 3 字节 ATT 头实际每包只能带 20 字节。我的数据帧 8 字节一包能带 2 帧但这样包太碎、开销大。协商到 247 字节后一包能带 30 帧效率高很多。BW16 和 CYD 都支持 MTU 协商在连接建立后主动请求即可。3. 核心细节解析与实操要点3.1 BW16 侧的固件配置与数据打包BW16 出厂固件通常带 AT 指令集但做 EEG 透传我建议直接刷自定义固件用 Arduino 环境开发。RTL8720DN 在 Arduino 里有对应的板级支持包装好之后就能像写 ESP32 一样写 BW16。核心逻辑就三件事读 UART、打包、通过 BLE Notify 发出去。读 UART 的时候有个细节不要用Serial.read()一个字节一个字节读那样效率低还容易丢。我用的方式是开一个缓冲区配合Serial.available()判断一次读一批。EEG 数据帧是定长的所以我可以按帧同步先找帧头比如 0xAA再读固定长度校验通过就入队。// BW16 侧伪代码UART 读帧 BLE Notify #define FRAME_HEADER 0xAA #define FRAME_LEN 8 uint8_t rxBuf[256]; uint8_t frame[FRAME_LEN]; void loop() { int n Serial.available(); if (n FRAME_LEN) { // 找帧头 while (Serial.available() Serial.peek() ! FRAME_HEADER) { Serial.read(); } if (Serial.available() FRAME_LEN) { Serial.readBytes(frame, FRAME_LEN); if (checksumOK(frame)) { bleNotify(frame, FRAME_LEN); // 通过 BLE 推送 } } } }打包的时候要注意字节序。前端出来的 24bit 数据是大端还是小端必须和接收端约定一致。我统一用大端高字节在前接收端解析时按同样顺序拼。如果这里搞错波形会变成一堆乱跳的噪声而且很难一眼看出来是字节序问题。注意BW16 的 BLE 协议栈在连续 Notify 时如果发送速度超过连接间隔允许的速率会触发流控数据会被丢弃。我的做法是在 Notify 前判断一下发送队列长度超过阈值就短暂等待宁可丢帧也不要让协议栈崩掉。3.2 ESP32-CYD 侧的接收与屏幕刷新CYD 这边要做的事比 BW16 多扫描 BLE、连接、订阅 Notify、解析数据、刷屏、同时还要跑网页服务器。ESP32 是双核的我建议把 BLE 接收和数据处理放在一个核屏幕刷新和网页服务放在另一个核用 FreeRTOS 的任务机制分开避免刷屏阻塞接收导致丢包。屏幕刷新是性能瓶颈。CYD 那块 2.8 寸屏是 SPI 接口的 ILI9341刷全屏很慢。做 EEG 波形显示我的做法是只刷新波形区域用 TFT_eSPI 库的pushImage或者局部重绘不要每帧清屏。具体来说波形区域固定高度每次新数据来了把旧波形左移一个像素在右侧画新点。这样每帧只动一小块刷新率能到 30fps 以上。// CYD 侧伪代码波形滚动显示 void drawWaveform(int16_t newSample) { // 将波形区域左移 1 像素 tft.setScrollArea(0, WAVE_Y, SCREEN_W, WAVE_Y WAVE_H); // 在右边缘画新点 int y map(newSample, -32768, 32767, WAVE_Y WAVE_H, WAVE_Y); tft.drawPixel(SCREEN_W - 1, y, TFT_GREEN); }网页部分我用的是 ESP32 自带的 WebServer 库开一个 WebSocket 端点把解析后的 EEG 数据以 JSON 格式推给浏览器。浏览器端用 Canvas 画波形这样手机、电脑都能看不用装任何软件。WebSocket 比 HTTP 轮询实时性好太多延迟能控制在 50ms 以内。3.3 数据帧格式设计与校验整条链路的数据帧格式必须统一我定义的是 8 字节一帧字节内容说明0帧头 0xAA用于帧同步1通道号0x01 或 0x022-4采样值高/中/低24bit 大端5序号0~255 循环用于丢包检测6状态位电极脱落、饱和等标志7校验和前 7 字节异或校验和用异或最简单计算快对 EEG 这种低速率数据足够。序号位很关键接收端通过序号连续性判断有没有丢包如果丢包率高就要回头查 BLE 连接参数或者 UART 波特率。提示帧头不要用 0x00 或 0xFF这两个值在数据里出现概率高容易误同步。0xAA 和 0x55 是比较好的选择二进制里 10101010 和 01010101 交替抗干扰能力强。4. 完整实操流程与关键环节实现4.1 硬件连接与供电检查先把硬件接起来。前端 EEG 模块的 UART TX/RX 经过电平转换接到 BW16 的 RX/TXBW16 供电 3.3V。CYD 单独供电USB 或者 5V 引脚都行。这里有个细节BW16 和 CYD 不要共用同一个电源尤其是 CYD 的屏幕背光电流较大共用电源时背光一亮电压波动会干扰 BW16 的 BLE 通信。我一开始共用波形上总有周期性尖峰分开供电后就干净了。接线完成后先别急着上电用万用表量一下 UART 线的电平确认是 3.3V 而不是 5V。然后上电用串口助手看 BW16 有没有正常输出 AT 响应如果还是 AT 固件或者看自定义固件的调试打印。4.2 BW16 固件烧录与 BLE 服务建立BW16 烧录需要用到 RTL8720DN 的烧录工具Arduino IDE 里选好板子后直接上传即可。烧录时要注意先按住 BOOT 键再上电进入下载模式否则会烧录失败。这个坑我踩过好几次每次都要愣一下才想起来。BLE 服务建立的核心代码结构是这样的先初始化 BLE创建服务和特征值设置 Notify 属性然后开始广播。广播名称我设成 EEG-BW16方便 CYD 扫描时过滤。// BW16 BLE 服务初始化伪代码 BLEService eegService(0000FFF0-0000-1000-8000-00805F9B34FB); BLECharacteristic eegChar(0000FFF1-0000-1000-8000-00805F9B34FB, BLENotify, 247); eegService.addCharacteristic(eegChar); BLE.addService(eegService); BLE.setLocalName(EEG-BW16); BLE.advertise();UUID 我用了标准的 FFF0 系列这是 BLE 透传常用的自定义 UUID 段不容易和系统服务冲突。特征值长度设 247配合 MTU 协商。4.3 CYD 扫描连接与数据解析CYD 这边先扫描找到名字是 EEG-BW16 的设备就发起连接。连接成功后请求 MTU 247然后订阅特征值的 Notify。订阅成功后BW16 一发数据CYD 的回调函数就会被触发。// CYD 侧 Notify 回调伪代码 void onNotify(BLERemoteCharacteristic* chr, uint8_t* data, size_t len) { for (size_t i 0; i FRAME_LEN len; i) { if (data[i] FRAME_HEADER checksumOK(data[i])) { int32_t sample (data[i2] 16) | (data[i3] 8) | data[i4]; if (sample 0x800000) sample | 0xFF000000; // 符号扩展 xQueueSend(eegQueue, sample, 0); // 丢进队列给显示任务 } } }这里有个关键点回调函数里不要做耗时操作。BLE 回调是在协议栈任务里跑的如果你在里面刷屏或者写文件会阻塞协议栈导致断连。我的做法是回调里只做解析和入队显示任务从队列里取数据再处理。4.4 屏幕波形与网页同步显示显示任务从队列取数据一边刷屏幕一边通过 WebSocket 推给网页。屏幕刷新用前面说的滚动方式网页端用 Canvas。两边数据源是同一个队列所以屏幕和网页是同步的延迟差异只在各自的渲染速度上。网页端我用了一个简单的环形缓冲区收到数据就画画满一圈就覆盖最旧的。这样内存占用固定不会越跑越卡。WebSocket 推送频率我控制在 50Hz也就是每 20ms 推一批既流畅又不至于把 Wi-Fi 带宽占满。注意ESP32 同时跑 BLE 和 Wi-Fi 时两者共用射频会有时间片竞争。如果发现 BLE 丢包率上升可以适当降低 Wi-Fi 的推送频率或者把 Wi-Fi 设成省电模式。我实测 BLE Wi-Fi 共存时BLE 连接间隔设 15ms 以上比较稳。5. 常见问题与排查技巧实录5.1 波形全是噪声或乱跳这是最常见的问题原因通常有三个。第一字节序搞反了24bit 数据高低字节拼错波形会变成高频噪声。排查方法发一个已知的固定值比如 0x000100看接收端解析出来是不是 256不是就是字节序问题。第二UART 波特率不匹配收到的数据帧头都对不上。用示波器量一下 TX 线的位宽115200 波特率下一位是 8.68us量出来对不上就改波特率。第三电极接触不良这个最直接看状态位里的电极脱落标志。5.2 BLE 频繁断连断连的原因比较多我整理了一个排查表现象可能原因解决方法连接几秒就断监督超时太短增大到 2s 以上距离远了就断发射功率低BW16 设最大发射功率数据量大时断协议栈流控降低 Notify 频率加发送队列随机断连电源干扰分开供电加滤波电容连不上广播间隔太长缩短广播间隔到 100ms我遇到最坑的一次是电源干扰BW16 和 CYD 共用 USB 供电CYD 屏幕刷新时电流突变BW16 电压瞬间跌落导致重启。分开供电后彻底解决。所以如果你遇到莫名其妙断连先查电源。5.3 屏幕刷新卡顿CYD 的 SPI 屏幕刷新慢是先天限制能优化的点有几个。第一提高 SPI 时钟ILI9341 能跑到 40MHzTFT_eSPI 库里设SPI_FREQUENCY 40000000。第二只刷局部别全屏清。第三降低颜色深度波形用单色不用抗锯齿。第四双缓冲用 Sprite 在内存里画好再一次性推屏避免闪烁。我用的是局部刷新 单色实测 30fps 没问题。5.4 网页端延迟大或卡死网页卡死通常是 WebSocket 消息堆积。如果浏览器处理不过来消息会排队越排越多。解决方法是在网页端做丢帧处理如果缓冲区里积压超过一定数量直接丢弃旧数据只画最新的。EEG 波形显示不需要每一帧都画丢几帧人眼看不出来。另外JSON 解析在浏览器端也有开销数据量大时可以用二进制传输省掉 JSON 序列化和解析的时间。5.5 实操避坑速查表坑点表现我的解法UART 电平不匹配丢包、乱码加 TXS0108E 电平转换字节序不一致波形噪声统一大端固定值验证共用电源随机断连分开供电回调里做耗时操作协议栈阻塞断连回调只入队帧头选 0x00误同步改用 0xAAMTU 没协商吞吐低连接后主动请求 247屏幕全屏刷新卡顿闪烁局部刷新 Sprite6. 几个我踩过之后才明白的经验第一条经验关于调试顺序。很多人一上来就把整条链路全接好然后发现没数据就开始瞎猜。正确的做法是分段验证先用串口助手确认前端输出正常再确认 BW16 能收到并打印再用手机 BLE 助手确认 BW16 能发出来最后才调 CYD 接收。每一段单独验证过问题范围就缩小到一段里排查快十倍。第二条关于数据可视化的重要性。EEG 数据不像温度、湿度那样直观光看数字根本判断不出对错。我强烈建议在链路搭好后第一时间把波形画出来。哪怕只是串口示波器也比看十六进制强。波形一出来噪声、丢包、字节序问题全都藏不住。第三条关于参数留余量。波特率、连接间隔、MTU 这些参数不要卡着理论极限设。我一开始把连接间隔设成 7.5ms想着延迟最低结果丢包率飙升。后来改成 15ms丢包率降到几乎为零。工程上稳定比极限重要得多。最后分享一个小技巧BW16 和 CYD 之间的 BLE 连接可以在 CYD 侧加一个自动重连逻辑。断连后不要傻等隔 1 秒重新扫描连接这样即使中间有干扰断了一下也能很快恢复用户体验好很多。这个逻辑不复杂但能省掉很多怎么又断了的烦恼。
返回列表