ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PCI从设备接口的VHDL实现与Quartus License配置实战

PCI从设备接口的VHDL实现与Quartus License配置实战 简介面向FPGA与数字系统设计者的PCI接口轻量资料包围绕Altera PCI IP核授权与VHDL实现两条主线帮助初学者快速理清PCI总线工作原理、license的实际作用以及FPGA开发中的IP核集成思路。RAR压缩包仅2KB共3个文件包括2个TXT文本与1个HTML文档文本记录基础知识和来源索引HTML方便阅读查询组合简明、便于直接查看。目前已有89人学习下载。资料虽小但覆盖PCI总线结构、信号定义、事务流程、中断处理等协议重点以及VHDL在PCI接口建模、Quartus工具链中时序约束与设计验证的关键逻辑对需要在嵌入式系统、工业控制、通信设备等场景中实现高性能低延迟数据交互的开发者而言是一份概念梳理与快速上手的实用参考。1. 一份PCI的VHDL工程卡住的往往是License不是时序从 pudn 上拿到的这份 pci.rar里面除了一堆 .vhd 源文件就是 www.pudn.com.txt 和 readme.html典型的老下载站打包件。但它对一个具体问题很有价值在 Altera FPGA 上自己写一个 PCI 从设备接口不依赖官方 PCI IP 核的授权。很多人拿着这套代码第一步装 Quartus 就被 license 卡住第二步编译又遇到信号定义和时序问题。这篇按实际拆解的路径走先讲清 PCI 从设备必须处理的信号和事务再给可综合的 VHDL 状态机骨架然后处理 Altera 工具链的 License 加载最后用仿真和板级现象反推问题。适合做 PCI 采集卡、不想为单个接口核支付授权费用的团队以及需要把老代码移植到现代工程里的工程师。2. PCI总线信号与事务时序32位从设备设计的前提2.1 地址数据复用与关键控制信号PCI 是同步总线所有信号在 PCI_CLK 上升沿采样。32 位 PCI 的 AD[31:0] 在同一个总线周期里分时传送地址和数据FRAME_n 拉低后的第一个时钟是地址期AD 上是地址C/BE_n[3:0] 上是总线命令随后进入数据期AD 上是数据C/BE_n 变成字节使能。正因为复用从设备必须先把地址锁存否则后面的数据期会把地址覆盖掉这是新手写 PCI 状态机最容易犯的错。从设备需要理解的控制信号可以压成一张表信号方向作用FRAME_n输入主设备置低表示事务开始最后一个数据期拉高IRDY_n输入主设备准备好发送或接收数据TRDY_n输出从设备准备好传输与 IRDY_n 同时为低时数据有效DEVSEL_n输出从设备声明命中地址STOP_n输出从设备请求终止当前事务IDSEL输入配置访问的设备选择通常接到 AD 的某一位PAR输出地址和数据总线的偶校验DEVSEL_n 的拉低时机有 fast、medium、slow 三种分别对应地址期后 1、2、3 个时钟。配置空间 Status 寄存器里的 DEVSEL 字段记录的就是这个档位BIOS 和操作系统枚举时会读它。多数自研从设备用 medium也就是地址期后第 2 个时钟拉低给地址译码留出余量时序上也更容易收敛。2.2 FRAME_n/IRDY_n/TRDY_n 握手与突发传输一次典型的存储器写事务是这样的主设备拉低 FRAME_n同时 AD 上放地址、C/BE_n 上放命令 0111存储器写下一个时钟进入数据期主设备把第一个数据放到 AD 上并拉低 IRDY_n。从设备完成译码后拉低 DEVSEL_n再拉低 TRDY_n。IRDY_n 和 TRDY_n 同时为低的那一个时钟32 位数据被写入从设备内部寄存器。突发传输的关键在于只要 FRAME_n 保持低主设备就不需要重新发送地址每次握手成功后地址自动按 4 字节递增。FRAME_n 拉高后的那个数据期是最后一个数据期从设备要在这个时候把最后一笔数据接收干净不能因为看到 FRAME_n 变高就立刻终止自己的状态机否则会丢掉最后一拍。C/BE_n 的每一根线对应 AD 的一个字节读取时要把数据按字节使能掩码写入时只更新被允许的字节。2.3 配置空间与 BAR 寄存器初始化PCI 枚举依赖配置空间也就是 256 字节的配置寄存器区。操作系统复位后通过配置读命令1010读取 Vendor ID、Device ID、Class Code 和 BAR。最常用的头区域布局如下表偏移寄存器说明0x00Vendor ID / Device ID厂商和设备标识枚举第一道门槛0x04Command / StatusCommand 控制响应使能Status 记录能力0x08Revision ID / Class Code类代码决定驱动匹配0x10-0x24BAR0-BAR5基地址寄存器0x3CInterrupt Line / Interrupt Pin中断路由用纯 VHDL 实现配置空间时Vendor ID 和 Device ID 往往定义为常量。这里特别提醒VHDL 里定义常量不只是写constant VENDOR_ID : std_logic_vector(15 downto 0) : x1172就完了还要注意位序。PCI 配置空间按小端处理AD[15:0] 是 Vendor IDAD[31:16] 是 Device ID拼接到 32 位数据总线时应该写成DEVICE_ID VENDOR_ID写反了系统枚举出来就是两个乱码 ID。配置空间读的数据通路可以这样表达-- 测试阶段用约定段量产前替换为申请到的 ID constant VENDOR_ID : std_logic_vector(15 downto 0) : x1172; constant DEVICE_ID : std_logic_vector(15 downto 0) : x4C43; cfg_read_data DEVICE_ID VENDOR_ID when cfg_addr(7 downto 2) 000000 else status_reg command_reg when cfg_addr(7 downto 2) 000001 else (others 0);这段代码里的cfg_addr来自地址期锁存的 AD[7:2]取高 6 位作为配置寄存器索引等价于按 4 字节对齐的配置地址除以 4。DEVICE_ID VENDOR_ID是 32 位拼接低位在前与 PCI 配置空间的小端格式一致。测试阶段不要用真实产品 ID建议先用约定段避免和系统里已有硬件冲突。BAR0 的编码同理IO 空间 bit0 写 1Memory 空间 bit0 写 0bit2:1 表示映射宽度可编程位的数量决定系统能给它分配多大的地址窗口。系统分配地址时会对 BAR 写全 1 再读回所以不可编程位必须固定为 0否则读回的掩码会暴露超出实现的功能位驱动会按错误的地址窗操作硬件。3. VHDL状态机实现PCI从设备地址译码与读写数据通路3.1 状态机骨架与锁存时机剥掉协议外壳直接给可综合的从设备状态机骨架。核心四个状态IDLE、ADDR、DATA、TURN_AROUND。状态转移关系先看下表当前状态转移条件下一状态本周期输出动作IDLEframe_n 0ADDRAD 保持高阻控制信号释放ADDR无条件DATA锁存地址和命令启动译码DATA最后一个数据期完成TURN_AROUND完成最后一笔握手后释放TURN_AROUNDframe_n 1IDLEAD 释放DEVSEL_n/TRDY_n 拉高下面代码是同步设计所有跳转在 clk 上升沿复位采用异步复位兼容 PCI 的 RST_n 时序。省略了库声明和 entity 端口定义。architecture rtl of pci_target is type pci_state_t is (IDLE, ADDR, DATA, TURN_AROUND); signal state : pci_state_t : IDLE; signal addr_latch : std_logic_vector(31 downto 0) : (others 0); signal cmd_latch : std_logic_vector(3 downto 0) : (others 0); signal devsel_n_o : std_logic : 1; signal trdy_n_o : std_logic : 1; begin devsel_n devsel_n_o; trdy_n trdy_n_o; ad ad_out when ad_oe 1 else (others Z); pci_fsm : process(clk, rst_n) begin if rst_n 0 then state IDLE; addr_latch (others 0); cmd_latch (others 0); devsel_n_o 1; trdy_n_o 1; elsif rising_edge(clk) then case state is when IDLE devsel_n_o 1; trdy_n_o 1; if frame_n 0 then state ADDR; end if; when ADDR addr_latch ad; cmd_latch cbe_n; state DATA; when DATA if decode_hit 1 then devsel_n_o 0; end if; if irdy_n 0 then trdy_n_o 0; -- 读写数据通路在 3.2 展开 end if; if frame_n 1 and irdy_n 0 and trdy_n_o 0 then state TURN_AROUND; end if; when TURN_AROUND devsel_n_o 1; trdy_n_o 1; ad_out (others Z); -- 假设主设备不发起背靠背事务实际工程需处理并发 frame_n if frame_n 1 then state IDLE; end if; end case; end if; end process; end rtl;这段骨架把 AD 设计为 inout 三态ad_out只在读数据期由ad_oe置位时驱动其余时间是高阻。ADDR 状态里必须采样 AD 和 C/BE_n因为它们只存在一个时钟周期漏采会导致地址丢失。DATA 状态里IRDY_n 和 TRDY_n 同时拉低代表一个数据期完成当 FRAME_n 已经拉高且本次握手完成说明这是最后一个数据期下一个周期进入 TURN_AROUND。TURN_AROUND 是 PCI 特有的总线翻转周期目的是让从设备在退出事务前释放 AD避免和下一个主设备的总线驱动冲突这个周期不能省。3.2 地址译码与突发传输的地址递增decode_hit要从锁存的地址判断是否落在自己的地址空间内。常见做法是掩码比较相当于把 BAR 的高位地址域和基址做与运算constant ADDR_MASK : std_logic_vector(31 downto 0) : xFFFFF800; constant BASE_ADDR : std_logic_vector(31 downto 0) : x00000000; decode_hit 1 when (addr_latch and ADDR_MASK) BASE_ADDR else 0;这里是 VHDL 里最容易写错的点之一掩码比较用(addr_latch and ADDR_MASK) BASE_ADDR逻辑上没问题但ADDR_MASK必须声明成和addr_latch等宽的 32 位写成xFFFFF800这种裸常量在一些综合器里会被推断为 16 位导致长度不匹配的编译错误。显式写出std_logic_vector(31 downto 0)就避免了这个坑。提示掩码和基址的位宽必须与地址总线一致综合器报 mismatch 时先检查常量声明是否正好 32 位。突发传输时每次握手成功地址自动加 4。这里的加 4 是指向下一个双字不是加 1if irdy_n 0 and trdy_n_o 0 then if cmd_latch C_MEM_READ or cmd_latch C_MEM_WRITE then addr_latch std_logic_vector(unsigned(addr_latch) 4); end if; end if;C_MEM_READ 和 C_MEM_WRITE 是 PCI 命令编码的常量值分别是 0110 和 0111放在包(Package)里集中定义不要散落在各个模块里。addr_latch是 std_logic_vector不能直接做算术必须先用unsigned转换再做加法最后再转换回来。地址的位宽取多少取决于 BAR 空间大小如果 BAR0 只声明了 4KB那么地址位只用低 12 位高位的掩码必须包含不可编程位否则分配地址时会错位。3.3 读写数据通路与关键信号延迟写通路AD 上的数据在 IRDY_n 拉低的同时有效但 TRDY_n 的拉低时机由从设备决定。所以从设备应该在 IRDY_n 为低、TRDY_n 为低的那个时钟边界采样 AD才是真正被双端确认的数据。代码里如果先拉低 trdy_n_o下一拍再采 AD等于给数据多争取了一个周期的稳定时间不违反 PCI 协议主设备会等待。读通路从内部 RAM 读到数据出现在 AD 引脚要经过 RAM 读、地址译码、IOE 输出三叔路径整条组合延迟要控制在 33MHz 一个周期内。实际工程中我一般会把 RAM 输出打一拍再送 ad_out多一个等待周期换取时序收敛的确定性process(clk) begin if rising_edge(clk) then if frame_n 1 then ad_out read_data_reg; -- 最后一个数据期后保持住避免悬空 end if; end if; end process;寄存输出后TURN_AROUND 周期要把 ad_out 置为高阻否则 AD 总线被持续驱动会和下一个总线事务冲突。上面的骨架在 TURN_AROUND 里已经把 ad_out 置为高阻两段代码配合起来读数据期结束到释放总线之间留了一拍正好覆盖 PCI 的 turn-around 时序要求。大块存储阵列不要用复位初始化。PCI 的 RST_n 只在加电时有效为每个存储器单元生成复位逻辑会严重拉低 Fmax。正确做法是状态机和控制寄存器用异步复位存储阵列只做写使能保护内容交给软件上电后初始化。VHDL process 里如果信号在某个分支没有默认赋值综合器会推断锁存器。FSM 的输出建议在 process 开头先给默认值状态分支里只做覆盖综合报告里的 inferred latch 警告基本就能清零。4. Quartus II 中的 License 配置从环境变量到 IP 核授权4.1 工具链选择Quartus 版本与许可约束Altera 的开发软件从 Quartus II 改名为 Quartus PrimeAltera 被 Intel 收购后叫 Intel Quartus Prime。和老接口打交道的研发团队很多人保留 Quartus II 13.0sp1 之前的版本因为那个阶段编译基础设计不需要额外的许可证文件。但正式项目我更推荐 Quartus Prime Lite 加有效授权原因有两个老版本对后续器件支持不完整以及 VHDL-2008 的语法支持不足pci.rar 里的老代码在新工具里反而容易暴露出可移植性问题提早发现是好事。这里要区分两类许可证一类是 Quartus 软件本身的授权另一类是 Intel FPGA IP 核的授权。如果使用官方 PCI Compiler IP 核Quartus 会检查对应的 IP 核许可如果 PCI 接口是自研纯 VHDL就不需要 IP 核许可只需要基础工具链的授权。这份 pci.rar 的价值恰恰在后者它是一套第三方 VHDL 实现可以免掉 IP 核授权环节只要把软件本身的许可证配置正确整个工程就能完整编译下载。4.2 License 文件加载GUI 与环境变量两条路径拿到 license.dat 后让 Quartus 识别它有两类路径。图形界面在 Quartus 菜单 Tools 里的 License Setup把 License file 指向 .dat 文件。脚本和 CI 环境里更常用环境变量 LM_LICENSE_FILE这是 FlexNet 体系的通用做法Linux 和 Windows 都认# Linux 临时生效只在当前终端内使用 export LM_LICENSE_FILE/opt/intelFPGA/license.dat quartus --64bit:: Windows 写入用户环境变量永久生效需要新开终端 setx LM_LICENSE_FILE C:\intelFPGA\license.dat注意setx写入的是注册表里的用户环境变量对已经打开的终端不生效必须新开窗口再启动 Quartus。set只对当前会话有效。很多人改完环境变量直接跑 Quartus 还报错问题就出在这里。license 文件和网卡 MAC 绑定是常见约束重装网卡驱动或启用了虚拟机网卡都会改变 MAC 集合导致原本有效的 license 失效报错特征是 hostid 字段不匹配。验证授权文件是否有效可以直接用 FlexNet 自带的 lmutillmutil lmstat -a -c /opt/intelFPGA/license.dat输出里能看到每个 feature 的当前状态。重点看是 UNUSED 还是 EXPIREDUNUSED 表示文件有效只是当前没人占用EXPIRED 是时间问题SERVER line 报错则是文件路径或 server 地址问题。4.3 常见 License 错误与排查顺序报错或现象可能原因处理方式Fatal License Error: Invalid license文件路径错误或文件损坏使用绝对路径确认 .dat 不是 0 字节Feature is expired系统时间超出授权期限校准系统时间检查 RTC 设置Hostid mismatch网卡 MAC 与 license 不一致停用虚拟网卡或重新绑定主机 MACCannot connect to license server节点锁定版被当网络版使用确认 license 是 node-locked 还是 floatingLM_LICENSE_FILE 被覆盖多个 EDA 工具变量冲突用分号拼接而不是覆盖多 EDA 工具共用一台机器时LM_LICENSE_FILE 被覆盖是最隐蔽的坑。安装有 Vivado 和 Quartus 的环境里后安装的工具可能把变量改成自己的路径启动 Quartus 时它会去读 Vivado 的 license server连不上就立刻报 Fatal Error。处理方法是把两个路径用分号拼接并且保证前一个路径可达。这个现象在同时做 FPGA 和嵌入式开发的机器上几乎必踩。4.4 Quartus 工程挂接第三方 VHDL 源文件把 pci.rar 里的 VHDL 挂进 Quartus 工程不要全选添加。先在工程目录建 src 子目录把顶层、子模块、仿真文件分开用 Quartus 的 Add File 按依赖顺序添加。VHDL 的 Library 声明如果写了 work 库编译顺序决定一切引用了 pci_pkg 的模块必须在包之后编译否则报 cannot find entity。编码问题值得单独讲。pudn 上下载的源码常见 GBK 或 ANSI 编码Quartus 默认按 UTF-8 读取注释里的中文会变成乱码文件里如果出现半个中文字符可能导致整个文件解析失败。保险做法是拿到文件先用编辑器另存为 UTF-8 无 BOM或者把非 ASCII 注释直接删掉。这一步花两分钟比后面在编译日志里猜乱码原因省时间得多。5. 仿真与板级排错从 ModelSim 波形到设备管理器感叹号自研 PCI 从设备调试第一个分水岭是仿真第二个是上板。板级现象很有误导性设备管理器里“PCI 简单通讯控制器”带黄色感叹号看起来像驱动问题根因往往是配置空间的 Vendor ID 或 Class Code 不正确系统匹配不到对应驱动。先把仿真做扎实再上板能省一半时间。ModelSim 里跑 VHDL 仿真testbench 要模拟主设备行为拉低 frame_n 发地址和命令等 devsel_n 拉低再按数据期逐个握手。仿真波形重点看 frame_n、irdy_n、trdy_n、devsel_n 四个信号。trdy_n 一直不拉低说明从设备状态机卡在等待devsel_n 拉低后又被立即释放通常是数据期把地址覆盖了也就是没有锁存地址的典型症状。这个症状在仿真里一分钟就能定位在板上可能折腾一下午。USB-Blaster 的代码 39 是 Altera 环境里另一个高频问题。Windows 设备管理器报代码 39 意味着驱动加载失败通常不是 JTAG 链的问题而是驱动签名或旧驱动残留。处理顺序彻底卸载旧驱动以管理员身份安装 Quartus 自带的驱动最后插拔设备让系统重新枚举。这个思路同样适用于 PCI 设备上电后无法识别先看枚举再看驱动不要上来就怀疑硬件。SignalTap II 抓总线信号时用 frame_n 下降沿触发采集深度 4K触发位置设置在中间能同时看到事务开始前后的状态。抓回波形先看 DEVSEL_n 拉低的时机是否符合设计再量 TRDY_n 和 IRDY_n 的握手节奏。时序约束方面SDC 文件按 33MHz 时钟约束参考值如下create_clock -name pci_clk -period 30.303 -waveform {0 15.151} [get_ports clk] set_input_delay -clock pci_clk -max 11 [get_ports {frame_n irdy_n ad[*] cbe_n[*]}] set_output_delay -clock pci_clk -max 12 [get_ports {trdy_n devsel_n ad[*]}]-period 30.303对应 33.33MHz 主频周期 30.3ns。set_input_delay的最大值来自 PCI 规范的总线参数表也就是主设备输出到从设备输入之间的总延迟set_output_delay是从设备输出到主设备采样沿之间的延迟预算。示例里的 11ns 和 12ns 按典型 33MHz 总线环境估算实际值要根据器件手册调整。SDC 里的 pin 名要和工程 net 名严格一致ad[*] 这种写法需要 TimeQuest 能匹配到对应总线否则会报找不到对象。约束值宁可多留 1-2ns 余量PCB 走线长度和负载数量都会改变实际延迟。最终验证顺序建议仿真通过状态机握手上板 SignalTap 确认真实事务的响应时序然后才看 Windows 枚举结果。如果设备枚举正常但资源冲突检查 BAR 空间大小和配置是否一致如果枚举不出来回头查 IDSEL 的连接和配置空间读通路这两个位置是自研 PCI 设备最容易出错的地方。本文还有配套的精品资源点击获取
返回列表