
简介一套基于赛灵思Spartan-6 FPGA开发板的Verilog逻辑例程合集使用ISE 14.7开发环境面向FPGA初学者与进阶设计者。资源包含30个独立工程从LED、按键、UART、PLL等基础模块到DDR3读写、Flash存储、USB、千兆以太网、音频、SD卡、VGA/LCD显示、OV7670/OV5640摄像头采集、AD/DA转换与DDS波形发生等常用功能每个例程均提供可综合的源码方便直接对照硬件板卡进行调试。压缩包大小约250MB以Verilog源码和配套教程文档为主目录按序号清晰划分便于快速定位。目前已有616人学习下载适合需要系统演练Spartan-6外设驱动、掌握FPGA接口时序设计或进行项目移植的工程师学习。1. 为什么还要碰Spartan-6和DDR3一块Spartan-6开发板在2025年看起来确实像旧物45nm工艺、ISE 14.7、DDR3最高到800MHz数据率。但你去二手平台搜一搜或者翻翻工控、通信、图像采集的存量项目会发现Spartan-6还在大量服役。干FPGA这行有个基本盘芯片可以老接口逻辑不会变。DDR3的读写调度、千兆以太网的RGMII时序约束、异步FIFO的跨时钟处理这些基本功练熟了换到Kintex、Versal只是换工具链和IP核的事。这套“SPARTAN6 DDR3 千兆以太网 30个Verilog例程”的合集恰好把这三大件凑齐了适合三类人刚入门想找一块便宜板子练手的被项目要求维护老平台的以及想系统梳理接口逻辑的。我不是在讲某个具体板卡的测评而是顺着这套硬件组合把DDR3和千兆以太网在Verilog里常见的落地做法、可抄代码和踩坑点完整捋一遍。2. Spartan-6平台选型与ISE 14.7的工程搭建2.1 Spartan-6的DDR3方案硬核MCB与软核MIG的差别Xilinx的Spartan-6和7系列有一个根本性区别Spartan-6把内存控制器做成了硬核叫MCBMemory Controller Block分布在各Bank边上。7系列用的是软核靠LUT和BRAM实现控制逻辑。这意味着Spartan-6的DDR3控制器不是完全免费的但性能和时序更容易收敛。MCB最高支持DDR3-800数据率800Mbps配置界面是ISE里的MIGMemory Interface Generator向导生成的控制器拓扑是“IP核 硬核MCB”双层结构。用MIG生成DDR3控制器时有几个关键参数必须在生成前确认清楚参数项常见选择影响Memory TypeDDR3 SDRAM决定MCB配置的管脚映射Frequency400MHz数据率800Mbps时钟越高时序约束越紧Burst Length8DDR3默认支持MIG按此规划地址映射CLCAS Latency建议让MIG自动计算不同DDR3颗粒的CL值有差异写死容易初始化失败用户接口位宽32bit或64bit位宽越大突发效率越高但逻辑占用也越多Pin/Bank必须匹配板卡原理图板卡上用哪个Bank的MCB原理图已经固化了这里有个容易忽略的坑MIG生成的DDR3用户接口不是简单的“读使能写使能”两根线而是基于Bank-Machine状态机的复杂协议。用户接口里有app_rdy、app_wdf_rdy、app_en这些握手信号初次接触的人很容易在app_wdf_rdy和app_rdy同时为高时才算有效写入这一点上翻车。2.1.1 MCB时钟与BUFG的关系Spartan-6的MCB需要独立的DCM/PLL输出时钟作为clk_mem和clk。MIG向导会在生成的顶层里自动插入DCM或PLL原语。但注意Spartan-6的MCB要求clk_mem必须是该Bank专用的时钟路径不能随便从全局时钟网络引过来。如果你在UCF里加了不合理的LOC约束编译时会报出The specified location is not valid for this component一类的错误这不是逻辑问题是物理约束问题。合集的教程里大概率会提醒打开MIG生成的.ucf文件检查LOC约束的Bank是否和板卡上的DDR3芯片所在的Bank一致。2.1.2 ISE 14.7的工程结构建议ISE 14.7是支持Spartan-6的最后一个版本之后Xilinx转向Vivado不再更新Spartan-6的支持。安装时注意ISE 14.7在Windows 10以上系统需要以管理员身份运行且要安装配套的ChipScope、PlanAhead组件。工程目录建议单独划出rtl/、ip/、ucf/、sim/四个目录IP核统一放在ip/下。Spartan-6的IP核生成文件多混在一起后期维护很痛苦。工程顶层逻辑参考结构module top_s6_ddr3_eth( input wire clk_50m, input wire rst_n, // DDR3 用户接口 output wire ddr3_clk_p, output wire ddr3_clk_n, output wire [14:0] ddr3_addr, output wire [2:0] ddr3_ba, output wire ddr3_cas_n, output wire ddr3_cke, output wire ddr3_cs_n, output wire ddr3_ras_n, output wire ddr3_we_n, inout wire [15:0] ddr3_dq, inout wire [1:0] ddr3_dqs_p, inout wire [1:0] ddr3_dqs_n, output wire [1:0] ddr3_dm, // 千兆以太网 RGMII input wire eth_rx_clk, input wire [3:0] eth_rxd, input wire eth_rx_ctl, output wire eth_tx_clk, output wire [3:0] eth_txd, output wire eth_tx_ctl, output wire eth_rst_n );代码里DDR3的用户接口一般是16位DQ、2个DQS组对应板卡的DDR3颗粒位宽。ddr3_dqs_p/dqs_n在Spartan-6里是inout类型这在MCB架构下会被自动处理。以太网接口列出的是RGMII信号12根线这是现在绝大多数PHY芯片的标准接口。2.2 DDR3时钟域与用户逻辑的跨时钟处理DDR3控制器跑在400MHz而你的用户逻辑比如以太网MAC层通常跑在125MHz或更低的时钟。MIG生成的用户接口自带一个clk信号和MCB同步的时钟所有用户逻辑都应该在这个时钟域下去操作DDR3接口信号。千万不要在顶层用自己分频产生的时钟去采DDR3接口的app_*信号MCB的时序参数全部由clk这一个源头保证。若一定要跨时钟域就插异步FIFO——这是Spartan-6上最常见的桥接结构。异步FIFO在Xilinx上有现成IP核也可以用纯Verilog自己写一个。我建议在DDR3这种关键路径上用IP核因为MCB接口对时序余量敏感自己写的格雷码同步器如果有亚稳态问题跑高速DDR3时会出现偶发读错数据极难排查。3. 千兆以太网的Verilog实现方案从GMII到RGMII的收与发3.1 为什么板载PHY几乎都是RGMII接口Spartan-6本身没有集成以太网MAC硬核必须用软核或手写逻辑实现MAC层。板卡上外接的PHY芯片比如常见的RTL8211EG、88E1512通常提供两种MAC接口GMII和RGMII。GMII是8位数据总线收发各7根调到125MHz信号太多RGMII把数据线缩减到4根在时钟的上升沿和下降沿各采4位加起来还是8位省了一半引脚。对Spartan-6这种硬件资源不富裕的平台RGMII是更务实的选型。代价是你必须在FPGA内部做一个RGMII到GMII的转换逻辑。3.1.1 RGMII发送方向的核心逻辑发送侧你需要把8位GMII数据拆成高4位和低4位分别对齐到时钟的上升沿和下降沿。同时RGMII的tx_ctl信号也分两半上升沿送tx_en下降沿送tx_err。代码片段的做法是// 发送方向GMII 降为 RGMII reg [3:0] rgmii_txd_r; reg rgmii_tx_ctl_r; reg tx_clk_90; reg [7:0] gmii_txd_dly; reg gmii_tx_en_dly; reg gmii_tx_err_dly; always (posedge clk_125m or negedge rst_n) begin if (!rst_n) begin gmii_txd_dly 8b0; gmii_tx_en_dly 1b0; gmii_tx_err_dly 1b0; end else begin gmii_txd_dly gmii_txd; gmii_tx_en_dly gmii_tx_en; gmii_tx_err_dly gmii_tx_err; end end // 使用 ODDR 原语保证沿对齐 ODDR #(.DDR_CLK_EDGE(SAME_EDGE)) u_txd_oddr_0 ( .Q(rgmii_txd[0]), .D0(gmii_txd_dly[0]), .D1(gmii_txd_dly[4]), .C(tx_clk_90), .CE(1b1), .R(1b0), .S(1b0) ); ODDR #(.DDR_CLK_EDGE(SAME_EDGE)) u_txctl_oddr ( .Q(rgmii_tx_ctl_r), .D0(gmii_tx_en_dly), .D1(gmii_tx_err_dly), .C(tx_clk_90), .CE(1b1), .R(1b0), .S(1b0) );这里的关键不是ODDR原语本身而是tx_clk_90。RGMII规定发送时钟相对于数据打90度相位偏移实际工程中常用PLL生成一个相移90度、频率仍是125MHz的时钟来驱动tx_clk输出。Xilinx的PLL原语里直接有CLKOUT_PHASE_SHIFT属性设成FIXED并给出四分之一周期即可。如果你的PHY芯片没有内部延迟补偿这个相移不做的话对端采到的数据会不稳定甚至完全采不到。3.1.2 接收方向的对齐和IDELAY接收侧PHY发出的rx_clk是与数据边沿对齐的FPGA内部必须把接收时钟移相90度后再去采数据。Spartan-6的IOB里有IDELAY原语可以做步进延迟但更常用的做法是直接用PLL对rx_clk做相移。注意rx_clk是PHY输出的频率和参考时钟不同源因此不能和发送时钟共用一个PLL。常见的做法是专门用一个PLL的CLKIN接eth_rx_clk输出相移90度的时钟作为接收逻辑的采样时钟。接收方向的数据拼接相对简单用组合逻辑就可以把四个半字节合回8位GMII数据。合集的例程里接收数据通路会多一个功能点——FIFO异步缓冲。因为rx_clk域和用户逻辑的时钟域比如DDR3的clk必然不同频用异步FIFO天然隔离这比直接打拍子可靠得多。3.2 一个精简的三速MAC状态机很多教程喜欢直接套Xilinx的三速MAC IP核这个IP核功能全但配置项多对新人反而不友好。我自己的习惯是只在链路层用精简自研MAC上层的ARP、ICMP、UDP按需软实现或部分硬化。核心的发送状态机可以精简为四个状态localparam IDLE 3d0, PREAMBLE 3d1, DATA 3d2, CRC 3d3;发送时先发7字节0x55前导码和1字节0xD5帧起始符然后以4位为单位输出MAC头目的地址、源地址、类型再输出有效负载最后计算和输出CRC32。CRC32可以用Xilinx的CRC IP核参数设成CRC-32/以太网多项式0x04C11DB7。这里最大的坑是CRC的计算范围和CRC的输出次序CRC要覆盖从目的MAC地址到帧数据段的全部内容但不包含前导码和帧起始符。CRC结果要先取反再按字节从高位到低位发送——这是一个极易出错的细节。完整描述MAC收发在千字以内根本不现实但可以给出一个有效的排错思路如果你的以太网收发逻辑通过PCping不通先用VIO核把MAC收到的原始帧内容抓出来看前导码是不是55 55 55 55 55 55 55 D5再看CRC是否通过。多数情况下问题出在CRC字节序上而不是状态机跑飞。4. 把30个例程串成一套可复用的工程源码库4.1 例程源码的常见分层组织方式30个例程是一个典型的学习包数量级从计数器、按键消抖、UART收发到DDR3读写、千兆以太网覆盖了“最小模块→接口协议→系统集成”三个层级。这类合集的源码我在实际项目里通常这样归类复用01_led/、02_key/管脚约束、时钟分频的模板几乎每个工程都会copy一份03_uart/、08_i2c/单字节/多字节串行接口适合做调试口和配置口15_fifo/、16_ram/片内存储结构是DDR3调试时的过渡方案22_ddr3/、25_ethernet/系统级IP集成学习价值最高这个分类逻辑很清楚前几类是纯逻辑中间类是存储接口后两类是复杂IP核。除非你从零开始写一个芯片级逻辑否则不要试图全部手写。比如DDR3控制器手写调度器和MCB的接口状态机成本极高收益不如直接用MIG生成的控制器。4.1.1 一个例程源码复用时的必要修改点从合集里复用一个例程至少需要改三处UCF管脚约束、顶层模块的端口例化、时钟频率参数。Spartan-6上没有像Vivado那样的XDC语法还是老一套UCF。有一类问题很常见把某个例程原封不动下到自己的板子上结果LED不亮或者时钟不工作。原因多半是开发板的外部时钟不同频。如果原例程里PLL配置的是50MHz输入、125MHz输出而你手上的板子是100MHz晶振PLL会直接失锁。这时候不要改RTL改仿真参数和PLL的CLKIN_DIVIDE值就行。DDR3读写控制实现的一个可复用的写法是把MIG的用户接口封成一个简单的读/写FIFO适配层外部逻辑只看到两个FIFOmodule ddr3_fifo_bridge #( parameter DATA_WIDTH 64 )( input wire clk_ddr, // MIG 用户时钟 input wire rst_n, // 写侧 FIFO 接口 input wire wr_fifo_clk, input wire [DATA_WIDTH-1:0] wr_fifo_data, input wire wr_fifo_en, output wire wr_fifo_full, // 读侧 FIFO 接口 input wire rd_fifo_clk, output wire [DATA_WIDTH-1:0] rd_fifo_data, output wire rd_fifo_empty, // MIG 用户接口 output wire [27:0] app_addr, output wire [2:0] app_cmd, output wire app_en, input wire app_rdy, output wire [DATA_WIDTH-1:0] app_wdf_data, output wire app_wdf_wren, output wire app_wdf_end, input wire app_wdf_rdy, input wire [DATA_WIDTH-1:0] rd_data, input wire rd_data_valid );这种桥接的意义在于DDR3的突发传输是固定长度的现在通常配置成8拍固定突发如果你不做地址重映射写侧FIFO每收到8个数据就必须触发一次写命令。实际工程中建议把DDR3的地址空间按“通道 页 偏移”的方式打散比如高8位作通道号中间10位作页号低10位作页内偏移这样不同通道的数据可以同时缓存避免一个通道的连续写独占整个DDR3带宽。4.2 一个最小系统的完整数据通路把DDR3和以太网串在一起看才体现合集的最终价值。设想一个典型场景PC通过千兆网口发送UDP包给FPGAFPGA解析出有效数据后写入DDR3缓存CPU或者后续逻辑再从DDR3读出数据。这个通路上的核心问题是字节序和数据对齐。以太网帧是字节流DDR3用户接口是固定位宽的并行数据。如果MAC层每次发送一个字节给FIFODDR3侧每次取8字节那就必须凑满8个字节再发起写请求。这个适配逻辑可以用一个简单的移位寄存器实现// 字节流拼装到 64 位 DDR3 数据通路 reg [63:0] buffer_data; reg [2:0] buffer_cnt; wire byte_valid; // MAC 层送来的字节有效信号 always (posedge clk or negedge rst_n) begin if (!rst_n) begin buffer_cnt 3b0; buffer_data 64b0; end else if (byte_valid) begin buffer_data {buffer_data[55:0], mac_rx_byte}; if (buffer_cnt 3d7) begin // 已凑满 8 字节发写请求 wr_en 1b1; buffer_cnt 3b0; end else begin buffer_cnt buffer_cnt 1b1; end end end这样做有几个好处MAC层的字节时钟和DDR3用户时钟是异步的通过FIFO已经解决了跨时钟而字节序的把控放在MAC层完成DDR3桥接层只认wr_fifo_en不关心包结构。如果你拿到配套的29个例程按这个思路去改造DDR3例程和以太网例程组合成一套网口图像采集/数据回传系统的难度会小很多。4.3 ALTERA与XILINX逻辑差异的一个提醒Spartan-6和Cyclone系列的Verilog例程不能直接互用主要差异有四处IP核配置向导不同MIG对应的是Altera的UniPHY原语名称不同ODDR对应ALTDDIO_OUTPLL原语接口不同UCF约束语法和QSF差异极大。看到很多学习者在群里问为什么ISE生成的IP核在Vivado或Quartus里打不开本质就是工具链专属的IP问题。合集的Verilog逻辑源码应该是平台相关的凡是涉及原语和IP核的文件拷贝到别的厂商平台时都要做接口替换。5. 最后一道关用自检回环验证DDR3和千兆以太网的连通性拿到一个工程先不要急着写大段业务逻辑。我建议先做一个“最小硬件确认”的操作把DDR3和以太网的硬件链路验证清楚再往上叠功能。这个操作非常简单——在顶层搭一个回环自检逻辑FPGA自发数据到DDR3的某个地址立刻读回来比较以太网则把收到的帧原样发回去。这套自检逻辑不过几百行Verilog但能帮你节省几天调试时间。DDR3自检的典型写法和坑点用一段PRBS伪随机序列生成器产生数据写入地址0到1024然后回读比较。PRBS的好处是数据模式足够随机能测出数据线上的固定驻留故障。代码主体是// PRBS 自检模块写地址递增读回比较 reg [31:0] prbs_data; reg [15:0] test_addr; reg test_fail; always (posedge clk or negedge rst_n) begin if (!rst_n) begin test_fail 1b0; test_addr 16b0; prbs_data 32hDEADBEEF; end else begin // 写入模式每个地址写一个 PRBS 值 prbs_data {prbs_data[30:0], prbs_data[31] ^ prbs_data[28]}; // 比较读取值 if (rd_data_valid rd_data ! prbs_data) test_fail 1b1; end endPRBS多项式选择要匹配你的数据位宽。这里用了XOR反馈移位寄存器多项式的选取可以查PRBS标准的百度百科或论文但工程上直接抄一个经典的31位PRBS就行。以太网回环自检更直接把RX侧的eth_rxd和eth_rx_ctl打几拍后直接送到TX侧同时把接收时钟域的数据跨到发送时钟域。注意发送方向要用独立的FIFO因为收发时钟是异步的。如果你的开发板上有PHY芯片要确保PHY工作在RGMII-to-RGMII模式部分PHY默认GMII需要用MDIO寄存器修改。你可以在PC端用Wireshark抓包看FPGA是不是原样返回了请求帧。如果返回来的是乱的数据八成是RGMII的时钟沿采样问题回到第3章检查接收时钟相移。最后给一个实用建议把30个例程中前20个简单的都过一遍综合和仿真再去做DDR3和以太网。这样你对ISE的编译错误类型会有直观认识。FPGA调试不是靠冥想是靠一次一次把信号拉出来看。合集里的教程可能有截图和比对波形但你自己动手RTL仿真一次犯过的错就变成经验了。本文还有配套的精品资源点击获取