简介:本资源是一套面向嵌入式与Windows内核开发初学者的实践型源码项目,聚焦I2C设备在Windows平台下的全栈通信实现,涵盖用户态WinRT API调用、KMDF驱动开发及SPB总线适配,适用于计算机专业高年级学生、驱动开发入门者及IoT系统学习者。压缩包共38个文件,含12个头文件(定义常量、接口与设备结构)、9个C++源文件(分别实现WinRT通信、KMDF驱动逻辑、LCD显示控制及用户态交互程序),以及vcxproj工程文件、ASL固件描述、INX安装脚本等关键构建要素,整体体积仅1.83MB,轻量易导入。已有51人下载学习,适合通过可运行代码理解内核驱动与硬件交互机制。读者可直接复现从用户模式I2C读写、到KMDF多设备管理、再到驱动与应用层DeviceIoControl通信的完整链路,尤其适合掌握符号链接创建、IOCTL命令设计、传感器数据采集与液晶显示协同等典型内核开发场景。
1. 这不是 WinRT 小玩具:Sensy 是一套能真正跑通「用户态 I2C → KMDF 驱动 → SPB 总线 → 硬件传感器」全链路的 Windows 内核通信验证系统
你手头那块带 I2C 接口的温湿度传感器模块,插在 Surface Pro 或某款工控主板上,Windows 默认根本不认——它既不走 HID,也不走 USB CDC,更不会自动枚举成 COM 口。这时候,90% 的开发者会立刻切到 Linux 下用i2cdetect+i2cget搞定,然后叹气:“Windows 做嵌入式驱动?算了吧。” Sensy 就是专治这种“算了吧”心态的硬核项目。它不是演示 API 调用的 PPT 工程,而是一套从main.cpp用户程序发起读请求、经DeviceIoControl穿过 IRP、由 KMDF 驱动解析为 SPB_TRANSFER_REQUEST、最终通过 ACPI DSDT 描述的硬件资源(注意:含rpi2_dsdt.ASL和编译工具asl.exe)落到物理 I2C 控制器的完整闭环。项目里没有虚拟设备模拟器,所有通信路径都强制绑定真实硬件抽象层(HAL),连I2CDisplay.cpp里写屏逻辑都预留了 GPIO 复位时序钩子。适合两类人:一是正在啃 Windows 驱动开发但卡在“驱动装上了却收不到数据”的实战派;二是需要把国产工业传感器快速接入 Windows 生态、又不愿绕道 WSL2 的产线工程师。它解决的不是“能不能读”,而是“怎么让 Windows 正儿八经把你当 I2C 主机用”。
2. 从用户态 WinRT 到内核态 KMDF:Sensy 的三层通信架构拆解与选型依据
Sensy 不是单点技术验证,而是分层推进的工程实践。它的目录结构(src/,winrtapi/,kmdf/,exe/,sys/)本身就是一张清晰的架构图谱。理解每一层存在的必要性,比直接编译更重要——否则你很可能在DeviceIoControl返回ERROR_INVALID_HANDLE时,还在 WinRT 层查权限,而问题其实在 ACPI 表没加载。
2.1 为什么 WinRT API 是第一站?而非直接上驱动
WinRT 提供的Windows.Devices.I2c命名空间,是 Windows 10+ 中唯一官方支持用户态直接访问 I2C 的机制。但它有硬性前提:目标设备必须被系统识别为SPB 设备(Simple Peripheral Bus),且其 ACPI 描述符(DSDT)中需明确定义_HID、_CID和SPB兼容性字段。Sensy 的rpi2_dsdt.ASL就是为此而生——它并非 Raspberry Pi 2 的原始 DSDT,而是裁剪重构版,专门将 I2C 控制器(如I2C0)和挂载其上的传感器(如TMP102)声明为 SPB 子设备,并注入CompatibleId = "SPB"。这步不可跳过:没有这个 ACPI 声明,WinRT 的I2cController.GetDefaultAsync()永远返回 null。
提示:
rpi2_dsdt.ASL中关键段落如下(已脱敏):Scope (_SB.PCI0.I2C0) { Device (TMP2) { Name (_HID, "MSFT0001") // Microsoft SPB-compatible HID Name (_CID, "SPB") // Critical: declares SPB bus compatibility Name (_UID, 2) Method (_STA, 0, NotSerialized) { Return (0x0F) } Name (_CRS, ResourceTemplate () { I2cSerialBus(0x48, ControllerInitiated, 0x00061A80, AddressingMode7Bit, "\\_SB.PCI0.I2C0") }) } }编译命令:
asl.exe -sa rpi2_dsdt.ASL生成rpi2_dsdt.aml,需通过 UEFI 固件或acpidump工具注入运行时系统(具体方法见第 4 章)。
2.2 KMDF 驱动为何必须基于 SPB?绕过 WinRT 直接操作硬件行不行
可以,但代价极高。若不用 SPB,就得自己实现 I2C 控制器的 HAL 层:解析 PCI 配置空间、映射 MMIO 寄存器、编写 bit-banging 时序、处理中断优先级……而 SPB 框架已由 Windows 内核封装好这些。Sensy 的sensy.sys驱动核心逻辑在I2CDisplay.cpp中,它不直接读写0x48地址,而是调用SpbDeviceReadDataSynchronously()—— 这个函数内部会根据 ACPI 描述自动定位控制器、配置时钟、发送 START/STOP 信号。你只需传入SPB_TRANSFER_OPTIONS结构体,比如:
SPB_TRANSFER_OPTIONS options = {0}; options.RequestType = SpbRequestTypeRead; options.TransferSize = 2; // 读取温度值 2 字节 options.SpbAddress = 0x48; // TMP102 设备地址 options.Timeout = 1000; // ms status = SpbDeviceReadDataSynchronously( m_SpbContext, &options, &readBuffer, &bytesRead );关键参数说明:
m_SpbContext是驱动初始化时通过SpbDeviceInitialize()获取的句柄,它绑定了 ACPI 中定义的\\_SB.PCI0.I2C0.TMP2路径;SpbRequestTypeRead表明这是标准 I2C 读事务,无需手动构造寄存器地址字节(TMP102 的温度寄存器0x00已在I2CDisplay.h的TEMP_REG_ADDR宏中固化)。
2.3 用户模式应用如何与 KMDF 驱动安全通信?符号链接不是摆设
exe/目录下的可执行文件(如sensy_test.exe)通过CreateFileW(L"\\\\.\\SensyDriver", ...)打开驱动暴露的设备对象。这里的"\\\\.\\SensyDriver"是符号链接名,由驱动在EvtDriverDeviceAdd()中调用WdfDeviceCreateSymbolicLink()创建。它必须与sensy.vcxproj中DEVICE_NAME宏定义严格一致(查看constants.h):
#define DEVICE_NAME L"\\Device\\SensyDriver" #define SYMBOLIC_NAME L"\\DosDevices\\SensyDriver" // 符号链接指向此路径若你在
main.cpp中写CreateFileW(L"\\\\.\\Sensy", ...)却忘了改SYMBOLIC_NAME,就会得到ERROR_PATH_NOT_FOUND。更隐蔽的坑是:符号链接创建后,必须确保驱动服务已启动(sc start sensy),且当前用户有SeLoadDriverPrivilege权限(普通用户默认无)。Sensy 的README.md中InstallDriver.bat脚本实际执行三步:sc create注册服务、sc config设置启动类型为demand、sc start启动——缺一不可。
3. 编译、签名与部署全流程:Visual Studio 2022 + WDK 22H2 实操指南
Sensy 的构建不是点一下 “Build Solution” 就完事。KMDF 驱动必须经过微软数字签名才能在默认 Secure Boot 模式下加载,而 WinRT 应用依赖特定 SDK 版本。以下步骤经实测验证(环境:Windows 11 22H2,Visual Studio 2022 17.4,WDK 22H2):
3.1 解决方案配置:三个关键属性页必须同步修改
打开sensy.sln后,右键sensy项目 → Properties → Configuration Properties:
- General → Target Platform Version:必须设为
10.0.22621.0(即 Windows 11 22H2 SDK)。若设为10.0.19041.0(20H1),SpbDeviceReadDataSynchronously()会因未导出而链接失败。 - Driver Settings → Driver Model:选择
KMDF 1.33(对应 WDK 22H2)。旧版 KMDF(如 1.27)不支持SpbDeviceInitialize()的异步回调模式,会导致I2CDisplay.cpp中EvtDeviceD0Entry()初始化超时。 - Linker → Advanced → Entry Point:设为
DriverEntry(非mainCRTStartup)。KMDF 驱动入口是WdfDriverCreate(),不是 C 运行时入口。
注意:
sensy.filters文件定义了源码过滤器(如I2CDisplay.cpp归属Source Files),若 VS 提示“无法找到文件”,请右键项目 → Add → Existing Item,手动添加src/下所有.cpp/.h文件,并确保其“Item Type”在属性页中设为C/C++ compiler(而非None)。
3.2 驱动签名:自签名证书 + Test Signing 模式双保险
Windows 默认拒绝加载未签名驱动。Sensy 提供tools/目录下的makecert.bat(已重命名脱敏),但实际应使用现代方式:
- 以管理员身份运行 PowerShell,执行:
# 创建测试证书(有效期1年) $cert = New-SelfSignedCertificate -Type Custom -Subject "CN=SensyTestCert" ` -KeyUsage DigitalSignature -FriendlyName "Sensy KMDF Driver" ` -CertStoreLocation "Cert:\LocalMachine\My" ` -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3") # 导出公钥供驱动签名 Export-Certificate -Cert $cert -FilePath "SensyTestCert.cer" - 在 VS 的
sensy项目属性 → Configuration Properties → Linker → General → Enable Incremental Linking 设为No(增量链接会破坏签名哈希)。 - 构建后,用
signtool.exe签名(路径通常为C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe):signtool sign /a /s My /n "SensyTestCert" /t http://timestamp.digicert.com sensy.sys - 启用测试签名模式(重启生效):
bcdedit /set testsigning on shutdown /r /t 0提示:
testsigning on后,桌面右下角会出现“测试模式”水印,这是正常现象。若跳过此步,sc start sensy会返回Error 5: Access is denied。
3.3 WinRT 应用构建:SDK 版本与 Capabilities 的隐性耦合
winrtapi/下的main.cpp使用 C++/WinRT,需确保:
- 项目属性 → General → Windows SDK Version 设为
10.0.22621.0(同驱动)。 Package.appxmanifest中必须声明iot功能(即使不跑在 IoT Core 上):
此处<Capabilities> <uap:IotCapability Name="allJoyn"/> <rescap:Capability Name="iot" /> <!-- 关键!否则 I2cController.GetDefaultAsync() 返回 null --> </Capabilities>rescap命名空间需在<Package>根节点声明:xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"。漏掉这一行,编译通过但运行时权限不足。
4. 避坑:ACPI 加载失败、SPB 设备未枚举、DeviceIoControl 返回错误的五大血泪现场
Sensy 的调试难点不在代码逻辑,而在硬件抽象层与操作系统策略的咬合。以下是实测中高频翻车点,按现象→原因→解决结构化整理:
4.1 现象:I2cController.GetDefaultAsync()返回 null,main.cpp直接崩溃
原因:WinRT I2C API 仅响应 ACPI 中声明为SPB兼容的设备。若rpi2_dsdt.aml未正确加载,或SPB字段拼写错误(如写成spb小写),系统不会报错,而是静默忽略。
解决:
- 用
acpidump.exe(WDK 工具)导出当前系统 DSDT:acpidump -d -t DSDT; - 用
iasl.exe -d dsdt.dat反编译,搜索SPB字符串; - 若未找到,说明
rpi2_dsdt.aml未注入。需进入 UEFI 设置,关闭Secure Boot(临时),启用ACPI Custom Table Loading,再通过efibootmgr或厂商工具加载。
4.2 现象:sc start sensy成功,但CreateFileW返回ERROR_FILE_NOT_FOUND
原因:符号链接未创建成功。常见于驱动EvtDriverDeviceAdd()中WdfDeviceCreateSymbolicLink()调用失败,而开发者忽略了status返回值检查(I2CDisplay.cpp第 127 行附近)。
解决:
- 在
EvtDriverDeviceAdd()中添加日志:KdPrint(("Symbolic link status: 0x%08X\n", status));; - 若
status=0xC0000035(STATUS_OBJECT_NAME_COLLISION),说明同名链接已存在,需先sc stop sensy+sc delete sensy清理; - 检查
constants.h中SYMBOLIC_NAME是否含非法字符(如空格、中文)。
4.3 现象:DeviceIoControl返回ERROR_INVALID_PARAMETER,但输入缓冲区大小与驱动IOCTL_SENSY_READ_TEMP定义完全一致
原因:驱动EvtIoDeviceControl()中未正确校验InputBufferLength和OutputBufferLength。Sensy 的I2CDisplay.cpp第 321 行有if (outputBufferLength < sizeof(TEMPERATURE_DATA))检查,若main.cpp中TEMPERATURE_DATA结构体因对齐问题实际大小 >sizeof()计算值,校验失败。
解决:
- 在
I2CDisplay.h中强制指定结构体对齐:#pragma pack(push, 1) typedef struct _TEMPERATURE_DATA { INT16 rawValue; FLOAT temperatureC; } TEMPERATURE_DATA, *PTEMPERATURE_DATA; #pragma pack(pop) - 重新编译驱动与应用,确保双方
sizeof(TEMPERATURE_DATA)均为 6 字节。
4.4 现象:驱动加载后,SpbDeviceReadDataSynchronously()返回STATUS_DEVICE_BUSY,反复重试无效
原因:SPB 总线被其他驱动占用。典型场景是系统自带的i2c-smbus驱动(如i2c_smbus.sys)抢先枚举了同一 I2C 控制器,导致 Sensy 的SpbDeviceInitialize()获取句柄失败。
解决:
- 用
devmgmt.msc→ 查看 → 显示隐藏设备,展开System devices,查找I2C Controller设备; - 右键 → 属性 → 驱动程序 → 禁用该设备(非卸载);
- 或在注册表
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\i2c_smbus下,将StartDWORD 改为4(Disabled)。
4.5 现象:sensy_test.exe读取温度值恒为0x0000,但用逻辑分析仪抓包确认 I2C 波形正常
原因:TMP102 等传感器需在读取前发送配置字节(Configuration Register)。Sensy 的I2CDisplay.cpp第 288 行SpbDeviceWriteDataSynchronously()发送0x0060(12-bit 分辨率,连续转换模式),但若硬件上拉电阻不足(<4.7kΩ),ACK 信号不稳定,写入失败。
解决:
- 用万用表测量 SDA/SCL 线对地电压,正常应为
3.3V;若低于2.8V,更换上拉电阻; - 在
I2CDisplay.cpp的EvtIoDeviceControl()中,SpbDeviceWriteDataSynchronously()后增加SpbDeviceReadDataSynchronously()读回配置寄存器0x01,验证写入是否成功。
5. 硬件验证与跨平台移植技巧:用逻辑分析仪抓包反推 SPB 时序,以及 DSDT 适配新主板的三步法
Sensy 的终极价值不在跑通 Demo,而在于提供了一套可复用的硬件验证范式。当你把sensy.sys移植到某款国产工控主板时,最可靠的验证手段不是看日志,而是用 Saleae Logic 16 抓取真实的 I2C 波形,并与 SPB 框架生成的时序比对。这一步能帮你绕过 80% 的“驱动说没问题但硬件没反应”的玄学问题。
5.1 用逻辑分析仪定位 SPB 驱动时序偏差
SPB 框架虽封装了底层,但时序参数仍受硬件影响。例如:某些 I2C 控制器要求tBUF(总线空闲时间)≥ 5μs,而 SPB 默认值可能为 3μs。此时SpbDeviceReadDataSynchronously()会返回STATUS_TIMEOUT。
操作步骤:
- 在
I2CDisplay.cpp的EvtIoDeviceControl()中,在SpbDeviceReadDataSynchronously()调用前后各插入一个 GPIO Toggle(如控制 LED):// 前置 Toggle(假设 GPIO 12) GpioPinWrite(GpioHandle, 12, FALSE); status = SpbDeviceReadDataSynchronously(...); // 后置 Toggle GpioPinWrite(GpioHandle, 12, TRUE); - 将逻辑分析仪通道 0 接 LED 阳极,通道 1 接 SDA,通道 2 接 SCL;
- 触发条件设为通道 0 下降沿,采样率设为 10MHz;
- 抓包后观察:若 SDA/SCL 在 LED 低电平期间无任何波形,则
SpbDeviceReadDataSynchronously()根本未发起通信(驱动初始化失败);若波形出现但无 ACK,则tLOW或tHIGH参数需调整。
提示:SPB 时序参数在
SPB_TRANSFER_OPTIONS中不可调,需在驱动EvtDeviceD0Entry()中通过SpbDeviceSetTransferOptions()设置全局参数:SPB_TRANSFER_OPTIONS globalOpts = {0}; globalOpts.ClockFrequency = 100000; // 100kHz globalOpts.BusFreeTime = 5000; // tBUF = 5μs in nanoseconds SpbDeviceSetTransferOptions(m_SpbContext, &globalOpts);
5.2 DSDT 适配新主板的三步法:从acpidump到rpi2_dsdt.ASL的迁移
不同主板的 I2C 控制器在 ACPI 中的路径和寄存器偏移不同。直接修改rpi2_dsdt.ASL效率低下,推荐流程:
- 提取原厂 DSDT:在目标主板上运行
acpidump -b,得到dsdt.dat; - 定位 I2C 控制器:用
iasl -d dsdt.dat反编译,搜索I2C或IIC,找到类似Scope (_SB.PCI0.I2C0)的节点; - 重构 SPB 设备节点:复制
rpi2_dsdt.ASL中的TMP2设备定义,粘贴到原厂 DSDT 的I2C0Scope 内,仅修改三处:_HID值保持MSFT0001(微软 SPB 标准);I2cSerialBus()中的地址0x48替换为你的传感器地址;\\_SB.PCI0.I2C0路径替换为原厂 DSDT 中实际的控制器路径(如\\_SB.PCI0.I2C1)。
注意:
I2cSerialBus()的第五个参数是控制器路径字符串,必须与 DSDT 中Device (I2C0)的路径完全一致,包括大小写和反斜杠数量。多一个\或少一个.都会导致SpbDeviceInitialize()返回STATUS_OBJECT_PATH_NOT_FOUND。
5.3 从 Sensy 到量产:剥离教育代码,构建最小可信驱动集
Sensy 包含大量教学注释和冗余功能(如I2CDisplay.cpp中的 LCD 写屏逻辑),实际部署时需精简:
- 删除
I2CDisplay.cpp中所有// DISPLAY开头的代码块(约 120 行); - 将
EvtIoDeviceControl()中的IOCTL_SENSY_READ_TEMP简化为单一SpbDeviceReadDataSynchronously()调用,移除重试循环; sensy.vcxproj中移除#define DEBUG_LOG宏,关闭KdPrint日志(避免内核日志刷屏)。
最终驱动体积可压缩至 12KB 以内,满足工业设备固件 OTA 更新的带宽限制。从那以后我每次移植新硬件,都强制走一遍逻辑分析仪抓包 + DSDT 路径校验 + SPB 时序验证三步,宁可多花两小时,也不愿在产线上对着 0x0000 的温度值干瞪眼。希望帮到你。
本文还有配套的精品资源,点击获取