APP下载

Windows下I2C总线的GPIO模拟

2017-03-24邹应双

电脑知识与技术 2017年1期

邹应双

摘要:简要介绍了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.