蓝牙开发那些事之聊一聊hcilog

对于蓝牙开发者和硬件工程师而言,掌握HCI日志分析技术就如同医生学会看X光片一样关键。HCI(Host Controller Interface)日志是蓝牙协议栈的”黑匣子”,记录了从应用层到射频底层的所有通信细节。

无论是解决偶发断连还是优化功耗,掌握这项技能都能让你从”凭经验猜”升级到”靠数据说话”的专家段位。

例如当BLE连接频繁断开时,通过HCI_Disconnection_Complete事件能直接看到错误码是0x3E(超时)还是0x08(加密失败)。

若日志出现”HCI_LE_Enhanced_Connection_Complete + Error 0x0D”:
表示对方设备拒绝连接,常见于蓝牙5.0设备尝试连接4.2设备时PHY不匹配

若发现HCI_Number_Of_Completed_Packets事件间隔忽大忽小,说明蓝牙射频被Wi-Fi或其他2.4G设备干扰,需调整AFH信道映射

若持续出现HCI_LE_Advertising_Report事件但无实际广播数据:
提示应用层未正确关闭扫描,导致射频持续耗电

HCI层的快递帝国——每个数据包都是加急件

如果把蓝牙协议栈比作一家跨国物流公司,那‌HCI层就是中转站的智能分拣机器人‌。当你按下手机上的”连接”按钮,这场史诗级物流大戏就开场了:

‌经典场景重现‌:

手机端Host协议栈(总调度中心)掏出小本本写下:”给耳机发个心跳包”。

快递小哥HCI抄起ACL数据包(蓝牙界的顺丰快递袋)就往Controller(基层网点)狂奔

Controller的射频小哥(最后一公里配送员)对着2.4GHz频段疯狂喊麦:”AirPods Pro你在吗?!”

某次调试现场实录:Host说发10个包,Controller只收到9个,双方在HCI层上演《消失的快递》悬疑剧。

想要偷看蓝牙设备“私聊”? 在快递站偷偷拆开快递袋就行了哦。

整整3000多页的core spec,关于HCI的,一共就有1000来页:

图4-29 core-spec关于hci的章节

大量的数据格式、标准,如果没玩过蓝牙的人,会看得云里雾里。

所以我希望换一个视角,我们从数据的角度出发,从代码的角度出发,去看一次经典蓝牙连接的全过程,可能结合实例去理解,对于初学者来说更容易一点。

这里的数据是指的HCILOG,一般来说研究通信,空中包很重要,但是抓包设备贵啊,而且hcilog一般情况下也就够用了,为什么呢?看一下架构图就明白了:

图4-30 双芯片架构

这个架构图是一个双芯片的架构,其中的HCI在嵌入式的场景下大概率是UART,在PC上就可能是个USB,比如说你的电脑不支持蓝牙,那么,在你的电脑的USB接口上插上一个usb的蓝牙dongle,再配合一个类似IVT的Bluesoleil或者其他的软件,你的电脑就支持蓝牙了。这个Bluesoleil就相当于host协议栈。

现在,这种架构慢慢变少了,把HOST和CONTROLLER放到一起,做成一颗芯片的场景越来越多,这个时候,HCI变成了一个虚拟层,一个纯粹软件意义的东西。

在btstack中其实也可以看到这样的单芯片架构,其传输层就是虚拟的:

图4-31 btstack传输层结构体

比如这个sx1280的芯片,就不是一个传统意义上的蓝牙controller芯片,而是这样的一个东西:

‌SX1280芯片可以支持蓝牙协议‌。SX1280是Semtech公司推出的一款高性能无线收发器,主要应用于2.4GHz频段的通信。该芯片不仅支持多种调制方式,包括LoRa、FLRC和FSK调制技术,还具备兼容蓝牙协议的能力‌12。这使得SX1280在物联网应用中表现出色,适用于各种需要长距离通信和高可靠性的场景,如智能家居、安全系统、定位追踪、穿戴设备、智能手环与健康管理等‌

sx1280的controller层收到btstack的host层的数据后,就是通过以下流程最后给到RF发送出去的。

图4-32 sx1280的数据下发路径

蓝牙协议栈被hci分成了Host和Controller两部分,狭义上的协议栈是指的Host, 其实Controller也是有协议栈的,包括LC、LMP等等。

Controller就是蓝牙模块,最终会操作到射频的硬件寄存器等部分,还包括LMP、LC层等偏底层的协议,这个一般芯片原厂提供的模块都封装好了的。

所以Host中上层profile所需要发送或者接收的包都是通过HCI接口和controller交互,你可以把hci理解为快递的自提点,Host需要发送的包放到自提点,剩下的controller什么时候发,controller会根据一个链路策略在合适的时机发送给peer端,同样的,controller收到peer端的包裹后也会通过hci接口发给Host。(HCI ACL data包)

所以空中包大多数通过hci接口都是能查看到的,除了少部分LMP包看不到,但是关系不大,后面会讲到。

很多开发android 手机端蓝牙app的同学可能已经知道怎么抓手机端的hcilog了,手机端的这个log,是有其固定格式的,叫做btsnoop.log,这个log的取得方式,根据手机型号的不同,方法也不同,大多数需要打开开发者模式等等。Like this:

图4-33 android手机端打开hcilog

不同手机平台存放这个log的路径也会不一样,详情请读者结合自己的手机型号进行百度。

对于我们协议开发者来说,手机端的hcilog我们不是那么关心,更关心的是设备端的hcilog。HCILOG的取得还是比较容易的,一般来说host协议栈会为你提供这样的接口,因为这个东西,对分析蓝牙的问题非常有用。

比如说linux使用的蓝牙协议栈是Bluez,Bluez提供了一套工具叫做hcitools,其中有若干命令,hcidump就是用来抓取hcilog的,使用方法很像tcpdump,参数都一模一样,-w是保存到文件,-t是加时间戳,-i是指定接口等等。

linux抓包宝典

# 启动抓包咒语(需要root权限)

sudo hcidump -i hci0 -w 私密日记.pcap



# 查看实时流量(建议配合降压药使用)

sudo hcidump -i hci0 -X

而Btstack也是直接可以抓取hcilog的,运行BTStack程序时,会生成hci_dump.pklg文件,可以使用WireShark打开此文件。

具体的用法可以在btstack的代码里全局搜索hci_dump,如下图

图4-34 btstack抓取hcilog

这里提到了,对于台式机这样的支持文件系统的系统来说,btstack可以直接保存hcilog到文件。

对于嵌入式设备来说,btstack可以通过uart或者segger rtt打印出来(建议实现segger rtt,因为串口还是挺慢的)。

从下图来看,支持的文件格式也包含了pklg和snoop格式。

图4-35 btstack支持两种格式的hcilog

我们在电脑上插一个蓝牙dongle,然后跑一下btstack,生成的hci_dump.pklg就可以导入wireshark打开了,打开后如图所示。

图4-36 wireshark打开hcilog

Ellisys这个蓝牙抓包界的”老司机”,除了常规的空中抓包外,还偷偷藏了一手绝活——hciinjection。这货就像个”双面间谍”,一边在UDP端口上蹲点监听(默认是127.0.0.1:24352),一边把HCI层的秘密情报实时传送回大本营。

图4-37 实时hcilog抓取

传统抓包像拍照片,hciinjection则是全程直播。当你的蓝牙设备突然抽风时,不用像没带手机上厕所那样懊悔”刚才怎么没抓包”。空中包和协议栈日志终于能像恩爱夫妻一样手牵手出现在同一时间轴上了,再也不用玩”大家来找茬”。

不方便的地方是,大多数嵌入式设备就像不会说话的树懒,连个网卡都没有,无法直接通过udp发送数据。一般需要现在嵌入式设备的实现一个将hci数据流通过串口发送给pc,然后pc上还需要实现一个程序,将接收到的串口数据通过udp转发给ellisys,比较麻烦。

当你终于搞定这套系统时,效果好比给近视眼戴上VR眼镜,那些偶现的bug终于被扒得底裤都不剩。

图4-38 实时hcilog抓取图

关于hcilog的抓取,就这么多。

路漫漫其修远兮,我们这就上路吧。

HCI指令的摩尔斯电码——从01030c00到宇宙大爆炸

图4-39 hci reset command

大部分开发过蓝牙底层的同学,看到这个“01 03 0c 00”的指令,都会虎躯一震——这是蓝牙世界的”芝麻开门”咒语。

因为这是上电后,Host向controller发出的第一条指令。

所有的蓝牙模块在上电或者需要复位的时候都会收到来自host的这条命令,它开辟鸿蒙,点亮人生。

虽然wireshark已经清晰地把这条命令解析了出来:

第一个字节01表示packet type: HCI Command

第二三字节是03 0c表示opcode: Reset

第四个字节表示parameter长度是0

/* 这条Reset指令的C语言版前世今生 */

struct hci_command {

    uint8_t type;       // 0x01表示我是老大

    uint16_t opcode;    // 0x0c03翻译成人类语就是"给我重启!"

    uint8_t plen;       // 参数长度0,意思是不带任何条件

};

我们还是去core spec里去寻找一下这条命令。

‌Core Spec生存指南‌:

打开3000页PDF时请备好红牛+眼药水

按Ctrl+F搜索”HCI_Reset”可能触发量子纠缠

看到”shall”这个词要高度警惕——这是协议作者给你挖的坑

(某工程师的Core Spec书签:第576页有咖啡渍,第1024页夹着辞职信)

首先,我们要了解,hci接口流动的数据一共有四种类型:command(0x01)、acl(0x02)、sco(0x03)和event(0x04)。

这4种数据类型,用一个头部信息来表示,参考bluetooth.h:

#define HCI_COMMAND_DATA_PACKET 0x01

#define HCI_ACL_DATA_PACKET       0x02

#define HCI_SCO_DATA_PACKET       0x03

#define HCI_EVENT_PACKET           0x04

这里01开头的数据,显然是一条命令。

命令的格式是这样的:

图4-40 hci command格式

其中opcode(操作码)由ogf(opcode group field,6个bits)和ocf(opcode command field,10个bits)组成,虽然第三个字节的低两位是OCF的高两位,但是因为这两个字节一般是0,所以一般我们的换算方法就很简单了,OCF就是第二个字节,OGF是第三个字节右移两位

所以这里的OCF是0x03,OGF是0xc右移两位,也是0x03

于是我们很容易就可以在core spec找到这条命令:

图4-41 core-spec中的reset command

下面,我们看看btstack是如何发出这一条hci command的。

我们知道协议栈的每层一般都有一个状态机的,btstack也不例外的。

Hci层的核心是一个结构体和一个函数

一个结构体是static hci_stack_t * hci_stack

图4-42 hci_stack结构体

hci_stack->state表示“状态”,hci_stack->substate表示“子状态”。为什么需要状态和子状态,接下来的初始化的过程就是个很好的例子。

一个函数是

hci_run

图4-43 hci_run函数

它就是根据hci_stack结构体中的这些状态、其他值来收发数据的。

所以,在刚刚上电的时候,状态是HCI_STATE_INITIALIZING ,hci_run会跑到如下代码块

switch (hci_stack->state){



        case HCI_STATE_INITIALIZING:



            hci_initializing_run();



            break;

看看hci_initializing_run函数,这又是一个状态机了,其中的状态就是上文提到的初始化的子状态,看看定义子状态的枚举定义,就知道其实初始化就并不简单的,涉及到一系列的命令、事件交互

蓝牙状态机的修仙之路——从筑基到渡劫

蓝牙协议栈的状态机堪比修真小说的升级体系:

// 状态机の九重天境界

typedef enum hci_init_state{

    HCI_INIT_SEND_RESET = 0,    // 筑基期:发个重启试试

    HCI_INIT_W4_SEND_RESET,    // 金丹期:等设备回个OK

    HCI_INIT_SEND_READ_LOCAL_VERSION_INFORMATION, // 元婴期:查查设备身份证

    HCI_INIT_W4_SEND_READ_LOCAL_VERSION_INFORMATION,

     // ...还有100多个隐藏关卡...

} hci_substate_t;

好在我们之前是抓过包的,看看我们这次抓包过程中,初始化都经历了哪些:

图4-44 初始化过程中的hcilog

看到没有,host和controller之间一个command,一个event,一问一答,一唱一和,还挺和谐的。一共16个命令,16个event,完成了这次的初始化进程。

好,闲篇不扯远,我们退回到hci_initializing_run这个子状态机:

图4-45 初始化状态机的状态切换

此时我们应该执行红色代码部分,首先把子状态变成HCI_INIT_W4_SEND_RESET,然后通过hci_send_cmd(&hci_reset);向controller下发命令

这里要着重讲一下的是流控,只要是传输层,没有流控肯定不行的,不可能host可以无限制往controller传数据,也不可能controller可以无限制向host传数据。

对于hci transport层的流控,是分成几种情况的。

我们今天遇到的是hci command,就只讲hci command的流控,以后遇到其他的再讲其他的。

一般来说,host向controller发送command之后,会收到controller的hci command complete event或者hci command status event,区别在于,hci command status event一般是对于那种异步的命令,但是会返回command status event给host表示已经开始执行command,当工作完成后会通知host一个相关的event

流控的关键点就在command completer event或者command status event中包含一个Num HCI Command Packete字节,用来标识controller目前可以接收的command数。

图4-46 command complete event格式

图4-47 command status event格式

对于大多数蓝牙模块来说,只能接收单命令,所以这个字节一般是1。当然也有一些controller比较强的,可以接收多个命令的。

在btstack中,还是做了简化的处理,只处理单命令的情况。

我们来看一下hci_send_cmd_va_arg这个函数,这个函数一开始先通过hci_can_send_command_packet_now判断一下能不能发送command:

图4-48 hci_send_cmd_va_arg函数

看一下hci_can_send_command_packet_now这个函数吧:

图4-49 hci_can_send_command_packet_now函数

首先判断一下传输层能不能用,这个是必须的,毕竟是需要用到物理介质去传输,人家如果没工夫搭理你也是不能强求的。

然后hci_stack->num_cmd_packets就是流控的概念了。

这里必须满足hci_stack->num_cmd_packets大于0的条件才可以发送command。

那么hci_stack->num_cmd_packets什么情况下会大于0呢?

搜索一下代码,逻辑还是比较清楚的。

在hci_send_cmd_packet这个函数的最后有这么一行代码:

hci_stack->num_cmd_packets- -;

图4-50 hci_send_packet函数

意思很明显,当host向controller发送一条command 的时候,host是处在一个“等”的状态,此时hci_stack->num_cmd_packet被置成0,(hci_stack->num_cmd_packets在btstack里简化了,取值只会是1或者0,所以hci_stack->num_cmd_packet- -的效果就是变成0了)

所以如果之后host还要发command的话是发不了的。

那么在什么时候才可以恢复呢?

就是在收到了command complete event或者command status event了以后:

图4-51 handle_command_complete_event函数

以上是command complete event的部分。

图4-52 handle_command_status_event函数

以上是command status的部分。

从注释也可以看到,为了降低复杂度,btstack是忽略了host num command packet位的,只支持单命令。

那如果controller抽风了,就是不想回command complete event或者command status event的话呢?

这就需要用到定时器了,看一下btstack的框图就知道定时器的地位有多重要。

图4-53 btstack协议架构

实际上任何协议栈都是需要定时器的,有一些需要定时执行的任务,还有一些超时处理,超时了该丢弃的丢弃,该重传的重传,等等。

在我们当前的场景,假如command超时没有收到controller回复的话:

图4-54 超时处理函数

在超时handler里去把hci_stack->num_cmd_packets重新置位,这样又可以继续发命令了。

记录一次本法师修仙渡劫现场:

曾经响应客户需求,需要自定义一条非标准hci command,当时写完代码后总感觉蓝牙连接变慢了。本法师用尽浑身解数:

量子纠缠调试法:在printf处打桩后bug会转移

玄学改运术:给编译器上香后再make

统统没用。

后来抓了hcilog发现,在发完新支持的这条hci command后,因为忘记写对应的hci command complete event了,导致超时后,才能下发下一条命令。

某修仙…啊不,工程师渡劫成功后感言:”调通HCI比追到女神还难!”

所以Hcilog有的时候还真是挺有用的。

好,发完命令,我们接着看数据。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容