[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 负责报告解析、解释和用户态接口。这个分层直接决定了今天 usbhid、i2c-hid、bt-hidp 等都要向 HID core 注册设备,而不是各自实现完整 HID 语义。
第二阶段:形成 descriptor 驱动的解析与事件分发模型。hid-core.c 不再把大部分设备能力理解为“设备类型表驱动”,而是通过解析 report descriptor 建立 hid_report、hid_field、hid_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,并把 match、probe、remove、uevent 都挂到 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()。这条路径完成几件非常关键的事情:
- 查表设置 quirks
- 检查设备是否应被忽略
- 检查底层 transport driver 是否提供了必需的
.raw_request() - 通过低层
.parse()读取设备原始 report descriptor - 依据 report descriptor 先扫描设备分组信息
- 设置设备名、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() 的处理顺序很重要:
- 先根据 report ID 找到对应
hid_report - 计算并校验报告大小
- 如果设备声明了 hidraw,就先把原始数据送到
hidraw_report_event() - 如果不仅仅是 hidraw 消费者,且 report 有 field,就执行
hid_process_report() - 处理完成后,如果当前 HID driver 定义了
report()回调,也会调用 - 若设备已连接到 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 级别分发的关键点。它的顺序是:
- 若存在 debug 监听,先 dump 输入
- 如果当前 HID driver 定义了
event()且 usage 匹配,则先交给 driver - 如果 driver 返回非 0,则中止继续分发
- 否则再按 claimed 标志决定是否送给 hidinput
- 若设备启用了 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_IDHID_NAMEHID_PHYSHID_UNIQMODALIAS
这些信息既影响 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 历史接口并存,接口边界容易混淆
hidinput、hidraw、hiddev、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 descriptorignore_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 descriptoritem parsing failedunbalanced collection at end of report descriptionunbalanced delimiter at end of report descriptionitem fetching faileddevice 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.cdrivers/hid/i2c-hid/i2c-hid-core.cdrivers/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_report、hid_field、hid_usage、collection、parser 状态和设备 bus 生命周期对象,内存占用与 descriptor 复杂度相关。
usbhid/i2c-hid
会额外维护传输层 buffer、IRQ、控制请求与总线相关状态。
hidraw.c
主要维护字符设备节点、读队列和缓冲区,资源占用相对可控,但用户态承担更多协议理解成本。
4. 隔离级别
这四者都不是安全隔离机制。hid-core.c、usbhid/i2c-hid、hidraw.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_lock 和 io_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_show、DEVICE_ATTR_RO(modalias)、hid_dev_attrs、hid_dev_bin_attrs、hid_dev_group、__ATTRIBUTE_GROUPS(hid_dev) 这一串声明不是装饰,它们最终经 hid_bus_type.dev_groups 在设备注册阶段一次性创建 sysfs 属性,让用户态在 add uevent 发生时就能同时看到 modalias 和 report_descriptor;hid_uevent() 又把 HID_ID、HID_NAME、HID_PHYS、HID_UNIQ 和 MODALIAS 填进环境变量,供热插拔和自动装载链路继续消费。设备模型文档明确要求属性组应在设备注册前准备好,否则用户态不会在 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_groups、uevent、module_init()、module_exit()、hidraw_init() 和 EXPORT_SYMBOL() 这类框架级入口,因为它们都要由 Linux 驱动模型和模块装载器来解释。([Linux 内核文档][3])
__hid_device_probe 设备与驱动真正绑定
1 | /** |
hid_device_probe HID 总线层 probe 封装
1 | /** |
hid_device_remove HID 总线层 remove 封装
1 | /** |
modalias_show 导出 HID 设备别名字符串
1 | /** |
DEVICE_ATTR_RO 只读 modalias 属性对象
1 | /** |
hid_dev_attrs 文本属性数组
设备模型文档要求设备属性最好在设备注册前通过属性组准备好,否则用户态不会在 add uevent 时收到这些属性的存在信息;这里的数组正是后续 hid_dev_group.attrs 的输入。([Linux 内核文档][2])
1 | /** |
hid_dev_bin_attrs 二进制属性数组
HID ABI 文档列出了每个 HID 设备目录下存在 report_descriptor 这个二进制 sysfs 文件;这里的二进制属性数组就是把那类文件并入 HID 设备默认属性组的入口。([Linux 内核文档][4])
1 | /** |
hid_dev_group HID 设备默认属性组
1 | /** |
__ATTRIBUTE_GROUPS 生成 HID 设备属性组数组
设备模型文档把 ATTRIBUTE_GROUPS() 作为“单个属性组场景”的标准写法;总线或设备只需要持有该宏生成的 *_groups 数组即可让设备核心在注册/注销时自动创建和删除整组 sysfs 属性。([Linux 内核文档][2])
1 | /** |
hid_uevent 填充 HID 设备热插拔环境变量
bus_type.uevent 回调由总线层在设备新增、移除等会生成 uevent 的时刻调用,用来往环境变量里补充总线自定义信息;这里填进去的 MODALIAS、HID_ID 等变量就是用户态热插拔链路能直接读到的上下文。([Linux 内核文档][3])
1 | /** |
hid_bus_type HID 总线对象
struct bus_type 的 name、dev_groups、drv_groups、match、probe、remove、uevent 都是设备核心直接消费的字段;bus_register() 成功后,这张表才真正进入全局总线列表并开始驱动设备匹配与 sysfs 展示。([Linux 内核文档][5])
1 | /** |
EXPORT_SYMBOL 导出 hid_bus_type
EXPORT_SYMBOL() 会把符号放进内核导出符号表,让其他模块可以像普通外部符号那样引用它。([Linux 内核文档][6])
1 | /** |
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 | /** |
hid_exit HID 总线与子接口退出
1 | /** |











