ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++虚拟串口连接蓝牙:从RFCOMM到COM口的完整通信链路

C++虚拟串口连接蓝牙:从RFCOMM到COM口的完整通信链路 简介这是一份基于 Windows 平台的 C 完整工程示例面向需要实现串口类蓝牙通信的 C 开发者和硬件调试人员。项目围绕“虚拟串口蓝牙”这一核心场景完整演示 Bluetooth API 调用、RFCOMM 协议连接、蓝牙设备发现与配对、CreateFile 创建虚拟串口、ReadFile 与 WriteFile 双向收发数据、错误处理以及多线程读写等关键环节并涉及蓝牙安全权限与资源释放等实用内容。资源压缩包内共 94 个文件主要包括源码 .cpp/.h、Visual Studio 解决方案与工程文件、可直接运行的 exe、pdb 调试符号、资源文件与图标以及中间编译产物、日志和说明文档等整体大小约 87.16MB。目前已有 401 人浏览学习适合希望在 Windows 下快速掌握蓝牙虚拟串口通信的初中级开发者参考。借助该工程读者可直接编译运行查看效果并通过对照源码理解底层 API 调用流程与工程配置方法为后续功能扩展、移植到具体硬件提供扎实基础。1. C通过虚拟串口连蓝牙一个COM口背后的整条通信链路折腾过Windows下蓝牙串口通信的开发者多半都有这种经历设备管理器里明明多了一个COM口自己写的C程序却打不开或者打开了就是乱码。C通过虚拟串口连接蓝牙并通信本质上就是把蓝牙RFCOMM通道映射成系统COM口然后用CreateFile、WriteFile、ReadFile这一套标准串口API去读写。这份BlueToothTest工程覆盖了从设备扫描、配对、打开串口到收发数据的完整流程适合做上位机、要对接HC-05这类蓝牙模块的C开发者。它解决的核心问题不是怎么写蓝牙底层协议而是怎么把一个蓝牙设备当成普通串口来用整条链路都是Win32 API不依赖第三方蓝牙库。2. 虚拟串口与RFCOMMCOM3背后那条蓝牙链路的三个关键层2.1 虚拟串口的本质蓝牙协议栈把RFCOMM通道伪装成了COM口蓝牙协议栈从下往上是HCI、L2CAP、RFCOMM和SDP。RFCOMM的作用是在L2CAP之上仿真出一根RS-232串行线让蓝牙设备能够传输字节流。Windows蓝牙驱动BTHENUM会把一个RFCOMM通道注册成系统设备设备管理器里就出现一个“标准串行 over 蓝牙链接”的COM口。应用程序这一层完全感知不到蓝牙的存在它面对的就是一个COM口打开、读写、关闭的姿势和操作USB转串口一模一样。把蓝牙藏成串口工程上的收益很明显串口开发的经验、调试工具、甚至串口调试助手都能直接复用。代价是蓝牙特有的状态会被隐藏——连接断开、信号变差、模块休眠这些事件在虚拟串口这一侧只表现为ReadFile返回错误或者连接超时。新手在“程序逻辑没错但就是连不上”的困惑里打转根源就在这里拿串口的直觉去推断蓝牙的状态。这里要区分清楚经典蓝牙和BLE。BLE走的是GATTWindows上对应的是另一套GATT客户端API接口形态是服务、特征值、通知根本不是文件句柄。所以如果你手里的设备是BLE微信小程序那种方案这套虚拟串口工程用不上。虚拟串口只服务经典蓝牙的SPPSerial Port Profile透传场景HC-05、HC-06、以及各种蓝牙转串口模块都属于这一类。理解这一点之后调试思路就顺了先看设备管理器有没有COM口再看COM口能不能被串口助手打开最后才是程序层面的读写。三层逐个排查基本能定位九成问题。2.2 0x1101这个服务UUID决定你连上的是不是串口服务RFCOMM只负责把字节流按帧搬运至于搬的是什么服务的数据要靠SDP来协商。蓝牙设备可以同时暴露很多服务串口仿真、拨号网络、文件传输、音频等等。每个服务在SDP里都挂一个128位的UUID其中串口仿真服务对应的短UUID是0x1101完整写法是00001101-0000-1000-8000-00805F9B34FB。0x1101之所以重要是因为Windows的蓝牙串口枚举、配对和驱动匹配全都依赖它。设备管理器里蓝牙串口的设备实例ID通常长这样BTHENUM{00001101-0000-1000-8000-00805F9B34FB}_DEV...看到这个ID就知道这是一个串口服务通道而不是蓝牙PAN或者HID。写代码时如果要做设备过滤这个特征值比设备名称可靠得多因为HC-05有时候显示成HC-05有时候又显示成linvor名称不稳定。代码里定义这个GUID也是最常见的操作// 蓝牙串口服务UUID对应短UUID 0x1101 static const GUID GUID_BTHPORT_SERIAL { 0x00001101, 0x0000, 0x1000, { 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB } };这个GUID在配对查询、SetupAPI枚举、SDP查询三个地方都会用到。它的含义可以理解为蓝牙世界的端口号设备到底提供的是不是串口服务拿这个UUID去SDP里比对就知道。如果模块不是标准的串口透传服务而是自定义UUIDWindows这边没法把它当虚拟串口暴露出来所以选型时优先买支持SPP的透传模块。HC-05这类模块在SDP里登记的就是这个标准串口服务这也是为什么它能在不装任何厂商驱动的情况下被Windows直接识别成COM口。2.3 工程包里有哪些东西老VC工程升级过来的常见痕迹打开压缩包里面除了源码和编译输出还有一批看起来“莫名其妙”的文件。我在这类老工程上翻过车先说结论真正要编译进项目的只有 .sln、.vcxproj 或 .vcproj、.cpp、.h。其他名字带 .old、UpgradeLog、_UpgradeReport_Files、.sdf、.ncb、.ipch、.suo 的全是升级过程和IDE产生的缓存可以安全忽略。从文件清单能看出这个工程的历史BlueToothTest.ncb 是VC6或VC2003的智能感知缓存BlueToothTest.sdf 和 ipch 目录又是VS2010之后的工程缓存UpgradeLog.XML 和 _UpgradeReport_Files 是VS升级向导的产物。这说明工程是从旧版Visual C一路升级上来的代码主体还是Win32风格不依赖MFC框架。这种代码结构简单反而好改。.sln.old 和 .suo.old 是升级前自动备份的版本万一升级后编译不过还能对照旧工程看差异。有个实用习惯值得说.suo 文件记录的是打开的文件标签、断点这些用户状态换机器或者发给别人时容易产生干扰我通常是直接删掉再打开解决方案。.sdf 和 ipch 加起来体积很大动辄几百MB如果用git管理这两个路径应该加进.gitignore不然每次编译提交都被拖累。另外这类老工程默认字符集可能是多字节如果编译时报一堆宽窄字符转换错误去项目属性的“字符集”里切到“使用Unicode字符集”或者反过来二选一对齐就行。3. 设备发现与配对从蓝牙地址到系统分配的COM号3.1 用Bluetooth API把附近的设备扫描出来Windows从Vista开始提供了一套完整的蓝牙API头文件是bluetoothapis.h链接库是Bthprops.lib。扫描设备的核心思路是构造一个BLUETOOTH_DEVICE_SEARCH_PARAMS搜索参数然后用BluetoothFindFirstDevice和BluetoothFindNextDevice轮询。下面这段是能直接编译的扫描代码会打印出已经配对或正在连接的设备名称和蓝牙地址#include windows.h #include bluetoothapis.h #include stdio.h #pragma comment(lib, Bthprops.lib) void ScanPairedDevices() { BLUETOOTH_DEVICE_SEARCH_PARAMS search { sizeof(search) }; search.fReturnRemembered TRUE; // 返回曾经配对过的设备 search.fReturnConnected TRUE; // 返回当前已连接的设备 search.fReturnAuthenticated TRUE; // 返回已验证的设备 BLUETOOTH_DEVICE_INFO info { sizeof(info) }; HBLUETOOTH_DEVICE_FIND hFind BluetoothFindFirstDevice(search, info); if (hFind NULL) { printf(没有找到设备错误码%lu\n, GetLastError()); return; } do { wprintf(Lname%ls , info.szName); printf(addr%02X:%02X:%02X:%02X:%02X:%02X\n, info.Address.rgBytes[0], info.Address.rgBytes[1], info.Address.rgBytes[2], info.Address.rgBytes[3], info.Address.rgBytes[4], info.Address.rgBytes[5]); info.dwSize sizeof(info); // 每次循环必须重置 } while (BluetoothFindNextDevice(hFind, info)); BluetoothFindDeviceClose(hFind); }有三处容易踩坑单独说一下。第一info结构体在循环里必须重置dwSize字段不然下一次调用会因结构体长度不符返回ERROR_INVALID_PARAMETER。第二BluetoothFindDeviceClose一定要在枚举结束后调用这个句柄泄漏不会马上报错但程序反复扫描时句柄数会持续增长。第三fReturnRemembered只返回“曾经配过对”的设备如果模块是新买的还没配过这段代码什么都不会打印需要在Windows蓝牙设置里先配对一次或者改用带UI的BluetoothSelectDevices对话框让用户手动选。如果控制台里中文名称乱码把工程字符集切到多字节或者干脆用OutputDebugString输出到调试器看。扫描拿到蓝牙地址6字节之后后续连接其实很少直接操作这个地址因为虚拟串口模式下驱动已经帮你把设备和COM口绑定好了。地址更多用于日志输出和区分同名设备比如手上同时有HC-05和HC-06的时候。另一个细节蓝牙地址和COM号之间没有公共API能直接做映射实际工程里要么记录配对顺序要么通过设备实例ID里的MAC段去比对后面第4章的枚举逻辑里会用到这个思路。3.2 配对与授权Windows弹窗背后的服务启用逻辑配对这件事程序里能做但第一次基本绕不开系统弹窗。Windows的蓝牙授权弹窗是系统级的不是程序UI的一部分用户得在弹窗里确认并输入PIN码。HC-05和HC-06的出厂PIN码一般是1234也有部分批次是0000配对失败时可以交替试。配对成功之后设备会出现在“设置→蓝牙和其他设备”列表里。更关键的一步是Windows要为这个配对好的服务分发COM口。具体过程由BTHENUM驱动完成用户看到的只是设备管理器里多出来一个或两个COM口。为什么是两个因为Windows会为传出的连接分配一个“传出”COM口同时也可能为设备主动连入的情况分配一个“传入”COM口。实际程序里应该用传出那个。程序里如果要做自动化配对可以调BluetoothAuthenticateDevice但签名里带HWND必须在UI线程调用而且PIN码是明文传进去的。工程里一般不推荐把配对写死在代码里原因很简单配对弹窗的时序、PIN码校验、驱动枚举COM口这三步是异步的自动化配对经常出现“弹窗没出来但代码已经下一步”的情况。我在实际项目里的做法是第一轮先人工配对程序只负责发现和打开串口只有在量产测试这种必须无人值守的场景才把配对逻辑加进去。启用服务的API是BluetoothSetServiceState它告诉蓝牙栈把某个UUID对应的服务设为启用状态BLUETOOTH_DEVICE_INFO info { sizeof(info) }; // 这里先通过地址找到设备填充info省略枚举过程 DWORD err BluetoothSetServiceState( NULL, // 使用默认蓝牙无线电 info, // 设备信息 GUID_BTHPORT_SERIAL, // 串口服务UUID 0x1101 BLUETOOTH_SERVICE_ENABLE); // 启用该服务 if (err ! ERROR_SUCCESS) { printf(启用串口服务失败错误码%lu\n, err); }这段代码在配对异常时会报ERROR_DEVICE_NOT_CONNECTED或ERROR_SERVICE_NOT_ACTIVE前者说明设备当前不在连接状态后者说明该设备没有暴露串口服务。排错的时候看错误码比瞎猜快得多。还有一类适配器芯片自身的问题要注意CSR8510 A10这类蓝牙适配器市面上很多是山寨固件现象是配对正常、COM口也出来了但一打开就报错或者一传数据就断换一个正品适配器问题就消失这种属于硬件玄学程序层面很难兜底。多设备场景下还有个容易忽略的点Windows同一时刻只让一个蓝牙适配器工作如果机器板载蓝牙和USB蓝牙适配器同时存在设备管理器里可能出现两个蓝牙无线电。枚举时可以用BluetoothFindFirstRadio限定具体适配器不然设备可能被分到错误的无线电下表现为时连时断。4. 打开虚拟串口并收发数据CreateFile、DCB与双线程4.1 先找到属于自己的那个COM号枚举蓝牙串口COM号在Windows里不是固定的。HC-05今天可能是COM7换一个USB口插适配器就变成COM9程序里写死COM号在演示工程里没问题拿到实际项目里就是个隐患。先进一点的做法是用SetupAPI枚举所有串口设备再从设备实例ID里过滤出蓝牙串口。GUID_DEVINTERFACE_COM_PORT的值是固定的{86E0D1E0-8089-11D0-9CE4-08003E301F73}它枚举出系统里所有COM设备。要识别哪个是蓝牙串口看设备实例ID里是否包含BTHENUM和00001101两个特征。过滤代码大致如下#include windows.h #include initguid.h // 让setupapi.h里的GUID定义生效 #include setupapi.h #pragma comment(lib, setupapi.lib) char FindBthComPort(char* out, int outSize) { HDEVINFO hDev SetupDiGetClassDevs(GUID_DEVINTERFACE_COM_PORT, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (hDev INVALID_HANDLE_VALUE) return 0; SP_DEVINFO_DATA devInfo { sizeof(devInfo) }; for (DWORD i 0; SetupDiEnumDeviceInfo(hDev, i, devInfo); i) { char inst[512] { 0 }; if (SetupDiGetDeviceInstanceIdA(hDev, devInfo, inst, sizeof(inst), NULL)) { // 蓝牙串口实例ID里同时带BTHENUM和1101 if (strstr(inst, BTHENUM) strstr(inst, 00001101)) { HKEY hKey SetupDiOpenDevRegKey(hDev, devInfo, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); if (hKey ! INVALID_HANDLE_VALUE) { DWORD type 0, size outSize; RegQueryValueExA(hKey, PortName, NULL, type, (BYTE*)out, size); RegCloseKey(hKey); SetupDiDestroyDeviceInfoList(hDev); return 1; } } } } SetupDiDestroyDeviceInfoList(hDev); return 0; }这个函数干的事是遍历COM设备→用实例ID特征识别蓝牙串口→打开注册表键读PortName字段。调用一次拿到类似“COM7”的字符串。过滤条件写两个是对的但要注意个别开发板也会暴露这个UUID必要时再按设备名称做二次过滤。拿到COM号后就可以拼出设备路径\\.\COM7 或者 \\?\COM7串口号大于9时使用 \\.\ 前缀是必须的不然老系统上打不开COM10以上的端口。还有一点initguid.h必须放在setupapi.h之前否则链接时会报GUID未定义的外部符号错误。4.2 打开设备CreateFile的参数别乱填打开虚拟串口的API和打开文件是同一个CreateFile只是参数有固定套路。以下是一个标准的串口打开加配置片段HANDLE OpenBluetoothCom(const char* comName) { char path[64] { 0 }; sprintf_s(path, \\\\.\\%s, comName); // 如 \\.\COM7 HANDLE hCom CreateFileA(path, GENERIC_READ | GENERIC_WRITE, // 可读可写 0, // 串口不支持共享必须0 NULL, OPEN_EXISTING, // 打开已有设备 0, // 同步模式不用FILE_FLAG_OVERLAPPED NULL); if (hCom INVALID_HANDLE_VALUE) { printf(打开失败 错误码%lu\n, GetLastError()); return INVALID_HANDLE_VALUE; } DCB dcb { sizeof(dcb) }; if (!GetCommState(hCom, dcb)) { // 先拿到当前配置再改 CloseHandle(hCom); return INVALID_HANDLE_VALUE; } dcb.BaudRate 9600; // HC-05出厂默认波特率 dcb.ByteSize 8; dcb.Parity NOPARITY; // 无校验 dcb.StopBits ONESTOPBIT; if (!SetCommState(hCom, dcb)) { printf(设置DCB失败 错误码%lu\n, GetLastError()); CloseHandle(hCom); return INVALID_HANDLE_VALUE; } COMMTIMEOUTS to { 0 }; to.ReadIntervalTimeout 50; // 两字节间隔超过50ms视为帧结束 to.ReadTotalTimeoutMultiplier 10; // 参数乘字节数单位毫秒 to.ReadTotalTimeoutConstant 100; // 固定额外100ms SetCommTimeouts(hCom, to); // 不设的话ReadFile可能永久阻塞 return hCom; }三个值得注意的位置。第一dwShareMode必须是0串口是独占设备不允许两个进程同时打开这就是为什么开着串口助手再运行程序会报ERROR_ACCESS_DENIED。第二DCB初始化必须从GetCommState拿到的现有值改起不能new一个空的DCB直接填因为DCB里有一堆保留字段和状态位空结构体会把fBinary这类标志清掉导致收发行为异常。第三COMMTIMEOUTS是所有串口编程里最容易漏的一步不设置的话ReadFile在没数据时会一直阻塞下去程序退出时线程都收不回来。4.3 收发线程同步轮询比异步等待更省心虚拟串口的读写和文件读写长得一样WriteFile发送、ReadFile接收但接收必须放独立线程。原因很直接ReadFile是阻塞的就算设了超时它也会让调用线程停留几十到上百毫秒放主线程里UI和消息循环都会被拖住。多线程方案在这里不是锦上添花是刚需。接收线程用同步ReadFile加轮询退出标志是这类工程里最稳的写法。相比FILE_FLAG_OVERLAPPED异步模式它不用处理OVERLAPPED结构、不用CancelIoEx、也不用担心线程退出时I/O还没撤销导致HANDLE悬空。代价是读超时期间最多浪费100ms的响应延迟对蓝牙串口这种本身就不要求硬实时的场景完全够用。接收线程核心代码static volatile LONG g_bRunning 1; DWORD WINAPI ReceivThread(LPVOID param) { HANDLE hCom (HANDLE)param; char buf[512]; DWORD read 0; while (g_bRunning) { if (!ReadFile(hCom, buf, sizeof(buf), read, NULL)) { // 设备断开时返回ERROR_DEVICE_NOT_CONNECTED或ERROR_OPERATION_ABORTED break; } if (read 0) { OnRecvData(buf, read); // 这里把数据交给你自己的成帧解析 } } return 0; }ReadFile在两种情况下返回收到数据或者超时到期。超时返回时read为0不打印错误直接继续循环这样线程每100ms醒来一次检查退出标志。程序退出时的顺序很重要先置g_bRunning为0再WaitForSingleObject等线程结束最后CloseHandle。顺序反了会出现在CloseHandle之后线程还在用HANDLE的问题表现为程序退出时报句柄无效或随机崩溃。发送侧相对简单主线程直接WriteFile把缓冲区写出去就行。但有一个细节如果后面加了定时发送线程或者UI发送线程两个线程同时写同一个HANDLE会底层交错需要加一个CRITICAL_SECTION锁住WriteFile调用。串口这个粒度上的竞争不处理好数据会偶发拼包排查起来很费时间。5. 蓝牙串口调试避坑连不上、乱码与句柄泄漏的五个现场5.1 配对和枚举HC-05连不上、COM口凭空消失现场一HC-05蓝牙模块连接不上。现象是Windows配对时一直转圈输入1234后提示无法连接或者配对请求超时。原因通常有三个模块还在AT指令模式按住按键上电进入的此时模块不响应配对要先断电重新上电进入正常透传模式指示灯慢闪才代表可被搜索PIN码不是1234而是0000部分批次出厂码不同模块已经被其他主机配对过HC-05作为从机同时只认一个主机换电脑连接时经常栽在这里。解决办法是依次排查确认模块指示灯状态PIN码轮流试1234和0000实在不行按键上电进入AT模式后用ATRESET恢复出厂设置再断电重新配对。这组操作能解决九成连不上问题。现场二设备管理器里蓝牙串口凭空消失或者多出好几个COM口。现象是配对显示成功但“端口(COM和LPT)”下面什么都没有或者出现成对的传出/传入COM口程序打开哪个都不对。原因分成两部分虚拟串口是配对后由BTHENUM驱动的枚举流程异步创建的有时候驱动初始化慢等几秒才出现CSR8510 A10这类免驱适配器的山寨固件对RFCOMM的枚举支持不完整会出现服务通道没注册成功就报成功的情况。解决办法是重新拔插蓝牙适配器强制驱动重新枚举或者去设备管理器里禁用再启用“Microsoft蓝牙枚举器”。配对完不要立刻插拔模块给驱动两三秒完成注册。程序里检测COM口要支持重试别在设备没就绪时就直接失败退出。5.2 数据传输乱码、断流、ReadFile卡死现场三收发全是乱码或偶发错位。现象是程序发出去的数据对端收到了但内容不对或者模块回传数据在串口助手里正常进到程序就乱。原因九成是波特率不匹配HC-05出厂默认9600但有的模块被改过波特率按键上电后可以用ATUART查询实际值另一半原因是DCB配置里校验位和数据位跟模块不一致比如模块被配置成偶校验程序里却是NOPARITY。解决办法是用串口助手把模块的实际参数探出来再回填到DCB结构里。改DCB时务必遵循先GetCommState再SetCommState的顺序只改需要的字段。排查这类问题我一般先看串口助手能不能正常收发能收就是把参数抄过来不能收就是模块本身配置的问题两步走很快能定位。现场四ReadFile一直不返回线程退出时卡死在WaitForSingleObject。现象是程序关闭时等接收线程结束等了几十秒还在等。原因是CreateFile时用了FILE_FLAG_OVERLAPPED但ReadFile传了空的OVERLAPPED参数系统直接报错或者COMMTIMEOUTS压根没设置ReadIntervalTimeout和ReadTotalTimeoutConstant全为0ReadFile进入了无超时阻塞。解决办法是统一用同步模式打开句柄并在读线程启动前设好COMMTIMEOUTS超时值按我前面给的ReadIntervalTimeout50、ReadTotalTimeoutConstant100来设这样ReadFile最多阻塞150ms就会返回轮询线程可以正常退出。进程退出时先置退出标志、等线程、再CloseHandle这个顺序不能反过来。5.3 资源管理句柄泄漏带来的后门COM残留现场五程序崩溃或者强行关掉之后COM口变成“被占用”状态重启电脑前谁都打不开。现象是自己程序挂着打不开串口关闭程序后串口助手也打不开任务管理器里也没找到残留进程。原因其实是串口句柄没被关闭。CreateFile返回的HANDLE在内核里对设备有引用计数进程就算退出只要句柄泄漏进了别的线程或者崩溃时没走清理路径设备的占用标记就还在。蓝牙虚拟串口比物理串口更敏感经常表现为“提示另一个程序正在使用中”。解决办法是给打开串口的代码块上一套分层清理用一个AutoCloseHandle的RAII包装类包住HANDLE异常路径里也走析构进程级加SetUnhandledExceptionFilter在兜底异常处理里先关闭串口句柄再退出。调试期间崩溃后先检查是否有多余的进程实例再拔插蓝牙适配器重置设备状态。这套习惯我从第一次被串口占用问题卡了两小时之后就再也没落下过。6. 数据完整性自测用带序号和累加和的帧协议验证通信验证蓝牙串口通没通最可靠的不是打开串口助手看几个字符而是让程序自己发、自己收、自己校验。把HC-05模块的TX和RX引脚短接程序就成了一个回环测试端发送一帧带序号的数据读回来校验序号连续性和校验和。这个测试一次同时验证了波特率、超时设置、收发线程和句柄生命周期比任何单点测试都有效。#pragma pack(push,1) typedef struct { unsigned char head; // 帧头 0xAA unsigned char len; // 有效数据长度 unsigned char seq; // 帧序号0~255循环 unsigned char data[64]; unsigned char sum; // headlenseqdata 的累加和 } TestFrame; #pragma pack(pop) unsigned char CalcSum(const TestFrame* f) { unsigned char s f-head f-len f-seq; for (int i 0; i f-len; i) { s f-data[i]; } return s; }测试时每200ms发一帧接收线程里校验sum不一致就计数加一seq跳号就记丢帧。连续跑30分钟丢帧率和错帧率都应该是0。这个帧结构故意用累加和而不是CRC16因为它验证的是通信链路本身代码短、可以手工核对排错时一眼能看出是哪个字节出了偏差。要让测试更严格把len填满64并且data里放递增模式能顺带发现字节错位的问题——错位在累加和校验下几乎一定会暴露出来。蓝牙连接断开时回环测试的表现很有特征ReadFile返回ERROR_DEVICE_NOT_CONNECTED。看到这个错误码时不用怀疑自己的代码去查设备管理器里COM口还在不在、模块是不是休眠了。从那以后我每接一个新蓝牙模块第一件事永远是把TX和RX短接跑一遍回环确认链路无误才写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
返回列表