Windows环境下C/C++ BLE开发实战:从UWP到跨平台方案
2026/9/8 2:34:02 网站建设 项目流程

简介:面向在Windows平台使用C/C++开发低功耗蓝牙(BLE)应用的工程人员,这份资源以WinRT为底层API,提供了一个可编译运行的Visual Studio工程示例,用于解决BLE设备枚举、广播监听、配对连接及GATT服务读写等常见通信需求。压缩包体积仅10KB,共包含11个文件:4个头文件与4个C++源文件构成核心代码,配合vcxproj工程文件和filters筛选器,结构紧凑,便于快速理解Windows BLE项目的组织方式。目前已有5951人学习下载,适合有一定C++基础、希望在桌面应用中集成蓝牙功能的开发者参考。源码不仅引用了DeviceEnumeration、Advertisement、GenericAttributeProfile等命名空间,还在工程入口与蓝牙句柄封装处展示了初始化、事件回调及异步操作的典型处理思路,可直接迁移到实际项目中,有效节省文档查阅与底层调试时间。 Windows环境上用C/C++开发BLE(低功耗蓝牙)应用,说难不难,说不难也确实是沟沟坎坎一堆。这个话题我断断续续摸了快两个月,从最初对着UWP文档一脸懵,到后来用C++/WinRT调通GATT服务读写,再到用SimpleBLE把同一套逻辑移植到Linux上,中间踩了不少坑。这篇就按我的思路和实操记录,把整个流程梳理一遍,希望能帮你少走弯路。

这篇内容主要面向这样的场景:你需要用C/C++在Windows上做BLE客户端,比如扫描一个BLE外设(温湿度传感器、心率计、ESP32-S3做的小板子、BLE串口透传模块等),连接它,读写特征值,订阅通知。适合的人群是嵌入式工程师、上位机开发同学,以及像我一样被项目逼着啃Windows BLE的老哥。文章会涉及方案选型、BLE协议基础、具体代码实现和排错经验,不预设你有多深的基础,但至少你得知道C++怎么编译链接。

1. 方案选型:动手之前先想清楚走哪条路

1.1 官方路线:UWP Bluetooth API与C++/WinRT

微软在Windows 10 1703之后主推的BLE开发接口是Windows.Devices.Bluetooth命名空间下的UWP API,涵盖广播监听(BluetoothLEAdvertisementWatcher)、设备管理(BluetoothLEDevice)和GATT操作(GattDeviceService、GattCharacteristic等)。这套接口最初是为UWP应用设计的,用C#调用最舒服,但C++也能通过C++/WinRT或老的C++/CX来访问。

我自己试下来,C++/WinRT的写法跟C#几乎一一对应,异步操作用co_await接concurrency任务,回调比C++/CX清爽不少。但有个很现实的问题:UWP API在纯Win32桌面应用里用起来有个“UseWinrt”的标志位要处理,而且编译出来的程序往往带有UWP或打包限制的痕迹,不是说你建一个控制台工程就能直接调用。

如果你只是做一个内部工具、上位机Demo,或者原型验证,UWP路线没问题。但如果是给客户交付的桌面软件,尤其是还要兼容Win7、Win10老版本,这条路线会让你在打包和权限声明上反复折腾。

1.2 第三方封装库路线:SimpleBLE与BLE-Adapter

后来我换了思路,尝试第三方跨平台库。SimpleBLE是我用得比较顺手的,它用C++重写了UWP底层,接口很精简,大致上就是scan、connect、discover services、read/write/notify这么几大类,并且同一套代码可以编译到Windows(内部走UWP)和Linux(内部走BlueZ)。对做跨平台工具链的团队来说,这笔账非常划算。

BLE-Adapter是个相对更底层的C++库,主要面向Linux,Windows端支持较弱。还有一个叫WinBLE的库,是单独针对Windows做的,用起来也不错但更新节奏慢,遇到新SDK版本可能需要自己修编译错误。综上,如果只锚定Windows平台,用官方UWP或者SimpleBLE都行;如果考虑以后往Linux迁移,直接上SimpleBLE更省心。

1.3 串口透传方案:适合硬件模块和工程快速落地

除了GATT这条路,实际工控场景里有一大批BLE串口透传模块——比如极低成本的BLE-UART模块,以及很多Modbus RTU设备通过蓝牙模块做无线采集——它们不暴露标准GATT服务,而是把所有数据一股脑扔进一个透传通道,PC端只需要把这个通道当成虚拟串口来读写。

这种场景下,用C/C++直接操作串口(CreateFile + WriteFile/ReadFile)反而比折腾UWP API简单。你只需要知道模块固件里写好的服务UUID和特征UUID,连接后收发即可。很多模块厂商还会提供配套的AT指令,用来配置波特率、广播名称、连接间隔等。这种“蓝牙当串口用”的做法,开发量最小,错误率最低,是我在工业项目里最常用的一条路。

2. BLE基础概念:这些内容绕不开

2.1 GAP、GATT、Service与Characteristic

很多人第一次接触BLE就被GAP、GATT、Attribute等术语绕晕。我习惯用图书馆做类比:GAP是图书馆的屏幕和门禁,负责广播和连接;GATT是馆内的图书分类规则,决定数据怎么组织和读取;Service是一排书架,Characteristic是书架上每一本可以读取或写入的书。

编程的时候,你真正打交道最多的是Characteristic,它有三个核心属性:Read(可读)、Write(可写)、Notify/Indicate(可订阅通知)。比如温湿度传感器,通常有若干个Characteristic:一个放温度值,一个放湿度值,一个可能有电池电量。PC端要做的,就是先按GATT规范找到这些服务,再对特定Characteristic发起读写或订阅。

2.2 广播类型与连接流程

BLE广播分为可连接广播和不可连接广播两大类,还有定向广播等变体。可连接广播的包在数据包里会携带厂商自定义数据或设备名称,扫描端通过BluetoothLEAdvertisementWatcher接收这些广播包,然后可以发起连接。

连接过程有两点值得注意:第一步是建立物理链路,也就是完成连接参数协商(连接间隔、从设备延迟、监督超时);第二步是服务发现,主设备要等从设备的GATT服务表准备好后才能枚举服务。实际编程中一个常见误区是:刚连接成功就立刻去GetGattServices,结果偶尔返回空。解决的办法是确认连接状态后再延迟几十毫秒,或者自己实现重试机制。

2.3 MTU:决定单包数据大小的关键参数

MTU(Maximum Transmission Unit)决定了BLE链路单次最多能传多少字节,默认是23字节,其中ATT头占3字节,应用层有效载荷只有20字节。如果你想一次写更多数据,比如OTA升级或者透传大数据块,必须在连接建立后主动发起MTU交换请求。

在Windows UWP里,你可以用GattSession去尝试提高MTU,常见的做法是请求MTU为512,设备支持的话就协商到尽可能大的值。但要注意,MTU交换并不保证成功,如果从设备不支持更大的MTU,协商结果还是默认值。实际开发时,我一般先把通知订阅好,再发起MTU协商,免得丢包。

2.4 配对与绑定:bond不等同于配对

配对(Pairing)是在连接过程中临时交换密钥以加密链路;绑定(Bonding)是配对后把长期密钥保存下来,下次连接无需再次配对。在Windows上,如果设备要求加密或MItM认证,系统会自动弹出配对框。

这个场景我遇到过好几次:用BLE调试助手绑定过的模块,换到自己的程序里就连接失败,原因是PC端和模块间绑定的旧密钥与模块内部的绑定信息不一致,模块拒绝重新配对。解决方法是在Windows系统蓝牙设置里删除这个设备(清除绑定信息),再重新扫描配对。

2.5 BR/BLE的区别

经典蓝牙BR/EDR和低功耗蓝牙BLE虽然共享2.4GHz频段,但协议栈完全不同。BR/EDR面向持续传输(音频、文件),功耗高;BLE面向小包突发传输,省电,支持广播和快速连接。编程上,这两类接口也不一样——经典蓝牙更多用BluetoothAPIs.h里的RFC0MM函数,BLE则是UWP的Windows.Devices.Bluetooth。

如果你在产品里混用两种蓝牙,比如既要传音频又要连低功耗传感器,需要仔细区分设备类型和接口,别指望一套API通吃。

3. 实操:用两套方案各写一遍核心流程

3.1 环境准备:VS Code + CMake + C++/WinRT

开发环境我推荐VS Code配CMake,原因很简单:C++/WinRT现在基本不需要Visual Studio的向导,VS Code写起来轻量,CMake构建跨平台也友好。需注意的是,C++/WinRT编译依赖Windows SDK版本至少1809以上,建议直接用Windows 11 SDK。

你可以在项目根目录放一个CMakeLists.txt,核心内容大概是:

cmake_minimum_required(VERSION 3.20) project(ble_demo) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(windowsapps REQUIRED) add_executable(ble_demo main.cpp) target_link_libraries(ble_demo PRIVATE windowsapps)

另外,VS Code里的c_cpp_properties.json需要把“cppStandard”设置成“c++20”,并且添加Windows SDK的include路径。C++/WinRT还要求代码中显式包含对应的winrt头文件,比如winrt/Windows.Devices.Bluetooth.h、winrt/Windows.Devices.Bluetooth.Advertisement.h等。

3.2 用C++/WinRT扫描BLE设备

C++/WinRT的异步模型基于co_await,扫描的核心代码如下:

#include <winrt/Windows.Devices.Bluetooth.Advertisement.h> using namespace winrt; using namespace Windows::Devices::Bluetooth::Advertisement; void scan_devices() { BluetoothLEAdvertisementWatcher watcher; watcher.Received([&](BluetoothLEAdvertisementWatcher const&, BluetoothLEAdvertisementReceivedEventArgs const& args) { auto addr = args.BluetoothAddress(); auto name = args.Advertisement().LocalName(); printf("device: %s, addr: %llX\n", name.c_str(), (uint64_t)addr); }); watcher.Start(); }

这里有个容易踩的坑:Received回调是在后台线程的,别在里边直接操作UI控件或者做长时间处理,否则要么抛跨线程异常,要么把消息循环卡死。我一般会把扫描到的设备信息封装成结构体,丢到生产者-消费者队列里,由主线程统一消费。

3.3 连接、发现服务与读写特征

连接设备需要先通过BluetoothLEDevice::FromBluetoothAddressAsync获取设备对象,成功后调用GetGattServicesAsync枚举服务,再枚举特征:

auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(addr); auto servicesResult = co_await device.GetGattServicesAsync(); for (auto& service : servicesResult.Services()) { auto charsResult = co_await service.GetCharacteristicsAsync(); for (auto& ch : charsResult.Characteristics()) { // 按特征UUID做分发 } }

读写特征时,如果是write,使用WriteValueAsync;如果是notify,先调用WriteClientCharacteristicConfigurationDescriptorAsync启用通知,再用ValueChanged事件订阅数据。

3.4 用SimpleBLE封装一套跨平台客户端

如果你打算用SimpleBLE,代码会更直白。初始化和扫描:

#include "simpleble/SimpleBLE.h" SimpleBLE::Adapter adapter = SimpleBLE::Adapter::get_adapters().value(); adapter.scan_for(3000); auto devices = adapter.scan_get_results();

连接和操作GATT:

device.connect(); auto services = device.services(); for (auto& svc : services) { for (auto& ch : svc.characteristics) { device.notify(svc.uuid, ch.uuid, [](const std::string& data){ // 处理通知 }); } }

对比能看出来,SimpleBLE把很多细节藏起来了,代码量少一半。但这也意味着出了问题你不好定位,所以我的建议是:调试阶段用C++/WinRT把底层流程跑通,工程交付阶段再换成SimpleBLE,或者直接用C++/WinRT写完也行,看你的维护习惯。

3.5 与ESP32-S3 BLE配网等场景的对接

这类BLE客户端开发最常见的落地场景之一,是给ESP32-S3做配网工具。ESP32-S3一般会开一个BLE GATT服务,PC端扫描到它以后,往指定的特征写入WiFi SSID和密码,设备重启再去连接路由器。写入的数据长度通常不大,但如果你用中文SSID,注意UTF-8编码,别搞成ANSI,不然设备端解码就乱了。

另一个高频场景是Modbus RTU蓝牙透传。我的做法是把BLE透传通道当成串口收发,PC端保留原来的Modbus主站逻辑,只不过底层读写的不是COM口而是BLE特征。实测下来吞吐量大概每包100字节左右,控制一个几台从站的RTU网络完全够用。

4. 常见问题与排查技巧实录

4.1 连接成功但服务表为空

这个现象通常在设备刚上电时容易发生。原因是设备启动后内部服务初始化尚未完成,或者连接参数协商和GATT发现同时进行导致冲突。排查思路:连接成功后等一段时间(至少500ms)再枚举服务;如果还不行,用BLE调试助手看看同一个设备在手机端是否正常枚举服务,以此判断是设备问题还是PC端问题。

4.2 Notify收不到数据

第一嫌疑是没启用特征的通知功能,也就是没有写Client Characteristic Configuration Descriptor。第二嫌疑是设备的通知走的是Indicate而不是Notify,这两者在CCCD配置里是不同的值(0x0002 vs 0x0001),订阅类型写反了自然收不到。第三嫌疑是MTU太小,数据被拆分后接收端没组包完全。

4.3 程序能跑但VS Code里报一堆Windows SDK错误

多半是CMake找不到windowsapps或者include路径不对。可以检查VS Code的编译配置里是否正确指定了Windows SDK版本,并且在CMakeLists里把WIN32和WINDOWS_APP属性设置好。

4.4 常用问题速查表

问题现象可能原因处理建议
扫描不到设备广播类型不可连接/权限不足检查设备广播参数;确认系统蓝牙开关打开
连接即失败绑定信息冲突/设备忙删除系统配对记录后重试
写入总是失败特征属性不支持写/数据长度超MTU查看特征属性;分片或协商MTU
Notify/Indicate收不到CCCD没配置/订阅类型不符按特征实际支持类型配置CCCD
程序崩溃在回调里跨线程访问UI或队列同步问题把数据处理切到主线程或加锁保护
长时间运行无响应消息循环被GATT异步操作阻塞改用异步接口,避免阻塞UI线程

4.5 调试辅助:硬件和软件的搭配

调试BLE上位机,我常备三样东西:一块支持BLE的手机当移动调试端,一个USB蓝牙4.0/5.0的适配器(PC内置蓝牙经常驱动不全),外加一块带OLED显示和按键的BLE开发板(比如ESP32-S3)做模拟外设。这样PC端出问题,我可以立刻在手机上验证外设是否正常,快速定位问题出在PC代码还是外设端。

写在最后

说实话,Windows BLE开发最大的坑不是C++语法,也不是协议有多深,而是微软把完整的BLE能力都放在了UWP API里,导致很多老C++开发者一下子不习惯这种异步加事件驱动的写法。我自己经历了从抵触到接受再到觉得挺好用的过程。如果你以后还要在Linux上做同样的功能,建议一开始就留意SimpleBLE这样的跨平台库,把业务逻辑和平台API解耦开,省得后面把代码重写一遍。最后再分享一个小经验:凡是涉及BLE的工程,务必把日志系统尽早搭好,打印出扫描、连接、服务发现、读写、通知的每一步状态和时间点,不然一旦出问题,凭肉眼在断点上找原因会把人逼疯。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询