接上文,本文章繼續記錄中泰聯創的數據採集卡驅動翻新過程。
中斷初始化部分代碼移植
分析PLX9x5x源碼可知,中斷初始化調用流程如下:
PLxEvtDeviceAdd-PLxInitializeDeviceExtension-PLxInterruptCreate
其中具體初始化代碼在PLxInterruptCreate函數中,這部分是純框架流程無需修改,直接就可以使用:
NTSTATUS
PLxInterruptCreate(
IN PDEVICE_EXTENSION DevExt
)
{
......
}
WdfInterruptCreate函數將創建設備中斷對象,後續和中斷相關操作都要用到DevExt->Interrupt。
WdfInterruptCreate函數成功返回之後,WDF框架會在系統加載設備時連接中斷,連接中斷後調用PLxEvtInterruptEnable函數;系統卸載設備時調用PLxEvtInterruptDisable函數後斷開中斷。
示例程序已經在PLxEvtInterruptEnable函數中對中斷寄存器進行了使能操作,這一點PCI9054和PCI9656兼容,所以延用此代碼即可,同時需要加上本地總線的中斷使能。
intCSR.bits.LocalIntInputEnable = TRUE;
示例程序已經在PLxEvtInterruptDisable函數中對中斷寄存器進行了禁止操作,這一點PCI9054和PCI9656兼容,所以延用此代碼即可。
intCSR.bits.LocalIntInputEnable = FALSE;
對於中泰聯創的老產品上使能和禁止中斷操作,則打算轉移到應用層進行,這樣可以增加驅動的適配性。
中斷事件代碼部分移植
老驅動在應用層創建事件句柄,傳輸給內核,在對應中斷髮生時觸發事件句柄,從而簡化應用層編程,新驅動需要實現這個功能,
1. 公共定義 (Public.h)
在 Public.h 中定義相關的 IOCTL 和常量:
#define EVENT_COUNT 3L
#define EVENT_SFifo 0L
#define EVENT_ALARM 1L
#define EVENT_TRIP 2L
#define PCI8KPLX_IOCTL_OPEN_IRQ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define PCI8KPLX_IOCTL_CLOSE_IRQ CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_BUFFERED, FILE_ANY_ACCESS)
2. 設備上下文擴展 (Private.h)
在 DEVICE_EXTENSION 結構體中添加用於存儲內核事件對象的指針數組:
typedef struct _DEVICE_EXTENSION {
// ... 現有成員 ...
//用於存儲內核事件對象指針
PKEVENT m_Events[EVENT_COUNT];
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
3. IOCTL 處理邏輯 (Control.c)
在 PLxEvtIoDeviceControl 中添加對 PCI8KPLX_IOCTL_OPEN_IRQ和PCI8KPLX_IOCTL_CLOSE_IRQ 的分發:
/**
* 主設備控制入口點
*/
VOID
PLxEvtIoDeviceControl(……)
{
……
switch (IoControlCode) {
case PCI8KPLX_IOCTL_OPEN_IRQ:
status = PCI8KPLX_IOCTL_OPEN_IRQ_Handler(Request, devExt);
break;
case PCI8KPLX_IOCTL_CLOSE_IRQ:
status = PCI8KPLX_IOCTL_CLOSE_IRQ_Handler(Request, devExt);
break;
// ... 其他 case ...
default:
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
WdfRequestComplete(Request, status);
}
並實現處理函數:
/**
* 功能:將應用層傳入的 ULONG 句柄數組轉換為內核 PKEVENT 對象數組
* 注意:此方法使用固定大小的數組,適用於事件類型和數量固定的場景。
* 對硬件寄存器的操作放在應用層dll中。
* 兼容性:使用 (HANDLE)(ULONG_PTR)ulH 確保 32 位應用在 64 位系統上的兼容性。
*/
NTSTATUS
PCI8KPLX_IOCTL_OPEN_IRQ_Handler(
_In_ WDFREQUEST Request,
_In_ PDEVICE_EXTENSION DevExt
)
{
NTSTATUS status = STATUS_SUCCESS;
PULONG pBuff = NULL; // 指向輸入緩衝區,其中包含 EVENT_COUNT 個 ULONG 句柄
size_t bufferSize = 0;
ULONG i;
// 1. 獲取輸入緩衝區 (預期包含 EVENT_COUNT 個 ULONG 句柄)
status = WdfRequestRetrieveInputBuffer(Request, sizeof(ULONG) * EVENT_COUNT, (PVOID*)&pBuff, &bufferSize);
if (!NT_SUCCESS(status)) {
return status;
}
// 2. 遍歷並處理句柄
for (i = 0; i < EVENT_COUNT; i++) {
ULONG ulH = pBuff[i];
HANDLE h = (HANDLE)(ULONG_PTR)ulH; // 關鍵轉換,確保64位兼容性
if (h != NULL) {
// 如果該位置已存在引用的事件對象,先釋放舊引用
if (DevExt->m_Events[i] != NULL) {
ObDereferenceObject(DevExt->m_Events[i]);
DevExt->m_Events[i] = NULL;
}
// 將用户態句柄轉換為內核事件對象指針
status = ObReferenceObjectByHandle(
h,
EVENT_MODIFY_STATE,
*ExEventObjectType,
UserMode,
(PVOID*)&DevExt->m_Events[i],
NULL
);
if (!NT_SUCCESS(status)) {
DevExt->m_Events[i] = NULL;
break; // 如果某個句柄轉換失敗,停止處理並返回錯誤
}
}
}
return status;
}
/**
* 事件句柄清理函數 (固定數組版)
* 功能:釋放 m_Events 數組中所有存儲的事件對象引用,並將指針置為 NULL。
* 注意:此操作會清除所有已註冊的事件句柄。
*/
NTSTATUS
PCI8KPLX_IOCTL_CLOSE_IRQ_Handler(
_In_ WDFREQUEST Request,
_In_ PDEVICE_EXTENSION DevExt
)
{
NTSTATUS status = STATUS_SUCCESS;
ULONG i;
//本函數不需要和應用層交互,所以用不到這個參數
UNREFERENCED_PARAMETER(Request);
// 為了保證操作的原子性,可能需要在此處添加設備級的鎖,如果 m_Events 訪問需要同步的話
// WdfWaitLockAcquire(DevExt->SomeLock, NULL);
// 遍歷 m_Events 數組
for (i = 0; i < EVENT_COUNT; i++) {
if (DevExt->m_Events[i] != NULL) {
// 釋放對該事件對象的引用,防止內存泄漏
ObDereferenceObject(DevExt->m_Events[i]);
// 清空指針
DevExt->m_Events[i] = NULL;
}
}
// WdfWaitLockRelease(DevExt->SomeLock);
// 此 IOCTL 通常沒有輸出數據,不需要調用 WdfRequestSetInformation
// 直接返回成功狀態
return status;
}
4. 清理事件防止內存泄漏
在 PlxCleanupDeviceExtension 中進行清理
VOID
PlxCleanupDeviceExtension(
_In_ PDEVICE_EXTENSION DevExt
)
{
ULONG i;
// 遍歷並釋放事件對象
for (i = 0; i < EVENT_COUNT; i++) {
if (DevExt->m_Events[i] != NULL) {
ObDereferenceObject(DevExt->m_Events[i]);
DevExt->m_Events[i] = NULL; // 清空指針是個好習慣
}
}
// ... 其他清理代碼 ...
}
5. 中斷觸發邏輯 (IsrDpc.c 參考)
在 DPC (如 PLxEvtInterruptDpc) 中,根據硬件狀態觸發相應的事件:
// 示例:觸發 SFIFO 觸發值事件
if (DevExt->m_Events[EVENT_SFifo] != NULL) {
KeSetEvent(DevExt->m_Events[EVENT_SFifo], IO_NO_INCREMENT, FALSE);
}
5. 注意事項
- 64位兼容性:代碼中使用了
(HANDLE)(ULONG_PTR)ulH,確保了 32 位應用程序在 64 位系統下運行時的句柄對齊。 - 資源釋放:在驅動卸載或設備清理回調(如
PlxEvtDeviceCleanup)中,應遍歷m_Events並對非空元素調用ObDereferenceObject,以防止內核內存泄漏。 - 固定大小限制:此方案使用固定大小的數組
m_Events[EVENT_COUNT],如果應用層需要管理的事件數量超過EVENT_COUNT,則需要增大該常量並重新編譯驅動和應用層。 - 應用層兼容性:此方案假定應用層發送的是
ULONG類型的句柄數組。它主要兼容發送此類數組的32位應用。如果64位應用也發送相同的ULONG數組(例如,應用層未區分位數),則其句柄必須是有效的32位兼容值(如WoW64子系統提供的句柄)。
應用層要做的和中斷相關的事情
老驅動中使用內核變量來保存一些數據,會增加驅動複雜度,所以都轉移到應用層,和中斷有關的有控制字,變量,因此在dll中聲明全局變量
//本來是內核中需要保存的內容,改成應用層保存
ULONG g_ulCtrlWord[MAX_CARD_COUNT];
//為了後面方便閲讀代碼,將賦值和讀取都封裝成函數
void SetCtrlWord(ULONG cardNo, ULONG ulCtrlWord) {
if (cardNo > MAX_CARD_COUNT)
return;
g_ulCtrlWord[cardNo] = ulCtrlWord;
}
ULONG GetCtrlWord(ULONG cardNo) {
if (cardNo > MAX_CARD_COUNT)
return 0;
return g_ulCtrlWord[cardNo];
}
老驅動中有一個InitIRQ函數,只是操作了控制字變量,新驅動將內核操作修改成應用層的變量操作
//改變控制字中的中斷相關位
unsigned long ulCtrlWord = GetCtrlWord(cardNO);
ulCtrlWord = SET_ULONG_BITS( ulCtrlWord, IRQ_MASK, IRQ, irqSource );
SetCtrlWord(cardNO, ulCtrlWord);
老驅動中使用OpenIRQ將中斷相關事件傳遞到內核,同時在內核中設置PCI9054的本地控制字使能FPGA中斷。新驅動中內核只是接收中斷相關事件,然後在應用層使能FPGA中斷。
//在DeviceIoControl之後將控制字寫入硬件
//使能之前設置好的中斷
WRITED(cardNO, OFF_CTRLWORD, GetCtrlWord(cardNO));
接下來要移植應用層訪問硬件寄存器的代碼