资讯动态

Windows环境下C/C++ BLE开发实战:从UWP到跨平台方案

发布时间:2026/9/8 2:34:03 来源:尧图企业网站定制
简介面向在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库主要面向LinuxWindows端支持较弱。还有一个叫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决定单包数据大小的关键参数MTUMaximum 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”设置成“c20”并且添加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();连接和操作GATTdevice.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查看特征属性分片或协商MTUNotify/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的工程务必把日志系统尽早搭好打印出扫描、连接、服务发现、读写、通知的每一步状态和时间点不然一旦出问题凭肉眼在断点上找原因会把人逼疯。本文还有配套的精品资源点击获取

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

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

免费获取报价