新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > FPGA:PCI项目

FPGA:PCI项目

作者:时间:2024-01-09来源:EEPW编译收藏

是功能强大的 PCI 开发平

本文引用地址:http://www.eepw.com.cn/article/202401/454595.htm

PCI 0 - 简单的

这是 PCI 代码的一个示例。 我们使用 PCI 写入命令来控制 LED。 写“0”可关闭 LED,写“1”可打开 LED!

台,这要归功于其可重新编程性和运行速度。

// 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 2.2 32 位,这是当今 PC
中使用的,较新的 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是面向事务的。
  1. 您开始交易

  2. 指定起始地址(一个时钟周期)

  3. 您可以根据需要发送任意数量的数据(许多后续时钟周期)

  4. 您结束交易

PCI 是 32 位总线,因此有 32 条线来传输数据。 在事务开始时,总线用于指定 32 位地址。 一旦指定了地址,就可以经历许多数据周期。该地址不会重新传输,而是在每个数据周期自动递增。 若要指定其他地址,将停止事务,并启动新事务。 因此,PCI带宽在突发模式下得到充分利用。
PCI 允许总线主控
PCI 事务在主从关系中工作。 主服务器是启动事务(可以是读取或写入)的代理。
虽然主机 CPU 通常是总线主控器,但所有 PCI 板卡都可能声明总线并成为总线主站。

PCI是即插即用的
PCI板是即插即用的。这意味着 host-CPU/host-OS 可以:
  • 确定PCI总线中每个PCI板卡的标识(制造商和功能(视频,网络...))

  • 确定每个板卡的能力/要求(需要多少内存空间,多少个中断......

  • 重新定位每个主板内存空间

最后一个功能是即插即用的重要组成部分。 每块板都响应一些地址,但可以对它响应的地址进行编程(即每块板生成自己的板/片选信号)。 这允许操作系统将每个板的地址空间“映射”到他想要的位置。
PCI“空间”
PCI 定义了 3 个“空间”,您可以在其中读取和写入。
当事务开始时,主节点指定事务的起始地址,是读还是写,以及他要与哪个空间通信。

  1. 内存空间

  2. IO 空间

  3. 配置空间

它们的工作原理如下:
  • 内存和 IO 空间是主力空间。 它们是“可重新定位的”(即每个板响应的地址可以移动)。

  • 配置空间用于即插即用。 在这个空间中,每个板都必须在非常特定的地址实现非常特定的寄存器,以便主机 CPU/OS 可以弄清楚每个板的身份/能力/要求是什么。 从那里,主机 CPU/OS 启用并配置其他两个空间。
    此空间是固定的,并且始终从所有 PCI 板的地址 0 开始;因此,PCI连接器的一行用作板选择(仅适用于此空间)。

为了符合要求,PCI板需要实现配置空间。 内存和 IO 空间是可选的,但在实践中始终使用一个或两个。

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 读写

现在让我们做一些真正的PCI交易......

IO 事务

最容易使用的 PCI 空间是 IO 空间。
  • 没有来自 CPU/OS 的虚拟化(即 CPU 地址 = 硬件地址)

  • 不需要驱动程序(在 Win98/Me 上为 true,而在 Win XP/2K 上,需要驱动程序,但下面提供了通用驱动程序)

IO 空间的缺点是它很小(在 PC 上限制为 64KB,即使 PCI 支持 4GB)并且非常拥挤。

查找可用空间

在 Windows 98/Me 上,打开“设备管理器”(从“控制面板”/系统),然后显示计算机/属性并检查“输入/输出 (I/O)”面板。

在 Windows XP/2000 上,打开“系统信息”程序(程序/附件/系统工具/系统信息),然后单击“I/O”。

许多外围设备都在使用 IO 空间,因此自由空间候选人需要进行一些研究。

驱动程序

在 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卡了!

设计注意事项
  1. 不使用 PCI_CBE 字节启用,因此软件应该只发出 32 位交易,对齐。

  2. 您可能会惊讶地发现,PCI“PAR”信号(总线奇偶校验)也没有使用。
    虽然 PAR 生成是 PCI 合规性所必需的,但它的检查可能不是因为我可以访问的 PC 在没有它的情况下工作正常...... 由于我无法在真实硬件中测试它,所以我省略了它。

  3. 上面的代码支持突发传输,但当前的 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 块寄存器可以“按设计”( 的)保证 0ns 保持时间。


PCI 3 - PCI 逻辑分析仪

现在我们可以在总线上发出读写事务,那么“查看”事务的实际情况不是很有趣吗?

这是用 Dragon 捕获的一个非常简单的交易。

在地址阶段,CBE 0x3,这意味着“IO 写入”。
它是地址0x00000000的 IO 写入,数据0x0200。

作为 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卡还没有在列表中...

配置空间

还记得PCI卡有三个“空间”吗?
  1. 内存空间

  2. IO 空间

  3. 配置空间

配置空间是PCI即插即用的核心。 操作系统(Windows、Linux等)首先读取该信息,以查找是否插入了PCI卡及其特性。

对于简单的电路板,配置空间仅包含 64 个字节。 它们的重要领域是:

抵消名字功能注意长度
0供应商 ID指定生产商...由 PCI-SIG 分配2 字节
2设备 ID设备编号...由制造商自己分配2 字节
4命令打开和关闭对PCI板的访问...但配置空间访问始终处于打开状态2 字节
16BAR0(基址寄存器 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 卡应该位于哪个地址。

还有一些其他细节被遗漏了,比如 BAR0 的某些部分是只读的......
请参阅 PCI 规范/书籍,了解实际细节。

Windows 即插即用

实现这些寄存器后,操作系统可以发现新硬件。




但是操作系统需要驱动程序才能...




。它同意分配内存资源。


PCI 5 - 适用于 Windows 的 PCI 软件驱动程序

现在我们需要PCI卡的驱动程序,有两种方法可以获取它。

简单的方法

简单的方法就是让别人为你做艰苦的工作!

查看 WinDriver。
这是一个商业工具包,可以在几分钟内为您构建 PCI 即插即用驱动程序解决方案。

它的工作原理是这样的:

  • 运行一个向导来检测您的即插即用设备,包括 PCI 卡。

  • 您选择您感兴趣的卡,为您的设备命名并创建一个“.inf”文件。

  • 这足以让 Windows 能够识别硬件并说服他应该使用 WinDriver 的驱动程序。 退出向导,然后通过 Windows 的即插即用硬件检测来安装驱动程序。

  • 安装驱动程序后,再次运行向导,这次是生成一些示例源代码来访问 PCI 卡。

WinDriver 给您 30 天的试用时间。
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”)

  • ...



关键词: FPGA PCI接口

评论


相关推荐

技术专区

关闭