[toc]

在这里插入图片描述

HID Core:全面了解与深度解析

文件路径 / 技术中文名 / 功能概述

  • 文件路径:drivers/hid/hid-core.c
  • 技术中文名:HID 核心层驱动
  • 功能概述:hid-core.c 是 Linux HID 子系统的公共核心实现,负责 HID 报告描述符解析、设备分组、驱动匹配、输入报告解释、事件分发,以及 hidinput、hidraw、hiddev 等上层消费路径的连接。它本身不是 USB、I2C、Bluetooth 这类传输驱动,而是位于这些传输驱动之上的通用 HID 核心。

介绍

drivers/hid/hid-core.c 在 Linux HID 栈中的位置非常明确:它属于 HID bus 的公共核心层,不直接负责总线收发,也不直接等同于输入子系统驱动。它的职责是把“某个传输层送上来的 HID 设备”和“上层具体的 HID 驱动、input 接口、hidraw 接口、hiddev 接口”连接起来。

从当前主线实现看,HID 子系统被组织成一种总线模型。传输驱动负责设备发现、底层收发、设备启动与停止;hid-core.c 负责报告解析、报告解释、用户态接口和驱动装配。也就是说,hid-core.c 的关键价值不在于“直接驱动某种硬件”,而在于为不同总线上的 HID 设备提供统一的中间层。

本文分析范围以 Linux 主线 drivers/hid/hid-core.c 为主,并对照 include/linux/hid.h 所描述的数据结构语义,以及 HID 官方内核文档中关于 report descriptor、transport driver、hidraw、hiddev、HID-BPF 的说明来理解调用链和边界。本文不展开某一个具体总线传输协议的细节,也不展开 evdev 用户态接口本身的完整语义。

历史与背景

1. 诞生背景

HID 设备的一个核心特点是:设备能力并不是靠“设备类型写死在驱动里”来表达,而是通过报告描述符动态声明。现代 HID 设备会通过 report descriptor 告诉主机:

  • 支持哪些 report
  • 每个 report 的字段布局是什么
  • 每个字段属于哪个 usage page / usage
  • 输入、输出、特性报告如何组织

如果没有一层统一的 HID 核心,那么 USB、I2C、Bluetooth 等不同传输栈都需要各自重复做这些事情:

  • 解析 HID 报告描述符
  • 把 bit-level 字段变成逻辑 usage
  • 将事件路由到 input、hidraw 或其他接口
  • 处理设备 quirks 和专用驱动匹配

这会造成重复实现、行为不一致和维护成本上升。

hid-core.c 解决的就是这个问题:把“与 HID 协议本身强相关”的公共逻辑集中到一个核心层中,而把总线细节留给 transport driver。

2. 发展历程

从源码版权和当前主线结构看,hid-core.c 是 Linux HID 子系统中历史很长的基础文件之一。它最早服务于传统 HID 设备,后续逐步演进出更完整的设备匹配、quirk 处理、hidraw/hiddev/input 多接口连接,以及当前主线中已经出现的 HID-BPF 接入能力。

对今天使用方式影响比较大的几个阶段有:

第一阶段:形成统一 HID bus 架构。
这一阶段的核心意义是明确分层:transport driver 负责底层收发,HID core 负责报告解析、解释和用户态接口。这个分层直接决定了今天 usbhidi2c-hidbt-hidp 等都要向 HID core 注册设备,而不是各自实现完整 HID 语义。

第二阶段:形成 descriptor 驱动的解析与事件分发模型。
hid-core.c 不再把大部分设备能力理解为“设备类型表驱动”,而是通过解析 report descriptor 建立 hid_reporthid_fieldhid_usage 等对象,再由这些对象驱动输入事件和其他接口。

第三阶段:增强特殊驱动与动态修补能力。
当前主线里,hid-core.c 同时支持:

  • 传统 quirk 查表
  • 专用 HID driver 的 report_fixup
  • 通过 BPF 对 report descriptor 做修补
  • 对“special driver”和 generic group 的重新匹配

这说明它已经不仅是静态解析器,而是一个支持动态修补和重新归类的核心层。

3. 社区与生态现状

从当前内核文档目录可以看出,HID 子系统仍然非常活跃,官方文档涵盖了:

  • HID 报告描述符解析
  • HID I/O transport drivers
  • hidraw
  • hiddev
  • UHID
  • HID-BPF
  • 各类具体协议和设备实现

这说明 HID 生态并没有停留在传统键盘鼠标时代,而是持续扩展到触控、传感器、手写笔、游戏控制器、用户态虚拟 HID、BPF 修补等方向。

当前主流应用场景包括:

  • USB HID
  • I2C HID
  • Bluetooth HID
  • 触控板、键盘、鼠标、游戏手柄
  • 专用厂商设备
  • 用户态虚拟 HID 设备
  • 面向特定设备问题的 HID-BPF 修补

核心原理与设计

1. 核心工作原理

1.1 HID bus 与核心层位置

hid-core.c 的第一性原理不是“驱动设备”,而是“维护 HID bus 上的公共设备语义”。主线代码里明确注册了 hid_bus_type,并把 matchproberemoveuevent 都挂到 HID bus 上。这意味着:

  • transport driver 先分配 hid_device
  • 把底层 ll_driver 绑定上去
  • 再通过 hid_add_device() 把设备交给 HID core
  • 后续设备匹配、probe、事件分发都走 HID bus 这一套机制

这也是为什么 USB HID、I2C HID 这样的实现虽然都叫 “HID”,但真正解析 report descriptor 和解释 usage 的主逻辑仍然在 hid-core.c

1.2 设备加入路径

设备进入 HID core 的关键入口是 hid_add_device()。这条路径完成几件非常关键的事情:

  1. 查表设置 quirks
  2. 检查设备是否应被忽略
  3. 检查底层 transport driver 是否提供了必需的 .raw_request()
  4. 通过低层 .parse() 读取设备原始 report descriptor
  5. 依据 report descriptor 先扫描设备分组信息
  6. 设置设备名、debug 节点并执行 device_add()

这里有两个容易忽略但很重要的点:

第一,raw_request 是强制前提。
这说明 HID core 把“能够与设备做原始 HID 报告请求交互”视为一个最基本的 transport 能力。

第二,group 不是纯静态表项。
对于 generic 设备,HID core 会先扫描 report descriptor,推断 group 和属性,再决定后续更合适的 driver 装配方式。

1.3 报告描述符解析

hid_parse_report() 负责把底层驱动读到的原始 report descriptor 复制到 dev_rdesc/dev_rsize 中;真正把它解析成 HID 内部对象的是 hid_open_report()

hid_open_report() 的关键逻辑是:

  • bpf_rdesc/bpf_rsize 取当前待解析的报告描述符
  • 若 HID driver 提供了 report_fixup(),先复制一份 descriptor 再做修补
  • 初始化 parser、collection 数组和相关状态
  • 通过 fetch_item() 从字节流逐项取出 HID item
  • 根据 item 类型分别交给 hid_parser_main()hid_parser_global()hid_parser_local()hid_parser_reserved()
  • 解析出 report、field、usage、collection 等结构
  • 检查 collection stack 和 delimiter 是否平衡
  • 完成默认 multiplier 的初始化
  • 最终把设备状态置为 HID_STAT_PARSED

从实现角度看,这条路径有几个明显特征:

  • 它按 HID item 语义逐项解释 descriptor,而不是用硬编码结构体去强行套设备类型
  • 它允许 driver 在 probe 前修补 report descriptor
  • 它对语法错误有明确防御,例如 long item 异常、collection 不平衡、delimiter 不平衡、fetch item 失败

这也解释了为什么 HID 子系统能覆盖差异很大的设备,同时又经常因为设备固件给出错误的 report descriptor 而出现兼容性问题。

1.4 设备分组与特殊驱动选择

hid_set_group() 在设备加到 bus 前会决定这个设备属于哪个 group。其行为大致分两类:

  • 如果启用了 ignore_special_drivers,则强制把设备视为 generic
  • 否则,对于尚未归组且没有声明 special driver quirk 的设备,先扫描 report descriptor 再决定 group

这个逻辑直接影响 driver 匹配。也就是说,hid-core.c 不是简单按总线 ID 和 VID/PID 做匹配,而是会结合 HID 层语义去调整设备分类。

同时,源码里还保留了 ignore_special_drivers 模块参数,用于强制忽略 special driver,让设备尽量走 generic 路径。这对于调试 HID 专用驱动与 generic 驱动的边界非常有价值。

1.5 probe 路径与 BPF 修补

当前主线中的 __hid_device_probe() 已经接入 HID-BPF 的 report descriptor 修补路径:

  • 如果还没有 bpf_rsize
  • 就从原始 descriptor 出发
  • 调用 BPF report descriptor fixup
  • 如果修补后 descriptor 发生变化,就重新扫描 group

这说明在当前主线里,descriptor 修补不再只依赖传统的 report_fixup() 回调,也可以走 BPF 路线。它的工程意义非常明确:对某些“只差一个 usage 或一个字节”的设备问题,不再一定需要为此加一份专用内核驱动。

1.6 输入报告处理

设备真正上报输入时,低层 transport driver 会调用 hid_input_report()。这条路径最终进入 __hid_input_report()hid_report_raw_event()

hid_report_raw_event() 的处理顺序很重要:

  1. 先根据 report ID 找到对应 hid_report
  2. 计算并校验报告大小
  3. 如果设备声明了 hidraw,就先把原始数据送到 hidraw_report_event()
  4. 如果不仅仅是 hidraw 消费者,且 report 有 field,就执行 hid_process_report()
  5. 处理完成后,如果当前 HID driver 定义了 report() 回调,也会调用
  6. 若设备已连接到 input,则再触发 hidinput_report_event()

这个顺序说明 HID core 同时服务多个消费路径:

  • hidraw 获取原始字节流
  • HID 核心解析字段与 usage
  • 专用 HID driver 可在 event() / report() 回调里插入处理
  • input 子系统拿到标准输入事件

1.7 差分事件模型

hid_process_report() 有一个很关键但容易被忽略的实现细节:它不是机械地把整份 report 每次都完整上抛,而是按 field 取值并维护“旧值 / 新值”,采用差分方式向上层汇报。

核心流程是:

  • hid_input_fetch_field() 先把当前 report 中每个 field 的值抽出来,放入 new_value
  • 对 variable field,逐 usage 分发事件
  • 对 array field,比较旧数组与新数组的差异,分别生成按下/释放一类的变化事件
  • 处理完以后再把 new_value 复制到 value

这意味着 hid-core.c 的输入处理不是简单的“收到字节就转发”,而是建立在一层使用语义和状态差异之上的事件解释模型。

1.8 事件分发模型

hid_process_event() 是 usage 级别分发的关键点。它的顺序是:

  1. 若存在 debug 监听,先 dump 输入
  2. 如果当前 HID driver 定义了 event() 且 usage 匹配,则先交给 driver
  3. 如果 driver 返回非 0,则中止继续分发
  4. 否则再按 claimed 标志决定是否送给 hidinput
  5. 若设备启用了 hiddev 且来自中断上下文,也会进入 hiddev 事件路径

这条链路说明 HID 专用 driver 对 usage 可以拥有优先处理权,而 hidinput / hiddev 则是核心层的通用下游。

1.9 连接与硬件生命周期

hid_connect() 负责把设备与不同上层接口连接起来。常见连接对象包括:

  • hidinput
  • hiddev
  • hidraw
  • 设备专用 driver

源码里还有一个重要检查:如果设备既没有任何 listener,又没有 raw_event 回调,核心层会直接报错并返回 -ENODEV。这是一种非常典型的防御性设计:没有消费者的设备不应被无意义地挂着。

与之对应,hid_hw_start() / hid_hw_stop() / hid_hw_open() / hid_hw_close() 负责协调低层 ll_driver 的 start/stop/open/close。
需要特别注意的是:

  • hid_hw_start() 应在成功 hid_parse 之后调用
  • hid_hw_open() 通过 ll_open_count 做引用计数
  • 第一个 open 才真正触发底层 open
  • 最后一个 close 才真正触发底层 close

这使得 HID core 能统一管理“设备被解析”“设备已启动”“设备开始投递事件”“设备不再需要投递事件”这几个不同阶段。

1.10 用户态接口与 sysfs

hid-core.c 不只处理内核内部对象,还直接暴露了一部分用户态可见信息。

例如 report_descriptor_read() 会把当前 hdev->rdesc 暴露给 sysfs,这也是为什么可以在 /sys/bus/hid/devices/.../report_descriptor 中直接读取设备 descriptor。

同时,hid_uevent() 会为设备填充:

  • HID_ID
  • HID_NAME
  • HID_PHYS
  • HID_UNIQ
  • MODALIAS

这些信息既影响 uevent,也影响用户态对 HID 设备的识别和调试。

2. 设计目标

hid-core.c 的设计目标可以概括为以下几类:

统一性
为 USB、I2C、Bluetooth 等不同 transport driver 提供统一的 HID 报告解析和设备语义层。

协议语义中心化
把 HID 报告描述符解析、report/field/usage 建模、usage 级事件分发统一放在核心层,而不是分散到各总线驱动中。

兼容性与可修补性
支持 quirks、专用 driver 的 report_fixup()、以及当前主线里的 HID-BPF descriptor fixup。

多出口消费
同一份 HID 数据能够同时服务 input、hidraw、hiddev 和专用 HID driver。

可维护性
transport driver 只需要关注底层数据通路与设备管理,避免在每条总线栈里重复实现一套 HID 解析器。

3. 核心优势

3.1 跨传输层复用

HID core 的最大优势,是把 HID 语义从 USB、I2C、Bluetooth 等 transport 中抽离出来。对 transport driver 来说,只要能把设备挂到 HID bus,并提供 parse/start/stop/open/close/raw_request 等低层能力,就能复用整个 HID 核心处理链。

3.2 以 descriptor 为中心,而不是以设备类型为中心

这使得 Linux 不需要为每种键盘、鼠标、触控板、手柄都重新写完整解释器,而是通过 report descriptor 和 usage 模型建立统一表示。对大规模兼容各种设备非常关键。

3.3 支持从原始路径到标准输入路径的多出口模型

同一设备既可以:

  • 作为 input 设备进入通用输入框架
  • 通过 hidraw 暴露原始报文
  • 对特定设备使用专用 HID driver
  • 在历史兼容场景下走 hiddev

这让 HID core 同时适合标准设备、半标准设备和需要调试的特殊设备。

3.4 对错误 descriptor 有多层修补手段

当前主线同时存在:

  • quirk 机制
  • special driver
  • report_fixup()
  • HID-BPF descriptor fixup

在工程实践中,这意味着很多设备兼容问题不必直接落到“重写 transport driver”这一层。

4. 局限性与缺点

4.1 强依赖 report descriptor 正确性

HID 的优势恰恰也是它的风险来源。设备只要 report descriptor 有问题,就可能在以下环节表现异常:

  • group 识别错误
  • usage 解释错误
  • input 事件不正确
  • 某些字段解析失败
  • hidraw 与 input 语义不一致

4.2 核心逻辑复杂,调用链长

hid-core.c 不是简单的桥接代码,它同时覆盖:

  • 设备生命周期
  • descriptor 解析
  • group 扫描
  • usage 级事件处理
  • hidraw/hiddev/input 路由
  • BPF/driver fixup
  • bus 匹配与 reprobe

因此它的理解成本并不低。对排障人员来说,必须明确区分问题是出在 transport、descriptor、core 解析、专用 driver,还是 input 映射层。

4.3 不处理所有设备特性

虽然 HID core 是公共核心,但它并不打算吞掉所有设备特化逻辑。某些设备仍然必须依靠:

  • 专用 HID driver
  • transport-specific quirk
  • 用户态 hidraw
  • BPF 程序

这意味着 HID core 不是“所有 HID 问题的唯一解决点”。

4.4 历史接口并存,接口边界容易混淆

hidinputhidrawhiddev、UHID、HID-BPF 同时存在,适用边界不同。很多排障问题并不是单点 bug,而是“选错了接口层”。

5. 不适用场景

以下场景通常不应优先把问题放到 hid-core.c 层解决:

第一类:设备根本不是 HID 协议。
如果底层设备只是厂商私有二进制协议,硬套 HID 栈通常会增加复杂度而不是降低复杂度。

第二类:问题明确属于 transport 层。
例如 USB 中断端点、I2C HID 电源时序、Bluetooth HID 连接管理等问题,应优先看 transport driver,而不是先怀疑 HID core。

第三类:仅需要用户态虚拟 HID 设备。
如果目标是用户态自建 HID 设备与内核交互,更合适的入口通常是 UHID,而不是直接改 hid-core.c

第四类:仅需要原始字节通道。
如果用户态应用完全知道设备协议,并且不需要内核做 usage 解释,那么 hidraw 往往比在 HID core 里增加专用逻辑更合适。

使用场景

1. 适合使用的场景

场景一:标准 HID 设备的通用接入

场景背景:设备基本遵循 HID 规范,report descriptor 质量较高。
为什么适合:HID core 能直接解析 descriptor,并把输入事件路由到标准 input 接口。
实际收益:不需要为常见键盘、鼠标、触摸设备单独写完整解析器。
注意事项:仍要确认 transport driver 的 .parse().raw_request()、启动与 open/close 路径正确。

场景二:多总线复用同一套 HID 语义

场景背景:同类设备可能跑在 USB、I2C 或 Bluetooth 上。
为什么适合:HID core 把 HID 协议层从 transport 层抽离,避免重复实现。
实际收益:专用 HID driver 可以更聚焦 usage 与设备特性,而不是重复做 descriptor 解析。
注意事项:transport 层只负责底层,不应把 HID 语义逻辑下沉过多。

场景三:设备 descriptor 轻微错误,需要修补

场景背景:设备整体可用,但 descriptor 某个 usage、某个字段或某个长度存在问题。
为什么适合:可以优先考虑 quirk、report_fixup(),在当前主线上也可结合 HID-BPF。
实际收益:不必一上来就重写专用 transport 或专用大驱动。
注意事项:修补应尽量局部化,避免影响其他设备或 generic 路径。

场景四:同时需要标准输入与原始报文调试

场景背景:既需要内核输入事件,又需要保留原始报文分析能力。
为什么适合:HID core 支持同时连到 hidinput 与 hidraw。
实际收益:既可用 evdev 看标准事件,也可通过 hidraw 看原始报文。
注意事项:要区分“原始数据已到达”与“usage 解释正确”是两件事。

2. 不推荐使用的场景

以下场景不建议优先从 hid-core.c 入手:

  • 设备协议本质不是 HID
  • 问题已确定在 USB/I2C/BT transport 层
  • 用户态虚拟设备更适合用 UHID
  • 用户态应用只需要原始字节流,不需要 HID 解释
  • 设备严重不遵循 report descriptor,且需要完整私有协议栈

3. 生产实践建议

配置与实现关注点

生产环境要特别关注以下几点:

  • 低层 transport driver 是否实现了 .raw_request()
  • .parse() 是否能稳定读出 report descriptor
  • ignore_special_drivers 是否被误用
  • 设备是否被分到了正确的 HID group
  • 是否应使用 hidraw、专用 HID driver 或 generic 路径
  • descriptor 修补逻辑是走 quirk、report_fixup() 还是 HID-BPF

监控与排障重点

排障时建议优先看四条线:

第一条是 descriptor 线
看 descriptor 能否被成功读取、是否能正确解析、是否存在 unbalanced collection 或 delimiter。

第二条是 transport 线
看底层 parse/raw_request/start/open/close 是否稳定。

第三条是 路由线
看设备最终连到了 hidinput、hidraw、hiddev 还是专用 driver。

第四条是 事件线
确认原始报告是否到达,usage 是否被正确解释,input 事件是否被正确映射。

常见故障模式

常见故障包括:

  • transport driver missing .raw_request()
  • bad device descriptor
  • item parsing failed
  • unbalanced collection at end of report description
  • unbalanced delimiter at end of report description
  • item fetching failed
  • device has no listeners, quitting

这些日志分别对应不同层次的问题,不能混为一谈。

4. 命令、配置与排障示例

查看当前 HID 设备:

1
ls /sys/bus/hid/devices/

查看某个设备的 report descriptor:

1
hexdump -C /sys/bus/hid/devices/0003:XXXX:YYYY.ZZZZ/report_descriptor

查看 HID 设备的 uevent 信息:

1
cat /sys/bus/hid/devices/0003:XXXX:YYYY.ZZZZ/uevent

如果设备同时导出了 hidraw,可结合用户态工具看原始报文;如果导出了 input 设备,则用事件工具看标准输入事件。实践里建议把“原始报文是否到达”和“上层事件是否正确”分开验证,不要只测其中一层。

对比分析

这里把 drivers/hid/hid-core.c 与三个最容易混淆的相邻实现做对比:

  • drivers/hid/usbhid/hid-core.c
  • drivers/hid/i2c-hid/i2c-hid-core.c
  • drivers/hid/hidraw.c

1. 实现方式

drivers/hid/hid-core.c
它是 HID bus 上层的公共核心,负责 report descriptor 解析、设备匹配、usage 级解释、用户态接口衔接。

drivers/hid/usbhid/hid-core.c
它是 USB HID transport driver,负责 USB 侧 parse/start/open/close/raw_request,并把设备注册到 HID core。

drivers/hid/i2c-hid/i2c-hid-core.c
它是 I2C HID transport driver,负责 I2C HID 协议与设备管理,再把设备注册给 HID core。

drivers/hid/hidraw.c
它不是 HID 核心解析器,也不是 transport driver,而是一个原始报文字符设备接口,提供未解析的 HID 数据给用户态。

2. 性能开销

hid-core.c
主要开销来自 descriptor 解析、field/usage 解释和事件分发。对普通 HID 设备来说,这部分开销通常可接受,但在高频、复杂 report 下会比纯原始字节转发更重。

usbhid/i2c-hid
主要开销集中在底层总线收发、缓存管理、中断处理和电源状态控制。它们本身不承担 usage 级解释主逻辑。

hidraw.c
路径最短,基本是原始报文入队和唤醒用户态,没有 HID usage 级解析开销。

3. 资源占用

hid-core.c
会维护 hid_reporthid_fieldhid_usage、collection、parser 状态和设备 bus 生命周期对象,内存占用与 descriptor 复杂度相关。

usbhid/i2c-hid
会额外维护传输层 buffer、IRQ、控制请求与总线相关状态。

hidraw.c
主要维护字符设备节点、读队列和缓冲区,资源占用相对可控,但用户态承担更多协议理解成本。

4. 隔离级别

这四者都不是安全隔离机制。
hid-core.cusbhid/i2c-hidhidraw.c 都运行在内核态。所谓边界差异,主要是“协议解释边界”和“用户态接口边界”,不是安全沙箱边界。

5. 启动速度

hid-core.c
启动成本主要在 descriptor 解析和对象构建。

usbhid/i2c-hid
除了设备注册给 HID core,还要完成底层总线和设备初始化,因此整体启动路径更依赖 transport 层。

hidraw.c
几乎不涉及 descriptor 解释本身,它依赖 HID core 和 transport 层先把设备带起来。

6. 运维复杂度

hid-core.c
复杂度高,原因是它位于多条路径交汇点,任何 transport、descriptor、usage、route 问题都可能在这里体现。

usbhid/i2c-hid
复杂度中等到高,取决于总线与平台细节。

hidraw.c
实现相对简单,但把协议理解压力转移到了用户态。

7. 适用场景差异

优先选 hid-core.c 的场景,是你需要理解 HID 协议层、descriptor 解析和 usage 级事件分发。
优先看 usbhid/i2c-hid 的场景,是你已经判断问题在底层总线或传输层。
优先用 hidraw.c 的场景,是用户态完全知道设备协议,只需要未解析的原始报文。

总结表

对比维度 drivers/hid/hid-core.c drivers/hid/usbhid/hid-core.c drivers/hid/i2c-hid/i2c-hid-core.c drivers/hid/hidraw.c
实现方式 HID bus 公共核心 USB HID transport driver I2C HID transport driver 原始报文字符设备接口
性能开销 解析与分发开销中等 总线与中断路径开销 总线与电源管理开销 最低,主要是原始数据转发
资源占用 维护 report/field/usage 等对象 维护 USB 侧状态与 buffer 维护 I2C HID 状态与 buffer 维护队列与字符设备状态
隔离级别 内核态,无额外安全隔离 内核态,无额外安全隔离 内核态,无额外安全隔离 内核态,无额外安全隔离
启动速度 取决于 descriptor 复杂度 取决于 USB 初始化 + HID core 注册 取决于 I2C HID 初始化 + HID core 注册 依赖前两层完成后使用
运维复杂度 中等到高 中等到高 中等
推荐场景 HID 协议层与 usage 级排障 USB 传输问题排障 I2C HID 传输问题排障 用户态需要原始报文

选型上应优先按“问题所在层次”判断,而不是按文件名相似程度判断。
如果问题是 HID 协议语义、descriptor、usage 映射、special driver 选择,就看 hid-core.c;如果问题是 USB/I2C 数据通路或设备启停,就看 transport driver;如果只是需要原始报文,就看 hidraw.c

总结

1. 关键特性总结

drivers/hid/hid-core.c 最重要的能力和限制可以归纳为:

  • 它是 HID bus 的公共核心,不是某条总线的底层驱动
  • 它以 report descriptor 为中心,构建 report/field/usage/collection 语义模型
  • 它统一了 input、hidraw、hiddev 和专用 HID driver 的接入点
  • 它支持 quirk、report_fixup() 和当前主线里的 HID-BPF 修补路径
  • 它强依赖 descriptor 正确性
  • 它本身并不取代 transport driver,也不解决所有设备特性问题

__hid_device_probe hid_device_probe hid_device_remove modalias_show DEVICE_ATTR_RO hid_dev_attrs hid_dev_bin_attrs hid_dev_group __ATTRIBUTE_GROUPS hid_uevent hid_bus_type EXPORT_SYMBOL hid_init hid_exit module_init module_exit MODULE_AUTHOR MODULE_DESCRIPTION MODULE_LICENSE HID 核心绑定、sysfs 可见性与总线注册退出路径梳理

作用与实现要点

这段代码的主线不是某个具体 HID 驱动的 probe(),而是 HID 总线核心怎样把一个 hid_device 交给某个 hid_driver。入口是 hid_bus_type.probe = hid_device_probe,它先用 driver_input_lockio_started 协调输入启动窗口,再把真正的绑定工作下沉到 __hid_device_probe()__hid_device_probe() 先处理 HID-BPF 报告描述符修正:只要当前设备还没有建立过 bpf_rdesc 状态,就会取原始 report descriptor 交给 hid_rdesc_fixup 路径,若描述符内容发生变化,就清掉已有 group 并重新按新描述符重扫。之后它再做驱动匹配、打开 devres 分组、重算 quirk、绑定 hdev->driver,最后进入驱动自定义 probe() 或默认的 hid_open_report()hid_hw_start() 路径。HID 文档明确把 report descriptor 视为设备能力的来源,HID-BPF 文档也明确说明 hid_rdesc_fixup 在 probe 时运行并可以改写描述符;驱动模型文档则说明 bus_type.probe/remove/uevent 是总线层统一消费的回调,devres_open_group() 返回的组 ID 可在失败或 remove 时统一释放整组资源。([Linux 内核文档][1])

这段代码的第二条主线是“可见性和生命周期”:modalias_showDEVICE_ATTR_RO(modalias)hid_dev_attrshid_dev_bin_attrshid_dev_group__ATTRIBUTE_GROUPS(hid_dev) 这一串声明不是装饰,它们最终经 hid_bus_type.dev_groups 在设备注册阶段一次性创建 sysfs 属性,让用户态在 add uevent 发生时就能同时看到 modaliasreport_descriptorhid_uevent() 又把 HID_IDHID_NAMEHID_PHYSHID_UNIQMODALIAS 填进环境变量,供热插拔和自动装载链路继续消费。设备模型文档明确要求属性组应在设备注册前准备好,否则用户态不会在 uevent 时感知到这些属性;HID ABI 文档也列出了 HID 设备目录中的 report_descriptor 文件。最外层的 hid_init() / hid_exit() 则通过 module_init() / module_exit() 接上模块装载与卸载路径:先注册 hid_bus_type,再拉起 hidraw 和调试设施;退出时反向拆除。module_init()module_exit() 的触发时机、EXPORT_SYMBOL() 的导出含义、MODULE_LICENSE() 的作用,内核文档都有明确说明。([Linux 内核文档][2])

平台关注:单核、无 MMU、ARMv7-M(STM32H750)

这段逻辑的控制流本身并不依赖多核一致性,也没有直接利用 MMU 语义,但它强依赖 Linux 的 HID 核心、设备模型、sysfs/uevent、devres、模块装载和 hidraw 这些基础设施;因此把它搬到 STM32H750 这类单核、无 MMU、通常不运行 Linux HID 栈的环境时,语义最先变化的不是分支本身,而是“谁来消费这些对象和宏”。单核不等于这些同步点可以删掉:down() / down_interruptible() 仍然要阻止 IRQ 或别的上下文在 probe/remove 期间穿插访问同一个 hid_device,状态位清除与 io_started 判断仍然承担锁移交和路径串行化作用;只是这些动作在单核上更多体现为本地抢占与中断窗口控制,而不是 CPU 间并发。真正没法直接照搬的是 bus_register()dev_groupsueventmodule_init()module_exit()hidraw_init()EXPORT_SYMBOL() 这类框架级入口,因为它们都要由 Linux 驱动模型和模块装载器来解释。([Linux 内核文档][3])

__hid_device_probe 设备与驱动真正绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* @brief 为一个 hid_device 建立与 hid_driver 的实际绑定关系
* @param hdev 当前 HID 设备对象,携带 report descriptor、quirk、状态位与 devres 容器
* @param hdrv 当前候选 HID 驱动,匹配成功后会写入 hdev->driver
* @return 0 表示绑定成功,负错误码表示 BPF 修正、匹配、资源分组或驱动初始化失败
*
* 补充说明:
* - 这里既处理 HID-BPF 对 report descriptor 的一次性修正,也处理真正的驱动 probe
* - devres 组故意保持打开状态,这样 probe 之后新增的 devres 资源也能在 remove 时一起释放
*/
static int __hid_device_probe(struct hid_device *hdev, struct hid_driver *hdrv)
{
const struct hid_device_id *id;
int ret;

if (!hdev->bpf_rsize) { /* 只有还没为当前设备建立过 BPF 修正后的描述符状态时,才走这条一次性修正路径 */
const __u8 *original_rdesc = hdev->bpf_rdesc;

if (!original_rdesc)
original_rdesc = hdev->dev_rdesc; /* 第一次 probe 还没有 bpf_rdesc 时,回退到设备原始 report descriptor */

hid_free_bpf_rdesc(hdev); /* 先释放上一次留下的 BPF 描述符副本,避免重探测或程序解绑后挂着旧缓冲区 */

hdev->bpf_rsize = hdev->dev_rsize; /* 把原始长度先记下来,同时也作为已经跑过一次 fixup 的标记 */
hdev->bpf_rdesc = call_hid_bpf_rdesc_fixup(hdev, hdev->dev_rdesc,
&hdev->bpf_rsize); /* 调用 HID-BPF 修正回调,后续解析都以这里返回的描述符为准 */

if (original_rdesc != hdev->bpf_rdesc) {
hdev->group = 0; /* 描述符来源变了,旧 group 归类不再可信,先清掉 */
hid_set_group(hdev); /* 按新的 report descriptor 重新计算设备分组,后续匹配与行为都跟着新分组走 */
}
}

if (!hid_check_device_match(hdev, hdrv, &id)) /* 总线层匹配没过时,不进入任何资源分配或驱动初始化 */
return -ENODEV;

hdev->devres_group_id = devres_open_group(&hdev->dev, NULL, GFP_KERNEL);
if (!hdev->devres_group_id) /* 后面要把 probe 阶段乃至 probe 之后加到设备上的 devres 资源都纳进同一组 */
return -ENOMEM;

hdev->quirks = hid_lookup_quirk(hdev); /* 每次重新绑定前都按当前设备特征重算 quirk,避免沿用上次绑定留下的状态 */
hdev->driver = hdrv; /* 先记住当前驱动,后续 remove 与默认路径都要从 hdev->driver 回取 */

if (hdrv->probe) {
ret = hdrv->probe(hdev, id); /* 专用 HID 驱动自己接管初始化细节 */
} else {
ret = hid_open_report(hdev); /* 默认路径先解析 report descriptor,建立 HID core 需要的报告结构 */
if (!ret)
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); /* 解析成功后再启动底层硬件连接与默认子接口接入 */
}

if (ret) {
devres_release_group(&hdev->dev, hdev->devres_group_id); /* probe 失败后把这组里挂上的资源一次放掉,避免半绑定状态泄漏 */
hid_close_report(hdev); /* 默认路径或驱动部分初始化过 report 解析状态时,这里把它收回来 */
hdev->driver = NULL; /* 清空 driver 指针,避免 remove 或后续重试误以为设备已成功绑定 */
}

return ret;
}

hid_device_probe HID 总线层 probe 封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @brief 作为 hid_bus_type.probe 入口,串行化绑定窗口并调用真正的绑定逻辑
* @param dev 设备模型传入的 struct device,实际类型为 hid_device
* @return 0 表示 probe 成功,负错误码表示加锁被打断或底层绑定失败
*
* 补充说明:
* - 这里通过 driver_input_lock 与 io_started 协调 probe 期间的输入启动窗口
* - 它只在 hdev 还没有 driver 时才真正进入 __hid_device_probe
*/
static int hid_device_probe(struct device *dev)
{
struct hid_device *hdev = to_hid_device(dev);
struct hid_driver *hdrv = to_hid_driver(dev->driver);
int ret = 0;

if (down_interruptible(&hdev->driver_input_lock)) /* 用可中断信号量串行化 probe/remove 与输入启动路径,避免在半绑定状态交错访问 */
return -EINTR;

hdev->io_started = false; /* 先把本轮 probe 的 I/O 启动状态清零,后面由真正启动路径决定是否接管锁的释放 */
clear_bit(ffs(HID_STAT_REPROBED), &hdev->status); /* 把 reprobe 标记清掉,让这次绑定按新的普通绑定窗口继续推进 */

if (!hdev->driver) /* 只有当前还没挂上驱动时才进入真正的 probe,避免重复绑定同一设备 */
ret = __hid_device_probe(hdev, hdrv);

if (!hdev->io_started)
up(&hdev->driver_input_lock); /* 如果 probe 期间没有切到已启动 I/O 的状态,就由这里把锁还回去 */

return ret;
}

hid_device_remove HID 总线层 remove 封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @brief 作为 hid_bus_type.remove 入口,串行化解绑并释放驱动占用的整组资源
* @param dev 设备模型传入的 struct device,实际类型为 hid_device
*
* 补充说明:
* - remove 先取下 driver_input_lock,避免解绑与输入打开关闭路径交错
* - 无论资源是在 probe 内还是 probe 之后以 devres 形式挂到设备上的,都会在这里统一释放
*/
static void hid_device_remove(struct device *dev)
{
struct hid_device *hdev = to_hid_device(dev);
struct hid_driver *hdrv;

down(&hdev->driver_input_lock);
hdev->io_started = false;

hdrv = hdev->driver;
if (hdrv) {
if (hdrv->remove)
hdrv->remove(hdev); /* 专用 HID 驱动自己决定解绑次序与硬件停机方式 */
else
hid_hw_stop(hdev); /* 没有自定义 remove 时,走 HID core 默认停机路径 */

devres_release_group(&hdev->dev, hdev->devres_group_id); /* 连 probe 后期和运行期间新增的 devres 资源也一起放掉 */
hid_close_report(hdev); /* 把 report descriptor 解析出来的内部结构收回 */
hdev->driver = NULL; /* 清空绑定关系,后续重探测才能重新进入 probe */
}

if (!hdev->io_started)
up(&hdev->driver_input_lock);
}

modalias_show 导出 HID 设备别名字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @brief 把当前 HID 设备编码成 sysfs 可读的 modalias 字符串
* @param dev 当前 HID 设备的 struct device
* @param a 对应的设备属性对象
* @param buf 输出缓冲区
* @return 写入 buf 的字符数
*
* 补充说明:
* - 这串别名把 bus、group、vendor、product 拼成稳定格式
* - 它与 uevent 里的 MODALIAS 使用同一套编码来源,用户态可用它做自动匹配
*/
static ssize_t modalias_show(struct device *dev, struct device_attribute *a,
char *buf)
{
struct hid_device *hdev = container_of(dev, struct hid_device, dev);

return sysfs_emit(buf, "hid:b%04Xg%04Xv%08Xp%08X\n",
hdev->bus, hdev->group, hdev->vendor, hdev->product);
}

DEVICE_ATTR_RO 只读 modalias 属性对象

1
2
3
4
5
6
7
8
9
10
11
/**
* 这段代码单元负责定义一个只读的设备属性对象 modalias。
* 它会被 hid_dev_attrs 数组收集,再经属性组挂到 HID 设备的 sysfs 目录。
* 它在设备注册并创建 sysfs 属性组时生效。
* 它把 modalias_show 绑定成读路径,让用户态读取当前设备的 HID 别名字符串。
*
* 补充说明:
* - 它是属性对象声明,不直接创建文件
* - 真正把它暴露到 sysfs 的是后面的 hid_dev_group 与 hid_dev_groups
*/
static DEVICE_ATTR_RO(modalias);

hid_dev_attrs 文本属性数组

设备模型文档要求设备属性最好在设备注册前通过属性组准备好,否则用户态不会在 add uevent 时收到这些属性的存在信息;这里的数组正是后续 hid_dev_group.attrs 的输入。([Linux 内核文档][2])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 这段代码单元负责收集 HID 设备需要暴露的普通 sysfs 属性。
* 它会被 hid_dev_group.attrs 读取。
* 它在 HID 设备注册并创建属性组时生效。
* 它决定哪些文本属性会出现在每个 HID 设备目录下。
*
* 补充说明:
* - 这里当前只挂了 modalias
* - 末尾的 NULL 是属性数组终止标记,设备核心据此知道列表结束
*/
static struct attribute *hid_dev_attrs[] = {
&dev_attr_modalias.attr, /* 把只读 modalias 属性挂进设备属性数组,后续会出现在每个 HID 设备目录 */
NULL,
};

hid_dev_bin_attrs 二进制属性数组

HID ABI 文档列出了每个 HID 设备目录下存在 report_descriptor 这个二进制 sysfs 文件;这里的二进制属性数组就是把那类文件并入 HID 设备默认属性组的入口。([Linux 内核文档][4])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 这段代码单元负责收集 HID 设备需要暴露的二进制 sysfs 属性。
* 它会被 hid_dev_group.bin_attrs 读取。
* 它在 HID 设备注册并创建属性组时生效。
* 它决定诸如 report_descriptor 这类需要按字节读取的文件会不会出现在设备目录里。
*
* 补充说明:
* - 这里把 report_descriptor 二进制文件并入默认 HID 设备属性组
* - 末尾的 NULL 同样是数组终止标记
*/
static const struct bin_attribute *hid_dev_bin_attrs[] = {
&bin_attr_report_descriptor, /* 暴露当前设备的 report descriptor 原始字节流,用户态可直接读取并做调试或复核 */
NULL
};

hid_dev_group HID 设备默认属性组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 这段代码单元负责把普通属性和二进制属性合成一个 HID 设备默认属性组。
* 它会被 hid_dev_groups 数组间接引用,再由 hid_bus_type.dev_groups 消费。
* 它在 HID 设备注册进入设备模型时生效。
* 它决定 HID 设备一创建出来就带哪些 sysfs 文件,避免属性晚于 uevent 出现。
*
* 补充说明:
* - .attrs 对应文本属性
* - .bin_attrs 对应 report_descriptor 这类二进制文件
*/
static const struct attribute_group hid_dev_group = {
.attrs = hid_dev_attrs, /* 普通文本属性从这里接入属性组 */
.bin_attrs = hid_dev_bin_attrs, /* 二进制属性从这里接入属性组 */
};

__ATTRIBUTE_GROUPS 生成 HID 设备属性组数组

设备模型文档把 ATTRIBUTE_GROUPS() 作为“单个属性组场景”的标准写法;总线或设备只需要持有该宏生成的 *_groups 数组即可让设备核心在注册/注销时自动创建和删除整组 sysfs 属性。([Linux 内核文档][2])

1
2
3
4
5
6
7
8
9
10
11
/**
* 这段代码单元负责把单个 hid_dev_group 包装成设备核心可直接消费的 hid_dev_groups 数组。
* 它会被 hid_bus_type.dev_groups 读取。
* 它在 HID 总线把设备注册进设备模型时生效。
* 它让设备核心自动在注册时创建 HID 默认属性组,在注销时自动清掉这些属性。
*
* 补充说明:
* - 宏会生成名为 hid_dev_groups 的数组
* - hid_bus_type 只需要把 .dev_groups 指向这个数组即可
*/
__ATTRIBUTE_GROUPS(hid_dev);

hid_uevent 填充 HID 设备热插拔环境变量

bus_type.uevent 回调由总线层在设备新增、移除等会生成 uevent 的时刻调用,用来往环境变量里补充总线自定义信息;这里填进去的 MODALIASHID_ID 等变量就是用户态热插拔链路能直接读到的上下文。([Linux 内核文档][3])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @brief 为 HID 设备生成 uevent 环境变量
* @param dev 当前 HID 设备
* @param env 将要发送给用户态的 uevent 环境变量容器
* @return 0 表示填充成功,-ENOMEM 表示环境变量空间不足
*
* 补充说明:
* - 这里把用户态热插拔链路需要的识别字段一次性塞进 env
* - MODALIAS 与 sysfs 的 modalias_show 采用同一套 HID 别名编码
*/
static int hid_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
const struct hid_device *hdev = to_hid_device(dev);

if (add_uevent_var(env, "HID_ID=%04X:%08X:%08X",
hdev->bus, hdev->vendor, hdev->product))
return -ENOMEM;

if (add_uevent_var(env, "HID_NAME=%s", hdev->name)) /* 让用户态无需回读设备对象也能直接拿到可读名称 */
return -ENOMEM;

if (add_uevent_var(env, "HID_PHYS=%s", hdev->phys)) /* 物理路径能帮助用户态区分同型号不同接入口 */
return -ENOMEM;

if (add_uevent_var(env, "HID_UNIQ=%s", hdev->uniq)) /* 唯一串号可用来区分同总线同 VID/PID 的多个实例 */
return -ENOMEM;

if (add_uevent_var(env, "MODALIAS=hid:b%04Xg%04Xv%08Xp%08X",
hdev->bus, hdev->group, hdev->vendor, hdev->product)) /* 自动装载链路实际最关心的是这串 MODALIAS */
return -ENOMEM;

return 0;
}

hid_bus_type HID 总线对象

struct bus_typenamedev_groupsdrv_groupsmatchproberemoveuevent 都是设备核心直接消费的字段;bus_register() 成功后,这张表才真正进入全局总线列表并开始驱动设备匹配与 sysfs 展示。([Linux 内核文档][5])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 这段代码单元负责定义 HID 总线的核心行为表。
* 它会被 bus_register() 消费,并在注册成功后被设备模型与驱动核心持续读取。
* 它在 hid_init() 注册总线后生效。
* 它决定 HID 设备与 HID 驱动如何匹配、何时进入 probe/remove、设备和驱动默认带哪些 sysfs 属性、以及热插拔事件如何生成。
*
* 补充说明:
* - .dev_groups 绑定的是前面生成的 hid_dev_groups
* - .probe/.remove/.uevent 是 HID 总线级统一入口,不是具体厂商驱动自己的回调
*/
const struct bus_type hid_bus_type = {
.name = "hid",
.dev_groups = hid_dev_groups, /* 每个 HID 设备注册时都会自动带上这组默认设备属性 */
.drv_groups = hid_drv_groups, /* HID 驱动对象自身的默认属性组由这里接入 */
.match = hid_bus_match, /* 新设备或新驱动加入 HID 总线时,由这里决定是否匹配 */
.probe = hid_device_probe, /* 匹配成功后统一从这里进入 HID 总线层 probe 封装 */
.remove = hid_device_remove, /* 解绑时统一从这里进入 HID 总线层 remove 封装 */
.uevent = hid_uevent, /* 设备新增移除等事件发生时,由这里补 HID 专有环境变量 */
};

EXPORT_SYMBOL 导出 hid_bus_type

EXPORT_SYMBOL() 会把符号放进内核导出符号表,让其他模块可以像普通外部符号那样引用它。([Linux 内核文档][6])

1
2
3
4
5
6
7
8
9
10
11
/**
* 这段代码单元负责把 hid_bus_type 导出给其他内核模块使用。
* 它会被模块装载器和导出符号表消费。
* 它在模块装载后立即生效。
* 它允许别的模块直接引用 HID 总线对象,从而把自己的设备或驱动挂到 HID 总线语义上。
*
* 补充说明:
* - 这里导出的不是一个回调,而是总线对象本身
* - 后续模块若依赖 HID 总线注册逻辑,可以直接链接这个符号
*/
EXPORT_SYMBOL(hid_bus_type);

hid_init HID 总线与子接口初始化

module_init() 指向的初始化函数在模块插入时执行,若内建则在启动阶段执行;而 bus_register() 负责把总线对象注册到驱动模型。hidraw 文档说明 hidraw 是“未经 HID 解析改写的原始访问接口”,因此它必须在 HID 总线已就绪之后再初始化。HID-BPF 文档则说明 report descriptor fixup 发生在 probe 期间,所以 CONFIG_HID_BPF 打开时这里把 hid_ops 指到 __hid_ops,后续 probe 才有 BPF 钩子可走。([Linux 内核文档][7])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @brief 初始化 HID 总线及其配套子设施
* @return 0 表示初始化成功,负错误码表示总线或子设施初始化失败
*
* 补充说明:
* - 先注册总线,再打开 BPF 钩子、hidraw 与调试设施
* - 错误回滚先撤子设施,再撤总线,避免留下半初始化的 bus 对象
*/
static int __init hid_init(void)
{
int ret;

ret = bus_register(&hid_bus_type);
if (ret) {
pr_err("can't register hid bus\n");
goto err;
}

#ifdef CONFIG_HID_BPF
hid_ops = &__hid_ops; /* 打开 HID-BPF 全局操作表,后续 probe 才能接入 report descriptor fixup 等钩子 */
#endif

ret = hidraw_init();
if (ret)
goto err_bus; /* hidraw 起不来时要撤掉前面注册的 HID 总线,避免只剩裸总线而缺子接口 */

hid_debug_init(); /* 总线与 hidraw 都就绪后,再拉起调试辅助设施 */

return 0;
err_bus:
bus_unregister(&hid_bus_type);
err:
return ret;
}

hid_exit HID 总线与子接口退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @brief 反向拆除 HID 总线及其配套子设施
*
* 补充说明:
* - 退出顺序与初始化相反,先停 BPF 钩子与调试接口,再停 hidraw,最后注销总线
* - bus_unregister() 之后,设备核心不再把新的设备或驱动纳入 HID 总线匹配流程
*/
static void __exit hid_exit(void)
{
#ifdef CONFIG_HID_BPF
hid_ops = NULL; /* 先撤掉全局 HID-BPF 操作表,避免退出窗口里继续走到旧钩子 */
#endif
hid_debug_exit();
hidraw_exit(); /* 先停原始字符接口,再把底层总线注销 */
bus_unregister(&hid_bus_type);
hid_quirks_exit(HID_BUS_ANY); /* 收尾全局 HID quirk 相关状态 */
}