ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Windows内核I2C驱动实战:从用户态到KMDF的完整通信链路

Windows内核I2C驱动实战:从用户态到KMDF的完整通信链路 简介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 的驱动主要做一件事通过 SPBSimple 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 IoTrpi2_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 KitWDK。版本匹配很重要——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 在目标机器上加载会报 0xC000035FSTATUS_INVALID_IMAGE_WIN_32之类的错误。# 用 MSBuild 命令行编译整个解决方案的示例 # 先找到 VS 的开发者命令提示符或者手动设置环境变量 msbuild sensy.sln /p:ConfigurationDebug /p:Platformx64 /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\SENSY0001devcon 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 返回 87ERROR_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现象安装驱动后系统直接蓝屏错误码 0x7ESYSTEM_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 日志不靠猜。希望帮到你。本文还有配套的精品资源点击获取
返回列表