文章轉自:http://blog.csdn.net/chenyujing1234/article/details/7896364
1、DDK串口開發框架
DDK對串口驅動提供了專門接口。只要編寫的驅動滿足這些接口,並按照串口標準的命名方法,不管是真實的串口設備,還是虛擬設備,Windows操作系統都會認爲
這個設備是一個標準的串口設備。用標準的串口調試工具都可以與這個設備進行通信。
1、1 串口驅動的入口函數
本章的實例程序是在HelloWDM驅動的基礎上修改而來,入口函數依然是DriverEntry,在DriverEntry函數中指定各種IRP的派遣函數,以及AddDevice 例程、卸載例程等。
/************************************************************************
* 函數名稱:DriverEntry
* 功能描述:初始化驅動程序,定位和申請硬件資源,創建內核對象
* 參數列表:
pDriverObject:從I/O管理器中傳進來的驅動對象
pRegistryPath:驅動程序在註冊表的中的路徑
* 返回 值:返回初始化驅動狀態
*************************************************************************/
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
KdPrint(("Enter DriverEntry\n"));
pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice;
pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloWDMDispatchControlp;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloWDMCreate;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloWDMClose;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloWDMRead;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloWDMWrite;
pDriverObject->DriverUnload = HelloWDMUnload;
KdPrint(("Leave DriverEntry\n"));
return STATUS_SUCCESS;
}
其中在AddDevice例程中,需要創建設備對象,這些都是和以前的HelloWDM驅動程序類似。在創建完設備對象後,需要將設備對象指定一個符號鏈接,該符號鏈接必須是
COM開頭,並接一下數字,如本例就採用了COM7。因爲COM1和COM2在有些計算機中有時會被佔用,因此,當該設備對象在指定符號鏈接時,應該避免採用這些名稱。
/************************************************************************
* 函數名稱:HelloWDMAddDevice
* 功能描述:添加新設備
* 參數列表:
DriverObject:從I/O管理器中傳進來的驅動對象
PhysicalDeviceObject:從I/O管理器中傳進來的物理設備對象
* 返回 值:返回添加新設備狀態
*************************************************************************/
#pragma PAGEDCODE
NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject)
{
PAGED_CODE();
KdPrint(("Enter HelloWDMAddDevice\n"));
NTSTATUS status;
PDEVICE_OBJECT fdo;
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyWDMDevice");
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&fdo);
if( !NT_SUCCESS(status))
return status;
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
pdx->fdo = fdo;
pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, PhysicalDeviceObject);
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\DosDevices\\COM7");
pdx->ustrDeviceName = devName;
pdx->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink(&(UNICODE_STRING)symLinkName,&(UNICODE_STRING)devName);
if( !NT_SUCCESS(status))
{
IoDeleteSymbolicLink(&pdx->ustrSymLinkName);
status = IoCreateSymbolicLink(&symLinkName,&devName);
if( !NT_SUCCESS(status))
{
return status;
}
}
// 設置爲緩衝區設備
fdo->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
KdPrint(("Leave HelloWDMAddDevice\n"));
return STATUS_SUCCESS;
}
在創建完符號鏈接後,還不能保證應用程序能找出這個虛擬的串口設備,還需要進一步修改註冊表。具體位置是HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM,可以在這裏加入新項目。本例的項目名是MyWDMDevice,類型爲REG_SZ,內容是COM7。
在上述步驟後,即在AddDevice例程中創建COM7的符號鏈接,並且在註冊表進行相應設置,系統會認爲有這個串口驅動,用任何一個串口調試軟件,都可以枚舉到
該串口。
1、2 應用程序與串口驅動的通信
其實對於一個真實的串口驅動,或者這個介紹的虛擬串口驅動,都需要遵循一組接口。這組接口由微軟事先定義好了,只要符合這組接口,
windows就會認爲這是一個串口設備。這裏所指的接口就是應用程序發的IO控制碼和讀寫命令,因此對於串口驅動只要對這些IRP的派遣函數編寫適當,就能實現一個串口驅動。
首先用IRPTrace看一下,需要對哪些IRP進行處理,筆者加載本章已經介紹的虛擬串口驅動,並用IRPTrace 攔截其IRP處理信息,在打開串口工具後,會發現IRPTrace立刻跟蹤到若干個IO控制碼。
下面依次解釋這些IO控制碼,理解這些IO控制碼,並處理好這些控制碼,是編寫串口驅動的核心。關於這些IO控制碼在ntddser.h文件中,都有相應的定義,並且還有
相應的數據結構定義。
(1)IOCTL_SERIAL_SET_QUEUE_SIZE
這個控制碼是應用程序向驅動請求設置串口驅動內部的緩衝區大小,它是向驅動傳遞SEARIAL_QUEUE_SIZE 數據結構來進行設置的,對於虛擬串口驅動來說,這是不需要
關心的。用IRPTrace可以看出,串口調試工具會向驅動發送的請求是0x400大小的緩衝區大小。
(2)IOCTL_SERIAL_GET_BAUD_RATE
串口調試工具會接着向驅動發送IOCTL_SERIAL_GET_BAUD_RATE命令,這主要是詢問驅動這個設備的波特率。驅動應該回應應用程序SEARIAL_BAUD_RATE數據結構,來通知波特率的數值。
(3)IOCTL_SERIAL_GET_LINE_CONTROL
串口調試工具接着向驅動發送IOTCL_SERIAL_GET_LINE_CONTROL命令,這主要是爲了返回串口的行控制信息,行控制信息用SERIAL_LINE_CONTROL數據結構表示。
- typedef struct _SERIAL_LINE_CONTROL {
- UCHAR StopBits;
- UCHAR Parity;
- UCHAR WordLength;
- } SERIAL_LINE_CONTROL,*PSERIAL_LINE_CONTROL;
其中StopBits是停止位,可以是STOP_BIT_1、STOP_BITS_1_5、STOP_BITS_2等取值。
Parity代表校驗位,可以是NO_PARITY、ODD——PARITY、EVEN_PARITY、MARK——PARITY、SPACE_PARITY。WorkLength是數據位,可以是5、6、7、8。
- case IOCTL_SERIAL_GET_LINE_CONTROL:
- {
- *((PSERIAL_LINE_CONTROL)(Irp->AssociatedIrp.SystemBuffer)) = pdx->Lc;
- Irp->IoStatus.Information = sizeof(SERIAL_LINE_CONTROL);
- break;
- }
(4)IOCTL_SERIAL_GET_CHARS
串口調試工具會接着向驅動發送IOCTL_SERIAL_GET_CHARS命令,這個命令是應用程序向驅動請求特殊字符,用來與控制信號握手,用數據結構SERIAL_CHARS表示。
- typedef struct _SERIAL_CHARS {
- UCHAR EofChar;
- UCHAR ErrorChar;
- UCHAR BreakChar;
- UCHAR EventChar;
- UCHAR XonChar;
- UCHAR XoffChar;
- } SERIAL_CHARS,*PSERIAL_CHARS;
其中EofChar代表是否是傳送結束、ErrorChar代碼是否傳送中有錯誤、BreadChar代碼是否傳送有停止等。
(5)IOCTL_SERIAL_GET_HANDFLOW
串口調試工具會接着向驅動發送IOCTL_SRIAL_GET_HANDFLOW命令,這個命令是負責向驅動程序獲得串口驅動的握手信號,握手信號用SERIAL_HANDFLOW數據
結構表示:
- typedef struct _SERIAL_HANDFLOW {
- ULONG ControlHandShake;
- ULONG FlowReplace;
- LONG XonLimit;
- LONG XoffLimit;
- } SERIAL_HANDFLOW,*PSERIAL_HANDFLOW;
(6)IOCTL_SERIAL_SET_WAIT_MASK
串口工具會接着向驅動發送IOCTL_SERIAL_SET_WAIT_MASK命令,這個命令主要是設置串口驅動的某些事件發生時,需要嚮應用程序通知,這些事件包括以下幾種事件:
- #define SERIAL_EV_RXCHAR 0x0001 // Any Character received
- #define SERIAL_EV_RXFLAG 0x0002 // Received certain character
- #define SERIAL_EV_TXEMPTY 0x0004 // Transmitt Queue Empty
- #define SERIAL_EV_CTS 0x0008 // CTS changed state
- #define SERIAL_EV_DSR 0x0010 // DSR changed state
- #define SERIAL_EV_RLSD 0x0020 // RLSD changed state
- #define SERIAL_EV_BREAK 0x0040 // BREAK received
- #define SERIAL_EV_ERR 0x0080 // Line status error occurred
- #define SERIAL_EV_RING 0x0100 // Ring signal detected
- #define SERIAL_EV_PERR 0x0200 // Printer error occured
- #define SERIAL_EV_RX80FULL 0x0400 // Receive buffer is 80 percent full
- #define SERIAL_EV_EVENT1 0x0800 // Provider specific event 1
- #define SERIAL_EV_EVENT2 0x1000 // Provider specific event 2
- case IOCTL_SERIAL_SET_WAIT_MASK:
- {
- PIRP pOldWaitIrp;
- PDRIVER_CANCEL pOldCancelRoutine;
- pdx->EventMask = *(PULONG)Irp->AssociatedIrp.SystemBuffer;
- KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql);
- pOldWaitIrp = pdx->pWaitIrp;
- if (pOldWaitIrp != NULL)
- {
- pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL);
- //對以前沒有進行完成例程的等待irp,進行完成
- if (pOldCancelRoutine != NULL)
- {
- pOldWaitIrp->IoStatus.Information = sizeof(ULONG);
- *(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = 0;
- pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS;
- pdx->pWaitIrp = NULL;
- }
- else
- {
- pOldWaitIrp = NULL;
- }
- }
- KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql);
- if (pOldWaitIrp != NULL)
- {
- IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT);
- }
- break;
- }
當設置新的阻塞事件時,哪果之前有等待的IRP,那麼先進行完成。
(7)IOCTL_SERIAL_WAIT_ON_MASK
這個IO控制碼是最重要的一個,當串口調試工具通過前面幾個IO控制碼初始華好後,就會發送這個請求。
在驅動程序中,應該阻塞在那裏,即返回PENDING狀態,且通過IoSetCancelRoutine設置取消例程,而不是完成這個IRP。
當IOCTL_SERIAL_SET_WAIT_MASK設置的事件中的一項發生時,阻塞狀態改爲完成,並通知應用程序是哪種事件發生了。
- case IOCTL_SERIAL_WAIT_ON_MASK:
- {
- PDRIVER_CANCEL pOldCancelRoutine;
- KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql);
- //等待irp一定被清除,且eventMask一定不爲0
- if ((pdx->pWaitIrp != NULL) || (pdx->EventMask == 0))
- ntStatus = STATUS_INVALID_PARAMETER;
- else if ((pdx->EventMask & pdx->HistoryEvents) != 0)
- {
- // Some events happened
- Irp->IoStatus.Information = sizeof(ULONG);
- *(PULONG)Irp->AssociatedIrp.SystemBuffer = pdx->EventMask & pdx->HistoryEvents;
- pdx->HistoryEvents = 0;
- ntStatus = STATUS_SUCCESS;
- }else
- {
- pdx->pWaitIrp = Irp;
- ntStatus = STATUS_PENDING;
- IoSetCancelRoutine(Irp, DriverCancelWaitIrp);
- if (Irp->Cancel)
- {
- pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL);
- if (pOldCancelRoutine != NULL)
- {
- ntStatus = STATUS_CANCELLED;
- pdx->pWaitIrp = NULL;
- }
- else
- {
- IoMarkIrpPending(Irp);
- }
- }
- else
- {
- IoMarkIrpPending(Irp);
- }
- }
- KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql);
- break;
- }
- VOID DriverCancelWaitIrp(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
- {
- KdPrint(("DriverCancelWaitIrp\n"));
- PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
- KIRQL OldIrql;
- IoReleaseCancelSpinLock(Irp->CancelIrql);
- KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql);
- pExtension->pWaitIrp = NULL;
- KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql);
- Irp->IoStatus.Status = STATUS_CANCELLED;
- IoCompleteRequest(Irp, IO_NO_INCREMENT);
- }
關於刪除例程的知識可以參考<<驅動程序的取消IRP >>
1、3 寫的實現
串口驅動除了需要完成處理IO控制碼外,還需要對讀寫IRP進行處理。一般情況下,作爲應用程序的串口調試工具會開啓多個線程,其中主線程負責與串口驅動初始化的IO控制碼通信。
另外一個很重要的線程就是發送IOCTL_SERIAL_WAIT_ON_MASK請求,對於沒有數據傳輸的情況下,這個IO控制碼請求會PENDING在那裏,即阻塞。當有傳送的請求時,相應的事件被觸發,剛纔因爲IOCTL_SERAIL_WAIT_ON_MASK的IRP被阻塞的線程得以繼續運行,
如果應用程序得知該事件是被寫入了一個字符,會去發出一個讀請求 ,對於驅動則是讀的IRP。如果循環過程,從而實現了一個虛擬攝像頭回寫的例子。
在對於寫IRP的派遣函數中,主要是將寫的數據存儲在設備擴展中,以便以後讀的時候將這些內容返回到應用程序。
另外一個很重要的內容,就是阻塞的IO控制甦醒過來。在本例中調用DriverCheckEvent函數,該函數將阻塞的IRP完成,使應用程序的線程得以繼續進行。並且這個線程還知道了SERIAL_EV_RXCHAR和SERIAL_EV_RX80FULL事件的到來,從而發起一個讀請求,傳送到驅動中就是讀IRP。
- NTSTATUS HelloWDMWrite(IN PDEVICE_OBJECT fdo,
- IN PIRP Irp)
- {
- KdPrint(("HelloWDMWrite\n"));
- NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success
- PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
- // 獲得當前IO堆棧
- PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );
- // 獲取當前IO堆棧的操作字節數
- ULONG DataLen = irpSp->Parameters.Write.Length;
- // 從IRP的緩衝區中得到數據
- PUCHAR pData = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;
- KIRQL OldIrql;
- PIRP pOldReadIrp = NULL;
- PDRIVER_CANCEL pOldCancelRoutine;
- // 設置IRP的操作字節數
- Irp->IoStatus.Information = 0;
- ntStatus = STATUS_SUCCESS;
- if (DataLen == 0)
- {
- ntStatus = STATUS_SUCCESS;
- }else if (DataLen>COMBUFLEN)
- {
- ntStatus = STATUS_INVALID_PARAMETER;
- }
- else
- {
- KdPrint(("Write\n"));
- // 獲取自旋鎖
- KeAcquireSpinLock(&pdx->WriteSpinLock, &OldIrql);
- // 複製內存塊
- RtlCopyMemory(pdx->Buffer,pData,DataLen);
- pdx->uReadWrite = DataLen;
- if (pdx->pReadIrp != NULL) // drop it out
- {
- // 記錄IRP
- pOldReadIrp = pdx->pReadIrp;
- // 設置取消函數
- pOldCancelRoutine = IoSetCancelRoutine(pOldReadIrp, NULL);
- if (pOldCancelRoutine != NULL)
- {
- pOldReadIrp->IoStatus.Information = 0;
- pOldReadIrp->IoStatus.Status = STATUS_SUCCESS;
- pdx->pReadIrp = NULL;
- }
- else
- {
- pOldReadIrp = NULL;
- }
- }
- // 檢查事件
- DriverCheckEvent(pdx, SERIAL_EV_RXCHAR | SERIAL_EV_RX80FULL);
- // DriverCheckEvent(pdx, SERIAL_EV_TXEMPTY);
- // 釋放自旋鎖
- KeReleaseSpinLock(&pdx->WriteSpinLock, OldIrql);
- if (pOldReadIrp != NULL)
- IoCompleteRequest(pOldReadIrp, IO_NO_INCREMENT);
- }
- Irp->IoStatus.Status = ntStatus;
- Irp->IoStatus.Information = DataLen;
- IoCompleteRequest( Irp, IO_NO_INCREMENT );
- return ntStatus;
- }
- VOID DriverCheckEvent(IN PDEVICE_EXTENSION pExtension, IN ULONG events)
- {
- KdPrint(("DriverCheckEvent\n"));
- PIRP pOldWaitIrp = NULL;
- PDRIVER_CANCEL pOldCancelRoutine;
- KIRQL OldIrql;
- KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql);
- pExtension->HistoryEvents |= events;
- events &= pExtension->EventMask;
- //相當於設置觸發事件
- if ((pExtension->pWaitIrp != NULL) && (events != 0))
- {
- pOldWaitIrp = pExtension->pWaitIrp;
- pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL);
- //是否已經被cancel掉?
- if (pOldCancelRoutine != NULL)
- {
- // Nein, also Request beenden
- pOldWaitIrp->IoStatus.Information = sizeof(ULONG);
- *(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = events;
- pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS;
- pExtension->pWaitIrp = NULL;
- pExtension->HistoryEvents = 0;
- }
- else
- {
- //如果cancel掉,就不用IoCompleteRequest了
- pOldWaitIrp = NULL;
- }
- }
- KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql);
- if (pOldWaitIrp != NULL)
- {
- KdPrint(("complete the wait irp\n"));
- IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT);
- }
- }
1、4 讀的實現
對於虛擬串口的讀工作,就變得相對簡單。因爲寫IRP會負責通知讓阻塞的線程繼續運行,並且通知是何種事件的來臨。串口調試軟件得知SERAIL_EV_RXCHAR這個事件
,因此發起了讀事件。在驅動中,就是進入讀IRP的派遣函數。
在該派遣函數中,負責將存儲在設備擴展中的數據通過IRP傳送到應用程序。同時,還需要做一些同步處理。
- NTSTATUS HelloWDMRead(IN PDEVICE_OBJECT fdo,
- IN PIRP Irp)
- {
- KdPrint(("HelloWDMRead\n"));
- NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success
- PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)fdo->DeviceExtension;
- PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );
- ULONG BufLen = irpSp->Parameters.Read.Length;
- PCHAR pBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
- KIRQL OldIrql;
- PDRIVER_CANCEL pOldCancelRoutine;
- Irp->IoStatus.Information = 0;
- DbgPrint("DeviceObject:%08X Read\n",fdo);
- if (BufLen == 0)
- {
- ntStatus = STATUS_SUCCESS;
- }
- else
- {
- KeAcquireSpinLock(&pExtension->WriteSpinLock, &OldIrql);
- // 內存複製
- RtlCopyMemory(pBuf,pExtension->Buffer,BufLen);
- Irp->IoStatus.Information = BufLen;
- if (BufLen==0 && pExtension->pReadIrp==NULL) // nothing, store
- {
- // 保存IRP
- pExtension->pReadIrp = Irp;
- Irp->IoStatus.Status = ntStatus = STATUS_PENDING;
- // 設置取消函數
- IoSetCancelRoutine(Irp, DriverCancelCurrentReadIrp);
- // 重新設置取消函數
- if (Irp->Cancel)
- {
- pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL);
- if (pOldCancelRoutine != NULL)
- {
- // Nein, also IRP hier abbrechen
- Irp->IoStatus.Status = ntStatus = STATUS_CANCELLED;
- pExtension->pReadIrp = NULL;
- }
- else
- {
- // 標記IRP掛起 Ja, Cancel-Routine wird Request beenden
- IoMarkIrpPending(Irp);
- }
- }
- else
- {
- IoMarkIrpPending(Irp);
- }
- }
- KeReleaseSpinLock(&pExtension->WriteSpinLock, OldIrql);
- }
- Irp->IoStatus.Status = ntStatus;
- if (ntStatus != STATUS_PENDING)
- IoCompleteRequest( Irp, IO_NO_INCREMENT );
- return ntStatus;
- }