FPGA:PCI项目
FPGA 是功能强大的 PCI 开发平
本文引用地址:https://www.eepw.com.cn/article/202401/454595.htmPCI 0 - 简单的PCI接口
台,这要归功于其可重新编程性和运行速度。
// Very simple PCI target
// Just 3 flip-flops for the PCI logic, plus one to hold the state of an LED
module PCI(CLK, RSTn, FRAMEn, AD, CBE, IRDYn, TRDYn, DEVSELn, LED);
input CLK, RSTn, FRAMEn, IRDYn;
input [31:0] AD;
input [3:0] CBE;
inout TRDYn, DEVSELn;
output LED;
parameter IO_address = 32'h00000200; // we respond to an "IO write" at this address
parameter CBECD_IOWrite = 4'b0011;
////////////////////////////////////////////////////
reg Transaction;
wire TransactionStart = ~Transaction & ~FRAMEn;
wire TransactionEnd = Transaction & FRAMEn & IRDYn;
wire Targeted = TransactionStart & (AD==IO_address) & (CBE==CBECD_IOWrite);
wire LastDataTransfer = FRAMEn & ~IRDYn & ~TRDYn;
always @(posedge CLK or negedge RSTn)
if(~RSTn) Transaction <= 0;
else
case(Transaction)
1'b0: Transaction <= TransactionStart;
1'b1: Transaction <= ~TransactionEnd;
endcase
reg DevSelOE;
always @(posedge CLK or negedge RSTn)
if(~RSTn) DevSelOE <= 0;
else
case(Transaction)
1'b0: DevSelOE <= Targeted;
1'b1: if(TransactionEnd) DevSelOE <= 1'b0;
endcase
reg DevSel;
always @(posedge CLK or negedge RSTn)
if(~RSTn) DevSel <= 0;
else
case(Transaction)
1'b0: DevSel <= Targeted;
1'b1: DevSel <= DevSel & ~LastDataTransfer;
endcase
assign DEVSELn = DevSelOE ? ~DevSel : 1'bZ;
assign TRDYn = DevSelOE ? ~DevSel : 1'bZ;
wire DataTransfer = DevSel & ~IRDYn & ~TRDYn;
reg LED; always @(posedge CLK) if(DataTransfer) LED <= AD[0];
endmodule
PCI 1 - PCI 的工作原理
中使用的,较新的 PCI 版本包括 PCI 2.3 和 PCI 3.0。
PCI 规范
PCI 由一个名为 PCI 特别兴趣小组(简称 PCI-SIG)的小组开发和维护。与以太网规范不同,PCI规范不能免费下载。 您需要成为 PCI-SIG 的成员才能访问该规范。 由于成为会员的费用很高,您可能需要检查您公司的硬件组(假设您在半导体行业工作),看看您是否可以访问该规范。
否则,这里有一个简短的介绍,然后是一些链接以获取更多信息。
PCI特性
PCI总线有4个主要特点:同步
面向事务/突发
总线母带
即插即用
PCI 是同步的
PCI 总线使用一个时钟。 默认情况下,时钟以 33MHz 运行,但可以运行得更低(一直到空闲 = 0MHz)以节省功耗,如果您的硬件支持,也可以运行更高 (66MHz)。PCI 面向事务/突发
PCI是面向事务的。您开始交易
指定起始地址(一个时钟周期)
您可以根据需要发送任意数量的数据(许多后续时钟周期)
您结束交易
PCI 允许总线主控
PCI 事务在主从关系中工作。 主服务器是启动事务(可以是读取或写入)的代理。虽然主机 CPU 通常是总线主控器,但所有 PCI 板卡都可能声明总线并成为总线主站。
PCI是即插即用的
PCI板是即插即用的。这意味着 host-CPU/host-OS 可以:确定PCI总线中每个PCI板卡的标识(制造商和功能(视频,网络...))
确定每个板卡的能力/要求(需要多少内存空间,多少个中断......
重新定位每个主板内存空间
PCI“空间”
PCI 定义了 3 个“空间”,您可以在其中读取和写入。当事务开始时,主节点指定事务的起始地址,是读还是写,以及他要与哪个空间通信。
内存空间
IO 空间
配置空间
内存和 IO 空间是主力空间。 它们是“可重新定位的”(即每个板响应的地址可以移动)。
配置空间用于即插即用。 在这个空间中,每个板都必须在非常特定的地址实现非常特定的寄存器,以便主机 CPU/OS 可以弄清楚每个板的身份/能力/要求是什么。 从那里,主机 CPU/OS 启用并配置其他两个空间。
此空间是固定的,并且始终从所有 PCI 板的地址 0 开始;因此,PCI连接器的一行用作板选择(仅适用于此空间)。
PCI桥接器
PCI 设备不直接连接到主机 CPU,而是通过“桥接”芯片。这是因为 CPU 通常不会本地“说”PCI,因此桥接器必须将事务从 CPU 总线转换为 PCI 总线。 此外,CPU 永远不会像 PCI 设备那样有 3 个内存空间。 大多数 CPU 有 1 个空间(内存空间),而其他 CPU 有 2 个空间(内存和 IO)。 桥接器必须玩一些技巧,以便 CPU 仍然可以访问所有 3 个 PCI 空间。
PCI电压
PCI板可以使用3.3V或5V信号。 有趣的是,目前的 PC 都使用 5V 信号。PCI 板连接器有一个或两个插槽,用于识别板是符合 3.3V 还是 5V 标准。 例如,这是为了确保仅 3.3V 的电路板无法插入 PC 的仅 5V PCI 总线。
下面以纯 5V 板为例: 虽然该板同时兼容 5V 和 3.3V:
PCI 时序
PCI 指定与其时钟相关的时序。使用33MHz时钟,我们有:
输入端7ns/0ns Tsu/Th(建立/保持)约束
输出端 11ns Tco(时钟至输出)
PCI 2 - PCI 读写
IO 事务
最容易使用的 PCI 空间是 IO 空间。没有来自 CPU/OS 的虚拟化(即 CPU 地址 = 硬件地址)
不需要驱动程序(在 Win98/Me 上为 true,而在 Win XP/2K 上,需要驱动程序,但下面提供了通用驱动程序)
查找可用空间
在 Windows 98/Me 上,打开“设备管理器”(从“控制面板”/系统),然后显示计算机/属性并检查“输入/输出 (I/O)”面板。
在 Windows XP/2000 上,打开“系统信息”程序(程序/附件/系统工具/系统信息),然后单击“I/O”。
驱动程序
在 Win98/Me 上,IO 空间不受保护,因此不需要驱动程序。对于 WinXP/2K,GiveIO 和 UserPort 是开放 IO 空间的免费通用驱动程序。
RAM PCI卡
让我们在PCI卡中实现一个小的RAM。RAM 为 32 位 x 16 个位置。 它足够小,可以使用“直接寻址”(IO 空间非常拥挤,否则需要间接寻址)。
我们需要在主机 PC 中选择一个空闲的 IO 空间。 每个 32 位位置需要 4 个字节地址,因此我们需要 4x16=64 个连续的可用地址。 我们在这里选择了 0x200-0x23F,但您可能需要选择其他东西。
首先是模块声明。
module PCI_RAM( PCI_CLK, PCI_RSTn, PCI_FRAMEn, PCI_AD, PCI_CBE, PCI_IRDYn, PCI_TRDYn, PCI_DEVSELn );
input PCI_CLK, PCI_RSTn, PCI_FRAMEn, PCI_IRDYn;
inout [31:0] PCI_AD;
input [3:0] PCI_CBE;
output PCI_TRDYn, PCI_DEVSELn;
parameter IO_address = 32'h00000200; // 0x0200 to 0x23F
parameter PCI_CBECD_IORead = 4'b0010;
parameter PCI_CBECD_IOWrite = 4'b0011;
然后,我们通过“PCI_Transaction”寄存器跟踪公交车上发生的事情。
“PCI_Transaction”在进行任何交易时被断言,无论是对我们,还是对公共汽车上的任何其他卡。
reg PCI_Transaction;
wire PCI_TransactionStart = ~PCI_Transaction & ~PCI_FRAMEn;
wire PCI_TransactionEnd = PCI_Transaction & PCI_FRAMEn & PCI_IRDYn;
always @(posedge PCI_CLK or negedge PCI_RSTn)
if(~PCI_RSTn) PCI_Transaction <= 0;
else
case(PCI_Transaction)
1'b0: PCI_Transaction <= PCI_TransactionStart;
1'b1: PCI_Transaction <= ~PCI_TransactionEnd;
endcase
// We respond only to IO reads/writes, 32-bits aligned
wire PCI_Targeted = PCI_TransactionStart & (PCI_AD[31:6]==(IO_address>>6)) & (PCI_AD[1:0]==0) & ((PCI_CBE==PCI_CBECD_IORead) | (PCI_CBE==PCI_CBECD_IOWrite));
// When a transaction starts, the address is available for us to register
// We just need a 4 bits address here
reg [3:0] PCI_TransactionAddr;
always @(posedge PCI_CLK) if(PCI_TransactionStart) PCI_TransactionAddr <= PCI_AD[5:2];
现在,再增加几个寄存器,以便能够声明交易并记住它是读取还是写入
wire PCI_LastDataTransfer = PCI_FRAMEn & ~PCI_IRDYn & ~PCI_TRDYn;
// Is it a read or a write?
reg PCI_Transaction_Read_nWrite;
always @(posedge PCI_CLK or negedge PCI_RSTn)
if(~PCI_RSTn) PCI_Transaction_Read_nWrite <= 0;
else
if(~PCI_Transaction & PCI_Targeted) PCI_Transaction_Read_nWrite <= ~PCI_CBE[0];
// Should we claim the transaction?
reg PCI_DevSelOE;
always @(posedge PCI_CLK or negedge PCI_RSTn)
if(~PCI_RSTn) PCI_DevSelOE <= 0;
else
case(PCI_Transaction)
1'b0: PCI_DevSelOE <= PCI_Targeted;
1'b1: if(PCI_TransactionEnd) PCI_DevSelOE <= 1'b0;
endcase
让我们认领交易。
// PCI_DEVSELn should be asserted up to the last data transfer
reg PCI_DevSel;
always @(posedge PCI_CLK or negedge PCI_RSTn)
if(~PCI_RSTn) PCI_DevSel <= 0;
else
case(PCI_Transaction)
1'b0: PCI_DevSel <= PCI_Targeted;
1'b1: PCI_DevSel <= PCI_DevSel & ~PCI_LastDataTransfer;
endcase
最后,RAM本身被写入或读取,PCI_AD总线相应地驱动。
// PCI_TRDYn is asserted during the whole PCI_Transaction because we don't need wait-states
// For read transaction, delay by one clock to allow for the turnaround-cycle
reg PCI_TargetReady;
always @(posedge PCI_CLK or negedge PCI_RSTn)
if(~PCI_RSTn) PCI_TargetReady <= 0;
else
case(PCI_Transaction)
1'b0: PCI_TargetReady <= PCI_Targeted & PCI_CBE[0]; // active now on write, next cycle on reads
1'b1: PCI_TargetReady <= PCI_DevSel & ~PCI_LastDataTransfer;
endcase
// Claim the PCI_Transaction
assign PCI_DEVSELn = PCI_DevSelOE ? ~PCI_DevSel : 1'bZ;
assign PCI_TRDYn = PCI_DevSelOE ? ~PCI_TargetReady : 1'bZ;
wire PCI_DataTransferWrite = PCI_DevSel & ~PCI_Transaction_Read_nWrite & ~PCI_IRDYn & ~PCI_TRDYn;
// Instantiate the RAM
// We use Xilinx's synthesis here (XST), which supports automatic RAM recognition
// The following code creates a distributed RAM, but a blockram could also be used (we have an extra clock cycle to get the data out)
reg [31:0] RAM [15:0];
always @(posedge PCI_CLK) if(PCI_DataTransferWrite) RAM[PCI_TransactionAddr] <= PCI_AD;
// Drive the AD bus on reads only, and allow for the turnaround cycle
reg PCI_AD_OE;
always @(posedge PCI_CLK or negedge PCI_RSTn)
if(~PCI_RSTn) PCI_AD_OE <= 0;
else
PCI_AD_OE <= PCI_DevSel & PCI_Transaction_Read_nWrite & ~PCI_LastDataTransfer;
// Now we can drive the PCI_AD bus
assign PCI_AD = PCI_AD_OE ? RAM[PCI_TransactionAddr] : 32'hZZZZZZZZ;
endmodule
现在我们可以读写PCI卡了!
设计注意事项
不使用 PCI_CBE 字节启用,因此软件应该只发出 32 位交易,对齐。
您可能会惊讶地发现,PCI“PAR”信号(总线奇偶校验)也没有使用。
虽然 PAR 生成是 PCI 合规性所必需的,但它的检查可能不是因为我可以访问的 PC 在没有它的情况下工作正常...... 由于我无法在真实硬件中测试它,所以我省略了它。上面的代码支持突发传输,但当前的 PC 网桥似乎不会发出突发(至少对于 IO 空间)。 x86 处理器支持突发 IO 指令 (REP IN/OUTS),但它们最终被分解为 PCI 总线上的单个事务。 此外,我不确定突发 IO 是否需要自动递增 IO 地址,特别是因为 REP INS/OUTS 指令不需要。
但是,由于不递增对时间有很好的影响(更多细节见下文),因此我以这种方式保留了代码。
发出 IO 读/写事务
在 PC 上,使用 x8086“IN”和“OUT”处理器指令发出 IO 事务。
某些编译器没有对这些函数的本机支持,因此您可能必须使用内联汇编程序函数。下面是 Visual C++ 的示例:
void WriteIO_DWORD(WORD addr, DWORD data)
{
__asm
{
mov dx, addr
mov eax, data
out dx, eax
}
}
DWORD ReadIO_DWORD(WORD addr)
{
__asm
{
mov dx, addr
in eax, dx
}
}
GUI PCI IO 训练器软件
您可以使用这个简单的 IOtest 应用程序在 PC 上发出 32 位 IO 读取和写入。
这直接适用于 Win98/Me。 确保 GiveIO 或 UserPort 在 WinXP/2K 上运行。 有一点很重要:可用空间在读取时返回0xFFFFFFFF。
时序注意事项
请记住,PCI 需要:
输入端7ns/0ns Tsu/Th(建立/保持)约束
输出端 11ns Tco(时钟至输出)
大多数PCI内核都非常复杂,如果不在IO块中注册输入,就不可能满足Tsu的要求。 如果不对输出做同样的事情,也很难满足 TCO。
但这些寄存器会增加设计延迟。 上面的代码非常简单,不需要 IO 块寄存器。
该代码使用 Dragon 开发板和 Xilinx 的 ISE 软件进行了测试。
它给出了类似的东西:
时序摘要: --------------- 时序误差:0 成绩:0 设计统计: 最小周期:9.667ns(最大频率:103.445MHz) 时钟前最短输入所需时间:5.556ns 时钟后最短输出所需时间: 10.932ns |
基本满足了时钟频率(103MHz对33MHz)。
Tsu 在 PCI_DEVSELn 和 PCI_TRDYn 信号上以很大的优势(5.556ns 对 7ns)满足,而 Tco 几乎没有满足(10.932ns 对 11ns)。
如果必须在突发读取时自动递增 IO 地址,则 AD 总线上不会满足 Tco。 由于地址是静态的,并且(仅用于读取周期)PCI总线在地址阶段之后需要一个周转周期,因此数据有一个额外的时钟周期来准备。 如果没有它,TCO约为13ns,因此高于最大11ns。 但是有了额外的时钟周期,我们实际上以 28ns 的松弛(=余量)来满足时序,这非常舒适。
唯一未满足的时序是输入保持时间(0nS),希望它足够低(对于最严重的违规者为0.3nS)。 但 Xilinx 不支持限制保持时间的方法,可能是因为使用 IO 块寄存器可以“按设计”(FPGA 的)保证 0ns 保持时间。
PCI 3 - PCI 逻辑分析仪
现在我们可以在总线上发出读写事务,那么“查看”事务的实际情况不是很有趣吗?
这是用 Dragon 捕获的一个非常简单的交易。
在地址阶段,CBE 0x3,这意味着“IO 写入”。
它是地址0x00000000的 IO 写入,数据0x0200。
FPGA 作为 PCI 逻辑分析仪
能够看到总线运行可能很有趣:更好地了解其操作。
检查事务内和事务之间的总线延迟。
进行事后分析(如果您的 PCI 内核存在功能问题)。
查看信号通常需要昂贵的设备,如总线扩展器和逻辑分析仪。 这可能很棘手,因为PCI规范不允许每个PCI信号(当然每个PCI卡)上有一个以上的IO负载。 这是因为总线对容性负载或线短截线很敏感,这些负载或短截线会使高速信号失真。
但是,FPGA不能像逻辑分析仪一样工作吗?
FPGA已经连接到总线,并具有内部存储器,可用于实时捕获总线操作。 Dragon 还有一个 USB 接口,可用于转储 PCI 捕获,而不会干扰 PCI 接口实现,即使 PCI 总线“死机”。
FPGA 还可以轻松创建复杂的触发条件,这些条件将比大多数逻辑分析仪更智能......如果要在地址 17x0 进行第二次读取后捕获第 1234 次写入,该怎么办?
捕获 PCI 信号
我们在这里构建了一个“状态”(=同步)逻辑分析器。捕获的信号是:
wire [47:0] dsbr = { PCI_AD, PCI_CBE, PCI_IRDYn, PCI_TRDYn, PCI_FRAMEn, PCI_DEVSELn, PCI_IDSEL, PCI_PAR, PCI_GNTn, PCI_LOCKn, PCI_PERRn, PCI_REQn, PCI_SERRn, PCI_STOPn}; |
只有 48 个信号!
很好,如果我们选择 3 个时钟的深度,则非常适合 256 个块。
实现起来很简单:一旦设置了触发条件,一个 8 位计数器开始为模块提供信号,另一个计数器允许 USB 读取模块数据。 还添加了逻辑,以允许一定程度的预触发采集 - Dragon 板文件中的详细信息。
blockram 输出按此顺序多路复用至 USB 控制器
case(USB_readaddr[2:0]) 3'h0: USB_Data <= bro[ 7: 0]; 3'h1: USB_Data <= bro[15: 8]; 3'h2: USB_Data <= bro[23:16]; 3'h3: USB_Data <= bro[31:24]; 3'h4: USB_Data <= bro[39:32]; 3'h5: USB_Data <= bro[47:40]; 3'h6: USB_Data <= 8'h01; // padding, added for ease of implementation 3'h7: USB_Data <= 8'h02; // padding, added for ease of implementation endcase |
最后,使用 USB 批量读取命令,采集数据并将其保存到“.pciacq”文件中以供进一步分析。
PCI总线查看器
用于查看“.pciacq”文件的软件可以在这里下载。包括一个示例“.pciacq”文件,该文件是此事务列表的结果捕获:
ReadIO_DWORD( 0x200 ); ReadIO_DWORD( 0x204 ); ReadIO_DWORD( 0x208 ); ReadIO_DWORD( 0x210 ); WriteIO_DWORD( 0x204, 0x12345678 ); WriteIO_DWORD( 0x208, 0x87654321 ); WriteIO_DWORD( 0x210, 0xDEADBEEF ); ReadIO_DWORD( 0x200 ); ReadIO_DWORD( 0x204 ); ReadIO_DWORD( 0x208 ); ReadIO_DWORD( 0x210 ); |
该软件如下所示: 一件有趣的事情:

在读取周转周期中,AD 总线显示上一次读取的数据......
PCI 4 - PCI 即插即用

我们的PCI卡还没有在列表中...
配置空间
还记得PCI卡有三个“空间”吗?内存空间
IO 空间
配置空间
配置空间是PCI即插即用的核心。 操作系统(Windows、Linux等)首先读取该信息,以查找是否插入了PCI卡及其特性。
对于简单的电路板,配置空间仅包含 64 个字节。 它们的重要领域是:
抵消 | 名字 | 功能 | 注意 | 长度 |
---|---|---|---|---|
0 | 供应商 ID | 指定生产商 | ...由 PCI-SIG 分配 | 2 字节 |
2 | 设备 ID | 设备编号 | ...由制造商自己分配 | 2 字节 |
4 | 命令 | 打开和关闭对PCI板的访问 | ...但配置空间访问始终处于打开状态 | 2 字节 |
16 | BAR0(基址寄存器 0) | PCI板应响应的地址 | ...后跟 BAR1 到 BAR5 | 每个 4 个字节 |
通过在这些位置实现正确的值和寄存器,操作系统可以“找到”PCI卡。
配置空间事务
每个PCI插槽都作为称为IDSEL的信号。 IDSEL 信号不沿总线共享;每个PCI插槽都有自己的插槽。当 PCI 卡在总线上看到配置空间事务,并且断言其自己的 IDSEL 时,它知道它应该响应。
parameter PCI_CBECD_CSRead = 4'b1010; // configuration space read parameter PCI_CBECD_CSWrite = 4'b1011; // configuration space write wire PCI_Targeted = PCI_TransactionStart & PCI_IDSEL & ((PCI_CBE==PCI_CBECD_CSRead) | (PCI_CBE==PCI_CBECD_CSWrite)) & (PCI_AD[1:0]==0); |
之后,它可以是读取或写入,但它的工作方式与内存或 IO 空间相同。
一些细节:
对于供应商 ID,我们只需选择一个数字;我们只是在实验,对吧?好的,0x0100工作正常。
设备 ID 可以保留为 0
命令位 0 是 IO 空间的“开/关”位,而位 1 是内存空间的“开/关”位。
BAR0 是操作系统写入的寄存器,一旦它决定 PCI 卡应该位于哪个地址。
请参阅 PCI 规范/书籍,了解实际细节。
Windows 即插即用
实现这些寄存器后,操作系统可以发现新硬件。
但是操作系统需要驱动程序才能...

。它同意分配内存资源。

PCI 5 - 适用于 Windows 的 PCI 软件驱动程序
简单的方法
简单的方法就是让别人为你做艰苦的工作!查看 WinDriver。
这是一个商业工具包,可以在几分钟内为您构建 PCI 即插即用驱动程序解决方案。
它的工作原理是这样的:
运行一个向导来检测您的即插即用设备,包括 PCI 卡。
您选择您感兴趣的卡,为您的设备命名并创建一个“.inf”文件。
这足以让 Windows 能够识别硬件并说服他应该使用 WinDriver 的驱动程序。 退出向导,然后通过 Windows 的即插即用硬件检测来安装驱动程序。
安装驱动程序后,再次运行向导,这次是生成一些示例源代码来访问 PCI 卡。
Windriver 可能不错,但 2000 美元,如果您只想尝试 PCI 即插即用机制,那就太贵了。
艰难的道路
使用 Microsoft Windows DDK。
安装 Windows DDK
最新的 Windows DDK 版本不是免费的,而早期的化身 (98/2000) 可以免费下载。DDK 易于安装。 对于 Win98 和 Win2000 DDK,首先安装 Visual C++ 5.0 或 6.0,然后安装 DDK 本身。 然后按照“install.htm”说明使用“build”命令生成一些示例驱动程序。
最低 WDM 即插即用驱动程序
以下是 Windows 设备管理器分配 PCI 卡使用的内存资源所需的最少代码。由于它是一个WDM驱动程序,所以它可以在WinXP/2000/98中工作。
WDM 驱动程序的入口点是“DriverEntry”函数(类似于 C 程序的“main”)。
其主要目的是发布回调函数的地址。 我们的最低驱动程序只需要 2 个。
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { DriverObject->DriverExtension->AddDevice = DevicePCI_AddDevice; DriverObject->MajorFunction[IRP_MJ_PNP] = DevicePCI_PnP; return STATUS_SUCCESS; } |
WDM 驱动程序至少创建一个“设备”(如果你的电脑有多个类似的项目,则同一 WDM 驱动程序可能会创建多个设备)。 在驱动程序可以创建设备之前,我们需要一个“设备扩展”结构。 每个设备都使用该结构来存储信息。 我们可以让它变得尽可能大,一个典型的设备会在其中存储许多字段。 我们的最小设备只需要一个字段。
typedef struct { PDEVICE_OBJECT NextStackDevice; } DevicePCI_DEVICE_EXTENSION, *PDevicePCI_DEVICE_EXTENSION; |
这个“NextStackDevice”是干什么用的?WDM实现细节...
WDM 设备处理 IRP(“I/O 请求数据包”、创建/读取/写入/关闭...... WDM 设备不是单独工作的,而是组装在设备的逻辑“堆栈”中。 IRP 请求沿堆栈发送,并在途中进行处理。 堆栈是从下到上创建的(底部=硬件层,顶部=逻辑层)。 创建堆栈时,每个设备都会将自身附加到正下方的设备。 设备通常将有关设备的信息存储在设备扩展的正下方,以便以后可以转发 IRP 请求。 设备并不真正知道它在堆栈中的位置,它只是在请求到来时处理或转发请求。
无论如何,现在我们可以实现DevicePCI_AddDevice。
它创建一个设备对象,并将设备附加到设备堆栈。
NTSTATUS DevicePCI_AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { // Create the device and allocate the "Device Extension" PDEVICE_OBJECT fdo; NTSTATUS status = IoCreateDevice(DriverObject, sizeof(DevicePCI_DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, &fdo); if(!NT_SUCCESS(status)) return status; // Attach to the driver below us PDevicePCI_DEVICE_EXTENSION dx = (PDevicePCI_DEVICE_EXTENSION)fdo->DeviceExtension; dx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, pdo); fdo->Flags &= ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; } |
最后,我们可以处理即插即用 IRP 请求。
我们的最小设备仅处理START_DEVICE和REMOVE_DEVICE请求。
NTSTATUS DevicePCI_PnP(PDEVICE_OBJECT fdo, PIRP IRP) { PDevicePCI_DEVICE_EXTENSION dx = (PDevicePCI_DEVICE_EXTENSION)fdo->DeviceExtension; PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(IRP); ULONG MinorFunction = IrpStack->MinorFunction; switch(MinorFunction) { case IRP_MN_START_DEVICE: // we should check the allocated resource... break; case IRP_MN_REMOVE_DEVICE: status = IRP_NotCompleted(fdo, IRP); if(dx->NextStackDevice) IoDetachDevice(dx->NextStackDevice); IoDeleteDevice(fdo); break; } // call the device below us IoSkipCurrentIrpStackLocation(IRP); return IoCallDriver(dx->NextStackDevice, IRP); } |
START_DEVICE请求是我们接受或拒绝内存资源的请求。 在这里,我们什么都不做,只是将请求向下转发到堆栈中,在那里它总是被接受。

现在,我们的设备获得了一些内存资源,但对它们不做任何事情。
为了更有用,驱动程序需要:
在接受内存资源之前检查它们
导出设备名称
实现一些“DeviceIOcontrol”以与 Win32 应用程序通信
处理更多 IO 请求 (“IRP”)
...
评论