ARTICLE DETAIL

资讯详情

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

无线EEG原型链路:BW16+ESP32-CYD实现脑电数据实时显示与网页推送

无线EEG原型链路:BW16+ESP32-CYD实现脑电数据实时显示与网页推送 1. 这条无线 EEG 原型链路到底在做什么脑电采集这件事很多人第一反应是实验室里那些几十万的设备其实这几年开源硬件和国产模组把门槛拉得很低了。我这次折腾的项目核心目标就一句话把脑电模块采集到的原始信号通过无线方式送到一块带屏幕的开发板上实时显示同时再转发到网页端做可视化。整条链路涉及三个关键角色——脑电采集模块负责读脑BW16 负责无线搬运ESP32-CYD 负责显示和转发。先说清楚适用人群。如果你是想入门脑机接口、生物电信号采集的爱好者或者手上已经有一块脑电模块但不知道怎么把数据无线传出来再或者你想做一个带屏幕的便携式生理信号监视器那这套方案基本可以直接抄。它不需要你懂射频电路设计也不需要你写复杂的协议栈核心工作集中在串口数据解析、BLE 通信配置和屏幕刷新逻辑上。为什么选 BW16 而不是直接用 ESP32 自带的蓝牙这是很多人会问的第一个问题。BW16 是基于 RTL8720DN 的双频 Wi-Fi 蓝牙模组它的 BLE 协议栈成熟度在同类模组里算比较稳的而且支持经典蓝牙和 BLE 双模。我实测下来BW16 在持续传输小包数据时的连接稳定性比某些 ESP32 的 BLE 实现要好一些尤其是在 2.4G 环境比较嘈杂的情况下。当然 ESP32 也能做但如果你手头正好有 BW16用它做无线桥接是合理的选择。ESP32-CYD 则是另一块很有意思的板子。CYD 是 Cheap Yellow Display 的缩写本质上是一块带 2.8 寸 ILI9341 触摸屏的 ESP32 开发板价格便宜、集成度高。它的作用有两个一是作为无线接收端把 BW16 发过来的脑电数据解析出来二是驱动屏幕做实时波形显示同时通过 Wi-Fi 把数据推送到网页端。这样一来你既有本地屏幕看波形又能在电脑或手机上通过浏览器远程观察。整条链路的数据流向是这样的脑电模块通过 UART 串口把原始数据吐给 BW16BW16 通过 BLE 把数据打包发送ESP32-CYD 接收后解析、显示、转发。听起来简单但中间每一步都有坑尤其是串口协议解析和 BLE 分包处理这两块后面会详细展开。提示这个项目涉及的是通用脑电采集模块的数据传输不涉及任何医疗诊断用途。如果你要做的是科研级信号分析建议在采集端就做好阻抗检测和参考电极处理。2. 硬件选型与链路架构拆解2.1 为什么是 BW16 ESP32-CYD 这个组合先聊选型逻辑。脑电模块的输出通常是 UART 串口波特率一般在 57600 或 115200数据格式多为二进制帧。你需要一个能读串口、又能无线上传的中间层。BW16 在这里扮演的是串口转 BLE的桥接角色。BW16 的优势在于它的 UART 接收缓冲和 BLE 发送可以做到比较好的流水线处理。我试过用普通 ESP32 做同样的桥接在高采样率下偶尔会出现串口溢出因为 BLE 协议栈占用的 CPU 时间片会干扰 UART 中断处理。BW16 的双核架构在这方面表现更从容一些一个核跑 BLE 协议栈另一个核处理串口数据互不干扰。ESP32-CYD 的选择理由更直接它自带屏幕省去了额外接线和驱动的问题。ILI9341 这块屏在 ESP32 生态里驱动非常成熟TFT_eSPI 库直接支持刷新率做波形显示完全够用。而且 CYD 板子上引出了足够的 GPIO方便你后续扩展按键、SD 卡存储等功能。两者之间的 BLE 连接采用自定义 GATT 服务。我没有用标准的串口透传服务因为脑电数据需要做帧同步和校验自定义服务可以更灵活地定义数据包结构。2.2 数据链路的三个关键环节整条链路可以拆成三段采集端到 BW16 的 UART 传输、BW16 到 ESP32-CYD 的 BLE 传输、ESP32-CYD 到网页的 Wi-Fi 推送。每一段的参数配置都会影响最终效果。UART 传输这一段核心是波特率匹配和数据帧解析。脑电模块的波特率必须和 BW16 的串口配置一致否则收到的全是乱码。我用的模块默认是 576008 数据位、1 停止位、无校验。数据帧通常是这样的结构帧头2 字节 数据长度1 字节 有效载荷N 字节 校验和1 字节。不同厂家的模块帧格式不一样你需要先拿到模块的通信协议文档。BLE 传输这一段关键参数是连接间隔和 MTU。连接间隔决定了数据发送的频率我设的是 7.5msBLE 规范允许的最小值之一这样每秒可以发送约 133 个数据包。MTU 我协商到了 247 字节这样每个 BLE 包可以携带更多的脑电数据减少分包开销。实测下来单通道 250Hz 采样、每样本 3 字节的配置下链路完全跑得动。Wi-Fi 推送这一段ESP32-CYD 作为 SoftAP 或者 STA 模式接入局域网通过 WebSocket 把数据推给浏览器。WebSocket 比 HTTP 轮询更适合实时波形延迟可以控制在 50ms 以内。2.3 供电与抗干扰的实操考量脑电信号非常微弱通常在微伏级别所以供电质量直接影响信号质量。我一开始用 USB 供电发现 50Hz 工频干扰很明显。后来改成锂电池供电干扰立刻小了很多。如果你必须用 USB 供电建议加一个磁珠或者 LC 滤波。BW16 和 ESP32-CYD 的供电也要注意。BW16 在 BLE 发送瞬间会有较大的电流尖峰如果电源退耦没做好可能导致模组复位。我在 BW16 的电源脚旁边并了一个 100uF 的电解电容和一个 0.1uF 的陶瓷电容问题就解决了。还有一点容易被忽略脑电模块的参考电极和地线处理。如果你的脑电模块是单端输入参考电极要接到耳垂或者乳突位置地线要单独走不要和数字地混在一起。这些细节在实验室里是常识但自己搭原型的时候很容易忽略。3. 核心细节解析与实操要点3.1 UART 串口数据解析的正确姿势脑电模块通过 UART 吐出来的数据是二进制流不是文本。这意味着你不能用Serial.readString()这种函数必须按字节读取并做帧同步。我踩过的第一个坑就是直接用文本方式读结果全是乱码。正确的做法是维护一个环形缓冲区逐字节读取检测帧头。伪代码逻辑是这样的先找帧头的第一个字节找到后再确认第二个字节然后读长度字段再读对应长度的数据最后校验。如果校验失败丢弃这一帧从下一个字节重新开始找帧头。// BW16 端 UART 解析核心逻辑示意 #define FRAME_HEADER_1 0xAA #define FRAME_HEADER_2 0x55 #define MAX_PAYLOAD 64 uint8_t rxBuffer[MAX_PAYLOAD 4]; uint8_t rxIndex 0; bool headerFound false; void parseUARTByte(uint8_t byte) { if (!headerFound) { if (byte FRAME_HEADER_1) { rxBuffer[0] byte; rxIndex 1; headerFound true; } return; } rxBuffer[rxIndex] byte; if (rxIndex 2 rxBuffer[1] ! FRAME_HEADER_2) { headerFound false; rxIndex 0; return; } if (rxIndex 3) { uint8_t payloadLen rxBuffer[2]; if (rxIndex payloadLen 4) { // 校验和验证 uint8_t checksum 0; for (int i 0; i payloadLen 3; i) { checksum rxBuffer[i]; } if (checksum rxBuffer[rxIndex - 1]) { handleEEGFrame(rxBuffer 3, payloadLen); } headerFound false; rxIndex 0; } } }这段代码的关键点在于帧头检测失败时要正确重置状态机否则会卡死。我一开始没处理好这个逻辑导致偶尔丢帧后整个解析器就停摆了。另外校验和的计算范围要确认清楚有些模块是从帧头开始算有些是从长度字段开始算这个必须看协议文档。注意UART 接收中断里不要做耗时操作比如 BLE 发送。正确做法是把解析好的帧放入队列在主循环里处理发送。3.2 BLE 服务与特征值的设计BW16 作为 BLE 外设需要定义一个自定义 GATT 服务。我定义的服务 UUID 是 128 位的自定义值避免和标准服务冲突。服务下面有两个特征值一个是数据通知特征用于 BW16 向 ESP32-CYD 推送脑电数据另一个是控制特征用于 ESP32-CYD 向 BW16 发送配置命令比如采样率切换、通道选择等。数据通知特征配置为 Notify 属性ESP32-CYD 端使能 CCCD客户端特征配置描述符后BW16 就可以主动推送数据了。这里有个细节BLE 的 Notify 单包最大长度受 MTU 限制。默认 MTU 是 23 字节有效载荷只有 20 字节。如果不协商 MTU每个包只能带 20 字节脑电数据效率很低。我在 BW16 端和 ESP32-CYD 端都做了 MTU 协商请求最终协商到 247 字节。协商过程是 ESP32-CYD 发起esp_ble_gattc_send_mtu_req()BW16 端响应。协商成功后每个 Notify 包可以带 244 字节有效数据。// ESP32-CYD 端 MTU 协商与 CCCD 使能示意 esp_ble_gattc_send_mtu_req(gattc_if, conn_id); // 在回调中处理 MTU 协商结果 case ESP_GATTC_CFG_MTU_EVT: if (param-cfg_mtu.status ESP_GATT_OK) { ESP_LOGI(TAG, MTU 协商成功: %d, param-cfg_mtu.mtu); } break; // 使能 Notify esp_ble_gattc_register_for_notify(gattc_if, remote_bda, notify_char_handle);MTU 协商不是一次就能成功的有些协议栈实现需要重试。我实测下来BW16 端第一次协商有时会返回默认值需要在回调里判断并重新发起。这个坑花了我不少时间排查。3.3 屏幕波形刷新的性能优化ESP32-CYD 的 ILI9341 屏幕分辨率是 320x240用 SPI 接口驱动。如果每收到一个脑电样本就刷新一次屏幕SPI 带宽根本不够而且 CPU 会被刷屏占满。我的做法是维护一个显示缓冲区每收到一批数据后统一刷新。具体来说我开了一个 320 点的环形缓冲区对应屏幕的横向像素。每收到一个新样本就把它映射到纵坐标存入缓冲区。然后每隔 30ms 用pushImage()或者逐行绘制的方式更新屏幕。TFT_eSPI 库的pushImage()在 320x240 全屏刷新时大约需要 20ms所以 30ms 的刷新间隔是可行的。如果你想要更高的刷新率可以只刷新变化区域而不是全屏。比如只刷新波形区域的那 200 行顶部留出 40 行显示状态信息。这样刷新时间可以降到 15ms 左右。还有一个优化点是颜色深度。ILI9341 支持 16 位色RGB565如果你不需要那么多颜色可以用 8 位色模式SPI 数据量减半刷新速度翻倍。波形显示用 8 位色完全够用。3.4 网页端 WebSocket 推送的实现ESP32-CYD 通过 Wi-Fi 接入局域网后运行一个 WebSocket 服务器。浏览器端用 JavaScript 的 WebSocket API 连接接收数据后用 Canvas 绘制波形。这里的关键是数据格式要统一。我定义的数据包格式是前 4 个字节是时间戳毫秒后面是脑电样本每个样本 2 字节16 位有符号整数。ESP32-CYD 把 BLE 收到的数据重新打包成这个格式通过 WebSocket 广播给所有连接的客户端。// 浏览器端 WebSocket 接收与 Canvas 绘制示意 const ws new WebSocket(ws://192.168.4.1:81); const canvas document.getElementById(eegCanvas); const ctx canvas.getContext(2d); let dataBuffer []; ws.onmessage (event) { const arrayBuffer event.data; const dataView new DataView(arrayBuffer); const timestamp dataView.getUint32(0, true); const samples []; for (let i 4; i arrayBuffer.byteLength; i 2) { samples.push(dataView.getInt16(i, true)); } dataBuffer.push(...samples); if (dataBuffer.length canvas.width) { dataBuffer dataBuffer.slice(-canvas.width); } drawWaveform(); }; function drawWaveform() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); ctx.strokeStyle #00ff00; ctx.lineWidth 1; for (let x 0; x dataBuffer.length; x) { const y canvas.height / 2 - dataBuffer[x] / 256; if (x 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); }WebSocket 服务器在 ESP32 上跑我用的是esp_websocket_server组件。注意 ESP32 的内存有限WebSocket 连接数不要超过 4 个否则容易内存不足。另外数据推送频率不要太高我设的是每 50ms 推送一次把这段时间内收到的样本打包发送。4. 完整实操流程与关键环节实现4.1 BW16 端固件开发环境搭建BW16 的开发环境用的是 Arduino IDE 加上 Realtek 的 AmebaD 核心包。安装步骤是先在 Arduino IDE 的首选项里添加 AmebaD 的板管理器 URL然后在板管理器里搜索 AmebaD 并安装。安装完成后选择 BW16 作为开发板。这里有个坑AmebaD 核心包的版本要和你的 Arduino IDE 版本匹配。我用的是 Arduino IDE 2.x需要安装 AmebaD 3.x 以上的核心包。如果版本不匹配编译时会报一堆找不到头文件的错误。安装好之后先烧一个最简单的 BLE 例程测试一下确认模组能正常广播。BW16 的烧录方式是串口烧录需要把模组上的 BOOT 引脚拉低再上电进入烧录模式。烧录完成后重新上电模组就会运行你的固件。提示BW16 的串口烧录和 UART 数据通信是同一组引脚所以烧录时需要断开脑电模块否则会冲突。我建议在脑电模块的 TX 线上加一个跳线帽烧录时拔掉。4.2 脑电模块与 BW16 的接线与配置脑电模块的接线一般是这样模块的 TX 接 BW16 的 RX模块的 RX 接 BW16 的 TXGND 对接VCC 按模块要求供电通常是 3.3V 或 5V。如果模块是 5V 逻辑电平而 BW16 是 3.3V需要在 TX 线上加一个电平转换电路最简单的做法是用一个分压电阻网络。配置方面首先要确认脑电模块的波特率。大部分模块支持通过串口命令修改波特率我建议统一设成 115200这样数据传输效率更高。然后要确认数据帧格式包括帧头、长度字段、校验方式。这些信息必须从模块的通信协议文档里获取不能靠猜。我用的模块是单通道 250Hz 采样每个样本 3 字节24 位有符号整数。数据帧格式是0xAA 0x55 长度 数据 校验和。每帧包含 10 个样本所以帧长度是 2 1 30 1 34 字节。在 115200 波特率下每秒可以传输约 338 帧也就是 3380 个样本远大于 250Hz 的需求所以串口带宽是充足的。4.3 ESP32-CYD 端固件开发与屏幕驱动ESP32-CYD 的开发环境是标准的 Arduino ESP32 核心包。安装好之后需要额外安装 TFT_eSPI 库和 ESP32 WebSocket 库。TFT_eSPI 库需要配置User_Setup.h文件指定屏幕驱动型号和引脚映射。CYD 板子的引脚映射是固定的TFT_CS 是 GPIO 15TFT_DC 是 GPIO 2TFT_RST 是 GPIO 4TFT_MOSI 是 GPIO 13TFT_SCLK 是 GPIO 14背光控制是 GPIO 21。这些参数在User_Setup.h里配置好编译时就不会报错。// TFT_eSPI User_Setup.h 关键配置 #define ILI9341_DRIVER #define TFT_CS 15 #define TFT_DC 2 #define TFT_RST 4 #define TFT_MOSI 13 #define TFT_SCLK 14 #define TFT_BL 21 #define TFT_BACKLIGHT_ON HIGH #define SPI_FREQUENCY 40000000SPI 频率我设的是 40MHz这是 ILI9341 的典型上限。如果你发现屏幕有花屏或者闪烁可以降到 27MHz 试试。背光控制引脚要设成 HIGH 才能点亮屏幕这个很容易忘。BLE 客户端部分的代码是基于 ESP32 的 Bluedroid 协议栈。需要先初始化 BLE扫描到 BW16 的设备名然后发起连接发现服务使能 Notify。这一套流程代码量不小但 ESP32 的例程里有现成的模板改一下 UUID 就能用。4.4 联调过程中的参数调优记录联调是最花时间的环节。我记录了几个关键参数的调优过程。第一个是 BLE 连接间隔。初始值设的是 30ms结果发现数据延迟很明显波形看起来一顿一顿的。后来改成 7.5ms波形立刻流畅了。但连接间隔太小会增加功耗如果是电池供电的场景可以折中设成 15ms。第二个是屏幕刷新率。初始设的是 100ms 刷新一次波形看起来像幻灯片。改成 30ms 后流畅度明显提升但 CPU 占用率也上去了。最后我用了双缓冲机制在一个缓冲区绘制的同时另一个缓冲区用于显示这样刷新率可以稳定在 30ms 而不撕裂。第三个是 WebSocket 推送频率。初始设的是每收到一帧就推送结果浏览器端卡顿严重。改成每 50ms 打包推送一次后浏览器端流畅了而且数据也没有丢失因为我在 ESP32 端做了缓冲队列。参数初始值调优后效果BLE 连接间隔30ms7.5ms波形流畅度明显提升屏幕刷新间隔100ms30ms从幻灯片变成实时波形WebSocket 推送间隔每帧推送50ms 打包浏览器端不再卡顿UART 波特率57600115200数据传输余量更充足BLE MTU23247分包开销减少 90%5. 常见问题与排查技巧实录5.1 串口收不到数据或全是乱码这是最常见的问题排查顺序是这样的先确认波特率是否匹配用示波器或者逻辑分析仪看一下 TX 线上的波形测量一个位的宽度反推波特率。如果波特率对再检查接线TX 和 RX 是否交叉连接。如果接线也对检查电平是否匹配5V 模块直接接 3.3V 的 RX 可能会损坏引脚。我遇到过一次特殊情况脑电模块的波特率是自适应的上电后会先发送一个同步字节BW16 需要回复确认后才切换到正常波特率。这种模块必须严格按照上电时序操作否则永远收不到数据。5.2 BLE 连接不稳定或频繁断开BLE 断连的原因通常有三个信号干扰、连接参数不匹配、电源不稳。信号干扰可以用手机上的 BLE 调试工具看一下 RSSI 值如果低于 -80dBm说明距离太远或者有遮挡。连接参数不匹配是指连接间隔、从机延迟等参数超出了双方的支持范围这个需要在代码里显式配置。电源不稳前面提过加退耦电容是最简单的解决办法。还有一个容易被忽略的点BW16 在 BLE 连接状态下如果串口数据量太大可能导致协议栈处理不过来而断连。解决办法是在 BW16 端加一个数据缓冲队列串口数据先入队BLE 发送线程从队列里取数据这样就不会因为突发数据量导致协议栈崩溃。5.3 屏幕显示异常或花屏屏幕花屏通常是 SPI 时序问题。先检查 SPI 频率是否过高降到 27MHz 试试。如果还花屏检查引脚映射是否正确特别是 CS 和 DC 引脚接错了会完全没显示。另外CYD 板子的背光引脚如果不拉高屏幕是黑的但其实是正常工作的这个很容易误判为屏幕坏了。还有一种情况是屏幕显示一半就卡住了这通常是内存不足导致的。ESP32 的 DMA 缓冲区有限如果同时跑 BLE、Wi-Fi 和屏幕刷新内存会很紧张。解决办法是降低屏幕分辨率或者减少颜色深度释放内存。5.4 网页端数据延迟或丢包WebSocket 延迟通常是因为 ESP32 端发送频率太高浏览器处理不过来。解决办法是在 ESP32 端做数据聚合每 50ms 发送一次而不是每帧都发。另外浏览器的 Canvas 绘制也要优化不要每收到一个数据点就重绘而是攒一批数据后统一重绘。丢包问题一般是 Wi-Fi 信号不好导致的。可以尝试把 ESP32-CYD 设成 SoftAP 模式让电脑直接连接它的热点这样延迟最低。如果必须走局域网确保 ESP32 和电脑在同一个子网并且没有防火墙拦截 WebSocket 端口。问题现象可能原因排查方法解决办法串口乱码波特率不匹配逻辑分析仪测位宽统一波特率BLE 频繁断连电源退耦不足示波器看电源纹波加 100uF0.1uF 电容屏幕花屏SPI 频率过高降低频率测试降到 27MHz网页延迟高推送频率过高看浏览器 CPU 占用改 50ms 打包推送数据丢帧缓冲区溢出打印队列长度加大环形缓冲区5.5 几个我踩过的坑和对应的技巧第一个坑是 BW16 的串口和烧录口复用。我在调试时忘了拔掉脑电模块的 TX 线结果烧录一直失败排查了半天才发现是串口冲突。后来我在脑电模块的 TX 线上加了一个跳线帽烧录时拔掉省事很多。第二个坑是 ESP32-CYD 的 USB 供电和屏幕背光抢电流。CYD 板子的 USB 接口供电能力有限如果屏幕背光全亮再加上 Wi-Fi 和 BLE 同时工作电压会跌到 3.0V 以下导致 ESP32 复位。解决办法是外接 5V 电源或者降低背光亮度。第三个坑是 BLE 的 CCCD 使能顺序。ESP32-CYD 必须在服务发现完成后才能使能 Notify如果顺序错了Notify 不会生效。这个在 ESP32 的例程里有标准流程照着改就行但如果你自己从头写很容易搞错。第四个坑是脑电数据的字节序。脑电模块输出的 24 位样本可能是大端序也可能是小端序这个必须确认。我一开始按小端序解析结果波形全是反的后来改成大端序才正常。提示调试脑电链路时建议先用信号发生器产生一个已知频率的正弦波输入脑电模块这样你可以直观地判断数据解析是否正确。比直接用真人脑电信号调试要方便得多。6. 链路扩展与后续可玩的方向这套原型链路跑通之后其实还有很多可以扩展的方向。最直接的是增加通道数。我目前用的是单通道如果你换成 4 通道或 8 通道的脑电模块只需要修改 UART 解析部分的帧长度和 BLE 数据包结构整体架构不用变。但要注意通道数增加后数据量成倍增长BLE 带宽可能成为瓶颈这时候可以考虑用 Wi-Fi 直连替代 BLE。另一个方向是在 ESP32-CYD 端做简单的信号处理。比如加一个 50Hz 陷波滤波器把工频干扰去掉。ESP32 的算力跑一个二阶 IIR 滤波器绰绰有余这样屏幕上显示的波形会更干净。如果你想做更复杂的处理比如 FFT 频谱分析ESP32 也能跑但需要优化内存使用。网页端也可以做得更丰富。目前只是画波形你可以加上频谱图、频带功率柱状图、数据录制和回放功能。WebSocket 传的是原始数据浏览器端可以用 JavaScript 做各种分析和可视化灵活性比在 ESP32 上做要高得多。还有一个有意思的方向是加 SD 卡存储。CYD 板子上有 SD 卡槽你可以把脑电数据同时存到 SD 卡里方便后续用电脑做离线分析。SD 卡写入和屏幕刷新、BLE 接收会有资源竞争需要合理安排任务优先级。最后再分享一个小技巧如果你觉得 BLE 配网太麻烦可以在 ESP32-CYD 上做一个 Web 配置页面通过浏览器输入 Wi-Fi 密码和 BLE 设备名保存到 NVS 里下次上电自动连接。这样就不用每次改代码重新烧录了。我在实际使用中发现这个功能虽然简单但极大提升了调试效率尤其是当你需要频繁切换脑电模块的时候。
返回列表