L2cap层是连接hci和上层profile的中转站,我们之前分析包格式的时候就说过,payload header中的llid如果标示是acl-u的话,说明就是个l2cap包。
上层profile在连接的时候,都需要先建立l2cap逻辑链路,每个逻辑链路分配cid(channel id),这也是l2cap最重要的功能:协议/信道多路复用
然后比较重要的是,l2cap提供分包和重组功能,比如说上层的包比较大,controller支持的包比较小,就有可能需要分包了,我们前文说过,payload header中的llid含有start和continue信息,这个信息哪来的呢,看一下hci acl data的格式
在handle中有两位pb flag就是用来标志这个事情的。
我们看一下btstack中的代码对于pb flag的处理:
在static void acl_handler(uint8_t *packet, int size)函数中
case 0x02: { // first fragment
// sanity check
if (conn->acl_recombination_pos) {
log_error( "ACL First Fragment but data in buffer for handle 0x%02x, dropping stale fragments", con_handle);
conn->acl_recombination_pos = 0;
}
// peek into L2CAP packet!
uint16_t l2cap_length = READ_L2CAP_LENGTH( packet );
// log_info( "ACL First Fragment: acl_len %u, l2cap_len %u", acl_length, l2cap_length);
// compare fragment size to L2CAP packet size
if (acl_length >= (l2cap_length + 4)){
// forward fragment as L2CAP packet
hci_emit_acl_packet(packet, acl_length + 4);
} else {
if (acl_length > HCI_ACL_BUFFER_SIZE){
log_error( "ACL First Fragment to large: fragment %u > buffer size %u for handle 0x%02x",
4 + acl_length, 4 + HCI_ACL_BUFFER_SIZE, con_handle);
return;
}
// store first fragment and tweak acl length for complete package
(void)memcpy(&conn->acl_recombination_buffer[HCI_INCOMING_PRE_BUFFER_SIZE],
packet, acl_length + 4);
conn->acl_recombination_pos = acl_length + 4;
conn->acl_recombination_length = l2cap_length;
little_endian_store_16(conn->acl_recombination_buffer, HCI_INCOMING_PRE_BUFFER_SIZE + 2, l2cap_length +4);
}
break;
}
如果是第一个分包,则红色字体将数据暂存到acl_recombination_buffer中去,然后、
case 0x01: // continuation fragment
// sanity checks
if (conn->acl_recombination_pos == 0) {
log_error( "ACL Cont Fragment but no first fragment for handle 0x%02x", con_handle);
return;
}
if ((conn->acl_recombination_pos + acl_length) > (4 + HCI_ACL_BUFFER_SIZE)){
log_error( "ACL Cont Fragment to large: combined packet %u > buffer size %u for handle 0x%02x",
conn->acl_recombination_pos + acl_length, 4 + HCI_ACL_BUFFER_SIZE, con_handle);
conn->acl_recombination_pos = 0;
return;
}
// append fragment payload (header already stored)
(void)memcpy(&conn->acl_recombination_buffer[HCI_INCOMING_PRE_BUFFER_SIZE + conn->acl_recombination_pos],
&packet[4], acl_length);
conn->acl_recombination_pos += acl_length;
// log_error( "ACL Cont Fragment: acl_len %u, combined_len %u, l2cap_len %u", acl_length,
// conn->acl_recombination_pos, conn->acl_recombination_length);
// forward complete L2CAP packet if complete.
if (conn->acl_recombination_pos >= (conn->acl_recombination_length + 4 + 4)){ // pos already incl. ACL header
hci_emit_acl_packet(&conn->acl_recombination_buffer[HCI_INCOMING_PRE_BUFFER_SIZE], conn->acl_recombination_pos);
// reset recombination buffer
conn->acl_recombination_length = 0;
conn->acl_recombination_pos = 0;
}
break;
如果收到continue fragment,则要比对一下数据总长度对不对,如果不对是要丢弃的,如果对的话,则和第一个包合并起来,并hci_emit_acl_packet提交给上层。
总的来说,l2cap的分包并不复杂,不像tcp协议有可能有好多个包需要合并,而且顺序打乱,l2cap的分包就是连续的两包,如果出问题则直接丢弃的。
L2cap还有一些其他功能,比如上文提到过的流控、重传,不过基本不用,还有QOS管理等等。
但是最重要的两个功能,就是协议复用和分包重组这两项。
下面我们看一下l2cap的逻辑信道是怎么连接的。
要讲连接,首先讲一下l2cap的cid。
上图可以看出,如果是有连接的方式,上层l2cap实体间进行数据交互,也就是profile的数据交互需要建立l2cap连接后,互相绑定对方的cid。
如果是无连接的方式,则是向fixed channel——2号channel发送数据,无连接广播包是一类不需要建立l2cap连接的方式,类似于LE的adv包。
笔者曾经实现了无连接的音乐播放,这里面挑战还是蛮大的,因为不像a2dp这种有连接的方式,传输是不可靠的,这里面涉及到底层链路设计,host层丢包处理等等,有机会可以讲一下,不过现在涉及到公司机密不太好公开的。
所谓fixed channel,是指不同于上层profile的动态分配的cid,蓝牙core spec规定了以下fixed channel。
其实重要的也就是signal channel。
因为所有的连接都是始于signal channel的交互。
怎么理解?
看看我们的抓包好了:
我们试图建立avdtp的l2cap连接,首先我们发送l2cap connect req,这个req就是cid 01上发的,也就是signal id。在这条指令中,我们用psm表示了目前要建立的连接是avdtp,用source cid告诉对方我们这一端的cid是0x40,注意这个cid是动态分配的。
然后对方成功建立l2cap连接的回复,对方告诉我们,destination cid是0x4d,也就是对方给到我们的cid是0x4d。
然后需要进行一个配置的工作,l2cap可以配置的选项有:
这里面最重要的是MTU,最大的包长。一般也就配置这一项就可以了。
比如我们这次就告诉对方我们能支持的最大包长是672字节。
这里面进行了几次的交互,直到双方协商一致为止。
看一下btstack的代码,l2cap_signaling_handle_configure_request函数中有这么一段:
if ((option_type == L2CAP_CONFIG_OPTION_TYPE_MAX_TRANSMISSION_UNIT) && (length == 2)){
channel->remote_mtu = little_endian_read_16(command, pos);
log_info("Remote MTU %u", channel->remote_mtu);
if (channel->remote_mtu > l2cap_max_mtu()){
log_info("Remote MTU %u larger than outgoing buffer, only using MTU = %u", channel->remote_mtu, l2cap_max_mtu());
channel->remote_mtu = l2cap_max_mtu();
}
channelStateVarSetFlag(channel, L2CAP_CHANNEL_STATE_VAR_SEND_CONF_RSP_MTU);
}
最终会在remote 的mtu和本地的mtu中找到一个最小值作为最终的mtu的。
配置过程完成后,就可以正式在双方的绑定的cid上通信了。
也就是————更上层的profile开始工作了。