资讯动态

PB03蓝牙5.2二次开发:从AT指令到SDK工程改造

发布时间:2026/10/26 10:51:03 来源:尧图企业网站定制
简介面向物联网与可穿戴设备开发者的PB-03蓝牙5.2二次开发资料包围绕安信可PB-03模块与PHY6252低功耗蓝牙SoC展开适合需要熟悉BLE5.2协议、AT指令、外设接口及FOTA升级的嵌入式工程师和学生。压缩包共1163个文件、约66.23MB主体为C源码与头文件辅以Keil工程文件、分散加载脚本、编译批处理和库文件并附PDF文档、Hex固件及OTA相关工具便于对照源码理解工程结构与烧录流程。已有2248人学习下载。资料覆盖GPIO等示例工程、开发环境配置、串口本地升级与远程固件升级等内容可帮助读者快速搭建Windows开发环境掌握低功耗休眠、UART/SPI/I2C/PWM等接口调试方法并借助现成工程缩短二次开发验证周期。1. 从“会用模组”到“会改模组”中间隔着一份文档资料拿到一块 PB03 蓝牙 5.2 模组插上 USB 转串口发几条 AT 指令手机就能搜到、能收发数据这一步大多数人十分钟就能走完。真正的坎在下一步产品要自己的服务 UUID、要自己的广播格式、要在空闲时把电流压到微安级这时候原厂 AT 固件就不够用了必须动 SDK。这份 PB03 蓝牙 5.2 二次开发资料的定位就在这一层它不是入门教程也不是协议栈手册而是一套能编译、能烧录、能改的工程底子加上一批把“为什么这么写”讲清楚的文档资料。2. 资料包拆箱五个目录各管一段先看哪份文档最省时间很多二次开发资料包下载下来是一个压缩包解压之后几十个文件夹摊在眼前新手第一反应是从doc里翻 PDF翻了两天还是不知道怎么点编译。我一般反过来先把工程跑起来再回头查文档。因为这类 BLE SoC 的 SDK初始化顺序、时钟配置、协议栈启动这些都已经被例程写死了你需要查文档的地方其实很集中——引脚复用、电气参数、AT 指令格式、烧录地址。先跑通再查查的时候才知道自己要找什么。2.1 目录结构工程、组件、工具、文档四条线一份完整的 PB03 二次开发包目录大体是下面这个形态。不同批次的资料包命名会有出入但分工基本一致对着这张表就能对上号目录里面装什么什么时候会打开它doc/数据手册、AT 指令手册、模组原理图与封装、射频与认证说明第一天看引脚和电气参数后面查 AT 格式examples/透传、Beacon、主机扫描、低功耗唤醒等例程抄初始化流程和事件回调的地方components/协议栈封装层、UART/GPIO/ADC/PWM 驱动、公共组件要改驱动或加外设时projects/Keil 工程文件、链接脚本scatter file、编译配置编译入口天天开tools/烧录上位机、OTA 打包脚本、串口调试工具下载固件、做升级包examples和projects是两个不同的东西很多人第一次会搞混examples里放的是源码和业务逻辑projects里放的是“用什么编译器、按什么地址布局、开哪些宏”的工程壳子。你新建自己的产品工程时正确姿势是把examples里的一个例程整个拷出来改名再去projects里复制一份工程文件指过去而不是在原目录里直接改——原目录留着当参照后面出问题可以两边 diff 一下这个习惯能省掉大量“改着改着不知道为什么就不行了”的时间。components这一层通常是预编译库加头文件的组合。协议栈核心大概率是.lib或.a形式源码不给你只暴露ble_gap.h、ble_gatt.h这类接口。这意味着两件事第一协议栈内部的行为你只能通过回调和日志观察遇到玄学问题要习惯从“我传进去的参数对不对”入手第二升级 SDK 时这一层会整体替换所以业务代码千万别去改components里的文件改了下次升级就全丢。2.2 文档该看哪几份数据手册看引脚AT 手册看协议原理图看板子doc目录里文档不少但真正每天要翻的就三份用途完全不同。数据手册解决的是“这个引脚能干什么”。PB03 这类模组引脚不多但复用关系密同一个脚可能同时是 UART_TX、PWM 输出和 ADC 输入。做产品时最常翻的就是引脚复用表和电气参数表驱动一个 LED 要不要外接限流电阻、某个脚上电默认电平是高还是低、IO 最大灌电流多少。这些在数据手册里是表格查起来很快在代码里猜能猜一下午。AT 指令手册解决的是“不改固件能走多远”。很多需求其实 AT 就够改个广播名、改个波特率、调个广播间隔、设个 MAC。先花半小时把 AT 手册的指令列表过一遍能省掉一次不必要的固件改造。手册里通常还会写清楚默认的服务 UUID 和特征值权限这一点很关键——你在手机端联调时用的 UUID八成就是从这里抄的。原理图解决的是“板子和我以为的是不是一回事”。尤其是串口TX/RX 有没有交叉、有没有硬件流控、有没有电平转换、复位脚和烧录脚引出来没有。血泪经验是同一批模组不同版本的底板串口引出顺序改过的概率不低不确认原理图直接插线第一次上电没反应很正常。2.3 一份最小可用的文件清单先只打开四个文件资料包再大第一轮只需要看四个文件顺序如下main.c——看初始化顺序。时钟、IO、串口、协议栈谁先谁后漏一步就是死机。app_ble_init.c或同类命名——看广播数据和服务表这是你要改的第一现场。UART 驱动文件——看收发是中断、DMA 还是轮询决定了你后面怎么写透传。链接脚本.sct——看代码跑在哪个地址、RAM 怎么切分、有没有独立的 OTA 分区。把这四个文件看完你对这个工程的理解就已经超过“照着例程瞎改”的水平了。剩下的驱动细节、外设例程等真要用的时候再翻不必一次吃完。3. 环境搭建与第一次烧录从编译到“手机能搜到”的最小闭环这一章的目标很明确让你在半小时内看到手机搜到设备名字。中间任何一步卡住都很正常但顺序别乱——先编译通过再烧录成功最后才是手机搜到。三步混在一起做出问题的时候你不知道是哪一层的问题。3.1 工具链Keil 工程怎么打开宏和优化等级怎么设这类 SDK 一般给的是 Keil MDK 工程编译器是 ARMCC。打开工程前先确认两件事器件支持包装了没有、工程里选的器件型号和模组实际芯片对不对得上。器件选错了编译能过烧进去不启动这个坑非常常见。工程里通常有一堆以BOARD_或MODULE_开头的宏定义作用是区分不同封装、不同 Flash 大小、不同底板。第一次编译前把这些宏跟你的实物对一遍尤其是带_EVB、_MINI后缀的那几个。优化等级我一般先设-O1-O0断点好打但代码大-O2以上单步调试会乱跳变量被优化掉-O1是调试和体积的中间点等产品定型再往上提。如果要做批量出固件或者接自动化直接用命令行编译就行不必每次都开 IDE:: build.bat —— 命令行编译方便接 CI 和批量出固件 :: -b 表示 batch 模式编译-j0 隐藏 IDE 窗口-o 指定日志输出 set UV4C:\Keil_v5\UV4\UV4.exe %UV4% -b projects\ble_uart\ble_uart.uvprojx -j0 -o build_log.txt if errorlevel 1 ( type build_log.txt exit /b 1 ) echo build ok这段脚本的重点在if errorlevel 1这一段。Keil 的UV4.exe编译失败时退出码不是 0但如果不接这段判断脚本会一路往下走最后打出一句“build ok”你拿着一个上次编译的旧hex去烧录然后怀疑人生。日志文件build_log.txt也要留着命令行模式下编译器的报错不会弹窗只写进日志。3.2 链接脚本与烧录地址这俩对不上烧完必然不启动链接脚本.sct或工程里的 IROM/IRAM 配置决定了代码被放在 Flash 的哪个地址。烧录工具里也有一个“起始地址”要填。这两个数字必须一致差一点都会出问题——轻则烧完不启动重则连烧录工具都连不上要靠拉低某个引脚进特殊模式救回来。常见的布局是一段引导区bootloader在低地址应用程序在引导区之后OTA 升级时新固件写到另一块区域校验通过再改跳转。你如果只烧应用区、板上却没有引导区那上电后 CPU 就在一片空白或者随机数据里跑表现就是“烧录成功毫无反应”。判断方法很简单看烧录工具擦除后读回的 Flash 前几十个字节是不是全0xFF如果是说明引导区本来就没烧。提示第一次烧录前先做一次全片擦除再烧完整固件引导 应用。分区域烧录是后面做 OTA 才需要考虑的事一上来就分只会把问题叠加。烧录工具一般还提供“烧录后校验”和“烧录后复位运行”两个勾选项。校验建议常开它能挡住 USB 线材接触不良导致的半截数据复位运行按需调试阶段我更愿意手动按复位这样串口助手能完整抓从上电开始打印的启动日志。3.3 编译、下载、验证用一段 Python 脚本确认服务真的在烧完固件第一件事不是打开串口助手而是用手机或电脑搜一下广播名。搜得到说明射频、协议栈、广播数据都没问题搜不到问题就在这三层里面别去查 GATT。搜到之后第二步才是连上去看服务用一段几十行的 Python 脚本比在手机上点来点去可靠得多还能反复跑import asyncio from bleak import BleakClient, BleakScanner # 默认透传服务一般是 FFF0 这一组具体以你烧的固件和 AT 手册为准 SVC 0000fff0-0000-1000-8000-00805f9b34fb TX 0000fff1-0000-1000-8000-00805f9b34fb # 模组 - 主机notify RX 0000fff2-0000-1000-8000-00805f9b34fb # 主机 - 模组write def on_notify(_, data: bytearray): print(RX -, data.hex(), repr(data)) async def main(): dev await BleakScanner.find_device_by_name(PB03-UART, timeout10) if dev is None: print(没搜到广播先回去查广播数据和射频) return async with BleakClient(dev) as cli: # 协商 MTU不协商就按 23 算单包只有 20 字节可用 print(mtu , cli.mtu_size) await cli.start_notify(TX, on_notify) await cli.write_gatt_char(RX, bhello, responseFalse) await asyncio.sleep(2) asyncio.run(main())几个参数值得单独说。find_device_by_name里的名字要和广播包里写的设备名完全一致差一个字符都搜不到很多人卡在这里以为是固件没烧进去。cli.mtu_size打印出来如果是 23说明 MTU 没协商成功后面发超过 20 字节的数据会被协议栈自动截断或直接返回错误。write_gatt_char的responseFalse走的是 Write Without Response速度快但没确认调试阶段建议先改成True确认链路通了再换回无响应模式。4. 核心链路拆解一个字节从手机到串口中间过了哪几层透传固件看起来简单——手机发什么串口出什么。但真出问题时你需要知道数据在哪一层被丢了。这一章按“广播 → 连接 → 服务 → 数据通路”的顺序把链路拆开每一段都对应一个可以单独验证的点。4.1 广播数据31 字节里塞什么顺序很讲究广播包长度上限是 31 字节里面是一串 AD Structure每个结构是“长度 类型 数据”。长度字节包含类型字节本身不包含长度字节自己这个定义第一次写很容易算错。下面这段是常见的组合Flags 必须放第一个/* 广播数据Flags 完整设备名总长必须 31 字节 */ static uint8_t adv_data[] { 0x02, 0x01, 0x06, /* Flags可发现 不支持 BR/EDR */ 0x0A, 0x09, P,B,0,3,-,U,A,R,T /* 完整名称长度 0x0A 1 类型 9 字符 */ };逐字节看0x02是这个结构的长度0x01是 Flags 类型0x06是取值bit1 通用可发现 bit2 不支持经典蓝牙第二个结构0x0A是长度0x09是“完整本地名称”类型后面 9 个字节是名字。名字长度超过 20 字节就得换成0x08缩短的本地名称并把完整名字塞进扫描响应包否则广播包超长协议栈会直接返回错误或者静默截断表现就是“手机搜到的是一个乱码名字”。广播参数是另一个容易踩的点/* 广播参数interval 单位 0.625ms160 * 0.625 100ms */ static const ble_gap_adv_param_t adv_param { .interval_min 160, /* 最快 100ms 一次 */ .interval_max 160, /* 最快和最慢设成一样广播节奏稳定 */ .adv_type ADV_TYPE_CONNECTABLE_UNDIRECTED, .channel_map 0x07, /* 37/38/39 三个信道全用 */ .filter_policy 0, };interval_min和interval_max不一样时协议栈会在区间内随机取值这是规范要求的能降低多设备同时广播时的碰撞概率。但调试阶段我更愿意固定成一个值抓包时好对数。广播间隔直接决定平均功耗100ms 和 1000ms 的差距在电池产品上是几百微安和几十微安的差距别等到功耗测试才发现。4.2 GATT 属性表透传服务的三个特征值怎么排属性表是服务的骨架。一个透传服务通常是一个主服务 两个特征值 一个 CCCD客户端特征配置描述符用来开关 notify。属性表写错顺序手机端看到的句柄就全乱了/* 透传服务属性表主服务 RX(写) TX(通知) CCCD */ static const gatt_attr_t uart_svc_attrs[] { GATT_PRIMARY_SERVICE_UUID128(UUID_UART_SVC), /* 主服务声明 */ GATT_CHAR_DECL(UUID_UART_RX, PROP_WRITE_NO_RSP), /* 特征声明手机 - 模组 */ GATT_CHAR_VALUE(UUID_UART_RX, 0, 244), /* 特征值本体最大 244 字节 */ GATT_CHAR_DECL(UUID_UART_TX, PROP_NOTIFY), /* 特征声明模组 - 手机 */ GATT_CHAR_VALUE(UUID_UART_TX, 0, 244), GATT_CCCD_DECL(UUID_UART_TX), /* 让手机能开关通知 */ };几个点解释一下。PROP_WRITE_NO_RSP是 Write Without Response吞吐高但没确认如果产品对可靠性要求高就改成PROP_WRITE代价是每包多一个 ATT 确认往返。特征值长度填 244 是因为 ATT_MTU 最大 247减去 3 字节的 ATT 头正好 244如果你的协议栈只支持到 517 或者只支持 23这个数要相应改。CCCD 必须紧跟在带 notify 属性的特征值后面别插到别的特征值后面去否则手机端start_notify会返回一个“属性不存在”的错误而这个错误在很多 BLE 调试工具里显示得相当含糊。4.3 数据通路串口中断拼包主循环统一发通知这是整个透传固件最容易出事的地方。串口数据是流式的BLE 是包式的中间的“拼包”和“发通知”这两个动作放在哪执行直接决定稳定性。/* 串口接收中断只搬数据、记时间戳不做任何协议栈调用 */ void uart_rx_isr_handler(void) { uint16_t n uart_read_fifo(g_rx_buf g_rx_len, sizeof(g_rx_buf) - g_rx_len); g_rx_len n; g_rx_tick get_sys_tick(); /* 记录最后一字节时刻用于 3ms 断帧 */ } /* 主循环里调用攒够一帧或超时才发一包 notify */ void app_poll(void) { if (g_rx_len 0) return; if (g_rx_len 244 || (get_sys_tick() - g_rx_tick) 3) { ble_gatt_notify(g_conn_handle, UUID_UART_TX, g_rx_buf, g_rx_len); g_rx_len 0; } }为什么不在中断里直接调ble_gatt_notify因为协议栈的通知接口内部要拿锁、要操作链路层缓冲在中断上下文里调用轻则偶发丢包重则直接死机——而且是那种一天出一次、复现困难的死机。我从那以后就守一条规矩协议栈接口只在主循环或者协议栈自己的回调里调中断里只做搬数据。断帧时间 3ms 是个经验值。串口一次连续发送的字节之间间隔通常在几十微秒量级主机的 USB 转串口驱动偶尔会有毫秒级的抖动取 2~5ms 之间比较稳。取值太小会把一帧拆成两包发出去取值太大则交互变卡。如果你的协议是定长的那更简单——攒够 N 字节就发不用超时。4.4 连接参数与 MTU 协商吞吐、延迟、功耗的三角透传做完接着一定会问“为什么手机发得快就丢包”。八成的答案是 MTU 和连接参数没调。MTU 是链路建立后协商的。默认 23可用载荷只有 20 字节你要发 100 字节的数据协议栈得切成 5 包中间任何一包丢了上层都不知情BLE 有链路层重传但 ATT 层不保证跨包顺序交付给应用。主动请求 MTU 之后能到 247可用载荷 244切包次数直接降下来。连接参数走的是 Connection Parameter Update Request三个参数都有单位陷阱最小/最大连接间隔单位 1.25ms从机延迟是“可以跳过几个连接事件”监督超时的单位是 10ms。下面这组配置是“低延迟优先”的典型值/* 连接参数更新间隔 15~30ms从机延迟 0监督超时 4s */ static const ble_gap_conn_param_t conn_param { .interval_min 12, /* 12 * 1.25ms 15ms */ .interval_max 24, /* 24 * 1.25ms 30ms */ .latency 0, /* 不允许跳过连接事件响应最快 */ .timeout 400, /* 400 * 10ms 4s超时即断链 */ };interval_min/interval_max单位是 1.25ms所以填 12 才是 15ms填 15 会变成 18.75ms很多人在这一步以为公式错了。latency填非零能让从机在没数据时一直睡功耗降得很明显代价是主机下发数据后从机要等latency个连接事件才能收到。监督超时必须满足规范里的不等式timeout要大于(1 latency) * interval_max * 2不满足的话主机直接拒绝这次更新请求你在日志里看到的只是一个失败返回码。5. 避坑与现场排查连不上、丢包、功耗高四条血泪记录注意这一章里的每一条都是“烧完录就懵了”的现场排查顺序我按最省时间的排法写别跳步。5.1 现象手机搜得到设备点连接转圈三秒后失败原因通常是广播包里少了 Flags或者广播类型不可连接。有些厂家的协议栈对 Flags 的检查比较宽松缺了也能被扫描到但一发起连接就被链路层拒绝。另一种情况是广播包长度算错最后一个 AD Structure 的长度字节超过了剩余字节数协议栈把整包按非法数据丢掉手机端看到的是“搜到了但连不上”。解决先用通用 BLE 调试工具看广播原始字节确认第一个结构是02 01 06确认广播类型是ADV_TYPE_CONNECTABLE_UNDIRECTED。然后在协议栈回调里把连接请求的返回码打出来如果返回的是0x3E连接建立失败或者0x06不支持的链路层参数问题就基本锁定在广播或链路参数上不用再往下查 GATT。5.2 现象发 20 字节以内正常超过就丢抓包软件显示包被截断原因是 MTU 没协商主机按 23 的默认值算一次最多写 20 字节多出来的部分在 ATT 层就报错或者被应用层丢掉。另一种可能是属性表里特征值的最大长度填成了 20即使 MTU 协商到 247写超过 20 字节也会返回Invalid Attribute Value Length。解决在连接成功回调里主动发起 MTU 交换把结果打日志同时核对属性表里特征值的max_len和实际 MTU 是否匹配。还有一点别忘Notify 也有单包上限MTU-3透传时如果一帧超过这个值得自己分片协议栈不会帮你切。5.3 现象烧录工具提示成功重新上电没任何反应串口一片安静原因大概率是烧录地址和链接脚本不一致或者引导区没烧。这个模组一类的小型 BLE SoC应用区和引导区是分开的烧录工具默认地址常常指向应用区你如果只烧了应用区上电后 CPU 去找引导代码找不到就停在启动文件的死循环里表现就是彻底安静。另一个常见原因是烧完忘了勾“复位运行”或者虽然勾了但复位脚被底板的电容拖慢实际没复位成功芯片还在旧的固件里跑。解决全片擦除 烧写完整固件 手动按复位。然后读回 Flash 前 256 字节看看是不是全0xFF如果是说明这一段根本没写进去回头查地址。这一步二分很快不用猜。5.4 现象功耗比手册标称高了整整一个数量级原因可能有好几个我一般按这个顺序排查第一广播间隔太短先把它从 20ms 改到 500ms 看电流掉不掉第二调试串口常开串口外设在空闲时也在耗电很多人忘了关第三GPIO 悬空输入脚没有上下拉处在“半高电平”状态漏电流很可观第四根本没进睡眠app_poll里有定时器一直醒着。解决一个一个关。先把所有外设初始化注释掉只留协议栈广播测一个基线电流然后逐个加回来每加一个测一次。这个方法笨但最快能定位到具体是哪一行代码在偷电。玄学问题往往就是这么一行gpio_init造成的。6. 二次开发改造把透传固件换成自定义 GATT 服务的三步走拿到资料包跑通之后绝大多数人的下一步是把它改成自己的产品固件。我的做法固定是三步每一步都能单独验证不会一次改崩。第一步只换 UUID不动逻辑。把属性表里的服务 UUID 和两个特征值 UUID 换成产品自己的 128 位 UUID广播名也改掉重新烧录用第 3 章那段 Python 脚本按新 UUID 跑一遍。这一步通了说明你的 UUID 表、属性表顺序、CCCD 位置都没问题。第二步改数据格式不动通路。原来的透传是“手机发什么就原样转给串口”产品固件通常要加个帧头、长度、校验。做法是在on_rx_write回调里加一层解析先攒到缓冲区校验通过再交给业务发送方向在app_poll里组装完整帧再 notify。这一步改完串口助手那边看到的应该是格式化后的数据手机端看到的也应该是格式化后的数据两边对不上就是解析写错了。第三步加版本号和 OTA 入口。产品固件一定要在广播包或者某个只读特征值里带一个固件版本号不然现场升级时你分不清手上这台是哪版。OTA 分区在链接脚本里就要留好后面改起来代价很大。改造点改动位置验证方式服务/特征 UUIDGATT 属性表Python 脚本按新 UUID 连接并收发数据帧格式write 回调 主循环发包串口与手机双向对照逐字节比版本号只读特征值或扫描响应包手机端读一次和编译时宏定义比对OTA 分区链接脚本 烧录工具地址烧一次旧版再推一次新版看是否回滚最后说个教训。我早年改这类固件习惯改完直接烧烧完直接测出了问题就从头翻代码。后来强制自己走一遍“编译日志、烧录校验、Python 脚本跑通、串口日志留档”这四步看起来慢实际快了很多——因为每次出问题都能确定是这四步里的哪一步引入的而不是一片空白地怀疑。资料包里的例程和文档资料可以反复看但验证流程必须自己有一套。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑