ARTICLE DETAIL

资讯详情

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

基于BW16与ESP32-CYD的无线脑电采集与实时波形显示系统

基于BW16与ESP32-CYD的无线脑电采集与实时波形显示系统 1. 项目缘起与整体链路设计脑电信号采集这件事早年我在实验室里接触的时候整套设备动辄十几万光是电极帽加放大器就占了大半个机柜数据还得通过并口或者专用采集卡往电脑里灌。后来消费级脑电模块慢慢多起来像单通道、双通道的干电极模块几百块就能拿到输出接口也统一成了串口或者 BLE。这就给个人开发者留出了很大的折腾空间——你可以用一块几十块钱的无线 MCU 把脑电数据接过来再通过无线链路转发到另一块带屏幕的开发板上实时画出波形同时还能开个网页做远程监视。我这次搭的这条链路核心就是三块东西脑电采集模块负责把头皮上的微弱电位差变成数字信号通过 UART 串口往外吐BW16作为无线桥接把串口数据通过 BLE 广播出去ESP32-CYD就是那块带 2.4 寸电容触摸屏的廉价开发板负责接收 BLE 数据一边在屏幕上画实时波形一边起一个轻量 Web 服务让同一局域网内的手机或电脑用浏览器就能看到数据。为什么选这个组合先说 BW16。它本质上是 RTL8720DN 模组做的开发板双频 WiFi 加 BLE5.0关键是它有两个硬件串口其中一个可以很干净地接脑电模块的 UART 输出另一个留给调试打印。市面上很多 ESP32 也能做 BLE 透传但 ESP32 同时跑 BLE 和 WiFi 的时候射频会打架丢包率在高速数据流下很难看。BW16 的双频特性让它在 BLE 转发这个单一任务上更稳功耗也低适合做纯粹的“数据搬运工”。再说 ESP32-CYD。这块板子火起来是因为它便宜带屏幕带触摸社区资料多。它在这里的角色是“接收端 显示端 服务端”。ESP32 本身支持 BLE 客户端模式可以主动去连 BW16 的 BLE 服务拿到数据后驱动 TFT 屏幕刷新波形同时用 WebServer 库起一个 HTTP 服务把最近的数据缓存成 JSON 接口网页端用 Canvas 或者 WebSocket 拉取。整条链路的数据流向是脑电模块 → UART → BW16 → BLE → ESP32-CYD → 屏幕显示 Web 页面这条链路最大的好处是解耦。采集端和显示端物理分离脑电模块可以戴在人头上BW16 揣在口袋里ESP32-CYD 放在桌上当监视器中间没有线缆束缚。对于做睡眠监测、注意力训练、或者简单的脑电波形观察来说这个原型已经够用了。适合谁来参考如果你手上有脑电模块但苦于没有好用的无线方案或者你想学 BLE 透传和 UART 转接的实际落地再或者你只是想找个小项目练手 ESP32 的屏幕和 Web 服务这条链路都能给你一个完整的参考。不需要你懂脑电信号处理只要会基本的 Arduino 开发和串口调试就行。2. 硬件选型与接线细节拆解2.1 脑电模块的 UART 输出特性我用的脑电模块是常见的单通道干电极方案输出波特率 57600数据包格式是固定的帧头加数据加校验。这里有个坑要注意很多脑电模块的 UART 电平是 3.3V 的但如果你买的是带 USB 转串口芯片的版本它可能直接输出 USB 信号那就没法接 MCU 的串口了。所以选模块的时候一定要确认它引出的是TX/RX/GND 三根线而不是一个 USB 口。模块上电后会自动开始往外吐数据频率大概每秒 50 到 100 个包。每个包的结构通常是帧头两个字节、数据长度一个字节、有效载荷若干字节、校验和一个字节。具体格式每个厂家不一样你得先拿 USB 转串口工具接电脑用串口助手抓一段原始数据把帧头和数据位对齐了再往下做。这一步千万别跳过我见过有人直接拿模块接 MCU结果因为波特率不匹配或者帧头没对齐读出来的全是乱码白白折腾一整天。2.2 BW16 的串口与 BLE 配置BW16 开发板上有两组 UART。我习惯把UART1留给脑电模块UART0通过板载的 USB 转串口芯片接电脑做调试打印。接线就三根脑电模块的 TX 接 BW16 的 RX脑电模块的 RX 接 BW16 的 TXGND 对 GND。注意不要接 5VBW16 的 IO 是 3.3V 电平接 5V 会烧。BW16 的 BLE 部分我把它配置成一个BLE 透传服务。具体做法是定义一个自定义的 GATT 服务里面放一个 Notify 特征值。BW16 收到串口数据后把数据打包成 BLE 通知发出去。这里的关键参数是MTU。默认 BLE 的 MTU 是 23 字节有效载荷只有 20 字节。脑电数据包如果超过 20 字节就得在 BW16 这边做分包或者跟 ESP32 协商更大的 MTU。我实测下来把 MTU 协商到 247 之后一个包能装 244 字节脑电数据基本一包就能发完省去了分包重组的麻烦。BW16 的 Arduino 环境配置稍微有点绕。你需要先在 Arduino IDE 的板管理器里添加 Realtek 的板支持 URL然后安装对应的核心包。安装完之后选板子要选RTL8720DN (BW16)烧录的时候按住 BOOT 键再按 RESET进入下载模式。这个操作我一开始老是忘后来在板子背面贴了个便签提醒自己。2.3 ESP32-CYD 的屏幕与 BLE 客户端ESP32-CYD 的屏幕是 2.4 寸 ILI9341 驱动分辨率 320x240SPI 接口。Arduino 环境下用TFT_eSPI库驱动需要在库的配置文件里把引脚定义改成 CYD 的对应引脚。这块板子的引脚定义网上有现成的直接抄就行但要注意背光引脚是 GPIO21触摸是 GPIO33 和 GPIO32这些在配置里都要对上。BLE 客户端这边ESP32 用NimBLE-Arduino库比原生的 BLE 库更省资源。NimBLE 的扫描和连接流程很清晰先扫描周围设备找到 BW16 的 MAC 地址或者设备名然后发起连接再注册通知回调。回调函数里拿到数据后直接丢给一个环形缓冲区主循环再从缓冲区里取数据做显示和 Web 推送。这里有个细节BLE 回调是在中断上下文里执行的里面不能做耗时操作所以缓冲区写入要快显示和网络发送都放到主循环里做。2.4 供电与接地注意事项整条链路里脑电模块对电源噪声非常敏感。我试过用同一个充电宝给 BW16 和脑电模块供电结果波形上全是 50Hz 的工频干扰。后来改成脑电模块单独用一颗纽扣电池或者低噪声 LDO 供电BW16 用另一路电源干扰就小了很多。如果你发现波形上有规律的尖峰先检查电源再检查接地。脑电模块的 GND 和 BW16 的 GND 一定要共地否则串口通信会不稳定。3. 固件实现与核心代码解析3.1 BW16 端串口到 BLE 的透传逻辑BW16 的固件逻辑很直白初始化串口初始化 BLE 服务然后在主循环里不断检查串口缓冲区有数据就通过 BLE 通知发出去。但这里有个性能问题如果串口数据来得太快主循环来不及处理数据就会丢。我的做法是开一个256 字节的环形缓冲区串口中断里把数据塞进缓冲区主循环里再从缓冲区取出来发 BLE。这样即使 BLE 发送有延迟串口数据也不会丢。// BW16 串口中断回调 void onUartData() { while (Serial1.available()) { uint8_t b Serial1.read(); ringBufWrite(uartBuf, b); } } // 主循环里发送 BLE 通知 void loop() { if (ringBufAvailable(uartBuf) PACKET_SIZE) { uint8_t packet[PACKET_SIZE]; ringBufRead(uartBuf, packet, PACKET_SIZE); bleNotify(packet, PACKET_SIZE); } }BLE 通知的发送频率也要控制。脑电模块每秒发 100 个包如果每个包都单独发一次 BLE 通知射频会一直处于忙状态功耗高而且容易丢包。我的做法是攒够 4 个包再发一次这样 BLE 通知频率降到 25Hz每次发 80 字节左右既保证了实时性又降低了射频压力。实测下来从脑电模块采集到 ESP32 屏幕显示端到端延迟大概在 80 到 120 毫秒之间对于波形观察来说完全够用。3.2 ESP32-CYD 端BLE 接收与数据解析ESP32 这边用 NimBLE 扫描并连接 BW16。连接成功后注册通知回调回调里把数据写入一个双缓冲区。一个缓冲区给屏幕刷新用一个给 Web 服务用避免两边抢数据。数据解析部分因为脑电模块的帧格式是固定的我直接在缓冲区里找帧头找到后按固定长度取出一帧校验通过后再提取有效载荷。// NimBLE 通知回调 void onNotify(NimBLECharacteristic* c) { std::string value c-getValue(); for (char ch : value) { ringBufWrite(bleBuf, (uint8_t)ch); } } // 主循环里解析帧 void parseEEG() { while (ringBufAvailable(bleBuf) FRAME_SIZE) { uint8_t frame[FRAME_SIZE]; ringBufPeek(bleBuf, frame, FRAME_SIZE); if (frame[0] 0xAA frame[1] 0x55) { // 校验通过提取数据 int16_t sample (frame[3] 8) | frame[4]; eegBuffer.push(sample); ringBufSkip(bleBuf, FRAME_SIZE); } else { ringBufSkip(bleBuf, 1); // 帧头不对滑动一个字节 } } }这里有个容易踩的坑BLE 通知的数据长度不固定。虽然我协商了 MTU但实际收到的通知长度可能因为底层分包而变化。所以解析的时候不能假设一次通知就是一帧必须把收到的所有字节都塞进缓冲区然后在缓冲区里做帧同步。我一开始就是假设一次通知一帧结果偶尔会丢帧波形上出现断点后来改成缓冲区滑动匹配才解决。3.3 屏幕波形刷新策略ESP32-CYD 的屏幕刷新是性能瓶颈。320x240 的分辨率如果每来一个数据点就重绘整个屏幕帧率会掉到个位数。我的做法是只重绘波形区域并且用TFT_eSPI 的 pushImage函数直接推像素而不是用 drawPixel 一个个画。具体来说我维护一个 320 点的波形数组每次新数据进来就左移一位然后只更新变化的那一列像素。// 波形刷新 void updateWaveform(int16_t newSample) { // 数组左移 for (int i 0; i 319; i) { waveBuf[i] waveBuf[i 1]; } waveBuf[319] newSample; // 只重绘最后一列 int y map(newSample, -2048, 2047, 0, 239); tft.drawPixel(319, lastY, TFT_BLACK); // 擦除旧点 tft.drawPixel(319, y, TFT_GREEN); // 画新点 lastY y; }但这样有个问题只画点不画线波形看起来是断断续续的。后来我改成画竖线把上一列的点和新列的点之间用线连起来波形就连续了。再后来发现竖线画法在快速变化时会有锯齿最终改成每 4 个点画一条线牺牲一点时间分辨率换取视觉平滑度。这个取舍看你的应用场景如果是要看波形细节那就老老实实画点如果只是看趋势画线更好看。3.4 Web 服务与前端页面ESP32 起 Web 服务用WebServer 库就够了。我开了两个接口/data返回最近 320 个点的 JSON 数组/返回一个内嵌的 HTML 页面。页面里用 Canvas 画波形每 100 毫秒 fetch 一次/data拿到新数据后重绘。这个方案简单但有个缺点每次都要传 320 个点的完整数组数据量大概 2KB 左右在局域网里没问题但如果想远程访问就有点浪费。后来我改成了WebSocket方案。ESP32 上用ArduinoWebSockets 库每来一个新数据点就推一次前端只更新最后一个点。这样数据量小实时性也更好。WebSocket 的握手和帧解析库都帮你做好了你只需要在onMessage回调里处理连接和断开就行。前端页面我用的是原生 JavaScript没有引入任何框架整个 HTML 文件不到 5KB直接存在 ESP32 的 Flash 里。canvas idwave width320 height240/canvas script const ws new WebSocket(ws:// location.host /ws); const ctx document.getElementById(wave).getContext(2d); let x 0; ws.onmessage (e) { const val parseInt(e.data); const y map(val, -2048, 2047, 240, 0); ctx.fillStyle black; ctx.fillRect(x, 0, 1, 240); ctx.fillStyle lime; ctx.fillRect(x, y, 1, 1); x (x 1) % 320; }; /script这个前端逻辑很简单每收到一个数据点就在当前 x 位置画一个绿点然后把 x 右移一位到头了就从 0 重新开始。这样屏幕上会显示最近 320 个点的波形滚动效果很自然。4. 联调过程中的典型问题与排查4.1 BLE 连接不稳定频繁断开这个问题我遇到的最多。表现是 ESP32 连上 BW16 之后过几秒就断开然后自动重连循环往复。排查下来有几个原因一是MTU 协商失败BW16 和 ESP32 都支持大 MTU但协商过程中如果有一方没响应就会回落到默认的 23 字节数据一多就超时断开。解决办法是在 ESP32 连接成功后主动调用bleClient-setMTU(247)并且检查返回值确认协商成功。二是射频干扰。BW16 如果同时开了 WiFi 和 BLE2.4G 频段会互相干扰。我的做法是 BW16 只开 BLEWiFi 完全关掉。ESP32-CYD 这边因为要起 Web 服务WiFi 必须开但它的 BLE 只做客户端连接数据量不大干扰可以接受。如果你发现 ESP32 的 WiFi 和 BLE 同时工作时丢包严重可以试试把 WiFi 信道固定在 1 或 11避开 BLE 的广播信道。三是电源噪声。BW16 在 BLE 发送瞬间电流会冲到 100mA 以上如果供电不足电压跌落会导致 BLE 复位。我在 BW16 的电源引脚旁边并了一颗 100uF 的电解电容和一颗 0.1uF 的陶瓷电容断开问题就少了很多。4.2 串口数据乱码或丢包串口乱码最常见的原因是波特率不匹配。脑电模块的波特率有时候是 57600有时候是 115200还有的是 9600。你必须先确认模块的规格书或者用串口助手试几个常见波特率看哪个能读出正常数据。另外BW16 的串口引脚如果和板载的 USB 转串口芯片共用可能会有冲突。我建议把脑电模块接在 UART1 上UART0 留给调试打印这样互不干扰。丢包问题通常是缓冲区太小。BW16 的默认串口缓冲区只有 64 字节脑电模块一秒发 100 个包每个包 10 字节就是 1000 字节每秒64 字节的缓冲区几毫秒就满了。解决办法是在Serial1.begin()之后调用Serial1.setRxBufferSize(512)把缓冲区加大。ESP32 这边同理NimBLE 的通知回调里如果处理太慢也会丢包所以回调里只做缓冲区写入不做解析。4.3 屏幕刷新闪烁或撕裂屏幕闪烁通常是因为刷新频率和屏幕扫描频率不同步。TFT_eSPI 库默认的 SPI 频率是 40MHz如果你超频到 80MHz屏幕可能会花屏。我的建议是保持默认频率不要贪快。撕裂问题是因为你在屏幕正在扫描的时候更新了显存解决办法是双缓冲在内存里维护一个完整的屏幕缓冲区更新完之后一次性推送到屏幕。但 ESP32 的内存有限320x240x2 字节就是 150KB加上其他开销内存会很紧张。折中方案是只对波形区域做双缓冲其他区域静态显示。4.4 Web 页面延迟高或卡顿Web 页面卡顿一般是数据推送频率太高。如果你每来一个数据点就推一次 WebSocket一秒 100 次浏览器渲染跟不上。我的做法是在 ESP32 这边做降采样每 4 个点取一个推给 WebSocket 的频率降到 25Hz页面就流畅了。另外前端 Canvas 的绘制也要优化不要每帧都清空整个画布只清空要更新的那一列。还有一个坑是ESP32 的 WebServer 和 BLE 同时工作时的内存冲突。ESP32 的 RAM 本来就不大BLE 协议栈占了一部分WebServer 的缓冲区又占了一部分如果再加上屏幕的显存很容易内存不足导致重启。我的解决办法是把 WebServer 的缓冲区调小并且把 HTML 页面存在 Flash 里而不是 RAM 里用PROGMEM关键字声明。4.5 常见问题速查表现象可能原因排查方法解决措施BLE 频繁断开MTU 协商失败打印 MTU 值主动 setMTU(247)串口乱码波特率不匹配换波特率试确认模块规格书波形有工频干扰电源噪声单独供电加 LDO 和滤波电容屏幕闪烁SPI 频率过高降低频率保持 40MHzWeb 页面卡顿推送频率过高降低推送频率降采样到 25HzESP32 重启内存不足打印剩余内存减小缓冲区用 PROGMEM5. 实测效果与可扩展方向5.1 端到端延迟与稳定性实测我把整条链路跑了一整晚脑电模块贴在额头上BW16 放在床头ESP32-CYD 放在桌上连着充电器。实测下来连续运行 8 小时没有断开端到端延迟稳定在 100 毫秒左右。丢包率方面我用序列号统计了一下8 小时总共发了大概 288 万个包ESP32 收到 287 万多个丢包率不到 0.3%。这个丢包率对于波形观察来说完全可以接受偶尔丢一两个点波形上看不出来。功耗方面BW16 平均电流 15mA 左右脑电模块 10mA整条链路用一块 2000mAh 的电池能撑 10 小时以上。ESP32-CYD 因为要驱动屏幕和 WiFi电流在 120mA 到 180mA 之间波动用充电宝供电比较合适。5.2 波形质量与去噪处理原始脑电信号里混着不少噪声主要是 50Hz 工频干扰和电极接触噪声。我在 ESP32 这边加了一个简单的滑动平均滤波窗口大小 4能滤掉一部分高频噪声。但滑动平均会引入相位延迟如果你要做实时反馈延迟会增加。更好的做法是用IIR 陷波滤波器专门滤 50Hz但计算量大一些。ESP32 跑 100Hz 采样率的 IIR 滤波完全没问题我试过用 biquad 滤波器CPU 占用率不到 5%。如果你要做更复杂的处理比如EEG 源定位的最小范数估计那 ESP32 就算不过来了。这种计算量得放到电脑或者服务器上做。我的做法是 ESP32 只负责采集和显示原始波形同时把数据通过 WebSocket 推给电脑电脑上用 Python 做进一步分析。这样分工明确ESP32 不会过载。5.3 后续可以怎么扩展这条链路目前只做了单通道如果你手上有双通道或者四通道的脑电模块BW16 的串口带宽完全够用只要把数据包格式改一下ESP32 这边多开几个波形缓冲区就行。屏幕可以分区域显示多个通道的波形Web 页面也可以用多个 Canvas 并排显示。另一个扩展方向是数据记录。ESP32-CYD 上有个 SD 卡槽你可以把脑电数据以 CSV 格式写到 SD 卡里方便后续离线分析。写卡的时候要注意SD 卡的写入速度有限如果采样率太高可能会丢数据。我的建议是先把数据缓存在内存里攒够一定量再批量写入减少文件系统开销。再一个就是触发反馈。如果你做的是注意力训练或者冥想辅助可以根据脑电波形的特征比如 alpha 波功率触发一些反馈比如屏幕变色、播放声音、或者通过 BLE 控制一个震动马达。ESP32 的 GPIO 很多接个马达或者 LED 都很方便。5.4 一些实操心得最后分享几个我在调试过程中总结的小技巧。第一先通链路再调细节。不要一上来就追求波形好看先把串口数据读通再把 BLE 连上最后再搞屏幕和网页。每一步都确认数据是对的再往下走。第二多用串口打印。BW16 和 ESP32 都有多余的串口把关键变量打印出来比猜问题快得多。第三电源要干净。脑电信号是微伏级别的电源上的一点纹波都会体现在波形上。我试过用充电宝供电波形上全是毛刺换成电池供电就干净了。第四不要怕改库。TFT_eSPI 和 NimBLE 这些库虽然好用但默认配置不一定适合你的场景该改就改改完记得备份。这条链路我前后折腾了大概两周中间踩了不少坑但跑通之后发现它的可玩性很高。你可以用它做睡眠监测、注意力训练、甚至简单的脑机接口实验。硬件成本加起来不到两百块比买成品设备便宜太多了。如果你也在做类似的东西希望这些经验能帮你少走点弯路。
返回列表