Windows下通过Smbus与设备通信
前段时间实习公司的项目有一个在Windows通过SMBus与设备通信的需求,原因是设备要插在PCIe slot上,而该slot没有USB的pin定义,于是领导想让我用SMBus实现。当时属于一个计算机小白,刚接到该任务的时候真是一点头绪没有,网上也几乎找不到这方面的参考文章。经过一段时间的摸索终于完成了这个功能,于是打算写本文章回顾一下,顺便向网友分享。
平台:
- Windows10 64-bit
- Intel PCH芯片组
路线确定:简单说说我的探索过程。
拓扑图:逻辑结构图。
编程步骤:具体的编程实现可以直接跳转这里。
路线确定
一开始在bing下搜索smbus关键词看看有没有现成的库函数或者文章,无果。寻求copilot帮助,生成了一些代码,也只是Linux平台下的。接着去Github搜索,出来的也基本是Linux平台下的代码,有些看着像windows平台的也没有说明文档,作为几乎零基础的小白实在无法判断。
之后去MSDN等地查看驱动相关的文章,读了不少,也没有适合本人目前阶段的参考内容。最后,还是在Github,注意到了一个开源项目叫OpenRGB,阅读其readme文件了解到这个项目的作用是替代个人家用主机中乱七八糟各品牌的RGB灯光驱动软件,可以使用该软件来统一调度不同品牌的ddr、显卡、主板、鼠标等RGB灯光。更进一步了解到市面上的厂商的RGB灯光控制大部分使用SMBus协议,于是觉得这个项目可以有不小的参考。
幸运的是,该项目的文档写的很好,其中对SMBus部分就有简要的介绍。这里简单介绍了一下有关的拓扑结构,也提到了Linux平台下有现成的i2c库支持你访问主板上的i2c/SMBus设备,而Windows并没有类似的东西,所以作者重新写了Windows平台下的驱动(我也是在此刻深刻感受到了Windows平台开发的不容易)。总之,我反复阅读了作者的文档之后知道了我可能会用上一个库叫WinRing0。
于是我接着去翻阅WinRing0的文档,了解到其可以用来access到x86/64 Windows下的IO port, MSR, PCI,再联想到之前有在设备管理器看到过SMBus Controller是位于PCI上的,觉得WinRing0将会是一个很有用的工具。之后从Intel官网获取到了PCH datasheet,上面详细介绍了SMBus Controller的PCI配置信息以及其IO Space的信息。通过反复阅读datasheet终于理清了实现我想要的功能的编程步骤。
调用PCH上的SMBus Controller的方法:
- 查阅对应的PCH datasheet,确定SMBus Controller的PCI地址(通常是bus 0, device 31, function 4)
- 查看PCI配置空间,确定Controller是否Enable,以及IO Space中的Base Address
- 访问对应的IO Space,修改其中的寄存器达成读写
拓扑图

方便理解,上图是硬件连接的简易示意图,我们实际上并不是直接与slave device通信,而是通过调用SMBus Controller的接口来间接与slave device通信。MSDN上对这种结构也有简单的介绍:

编程步骤
接下来介绍编程的主要部分。
下载好WinRing0后,新建Visual Studio解决方案,添加WinRing0动态链接库,前置工作完成。
以Intel 500系列PCH举例子,首先检查SMBus Controller的PCI配置空间,Command寄存器中的IO Space Enable位和Host Configuration寄存器中的HST_EN位是否置一,不是则手动置一,然后读取IO空间的基地址。



//伪代码
uint8_t pci_command = pci_read_byte(SMBUS_LOCATION, COMMAND);
uint8_t pci_host_configuration = pci_read_byte(SMBUS_LOCATION, HOST_CONFIGURATION);
uint16_t smb_bar = 0;
if (!(pci_command & 0x01)) {
pci_write_byte(SMBUS_LOCATION, COMMAND, pci_command | 0x01);
}
if (!(pci_host_configuration & 0x01)) {
pci_write_byte(SMBUS_LOCATION, HOST_CONFIGURATION, pci_host_configuration | 0x01);
}
smb_bar = (uint16_t)pci_read_word(SMBUS_LOCATION, SMA_BASE_ADDRESS);
然后可以访问IO空间来修改其中的寄存器以控制Controller,通常我们只用得到0h - 6h这七个寄存器。

Host Status Register Address:

首先将低6位置一,这会使Controller清空标志位以开始下一次传输。注意,如果In Use Status为1的话说明该Controller正在使用中(一般没人用这个,直接用吧,不影响我们通信就行),当通信成功时,INTR位为1,否则第2-4位其中一个会为1。
Host Control Register:

这位更是重量级寄存器,你需要在SMB_CMD位选择你要使用的smbus protocol,具体可选哪些协议需要参考Volume 1的27.4.1章节,当START置1时,Controller会根据你的寄存器设置开始通信。(建议INTREN位也置1)
之后的几个寄存器就相对简单了:
- Host Command Register: 放发给slave的command。
- Transmit Slave Address Register: smbus slave的地址。
- Data 0\1 Register: write时需要将数据放这,read时Controller返回的数据会在这里。
所以,以read byte操作举例,IO空间的操作步骤为:
//伪代码
uint8_t result = 0;
while (io_port_read_byte(smb_bar + IO_HOST_STATUS) & 0x40) {
//被占用了
wait();
}
io_port_write_byte(smb_bar + IO_HOST_STATUS, 0x1e);//清空标志位
io_port_write_byte(smb_bar + IO_TRANSMIT_SLAVE_ADDRESS, your_slave_addr | 0x01);//写入slave的地址
io_port_write_byte(smb_bar + IO_HOST_COMMAND, your_command);//写入command
io_port_write_byte(smb_bar + IO_HOST_CONTROL, 0x49);//开始传输
while (io_port_read_byte(smb_bar + IO_HOST_STATUS) & 0x01) {
//等待Host执行完逻辑
wait();
}
result = io_port_read_byte(smb_bar + IO_DATA_0);