脑电采集这件事很多人卡在第一步电极帽、放大器、采集软件一整套下来预算直接劝退。但如果你只是想做一个能跑通的原型链路——采集几路脑电信号、无线传到电脑、实时画出波形——其实用两块几十块钱的开发板就能搭起来。我最近用 BW16 做无线桥接、ESP32-CYD 做屏幕显示把一条从模拟前端到网页可视化的 EEG 原型链路跑通了。整条链路的核心思路是模拟前端负责把微伏级的脑电信号放大并数字化BW16 通过 UART 接收数据再用 BLE 转发出去ESP32-CYD 一边在本地屏幕上画波形一边把数据推到网页端做可视化。这套方案适合做脑机接口入门、生物电信号采集学习、或者单纯想玩一下 EEG 可视化的朋友不需要昂贵的专业设备也不需要复杂的上位机软件。1. 为什么选 BW16 加 ESP32-CYD 这套组合1.1 脑电原型链路里每个环节到底需要什么先说清楚一条 EEG 原型链路的基本构成。从头皮电极出来的信号幅度大概在 10 到 100 微伏之间这个量级直接进 ADC 是采不到的必须经过仪表放大器做差分放大再经过滤波去掉工频干扰和基线漂移最后送到 ADC 数字化。数字化之后的原始数据需要传到电脑或者手机上进行显示和分析这一步就是无线传输要解决的问题。传统做法是用专用的生物电采集芯片比如 ADS1299 这类它集成了放大、滤波、ADC 甚至内部参考源性能确实好但价格不便宜而且封装往往是 QFN 或者 BGA手工焊接难度大。对于原型验证阶段来说完全可以用分立运放搭一个简易的模拟前端配合开发板自带的 ADC 先跑通链路等验证完逻辑再换成专用芯片。无线传输这块常见的选择有 WiFi、BLE、Zigbee 几种。WiFi 带宽大但功耗高Zigbee 需要额外的协调器BLE 在功耗和易用性之间平衡得比较好手机和电脑都能直接连不需要额外的接收设备。BW16 这颗模组同时支持 WiFi 和 BLE用它做无线桥接很合适。显示端我选了 ESP32-CYD这块板子自带一块 2.8 寸的 TFT 屏幕和触摸功能价格便宜用来做本地波形显示和简单的交互控制非常方便。它和 BW16 之间通过 UART 通信BW16 把从模拟前端收到的数据转发给 ESP32-CYD后者负责显示和上传。1.2 BW16 在这条链路里扮演什么角色BW16 是基于 RTL8720DN 的模组双频 WiFi 加 BLE5.0跑起来之后可以同时做 WiFi 和 BLE 的事情。在这条链路里我让 BW16 专门负责无线桥接通过 UART 从模拟前端的 MCU 接收打包好的 EEG 数据然后通过 BLE 的 GATT 服务把数据转发出去。为什么不让 BW16 直接做采集因为它的 ADC 精度和抗干扰能力不如专门的模拟前端而且它的 GPIO 数量有限接多个电极通道会比较吃力。BW16 的 UART 配置需要注意波特率匹配。我用的波特率是 115200这个速率对于几通道、几百 Hz 采样率的 EEG 数据来说足够了。假设你有 4 个通道每个通道采样率 250 Hz每个采样点用 2 字节表示那么数据率是 4 × 250 × 2 2000 字节每秒加上包头包尾和校验115200 的波特率绰绰有余。如果你要跑 8 通道、500 Hz 采样率数据率会到 8000 字节每秒115200 仍然够用但如果你还想在 UART 上跑调试信息就得考虑提高到 460800 或者把调试信息关掉。BLE 这边BW16 需要配置一个 GATT 服务包含一个用于发送 EEG 数据的特征值。特征值的属性设置为 Notify这样客户端订阅之后就能持续收到数据。MTU 大小会影响单包能传多少数据默认的 23 字节 MTU 去掉 ATT 头之后只剩 20 字节对于 EEG 数据来说有点小建议协商到 247 字节这样一包能传更多采样点减少丢包概率。1.3 ESP32-CYD 的屏幕和联网能力怎么用ESP32-CYD 自带 2.8 寸 ILI9341 屏幕分辨率 320×240用来画几通道的 EEG 波形完全够用。我把它分成上下两个区域上半部分画实时波形下半部分显示一些状态信息比如采样率、丢包数、电池电压之类的。触摸屏可以用来做简单的交互比如切换通道显示、调整时间轴缩放。联网方面ESP32-CYD 支持 WiFi我让它同时做两件事一是通过 WiFi 把数据推到网页端做可视化二是保留一个本地 WebSocket 服务方便手机或者另一台电脑直接连上来看波形。网页端我用的是 Canvas 画波形数据通过 WebSocket 推过去延迟很低实测在局域网内从采集到网页显示大概 100 到 200 毫秒。ESP32-CYD 和 BW16 之间的 UART 连接需要注意电平匹配。BW16 的 IO 电平是 3.3VESP32-CYD 也是 3.3V可以直接连不需要电平转换。接线的时候注意 TX 接 RX、RX 接 TX共地。如果你用的是硬件串口ESP32 的 UART1 或者 UART2 都可以我用的 UART2因为 UART0 要留给调试输出。2. 模拟前端到 BW16 的 UART 数据打包2.1 模拟前端输出的数据格式怎么定模拟前端这块我用的是分立运放搭的仪表放大加二级滤波MCU 用的是 STM32F103因为它 ADC 速度快、DMA 好用而且价格便宜。ADC 配置成 12 位、多通道扫描模式用 DMA 搬运数据采样率通过定时器触发控制。每个通道采到的原始值是一个 0 到 4095 的整数对应 0 到 3.3V 的电压。原始 ADC 值直接传没有意义因为脑电信号是差分信号而且有直流偏置。我在 MCU 里做了简单的处理先减去一个基线值上电时采一段时间的平均值然后做一阶高通滤波去掉基线漂移再做一阶低通滤波去掉高频噪声。处理完之后的数据还是整数但范围大概在 -2048 到 2047 之间用 int16 表示刚好。打包格式我设计得很简单每帧以两个字节的包头 0xAA 0x55 开始然后是一个字节的帧类型0x01 表示 EEG 数据一个字节的通道数两个字节的采样点数然后是各个通道的数据交错排列最后是两个字节的校验和。校验和用简单的累加和虽然不复杂但足够发现大部分传输错误。注意包头不要用 0xAA 0xAA 这种容易在数据里出现的组合0xAA 0x55 相对安全但也不是绝对如果数据里恰好出现这个组合接收端会误判。更稳妥的做法是用一个字节的帧头加一个字节的帧头取反比如 0xAA 0x55接收端检测到 0xAA 之后检查下一个字节是不是 0x55如果是才开始解析。2.2 UART 传输的波特率和流控怎么选波特率的选择前面提过115200 对于几通道的 EEG 数据够用。但如果你要跑更高的采样率或者更多的通道就得往上提。460800 和 921600 都是常见的选择BW16 的 UART 支持到几兆波特率STM32 的 USART 在 72MHz 时钟下也能跑到 4.5Mbps所以波特率不是瓶颈。流控这块如果你的数据率接近 UART 的极限建议开启硬件流控RTS/CTS否则接收端来不及处理的时候会丢数据。BW16 的 UART 支持硬件流控STM32 这边也可以用硬件流控。但如果你数据率不高比如 115200 下只用了不到一半的带宽那不开流控也没问题软件上做好缓冲就行。我在实际测试中发现一个问题BW16 在 BLE 连接建立或者断开的时候UART 接收会有短暂的卡顿如果这时候 STM32 还在持续发数据就会丢包。解决办法是在 STM32 这边加一个发送缓冲队列BW16 那边如果来不及收通过流控信号告诉 STM32 暂停发送等 BLE 稳定了再继续。如果没有硬件流控那就得在协议层做重传但 EEG 数据是流式的重传意义不大丢几个包对波形显示影响不大所以简单做法就是允许丢包在显示端做插值。2.3 数据打包的代码实现STM32 这边的打包代码大概长这样#define FRAME_HEADER_0 0xAA #define FRAME_HEADER_1 0x55 #define FRAME_TYPE_EEG 0x01 typedef struct { uint8_t header[2]; uint8_t type; uint8_t ch_count; uint16_t sample_count; int16_t data[CH_COUNT * SAMPLES_PER_FRAME]; uint16_t checksum; } eeg_frame_t; void send_eeg_frame(int16_t *buffer, uint8_t ch, uint16_t samples) { eeg_frame_t frame; frame.header[0] FRAME_HEADER_0; frame.header[1] FRAME_HEADER_1; frame.type FRAME_TYPE_EEG; frame.ch_count ch; frame.sample_count samples; uint16_t sum 0; for (int i 0; i ch * samples; i) { frame.data[i] buffer[i]; sum (uint16_t)buffer[i]; } frame.checksum sum; HAL_UART_Transmit_DMA(huart1, (uint8_t *)frame, sizeof(frame.header) sizeof(frame.type) sizeof(frame.ch_count) sizeof(frame.sample_count) ch * samples * sizeof(int16_t) sizeof(frame.checksum)); }这段代码里CH_COUNT和SAMPLES_PER_FRAME根据你的实际配置来定。我一般一帧打包 10 个采样点这样帧率是采样率的十分之一比如 250 Hz 采样率对应 25 帧每秒这个帧率对于 BLE 传输来说很轻松。校验和这里用的是简单的累加和把每个采样点的值当成无符号数累加。接收端收到之后重新算一遍和帧里的校验和对比不一致就丢弃这一帧。虽然简单但实测能发现绝大部分传输错误。3. BW16 的 BLE 服务配置与数据转发3.1 BW16 的 UART 接收和缓冲策略BW16 这边UART 接收我用的是中断加环形缓冲的方式。每收到一个字节就进中断把数据塞进环形缓冲主循环里再从缓冲里取数据做解析。环形缓冲的大小我设的是 2048 字节对于 115200 波特率来说大概能缓冲 170 毫秒的数据足够主循环处理其他事情了。解析的时候先找包头 0xAA 0x55找到之后根据帧类型和通道数、采样点数算出这一帧的总长度然后检查缓冲里有没有足够的数据不够就等下一轮。够的话就把整帧取出来校验和验证通过之后把数据提取出来准备通过 BLE 发送。这里有个细节BW16 的 UART 中断优先级要设得比较高否则 BLE 协议栈处理的时候可能会阻塞 UART 中断导致丢数据。我在实际调试的时候发现如果 UART 中断优先级低于 BLE 任务在 BLE 连接参数更新的时候会丢几个字节。把 UART 中断优先级提到最高之后就没这个问题了。3.2 BLE GATT 服务的特征值设计BLE 这边我定义了一个自定义的 GATT 服务UUID 用随机生成的 128 位 UUID避免和标准服务冲突。服务里包含两个特征值一个用于发送 EEG 数据属性是 Notify另一个用于接收控制命令属性是 Write。控制命令包括开始采集、停止采集、调整采样率、切换通道等等。EEG 数据特征值的 UUID 我设成0000fff1-0000-1000-8000-00805f9b34fb这种格式虽然这种格式看起来像标准 UUID但实际上是自定义的。Notify 的配置需要注意 CCCD客户端特征配置描述符客户端订阅之后才能收到通知。BW16 的 SDK 里发送 Notify 的 API 是ble_server_send_notify之类的具体名字看你的 SDK 版本。MTU 协商这块BW16 默认的 MTU 是 23我主动请求协商到 247。协商成功之后单包能发 244 字节的 payload对于 EEG 数据来说一包能装 122 个 int16 采样点如果 4 通道的话就是 30 个采样点每通道够用了。如果 MTU 协商失败那就只能按 20 字节发效率会低很多所以建议在连接建立之后立刻发起 MTU 协商。3.3 数据从 UART 到 BLE 的转发逻辑转发逻辑很简单主循环里检查环形缓冲里有没有完整的帧有的话就解析出来然后把数据通过 BLE Notify 发出去。但这里有个问题BLE Notify 的发送频率不能太高否则协议栈会忙不过来。我实测下来BW16 每秒发 50 到 100 个 Notify 包比较稳再高就容易出问题。所以我在转发的时候做了一个简单的速率控制如果一帧数据比较大就拆成多个 Notify 包发如果帧率太高就攒几帧一起发。比如 250 Hz 采样率、4 通道、每帧 10 个采样点帧率是 25 帧每秒每帧数据量是 4 × 10 × 2 80 字节加上包头包尾大概 90 字节一个 Notify 包就能装下。25 个 Notify 每秒完全在安全范围内。如果采样率提高到 1000 Hz帧率变成 100 帧每秒每帧 90 字节那就是 100 个 Notify 每秒接近上限了。这时候可以把每帧的采样点数从 10 提高到 20帧率降到 50 帧每秒每帧数据量变成 170 字节左右还是一个 Notify 包能装下Notify 频率降到 50 每秒就稳了。提示BW16 的 BLE 发送缓冲有限如果 Notify 发得太快ble_server_send_notify会返回失败。我的做法是检查返回值如果失败就把数据暂存到一个待发队列里等下一次循环再试。待发队列不要设太大否则延迟会累积我设的是 5 帧的容量超过就丢弃最旧的数据。4. ESP32-CYD 的屏幕波形绘制与网页推送4.1 从 UART 收到数据之后怎么解析和缓存ESP32-CYD 这边UART 接收 BW16 转发过来的数据。等等这里需要澄清一下链路BW16 是通过 BLE 把数据发出去的ESP32-CYD 如果要收要么也走 BLE要么 BW16 同时通过 UART 把数据发给 ESP32-CYD。我采用的是后者BW16 在通过 BLE 发送的同时也通过另一个 UART 口把数据转发给 ESP32-CYD。这样 ESP32-CYD 不需要处理 BLE 协议栈只需要处理 UART 数据简单很多。BW16 有两个 UART一个用来接收 STM32 的数据另一个用来转发给 ESP32-CYD。转发的时候直接把收到的原始帧透传过去就行不需要重新打包。ESP32-CYD 收到之后按照同样的协议解析提取出各个通道的采样值。缓存方面我用了一个环形缓冲来存最近的采样点缓冲大小是 1024 个采样点每通道。屏幕绘制的时候从缓冲里取最新的数据画波形网页推送的时候也是从缓冲里取数据。这样屏幕显示和网页显示用的是同一份数据不会出现两边不一致的情况。4.2 ILI9341 屏幕上画多通道波形的技巧在 320×240 的屏幕上画 4 通道波形每个通道分到 60 个像素的高度剩下 60 个像素用来显示状态信息。波形的纵轴范围我设的是 -2048 到 2047映射到 60 个像素上每个像素大概对应 68 个 ADC 值。这个分辨率对于观察脑电信号的节律来说够了但如果要看更精细的波形细节就得放大纵轴或者减少显示的通道数。绘制的时候我用的是 TFT_eSPI 库它支持 DMA 刷屏速度比普通的 SPI 刷屏快很多。每帧绘制的时候先清除上一帧的波形区域然后画新的波形。清除和绘制都用 DMA这样不会阻塞主循环。实测下来4 通道、每通道 320 个点的波形刷新率能到 30 帧每秒以上看起来很流畅。颜色方面每个通道用不同的颜色区分通道 1 用红色通道 2 用绿色通道 3 用蓝色通道 4 用黄色。背景用黑色网格线用深灰色。这样在屏幕上看起来很清楚即使是在光线比较强的环境下也能看清。触摸屏这块我做了几个简单的交互单击屏幕切换显示模式全部通道叠加显示或者分通道显示左右滑动调整时间轴缩放上下滑动调整纵轴增益。这些交互通过 TFT_eSPI 的触摸库来实现响应速度还不错。4.3 网页端 WebSocket 推送和 Canvas 绘制网页端这块ESP32-CYD 跑一个 WebSocket 服务器网页通过 JavaScript 连接上来接收数据并绘制。WebSocket 的好处是双向通信网页也可以发控制命令给 ESP32-CYD比如调整采样率、切换通道等等。数据推送的格式我用的是二进制每个采样点用 2 字节的 int16 表示多个通道交错排列。网页端收到二进制数据之后用 DataView 解析成整数然后画到 Canvas 上。Canvas 的绘制我用的是 requestAnimationFrame每帧绘制一次和屏幕刷新率同步。网页的布局很简单上面一个大 Canvas 画波形下面几个按钮做控制。Canvas 的宽度自适应窗口大小高度固定 400 像素。波形的绘制逻辑和屏幕上类似也是每个通道一个颜色纵轴映射到 Canvas 高度。时间轴我设的是显示最近 5 秒的数据可以通过按钮调整到 1 秒、2 秒、10 秒。延迟方面从 STM32 采集到网页显示实测大概 100 到 200 毫秒。这个延迟对于脑电信号的实时观察来说完全可以接受毕竟脑电信号本身的变化就很慢几百毫秒的延迟不影响观察节律和趋势。注意WebSocket 推送的数据量如果太大网页端处理不过来会卡顿。我的做法是在 ESP32-CYD 这边做降采样如果采样率是 1000 Hz推给网页的时候降到 250 Hz这样数据量减少到四分之一网页端处理起来很轻松而且 250 Hz 对于观察脑电节律来说足够了。5. 实测中遇到的坑和解决办法5.1 BLE 连接不稳定和丢包问题调试过程中遇到最多的问题就是 BLE 连接不稳定。具体表现是连接建立之后过一段时间会断开或者数据传着传着就停了。排查下来有几个原因一是连接参数设置不合理连接间隔太短会导致功耗高、容易断太长会导致延迟大、丢包多。我最后把连接间隔设成 30 毫秒从机延迟设成 0超时设成 4 秒这样在延迟和稳定性之间取得了比较好的平衡。二是 MTU 协商失败。有些手机或者电脑的 BLE 协议栈不支持大的 MTU协商的时候会失败然后回退到默认的 23。这种情况下数据包变小发送频率变高容易丢包。解决办法是在代码里检测 MTU 协商结果如果协商失败就主动降低数据率比如把每帧的采样点数减少或者把采样率降下来。三是 WiFi 和 BLE 共存的问题。BW16 同时支持 WiFi 和 BLE但如果两个都在用射频资源会冲突导致 BLE 丢包。我的做法是 BW16 只开 BLEWiFi 关掉WiFi 的事情交给 ESP32-CYD 做。这样两个射频各干各的互不干扰。5.2 UART 数据错位和校验失败UART 数据错位是另一个常见问题。表现是解析出来的数据乱七八糟波形完全不对。排查下来原因主要有两个一是波特率不匹配STM32 和 BW16 的波特率差了一点点短时间内看不出来时间长了就会累积误差导致错位。解决办法是用高精度的晶振或者用自动波特率检测。我用的 STM32F103 外部晶振是 8MHz倍频到 72MHz波特率误差在 0.1% 以内问题不大。二是数据里恰好出现了包头组合。前面提过0xAA 0x55 不是绝对安全如果数据里恰好有这个组合接收端会误判。我的解决办法是在包头后面加一个长度字段接收端检测到包头之后先读长度字段然后根据长度字段跳过相应数量的字节再找下一个包头。这样即使数据里出现了包头组合也不会导致解析错位最多是丢一帧。校验失败的话先检查硬件连接TX/RX 有没有接反地有没有共。然后检查波特率、数据位、停止位、校验位是否匹配。如果硬件没问题那就是软件上的校验和计算方式不一致检查发送端和接收端的校验和算法是不是一样的。5.3 屏幕刷新率和数据率的平衡ESP32-CYD 的屏幕刷新和数据接收之间需要平衡。如果屏幕刷新太频繁会占用大量 CPU 时间导致 UART 接收不及时丢数据。如果屏幕刷新太慢波形看起来会卡顿。我的做法是把屏幕刷新放在一个独立的 FreeRTOS 任务里优先级设得比 UART 接收任务低。UART 接收任务只管收数据存缓冲屏幕刷新任务从缓冲里取数据画波形。这样即使屏幕刷新偶尔卡一下也不会影响数据接收。另外屏幕刷新的时候不要每次都全屏重绘只重绘波形区域。状态信息区域只在数据变化的时候重绘比如丢包数变了才更新。这样能省不少 CPU 时间。实测下来4 通道波形、30 帧每秒的刷新率CPU 占用大概 40% 左右还有足够的余量处理 WebSocket 推送和其他任务。5.4 电源噪声对脑电信号的影响脑电信号非常微弱电源噪声很容易耦合进来。我在调试的时候发现如果用 USB 供电波形上会有明显的 50 Hz 工频干扰和开关电源的高频噪声。解决办法有几个一是用电池供电锂电池或者干电池都行电池供电最干净。二是加 LC 滤波在电源入口处加电感和电容滤掉高频噪声。三是模拟地和数字地分开最后单点共地避免数字噪声串到模拟部分。电极这块我用的是干电极不需要导电膏但接触阻抗比较大信号质量不如湿电极。如果你对信号质量要求高建议用湿电极涂一点导电膏接触阻抗能降到 10 千欧以下信号会干净很多。电极的位置按照 10-20 系统来放最简单的就是额叶和耳垂参考能采到比较明显的 alpha 节律。提示调试脑电信号的时候先不要接电极把输入端短接看看基线噪声有多大。如果短接的时候噪声就很大说明是电路或者电源的问题不是电极的问题。短接噪声应该在几个微伏以内如果超过几十微伏就得检查放大器的增益设置和滤波参数了。6. 这条链路还能怎么扩展6.1 增加通道数和提高采样率目前跑的是 4 通道、250 Hz 采样率对于观察 alpha 节律和简单的脑电实验来说够了。如果要增加通道数比如做到 8 通道或者 16 通道主要瓶颈在模拟前端的 ADC 速度和 UART 带宽。STM32F103 的 ADC 在 12 位分辨率下最快能到 1 MHz 采样率8 通道扫描的话每通道 125 kHz远远够用。UART 带宽前面算过115200 下 8 通道 500 Hz 采样率的数据率是 8000 字节每秒也够用。如果要提高采样率到 1000 Hz 甚至更高就得考虑换 MCU 了。STM32F103 的 ADC 在 12 位下 1 MHz 采样率如果 16 通道扫描每通道只有 62.5 kHz对于 1000 Hz 采样率来说还是够的。但如果要更高的分辨率比如 16 位或者 24 位就得用外部的 ADC 芯片比如 ADS1299 或者 ADS131M08这些芯片专门为生物电信号设计性能好很多。6.2 加入阻抗测量和信号质量评估脑电电极的接触阻抗直接影响信号质量如果能实时测量阻抗就能知道电极有没有贴好。阻抗测量的原理是往电极注入一个已知的微小交流电流然后测量电压根据欧姆定律算出阻抗。很多专用的生物电前端芯片都集成了阻抗测量功能比如 ADS1299 就有。如果不想换芯片也可以用分立方案做阻抗测量用一个 DAC 产生正弦波通过一个已知电阻注入到电极然后用 ADC 测量电极上的电压算出阻抗。这个方法精度不如专用芯片但胜在简单对于原型验证来说够用了。信号质量评估这块可以计算信号的噪声水平、工频干扰强度、基线漂移速度等指标然后在屏幕上显示出来。如果质量太差就提示用户检查电极。这个功能对于实际使用很有帮助尤其是对于非专业的用户来说。6.3 网页端加入简单的信号处理网页端目前只是画原始波形其实可以在浏览器里做一些简单的信号处理比如带通滤波、陷波滤波、FFT 频谱分析等等。JavaScript 做这些计算完全没问题性能也够。带通滤波可以用 IIR 滤波器截止频率设成 0.5 Hz 到 45 Hz去掉基线漂移和高频噪声。陷波滤波用 50 Hz 或者 60 Hz去掉工频干扰。FFT 频谱分析可以用 Web Audio API 里的 AnalyserNode或者自己用 JavaScript 实现一个简单的 FFT。频谱图对于观察脑电的节律成分很有帮助比如 alpha 节律在 8 到 13 Hz 会有明显的峰值。如果做脑机接口实验频谱特征是很常用的输入。再进一步可以在网页端做简单的分类比如用阈值判断 alpha 节律的强度或者用简单的机器学习模型做二分类。这些都可以在浏览器里跑不需要服务器。TensorFlow.js 或者 ONNX Runtime Web 都支持在浏览器里跑模型对于原型验证来说很方便。6.4 从原型到产品的距离这条链路目前还是原型阶段离产品还有距离。主要差距在几个方面一是模拟前端的性能分立运放的噪声和共模抑制比不如专用芯片如果要做产品建议换成 ADS1299 这类专用芯片。二是电源管理原型阶段用 USB 或者电池供电产品需要考虑充电管理、低功耗模式、电池寿命等问题。三是外壳和电极原型阶段用杜邦线连接产品需要设计合适的外壳和电极接口保证接触可靠、佩戴舒适。不过原型阶段的价值在于验证链路和算法等这些跑通了换成专用芯片和产品级设计就是工程问题了。我个人的经验是原型阶段不要追求性能先把链路跑通把数据格式、传输协议、显示逻辑这些定下来后面换硬件的时候软件改动很小。提示如果你打算把这条链路用于实际的数据采集记得在软件里加时间戳。时间戳可以来自 MCU 的定时器也可以来自电脑的时钟但一定要有否则后面做数据分析的时候没法对齐不同通道的数据。时间戳的精度要求不高毫秒级就够了但一定要单调递增不能回退。最后分享一个小技巧调试 BLE 的时候如果手头没有专业的 BLE 调试工具可以用手机上的通用 BLE 调试应用先扫一下看看服务能不能被发现特征值能不能订阅。这一步能排除很多基础问题比如服务没注册成功、UUID 写错了、权限没开等等。等手机能收到数据了再上电脑端做更复杂的处理。这个顺序能省不少时间我一开始就是直接在电脑上调试结果卡了好久才发现是服务注册的问题。