基于PCI卡的FPGA硬件延时驱动
一、软件延时的缺点:
项目来源于一个基于windows操作系统的PCI控制仪器,由于测试过程中需要用到很多微秒(us)和毫秒(ms)级的延时函数,且对延时精度要求相对较高。原有系统采用了windows API 对CPU节拍计数来实现高精度定时,代码如下:
1: void WINAPI delay_us(DWORD dw_us)
2: {
3: LARGE_INTEGER CurrTicks, TicksCount;
4:
5: QueryPerformanceFrequency(&TicksCount);
6: QueryPerformanceCounter(&CurrTicks);
7:
8: TicksCount.QuadPart = TicksCount.QuadPart * dw_us /1000000i64;
9: TicksCount.QuadPart += CurrTicks.QuadPart;
10:
11: while(CurrTicks.QuadPart<TicksCount.QuadPart)
12: QueryPerformanceCounter(&CurrTicks);
13: }
这种延时方法有两个主要问题:
- 定时精度差。windows是一个分时复用的操作系统,若定时时间内进程切换或者系统繁忙时,实际延时时间会变长。
- CPU资源占用率高。实测在一般PC上(没有考虑多核问题),并行跑两个软件延时进程,CPU占用率几乎100%。
考虑用PCI卡上有一片FPGA芯片,若用FPGA实现硬件逻辑的定时器,能达到更高的定时精度,且能有效降低对CPU资源的占用。
参考资料:
二、硬件延时方案
硬件延时总体方案结构图
- 上层应用程序需要修改delay_ms()和delay_us()两个函数, 从原来查询CPU节拍数的计时方法,改写为向驱动预置FPGA定时器的起始定时数据并启动定时器。对于用户测试程序不需要任何改动,延时继续调用delay_ms()和delay_us()函数,
- 需要延时时,delay_ms()或者delay_us()函数根据预设的FPGA时钟频率计算出定时的预置数据,发送给PCI驱动程序。PCI驱动程序起中间桥梁的作用,连接上层测试程序和底层板载FPGA.
- PCI驱动程序接收到delay_ms()或者delay_us()调用消息后,向FPGA申请定时器,申请成功后,启动FPGA中对应定时器开始定时,同时APP层线程挂起等待定时中断。
- 当定时到时,FPGA向PCI总线发送硬件中断,PCI驱动响应中断,清除相应中断标志、释放FPGA定时器后触发APP层线程等待事件。
- APP层接收到定时到事件后恢复线程,恢复执行后续动作。
PCI驱动与FPGA定时控制过程示意图
三、PCI驱动设计
定时过程驱动主要动作为:
- App层通过WritePort()或者DeviceIoControl()函数向PCI驱动层发送的定时控制指令和数据。
- 驱动层接收到启动定时指令后产生定时IRP消息包,申请FPGA定时器并启动定时,启动成功后,将IRP消息包挂起(pinding),返回App层状态为挂起。
- App层接受到驱动挂起信号后,等待定时事件完成。
- 定时时间到后,FPGA定时单元产生硬件中断信号,PCI驱动进入中断服务例程(ISR),清楚中断信号后,将对应挂起的IRP完成,返回成功完成信号。
- App层等待的定时到事件被触发,完成等待,继续后续指令。
从上可知,其关键是IRP消息包不是一次直接处理完成的,而是先定时将IRP包暂时挂起,然后在定时到后的中断中完成这个IRP消息包。
微软推荐对IRP的处理流程如下图所示,驱动接受到IRP消息包,先通过分发程序找到相应执行例程,然后通过StartIO将IRP包压入顺序队列,并暂时将IRP包挂起。驱动程序逐一执行IRP队列中的IRP包,返回执行后的状态(penging)。此时IRP包还没有真正完成,中断到达后在中断例程中启动一个延迟执行例程(DPC),然后在DPC中完成此IRP包,并返回成功状态。
标准驱动IRP消息完成过程示意图
针对这个项目的实时性要求,我改进了其中有两点:
- 定时IRP包不用StartIO例程串行化执行,而是采用KeSynchronizeExecution()函数完成定时IRP包。
这主要是因为StartIO例程将IRP包压入队列串行化执行后会增加定时延迟,这个延迟是不可控的。另一反面,定时IRP包在完成定时挂起之前不允许有任何打断(受限于FPGA的状态机),采用KeSynchronizeExecution()函数后,将定时IRP包的优先级暂时提高到和硬件中断一个级别 (DIRQL) ,优先级高于StartIO例程。这样能保证定时IRP包执行的连续性和实时性。主要代码如下:
1: //------------------------ APP ----------------------------------
2: int my_delay_us(DWORD dw_us)
3: { ...
4: HANDLE hDevice = CreateFile( _T("\\\\.\\PCI_Driver_Tusion"),
5: GENERIC_READ | GENERIC_WRITE,
6: 0, // share mode none
7: NULL, // no security
8: OPEN_EXISTING,
9: FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //opened or created for asynchronous I/O.
10: NULL
11: );
12:
13: //creat overlap event
14: OVERLAPPED overlap={0};
15: overlap.hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
16: bResult = DeviceIoControl( hDevice, START_TIME_PROCESS, &count, sizeof(count), &dResult, sizeof(dResult), &dwOutput, &overlap );
17: ....
18: status = WaitForSingleObject( overlap.hEvent,5000 ); //wait for timer
19: ...
20: bResult = CloseHandle(hDevice);
21: ...
22: return 0;
23: }
24:
25: //---------------------- PCI Driver ---------------------------
26: #pragma LOCKEDCODE
27: NTSTATUS PCI_Test_DeviceControl(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
28: { ...
29: switch (ControlCode)
30: { ...
31: // Set timer request
32: case START_TIME_PROCESS:
33: {
34: ...
35: BOOLEAN SyncState = KeSynchronizeExecution(dx->InterruptObject,(PKSYNCHRONIZE_ROUTINE)SetTimerRoutine,(PVOID)dx);
36: if( SyncState == TRUE )
37: {
38:
39: KdPrint((" PCI_Test_Driver:KeSynchronizeExecution set OK! Pending the thread\n"));
40: info = 4;
41: pIrp->IoStatus.Status = STATUS_PENDING;
42: pIrp->IoStatus.Information = info;
43: // pending the delay timer IRP
44: IoMarkIrpPending(pIrp);
45: KdPrint((" PCI_Test_Driver:complete IoMarkIrpPending(); \n"));
46: return STATUS_PENDING;
47: }
48: }
49: }
50:
- 定时IRP直接在中断服务例程(ISR)中完成,而不采用ISR调用DPC再完成。
这样做出发点也是为了提高实时性,毕竟DPC的调用需要延迟时间。但这里就出问题了。
未完待续。。。。
参考资料: