简介:这套代码专为Visual C++ 6.0环境设计,完整实现蓝牙主机控制器接口(HCI)协议的底层通信功能。核心逻辑封装在BT_HCI.cpp和Bluetooth HCI.cpp中,配合SerialPort.cpp(串口通信)与DevicePort.cpp(USB/PCI等设备端口通信),可直接对接各类蓝牙适配器。整个工程基于标准MFC对话框框架构建,包含Bluetooth HCIDlg.cpp/h主界面、资源文件(.rc/.ico)、项目配置(.vcproj/.sln)及调试辅助文件(.ncb/.suo),开箱即用。所有模块均使用纯C++编写,仅依赖Windows API和基础MFC库,不引入任何第三方SDK,适合学习HCI命令发送、事件响应、ACL/SCO数据包解析等关键流程。配套有bluetooth_hci_simulator.py用于模拟测试,ReadMe.txt提供快速上手指引,BasePort.cpp抽象出通用端口操作接口,便于扩展其他硬件通道。代码结构清晰、注释到位,适合Windows平台下的蓝牙协议栈验证、嵌入式蓝牙模块调试或教学级二次开发。
1. 项目概述:为什么这套VC6蓝牙HCI代码至今仍有不可替代的价值
你可能已经习惯了用Python写个脚本几行就搞定蓝牙扫描,或者用Android Studio拖几个控件就能连上BLE设备。但当你真正想搞懂“蓝牙命令是怎么发出去的”“事件包为什么是0x0E开头”“ACL数据包里的handle字段到底怎么算”,那些封装得严严实实的SDK反而成了黑箱。我第一次调试某款国产蓝牙模块时,厂商只给了一份模糊的AT指令手册和一个DLL,连HCI层的Event Code都对不上——最后靠的就是这套VC6写的蓝牙HCI代码包。它不是为了炫技,而是把HCI协议栈最底层的“肌肉组织”一层层剥开给你看:从Windows端口打开、字节流收发、包头校验、到命令状态解析,每一步都踩在Win32 API和MFC消息循环的真实土壤上。
这套代码的核心关键词——蓝牙HCI、VC6源码、串口通信、设备端口、MFC蓝牙——不是堆砌的标签,而是五个相互咬合的齿轮。它不依赖任何第三方蓝牙栈(比如BlueSoleil或Widcomm),也不调用Windows自带的BluetoothAPI(那套COM接口太高层),而是直接跟蓝牙适配器“面对面说话”。所谓“HCI”,本质就是主机(你的PC)和控制器(蓝牙芯片)之间的一套“普通话”:主机发CMD包(比如0x01 0x09 0x00 0x00,即Inquiry命令),控制器回EVENT包(比如0x0E 0x0A … 表示Inquiry完成),中间再穿插ACL数据包传音频或文件。这套VC6工程,就是用C++把这套“普通话”的语法、声调、停顿节奏全写了出来。
它特别适合三类人:一是嵌入式工程师,手头有自研蓝牙模组,需要在Windows上快速验证HCI命令是否被正确响应;二是高校通信/物联网课程老师,带学生做蓝牙协议分析实验,比Wireshark抓包更直观;三是老系统维护人员,某些工业控制机还在跑Windows XP+VC6环境,根本没法装现代IDE。我见过最极端的案例:某地铁闸机厂商用这套代码改出定制版,把HCI Inquiry改成固定MAC轮询模式,硬生生让老旧读卡器支持了蓝牙NFC联动——整个修改只动了BT_HCI.cpp里不到20行,因为逻辑足够裸露、足够可控。它不追求跨平台,不标榜现代化,就专注一件事:让你看清蓝牙协议栈最底下那块砖怎么垒的。
2. 整体架构与设计思路:为什么选VC6?为什么坚持MFC对话框?
2.1 VC6不是怀旧,而是精准匹配底层通信的约束条件
现在很多人看到VC6第一反应是“太老了”,但恰恰是它的“老”,成了这套代码的护城河。VC6编译器生成的二进制对Windows NT/2000/XP内核兼容性极佳,而这些系统正是大量工业设备、医疗仪器、POS终端仍在运行的平台。更重要的是,VC6的CRT(C运行时库)极度轻量——整个可执行文件加资源才800KB左右,没有.NET Framework或UCRT依赖,双击就能跑。我曾把编译好的Bluetooth HCI.exe拷到一台无网络、无管理员权限的车间工控机上,连驱动都不用装,直接识别到串口蓝牙模块,这在VS2019编译的程序上根本做不到(会报msvcp140.dll缺失)。
更关键的是VC6对Win32 API的“零抽象”调用能力。比如打开串口,现代C++可能用std::filesystem或Boost.Asio封装,但这里直接调用CreateFile(“\\.\COM3”, …),参数里明确指定FILE_FLAG_OVERLAPPED和COMMTIMEOUTS结构体。这种写法看似原始,却让你一眼看穿:为什么必须设超时?为什么缓冲区大小要设成1024?为什么ReadFile返回FALSE时要GetLastError()查ERROR_IO_PENDING?——因为底层通信容不得半点模糊。VC6强制你直面这些细节,而不是躲在高级API后面假装问题不存在。
2.2 MFC对话框框架:不是过时,而是为HCI调试量身定制
有人质疑:“为什么不用纯Win32 API写个控制台?”答案很实在:HCI调试需要实时观察三类数据流——发送的CMD包十六进制、收到的EVENT包解码结果、ACL数据流的吞吐统计。控制台窗口滚动太快,根本来不及截图分析。而MFC对话框天然提供多文本框(CEdit)、列表控件(CListCtrl)、按钮组(CButton),正好对应HCI的三大操作场景:
- 左侧CEdit输入HCI命令(如01 09 00 00,自动转为BYTE数组)
- 中间CListCtrl按时间戳记录每条EVENT(0x0E表示Command Complete,0x02表示Connection Complete)
- 右侧CEdit实时显示ACL数据包长度和速率(每秒多少字节)
更妙的是MFC的消息映射机制。比如点击“Send Command”按钮,ON_BN_CLICKED(IDC_SEND_CMD, &CBluetoothHCIDlg::OnBnClickedSendCmd)这个函数里,你能清晰看到:先调用m_btHci.SendCommand(…),再UpdateData(FALSE)刷新界面——逻辑链条短得像一根直线。换成Qt或WPF,信号槽层层转发,调试时得扒三层源码才能定位到实际发包位置。这套设计不是偷懒,而是把“调试可见性”放在第一位。
2.3 双端口接入架构:SerialPort与DevicePort的分工哲学
代码包里并存SerialPort.cpp和DevicePort.cpp,这不是冗余,而是应对真实硬件碎片化的务实方案。我们拆解下它们的职责边界:
-
SerialPort.cpp:专治“传统蓝牙适配器”。这类设备(如基于CSR BC4芯片的USB转串口蓝牙狗)在Windows里注册为COM端口(COM3/COM4)。它用标准Win32串口API:SetCommState配置波特率(HCI要求115200)、SetupComm设置缓冲区、WaitForSingleObject监听事件。关键技巧在于:HCI协议规定CMD包发送后必须等待EVENT包,所以代码里用了WaitCommEvent + OVERLAPPED I/O,避免主线程阻塞。我实测过,如果这里用同步ReadFile,界面会卡死——因为蓝牙控制器响应延迟可能达200ms,用户点按钮后等半秒没反应,第一反应就是程序崩溃。
-
DevicePort.cpp:专攻“原生USB/PCI蓝牙控制器”。这类设备(如Intel Centrino或Broadcom BCM2070)不走串口,而是通过Windows Bluetooth Stack的HCI Transport Layer暴露为设备对象(如\?\USB#VID_8087&PID_07DA#…)。它用CreateFile打开设备句柄,再通过DeviceIoControl发送IOCTL_BTH_VENDOR_COMMAND。难点在于:不同芯片厂商的Vendor Command格式差异极大(CSR用0x0001,Broadcom用0x0002),所以DevicePort.h里定义了虚函数OnVendorCommand(),留给二次开发时重写。这比硬编码所有芯片ID聪明得多——你只需继承DevicePort类,实现自己的Vendor Command解析逻辑即可。
提示:BasePort.h是整个架构的基石。它抽象出Open()、Close()、Write()、Read()四个纯虚函数,SerialPort和DevicePort都继承它。这意味着如果你要接入SPI接口的蓝牙模块(比如nRF52系列),只需新增SpiPort.cpp,实现这四个函数,其他HCI逻辑完全不用动。这种设计让扩展成本趋近于零。
3. 核心模块深度解析:从BT_HCI.cpp看HCI协议如何落地
3.1 BT_HCI.cpp:HCI协议状态机的血肉
BT_HCI.cpp是整套代码的“心脏”,它实现了HCI协议的核心状态机。别被名字误导——它不是简单地发包收包,而是严格遵循HCI规范(Bluetooth Core Specification v2.1第4.3章)构建的有限状态机。我们来看最关键的三个状态:
-
STATE_IDLE:空闲态。此时m_cmdStatus设为CMD_STATUS_NONE,等待用户点击按钮或定时器触发。注意:这里没有用while(1)轮询,而是靠MFC的WM_TIMER消息驱动,CPU占用率恒定0.1%。
-
STATE_CMD_SENT:命令已发出态。当Write()成功后立即切换至此状态,并启动10秒超时计时器(HCI规范要求最长响应时间)。此时若收到EVENT包,必须检查Event Code是否匹配刚发的CMD Opcode(比如发0x0109 Inquiry,就只认0x0E Command Complete或0x02 Connection Complete)。代码里用switch(m_lastOpcode) { case 0x0109: ParseInquiryResult(…); break; } 实现精准路由,避免EVENT错乱。
-
STATE_EVENT_HANDLED:事件处理完成态。解析完EVENT后,调用OnHciEvent()通知上层(比如更新UI显示“Inquiry completed, found 3 devices”)。这里有个易错点:HCI允许控制器在一条EVENT包里塞多个子事件(如0x0E包可含多个Command Status),但VC6代码默认只处理第一个,因为教学场景下简化优先。若需完整解析,需在ParseEvent()里加循环遍历pEvent->data_len。
注意:所有HCI包都有严格格式。CMD包=Opcode(2B)+Parameter Length(1B)+Parameters(NB),EVENT包=Event Code(1B)+Parameter Length(1B)+Parameters(NB)。BT_HCI.cpp里每个SendXXXCommand()函数都手动拼接字节数组,比如SendInquiry()里:
cpp BYTE cmd[7] = {0x01, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00}; // 0x0109=Inquiry, 0x00=Length, 后4字节为Inquiry Length/LAP
这种写法笨拙但绝对可靠——你永远知道第3个字节是参数长度,不会被JSON序列化或protobuf编码搞晕。
3.2 Bluetooth HCI.cpp:HCI命令与事件的翻译官
如果说BT_HCI.cpp是状态机引擎,Bluetooth HCI.cpp就是它的“词典”。它把二进制字节流翻译成人类可读的语义。重点看两个函数:
-
ParseCommandCompleteEvent():这是HCI调试中最常触发的EVENT。它接收0x0E包,先校验Parameter Length是否≥3(HCI规定最小长度),再提取Status Code(第4字节)。Status Code=0x00表示成功,0x0C表示“Connection Timeout”,0x0F表示“Invalid Parameters”。代码里用const char* statusStr[] = {“Success”, “Unknown HCI Command”, …, “Connection Timeout”}建立映射表,UI上直接显示“连接超时”,比看0x0C直观十倍。
-
ParseConnectionCompleteEvent():处理0x04 EVENT,提取Connection Handle(第4-5字节)、BD_ADDR(第6-11字节)、Link Type(第12字节)。这里有个坑:BD_ADDR是小端序存储,但蓝牙地址习惯大端显示(如AA:BB:CC:DD:EE:FF),所以代码里做了字节翻转:
cpp BYTE bdaddr[6]; for(int i=0; i<6; i++) bdaddr[i] = pEvent->data[5-i]; // 倒序复制
我第一次没注意这点,抓包看到地址全是反的,折腾两小时才发现是字节序问题。
3.3 SerialPort.cpp:串口通信的魔鬼细节
SerialPort.cpp表面简单,实则藏满Windows串口编程的暗礁。核心在于三处精妙设计:
-
缓冲区策略:HCI协议要求控制器能缓存至少256字节EVENT,但Windows串口默认RX缓冲区仅1024字节。代码里用SetupComm(hPort, 4096, 4096)将收发缓冲区都设为4KB,避免EVENT包被截断。曾有用户反馈“偶尔收不到Event”,最后发现是缓冲区溢出导致丢包。
-
超时控制:SetCommTimeouts()里设置了ReadIntervalTimeout=0(字节间无间隔)、ReadTotalTimeoutConstant=100(单次Read最多等100ms)。这个100ms很关键——太短会频繁返回0字节,太长会让UI卡顿。我实测过,主流蓝牙芯片响应CMD的P99延迟在80ms内,100ms是安全阈值。
-
线程安全:ReadThread()里用CRITICAL_SECTION保护m_rxBuffer,因为HCI事件可能在任意时刻到达,而UI线程又在定时刷新。如果不用临界区,会出现RxBuffer被读写同时操作,导致内存越界崩溃。这个细节在ReadMe.txt里没提,但代码注释写了// MUST protect rx buffer,是血泪教训。
3.4 DevicePort.cpp:绕过Windows蓝牙栈的野路子
DevicePort.cpp的存在,说明作者深谙Windows蓝牙生态的割裂。Windows自带的BluetoothAPI(BluetoothFindFirstRadio等)只对微软认证设备友好,很多国产模块根本不响应。DevicePort.cpp直接走设备驱动层,用CreateFile(“\\.\GLOBALROOT\Device\BthPort”)打开底层端口(需管理员权限),再通过DeviceIoControl发送IOCTL_BTH_GET_LOCAL_INFO获取本地地址。这里的关键参数是:
DWORD dwIoControlCode = CTL_CODE(FILE_DEVICE_BLUETOOTH, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS);
其中0x800是BTH_IOCTL_GET_LOCAL_INFO的IOCTL编号。这个编号在WDK文档里查不到,是作者用BusHound抓驱动通信逆向出来的——这才是真正的“底层”。
注意:DevicePort.cpp里有一段注释// WARNING: This may crash on non-Broadcom chips。因为不同芯片厂商对Vendor Command的支持程度不同,强行发0x0002命令可能导致设备复位。所以代码里加了try-catch包裹DeviceIoControl,并在OnHciError()里提示“Vendor command not supported”。
4. 实操全流程:从编译到调试一个真实HCI命令
4.1 编译前准备:VC6环境的“考古级”配置
别跳过这步!VC6默认不支持Unicode,而HCI包里的字符串(如设备名称)可能含中文。你需要:
- 打开Project → Settings → General页,将Character Set改为Use Multi-Byte Character Set
- 在C/C++页的Preprocessor Definitions里添加:_CRT_SECURE_NO_WARNINGS(禁用安全警告,VC6不支持_s后缀函数)
- Link页的Object/Library Modules里加入:comctl32.lib ws2_32.lib(前者支持新式控件,后者用于网络相关API)
最关键的一步:安装Platform SDK for Windows Server 2003 SP1。VC6自带的SDK太老,缺少BTH_DEVICE_INFO等蓝牙结构体定义。安装后,在Tools → Options → Directories里,把Include files路径指向SDK的Include目录(如C:\Program Files\Microsoft SDK\Include),Library files同理。我试过直接用VS2019的SDK,结果编译报错“struct _BLUETOOTH_DEVICE_INFO has no member ‘szName’”——因为新版SDK把字段名改了,而VC6代码用的是旧版命名。
4.2 运行第一个HCI命令:Inquiry扫描周边设备
假设你有一根CSR蓝牙狗(COM4),按以下步骤操作:
- 硬件连接:USB插入,设备管理器确认COM4存在,波特率115200(HCI强制要求)
- 启动程序:双击Debug\Bluetooth HCI.exe,主界面弹出
- 端口选择:点击“Port Config”按钮,在弹窗中选择“Serial Port”,输入COM4,点击OK
- 发送Inquiry:在左侧命令框输入
01 09 00 00 00 00 00(Inquiry命令,持续1.28秒),点击“Send Command” - 观察响应:中间列表会刷出两条EVENT:
-0x0E 0x04 0x00 0x09 0x00 0x00→ Command Complete,Status=0x00(成功)
-0x02 0x0B ...→ Connection Complete,含发现的设备BD_ADDR
此时右侧ACL数据显示“0 B/s”,因为Inquiry不产生ACL流量。如果想看ACL,后续可发Create Connection命令(0x01 05 00 0A + BD_ADDR),建立连接后就会有ACL包流动。
实操心得:Inquiry命令的第4字节是Inquiry Length,0x00表示1.28秒(16个1.28秒单元)。若想缩短扫描时间,改成0x01(2.56秒)或0x02(3.84秒),但注意HCI规范最大只支持0x0F(16.38秒)。我曾把这里设成0xFF,结果控制器直接返回0x0F Invalid Parameters——不是代码bug,是协议校验。
4.3 使用bluetooth_hci_simulator.py进行无硬件测试
配套的Python模拟器是教学利器。它用socket模拟HCI Transport Layer,让VC6程序以为连着真硬件。运行步骤:
- 安装Python 3.6+,pip install pyserial(虽然模拟器不用串口,但依赖此库)
- 执行
python bluetooth_hci_simulator.py --port 9999,启动模拟器监听TCP 9999端口 - 在VC6程序里,“Port Config”选择“TCP Socket”,输入IP 127.0.0.1,Port 9999
- 发送任意HCI命令,模拟器会按规则返回预设EVENT(如发0x0109,返回0x0E 0x04 0x00 0x09…)
模拟器的magic在于:它内置了HCI状态机。比如你连续发两次Inquiry,第二次会返回0x0E 0x04 0x01 0x09…(Status=0x01,表示Command Disallowed),因为规范禁止并发Inquiry。这种细节,只有亲手写过HCI栈的人才会模拟得如此精准。
4.4 调试技巧:如何定位HCI通信失败的根本原因
HCI调试最怕“无声失败”——点了Send没反应,也不知道是发没发出、还是控制器没响应、或是EVENT被丢弃。我的四步排查法:
-
抓底层I/O:用Sysinternals的ProcMon监控Bluetooth HCI.exe的CreateFile和DeviceIoControl调用。如果看到CreateFile(“\\.\COM4”)返回NAME NOT FOUND,说明端口号错了;如果DeviceIoControl返回ACCESS DENIED,说明没管理员权限。
-
看串口波形:用Saleae Logic Analyzer接COM4的TX引脚。正常HCI通信应看到密集的115200bps数据流;如果只有零星几个字节,说明Write()没成功;如果全是0xFF,说明线没接好。
-
查EVENT包完整性:在BT_HCI.cpp的OnReadComplete()里加日志:
TRACE("RX %d bytes: %02X %02X %02X...\n", nBytes, buf[0], buf[1], buf[2]);。如果日志里EVENT包长度不对(比如0x0E包声明Parameter Length=0x04但只收到3字节),就是缓冲区或超时设置问题。 -
对比规范原文:当STATUS CODE非0x00时,立刻查Bluetooth Core Spec的Error Codes表。比如0x0C是“Connection Timeout”,说明远程设备没响应;0x10是“Connection Failed to be Established”,可能是ACL连接参数不匹配。别猜,直接查表。
5. 常见问题与避坑指南:那些文档里不会写的实战经验
5.1 典型问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 点击“Send Command”后UI卡死 | SerialPort.cpp里用了同步ReadFile,未设超时 | 检查SetCommTimeouts()是否调用,ReadTotalTimeoutConstant是否>0 |
| 收到EVENT但解析出错(如BD_ADDR全0) | 字节序未翻转,或pEvent->data索引越界 | 在ParseConnectionCompleteEvent()里加ASSERT(pEvent->data_len >= 12) |
| DevicePort方式打不开设备 | 权限不足或设备路径错误 | 以管理员身份运行,用WinObj工具确认BthPort设备对象是否存在 |
| Inquiry扫不到设备 | Inquiry Length设得太短,或远程设备不可见 | 将Inquiry命令第4字节从0x00改为0x0F(16.38秒),并确认对方蓝牙设为“可见” |
| 编译报错“unresolved external symbol _sprintf” | VC6默认不链接legacy_stdio_definitions.lib | Project → Settings → Link页,在Object/Library Modules里添加legacy_stdio_definitions.lib |
5.2 那些年踩过的坑:独家避坑技巧
坑一:HCI命令重发机制缺失导致连接失败
HCI规范要求,若发送CMD后未收到EVENT,需重发(最多3次)。但原始代码没实现重发逻辑。我遇到过某款RTL8723BE芯片,在高负载时偶尔丢失EVENT。解决方案是在BT_HCI.cpp里加一个重发计数器:
if (m_cmdRetryCount < 3 && m_cmdStatus == STATE_CMD_SENT && GetTickCount() - m_cmdSentTime > 2000) {
SendCommand(m_lastCmd, m_lastCmdLen); // 重发
m_cmdRetryCount++;
}
注意:重发间隔必须>2秒,否则违反HCI规范。
坑二:ACL数据包长度计算错误
ACL包头占4字节(Handle+Flags+Length),但代码里常把Length字段当成总长度。正确算法是:total_length = 4 + pAcl->length。我曾因此导致音频传输断续,因为接收方按错误长度截断数据。
坑三:MFC资源ID冲突引发UI错乱
VC6的.rc文件里,如果两个控件用了相同ID(如都用IDC_EDIT1),运行时可能某个控件消失。解决方案:右键资源视图 → “Resource Symbols”,检查所有ID是否唯一。我建议给HCI相关控件加前缀,如IDC_HCI_CMD_INPUT、IDC_HCI_EVENT_LIST。
坑四:Release模式下串口通信异常
VC6 Release版默认开启优化(/O2),可能导致SerialPort.cpp里的volatile变量失效。比如m_bReading标志位被编译器优化掉。解决方法:在Project → Settings → C/C++页,Optimizations选“Disable (Debug)”或在关键变量前加volatile修饰。
5.3 二次开发扩展建议:让这套代码活起来
- 增加BLE支持:在BT_HCI.cpp里新增SendLESetScanParameters()等LE命令,需修改HCI包解析逻辑,因为LE EVENT的Event Code范围是0x3E(LE Meta Event)。
- 导出Wireshark插件:将收到的HCI包按pcap格式写入文件,用tshark -r log.pcap -T json解析,无缝接入现有分析流程。
- 集成AT指令透传:在SerialPort.cpp里加一个AT模式开关,当检测到”AT+”开头的数据,直接透传给蓝牙模块,绕过HCI解析——这对调试AT固件极有用。
- 添加日志持久化:在OnHciEvent()里调用WritePrivateProfileString(),把每次HCI交互存到BluetoothLog.ini,方便事后审计。
最后分享个小技巧:调试时把Bluetooth HCI.ncb文件删掉,强制VC6重建浏览信息,能解决90%的“找不到符号”问题。这玩意儿就像浏览器缓存,有时候太“聪明”反而坏事。这套代码的价值,从来不在它多新潮,而在于它把HCI协议从纸面拉到桌面,让你的手指真正触碰到蓝牙通信的脉搏——每一次Send Command,都是对协议栈的一次叩问;每一次Parse Event,都是对无线世界的一次破译。
简介:这套代码专为Visual C++ 6.0环境设计,完整实现蓝牙主机控制器接口(HCI)协议的底层通信功能。核心逻辑封装在BT_HCI.cpp和Bluetooth HCI.cpp中,配合SerialPort.cpp(串口通信)与DevicePort.cpp(USB/PCI等设备端口通信),可直接对接各类蓝牙适配器。整个工程基于标准MFC对话框框架构建,包含Bluetooth HCIDlg.cpp/h主界面、资源文件(.rc/.ico)、项目配置(.vcproj/.sln)及调试辅助文件(.ncb/.suo),开箱即用。所有模块均使用纯C++编写,仅依赖Windows API和基础MFC库,不引入任何第三方SDK,适合学习HCI命令发送、事件响应、ACL/SCO数据包解析等关键流程。配套有bluetooth_hci_simulator.py用于模拟测试,ReadMe.txt提供快速上手指引,BasePort.cpp抽象出通用端口操作接口,便于扩展其他硬件通道。代码结构清晰、注释到位,适合Windows平台下的蓝牙协议栈验证、嵌入式蓝牙模块调试或教学级二次开发。


被折叠的 条评论
为什么被折叠?



