【Bluedroid】蓝牙HID Device virtual_cable_unplug全流程源码解析

发布于:2025-05-20 ⋅ 阅读:(14) ⋅ 点赞:(0)

本文基于Android蓝牙协议栈代码,深入解析HID设备执行虚拟电缆拔出(Virtual Cable Unplug, VCU)的核心流程,涵盖协议交互、状态管理、资源释放三大关键模块。重点剖析以下机制:

  1. 跨层级事件驱动的状态机设计

  2. 控制通道的优先级传输保障

  3. 协议栈资源的原子化释放策略

  4. 与电源管理子系统的协同工作模式

一、流程概述

1.1 VCU触发与协议栈传递

①应用层触发

应用调用virtual_cable_unplug(),触发断开流程,进行双重状态检查:

  • 应用注册状态(btif_hd_cb.app_registered

  • 服务启用状态(btif_hd_cb.status == BTIF_HD_ENABLED

②协议栈封装

BTA_HdVirtualCableUnplug():分配BT_HDR_RIGID结构体,封装BTA_HD_API_VC_UNPLUG_EVT事件,通过bta_sys_sendmsg投递至系统队列。

③事件路由

bta_hd_hdl_event():将事件路由至BTA_HD_CONN_ST状态,触发bta_hd_vc_unplug_act动作函数。

1.2 协议层处理

①协议报文构建

HID_DevVirtualCableUnplug()

  • 选择控制通道(HID_CHANNEL_CTRL

  • 设置消息类型为HID_TRANS_CONTROL

  • 参数标记为HID_PAR_CONTROL_VIRTUAL_CABLE_UNPLUG

②物理层传输

hidd_conn_send_data()

  • 拥塞检测:检查HID_CONN_FLAGS_CONGESTED标志

  • 缓冲区分配:使用控制通道专用512B缓冲区

  • 报文封装:构建仅含协议头的精简报文(长度=1字节)

  • 连接检查:若已断开则直接返回错误(不缓存VCU报文)

1.3 连接终止处理

①状态同步

  1. 设置bta_hd_cb.vc_unplug = TRUE,标记断开进行状态

  2. 通过bta_sys_busy()bta_sys_idle()快速完成电源状态切换

②主机响应

  1. 主机收到VCU后应在200ms内释放连接资源(参考HID 1.1规范)

  2. 设备端监测L2CAP通道关闭事件,触发BTA_HD_INT_CLOSE_EVT

二、源码解析

virtual_cable_unplug

packages/modules/Bluetooth/system/btif/src/btif_hd.cc
/*******************************************************************************
 *
 * Function         virtual_cable_unplug
 *
 * Description      Sends Virtual Cable Unplug to host
 *
 * Returns          bt_status_t
 *
 ******************************************************************************/
static bt_status_t virtual_cable_unplug(void) {
  log::verbose("");

  if (!btif_hd_cb.app_registered) {
    log::warn("application not yet registered");
    return BT_STATUS_NOT_READY;
  }

  if (btif_hd_cb.status != BTIF_HD_ENABLED) {
    log::warn("BT-HD not enabled, status={}", btif_hd_cb.status);
    return BT_STATUS_NOT_READY;
  }

  BTA_HdVirtualCableUnplug();

  return BT_STATUS_SUCCESS;
}

向主机发送 "虚拟电缆拔出"(Virtual Cable Unplug) 信号。这个信号用于模拟物理设备断开连接的情况,通知主机设备即将停止通信并进入低功耗状态。

BTA_HdVirtualCableUnplug

packages/modules/Bluetooth/system/bta/hd/bta_hd_api.cc
/*******************************************************************************
 *
 * Function         BTA_HdVirtualCableUnplug
 *
 * Description      This function is called when VCU shall be sent
 *
 * Returns          void
 *
 ******************************************************************************/
void BTA_HdVirtualCableUnplug(void) {
  log::verbose("");

  BT_HDR_RIGID* p_buf = (BT_HDR_RIGID*)osi_malloc(sizeof(BT_HDR_RIGID));
  p_buf->event = BTA_HD_API_VC_UNPLUG_EVT; // 表示这是一个虚拟电缆拔出事件

  bta_sys_sendmsg(p_buf);
}

通过消息队列机制向 HID 状态机发送异步事件,通知主机设备即将断开连接。

bta_hd_hdl_event(BTA_HD_API_VC_UNPLUG_EVT)

packages/modules/Bluetooth/system/bta/hd/bta_hd_api.cc
/*******************************************************************************
 *
 * Function         bta_hd_hdl_event
 *
 * Description      HID device main event handling function.
 *
 * Returns          void
 *
 ******************************************************************************/
bool bta_hd_hdl_event(const BT_HDR_RIGID* p_msg) {
  log::verbose("p_msg->event={}", p_msg->event);

  switch (p_msg->event) {
    case BTA_HD_API_ENABLE_EVT:
      bta_hd_api_enable((tBTA_HD_DATA*)p_msg);
      break;

    case BTA_HD_API_DISABLE_EVT:
      if (bta_hd_cb.state == BTA_HD_CONN_ST) {
        log::warn("host connected, disconnect before disabling");

        // unregister (and disconnect)
        bta_hd_cb.disable_w4_close = TRUE;
        bta_hd_better_state_machine(BTA_HD_API_UNREGISTER_APP_EVT,
                                    (tBTA_HD_DATA*)p_msg);
      } else {
        bta_hd_api_disable();
      }
      break;

    default:
      bta_hd_better_state_machine(p_msg->event, (tBTA_HD_DATA*)p_msg);
  }
  return (TRUE);
}

负责接收和分发来自上层应用或下层协议栈的各种事件。作为 HID 设备状态机的入口点,根据不同的事件类型执行相应的处理逻辑。

bta_hd_better_state_machine

    ...
    case BTA_HD_CONN_ST:
      switch (event) {
        ...
        case BTA_HD_API_VC_UNPLUG_EVT:
          bta_hd_vc_unplug_act();
          break;
          
       ...

bta_hd_vc_unplug_act

packages/modules/Bluetooth/system/bta/hd/bta_hd_act.cc
/*******************************************************************************
 *
 * Function         bta_hd_vc_unplug_act
 *
 * Description      Sends Virtual Cable Unplug
 *
 * Returns          void
 *
 ******************************************************************************/
void bta_hd_vc_unplug_act() {
  tHID_STATUS ret;

  log::verbose("");

  // 指示设备正在进行虚拟电缆拔出操作
  bta_hd_cb.vc_unplug = TRUE;

  ret = HID_DevVirtualCableUnplug();

  if (ret != HID_SUCCESS) {
    log::warn("HID_DevVirtualCableUnplug returned {}", ret);
  }

  /* trigger PM */
  bta_sys_busy(BTA_ID_HD, 1, bta_hd_cb.bd_addr);
  bta_sys_idle(BTA_ID_HD, 1, bta_hd_cb.bd_addr);
}

负责执行 "虚拟电缆拔出"(Virtual Cable Unplug, VCU) 操作。该操作通知主机设备即将断开连接,是实现设备优雅断开的关键步骤。还与电源管理系统协同工作,确保在断开过程中正确管理设备功耗。

HID_DevVirtualCableUnplug

/*******************************************************************************
 *
 * Function         HID_DevVirtualCableUnplug
 *
 * Description      Sends Virtual Cable Unplug
 *
 * Returns          tHID_STATUS
 *
 ******************************************************************************/
tHID_STATUS HID_DevVirtualCableUnplug(void) {
  log::verbose("");

  return hidd_conn_send_data(HID_CHANNEL_CTRL, HID_TRANS_CONTROL,
                             HID_PAR_CONTROL_VIRTUAL_CABLE_UNPLUG, 0, 0, NULL);
}

调用底层连接层函数,构建并发送 VCU 控制消息:

  • 通道选择:使用控制通道 (HID_CHANNEL_CTRL) 确保消息可靠传输

  • 消息类型:设置为控制传输 (HID_TRANS_CONTROL)

  • 参数值:使用HID_PAR_CONTROL_VIRTUAL_CABLE_UNPLUG表示 VCU 操作

VCU 命令是 HID 协议中定义的标准控制命令,用于模拟物理电缆断开的情况。当主机收到此命令后,应:

  1. 停止向设备发送任何命令

  2. 释放与该设备相关的所有资源

  3. 更新设备连接状态为 "断开"

hidd_conn_send_data

/packages/modules/Bluetooth/system/stack/hid/hidd_conn.cc
/*******************************************************************************
 *
 * Function         hidd_conn_send_data
 *
 * Description      Sends data to host
 *
 * Returns          tHID_STATUS
 *
 ******************************************************************************/
tHID_STATUS hidd_conn_send_data(uint8_t channel, uint8_t msg_type,
                                uint8_t param, uint8_t data, uint16_t len,
                                uint8_t* p_data) {
  BT_HDR* p_buf;
  uint8_t* p_out;
  uint16_t cid;
  uint16_t buf_size;

  log::verbose("channel({}), msg_type({}), len({})", channel, msg_type, len);

  tHID_CONN* p_hcon = &hd_cb.device.conn;

  // 1. 拥塞检查
  if (p_hcon->conn_flags & HID_CONN_FLAGS_CONGESTED) {
    log_counter_metrics(android::bluetooth::CodePathCounterKeyEnum::
                            HIDD_ERR_CONGESTED_AT_FLAG_CHECK,
                        1);
    return HID_ERR_CONGESTED;
  }

  // 2. CID 和缓冲区大小确定
  // 根据消息类型和通道确定 L2CAP 通道 ID (CID)
  // 为控制和中断通道分配不同的缓冲区大小
  switch (msg_type) {
    case HID_TRANS_HANDSHAKE:
    case HID_TRANS_CONTROL:
      cid = p_hcon->ctrl_cid;
      buf_size = HID_CONTROL_BUF_SIZE;
      break;
    case HID_TRANS_DATA:
      if (channel == HID_CHANNEL_CTRL) {
        cid = p_hcon->ctrl_cid;
        buf_size = HID_CONTROL_BUF_SIZE;
      } else {
        cid = p_hcon->intr_cid;
        buf_size = HID_INTERRUPT_BUF_SIZE;
      }
      break;
    default:
      log_counter_metrics(
          android::bluetooth::CodePathCounterKeyEnum::HIDD_ERR_INVALID_PARAM,
          1);
      return (HID_ERR_INVALID_PARAM);
  }

  // 3. 内存分配与数据封装
  p_buf = (BT_HDR*)osi_malloc(buf_size);
  if (p_buf == NULL) {
    log_counter_metrics(
        android::bluetooth::CodePathCounterKeyEnum::HIDD_ERR_NO_RESOURCES, 1);
    return (HID_ERR_NO_RESOURCES);
  }

  p_buf->offset = L2CAP_MIN_OFFSET;
  p_out = (uint8_t*)(p_buf + 1) + p_buf->offset;
  *p_out = HID_BUILD_HDR(msg_type, param); // 构建 HID 头
  p_out++;

  p_buf->len = 1;  // start with header only

  // add report id prefix only if non-zero (which is reserved)
  // 添加报告 ID(如果非零)
  if (msg_type == HID_TRANS_DATA && (data || param == HID_PAR_REP_TYPE_OTHER)) {
    *p_out = data;  // report_id
    p_out++;
    p_buf->len++;
  }

  // 添加数据
  if (len > 0 && p_data != NULL) {
    memcpy(p_out, p_data, len);
    p_buf->len += len;
  }

  // check if connected
  // 4. 连接状态检查
  if (hd_cb.device.state != HIDD_DEV_CONNECTED) {
    // for DATA on intr we hold transfer and try to reconnect
    // 如果未连接但发送的是中断通道数据,保存数据并尝试重新连接
    if (msg_type == HID_TRANS_DATA && cid == p_hcon->intr_cid) {
      // drop previous data, we do not queue it for now
      if (hd_cb.pending_data) {
        osi_free(hd_cb.pending_data);
      }

      hd_cb.pending_data = p_buf;
      if (hd_cb.device.conn.conn_state == HID_CONN_STATE_UNUSED) {
        hidd_conn_initiate();
      }

      return HID_SUCCESS;
    }
    log_counter_metrics(android::bluetooth::CodePathCounterKeyEnum::
                            HIDD_ERR_NO_CONNECTION_AT_SEND_DATA,
                        1);
    return HID_ERR_NO_CONNECTION;
  }

  log::verbose("report sent");

  // 5. L2CAP 数据发送
  if (!L2CA_DataWrite(cid, p_buf)) {
    log_counter_metrics(android::bluetooth::CodePathCounterKeyEnum::
                            HIDD_ERR_CONGESTED_AT_DATA_WRITE,
                        1);
    return (HID_ERR_CONGESTED);
  }

  return (HID_SUCCESS);
}

将 HID 数据封装为 L2CAP 数据包并发送到主机设备。

电源管理通知的流程分析同【Bluedroid】蓝牙HID DEVICE 报告发送与电源管理源码解析-CSDN博客分析,不再赘述。

三、时序图

四、总结

蓝牙 HID 协议中的虚拟电缆拔出机制是确保设备与主机之间优雅断开连接的重要组成部分。通过多层级的组件协作和标准化的控制消息格式,系统能够准确地通知主机设备即将断开,并在断开过程中合理管理电源。

虚拟电缆拔出流程展现了蓝牙HID协议栈的三大核心设计思想:

  1. 协议合规优先:严格遵循HID 1.1规范定义的控制报文格式与交互时序

  2. 资源效能平衡:通过固定大小控制缓冲区实现确定性的内存占用

  3. 状态驱动架构:基于事件的状态机模型确保断开过程的状态一致性

通过模拟物理电缆拔出行为,VCU机制有效解决了蓝牙设备在低功耗模式切换时的连接管理难题。



网站公告

今日签到

点亮在社区的每一天
去签到