Windows下I2C总线的GPIO模拟
2017-03-24邹应双
邹应双
摘要:简要介绍了I2C总线操作和基于Windows内核模式驱动的用户态I/O端口访问,分析了Windows平台下GPIO管脚模拟I2C总线的可行性,讲解了编程实现过程,连接I2C接口的安全芯片进行了验证。
关键词:I2C总线;GPIO管脚;Windows;内核模式驱动
中图分类号:TP311 文献标识码:A 文章编号:1009-3044(2017)01-0100-04
Abstract: I2C bus operations and user-mode I/O port access using Windows kernel mode driver are briefly introduced in this paper. And a feasibility analysis on simulating I2C bus with GPIO pins under Windows is made. Then we do a programming impelmentation to the simulating method, and verify the software by accessing an I2C-interfaced security chip.
Key words: I2C bus; GPIO pins; Windows; kernel mode driver
1 背景
I2C總线是Philips公司推出的两线式串行总线,用于嵌入式系统连接各种低速外围设备,如RTC、EEPROM、传感器、安全芯片等。许多单片机、嵌入式芯片等都带有I2C主控器,其采用的操作系统如嵌入式Linux等均带有I2C驱动程序,编程中可直接使用。对于不含I2C主控器的芯片,为了满足定制系统设计需求,一般也有大量的GPIO管脚,可用于软件模拟I2C总线。
Intel公司的X86系列CPU和配套的桥接芯片,除了用于桌面PC,还广泛用于网关、收银、视频等各种嵌入式服务器。这类服务器一般采用Windows Server操作系统,不带有I2C主控器或不提供I2C驱动程序。为了实现软件授权和保护,这类嵌入式服务器首先会选择管脚少、性价比高的I2C接口的安全芯片。为了满足这种需求,一种方法是采用USB转I2C的专用芯片,但这将增加硬件成本和软件复杂度。
基于I2C总线的简单性,可选用桥接芯片的GPIO管脚来模拟I2C主控器。GPIO管脚通过X86的I/O指令即可完全控制;但普通应用程序在Windows下无法直接访问I/O端口,可通过内核模式驱动程序来实现。在Windows平台下通过GPIO管脚模拟I2C总线,将是一种简单有效、低成本的解决方法。本文就这个模拟过程进行探讨。
2 I2C总线的读写
I2C总线通过时钟和数据两根线即可实现完善的同步数据传输。当发送数据时,一个设备作为主机,另一个设备作为从机。主设备为数据传输产生时钟信号。I2C通讯协议要求在时钟线(SCL, Serial Clock Line)处于低电平时,数据线(SDA, Serial Data Line)才能变化。协议中每个从设备都有一个地址,会一直监视总线上的主设备要初始化数据传输时发出的地址并匹配。
总线的工作流程如下:
空闲:当总线上没有数据通讯发生时,SCL和SDA通过上拉电阻呈高电平。
开始:SCL为高时,SDA由高变低,这时数据传输开始。
地址:主设备发送地址信息,包含7位的从设备地址和1位的读写位(表明数据流的方向)。发送完一个字节后,从设备会发送一位的认可位(ACK)。
数据:根据方向位,数据在主设备和从设备之间传输。数据一般以8位传输,高位在前。接收器上用一位的ACK表明一个字节收到了。传输可被终止或重新开始。
停止:当SCL为高时,SDA由低变高,这时数据传输结束,总线重新进入空闲状态。
一次完整的数据传输时序如图1所示。
标准I2C总线的传输速率是100KHz,通过线与逻辑实现慢速设备等待。I2C总线的这些特性允许主设备的功能通过两个GPIO管脚模拟而实现。
3 Windows下的I/O端口访问
端口I/O指令允许X86 CPU与系统中的其他硬件设备通信。对于硬件设备的低层次直接控制,C函数_inp()和_outp()(用X86处理器的IN和OUT指令实现)允许从端口读入或向一个端口写。但在Windows应用程序中插入_inp()或者_outp(),将导致特权指令异常消息,并给出终止或调试出错应用程序的选择。Windows的体系结构决定了应用程序不能直接通过IN和OUT指令访问硬件。否则,应用程序可以关闭中断、破坏显示或驱动器等硬件设备,危及系统的稳定性。所以,通过内核模式驱动程序间接访问I/O端口是Windows下访问硬件资源的唯一途径。
实现对I2C主控器的模拟,只需要简单的I/O访问即可实现。如果编写完整的内核模式的I2C驱动程序,将涉及复杂的、花费大量时间的Windows内核模式驱动驱动程序的开发和调试工作。编写最简驱动实现I/O端口访问,封装好用户态访问的接口,将I2C实现代码放在用户态,将极大地简化开发工作,同时增加二次开发利用的灵活性。
这样通过内核模式驱动程序实现I/O访问的副作用是每一次I/O操作都要通过Windows的I/O子系统发送请求,需要花费数千个时钟周期。但这个时间成本和100KHz的慢速I2C的一个位周期相当,对于数据传输量不大的应用,在性能上可接受。
4 编程实现
本文的目标硬件平台为Intel Core i3-4330 CPU、Intel DH82H81桥接芯片、Maxim DS28C22安全芯片。桥片和安全芯片的连接如图2所示。
本文的目標软件平台是64位的Windows Server 2008,开发平台是Windows 10 专业版,选用WDK(Windows Driver Kit) 7.1和Visual Studio 2015专业版。通过查询方式实现I2C读写,驱动层提供I/O端口访问功能,pioctl.dll库封装驱动成类似于IN/OUT指令的接口,i2c.dll实现I2C读写函数,提供给上层做应用开发。软件层次结构如图3所示。
下面按自底向上的顺序简单介绍各层次的实现源码。
4.1 内核模式驱动
和Linux驱动的开发相比,Windows驱动开发的门槛要高一些,首先需安装WDK,了解其中的核心态函数,熟悉WDM、WDF等驱动程序框架。
WDK中提供了大量的样例驱动供驱动开发者参考。考虑到我们的驱动只需提供X86的IN和OUT指令的访问接口,特选择WDK样例中源码最简单的src/general/ioctl/wdm为基础,命名为pioctl,并对驱动源码中的函数名等做适当重命名,添加上I/O端口访问的代码,即实现了本驱动。这个开发过程不需要对Windows驱动开发有较深入的了解。
本驱动程序的驱动加载和卸载、设备打开和关闭等例程无新加代码,不是本文的重点,下面仅对I/O端口操作相关的代码做说明。
NTSTATUS PioctlDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PIO_STACK_LOCATION irpSp;
NTSTATUS ntStatus = STATUS_SUCCESS;
ULONG inBufLength, outBufLength;
ULONG dataBufSize;
PULONG pIOBuffer;
ULONG nPort;
irpSp = IoGetCurrentIrpStackLocation(Irp);
inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;
switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
{ // 检查用户态参数
case IOCTL_PIOCTL_WRITE_PORT_ULONG:
dataBufSize = sizeof(ULONG);
if (inBufLength < (sizeof(ULONG) + dataBufSize)) {
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}
break;
case IOCTL_PIOCTL_READ_PORT_ULONG:
dataBufSize = sizeof(ULONG);
if (inBufLength != sizeof(ULONG) || outBufLength < dataBufSize) {
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}
break;
default:
ntStatus = STATUS_INVALID_PARAMETER;
goto End;
}
pIOBuffer = (PULONG)Irp->AssociatedIrp.SystemBuffer;
nPort = *pIOBuffer; // I/O端口号
switch ( irpSp->Parameters.DeviceIoControl.IoControlCode )
{ // 判定I/O控制码
case IOCTL_PIOCTL_READ_PORT_ULONG: // IND
*(PULONG)pIOBuffer = READ_PORT_ULONG((PULONG)((ULONG_PTR)nPort));
pIrp->IoStatus.Information = dataBufSize; // 读取的字节数
break;
case IOCTL_PIOCTL_WRITE_PORT_ULONG: // OUTD
pIOBuffer++;
WRITE_PORT_ULONG((PULONG)((ULONG_PTR)nPort), *(PULONG)pIOBuffer);
Irp->IoStatus.Information = dataBufSize; // 写的字节数
break;
default:
Irp->IoStatus.Information = 0;
ntStatus = STATUS_INVALID_DEVICE_REQUEST;
break;
}
End:
Irp->IoStatus.Status = ntStatus;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return ntStatus;
}
4.2 用户态I/O端口读写接口
为了应用程序的开发方便,实现了pioctl.dll库,以负责自动动态加载卸载驱动程序、提供I/O端口的用户态访问接口。
1)DLL入口函数
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
if (pioctl_init() < 0) // 利用SCM函数加载驱动; 打开设备文件
return FALSE;
break;
case DLL_PROCESS_DETACH:
pioctl_deinit(); // 关闭设备文件; 利用SCM函数卸载驱动
break;
}
return TRUE;
}
2)端口访问函数
PIOCTL_API unsigned long pioctl_inpd(unsigned short port)
{
ULONG PortNumber = (ULONG)port;
ULONG Data;
ULONG bytesReturned;
BOOL bRc;
bRc = DeviceIoControl(s_hDevice, (DWORD)IOCTL_PIOCTL_READ_PORT_ULONG,
&PortNumber, sizeof(PortNumber), &Data, sizeof(Data), &bytesReturned, NULL);
if (!bRc) {
fprintf(stderr, "Error in DeviceIoControl : %d\n", GetLastError());
return -1;
}
return Data;
}
pioctl_outpd()类似,不再列举出。
4.3 I2C读写函数
本文采用查询方式实现I2C读写函数。硬件上用DH82H81的GPIO8模拟SCL、GPIO15模拟SDA,封装成i2c.dll库。这两个管脚为专用GPIO脚,通过GP_IO_SEL和GP_LVL两个端口即可控制其I/O方向和电平值。下面以I2C操作中的启动和发送字节为例,讲解其实现。其他操作的实现过程类似,不再赘述。
1)初始化函数
#define SCL GPIO8
#define SDA GPIO15
void i2c_init(void)
{
gpioDir = pioctl_inpd(GP_IO_SEL) & ~((1 << SCL) | (1 << SDA)); // 0:OUTPUT
pioctl_outpd(GP_IO_SEL, gpioDir); // 设置GPIOs的I/O方向
gpioVal = pioctl_inpd(GP_LVL) | ((1 << SCL) | (1 << SDA));
pioctl_outpd(GP_LVL, gpioVal); // 设置GPIOs的电平
}
2)端口的位操作函数
int gpio_in(int gpio_num)
{ // 读取GPIOs输入脚
if ((gpioDir & (1< gpioDir |= (1< pioctl_outpd(GP_IO_SEL, gpioDir); } return (pioctl_inpd(GP_LVL) & (1< } void gpio_out(int gpio_num, int level) { // 写GPIOs输出脚 if ( gpioDir & (1< gpioDir &= ~(1< pioctl_outpd(GP_IO_SEL, gpioDir); } gpioVal = pioctl_inpd(GP_LVL) & ~(1< if (level) gpioVal |= 1< pioctl_outpd(GP_LVL, gpioVal); // 设置GPIO的电平 } 3)I2C读寫函数 #define i2c_scl(level) gpio_out(SCL, level) #define i2c_sda(level) gpio_out(SDA, level)
#define i2c_sda_in() gpio_in(SDA)
void i2c_start(void)
{ // 当SCL为高电平时,SDA发生由高到低的跳变
i2c_scl(1);
i2c_sda(0);
i2c_scl(0);
}
int i2c_send_data(unsigned char octet)
{ // 发送一个字节
int i, ack;
for(i=0x80; i>0; i>>=1) {
i2c_sda(octet & i ? 1 : 0);
i2c_scl(1);
i2c_scl(0);
}
i2c_sda(1); // 发送器件释放SDA线
i2c_scl(1);
ack = i2c_sda_in(); // 讀取低电平有效的ACK位
scl(0); // 实现了了ACK
return (ack); // 返回ACK位
}
4.4 驱动加载和调试
由于目标软件平台为64位系统,pioctl.sys驱动相应编译成64位,需要禁用Windows Server 2008的数字签名,编译的驱动才能加载。
在pioctl.sys驱动中通过KdPrint()宏输出调试信息,通过WinDbg工具捕获调试信息,以和printf函数类似的方式调试代码。
5 结束语
本文基于GPIO管脚的I2C操作模拟方法,在图2所示的采用Windows Server 2008的目标平台上做测试,实现了和D28C22安全芯片的可靠通信,验证了本方法的正确性。该方法的原理可供类似的采用Intel X86方案的产品设计参考,以有效节省采用转接芯片的成本、降低软件开发的难度。
这种用户态I/O端口访问的硬件模拟,花费较高的CPU时间成本,对于高速的简单I/O设备访问,有较大的局限性。但基于本方法,可在用户态将硬件操作代码调试好,再直接封装到内核模式驱动程序中,将极大地降低开发特定Windows设备驱动的难度。
参考文献:
[1] 田磊, 宋圆方. 基于Windows CE的IIC设备驱动的实现[J]. 西安邮电学院学报, 2008(1): 126-128.
[2] 蔡纯洁, 邢武. PIC 16/17 单片机原理与实现[M]. 合肥: 中国科学技术大学出版社, 1997.
[3] 保拉?汤姆林森. Windows NT/2000编程实践[M]. 北京: 中国电力出版社, 2001.
[4] 张佩, 马勇, 董鉴源. 竹林蹊径——深入浅出Windows驱动开发[M]. 北京: 电子工业出版社, 2011.