ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32WB55+STM32CubeMX BLE开发实战:从P2P Server到手机通信

STM32WB55+STM32CubeMX BLE开发实战:从P2P Server到手机通信 简介面向STM32WB55系列开发者的低功耗蓝牙入门资料包适合正在使用STM32CubeMX与STM32集成开发环境进行蓝牙应用开发的嵌入式工程师和高年级学生。工程基于NUCLEO-WB55开发板演示如何通过图形化配置工具快速生成蓝牙工程并实现与手机APP的无线连接和数据交互覆盖从底层外设初始化到上层协议栈调用的完整开发链路。压缩包共三百九十二个文件整体约二十六点七二兆字节包括五十八个C源码文件、一百四十个头文件、若干编译生成的目标文件、可执行映像和烧录文件以及界面的工程配置文件与文本说明可以直接编译、烧录并根据项目需求修改。代码中定时器、串口、时钟、实时时钟等外设驱动模块展示了HAL硬件抽象层的基础用法蓝牙主机控制器接口文件则对应协议栈的底层通信有助于理解蓝牙广播、扫描和连接的过程。已有两千一百五十九人学习下载配合作者提供的图文教程和视频演示适合希望快速掌握低功耗蓝牙开发、降低入门门槛的开发者。1. 为什么 STM32WB55 STM32CubeMX 是当前最省事的 BLE 入门组合做 BLE 开发最痛苦的不是写 GATT 服务而是协议栈怎么集成、中断优先级怎么调、广播包怎么组。STM32WB55 把问题拆成了两半Cortex-M0 内核单独跑蓝牙协议栈Cortex-M4 内核只跑你的应用代码两核之间通过 IPCC 硬件信号通信。配合 STM32CubeMX勾选 Bluetooth LE 和 P2P Server 后协议栈初始化、HCI 事件分发、GATT 回调框架全部生成好剩下要手写的业务代码往往不到 100 行。本文用 NUCLEO-WB55 和一个 STM32CubeMX 生成的工程 STM32WB_BLE_2 走完整条链路从 CubeMX 配置到手机 APP 收发数据再把广播包、连接参数、串口调试和低功耗下载这几个坑点挨个拆开讲。2. CubeMX 生成 BLE 工程从引脚到协议栈的关键配置2.1 选型、时钟与 RTC 唤醒源必须先定不是所有 WB 系列型号都适合直接跑 BLE 应用。STM32WB55 是一颗双核无线 MCUM4 内核负责应用M0 内核负责射频协议栈RAM 资源比 WB50/WB30 宽裕跑完整的 GATT Server 加 OTA 都不吃力。NUCLEO 板载了 ST-LINK/V3直接用 USB 线供电和下载不需要额外调试器这对看协议栈日志尤其方便。时钟配置是整个 CubeMX 工程最容易出错的第一步。BLE 协议的通信时序依赖精确定时RF 子系统使用 HSE 作为参考时钟能得到更稳定的连接间隔。在 CubeMX 的 RCC 面板里HSE 选择 CRYSTAL/Ceramic Resonator然后在 Clock Configuration 里确认系统时钟频率符合芯片最大值。M0 网络核的时钟频率和 M4 应用核可以不同但 PPB 总线上的定时器配置要保证 BLE 协议栈的唤醒源可用。RTC 是第二个不能省的外设。BLE 协议栈在低功耗模式下靠 RTC 唤醒来处理连接事件STM32CubeMX 生成 BLE 工程时会默认加入 RTC 初始化和校准逻辑。如果你在 CubeMX 里漏勾 RTC生成的代码运行到 APP_Init 时大概率会 assert 失败。实践中的做法是在 Timers 分类里勾选 RTC激活 WakeUp 功能闹钟中断可选但 WakeUp 定时器必须开着。RTC 时钟源一般选 LSENUCLEO 板上有 32.768kHz 晶振不要为了省外部晶振去选 LSI否则 BLE 连接后漂移会非常明显。2.2 打开 Bluetooth LE 协议栈并确认 P2P Server 生成在 CubeMX 的 Categories 面板中选择 Middleware and Software Packs - STM32_WPAN - Bluetooth LE勾选后主界面会出现蓝牙配置面板。这里有几个让我最初摸不着头脑的选项Device Name 是广播时手机看到的设备名默认填 STM32WB55稍后可以改Address Type 默认 Public对于评估板直接用 Public 就行Pairing Mode 默认开启如果只是自己验证数据通路可以临时关掉省去配对弹窗的干扰。Services 选项卡里勾选 P2P Server。这是意法半导体提供的一组标准示例服务包含一个 Write 特征和一个 Notify 特征正好用来验证手机下发数据到板子和板子主动推送数据到手机两条链路。配置完成后我习惯把以下参数记下来方便生成代码后对照配置项推荐值作用Device NameSTM32WB55手机扫描列表中显示的名字Address TypePublic无需配对即可连接Bonding ModeDisabled验证阶段跳过密钥生成P2P ServerEnabled生成写特征和通知特征RTC WakeUpEnabled低功耗唤醒与连接事件调度USART1Async 115200输出运行日志和调试信息还需要确认 LPUART1 是否被占用。STM32WB 的 HCI 通信接口默认使用 LPTIM/LPUARTCubeMX 在启用 BLE 后会自动把这些底层资源分配好你不要手动把它们改成普通 GPIO 或停用否则生成代码编译时看不到错误运行时协议栈完全无法响应。2.2.1 服务类型选择P2P Server 还是自定义服务第一次做 BLE 项目我强烈建议先用 P2P Server把广播、连接、收发、断连重连跑通再考虑自定义服务。P2P Server 的代码结构已经把 GATT 初始化、特征添加、事件回调都分好了你只需要在回调函数里加业务逻辑。自定义服务需要自己调 aci_gatt_add_serv 和 aci_gatt_add_char写属性权限和 UUID 的排列组合Debug 成本明显更高。后面章节会专门讲怎么把 P2P Server 改成自定义 UUID先基于默认工程把链路走通更重要。2.3 生成后的 STM32WB_BLE_2 源码结构哪些文件在替你干活点击 GENERATE CODE 后CubeMX 会生成一个完整的 STM32CubeIDE 工程。工程文件清单里出现了 stm32wbxx_hal_uart.c、stm32wbxx_hal_tim.c、stm32wbxx_hal_rcc.c 这些标准 HAL 库文件它们是应用层跑串口、定时器和时钟复位用的。看到 stm32wbxx_hal_tim_ex.c 不用紧张这是 HCI 层做硬件流控和超时统计的辅助模块不是你的业务定时器。真正值得关注的是 ble_hci_le.c它是 HCI 层函数库M4 应用核通过它向 M0 协议栈发送命令。HAL_UART 和 HAL_TIM 在这里扮演两个角色HCI 通信本身可能走 UART 或 LPUART而协议栈事件节拍需要定时器辅助。也就是说CubeMX 生成工程时外设分配是环环相扣的手动删除任何一个看似无用的 HAL 模块都可能让 BLE 静默失败。2.3.1 代码骨架与初始化顺序打开生成工程里的 main.c主要函数调用顺序比普通单片机工程多了一个关键步骤MX_APPE_Config()。简化后主流程如下int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_RTC_Init(); MX_USART1_UART_Init(); /* 初始化 BLE 应用层 */ MX_APPE_Config(); /* 主循环不断轮询 BLE 事件和低功耗处理 */ while (1) { APP_Process(); } }MX_APPE_Config 是 CubeMX 从 app_entry.c 里引入的函数它内部会完成 M0 协议栈固件启动、GATT 服务注册、广播参数初始化。APP_Process 则是一个非阻塞状态机负责把 HCI 事件分发给上层回调。注意不要在初始化完成前调用 P2P 服务发送接口否则数据会被丢弃。标准的调试顺序是先确认 RTC 初始化成功、再确认 HAL_UART 初始化成功、最后看到 APP_Process 正常循环再开始看 BLE 广播。3. 让手机 APP 看到设备广播包、连接参数与串口调试3.1 串口重定向printf 从 UART1 输出生成代码默认不会自动输出日志但 STM32WB55 的调试最依赖串口。把 printf 重定向到 USART1 是第一步。我一般在 main.c 前面加上重定向函数工程用 STM32CubeIDE 时GCC 编译器走 __io_putchar#include stdio.h #ifdef __GNUC__ int __io_putchar(int ch) #else int fputc(int ch, FILE *f) #endif { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }使用 Keil 或 AC6 编译器时__io_putchar 的签名可能不匹配改用 fputc 分支即可。HAL_UART_Transmit 的最后一个参数 0xFFFF 是超时时间单位是毫秒如果波特率较低而日志打印很频繁建议改成 100 或更小避免阻塞 BLE 主循环。CubeMX 中 USART1 波特率选 115200数据位 8停止位 1无校验与串口终端保持一致就行。重定向完成后在 main 函数前面加一行 printf(BLE App Start\r\n);如果串口工具收到日志说明硬件通路和时钟配置都没问题。3.2 广播数据怎么改AD Type 与扫描响应手机扫描设备时看到的名字和服务 UUID 来自广播包。CubeMX 生成的 P2P Server 工程在 app_ble.c 的 APP_Init 里会调用 aci_gap_set_discoverable 启动广播。广播包由多个 AD Structure 拼接而成每个 Structure 的组成是长度 AD Type 数据。常用 AD Type 如下AD Type值用途FLAGS0x01是否可发现、是否支持 BR/EDR16-BIT SERVICE UUID0x03完整 16 位服务 UUID 列表SHORTENED LOCAL NAME0x08短名称COMPLETE LOCAL NAME0x09完整名称TX POWER LEVEL0x0A广播发射功率默认广播数据是协议栈自动生成的你可以手动替换成下面这组const uint8_t adv_data[] { 0x02, 0x01, 0x06, // FLAGS可发现单模低功耗 0x03, 0x03, 0x00, 0xF0, // 16 位服务 UUID0xF000 的 LSB 在前 0x09, 0x09, S, T, M, 3, 2 // 完整设备名STM32 };第一段长度 0x02 表示后面还有 2 字节第二段长度 0x03 表示后面有 3 字节其中 AD Type 是 0x03UUID 以低字节在前的方式填入。所有段的长度值都不包含长度字节本身这是新手上报数据长度最容易算错的地方。调用时传入 adv_data 的实际字节数不能直接写 sizeof(adv_data)因为数组里包含的是整个结构体而不仅是广播内容。使用 ST 的 ble_gap 接口时我一般这样传参aci_gap_set_discoverable(sizeof(adv_data), adv_data, 0, 0, 0, 0, 0, 0)。参数较多关注前两个和最后的超时时间即可。3.3 连接参数调整从机延时与连接间隔手机连接后BLE 连接的时序由连接间隔、从机延时和超时时间三个参数决定。连接间隔的单位是 1.25ms默认值通常配置为 30ms 到 50ms。间隔越短数据吞吐越高但功耗也越高间隔越长手机会觉得响应迟钝。从机延时是设备主动跳过的连接事件数跳到 4 相当于间隔放大到原来的 5 倍适合低功耗场景但不适合实时交互。如果要在运行时修改连接参数可以用 hci_le_connection_update。示例参数含义如下uint16_t conn_handle 0; // 连接句柄从 HCI 事件中获取 hci_le_connection_update( conn_handle, // 连接句柄 24, // 最小连接间隔 24*1.25ms 30ms 40, // 最大连接间隔 40*1.25ms 50ms 0, // 从机延时 0 500 // 超时时间 500*10ms 5000ms );不是所有手机都会接受这个请求有些手机会拒绝主机侧发起的参数更新。遇到这种情况可以在 CubeMX 的 BLE 配置里把连接参数的上下限直接写成固定值让连接建立时就使用该参数而不靠运行期协商。实际测试中30ms 连接间隔配合 20 字节 MTU单方向传输一包数据大约 3ms 到 5ms足够大多数传感器上报应用使用。4. 动手连接从扫描到写特征走通一条 BLE 数据链路4.1 手机端 APP 选择nRF Connect 更利于看协议细节手机 APP 我常用 nRF Connect它能完整显示广播包内容、服务 UUID、特征属性和 MTU 大小。LightBlue 界面更简洁但 nRF Connect 在调试时能看到更多底层字段。两者都不需要注册账号直接打开就能扫描。4.2 连接、发现服务与订阅通知的完整操作首轮调试建议按以下步骤执行每步都确认一次串口日志不要直接跳到写数据打开 APP 的 Scan 页面找到名为 STM32WB55 或你在广播数据里改名的设备。点击 CONNECT连接成功后进入设备信息页面。切换到 GATT 页面点击加号展开服务列表看到 UUID 以 F000 开头的 P2P Server 服务。展开服务下的特征找到 Notify 特征点击右侧的图标开启通知。保持设备在广播和连接状态观察 NUCLEO 板串口输出是否有连接事件日志。这一步容易遇到的问题和排查思路整理成表现象大概率原因排查方向扫描不到设备RTC 或 HCI 初始化卡住检查 RTC 是否使能M0 固件是否烧录能扫到但连不上广播数据长度错误检查 adv_data 长度及 AD Structure 合法性连接后立刻断开连接参数下限低于手机支持值把连接间隔调大或固定为单值写特征无响应没有订阅 CCCD先开启 Notify再写数据4.3 写数据并观察板端串口日志P2P Server 工程默认会在收到写数据时触发回调函数但这个回调里并没有帮你打印数据。我通常在 p2p_server_app.c 的 BLE 事件回调里加一段打印把它用作链路验证的探针void P2P_SERVER_App_Notification(BLE_Server_Event_t *p_Notification) { if (p_Notification-Event BLE_SERVER_EVT_WRITE_NO_RESP || p_Notification-Event BLE_SERVER_EVT_WRITE) { printf(RX len%d\r\n, p_Notification-DataLength); for (uint8_t i 0; i p_Notification-DataLength; i) { printf(%02X , p_Notification-Data[i]); } printf(\r\n); } }p_Notification 的具体字段名会根据 STM32CubeMX 版本的兼容层略有差异我见过旧版本用 DataLength 和 DataBuffer新版本用 DataLength 和 Data。编译报错时先看生成头文件里的结构体定义不要把名字写死。逻辑说明BLE_SERVER_EVT_WRITE_NO_RESP 是写入无响应特征触发的事件BLE_SERVER_EVT_WRITE 是普通写请求触发的事件两端都打印数据长度和 HEX 内容可以立刻确认整条链路是否走通。5. 进阶技巧改成自定义服务和一把过烧录的避坑点5.1 把 P2P Server 换成自己的 16 位 UUID 服务P2P Server 的代码结构非常适合直接改成私有服务。打开 p2p_server_app.h你会看到类似 P2P_SERVER_SERVICE_UUID 的宏定义。把它从默认的 0xF000 改成你自己的服务 UUID例如 0xF100然后同步修改写特征和通知特征的 UUID。改完后手机 APP 需要重新扫描因为广播包里的服务 UUID 也变了。如果你需要增加一个特征而不是改现有的先复制 P2P_SERVER_Add_Char 里的命令块再用 aci_gatt_add_char 添加新特征并设置合适的属性权限。常见的属性值含义是0x08 表示 Write0x10 表示 Notify0x20 表示 Indicate0x02 表示 Read。写请求长度如果没有特殊需求默认 20 字节即可不要一边写 20 一边又往特征里塞 40 字节数据GATT 层会直接丢包。5.2 使能低功耗后下载失败先用命令行擦除整片 FlashSTM32WB55 在使能低功耗后M0 网络核会进入很深的睡眠此时再用 IDE 下载程序很可能提示连接不上目标。这不是板子坏了而是调试口被低功耗逻辑占用。正确的做法是先进入 bootloader 模式或用命令行工具把整片 Flash 擦掉再重新下载。最省事的方式是使用 STM32CubeProgrammer 的命令行模式。Windows 下打开命令行工具输入STM32_Programmer_CLI.exe -c portSWD modeHOTPLUG -el参数说明-c 后面指定连接方式portSWD 表示使用 ST-LINK 的 SWD 接口modeHOTPLUG 表示热插拔模式适合目标板已经上电的情况-el 是 erase entire Flash 的缩写会把 M4 和 M0 两个核的代码全部擦除。执行成功后再回到 STM32CubeIDE 正常下载基本一次就能通过。如果连命令行也连接不上把 NUCLEO 板断电按住板上的 NRST 键不放再上电后点击下载等下载进度开始后再松开 NRST。这个技巧屡试不爽核心原因是 NRST 强制复位了 M0 核让 HCI 层暂时让出了调试总线。实际项目里可以把模式切换引脚接出来用 MCU 控制进入系统 bootloader比每次手动按键稳定得多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进