标题 | Windows下I2C总线的GPIO模拟 |
范文 | 邹应双 摘要:简要介绍了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. |
随便看 |
|
科学优质学术资源、百科知识分享平台,免费提供知识科普、生活经验分享、中外学术论文、各类范文、学术文献、教学资料、学术期刊、会议、报纸、杂志、工具书等各类资源检索、在线阅读和软件app下载服务。