鄒應(yīng)雙
摘要:簡(jiǎn)要介紹了I2C總線操作和基于Windows內(nèi)核模式驅(qū)動(dòng)的用戶態(tài)I/O端口訪問,分析了Windows平臺(tái)下GPIO管腳模擬I2C總線的可行性,講解了編程實(shí)現(xiàn)過程,連接I2C接口的安全芯片進(jìn)行了驗(yàn)證。
關(guān)鍵詞:I2C總線;GPIO管腳;Windows;內(nèi)核模式驅(qū)動(dòng)
中圖分類號(hào):TP311 文獻(xiàn)標(biāo)識(shí)碼:A 文章編號(hào):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公司推出的兩線式串行總線,用于嵌入式系統(tǒng)連接各種低速外圍設(shè)備,如RTC、EEPROM、傳感器、安全芯片等。許多單片機(jī)、嵌入式芯片等都帶有I2C主控器,其采用的操作系統(tǒng)如嵌入式Linux等均帶有I2C驅(qū)動(dòng)程序,編程中可直接使用。對(duì)于不含I2C主控器的芯片,為了滿足定制系統(tǒng)設(shè)計(jì)需求,一般也有大量的GPIO管腳,可用于軟件模擬I2C總線。
Intel公司的X86系列CPU和配套的橋接芯片,除了用于桌面PC,還廣泛用于網(wǎng)關(guān)、收銀、視頻等各種嵌入式服務(wù)器。這類服務(wù)器一般采用Windows Server操作系統(tǒng),不帶有I2C主控器或不提供I2C驅(qū)動(dòng)程序。為了實(shí)現(xiàn)軟件授權(quán)和保護(hù),這類嵌入式服務(wù)器首先會(huì)選擇管腳少、性價(jià)比高的I2C接口的安全芯片。為了滿足這種需求,一種方法是采用USB轉(zhuǎn)I2C的專用芯片,但這將增加硬件成本和軟件復(fù)雜度。
基于I2C總線的簡(jiǎn)單性,可選用橋接芯片的GPIO管腳來模擬I2C主控器。GPIO管腳通過X86的I/O指令即可完全控制;但普通應(yīng)用程序在Windows下無法直接訪問I/O端口,可通過內(nèi)核模式驅(qū)動(dòng)程序來實(shí)現(xiàn)。在Windows平臺(tái)下通過GPIO管腳模擬I2C總線,將是一種簡(jiǎn)單有效、低成本的解決方法。本文就這個(gè)模擬過程進(jìn)行探討。
2 I2C總線的讀寫
I2C總線通過時(shí)鐘和數(shù)據(jù)兩根線即可實(shí)現(xiàn)完善的同步數(shù)據(jù)傳輸。當(dāng)發(fā)送數(shù)據(jù)時(shí),一個(gè)設(shè)備作為主機(jī),另一個(gè)設(shè)備作為從機(jī)。主設(shè)備為數(shù)據(jù)傳輸產(chǎn)生時(shí)鐘信號(hào)。I2C通訊協(xié)議要求在時(shí)鐘線(SCL, Serial Clock Line)處于低電平時(shí),數(shù)據(jù)線(SDA, Serial Data Line)才能變化。協(xié)議中每個(gè)從設(shè)備都有一個(gè)地址,會(huì)一直監(jiān)視總線上的主設(shè)備要初始化數(shù)據(jù)傳輸時(shí)發(fā)出的地址并匹配。
總線的工作流程如下:
空閑:當(dāng)總線上沒有數(shù)據(jù)通訊發(fā)生時(shí),SCL和SDA通過上拉電阻呈高電平。
開始:SCL為高時(shí),SDA由高變低,這時(shí)數(shù)據(jù)傳輸開始。
地址:主設(shè)備發(fā)送地址信息,包含7位的從設(shè)備地址和1位的讀寫位(表明數(shù)據(jù)流的方向)。發(fā)送完一個(gè)字節(jié)后,從設(shè)備會(huì)發(fā)送一位的認(rèn)可位(ACK)。
數(shù)據(jù):根據(jù)方向位,數(shù)據(jù)在主設(shè)備和從設(shè)備之間傳輸。數(shù)據(jù)一般以8位傳輸,高位在前。接收器上用一位的ACK表明一個(gè)字節(jié)收到了。傳輸可被終止或重新開始。
停止:當(dāng)SCL為高時(shí),SDA由低變高,這時(shí)數(shù)據(jù)傳輸結(jié)束,總線重新進(jìn)入空閑狀態(tài)。
一次完整的數(shù)據(jù)傳輸時(shí)序如圖1所示。
標(biāo)準(zhǔn)I2C總線的傳輸速率是100KHz,通過線與邏輯實(shí)現(xiàn)慢速設(shè)備等待。I2C總線的這些特性允許主設(shè)備的功能通過兩個(gè)GPIO管腳模擬而實(shí)現(xiàn)。
3 Windows下的I/O端口訪問
端口I/O指令允許X86 CPU與系統(tǒng)中的其他硬件設(shè)備通信。對(duì)于硬件設(shè)備的低層次直接控制,C函數(shù)_inp()和_outp()(用X86處理器的IN和OUT指令實(shí)現(xiàn))允許從端口讀入或向一個(gè)端口寫。但在Windows應(yīng)用程序中插入_inp()或者_(dá)outp(),將導(dǎo)致特權(quán)指令異常消息,并給出終止或調(diào)試出錯(cuò)應(yīng)用程序的選擇。Windows的體系結(jié)構(gòu)決定了應(yīng)用程序不能直接通過IN和OUT指令訪問硬件。否則,應(yīng)用程序可以關(guān)閉中斷、破壞顯示或驅(qū)動(dòng)器等硬件設(shè)備,危及系統(tǒng)的穩(wěn)定性。所以,通過內(nèi)核模式驅(qū)動(dòng)程序間接訪問I/O端口是Windows下訪問硬件資源的唯一途徑。
實(shí)現(xiàn)對(duì)I2C主控器的模擬,只需要簡(jiǎn)單的I/O訪問即可實(shí)現(xiàn)。如果編寫完整的內(nèi)核模式的I2C驅(qū)動(dòng)程序,將涉及復(fù)雜的、花費(fèi)大量時(shí)間的Windows內(nèi)核模式驅(qū)動(dòng)驅(qū)動(dòng)程序的開發(fā)和調(diào)試工作。編寫最簡(jiǎn)驅(qū)動(dòng)實(shí)現(xiàn)I/O端口訪問,封裝好用戶態(tài)訪問的接口,將I2C實(shí)現(xiàn)代碼放在用戶態(tài),將極大地簡(jiǎn)化開發(fā)工作,同時(shí)增加二次開發(fā)利用的靈活性。
這樣通過內(nèi)核模式驅(qū)動(dòng)程序?qū)崿F(xiàn)I/O訪問的副作用是每一次I/O操作都要通過Windows的I/O子系統(tǒng)發(fā)送請(qǐng)求,需要花費(fèi)數(shù)千個(gè)時(shí)鐘周期。但這個(gè)時(shí)間成本和100KHz的慢速I2C的一個(gè)位周期相當(dāng),對(duì)于數(shù)據(jù)傳輸量不大的應(yīng)用,在性能上可接受。
4 編程實(shí)現(xiàn)
本文的目標(biāo)硬件平臺(tái)為Intel Core i3-4330 CPU、Intel DH82H81橋接芯片、Maxim DS28C22安全芯片。橋片和安全芯片的連接如圖2所示。
本文的目標(biāo)軟件平臺(tái)是64位的Windows Server 2008,開發(fā)平臺(tái)是Windows 10 專業(yè)版,選用WDK(Windows Driver Kit) 7.1和Visual Studio 2015專業(yè)版。通過查詢方式實(shí)現(xiàn)I2C讀寫,驅(qū)動(dòng)層提供I/O端口訪問功能,pioctl.dll庫(kù)封裝驅(qū)動(dòng)成類似于IN/OUT指令的接口,i2c.dll實(shí)現(xiàn)I2C讀寫函數(shù),提供給上層做應(yīng)用開發(fā)。軟件層次結(jié)構(gòu)如圖3所示。
下面按自底向上的順序簡(jiǎn)單介紹各層次的實(shí)現(xiàn)源碼。
4.1 內(nèi)核模式驅(qū)動(dòng)
和Linux驅(qū)動(dòng)的開發(fā)相比,Windows驅(qū)動(dòng)開發(fā)的門檻要高一些,首先需安裝WDK,了解其中的核心態(tài)函數(shù),熟悉WDM、WDF等驅(qū)動(dòng)程序框架。
WDK中提供了大量的樣例驅(qū)動(dòng)供驅(qū)動(dòng)開發(fā)者參考。考慮到我們的驅(qū)動(dòng)只需提供X86的IN和OUT指令的訪問接口,特選擇WDK樣例中源碼最簡(jiǎn)單的src/general/ioctl/wdm為基礎(chǔ),命名為pioctl,并對(duì)驅(qū)動(dòng)源碼中的函數(shù)名等做適當(dāng)重命名,添加上I/O端口訪問的代碼,即實(shí)現(xiàn)了本驅(qū)動(dòng)。這個(gè)開發(fā)過程不需要對(duì)Windows驅(qū)動(dòng)開發(fā)有較深入的了解。
本驅(qū)動(dòng)程序的驅(qū)動(dòng)加載和卸載、設(shè)備打開和關(guān)閉等例程無新加代碼,不是本文的重點(diǎn),下面僅對(duì)I/O端口操作相關(guān)的代碼做說明。
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)
{ // 檢查用戶態(tài)參數(shù)
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端口號(hà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; // 讀取的字節(jié)數(shù)
break;
case IOCTL_PIOCTL_WRITE_PORT_ULONG: // OUTD
pIOBuffer++;
WRITE_PORT_ULONG((PULONG)((ULONG_PTR)nPort), *(PULONG)pIOBuffer);
Irp->IoStatus.Information = dataBufSize; // 寫的字節(jié)數(shù)
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 用戶態(tài)I/O端口讀寫接口
為了應(yīng)用程序的開發(fā)方便,實(shí)現(xiàn)了pioctl.dll庫(kù),以負(fù)責(zé)自動(dòng)動(dòng)態(tài)加載卸載驅(qū)動(dòng)程序、提供I/O端口的用戶態(tài)訪問接口。
1)DLL入口函數(shù)
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函數(shù)加載驅(qū)動(dòng); 打開設(shè)備文件
return FALSE;
break;
case DLL_PROCESS_DETACH:
pioctl_deinit(); // 關(guān)閉設(shè)備文件; 利用SCM函數(shù)卸載驅(qū)動(dòng)
break;
}
return TRUE;
}
2)端口訪問函數(shù)
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讀寫函數(shù)
本文采用查詢方式實(shí)現(xiàn)I2C讀寫函數(shù)。硬件上用DH82H81的GPIO8模擬SCL、GPIO15模擬SDA,封裝成i2c.dll庫(kù)。這兩個(gè)管腳為專用GPIO腳,通過GP_IO_SEL和GP_LVL兩個(gè)端口即可控制其I/O方向和電平值。下面以I2C操作中的啟動(dòng)和發(fā)送字節(jié)為例,講解其實(shí)現(xiàn)。其他操作的實(shí)現(xiàn)過程類似,不再贅述。
1)初始化函數(shù)
#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); // 設(shè)置GPIOs的I/O方向
gpioVal = pioctl_inpd(GP_LVL) | ((1 << SCL) | (1 << SDA));
pioctl_outpd(GP_LVL, gpioVal); // 設(shè)置GPIOs的電平
}
2)端口的位操作函數(shù)
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); // 設(shè)置GPIO的電平 } 3)I2C讀寫函數(shù) #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)
{ // 當(dāng)SCL為高電平時(shí),SDA發(fā)生由高到低的跳變
i2c_scl(1);
i2c_sda(0);
i2c_scl(0);
}
int i2c_send_data(unsigned char octet)
{ // 發(fā)送一個(gè)字節(jié)
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); // 發(fā)送器件釋放SDA線
i2c_scl(1);
ack = i2c_sda_in(); // 讀取低電平有效的ACK位
scl(0); // 實(shí)現(xiàn)了了ACK
return (ack); // 返回ACK位
}
4.4 驅(qū)動(dòng)加載和調(diào)試
由于目標(biāo)軟件平臺(tái)為64位系統(tǒng),pioctl.sys驅(qū)動(dòng)相應(yīng)編譯成64位,需要禁用Windows Server 2008的數(shù)字簽名,編譯的驅(qū)動(dòng)才能加載。
在pioctl.sys驅(qū)動(dòng)中通過KdPrint()宏輸出調(diào)試信息,通過WinDbg工具捕獲調(diào)試信息,以和printf函數(shù)類似的方式調(diào)試代碼。
5 結(jié)束語(yǔ)
本文基于GPIO管腳的I2C操作模擬方法,在圖2所示的采用Windows Server 2008的目標(biāo)平臺(tái)上做測(cè)試,實(shí)現(xiàn)了和D28C22安全芯片的可靠通信,驗(yàn)證了本方法的正確性。該方法的原理可供類似的采用Intel X86方案的產(chǎn)品設(shè)計(jì)參考,以有效節(jié)省采用轉(zhuǎn)接芯片的成本、降低軟件開發(fā)的難度。
這種用戶態(tài)I/O端口訪問的硬件模擬,花費(fèi)較高的CPU時(shí)間成本,對(duì)于高速的簡(jiǎn)單I/O設(shè)備訪問,有較大的局限性。但基于本方法,可在用戶態(tài)將硬件操作代碼調(diào)試好,再直接封裝到內(nèi)核模式驅(qū)動(dòng)程序中,將極大地降低開發(fā)特定Windows設(shè)備驅(qū)動(dòng)的難度。
參考文獻(xiàn):
[1] 田磊, 宋圓方. 基于Windows CE的IIC設(shè)備驅(qū)動(dòng)的實(shí)現(xiàn)[J]. 西安郵電學(xué)院學(xué)報(bào), 2008(1): 126-128.
[2] 蔡純潔, 邢武. PIC 16/17 單片機(jī)原理與實(shí)現(xiàn)[M]. 合肥: 中國(guó)科學(xué)技術(shù)大學(xué)出版社, 1997.
[3] 保拉?湯姆林森. Windows NT/2000編程實(shí)踐[M]. 北京: 中國(guó)電力出版社, 2001.
[4] 張佩, 馬勇, 董鑒源. 竹林蹊徑——深入淺出Windows驅(qū)動(dòng)開發(fā)[M]. 北京: 電子工業(yè)出版社, 2011.