☰
Windows内核I2C驱动实战:从用户态到KMDF的完整通信链路
2026/9/28 1:55:41 网站建设 项目流程

简介:Sensy 是一套面向嵌入式与 Windows 内核驱动学习者的教育性质源码项目,围绕 I2C 设备通信展开,从用户模式逐步深入到 KMDF 驱动开发,适合具备一定 C++ 基础、希望理解 Windows 驱动框架与 SPB 总线机制的开发者参考实践。资源包共 38 个文件,以 h 头文件、cpp 源文件、vcxproj 工程文件为主,另含 asl 描述文件、inx 驱动安装信息、rc 资源脚本及 sln 解决方案等,压缩包约 1.83MB,结构覆盖用户模式应用与内核驱动两大模块。项目包含三条主线:使用 WinRT API 在用户模式下与 I2C 设备通信;开发可同时管理多个 I2C 设备的 KMDF 驱动,实现传感器温度读取并驱动液晶屏显示;以及通过符号链接与 DeviceIoControl 与驱动交互的用户态程序。已有 51 人学习,读者可借此完整梳理从应用层到内核层的通信链路,掌握驱动编译、部署与调试的基本思路。

1. 从用户态到内核态:Sensy 这套 I2C 通信系统到底能跑通什么

很多人第一次碰 I2C,都是在树莓派或者单片机上用现成库调通一个传感器就收工了。但如果你想知道 Windows 下从用户态一路打到内核态、用 KMDF 驱动去读写 I2C 设备是什么体验,Sensy 这个项目就是冲着这件事来的。它是一套完整的 Windows 驱动框架下的 I2C 设备通信系统源码,包含用户态 WinRT API 通信、KMDF 内核驱动、以及通过符号链接和 DeviceIoControl 做用户态与内核态交互的完整链路。项目定位是教育性质,但代码结构并不玩具——有 .sln 解决方案、.vcxproj 工程文件、.filters 过滤器配置,还有 rpi2_dsdt.ASL 这种 ACPI 描述表文件,说明它认真考虑过在真实硬件上跑起来需要什么。适合谁?适合已经会写 C++、想搞清楚 Windows 驱动模型和 SPB 框架怎么配合的嵌入式方向从业者,也适合做 Windows IoT 或者工控上位机、需要直接和 I2C 传感器打交道的工程师。你拿到这套源码,能省掉从零搭 KMDF 工程、配 WDF 模板、写 INF 文件这些反复翻车的环节。

2. 拆开 Sensy 的工程结构:哪些文件在干什么,先理清再动手

2.1 从解决方案到驱动入口:sensy.sln 与 kmdf 目录

拿到压缩包解压后,第一眼看到的是 sensy.sln 和 sensy.vcxproj,这是 Visual Studio 的解决方案和工程文件。用 VS 打开 .sln 之后,你会看到工程里至少有两个配置维度:一个是用户态应用(对应 winrtapi 目录下的 I2CDisplay.cpp、main.cpp),另一个是 KMDF 驱动(对应 kmdf 目录,编译产物是 .sys)。sensy.filters 是 VS 的筛选器文件,用来在 IDE 里按目录结构组织源文件,不影响编译结果,但能帮你快速定位代码。

kmdf 目录下是驱动核心。KMDF 驱动和传统 WDM 驱动的区别在于,KMDF 把大量样板代码(比如 IRP 处理、电源管理、即插即用状态机)封装成了框架对象,你只需要实现 EvtDriverDeviceAdd、EvtIoDeviceControl 这些回调。Sensy 的驱动主要做一件事:通过 SPB(Simple Peripheral Bus)框架和 I2C 设备通信,从传感器读温度,再把温度值写到 LCD 显示屏上。SPB 是 Windows 提供的一套抽象层,让驱动不用直接操作 I2C 控制器寄存器,而是调用 SpbTargetGetConnectionParameters 之类的 API 拿到连接参数,再用 I2C 请求对象发读写。

提示:如果你之前只写过用户态程序,第一次打开 KMDF 工程会觉得回调函数满天飞。建议先看 EvtDriverDeviceAdd 里怎么创建设备对象,再看 EvtIoDeviceControl 里怎么处理用户态发来的 IOCTL 请求,这条线理清了,整个驱动就通了。

2.2 用户态与内核态的桥:符号链接和 DeviceIoControl

用户态程序(winrtapi 目录)有两种通信方式。第一种是直接用 WinRT API 和 I2C 设备通信,这种方式不需要加载自定义驱动,适合快速验证硬件是否正常。第二种是通过符号链接打开 Sensy 驱动创建的设备对象,然后用 DeviceIoControl 发 IOCTL 控制码,让驱动去读写 I2C 设备。第二种方式才是这个项目的核心价值——它演示了完整的用户态到内核态再到硬件的调用链。

符号链接是驱动在 EvtDriverDeviceAdd 里通过 WdfDeviceCreateSymbolicLink 创建的,名字通常类似\\??\\SensyDevice或者\\DosDevices\\SensyDevice。用户态用 CreateFile 打开这个链接,拿到句柄后调 DeviceIoControl,传入控制码和输入输出缓冲区。控制码在 constants.h 里定义,驱动和用户态程序共用这个头文件,保证两边对 IOCTL 的理解一致。

// constants.h 中定义 IOCTL 控制码的常见写法 // 设备类型自定义,避免和系统已有类型冲突 #define SENSY_DEVICE_TYPE 0x8000 // 定义从传感器读温度的 IOCTL // METHOD_BUFFERED 表示输入输出都走系统缓冲区,适合小数据量 #define IOCTL_SENSY_READ_TEMP \ CTL_CODE(SENSY_DEVICE_TYPE, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) // 定义向 LCD 写数据的 IOCTL #define IOCTL_SENSY_WRITE_LCD \ CTL_CODE(SENSY_DEVICE_TYPE, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

上面这段代码的关键在于 CTL_CODE 宏的四个参数。第一个是设备类型,用 0x8000 以上的值可以避开系统已占用的范围。第二个是功能号,同一个设备类型下不同功能用不同编号。第三个是缓冲方式,METHOD_BUFFERED 最省心,系统帮你把用户态缓冲区拷到内核态,不用自己做 ProbeForRead/ProbeForWrite。第四个是访问权限,FILE_ANY_ACCESS 表示不限制。参数改错会导致 DeviceIoControl 返回 ERROR_INVALID_PARAMETER,这是新手最常见的翻车点之一。

2.3 ACPI 描述表与硬件枚举:rpi2_dsdt.ASL 的作用

rpi2_dsdt.ASL 是 ACPI 源语言文件,配合 asl.exe 编译器使用。在 Windows IoT 或者嵌入式 Windows 上,I2C 设备不是自动就能被驱动发现的,需要在 ACPI 表里声明设备节点,告诉系统这个 I2C 设备挂在哪个控制器下、地址是多少、用哪个中断。ASL 文件编译成 AML 字节码后,由固件在启动时加载,Windows 的 ACPI 驱动解析后创建对应的设备对象,你的 KMDF 驱动才能通过 SPB 框架拿到连接参数。

如果你是在树莓派 2 上跑 Windows IoT,rpi2_dsdt.ASL 就是给这个板子定制的。里面会有一段类似这样的设备声明:

// 在 ACPI 表中声明一个 I2C 设备 Device (I2C1) { Name (_HID, "SENSY0001") // 硬件 ID,驱动 INF 里要匹配这个 Name (_CRS, ResourceTemplate () { I2cSerialBus ( 0x48, // I2C 从设备地址 ControllerInitiated, // 由控制器发起传输 400000, // 总线速度 400kHz AddressingMode7Bit, // 7 位地址模式 "\\_SB.I2C1", // 所属 I2C 控制器路径 0, ResourceConsumer) }) }

这段 ASL 里,_HID 是硬件标识,驱动的 INF 文件里必须有一个匹配的硬件 ID,否则驱动不会加载。_CRS 里的 I2cSerialBus 描述了从设备地址、总线速度和地址模式。地址 0x48 是常见温度传感器(比如 LM75)的默认地址,400000 表示 400kHz 快速模式。如果你换了一个地址不同的传感器,这里必须改,否则驱动发出去的请求没人应答,表现为读超时。

注意:ASL 文件不是随便改改就能用的,编译成 AML 后要替换固件里的表,或者通过 Windows 的 ACPI 表加载机制注入。不同板子的方法不一样,树莓派上通常要改 UEFI 固件里的 ACPI 表。这一步没有统一教程,得看你手头的硬件文档。

3. 把 Sensy 跑起来:环境配置、编译和第一次通信

3.1 驱动开发环境:WDK 版本和 VS 工作负载

编译 KMDF 驱动需要 Visual Studio 加上 Windows Driver Kit(WDK)。版本匹配很重要——VS 2019 配 WDK 2004 或者 VS 2022 配 WDK 22H2,混搭会出现找不到 wdf.h 或者链接错误。安装的时候在 VS Installer 里勾选「使用 C++ 的桌面开发」和「使用 C++ 的 Windows 驱动程序开发」两个工作负载,WDK 会作为可选组件出现在单个组件列表里。

装完之后打开 sensy.sln,检查每个工程的「目标平台版本」和「目标 OS 版本」。驱动工程的配置属性里,Driver Settings 下面有「Target OS Version」和「Target Platform」,一般选 Windows 10 或更高,平台选 Desktop。如果目标平台选错,编译出来的 .sys 在目标机器上加载会报 0xC000035F(STATUS_INVALID_IMAGE_WIN_32)之类的错误。

# 用 MSBuild 命令行编译整个解决方案的示例 # 先找到 VS 的开发者命令提示符,或者手动设置环境变量 msbuild sensy.sln /p:Configuration=Debug /p:Platform=x64 /t:Rebuild # 编译完成后,驱动 .sys 文件在 kmdf\x64\Debug\ 目录下 # 用户态 exe 在 winrtapi\x64\Debug\ 目录下

用命令行编译的好处是能看清每一步的输出,哪个文件报错、哪个链接找不到符号,一目了然。参数 /t:Rebuild 表示强制重新编译所有文件,避免增量编译缓存导致的玄学问题。如果你在 VS 里编译,注意看输出窗口的「生成」标签页,不要只看错误列表,有些警告在错误列表里不显示但会影响驱动加载。

3.2 部署驱动:测试签名和 devcon 安装

Windows 64 位系统默认要求驱动有数字签名,自己编译的测试驱动需要开启测试签名模式。在管理员命令提示符里执行:

# 开启测试签名模式,重启后生效 bcdedit /set testsigning on # 重启电脑 shutdown /r /t 0

重启后桌面右下角会出现「测试模式」水印,这是正常的。然后可以用 devcon 工具安装驱动,devcon 在 WDK 的 tools 目录下能找到。

# 用 devcon 安装驱动,inf 文件在 kmdf 工程目录下 devcon install sensy.inf "ACPI\SENSY0001" # 如果已经安装过旧版本,先更新 devcon update sensy.inf "ACPI\SENSY0001" # 查看设备是否正常启动 devcon status "ACPI\SENSY0001"

devcon install 后面的硬件 ID 必须和 ASL 里 _HID 定义的一致,否则会提示「找不到匹配的设备」。如果设备管理器里出现黄色感叹号,右键属性看「设备状态」,错误码 52 表示签名问题,错误码 10 表示驱动启动失败,通常是 EvtDriverDeviceAdd 里某个 WDF 调用返回了错误但没处理。

3.3 用户态程序调用:WinRT API 和 DeviceIoControl 两条路

用户态程序里,WinRT API 那条路适合快速验证 I2C 硬件。用 C++/WinRT 的话,大致流程是:先通过 I2cController::GetDefaultAsync 拿到默认控制器,然后 I2cController::GetDevice 用从设备地址打开设备,最后用 I2cDevice::WriteRead 或者 Read 发请求。

// 用 C++/WinRT 直接和 I2C 设备通信的简化示例 #include <winrt/Windows.Devices.I2c.h> using namespace winrt::Windows::Devices::I2c; // 打开默认 I2C 控制器上的从设备 I2cConnectionSettings settings(0x48); // 从设备地址 settings.BusSpeed(I2cBusSpeed::FastMode); // 400kHz settings.SharingMode(I2cSharingMode::Exclusive); auto controller = I2cController::GetDefaultAsync().get(); auto device = controller.GetDevice(settings); // 读一个字节的温度寄存器 uint8_t reg = 0x00; uint8_t buffer[2] = { 0 }; device.WriteRead({ &reg, 1 }, buffer); // buffer[0] 和 buffer[1] 组成温度值,具体格式看传感器手册

这段代码里,I2cConnectionSettings 的地址和 ASL 里的地址要一致。BusSpeed 选 FastMode 对应 400kHz,如果传感器只支持 100kHz 标准模式,这里要改成 StandardMode,否则通信会出错。WriteRead 的第一个参数是要写的寄存器地址,第二个参数是读回来的数据缓冲区。注意 WinRT API 在桌面版 Windows 上需要设备有对应的 ACPI 描述,不是所有主板都暴露 I2C 控制器给用户态。

DeviceIoControl 那条路则是打开驱动创建的符号链接,发 IOCTL 控制码。这种方式的好处是驱动可以在内核态做更多事情,比如同时控制多个 I2C 设备、处理中断、做数据缓冲。

// 用户态通过符号链接和 DeviceIoControl 和 Sensy 驱动通信 #include <windows.h> #include "constants.h" HANDLE hDevice = CreateFile( L"\\\\.\\SensyDevice", // 符号链接名,注意前缀 GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hDevice == INVALID_HANDLE_VALUE) { // 驱动没加载或者符号链接名不对 return -1; } uint8_t tempBuffer[4] = { 0 }; DWORD bytesReturned = 0; BOOL ok = DeviceIoControl( hDevice, IOCTL_SENSY_READ_TEMP, // 控制码,和驱动里定义的一致 nullptr, 0, // 输入缓冲区 tempBuffer, sizeof(tempBuffer), // 输出缓冲区 &bytesReturned, nullptr); if (ok) { // tempBuffer 里是驱动读回来的温度数据 } CloseHandle(hDevice);

CreateFile 的路径前缀\\\\.\\是 Windows 设备命名空间的固定写法,后面跟符号链接名。符号链接名要和驱动里 WdfDeviceCreateSymbolicLink 创建的名字完全一致,大小写不敏感但拼写不能错。DeviceIoControl 的输入输出缓冲区大小要和驱动里 EvtIoDeviceControl 处理的预期一致,驱动里用 WdfRequestRetrieveInputBuffer 和 WdfRequestRetrieveOutputBuffer 拿缓冲区,如果用户态传的大小不对,这两个调用会返回 STATUS_BUFFER_TOO_SMALL。

4. 避坑与排查:I2C 驱动开发里那些让人抓狂的瞬间

4.1 设备管理器黄色感叹号,错误码 10

现象:驱动安装后设备管理器里显示黄色感叹号,属性里设备状态是「该设备无法启动(代码 10)」。

原因:EvtDriverDeviceAdd 回调里某个 WDF 调用失败了,但代码没有检查返回值,框架直接让设备启动失败。常见的是 WdfDeviceCreate 或者 WdfIoQueueCreate 返回了错误状态。

解决:在 EvtDriverDeviceAdd 里每一步 WDF 调用后都加 NT_SUCCESS 检查,失败时用 WdfDeviceSetFailed 或者直接返回错误状态。调试时可以在驱动里加 DbgPrintEx 输出,然后用 DebugView 抓内核日志。重点看 WdfDeviceCreate 的返回值,如果返回 STATUS_INSUFFICIENT_RESOURCES,通常是 WDF 对象属性配置有问题。

4.2 DeviceIoControl 返回 ERROR_INVALID_PARAMETER

现象:用户态调 DeviceIoControl 失败,GetLastError 返回 87(ERROR_INVALID_PARAMETER)。

原因:控制码不匹配,或者输入输出缓冲区大小和驱动预期不一致。控制码在 constants.h 里定义,但用户态和驱动如果引用了不同版本的头文件,功能号或者缓冲方式可能不同。

解决:确认用户态工程和驱动工程引用的是同一个 constants.h。检查 CTL_CODE 的第三个参数,METHOD_BUFFERED 要求输入输出都走系统缓冲区,驱动里用 WdfRequestRetrieveInputBuffer 和 WdfRequestRetrieveOutputBuffer 拿到的指针是系统缓冲区的,不要直接当用户态指针用。如果驱动里用了 METHOD_NEITHER,那用户态传的指针就是原始用户态地址,驱动必须自己做 ProbeForRead/ProbeForWrite,这种方式容易出安全漏洞,不建议新手用。

4.3 I2C 读超时,传感器没反应

现象:驱动发 I2C 读请求后一直等不到完成,最终超时返回 STATUS_IO_TIMEOUT。

原因:从设备地址不对,或者总线速度不匹配,或者传感器根本没有上拉电阻。I2C 总线需要 SDA 和 SCL 上有上拉电阻,很多传感器模块自带,但如果你直接接芯片,忘了加上拉,总线电平拉不上去,通信必然失败。

解决:先用示波器或者逻辑分析仪看 SDA 和 SCL 波形,确认地址字节发出去后有没有 ACK。如果没有 ACK,检查从设备地址——7 位地址左移一位后才是写地址,读地址再加 1。比如 0x48 的写地址是 0x90,读地址是 0x91。ASL 和 WinRT API 里填的是 7 位地址,但有些底层驱动 API 要的是 8 位地址,这个转换很容易搞混。另外确认总线速度,400kHz 模式下线缆太长或者上拉电阻太大都会导致波形边沿变缓,降速到 100kHz 试试。

4.4 驱动加载后系统蓝屏,错误码 0x0000007E

现象:安装驱动后系统直接蓝屏,错误码 0x7E(SYSTEM_THREAD_EXCEPTION_NOT_HANDLED)。

原因:驱动里访问了无效内存,或者在没有获取对象引用的情况下使用了 WDF 对象。KMDF 里每个 WDF 对象都有引用计数,如果你在 EvtIoDeviceControl 里把请求对象保存到全局变量,但没有调 WdfObjectReference,请求完成后对象被释放,后续访问就是野指针。

解决:不要在驱动里保存 WDF 对象的裸指针到全局变量。如果确实需要跨回调使用,用 WdfObjectReference 增加引用计数,用完再 WdfObjectDereference。另外检查所有 WdfRequestRetrieveOutputBuffer 返回的缓冲区指针,只在当前回调内使用,不要传到别的线程。蓝屏问题用 WinDbg 分析 dump 文件最快,加载符号后看调用栈,通常能直接定位到出问题的驱动函数。

4.5 测试签名模式开了但驱动还是装不上

现象:bcdedit 显示 testsigning 已经是 on,但 devcon 安装驱动时提示签名验证失败。

原因:驱动 .sys 文件没有用测试证书签名,或者证书没导入到受信任的根证书颁发机构。测试签名模式只是允许加载测试签名的驱动,不是允许加载完全没签名的驱动。

解决:用 VS 自带的签名工具或者 makecert + signtool 生成测试证书,导入到「受信任的根证书颁发机构」和「受信任的发布者」两个存储区。然后在驱动工程的属性里配置「测试证书」和「签名」选项,让 VS 编译后自动签名。如果手动签名,命令是signtool sign /v /s TestCertStore /n CertName /t http://timestamp.digicert.com sensy.sys。注意时间戳服务器地址可能因为网络原因连不上,连不上就去掉 /t 参数,但这样签名证书过期后驱动会失效。

5. 进阶技巧:用 WinDbg 和 ETW 把驱动行为看透

驱动调试最痛苦的地方在于它跑在内核态,printf 那套完全用不了。Sensy 这种涉及 I2C 通信的驱动,出问题时你不仅要知道驱动有没有收到请求,还要知道 I2C 总线上实际发了什么。我一般会同时开两条线:一条用 WinDbg 做内核调试,另一条用 ETW 抓 SPB 框架的日志。

WinDbg 内核调试需要两台机器,一台跑目标系统,一台跑调试器,用串口或者网络连接。配置好之后,在调试器里可以下断点、看调用栈、检查内存。对于 Sensy 驱动,我通常会在 EvtIoDeviceControl 入口下断点,确认用户态的 IOCTL 有没有正确到达驱动。命令是bp sensy!EvtIoDeviceControl,然后g让目标机继续跑,用户态一发请求就会断下来。断下来之后用kb看调用栈,dv看局部变量,dt看结构体内容。

ETW 那条线更轻量,不需要两台机器。Windows 自带 SPB 框架的 ETW 提供程序,用 logman 或者 tracelog 可以抓 I2C 传输的详细日志。

# 启动 ETW 跟踪,抓 SPB 框架的日志 logman start SensyTrace -p Microsoft-Windows-SPB -o sensy.etl -ets # 运行你的用户态程序,触发 I2C 通信 # 然后停止跟踪 logman stop SensyTrace -ets # 用 tracerpt 把 etl 转成文本 tracerpt sensy.etl -o sensy.txt -of CSV

转出来的 CSV 里能看到每次 I2C 传输的从设备地址、读写方向、数据长度和完成状态。如果某次传输状态是「超时」或者「NACK」,你就知道问题出在硬件层而不是驱动逻辑层。这个方法的血泪经验是:ETW 日志的时间戳精度比 DbgPrint 高得多,而且不需要重新编译驱动,生产环境也能用。

还有一个容易被忽略的点是电源管理。KMDF 驱动默认支持电源状态转换,如果你的 I2C 设备在系统休眠后没有正确恢复,可能是 EvtDeviceD0Entry 回调里没有重新初始化设备。Sensy 的驱动里如果有电源管理回调,检查一下 D0Entry 里有没有重新配置 I2C 连接参数。我见过太多案例是休眠唤醒后设备不工作,查了半天发现是电源状态转换时丢了上下文。

从那以后我每次写 KMDF 驱动,都强制走一遍「用户态发请求 → 驱动收请求 → I2C 传输 → 返回结果」的完整链路,每一步都加 ETW 日志,不靠猜。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询