-
-
[原创] 实战解析I2C HID协议----以I2C Touch Pad为例
-
发表于: 1天前 250
-
本文是<实战解析 USB HID 协议>和<实战解析 USB HID 协议 2>的扩展篇. 不过这次将目光转移到应用了I2C HID协议的设备.
1. 硬件部分
在单片机/嵌入式应用场景中, 设备间通信只需SCL/SDA 2根信号线; 而HID over I2C协议还额外需要一根INT中断信号. 当I2C Slaver有数据时, 如触摸Pad, 会触发INT信号 (Assert INT#). Host收到中断后才会发起I2C事务(直至此时Host才使能SCL信号, 再通过SDA信号读取数据). 由于这个特性, HID over I2C信号在逻辑分析仪中的输出会呈现一定的规律性: SCL/SDA仅在INT# Assert期间变化; 在INT# De-Assert期间无变化, 这种特性使得波形比较清澈(这样设计可能也是为了省电):

由Acute解析的HID Over I2C波形
常见的I2C HID设备有I2C Touch Pad和I2C Touch Panel, 正好我手边有台带FocalTech I2C Touch Pad的二手笔记本且Touch Pad PCB上有INT/SCL/SDA这些信号的丝印, 方便接转接板. 本文的内容将基于此笔记本展开.

FocalTech I2C Pad PCB

2.固件部分
硬件部分的准备完成, 再看看固件/OS部分. HID over I2C协议获取HID Report Descriptor的过程有点绕. OS需要从ACPI中获得HID Descriptor Register. OS通过HID Descriptor Register去Touch Pad中读取HID Descriptor. 注意, 目前读到的是HID Descriptor, 其结构如下:
typedef struct _HIDDescriptor
{
uint16 wHIDDescLength;
uint16 bcdVersion;
uint16 wReportDescriptorLength;
uint16 wReportDescriptorRegister;
uint16 wInputRegister;
uint16 wMaxInputLength;
uint16 wOutputRegister;
uint16 wMaxOutputLength;
uint16 wCommandRegister;
uint16 wDataRegister;
uint16 wVendorID;
uint16 wProductID;
uint16 wVersionID;
uint32 Reserved;
}HIDDescriptor;OS再从HIDDescriptor->wReportDescriptorRegister获得Report Descriptor Register地址, 最终通过该地址获得HID Report Descriptor. 该过程的示意图如下:

另外OS从HIDDescriptor->wCommandRegister获得Command Register, 该寄存器用于Host向Slave发出如SetPower/RESET请求.
2.1. SetPower/RESET
OS在读取HID Descriptor后, 在进一步读取HID Report Descriptor前, 还需要对HID Over I2C设备依次执行2个动作SetPower和RESET

这2个命令都是OS通过向HIDDescriptor->wCommandRegister(在本文中是0x22)指定的寄存器写入2 BYTE的Command实现.
| 命令 | HOST向wCommandRegister写入的2 BYTE数值 | 备注 |
| SET_POWER | BYTE0=0x08, BYTE1=0x00 | SET_POWER=On, 让设备开始工作 |
| RESET | BYTE0=0x01, BYTE1=0x00 | 1.Slave重置自身; 2.重置完成后Assert INT# 3.Host发现INT# Assert后发起I2CRead事务 4.Slave持续向Host返回0x00 0x00作为重置成功的标志 5.Host读取完毕后, Slave将De-Assert INT# |
附注, 如果RESET流程执行失败, Windows设备管理器中的I2C 设备会出现黄色感叹号.
2.2. 获取I2C Slave Address和HID Descriptor Register Address
HID over I2C协议本质上依然是I2C协议和HID协议. 对于I2C协议, 固件需要向主机提供I2C Slave Address; 而对于HID协议, 固件需要向Host提供HID Descriptor Register. Host获得Descriptor Register后, 再通过I2C 协议(Repeat Start事务)从Pad固件中获得 HID Descriptor. 这种关系就像C语言里的指针: HID Descriptor Register就像HID Descriptor类型的指针, 通过该指针获取HID Descriptor结构体变量.
| 获取方式 | 备注 | |
| I2C Slaver Address | OS enable ACPI以后, 由Method(_INI)设定, OS通过Method(_CRS)获取 | 字面意思, 就是Slave设备地址, 在本文中就是I2C Touch Pad Slave地址. |
| HID Descriptor Register | OS enable ACPI以后, 由Method(_INI)设定, OS通过Method(_DSW)获取 | HID over I2C协议中最重要的部分. 由厂商提供, 固件设定的16bit地址. Method(_DSW)中获得HID Descriptor Register, 再从Register中获得HID Descriptor. |
作为Pad厂商和主机厂商以外的读者, 可以通过RW dump整个ACPI Table获得这2个信息(本文使用的ACPI Dump文件参见附录):
a. 首先查看设备管理器, 定位I2C Touch Pad的Bios Device Name:

b. 根据Bios Device Name知I2C Touch Pad的ACPI Device Name是TPD0, 搜索整个ACPI Table. 可以看到在Device(TPD0) Scope中找到Method(_INI) :

Method(_INI)初始化I2C slave Address/HID Descriptor Register Address

Method(_CRS)和Method(_DSW)返回I2C slave Address/HID Descriptor Register Address
2.3. 从逻辑分析仪观察获得HID Report Descriptor的过程
读者只能从ACPI Table中看到I2C Slave Address和HID Descriptor Register Address, 但是OS和Pad交互获得HID Report Descriptor的过程只能依赖逻辑分析仪读到. 我将通过2小节来演示该过程(演示中用到的逻辑分析仪文件见附件: HIDDescriptorAndHIDReportDescriptor.TBW).
2.3.1. I2C Host发起HID Descriptor Register读HID Descriptor
逻辑分析仪上读到的第一段I2C事务如下:

对波形解读:
| 动作 | 注释 |
| Start | I2C事务开始 |
| Addr:0x2C+Write | 寻址Slave, 准备写入地址 |
| Command Register:0x20 | 写入0x20, 这是从ACPI中读到的HID Descriptor Register |
| SR (Start Repeat) | 不释放总线, 切换模式 |
| Addr:0x2C+Read | 再次寻址Slave, 开始从0x20处读取 |
Host从 Pad中读到的内容如上图中红框部分, 下表将数值还原为HID Descriptor:
| Offset | 在HID Descriptor中的字段 | Value | 注释 |
| 0x00 | wHIDDescLength | 0x001E | HID Over I2C Spec规定的固定内容, 指明整个HID Descriptor长度为0x1E |
| 0x02 | bcdVersion | 0x0100 | HID Over I2C Spec规定的固定内容, bcd格式的版本号 |
| 0x04 | wReportDescriptorLength | 0x02AE | HID Report Descriptor长度 |
| 0x06 | wReportDescriptorRegister | 0x0021 | Host 会读取这个地址来获取 HID Report Descriptor |
| 0x08 | wInputRegister | 0x0024 | 读取输入报告(如Pad移动/按键)的地址 |
| 0x0A | wMaxInputLength | 0x0020 | 输入报告的最大长度 |
| 0x0C | wOutputRegister | 0x0025 | 发送输出报告的地址 |
| 0x0E | wMaxOutputLength | 0x0000 | N/A |
| 0x10 | wCommandRegister | 0x0022 | 发送 Reset/SetPower 命令的地址 |
| 0x12 | wDataRegister | 0x0023 | 进行数据传输时所依赖的寄存器地址 |
表格中wInputRegister字段像在表达: 当有输入事件产生时(INT# Assert时), OS会从wInputRegister所指向寄存器中读取Input Report. 然而, 在实际测试过程中, 我并没有从逻辑分析仪中看到这样的I2C事务. 不知道为啥Windows为啥没有完全按HID Over I2C协议实现(因为I2C事务是HOST发起的, 因此可以排除设备端不按标准实现的可能).
2.3.2. I2C Host再次发起读事务获取HID Report Descriptor
OS填充完HID Descriptor后, 了解到HID Report Descriptor Register=0x21, 之后OS会从0x21处(如下图红框处所示)获取HID Report Descriptor:

2.4. 解析HID Report Descriptor
我已将读到的HID Report Descriptor Hexdump保存到附件HidReportDescriptorptor.CSV. 我使用的的Touch Pad具有多点触摸功能, 而.csv文件中REPORT_ID(01)所描述的Usage_Page/Usage/Collection&EndCollection用于描述多点触摸功能的Input Report. 在之前的文章中已经有比较详细的解析HID Report Descriptor的步骤, 因此本文直接给出多点触摸功能的Input Report (Size = 0x20 Byte):

2.5. 解析Touch Pad 移动引起的Input Report
有了HID Report Descriptor这个模版, 现在可以解释逻辑分析仪上I2C Touch Pad移动产生的数据了(同样, 演示用的逻辑分析仪文件见附件: InputReportWhenFingerMove.TBW).

从逻辑分析仪窗口可以得到下列结论:
1. 当INT# Assert(产生了手指移动/按压一类事件)时, Host端以”Start+I2CSlaveAddr + Rd”的时序, 直接读取Input Report; 而并非以”Start+I2CSlaveAddr + Wr + HIDDescriptor.wInputRegister + StartRepeat + Rd+ HIDDescriptor.wInputRegister”这种预想的时序读取Input Report.
2. Host从Pad读到的Input Report具有固定的格式: 2字节InputReport长度+1字节ReportID+5*5字节多点触摸信息+1字节有效触摸点数+1字节按键状态.
3. 猜想
本系列完结, 实在不想写HID Over GPIO的文章了. 不知道有没有做个过滤驱动修改Input Report中位移的可行性. 我先研究研究, 如果可行, 再更新一篇.
4. 后记
写这篇文章, 始于一次Issue: I2C Touch Panel息屏回来后, Touch功能丢失并伴随设备管理器里I2C设备黄标(黄标原因是读不到_HID Descriptor). 由于复现步骤比较稳定, 抱着死马当活马医的心态, 我抓了一把HID over I2C协议, 结果发现主机发出SET_POWER Command后, INT#再也没有Assert, INT#/SCL/SDA 3个信号平静的就像湖面. 咨询屏幕厂商后才发现是电源设计有问题.
复盘Issue时, 我打算选用张银奎老师的幽兰笔记本来记录HID Over I2C协议的工作原理. 然而拆开后发现, 幽兰笔记本的TouchPad挂在USB Hub下面. 因此打消了写这篇文章的念头. 一次偶尔逛海鲜市场的时候发现居然有售卖公司的二手笔记本, 型号更是我首次Power On的型号. 交织不信任和收藏的复杂心态, 我收了这台机器, 到手后惊奇于居然真的能用!
但是要写这篇文章依然有限制: 1. 回路设计不能公开; 2. Bios代码不能公开;
比较巧的是, Touch Pad PCB上有信号丝印; 另外, 和HID Over I2C紧密相关的ACPI代码可以通过RW dump出来. 最终这篇文章还是艰难的诞生了~
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!