news 2026/9/29 13:23:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于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 的 TX,GND 对 GND。注意不要接 5V,BW16 的 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 驱动,分辨率 320x240,SPI 接口。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 id="wave" width="320" height="240"></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 和 BLE,2.4G 频段会互相干扰。我的做法是 BW16 只开 BLE,WiFi 完全关掉。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 频率过高降低频率保持 40MHz
Web 页面卡顿推送频率过高降低推送频率降采样到 25Hz
ESP32 重启内存不足打印剩余内存减小缓冲区,用 PROGMEM

5. 实测效果与可扩展方向

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 这些库虽然好用,但默认配置不一定适合你的场景,该改就改,改完记得备份。

这条链路我前后折腾了大概两周,中间踩了不少坑,但跑通之后发现它的可玩性很高。你可以用它做睡眠监测、注意力训练、甚至简单的脑机接口实验。硬件成本加起来不到两百块,比买成品设备便宜太多了。如果你也在做类似的东西,希望这些经验能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 13:14:03

DeepSeek职场应用实战:任务分类、提示词与参数调优指南

简介&#xff1a;来自清华大学人机协同团队的《DeepSeek如何赋能职场应用&#xff1f;》第二讲课件&#xff0c;面向职场人士、管理者和人工智能应用开发者&#xff0c;系统梳理DeepSeek从提示语技巧到多场景应用的完整路径。资源共1个PDF文件&#xff0c;压缩包约9.57MB&#…

作者头像 李华
网站建设 2026/9/29 13:09:19

硬件偶发bug排查三板斧:换机排除、录屏取证、批次对照

做硬件调试这行&#xff0c;最怕的不是东西彻底坏了&#xff0c;而是"时好时坏"。一块板子在你手里跑一整天都没事&#xff0c;一到客户现场就偶发断连&#xff1b;代码编译零报错&#xff0c;烧录却十次里有两三次失败&#xff1b;串口调试助手时不时蹦出乱码&#…

作者头像 李华
网站建设 2026/9/29 12:55:11

H3CTE Lab备考:用参考配置基线快速定位网络故障的排错方法论

简介&#xff1a;这份H3CTE Lab考试拓扑图及参考配置文档由阿寇鲜生整理&#xff0c;面向备考华为H3CTE认证的网络工程师。文档涵盖考试常用拓扑图&#xff0c;并结合OSPF、IS-IS、BGP等动态路由协议&#xff0c;VRRP、HSRP冗余协议&#xff0c;以及MPLS、GRE隧道等配置示例&am…

作者头像 李华
网站建设 2026/9/29 12:43:05

用WinHex定位文件第一扇区:数据恢复与磁盘诊断实战

简介&#xff1a;这是一份面向数据恢复、系统调试与安全分析人员的PPT演示文稿&#xff0c;以磁盘结构为主线&#xff0c;结合WinHex界面逐步截图&#xff0c;由浅入深地讲解MBR中分区表&#xff08;16字节/项&#xff09;、0x55AA结束标识的识别&#xff0c;以及DBR中FAT表个数…

作者头像 李华