前言:
蓝牙产品在实际落地中,很多时候采用host+controller的通信模型,其中host负责实现协议栈profile是运行在主控cpu上的。controller为另外一颗单独的蓝牙芯片,负责蓝牙link layer的处理,两个芯片通过hci消息来交互数据。
hci定义了消息格式,但只有消息格式是不完整的,还需要定义在什么硬件总线上传输数据。协议规定hci消息可以在usb总线上传输,也可以在uart协议上传输,还可以在sdio协议上传输,这部分是hci的传输层定义的内容。
传输层HCI:
传输层,主机控制层接口,通过硬件接口uart/usb/sdio把host协议层的数据发送给controller层,并且接收controller层的数据。
usb总线传输:
由于USB总线除了VCC和GND之外,数据总线只有两根,所以在蓝牙里面,也将传输HCI消息的USB总线叫做H2协议。
uart总线传输:
H4:基于UART的传输,在tx和rx数据线上直接传输hci格式的数据,相对简单,但为了增加传输层流控,往往需要再增加cts和rts两根线,另外一个问题是由于它直接传输hci原始数据,没有任何的再次封装,因此当某一个数据出现错误时,即使是出现了一个错误,也可能导致后面的所有数据都无法被正确的解析出来(比如表示data length的字节出现了错误,因为它无法从错误的数据中解析出来正确的数据头),遇到这种情况,只能重启controller和host来解决问题了。
H5:基于UART的传输,也叫做3线uart协议,在tx和rx的数据线上传输经过slip封装的hci格式的消息,相对复杂,但是由于slip自身有重传的机制,也有错误检测的机制,因此它不需要cts/rts来实现流控,同时即使传输过程中某个数据位发生了错误,它也可以检测出来然后丢掉,不会影响后续数据的正确解析。
sdio总线传输:
sdio总线也可以传输hci指令,这种方式遇到的比较少,因为sdio的速率比较高,用在wifi的场景下比较合适,用在蓝牙这种低速率的场景下有点浪费了。
H5协议:
h5协议也叫做3线uart,使用slip协议对数据包进行封装,以保证数据的可靠性和完整性,我们分几个部分介绍slip协议的实现。在tx/rx数据线上的传输的数据,是hci消息经过slip格式封装的,如图1所示。“Slip Packet 1”可以表示一个hci命令,一个vent,一个acl数据,或和其他消息,“Slip Packet2”代表另外一个h5消息。
图1
Slip codec:
从图2中可以发现,单个slip包是以0xC0开始,并以0xC0结束的,host和controller在解析slip包时,是按照0xC0来分割数据,解析出一条一条单独的hci消息的。使用特殊字符来表示一段消息的起始和结束时,会涉及一个转义的过程,以slip协议为例,就是需要将原始数据中的0xC0转义为其他字符,以防被通信的对方错误的以为是起始符号和结束符号。
图2
slip的转义如下,原始数据中的0xC0数据,转义为0xDB 0xDC。同时0xDB转义为0xDB 0xDD。在数据发送的slip封包时,将hci数据中的0xC0字符进行转义然后在发送到tx总线上,同理在解析消息时,遇到0xDB后,需要将后面跟着的数据一起转义为相应的字符。
图3
Slip header:
如图1所示,slip的header共4个字节。通过H5传输层发送的每个数据包都会有header字段,需要说明的是传输层不支持分包与重组。
Seq Number:共3bit,取值范围为0~7,表示数据包的序号从0开始,每次+1,当到达7之后,下次数据包的seq number重新归0。需要说明的是如果是重传包,seq number值不变。另外如果是unreliable packet,则seq number字段为0,并且在解析时会被忽略该字段。
Ack Number:共3bit,取值范围为0~7,表示期望收到的对方的数据包的seq number,比如host已经收到了seq number为1的controller上报的hci消息,则host会回复ack为2的消息给controller。ack number有两层作用,第一层表示你发的seq number为1的消息我收到了,第二层意思表示host期待接收controller下一个seq number为2的消息。seq和ack的机制是实现流控和重传的核心思想,后面会详细介绍。
Data Integrity Check Present:1bit,表示本slip数据包是否有数据完整性校验字段(Data Integrity Check)。
Reliable Packet:1bit,表示本slip数据包是否为Reliable包,详细内容后面介绍。
Packet Type:4字节,表示hci消息的包类型,Packet Type为1,2,3,4和5是ble比较常见的类型,其他是H5特有的消息类型,Acknowledgment packets表示是一个单纯的ack,也叫做pure ack,该消息只包含ack number字段,不包含有效的seq number和数据。Link Control Packet指的是sync,sync rsp,config和config rsp等消息,他们是H5协议在Link Establishment时用到的。
图4
Payload Length:表示图二中Payload的数据总长度(总字节数),不包括Header字段,不包括可选的Data Intergrity Check字段。
Header Checksum:1个字节,用来校验header数据的正确性。
Data Intergrity Check:
数据完整性检查字段是可选的。它可用于确保数据包有效。数据完整性检查字段附加在数据包的末尾。Packet Header和Payload的每个字节均用于计算数据完整性检查。
Reliable Packets:
带着有效数据的h5包,叫做可靠包。
Unreliable Packets:
图4中的Acknowledge packets type消息包,是unreliable packets,也叫做pure ack,目的是用于告诉对端,我收到了(但是我没有什么数据要发给你)。
Example Of Message Communication:
我们这里使用Sliding Window Size为1举例(Sliding Window Size的概念,在Link Establishment章节介绍)。
正常通讯:
如图5所示,#2,#4,#6是Host发给Controller的hci command,#3,#5,#7是Controller的hci event回复,例如Host收到Controller回复的event #7之后,暂时没有需要继续发送的hci command,因此回复了一包pure ack #8,表示已经收到了Controller的消息了(否则Controller会一直重传#7包)。
图5
超时回复:
如图6所示,与正常通信类似,host在收到了Controller的hci event之后,启动了一个超时回复timer,在这个timer到达之后,如果仍然没有要发送的数据,则发送一个pure ack(ACK2),如果在timer未到达之前就有要发送的hci command,则发送command,取消掉该timer。
pure ack只有在没有要发送的数据时才会使用,它本身不带任何数据,host在发送#2,#4,#6时,也启动了超时回复timer,只是在timer到达时,发现存在要发送的hci command,则取消掉了timer,直接发送了command。
#2,#4,#6本身就充当了ack的作用,并且又携带了有效的hci command,在它们存在的时候,应该去避免发送pure ack,从而提高通讯效率。当然host端完全可以在#2,#4,#6之前也发送pure ack,比如在#2之前,发送一包#1.5的pure ack(ACK 7)。
在图6中,除了Host端启动了超时回复timer,另外一端Controller在发送了#7包后,也启动了重传timer,在重传timer到达之后,仍然未收到Host发送的消息时(类似hci command的#2,或者pure ack #8),Controller会发送“#7包”的重复包,也就是图6中的#9,但该图中由于Host发送了ACK2,所以#9被取消掉了。
Host端在处理H5协议实现时,会配置两个timer,一个用于回复ack,一个用于重传。Controller也是同样的实现逻辑,也会存在两个timer。这里面有两个点需要注意:一个是Host和Controller在配合实现H5协议时,需要双方拥有相同的超时时间(Host的超时回复timeout和Controller超时回复timeout相同,Host的重传timeout和Controller的重传timeout相同);另一个是重传的timeout应该比回复的timeout的数值大,否则的话#9就回出现在#8之前,多余了一包消息交互。
图6
超时重传:
重传机制是H5协议的核心之一,Host和Contoller是两个独立的芯片,他们之前是通过硬件总线相连接,因此就存在误码率的问题,当出现了错误数据位,根据H5的格式规范,这一包数据就会被另外一端丢失,因此特别依赖重传机制来恢复正常的工作,这个也是H5协议比H4协议有优势的地方。
重传情况1,如图7所示,Controller发给Host的#7包,由于其他硬件设备的偶尔干扰(或者其他原因),导致了出现了错误bit,被Host丢弃了,此时Host和Controller分别存在一个重传timer,它们的timeout相同,并且Host启动timer的时间要早于Controller(#6包比#7包要早,Host和Controller在发送完一包数据时,启动了重传timer),因此Host的重传timeout首先到期,然后它发了一包#8(#6的重传包),#8包被Controller正确解析,但是由于它SEQ为5,发现是Controller之前处理过的seq number消息,因此#8被丢弃的同时(#6包被Controller正确接收,只是Host并不知道)启动一个超时回复timer,准备回复ack,其他Controller的逻辑依然正常运行,在它的重传的timeout到达后,它发送了#9,同时取消掉了回复ack的timer,Host收到#9之后正确解析,发现是它期待收到的,并且消息里面的ACK Number也是符合预期的,结束掉继续重传的timer,回复pure ack,至此重传逻辑执行结束,双方开始恢复正常通信。
图7
重传情况2,如图8所示,host发送的#8包(pure ack)被意外损坏了,Controller没有成功解析出来这条消息,随后Controller在重传timer到达后,发送了#9包,host端在收到了#9包后,根据seq number判断出来是一个处理过的重复包,启动了超时回复timer,在timeout到达时回复了#10包,双方恢复正常通信,这里面有一个点,重传timer的timeout一定要比超时回复ack的timeout数值大,因此才能保证在#10发送之前不会出现Controller发送的#11,#13等等等。
图片8占位
图8
特殊处理:
刚刚提到过,应该尽量减少pure ack的发送,使用带数据的reliable packets来发送,但是也有特殊的情况,比如我们打开了扫描,并且Sliding Window Size为1,那么host在接收controller上报的adv report包时,就需要及时的回复ack,以实现可以快速的接收到后面的adv report,实现在单位时间内接收到更多的广播包,这时就可以使用pure ack来实现,每收到一包adv report,都快速回复一包pure ack,而不是等待超时回复的timeout到达后才回复pure ack。当然这种方式不是最好的方案,建议的最好方案还是配置Sliding Window Size。
撞包处理:
H5协议在实际工程落地时,会有遇到很多问题,比如最常见的就是Host和Controller撞包的问题。比如当host配置controller为扫描状态,controller会不断的上报adv report,这个时候,host又发送了一包控制命令给controller,这时就存在撞包的可能,如图9。从图中可以发现#7和#8几乎是同时发送的,在Controller的角度来看,是先发送了#7,然后收到了#8,但是在host一端来看是相反的,它认为先发送了#8,然后收到了#7,这就是撞包的情况。处理撞包的逻辑时,核心思想是把同一个包中的seq和ack分开来处理。我们从Controller的角度来解释这次撞包的处理逻辑,Controller在发送了#7之后,启动了重传timer,然后收到了#8,解析#8包发现SEQ是正确的,但ACK不正确(因为ACK不正确,无法确定对方是否收到了#7包,因此没有取消掉重传timer),因此无法继续发送其他数据包(无法确定本包是否已被处理,因此重传任务还在),但由于seq是正确的,因此启动了超时回复ack的timer,由于重传timer大于超时回复ack的timer,所以超时回复ack的timer会优先就绪,然后Controller回复了pure ack,在host侧也是同样的逻辑,也会回复一包pure ack,后面双方发送的这两包里面的pure ack中的ACK是正确的,因此双方取消掉了各自的重传任务,撞包处理结束,恢复正常通信。
图片9占位
图9
Link Establishment:
图10
之前介绍的消息通信的事例,都是在Active状态下的正常通信,但是在双方进入Active状态之前,需要先经过其他几个状态,这几个状态是短暂的,但是也是必须的。
Uninitialized:
Host和Controller在上电之后所处的第一个状态,在此状态下,双方会100ms周期发送h5的sync包,同时在收到了对方的sync之后,回复sync response,双方在收到对方的sync response包之后,进入到initialized状态,这个过程是用来双方确定波特率等uart的配置信息的,如果双方不在同一个波特率状态,也无法收到彼此的sync包。sync和sync response同样符合h5格式,他们属于Link Control Packet,packet type = 0x0f。
sync消息:0x01,0x7E。
sync response消息:0x02,0x7D。
Initialized:
进入initialized之后,双方会周期发送h5的config包,同时在收到了对方的config之后,回复config response,双方在收到对方的config response包之后,进入到Active状态,该过程的主要目的是为了协商双方的Sliding Window Size。但是如果收到了上一个状态的sync包消息,也需要回复sync response消息。
config消息:0x03,0xfc,Configuration Field。
config response消息:0x04,0x7B,Configuration Field。
Configration Field包括:Sliding Window Size,OOF Flow Control,Data Integrity Check Type,Version Number。
Sliding Window Size表示本端(Host或者Controller)在收到对端的ack之前,可以发送多少个数据包,这个参数代表该端的处理能力,在config阶段双方交互彼此的Sliding Window Size,然后双方会以最小的Sliding Window Size一方为协商的值,以此进行通信(比如host的Sliding Window Size为2,Controller的Sliding Window Size为3,则使用Sliding Window Size为2作为最终的双方通信配置参数)。
Active:
进入到Active状态之后,双方设备就开始正常通信了,在该状态下,如果收到了对端的config消息,需要回复config response消息,如果收到了对端的sync消息,则证明对端已经重启了,则本端也进行重置操作,重新进入Uninitialized状态。
Sliding Window Size:
本端设备在未收到对端设备的ack之前,可以发送的最大消息包数量(不包括重传消息)。如图11为Slidling Window Size为2的消息交互模型,可以看看到左侧的通信方连续发送了SEQ1和SEQ2的消息。
图11
Recommand Parameters:
slip协议中,时序参数的理论建议值:
Acknowledgment of packets:
ack_timeout = 2 * Tmax。
Resending reliable packets:
resend_timeout = 3 * Tmax。
Tmax的计算方式:
Tmax的计算方式如下图,实际开发中,BLE的单包的最大长度不会是4095,我们以256作为最大值进行计算,Tmax的值为2.7ms。
工程实现的建议值:
resend_timeout的值为3*2.7ms,ack_timeout为2*2.7ms,但是实际工程师,这样的配置会有些太苛刻了,因为代码运行和不同的代码架构也会引入时间开销,某些极端场景,可能host还没有处理完上一包的消息呢,controller的resend_timeout已经到了,已经开始重传了,我的经验值是resend_timeout为50ms,ack_timeout为10ms,但并不适用所有平台,希望大家遇到这个问题时可以根据实际情况调整这两个参数。
Disadvantages and deficiencies:
H5协议有很多优点,但是也需要付出很多代价,带来的开销如下:
uart会变得更繁忙,相比h4协议,h5需要回复ack,因此会使得双方的通信数据变多。
开发周期更长,h5协议在实现时涉及很多线程同步的逻辑,处理好需要比h4更多的时间。
实现h5协议会占用更多主控的cpu资源和内存资源。
Suggestion:
H5协议本身并不复杂,但是实现起来需要处理好很多细节,才能开发出稳定运行的代码,本人建议关注的细节有以下几点:
resend timeout一定要比ack timeout大,并且需要根据实际项目的运行情况来配置。
撞包,重传的部分一定要把逻辑捋清楚,controller和host一定要统一逻辑。
多线程同步一定要做好,以host的h5代码逻辑实现为例,重传任务,超时回复任务,数据正常发送任务,都会操作uart的tx,如果互斥处理的不好,就会出现异常情况,比如发送了不该发送的pure ack,会给对端的Controller数据包解析造成影响,本人建议只有一个线程可以操作uart tx,其他任务想要操作tx时,向该任务发送消息,这样子互斥问题就没有了,该线程统一管理uart tx资源,uart rx资源同样的逻辑。