2. MTK8676蓝牙协议栈架构解析

好,咱们直接进入正题。这一章我带你看看MTK8676的蓝牙协议栈到底长什么样。说实话,我第一次接触MTK的协议栈时,也被它的分层结构搞得有点晕。但摸清楚之后你会发现,其实套路很清晰。

2.1 蓝牙协议栈分层结构

标准的蓝牙协议栈,从上到下大概是这样:

  • 应用层:电话、电话本同步、音频控制等
  • SDP:服务发现协议,找对方有什么服务
  • RFCOMM:串口仿真,电话本同步全靠它
  • L2CAP:逻辑链路控制与适配,数据分包重组
  • HCI:主机控制器接口,软件和硬件的分界线
  • Link Manager / Link Controller:底层链路管理
  • RF / Baseband:射频和基带,硬件干活的地方

嗯,这里要注意。MTK8676虽然是颗SoC,但它的蓝牙部分依然遵循这个分层。只不过MTK把HCI以下的层都固化在硬件里了,我们开发者主要跟HCI以上的层打交道。

核心要点:HCI是分水岭。HCI以下叫「控制器」,HCI以上叫「主机」。MTK8676里,控制器是硬件IP,主机跑在ARM核上。

2.2 各层协议详解

2.2.1 HCI层

HCI说白了就是命令通道。主机发命令给控制器,控制器回事件给主机。数据走另外的ACL/SCO通道。

我个人习惯把HCI命令分成三类:

  • 配置类:设置蓝牙地址、设备名、扫描参数
  • 控制类:发起连接、断开连接、配对
  • 数据类:ACL数据收发、SCO语音数据

我在项目中遇到过一个问题:HCI命令的响应超时。MTK的控制器有时候处理慢,默认超时5秒不够用。我后来把超时改成了10秒,世界清净了。

2.2.2 L2CAP层

L2CAP负责把上层的数据切成适合底层传输的包。它有几个关键参数:

参数说明典型值
MTU最大传输单元672字节(RFCOMM常用)
MPS最大分段大小48或64字节
Flush Timeout数据刷新超时0xFFFF(无限重传)

你想想看,如果MTU设得太小,电话本同步时数据包频繁拆分,效率极低。我建议电话本同步场景下MTU至少设到1000以上。

2.2.3 RFCOMM层

RFCOMM模拟串口。电话本同步(PBAP)就是跑在RFCOMM上的。每个RFCOMM会话对应一个虚拟串口通道,通道号从1到30。

这里有个坑:RFCOMM的流控。我曾经因为没处理好流控,导致电话本同步时数据丢失。后来加了CTS/RTS硬件流控模拟,问题才解决。

2.2.4 SDP层

SDP用来发现对方支持什么服务。比如手机支持PBAP吗?支持HFP吗?

SDP记录里包含:

  • 服务类ID(UUID)
  • 协议描述符(告诉你怎么连)
  • 服务名称
  • 支持的特性(比如PBAP的版本号)

小技巧:调试时可以用SDP查询工具看看手机到底暴露了哪些服务。我经常用这个来确认手机端配置是否正确。

2.3 MTK专有协议扩展

MTK在标准协议之上加了一些私货。这些扩展主要为了解决两个问题:

  1. 性能优化:标准协议在某些场景下效率不够
  2. 功能增强:MTK芯片特有的硬件能力需要软件配合

常见的MTK扩展包括:

  • MTK_EXT_HCI:扩展HCI命令,用于控制硬件加速器
  • MTK_FAST_ACL:快速ACL连接,减少配对时间
  • MTK_PBAP_EXT:电话本同步的加速传输模式

我记得有一次客户要求电话本同步必须在3秒内完成。标准PBAP根本做不到,后来启用了MTK的PBAP_EXT扩展,把传输速度提升了3倍,才勉强达标。

警告:MTK扩展不是标准蓝牙规范的一部分。如果你的产品需要过BQB认证,记得把这些扩展关掉,或者确保它们不影响标准协议行为。

2.4 协议栈初始化流程

初始化流程我总结为「三步走」:

  1. 硬件初始化:上电、时钟、复位、下载固件
  2. 协议栈启动:HCI重置、读取版本、配置参数
  3. 服务注册:注册SDP记录、打开RFCOMM通道

代码层面大概是这样:

// 伪代码,实际MTK API略有不同
void bt_stack_init(void) {
    // 第一步:硬件初始化
    mtk_bt_hw_init();           // 上电、时钟
    mtk_bt_fw_download();       // 下载固件到蓝牙核
    
    // 第二步:协议栈启动
    hci_reset();                // 重置控制器
    hci_read_local_version();   // 读取版本信息
    hci_set_event_mask();       // 设置感兴趣的事件
    
    // 第三步:服务注册
    sdp_register_service(PBAP_UUID, pbap_sdp_record);
    rfcomm_register_channel(PBAP_CHANNEL, pbap_callback);
    
    // 启动完成
    printf("蓝牙协议栈初始化完成\n");
}

嗯,这里要注意顺序。我曾经试过先注册服务再初始化硬件,结果回调函数被空指针调用了,直接死机。所以顺序一定不能乱。

2.5 关键数据结构分析

协议栈里最核心的数据结构就这几个:

2.5.1 HCI命令缓冲区

typedef struct {
    uint16_t opcode;      // 操作码
    uint8_t  param_len;   // 参数长度
    uint8_t  params[255]; // 参数数据
} hci_cmd_t;

这个结构体虽然简单,但坑不少。opcode的高字节是OGF(操作组字段),低字节是OCF(操作码字段)。我刚开始时老是把这两个搞反,发出去的命令控制器根本不认。

2.5.2 ACL数据包

typedef struct {
    uint16_t handle;      // 连接句柄
    uint8_t  pb_flag;     // 包边界标志
    uint8_t  bc_flag;     // 广播标志
    uint16_t data_len;    // 数据长度
    uint8_t  data[0];     // 灵活数组成员
} acl_packet_t;

pb_flag特别重要。第一个包设为0x00表示起始包,后续包设为0x01表示继续包。L2CAP重组时就靠这个标志来判断包的边界。

2.5.3 SDP记录结构

typedef struct {
    uint16_t  uuid;           // 服务UUID
    uint8_t   *name;          // 服务名称
    uint8_t   rfcomm_channel; // RFCOMM通道号
    uint16_t  profile_version;// 配置文件版本
    uint32_t  features;       // 支持的特性位图
} sdp_record_t;

features字段是个位图,每一位代表一个特性。比如PBAP的v1.2支持「下载电话本」和「下载通话记录」两个特性,分别对应bit0和bit1。

经验之谈:调试SDP时,我建议先把features设成0xFFFFFFFF,看看对方能识别哪些。然后再逐步缩小范围,找到最合适的配置。

2.6 小结

这一章我们走了一遍MTK8676的蓝牙协议栈。从HCI到SDP,从标准协议到MTK扩展,再到初始化和数据结构。说白了,协议栈就是个层层封装的过程。每一层只关心自己的事,把复杂留给下层,把简单留给上层。

下一章我会带你实战电话本同步的代码实现。到时候你会看到这些数据结构是怎么在代码里活起来的。