不用怀疑,手机和单片机之间通过蓝牙打交道,最省事、最常被拿来起手的方案就是透传。我最早玩BLE的时候,也踩过模块厂商私有协议的坑,后来换成N32WB03X这种自带BLE射频的单片机之后,才真正觉得透传这件事是可以自己掌控的。这篇就用实际工程的经验,把从环境搭建、GATT服务设计、代码实现到调试助手使用的完整链路捋一遍,顺便把我在调试中遇到过的问题和排查套路交代清楚。
1. 项目概述与整体设计思路
1.1 N32WB03X为什么适合做透传
N32WB03X是Cortex-M0内核的低功耗蓝牙SoC,主频48MHz,支持BLE 5.1,内置BLE射频收发器,Flash有144KB,SRAM将近29KB。这个配置在BLE MCU里属于性价比很能打的水平。做透传场景不需要大算力,关键是射频性能和功耗控制,再加上它自带比较丰富的外设资源,比如UART、SPI、I2C、ADC、PWM,意味着MCU端除了透传,还能顺手把传感器数据采集、电机控制、LED驱动这些活一起干了。
我起初看过一些低成本的BLE模块方案,模块+外部MCU的架构虽然成熟,但会给整机带来两个麻烦:一是模块和MCU之间的串口通信协议需要自己定,数据流转效率低;二是成本高、体积大,结构上不好摆。N32WB03X这种单芯片方案则把应用层和BLE协议栈跑在同一颗芯片上,数据直接在内存里搬运,延迟更低,开发链路缩短,物料成本也能压下来。
虽说名字看起来像是用来处理“无线数据传输”,但它的应用范围比想象中广得多,包括如下几类场景:
- 智能家居:设备状态上报、远程开关控制。
- 穿戴设备:心率、计步等传感器数据同步到App。
- 工业现场:传感器采集节点和手机之间做参数配置和实时读取。
- 消费电子:玩具遥控、电子烟、智能锁等短距离控制。
总而言之,只要有“手机和硬件设备之间交换小数据量信息”的需求,都可以用N32WB03X做透传来解决。
1.2 手机与MCU透传的典型架构
先说清楚透传到底传的是什么。BLE透传本质上是建立一个逻辑上的“数据管道”,它将手机端应用层的数据封装成ATT协议的Write/Notification操作,交到对端设备的GATT层;对端设备处理完再通过Notification原路返回数据给手机。作为开发者,我们要做的就是在中间这一层把数据“透明”地接住。
整体系统可以拆成四个角色:
- BLE Server(从机,也就是N32WB03X):对外提供服务和特征值,接收手机发来的写请求,主动上报数据给手机。
- BLE Client(主机,也就是手机App或调试助手):发起连接、发现服务、写入数据、接收通知。
- GATT层:定义数据组织方式和访问规则。透传的数据就是放在某个Characteristic(特征值)的属性缓冲区里。
- 物理链路:BLE 5.1射频,采用2.4GHz频段,支持1M/2M PHY。
数据从MCU侧到手机侧的流经路径大致是:
- MCU外设(如UART接收来自传感器的数据)将数据放入应用缓冲区。
- 应用层调用协议栈的发送接口,由协议栈将数据封装为GATT Notification。
- BLE协议栈通过射频把数据包发出去。
- 手机端的BLE调试助手收到Notification并显示出来。
反过来,手机下发数据则是走GATT Write,由MCU端协议栈通过回调函数通知应用层,应用层再决定是解析命令、控制IO,还是将数据原封不动地发到串口。
所以,所谓“透传”,在GATT层并非真的有一个透明管道,而是两端各自实现了“接住数据”的能力。做这个项目的核心工作,就是把这个双向通道的服务框架搭好,并且保证数据不出错、不丢包。
1.3 方案选型与设计取舍
我最初在方案调研时,重点比较过三类实现方式:
- BLE透传模块(如某宝常见的JDY-08、CC2541模块):开发简单,但受限于模块厂商的AT指令集,没办法做深度的连接参数控制和低功耗策略调整,数据吞吐上限也很有限。
- 纯蓝牙从机MCU方案(如NRF52832,辅以外挂MCU):灵活性强,但成本偏高,对于只做“单芯片设备”的场景有点浪费。
- N32WB03X单芯片方案:集成BLE协议栈和应用处理器,可以用一套SDK完成所有事情,兼顾开发效率和可控性。
最后选择N32WB03X,还有一个关键原因:国民技术的SDK质量和文档齐全程度在国产BLE MCU里属于上乘水准,官方提供了丰富的例程,包含透传、低功耗、OTA等。基于官方例程的二次开发门槛非常低,适合项目周期紧的情况。
另外,考虑到不是所有开发者的MCU基础都是从BLE开始,我建议在动手之前先掌握几个基础概念:GATT Server/Client的区别、Characteristic的属性和回调机制、以及广播包和服务发现的基本流程。这些内容在后面代码分析中会反复出现。
2. 开发环境准备与工程搭建
2.1 软件环境清单与安装注意事项
SDK和开发环境的版本匹配是很多人卡壳的第一个地方,我列一下我最终确认能用的组合:
| 工具 | 版本/型号 | 用途 |
|---|---|---|
| KEIL MDK | 5.30及以上,推荐5.36 | 编译调试工程 |
| N32WB03x_SDK | 官方最新版(带BLE协议栈库) | 提供驱动、协议栈API和例程 |
| 蓝牙协议栈文档 | 随SDK提供(PDF/CHM) | 查阅GATT API和事件定义 |
| BLE调试助手 | nRF Connect / LightBlue / 官方助手 | 手机端收发包测试 |
| USB转TTL模块 | CH340/CP2102均可 | 查看MCU串口日志 |
| 逻辑分析仪 | Saleae 16MHz及以上 | 分析UART时序和IO状态 |
安装注意事项有几点值得强调:
注意:KEIL安装完成后,务必先安装N32WB03X的器件支持包(或通过Pack Installer导入),否则工程打开后芯片型号是空的,会导致编译报错或者烧录失败。
另外,如果你的开发板带有板载DAPLink调试器,接线就直接用USB线供电并调试。如果是自制板,建议预留SWD接口和UART日志串口,这在项目调试阶段几乎是保命设计。
2.2 硬件准备与核心外设规划
N32WB03X开发板本身就集成了蓝牙天线、晶振、去耦电容和必要的射频匹配电路,直接拿来用没有问题。如果自制硬件,需要注意以下核心点:
- 射频电路:蓝牙天线区域禁铺地,需要预留π型匹配网络。
- 电源设计:推荐使用LDO给MCU供电,纹波控制在50mV以内;避免数字电路串扰进入射频前端。
- 晶振选择:32MHz主晶振需要精度在±20ppm以下,蓝牙通信的稳定性严重依赖晶振精度。
- 串口规划:UART_RX和UART_TX选在可用的普通GPIO上,留出跳线或排针方便调试。
- 复位与启动配置:保证BOOT引脚默认状态能从Flash启动,避免烧录后无法运行。
关于“MCU没有USB差分信号数据引脚怎么办”这一点,我在该项目里也遇到过类似困惑。N32WB03X是没有USB外设的,所以手机和MCU之间走蓝牙是唯一选择。如果调试时需要通过USB串口与PC通信,就需要外接USB转TTL模块,模块的TX、RX交叉连接到MCU的UART引脚,共地之后即可通信。千万不要试图在N32WB03X上硬接USB的D+/D-,它没有对应引脚,接了也没反应。
2.3 SDK工程结构与基础例程讲解
解压SDK后,常见目录结构如下:
Doc:芯片手册、API参考、硬件设计指南。Firmware:蓝牙协议栈固件(fwp文件)和烧录工具说明。Middleware:协议栈头文件、库文件。Projects/ble/uart:官方透传例程,几乎是所有透传项目的起点。Projects/ble/dfu:基于BLE的固件升级例程。
我开发时直接在Projects/ble/uart基础上修改,已有的代码包含:
- 广播初始化
- GATT服务注册
- 串口收发缓冲和回调
- 串口数据转发到BLE(Tx)
- BLE数据写回调转发到串口(Rx)
这个例程已经实现了最基础的“串口和BLE互转”功能。但要注意,例程的缓冲区和连接参数是经验值,实际产品中应根据自己的数据量、峰值带宽和功耗需求去调整,不能无脑套用。
2.4 移植与编译调试技巧
新建自己的工程时,不必从零开始,最好把官方例程复制一份再改。以下几个地方必须确认:
- 芯片型号选择:在KEIL的Options for Target中,确保Device选的是N32WB03x系列对应的型号。
- 宏定义:预定义宏可能需要包含
USE_STDPERIPH_DRIVER、N32WB03x等,不同版本SDK略有差异。 - 头文件路径:把
Middleware、Library、Projects下的头文件目录都加进Include Path,缺一个都编译不过。 - 协议栈固件:烧录时一般先烧录
fwp(协议栈固件),再烧录用户应用程序。有些版本SDK支持合并烧录,具体看官方烧录工具说明。
编译时如果报“未定义符号”的错误,多半是协议栈库文件没有正确链接,或者头文件路径不完整。可以先找到例程的.uvprojx文件,对比一下工程配置,再决定是直接改例程还是重新建工程。
3. BLE透传的核心代码实现
3.1 透传服务的GATT设计与UUID规划
BLE的GATT服务结构是层级嵌套的:服务(Service)包含特征(Characteristic),特征包含属性和描述符(Descriptor)。设计透传服务最简单的方式是参考Nordic的NUS(Nordic UART Service),但也可以用自定义UUID。
我这里推荐定义两个Characteristic:
- TX Characteristic(Notify):MCU向手机发送数据用,属性为Notify。
- RX Characteristic(Write):手机向MCU发送数据用,属性为Write或Write Without Response。
为什么不直接用一个Characteristic同时支持读和写?因为Notify的推送语义和Write的写入语义混在一个特征里,会给调试和使用带来不必要的理解成本,而且手机端很多调试工具对同一特征的不同操作支持也不一致,拆开更清晰。
自定义UUID示例:
| 角色 | 名称 | UUID | 属性 |
|---|---|---|---|
| 服务 | UART Service | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E | 主服务 |
| 特征 | TX Characteristic | 6E400003-B5A3-F393-E0A9-E50E24DCCA9E | Notify |
| 特征 | RX Characteristic | 6E400002-B5A3-F393-E0A9-E50E24DCCA9E | Write |
也可以用更短的16bit自定义UUID,但在BLE 5.0之后的设备上,建议使用128bit UUID减少碰撞风险。
3.2 广播初始化与连接参数的设置
设备要能被手机发现,需要先开启广播。广播参数中重点是广播间隔(Advertising Interval)、广播名称和设备外观(Appearance)。对于透传场景,我建议:
- 广播间隔设为80ms~200ms之间。间隔太短(如20ms)会被手机端认为占空比过高,频繁扫描时耗电;间隔太长(如1000ms)则连接不稳定,用户看到信号弱。
- 广播名称长度尽量控制在20字节以内,否则有些手机端工具显示不全。
- 如果不需要所有手机都能连接,可以在广播数据中加入厂商自定义字段,手机端做过滤后再去连接。
连接参数(Connection Interval)决定了透传的实时性和功耗:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 最小连接间隔 | 20ms | 低速数据足够了 |
| 最大连接间隔 | 40ms | 限制功耗上限 |
| Slave Latency | 4 | 降低空包唤醒频率 |
| Supervision Timeout | 400ms | 至少大于连接间隔x(1+Latency)x2 |
实际调试中,手机端的蓝牙协议栈会发起连接参数更新请求,如果设备不希望被手机改得很激进(比如改成7.5ms),可以拒绝,或者由设备在连接建立后主动发起参数更新。
3.3 MCU接收手机数据的关键回调
BLE数据的接收是通过协议栈的回调机制实现的,SDK中定义了事件回调函数ble_gatt_server_callbacks,其中包含写请求和写命令的处理。当手机执行Write操作时,协议栈会把数据通过以下回调上传到应用层:
int ble_gatts_write_event_handler(uint8_t conn_handle, uint8_t att_handle, uint16_t data_len, const uint8_t *data) { // 判断数据对应的特征值句柄,这里假设是RX特征 if (att_handle == rx_att_handle) { // 把收到的数据放入串口发送缓冲区,或直接做命令解析 uart_send_buffer(data, data_len); } return 0; }回调收到了数据,不代表应用层可以立刻使用,必须尽快把数据搬走。如果回调中做耗时操作(比如发送到Flash、长时间循环等待),会阻塞协议栈的运行,导致后续事件堆积、连接断开。
一个比较稳妥的做法是:回调里把数据拷到全局缓冲区,置一个标志位,然后在主循环里处理这个标志。
3.4 MCU主动推送数据到手机
数据上报的核心是调用协议栈的Notify发送接口:
void ble_send_notification(uint8_t* data, uint16_t len) { uint8_t conn_handle = app_conn_handle; if (conn_handle == 0xFF) { // 没有连接,不发送 return; } uint8_t buffer[244]; uint16_t send_len = len; // 如果数据超过MTU-3,需要分帧发送 if (send_len > (ATT_MTU - 3)) { send_len = ATT_MTU - 3; } memcpy(buffer, data, send_len); ble_gatts_notify(conn_handle, tx_att_handle, send_len, buffer); }注意这里ATT_MTU - 3是有效载荷上限,因为ATT头占据1字节Opcode + 2字节Handle。在BLE 4.2之前的设备默认MTU是23,对应有效载荷20字节;如果通过MTU协商提高到247,则最多可以一次发244字节。
提示:调用Notify发送前,务必先确认手机端是否为此特征使能了CCCD(Client Characteristic Configuration Descriptor)。如果没有使能Notify,数据发送不会成功或者被协议栈直接丢弃。
3.5 MTU、连接间隔与数据吞吐率的权衡
在项目初期,建议把MTU显式协商为247,因为默认23字节的MTU对透传来说实在太憋屈了。MTU协商由Client发起,也可以由Server端主动发起,但大部分情况下是手机端来决定。
数据吞吐率可以用一个简单公式估算:
吞吐率 ≈ 有效载荷 / (连接间隔 * (1 + Slave Latency))以连接间隔30ms、MTU 247、每个连接事件发送一个包为例:
- 每秒约有33个连接事件,每个包244字节,理论峰值约8KB/s。
- 如果每次连接事件发2~3包,吞吐量可以提升到16~20KB/s。
不过顺序发送多个包时需要控制发送速率,连续发送过快会导致协议栈缓冲区溢出,建议两个包之间间隔2ms左右,或者在发送完成后等待回调再发下一包。
3.6 透传数据完整性的保证
BLE的L2CAP层提供CRC校验,射频数据基本不会错包,但应用层的数据完整性取决于发送方和接收方的缓冲处理。在实现时,可以采用以下措施:
- 发送帧尾加校验字节(如CRC8或CRC16)。
- 接收端组帧,按固定包头进行分包。
- 手机下发大文件时,MCU端做ACK确认重传机制。
最简单的透传demo通常不做这些,但正式产品一定要加。不要把BLE当成有线的串口来用,BLE连接可能因为距离、干扰而断连,数据链路的可靠性必须由应用层兜底。
4. BLE调试助手使用详解与技巧
4.1 常用BLE调试工具对比
手机端的BLE调试工具非常多,我花过不少时间逐个试,这里给出一个基于实测的体验对比:
| 工具名称 | 平台 | 核心优势 | 不足之处 |
|---|---|---|---|
| nRF Connect | Android/iOS | 支持服务发现、MTU设置、数据收发、日志导出 | 界面信息量大,新手略难上手 |
| LightBlue | iOS | 界面友好,操作简单,适合快速验证 | Android版本支持力度稍弱 |
| 国民技术官方调试助手 | Android | 针对自家芯片有优化,支持厂家扩展功能 | 更新频率较低 |
| Serial Bluetooth Terminal | Android | 适合模拟串口透传场景 | 对GATT细节控制较少 |
日常调试我主力用的是nRF Connect,因为它能同时看到广播数据、连接参数、服务列表和收发记录,信息非常完整。但如果是面向非技术客户演示,LightBlue更好用,界面干净。
4.2 扫描、过滤与连接设置
打开nRF Connect后,首先进入扫描界面。如果附近设备很多,可以启用扫描过滤:
- 按RSSI过滤:设置信号强度阈值,比如-60dBm以上才显示。
- 按设备名称过滤:输入N32WB03X广播里配的名称。
- 按服务UUID过滤:输入前一步写的UART Service UUID,能精确定位。
扫描到设备后,点击CONNECT。连接成功后,工具会显示该设备所有服务。找到UUID为6E400001的UART Service,进入后可以看到RX和TX两个Characteristic。
一个常见的困惑是:刚开始压根看不到这个Service。这多半是因为设备没广播出来,或者设备广播包里没有包含服务UUID,但连接后服务发现一定能拿到。如果连接后连服务都看不到,多半是协议栈配置错误或GATT表初始化问题。
4.3 快速定位RX/TX特征与使能通知
连接之后,最紧要的一步是使能TFX特征的Notify。具体操作:
- 展开TX Characteristic(UUID 6E400003)。
- 点击“向下箭头”图标,选择“Enable Notifications”。
- 此时工具会自动写入
01 00到CCCD。
使能之后,只要N32WB03X端调用发送接口,手机端立刻就能看到数据。
注意:如果在同一连接中重复开关Notify,有些工具和协议栈会报错或者出现CCCD状态不同步,建议保持常开,除非你正在测试低功耗模式。
接着往RX特征(UUID 6E400002)发送数据:
- 点击RX Characteristic的“向上箭头”图标。
- 在输入框中填入要发到MCU的数据,可以选择UTF-8文本或十六进制。
- 点击发送。
发送时如果数据量很大,工具会分成多包发送,MCU端可能连续收到多个回调,需要自己拼装。
4.4 数据收发验证与多包发送技巧
透传调试中最有价值的动作是验证“长度”和“内容”都正确:
- 短数据测试:发ASCII字符串“hello”到MCU,MCU串口应原样输出。
- 长数据测试:发256字节的全0xFF,看MCU端收到的数据长度和内容是否一致。
- 双向同时测试:串口端发一批数据给MCU再转发到手机,同时手机端发另一批数据给MCU再转发到串口,看两端是否互不干扰。
关于BLE调试助手的使用技巧,我分享两个非常实用的:
- 在nRF Connect中修改MTU:连接成功后,点击右上角“...”菜单,找到“MTU”或“Request MTU”,手动输入247。修改MTU后,再发送长数据,工具会把数据拆分成多包自动发送,但MCU收到的数据是分帧的,仍需要应用层拼包。
- 使用日志导出功能:nRF Connect可以导出连接过程的日志。如果怀疑丢包,可以把收发的数据和时间戳一起导出,在电脑上对比分析,这比在手机上肉眼判断可靠得多。
4.5 真实场景:从手机发一块168字节数据给MCU再回传
我实际测试过一个典型场景:
- 手机端发送168字节数据。
- MTU协商到247。
- 一次性Write,MCU收到后原样通过UART输出。
- UART端回送168字节,MCU通过Notify上报,手机端收到完整数据。
结果发现,在第2步之前直接用默认MTU会失败,手机上显示发送的部分数据没有被接收。排查后发现是MTU不足导致数据被协议栈截断。通过在调试助手中手动设置MTU后,数据一次抵达MCU,文件传输也正常运行。
这个案例说明,在开发阶段尽早确认MTU值非常重要。如果最终产品要支持第三方App,务必在设备侧通过GAP参数更新方式发起MTU协商,这样手机App即使不专门设置MTU,也能获得更大的数据包传输能力。
5. 常见问题与排查技巧实录
5.1 连上设备后广播消失,如何处理
BLE连接建立后,从机默认会停止广播。但有些场景下你希望设备在连接期间也能继续广播(Android iBeacon等),就需要在协议栈连接事件回调里重新开启广播。N32WB03X的SDK中有相关API,需要确认连接参数。
实践中,如果发现断开后设备不广播,需要检查:
- 是否调用了广播启动接口。
- 是否在连接断开事件回调里重新调了广播启动。
- 是否有其他任务阻塞了主循环,导致协议栈事件处理不过来。
连接事件回调伪代码如下:
void ble_gap_event_handler(uint8_t event, uint8_t conn_handle, void *param) { switch(event) { case GAP_EVT_CONNECTED: app_conn_handle = conn_handle; break; case GAP_EVT_DISCONNECTED: app_conn_handle = 0xFF; // 重新开启广播 ble_gap_adv_start(); break; default: break; } }5.2 数据丢失或收不全的问题
数据丢失可以分成两种:
第一种是应用层丢包,比如UART缓冲区溢出。N32WB03X的UART接收如果使用中断方式,在高波特率(如115200)下,如果中断处理来不及取数据,DMA或环形缓冲区可能溢出。解决办法是加大缓冲,或者改用DMA接收。
第二种是BLE协议栈发送缓冲区满。数据上报时,如果发送接口返回BLE_ERROR_NO_MEM或类似错误码,说明协议栈缓冲区已满,此时应等待一段时间再发。
排查丢包时,建议先锁定问题在哪一层:在MCU端串口日志中加打印,确认数据从UART到了应用层;再在手机端看是否收到。如果UART端数据完整但手机端缺,问题就在BLE链路,反之则在UART采集环节。
5.3 手机搜索不到设备
这种情况非常常见,基本按以下顺序排查:
- 开发板供电是否正确,电流是否足够。BLE设备在广播瞬间电流可能达到几十毫安,劣质USB线压降过大可能导致芯片复位。
- 有没有错误地把天线当作普通走线,被外壳金属包裹、被地平面遮挡,导致信号衰减严重。
- 广播参数是否太过保守,广播间隔太长或者开启了白名单过滤,导致手机扫描不到。
- 是不是芯片没跑起来,比如晶振没振、程序卡在HardFault。看串口日志或仿真异常来确认。
你还可以把N32WB03X的串口日志用拔插USB线的方式观察启动打印,如果完全没有启动信息,多半是硬件问题或电源问题。
5.4 连接不稳定/频繁断开
连接断开是多因素叠加的结果。我遇到过的一次典型情况是:
- 手机放在口袋,设备放在桌上,距离不到5米,但频繁掉线。
- 用蓝牙抓包工具分析后,发现每次断开前都出现较多重传包。
- 最终定位到是干扰环境下使用了太长的广播间隔和太小的Supervision Timeout,连接参数不匹配导致超时。
优化思路如下:
| 参数 | 优化前 | 优化后 |
|---|---|---|
| 最小连接间隔 | 7.5ms | 15ms |
| 最大连接间隔 | 50ms | 30ms |
| Slave Latency | 8 | 4 |
| Supervision Timeout | 200ms | 400ms |
缩短连接间隔使设备更及时响应;降低Slave Latency减少空中唤醒延迟;增大超时时间避免短暂的射频干扰导致掉线。注意,连接间隔越短越耗电,实际项目要结合电池容量折中。
5.5 使用PC端工具辅助排查
如果手机端调试工具信息不够,建议使用PC端的蓝牙调试工具,常见的有:
- Wireshark + BLE dongle:抓空中的BLE包,能看到所有连接事件和数据包内容,缺点是抓包环境搭建复杂。
- BlueZ + btmon(Linux环境):可以通过hci接口抓取蓝牙HCI层的事件,定位问题效率很高。
- 逻辑分析仪:分析N32WB03X的串口输出、GPIO翻转时间点。
我个人更推荐在开发早期就抓一次长连接的完整蓝牙日志,这样能直观看到手机端和MCU端的连接参数协商过程、MTU更新过程、以及每个Notify的发送间隔。排查丢包和延迟问题时,这些数据比任何猜测都管用。
6. 实操心得总结
N32WB03X做蓝牙透传,核心并不在于代码量有多少,而在于你对GATT服务模型、事件回调和连接参数的理解是否清晰。把这个架构想明白了,后面无论是加传感器数据上报、做OTA还是低功耗优化,都是在现有框架上添砖加瓦。
我个人的习惯是:新项目第一步不是直接写业务代码,而是先用官方例程把“空转透传”跑通,然后立刻用nRF Connect做一次35字节、100字节、247字节的三档数据回环测试,确认链路质量后再开始加业务逻辑。这一步看起来费时间,但能省下后面无数排查的功夫。
另外,不要小看BLE调试助手的使用技巧。你以为连接成功、能看到服务就万事大吉了?实际上MTU协商、CCCD使能状态、连接参数协商结果都能在调试助手里看出来。调试工具绝不是“能看数据就行”,而是整个开发流程中最重要的信息窗口。
如果在后续开发中遇到问题,不妨先把手机端日志导出来,再配合MCU端串口日志一起看,定位问题会快很多。还有一点特别值得提醒:注意区分“手机端没显示数据”和“MCU端根本没发出数据”这两个完全不同的排查方向。我在实际项目中见过好几个人,因为手机端没显示就一直改MCU代码,最后发现是调试助手的Notify没使能。
先跑通最简单的双向透传,再逐步升级为带协议、带校验的可靠传输,这是我认为最稳妥的学习路径。无论你是拿它做产品原型,还是纯粹学习BLE技术,按这个思路来,基本上不会走弯路。