简介:Sensy 是一套面向嵌入式与 Windows 内核驱动学习者的教育性质源码项目,围绕 I2C 设备通信展开,从用户模式逐步深入到 KMDF 驱动开发。项目演示了如何用 WinRT API 在用户态与 I2C 设备通信,并进一步实现基于 Simple Peripheral Bus 的 KMDF 驱动,完成从传感器读取温度并在液晶屏上显示,再通过符号链接与 DeviceIoControl 由用户模式应用访问驱动,适合想打通用户态与内核态通信链路的开发者练手。资源包共 38 个文件,以 h 头文件、cpp 源文件、vcxproj 工程文件为主,另含 asl 与 inx 驱动配置、rc 资源脚本及 sln 解决方案等,压缩包约 1.83MB,结构覆盖 winrtapi 与 kmdf 两条主线。目前已有 51 人学习下载,可帮助读者理解驱动分层、SPB 总线访问与设备控制流程,并对照源码梳理完整工程组织方式。
1. Sensy 这套 Windows 驱动框架下的 I2C 通信系统,到底解决了谁的痛点
如果你在 Windows 上做过 I2C 设备对接,大概率经历过这样的场景:拿到的传感器模块在 Linux 上用 i2c-dev 几行代码就跑通了,换到 Windows 上却连设备地址都扫不到。Sensy 这个项目瞄准的正是这个断层——它基于 Windows 驱动框架(WDF)构建了一套完整的 I2C 设备通信系统,把内核态的 I2C 控制器驱动、用户态的通信接口、以及设备枚举与数据读写逻辑串成了一条可复现的链路。它适合两类人:一是需要在 Windows 环境下对接 I2C 传感器、EEPROM 或 MCU 的嵌入式工程师,二是想通过一个完整案例理解 WDF 驱动开发流程的 Windows 驱动初学者。源码包里通常包含驱动工程、用户态测试程序和必要的 INF 配置文件,拿到手后你需要自己编译、签名、安装,才能看到设备管理器里多出来的那个 I2C 设备节点。这套东西不依赖特定硬件厂商的 SDK,走的是通用 I2C 控制器抽象,所以适配面比想象中宽。
2. 先搞清楚 Windows 驱动框架里 I2C 通信的底层逻辑
2.1 WDF 驱动模型与 I2C 控制器的关系
Windows 驱动框架分为 KMDF(内核模式)和 UMDF(用户模式)两条路线。I2C 设备通信系统通常走 KMDF,因为 I2C 控制器本身是挂在 ACPI 或 PCI 总线下的硬件资源,需要内核态直接操作寄存器或调用 SPB(Simple Peripheral Bus)框架提供的接口。Sensy 的驱动层本质上是一个 SPB 客户端驱动,它不直接碰 I2C 控制器的寄存器,而是通过SpbRequest系列 API 向底层控制器驱动发送读写请求。这种分层设计的好处是:你的驱动不需要关心 I2C 控制器是 Intel 的还是 AMD 的,只要控制器驱动正确实现了 SPB 接口,上层就能统一调用。
理解这一点很关键,因为很多初学者一上来就去翻 I2C 协议时序,想自己用 GPIO 模拟,结果在 Windows 上被中断处理和时序精度搞得焦头烂额。Sensy 的做法是站在 SPB 框架的肩膀上,把精力集中在设备逻辑本身。驱动入口通常是EvtDriverDeviceAdd回调,在里面创建WDFDEVICE对象,然后通过WdfFdoInitSetFilter或直接作为 FDO 挂载到 I2C 控制器下。设备上下文结构体里会保存SPB_CONTROLLER_CONFIG和SPB_TARGET_CONFIG,前者描述控制器连接参数,后者描述目标设备的 I2C 从地址、地址宽度、总线速度等。
2.2 I2C 目标设备的枚举与资源分配
在 ACPI 系统中,I2C 设备通常通过_CRS方法声明自己的从地址和连接参数。Sensy 的 INF 文件里会有一个DDInstall.HW节,配合AddReg写入设备的硬件 ID 和兼容 ID。当系统枚举到匹配的 ACPI 节点时,PnP 管理器会加载你的驱动,并调用EvtDevicePrepareHardware回调。在这个回调里,你需要调用WdfIoTargetCreate创建 IoTarget,然后用WdfIoTargetOpen打开底层控制器驱动暴露的远程 IO 目标。
资源分配的核心是SPB_TARGET_CONFIG结构体,它包含以下关键字段:
| 字段 | 含义 | 典型值 |
|---|---|---|
| TargetMode | 目标模式 | SpbTargetModeI2C |
| ConnectionSpeed | 总线速度 | 100000(标准模式)或 400000(快速模式) |
| Address | 从设备地址 | 0x48(7 位地址) |
| AddressWidth | 地址字节数 | 1 或 2 |
这些参数如果填错,最直接的表现就是WdfIoTargetOpen返回STATUS_IO_TIMEOUT或STATUS_INVALID_PARAMETER。我一般会先用逻辑分析仪抓一下 SDA/SCL 波形,确认地址和速度是否匹配,再回头改驱动配置。
2.3 用户态与内核态的通信接口设计
驱动跑起来之后,用户态程序怎么读写 I2C 设备?Sensy 通常采用两种方式:一种是暴露一个设备接口 GUID,用户态用CreateFile打开设备句柄,然后通过DeviceIoControl发送自定义 IOCTL 码;另一种是创建一个虚拟串口或 HID 设备,让用户态用标准 API 访问。前者更灵活,适合自定义协议;后者兼容性好,但需要额外处理 HID 报告描述符。
以 IOCTL 方式为例,驱动里需要定义IOCTL_I2C_READ和IOCTL_I2C_WRITE两个控制码,然后在EvtIoDeviceControl回调里解析输入输出缓冲区。用户态调用时,输入缓冲区放寄存器地址和长度,输出缓冲区接收数据。这里有个血泪经验:输入输出缓冲区的方法要选METHOD_BUFFERED,否则在 64 位系统上容易遇到指针截断问题。缓冲区大小也要在 IOCTL 定义里写清楚,不然DeviceIoControl会直接返回ERROR_INSUFFICIENT_BUFFER。
3. 从源码到跑通:Sensy 驱动工程的编译与部署步骤
3.1 搭建 WDK 编译环境与工程结构解析
编译 Sensy 驱动需要 Visual Studio 2022 加上 Windows Driver Kit(WDK)对应版本。安装时注意勾选“Windows Driver Kit”和“Spectre 缓解库”,后者在较新的 WDK 里是必选项,不装会在链接阶段报错。工程目录通常长这样:
Sensy/ ├── driver/ │ ├── SensyI2C.sln │ ├── SensyI2C.vcxproj │ ├── Driver.c │ ├── Device.c │ ├── I2C.c │ └── SensyI2C.inf ├── user/ │ ├── SensyTest.sln │ └── SensyTest.cpp └── README.md打开SensyI2C.sln后,先检查项目属性里的“目标平台版本”和“目标 OS 版本”是否与你当前系统匹配。我一般会把“目标平台”设为Desktop,“目标 OS 版本”设为Windows 10或Windows 11,因为 SPB 框架在 Win10 之后才完全稳定。编译配置选Release和x64,Debug 版本虽然能跑,但性能差很多,而且调试输出会拖慢 I2C 时序。
编译命令可以用 MSBuild 直接跑:
msbuild SensyI2C.sln /p:Configuration=Release /p:Platform=x64 /t:Rebuild如果报错MSB8040: Spectre-mitigated libraries are required,说明 Spectre 库没装,去 VS Installer 里补上“MSVC v143 - VS 2022 C++ Spectre-mitigated libs”。编译成功后会在x64/Release/下生成SensyI2C.sys和SensyI2C.inf,这两个文件就是后续安装的全部素材。
3.2 驱动签名与测试模式安装
Windows 64 位系统强制要求内核驱动有数字签名,自己编译的驱动没有微软 WHQL 签名,所以必须开启测试模式或者用自签名证书。测试模式的方法是在管理员命令行里执行:
bcdedit /set testsigning on然后重启。重启后桌面右下角会出现“测试模式”水印,这是正常的。接下来用MakeCert或 PowerShell 的New-SelfSignedCertificate生成自签名证书,再用signtool给SensyI2C.sys签名:
signtool sign /fd SHA256 /a /f MyCert.pfx /p password SensyI2C.sys签名完成后,把.sys和.inf放到同一个目录,右键.inf选择“安装”。如果安装失败,去C:\Windows\INF\setupapi.dev.log看日志,最常见的错误是0xE000024B,意思是驱动签名无效,检查证书是否导入到“受信任的根证书颁发机构”和“受信任的发布者”里。
安装成功后,设备管理器里会出现一个“Sensy I2C Device”节点。如果节点带黄色感叹号,右键属性看错误代码。代码 10 通常是资源冲突,代码 52 是签名问题,代码 28 是驱动未安装。我一般会先用pnputil /enum-drivers确认驱动包是否被系统识别,再用devcon status查看设备状态。
3.3 用户态测试程序的编译与通信验证
用户态测试程序SensyTest.cpp的核心逻辑是打开设备、发送 IOCTL、打印结果。编译时链接setupapi.lib和user32.lib,如果用 Visual Studio 直接建一个空项目把源码拖进去就行。关键代码片段如下:
#include <windows.h> #include <stdio.h> #define IOCTL_I2C_READ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_I2C_WRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) int main() { HANDLE hDevice = CreateFile( L"\\\\.\\SensyI2C", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL ); if (hDevice == INVALID_HANDLE_VALUE) { printf("Open device failed: %lu\n", GetLastError()); return 1; } BYTE regAddr = 0x00; BYTE readBuf[2] = {0}; DWORD bytesReturned = 0; BOOL ok = DeviceIoControl( hDevice, IOCTL_I2C_READ, ®Addr, sizeof(regAddr), readBuf, sizeof(readBuf), &bytesReturned, NULL ); if (ok) { printf("Read: 0x%02X 0x%02X\n", readBuf[0], readBuf[1]); } else { printf("IOCTL failed: %lu\n", GetLastError()); } CloseHandle(hDevice); return 0; }这段代码里,CreateFile的设备路径\\\\.\\SensyI2C必须和驱动 INF 里定义的设备接口符号链接名一致。DeviceIoControl的输入缓冲区放寄存器地址,输出缓冲区接收数据。如果返回ERROR_INVALID_FUNCTION,说明 IOCTL 码没匹配上,检查驱动里EvtIoDeviceControl的OutputBufferLength和InputBufferLength判断逻辑。如果返回ERROR_IO_DEVICE,多半是 I2C 从设备没响应,用示波器看 SDA 线是否被拉低。
4. 避坑指南:I2C 驱动开发中最容易翻车的五个地方
4.1 现象:设备管理器显示驱动已加载,但 IOCTL 全部超时
原因:SPB_TARGET_CONFIG里的ConnectionSpeed设成了 400000,但实际硬件走线太长或上拉电阻太大,导致快速模式下信号边沿变缓,从设备采样出错。解决:先把速度降到 100000 测试,确认通信正常后再逐步提高。如果必须跑 400000,检查上拉电阻是否在 2.2k 到 4.7k 之间,走线是否超过 30cm。
4.2 现象:编译通过,但安装时提示“无法验证驱动程序的数字签名”
原因:自签名证书没有导入到“受信任的根证书颁发机构”,或者签名时用的哈希算法是 SHA1 而系统要求 SHA256。解决:用certmgr.msc手动导入证书到“受信任的根证书颁发机构”和“受信任的发布者”,签名命令加上/fd SHA256参数。如果还不行,检查bcdedit的testsigning是否真的开启了,有些主板 BIOS 里的 Secure Boot 会覆盖这个设置。
4.3 现象:用户态CreateFile返回ERROR_FILE_NOT_FOUND
原因:驱动没有创建符号链接,或者符号链接名和用户态代码里的不一致。解决:在EvtDriverDeviceAdd里调用WdfDeviceCreateSymbolicLink创建设备名,同时用WdfDeviceCreateDeviceInterface注册接口 GUID。用户态可以用SetupDiGetClassDevs按 GUID 枚举设备,这样比硬编码符号链接更可靠。
4.4 现象:连续读写多次后驱动蓝屏,错误码IRQL_NOT_LESS_OR_EQUAL
原因:在EvtIoDeviceControl回调里直接访问了分页内存,或者没有正确设置WDF_OBJECT_ATTRIBUTES的ExecutionLevel。解决:把设备上下文结构体用WdfMemoryCreate分配为非分页内存,或者在回调里调用WdfRequestFormatRequestUsingCurrentType确保请求格式正确。蓝屏问题用 WinDbg 分析 dump 文件最快,!analyze -v会直接指出出错的函数和行号。
4.5 现象:I2C 读取的数据偶尔错位,比如高字节和低字节颠倒
原因:从设备的数据格式是“高字节在前”还是“低字节在前”没搞清,或者驱动里SpbRequest的Direction参数设反了。解决:先查从设备数据手册确认字节序,然后在驱动里用SpbRequestGetParameters检查Direction是SpbTransferDirectionFromDevice还是SpbTransferDirectionToDevice。如果字节序确实需要交换,在用户态做比在驱动里做更安全,因为驱动里做字节交换容易引入竞态条件。
5. 进阶技巧:用 TraceView 抓 I2C 通信日志与性能调优
驱动跑通之后,下一步是验证稳定性和性能。Windows 自带的 ETW(Event Tracing for Windows)可以抓取 SPB 框架的内部日志,配合 TraceView 工具能看到每次 I2C 传输的耗时和返回值。具体做法是:在驱动里用WPP_INIT_TRACING宏初始化 WPP 跟踪,然后在关键函数入口加TraceEvents调用。编译时 WDK 会自动生成.tmh文件,用 TraceView 打开这个文件就能实时看到日志。
我一般会在EvtIoDeviceControl里加三行跟踪:进入时打印 IOCTL 码和缓冲区长度,调用SpbRequest前打印目标地址和速度,返回后打印状态码和实际传输字节数。这样一旦出现超时,能立刻定位是请求没发出去还是从设备没响应。性能调优方面,I2C 标准模式 100kHz 下每字节大约 90 微秒,快速模式 400kHz 下大约 22 微秒。如果你的应用需要连续读取多个寄存器,尽量合并成一次SpbRequest传输,减少 IOCTL 往返开销。实测下来,合并传输比逐字节读取快 3 到 5 倍。
还有一个容易被忽略的点:驱动的电源管理。I2C 设备在系统休眠时应该进入低功耗状态,在EvtDeviceD0Exit回调里把SPB_TARGET_CONFIG的ConnectionSpeed降到 100000 或者直接关闭 IoTarget,唤醒时再恢复。如果不做这一步,有些笔记本在合盖再打开后会丢失 I2C 设备,设备管理器里直接消失,只能重启。这个坑我踩过两次,后来在EvtDeviceD0Entry里加了重新打开 IoTarget 的逻辑才彻底解决。
最后说一个调试习惯:每次改完驱动配置,先卸载旧驱动再装新的。用devcon remove或者设备管理器里“卸载设备”并勾选“删除驱动程序软件”,否则系统可能缓存旧版本驱动,导致你改了代码但行为没变,白白浪费半天时间。希望帮到你。
本文还有配套的精品资源,点击获取