
简介面向STM32WB55系列开发者的BLE入门工程资源包基于NUCLEO-WB55开发板演示如何通过STM32CubeMX快速生成一个能够连接手机APP的简单蓝牙应用帮助嵌入式开发者快速上手STM32WB双核无线MCU的蓝牙开发流程。资源共392个文件压缩包约26.72MB核心包含c/h源码、uvprojx与ioc工程配置、hex/axf烧录文件、map/sct链接脚本等可完整还原工程构建过程同时保留了o、crf等编译中间产物便于分析代码与构建依赖关系。目前已有2159人学习下载。包内BLE初始化代码、HCI层驱动及回调机制可作为实际项目的基础模板配合作者CSDN博文和B站视频讲解可系统掌握CubeMX图形化配置、STM32IDE编译下载、手机端APP扫描连接等关键步骤覆盖从外设初始化到无线协议栈调用的完整链路适合正在学习低功耗蓝牙和STM32WB开发的学生、工程师参考复用。无论是入门演示还是二次开发都能从中获得清晰的代码路径与排错参考。1. STM32WB55 STM32CubeMX 的第一印象第一次在 STM32WB55 上做 BLE 开发的人大多不是被蓝牙协议吓跑的而是被工程结构绕晕的。这颗芯片是双核设计Cortex-M4 跑应用逻辑Cortex-M0 单独跑蓝牙协议栈两边通过 IPCC 硬件中断通信。好处是协议栈由 ST 以二进制库形式提供你不需要维护蓝牙协议源码坏处是工程里多了一堆自动生成的中间层文件新手容易在“到底该改哪个文件”上卡住。用 STM32CubeMX 生成 BLE 工程后打开代码会看到 STM32_WPAN、App、Middlewares 等目录第一眼觉得复杂其实要动的只有 app_ble.c、ble_conf.h 和少量服务文件。本文会从 CubeMX 配置讲起落到协议栈固件烧录、心率服务配置、手机 APP 连接验证和稳定性调试按一条完整链路走完。适合有 STM32 基础、第一次接触 BLE 的开发者目标是让你在半小时内看到手机 APP 上跳出第一组来自板子的数据。2. 在 STM32CubeMX 里准备 STM32WB55 的 BLE 工程2.1 为什么选 STM32WB55 而不是外挂蓝牙模块STM32WB55 这类单芯片无线 MCU和“MCU 串口蓝牙模块”的方案相比最大区别在于数据通路。串口模块方案里MCU 把数据打包成 AT 指令或自定义帧经 UART 发给蓝牙 SoC再由 SoC 封装成 GATT 数据包。这条链路多了一次串口传输波特率、缓冲区、流控都会成为吞吐和延时的瓶颈低功耗场景下还要额外管理两个芯片的睡眠同步。而 STM32WB55 的 M4 核和 M0 核在同一颗芯片内协议栈跑在 M0 上M4 通过 API 直接操作 GATT 服务和特征值不需要物理链路中转。广播、扫描、连接管理全部由 M0 核处理M4 核只关心业务数据。开发时可以把 BLE 看作一个外设而不是一个需要“对话”的通信对象代码结构清晰很多。从选型角度我一般会关注三个指标对比项STM32WB55 单芯片方案外挂 BLE 模块方案数据路径M4 核 API 直调 GATTMCU 串口 → 模块协议栈吞吐瓶颈连接间隔和 MTU串口波特率 模块处理能力低功耗协同双核共享电源管理需要双芯片睡眠握手调试复杂度集成 ST-LINK日志走 VCP需额外引 UART 日志认证成本模块已认证整机再认证负担小模块认证不能完全覆盖整机实际项目中如果只是透传开关量、传感器数据外挂模块确实更快起步但如果要做心率计、运动手表这类需要协议栈深度定制的产品STM32WB55 这类内置协议栈的芯片更合适。NUCLEO-WB55RG 开发板已经把射频天线、晶振、ST-LINK 都做好了拿来验证 BLE 功能比自制板子省很多事。2.2 CubeMX 里的关键配置项打开 STM32CubeMX新建工程后选择开发板对应的芯片型号 STM32WB55RGV6或者直接在 Board Selector 里选 NUCLEO-WB55RG会自动加载默认引脚配置。系统时钟我会将 M4 核主频设为 64MHzHSE 使用板载 32MHz 晶振并开启 RTC 日历。BLE 协议栈内部依赖 RTC 做计时不配置 RTC 会导致连接初始化失败。中间件部分是重点。在 Categories 里找到 Middleware and Software Packs → STM32_WPAN → BLE勾选后会出现协议栈参数页面。这里有一项容易漏Wireless Firmware Update 相关的选项在生成代码时不会自动下载协议栈镜像CubeMX 只会生成工程结构M0 核的固件需要单独烧录。生成代码前建议把以下参数确认一遍配置项设置值备注RTC 时钟源LSE 32.768kHz协议栈计时依赖USART1 模式Asynchronous115200-8-N-1日志输出走 ST-LINK VCPBLE GATT 服务Heart Rate0x180D作为最小可验证服务BLE 设备名称STWB55-HR方便手机端识别BLE 广播间隔30ms0x30 单位 0.625ms便于手机快速发现设置完成后点击右上角 Generate Code选择 STM32CubeIDE 或 Keil 工程格式。生成后的工程里M4 核的应用代码在 App/ 目录协议栈相关的配置文件散在中 layers 和 Middlewares 里。2.3 先认识 app_ble.c 和 ble_conf.h生成代码后不要急着编译烧录先看两个文件。第一个是 App/app_ble.c这是 M4 核侧 BLE 应用的入口main() 函数里会调用MX_App_BLE_Init()它的内部完成协议栈初始化、GATT 服务注册和广播启动。下面是一段典型的 main() 调用序列int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_RTC_Init(); MX_LPUART1_UART_Init(); /* 调试日志串口 */ MX_App_BLE_Init(); /* BLE 协议栈初始化必须放在外设初始化之后 */ while (1) { MX_App_BLE_Process(); /* BLE 应用主循环处理协议栈事件 */ } }这段代码的关键在顺序RTC 必须在 BLE 初始化之前完成因为协议栈启动时会读取当前时间并启动计时器MX_App_BLE_Process()需要在主循环里被持续调用它会从 M0 核收取事件并分发给对应的服务回调。如果主循环里放了阻塞延时BLE 事件得不到及时处理手机端会表现为连接超时或数据刷新停滞。第二个要看的文件是 App/ble_conf.h里面集中了协议栈的编译期配置项比如最大连接数、最大服务数量、缓冲区大小等。默认配置已经够本次测试用不建议新手改动等出现资源不足的编译错误再回头调整。2.4 M0 核协议栈固件烧录是第一个坑CubeMX 生成的工程编译烧录后手机扫描不到任何设备这是刚接触 STM32WB55 的人最容易踩的坑。原因不是代码写错了而是 M0 核还是空的BLE 协议栈固件根本没有加载。STM32WB55 的双核结构决定了 M4 核应用和 M0 协议栈是两份独立固件烧录 M4 工程并不等于烧录了蓝牙协议栈。正确做法是先用 STM32CubeProgrammer 或 STM32CubeIDE 的烧录功能把无线固件写入 M0 核。具体固件文件在 CubeMX 安装目录或 STM32CubeWB 固件包的 Projects 相关路径下文件名类似 stm32wb5x_BLE_Stack_full_fw.bin。烧录时选择地址 0x08000000 之后的区域默认偏移量在 CubeProgrammer 里选择 STM32WB 专用选项即可。烧完后复位板子再运行 M4 核的 BLE 工程手机端才能搜索到 STWB55-HR 设备名。提示如果烧录 M4 工程后设备经常复位或卡死在Error_Handler()优先检查 M0 固件是否已烧录其次检查 RTC 是否配置成功这两个问题占了初始化失败的大部分原因。3. 配置蓝牙服务与特征值让数据包能发到手机3.1 服务、特征值、通知的最小模型BLE 的数据交互模型可以简化成三级结构一个设备可以包含多个服务Service每个服务包含多个特征值Characteristic数据实际存放在特征值里。手机 APP 连接设备后需要先发现服务再找到特征值最后决定是读取数据还是订阅通知。打开通知后设备可以在自己的节奏下把数据主动推给手机这是传感器类应用最常用的模式。在 CubeMX 中启用 Heart Rate 服务后工程会生成一套完整的服务代码包含心率测量、身体传感器位置、心率控制点三个特征值。这里最核心的是心率测量特征值UUID 为 0x2A37属性为通知。它的数据格式遵循蓝牙 SIG 的定义第一个字节是标志位指示心率值是 8 位还是 16 位、是否支持传感器接触检测后续字节才是具体心率数值。3.2 在 CubeMX 里把服务名称改成自己想要的CubeMX 的 BLE 配置窗口里Heart Rate 服务默认设备名是 STM32WB55广播时手机端看到的也是这个名字。在 BLE 参数页面找到 Device Name 配置项改成 STWB55-HR重新生成代码。这里要注意设备名长度会占用广播数据包的空间BLE 广播包的有效承载是 31 字节设备名过长会导致广播包放不下而广播失败。设备名超过 20 个字符时建议把广播模式改成包含设备名的方式或者在广播数据里只广播服务 UUID。服务 UUID 一般不需要改0x180D 是蓝牙 SIG 定义的标准心率服务 UUID手机上 nRF Connect 这类工具会直接识别出 Heart Rate 服务名便于验证。如果执行的是私有协议可以自定义 128 位 UUIDCubeMX 里也支持配置但调试时手机端显示为未知服务区分难度大一些。第一次调试建议先用标准服务。3.3 在代码里周期更新心率值生成代码后Heart Rate 服务定义在 App/hrs_app.c 中服务注册在 app_ble.c 里注册函数会分配服务句柄和特征值句柄。更新心率值需要调用 GATT 层的AciGattUpdateCharValue()函数把新的心率值推送到协议栈协议栈会在连接建立且通知使能后自动发送给手机。下面是一段模拟心率更新的实现可以直接放在定时器回调或主循环里轮询调用#define HEART_RATE_UUID_SIZE 2 uint8_t heart_rate_data[2]; void HRS_UpdateHeartRate(uint8_t heart_rate) { heart_rate_data[0] 0x06; /* bit0: 08位心率值, bit1: 1支持传感器接触检测, bit2: 1接触检测已启用 */ heart_rate_data[1] heart_rate; /* 心率值 */ AciGattUpdateCharValue(HRS_HANDLE, HEART_RATE_UUID_SIZE, sizeof(heart_rate_data), heart_rate_data); }第一个参数HRS_HANDLE是心率服务中特征值的句柄它在hrs_app.c里通过宏定义暴露出来不需要手动指定。第二个参数是特征值 UUID 的长度标准 16 位 UUID 填 2。调用这个函数后协议栈会判断当前是否有手机连接以及通知是否使能两者都满足时才会发出数据。如果你的服务启用了多个特征值每个特征值都需要单独调用对应的更新函数。在主循环里周期调用模拟 60~100 次/分的心率变化uint8_t simulated_heart_rate 60; while (1) { MX_App_BLE_Process(); if (HAL_GetTick() - last_tick 1000) /* 每1秒更新一次 */ { simulated_heart_rate 60 (HAL_GetTick() / 1000) % 40; HRS_UpdateHeartRate(simulated_heart_rate); last_tick HAL_GetTick(); } }这里用HAL_GetTick()做非阻塞延时避免占用 BLE 事件处理。很多初学者会在这里直接写HAL_Delay(1000)后果是主循环卡在延时里BLE 事件无法及时处理手机端断连重连频繁。数据更新频率和连接参数需要匹配1 秒更新一次配 30ms 连接间隔绰绰有余如果更新频率超过连接间隔能承载的速率数据会产生积压。3.4 广播参数的调整方法广播参数决定手机能否快速搜索到设备以为连接后的稳定性。CubeMX 生成的广播配置在app_ble.c的Advertising_Init()中主要参数有广播间隔、广播类型、广播数据内容。我把默认的广播间隔从 100ms 调到 30ms缩短手机侧的扫描发现时间代价是功耗略有上升。参数修改和说明如下const Advertising_Init_t adv_params { .adv_int_min 0x30, /* 最小广播间隔0x30 * 0.625ms 30ms */ .adv_int_max 0x30, /* 最大广播间隔同上 */ .adv_type ADV_IND, /* 可连接、可发现广播 */ .adv_channel_map ADV_CH_ALL, /* 三个广播信道都启用 */ };广播间隔单位是 0.625ms0x30 即 48换算后约 30ms。Android 和 iOS 对广播扫描的策略不同iOS 后台扫描对广播间隔更敏感如果发现手机需要反复下拉刷新才能搜到设备优先把广播间隔降到 20~30ms。ADV_IND是可连接无定向广播类型适合大部分点对点应用。如果设备要同时被多个手机扫描到不要用定向广播定向广播只对指定设备有效。注意广播间隔调到 20ms 以下可能触发部分手机协议栈的过滤机制导致扫描反而更慢建议以 30ms 为经验下限。4. 用手机 APP 连接并验证收发数据4.1 先用现成工具做一次完整验证写 Android 代码之前先用 nRF Connect 或 LightBlue 这类通用 BLE 调试工具验证硬件链路。App 启动后扫描设备列表找到 STWB55-HR点击连接。连接成功后进入服务列表界面找到 Heart Rate 服务UUID 0x180D展开后能看到心率测量特征值0x2A37。点击特征值右侧的通知图标使能通知主界面会开始周期性刷新心率数据。这一步能验证三件事广播正常、服务注册正确、通知推送链路通畅。如果卡在某个环节按表格排查现象可能原因处理方式搜索不到设备M0 协议栈固件未烧录重新烧录 BLE Stack 固件搜索到但连接失败广播类型或 MAC 地址冲突重启板子并检查是否多个设备同名连接成功但无数据未使能通知或更新函数未调用在 APP 里打开通知开关检查主循环调用数据刷新卡顿连接间隔过长或主循环阻塞缩短连接间隔去除 HAL_Delay4.2 用最小 Android 代码实现连接与通知使能调试工具验证通过后再写自己的手机 App 就是水到渠成的事。下面的 Kotlin 代码实现了扫描、连接、使能通知三个核心步骤基于原生 Android BluetoothLeScanner API不引入第三方库方便理解 BLE 的基本流程val targetService UUID.fromString(0000180d-0000-1000-8000-00805f9b34fb) val targetChar UUID.fromString(00002a37-0000-1000-8000-00805f9b34fb) // 扫描并连接 val callback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { val device result.device if (device.name STWB55-HR) { bluetoothLeScanner.stopScan(this) gatt device.connectGatt(context, false, gattCallback) } } } bluetoothLeScanner.startScan(callback) // 连接成功后开启通知 private val gattCallback object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices() } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { val service gatt.getService(targetService) ?: return val char service.getCharacteristic(targetChar) ?: return gatt.setCharacteristicNotification(char, true) } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val data characteristic.value val heartRate if (data.size 1) data[1].toInt() else 0 runOnUiThread { textView.text 心率: $heartRate } } }这段代码里有几个关键点。UUID 使用完整的 128 位格式16 位 UUID 要补全为标准蓝牙基 UUID 前缀也就是0000xxxx-0000-1000-8000-00805f9b34fb。setCharacteristicNotification只是告诉手机本地蓝牙栈要监听这个特征值如果服务端特征值属性是通知类型这一步就可以如果是指示类型还需要额外写客户端特征值描述符。onCharacteristicChanged是通知数据到达的回调心率数据按 SIG 格式解析data[0]是标志位data[1]是心率值。4.3 连接参数与数据刷新率的取舍手机连接后连接间隔由手机端发起更新请求板子端可以拒绝或接受。STM32WB55 的协议栈在收到连接参数更新请求时会触发事件如果板子在app_ble.c的连接事件回调里没有显式接收默认会接受大部分合理参数。心率数据 1 秒刷新一次默认连接间隔 30ms 已经足够。如果需要以更高频率推送数据比如 50Hz 的加速度波形就要关注 MTU 大小。STM32WB55 的默认 MTU 是 23 字节扣除 ATT 头后单包有效载荷只有 20 字节。开启数据长度扩展后单包可以提升到 247 字节能有效降低大数据量传输的包数量。MTU 协商的代码在ble_conf.h附近配置也可以在连接建立后主动发起AciGattUpdateMtuRequest。多数手机 APP 在连接成功后会自己发起 MTU 协商板子端只要允许就可以。5. 连接不稳定时的三个排查技巧5.1 用连接事件日志定位断连原因连接不稳定时先确认掉线发生在数据更新期间还是空闲期间。STM32WB55 的协议栈事件可以通过串口日志输出在app_ble.c里找到连接事件回调添加一条日志打印连接句柄和设备地址static void Connection_Event_Callback(uint8_t event, void *data) { switch (event) { case BLE_CONNECTION_STATUS_EVENT: APP_BLE_Print(Connected, handle%d\n, ((Connection_Status_evt_t *)data)-connection_handle); break; case BLE_DISCONNECTION_STATUS_EVENT: APP_BLE_Print(Disconnected, reason0x%02x\n, ((Disconnection_Status_evt_t *)data)-reason); break; default: break; } }断开原因码 0x08 表示连接超时也就是双方在规定时间内没有交换数据包。这种情况重点检查发射功率和天线匹配原因码 0x13 表示远端设备主动断开通常是手机端退出了连接。日志通过 ST-LINK VCP 输出波特率 115200调试阶段一直开着非常有用。5.2 验证低功耗与传输功耗的边界NUCLEO 板用 USB 供电时电流充足连接稳定不代表低功耗场景下同样可靠。换成电池供电后电压跌落会导致射频发射功率下降表现为距离稍远就丢包。排查时用带电流监测的供电工具观察发射瞬间电流STM32WB55 在 0dBm 发射时峰值电流大约是 8~10mA 量级如果供电回路压降过大会直接影响射频链路。5.3 做一个连接自愈的兜底逻辑最后给一个工程上常用的技巧板子端检测到连接断开且不再重连时主动重启广播。协议栈默认在断开后会停止广播如果手机端没有重连逻辑设备就进入了不可发现状态。在断开事件回调里重新调用广播启动函数让设备回到可发现状态能减少现场反复按复位键的情况。case BLE_DISCONNECTION_STATUS_EVENT: APP_BLE_Print(Disconnected, restart adv\n); AciGapSetDiscoverable(ADV_IND, 0x30, 0x30, PUBLIC_ADDR); break;这段代码在断开后立即恢复广播广播参数和初始配置保持一致。实际项目中还需要加上重连次数限制和低功耗策略避免设备在无人连接时持续广播耗尽电池。调试完这几点一套 STM32WB55 的 BLE 连接链路就算真正跑通了。本文还有配套的精品资源点击获取