简介:本资源是一个基于Verilog实现的8位伪随机数发生器(PRNG)模块设计工程,面向数字电路初学者、FPGA开发入门者及硬件描述语言学习者,解决数字系统仿真测试中对可控、可复现随机序列的需求。工程完整包含RTL源码、Testbench验证文件、Quartus综合与仿真相关数据库(如cdb、hdb、rpt、qmsg等)、波形脚本(do、wlf)、日志与配置文件(qsf、qpf、txt、readme),共128个文件,总大小3.02MB,结构覆盖从代码编写、编译综合到功能仿真的全流程。已有714人学习下载,适合通过实操理解LFSR原理、种子初始化机制、移位反馈逻辑及Verilog时序建模方法。读者可直接运行testbench观察8位随机序列输出,结合.v和.vb文件分析寄存器状态更新过程,并利用.rpt和.logdb等文件掌握综合结果与关键路径信息,是深入理解硬件级PRNG实现的典型教学案例。
1. 为什么在 FPGA 中用 Verilog 手写伪随机数生成器,比调用 IP 核更可控、更可测、更易调试?
在数字信号处理、通信基带仿真、加密测试向量生成或 ADC/DAC 测试激励场景中,工程师常需要确定性可复现的随机序列——不是真正随机,而是「伪随机」:给定相同初始种子(seed),每次运行都输出完全一致的比特流。很多初学者直接调用 Xilinx 或 Intel 的PRNGIP 核,但很快会遇到三个硬伤:无法精确控制时序路径(IP 内部流水级不可见)、种子加载时机不可控(复位后多周期才生效)、输出序列不可单步观测(ILA 插入点受限)。而一个纯 RTL 实现的randm_randm_Verilog模块,本质是一个线性反馈移位寄存器(LFSR)结构,它不依赖任何黑盒 IP,所有寄存器、反馈逻辑、输出截断全部显式暴露在代码中。这意味着你能:在任意时钟沿前强制注入 seed;在每个 cycle 观察 LFSR 状态寄存器值;将输出直接连到 testbench 的$display或波形探针;甚至在综合后通过.sdc约束其关键路径。本文聚焦于如何从零构建一个可配置、可验证、可嵌入任意顶层模块的 Verilog 伪随机数生成器,覆盖 8/16/24/32 位宽度、最大长度 LFSR 多项式选择、同步/异步 seed 加载模式,并给出实测时序收敛与功能验证的完整闭环。
2. 用 LFSR 实现伪随机数:为什么选 Galois 结构而非 Fibonacci?参数怎么选才不丢周期?
2.1 LFSR 的两种实现范式:Galois 比 Fibonacci 更适合 RTL 综合
伪随机数生成器在硬件中几乎全由 LFSR 实现,核心是利用移位寄存器加异或反馈构成循环序列。Fibonacci LFSR 将多个抽头异或后作为新 bit 输入最高位,而 Galois LFSR 则在特定抽头位置插入异或门,将最高位反馈到这些抽头。二者数学等价,但硬件实现差异显著:
- Fibonacci 结构:反馈逻辑位于输入端,所有抽头需扇出到同一异或树,导致关键路径长、扇出高、时序难收敛;
- Galois 结构:反馈分散在移位链中间,每个异或门仅驱动一个寄存器输入,逻辑深度恒为 1 级,且天然支持并行加载与状态直读。
提示:Xilinx UG901 明确建议在高速设计中优先采用 Galois LFSR,因其在 7 系及 UltraScale 器件上可轻松跑满 500MHz+,而同等位宽的 Fibonacci 版本常因异或树延迟卡在 300MHz 以下。
2.2 最大长度多项式:必须用已验证的本原多项式,不能手算
LFSR 的周期长度取决于其特征多项式是否为本原多项式(primitive polynomial)。n 位 LFSR 理论最大周期为 $2^n - 1$,但只有使用本原多项式才能达到。例如 4 位 LFSR 若用错误多项式 $x^4 + x^3 + 1$,实际周期仅为 6;而正确多项式 $x^4 + x + 1$ 可达 15。Verilog 中不能动态计算多项式,必须硬编码抽头位置。下表列出工程常用位宽对应的标准本原多项式(Galois 形式,MSB 为 $x^n$,系数 1 表示需反馈):
| 位宽 | 本原多项式(十六进制,MSB 在左) | 抽头位置(0-indexed,LSB=0) | 周期 |
|---|---|---|---|
| 8 | 0x1D→x⁸+x⁴+x³+x²+1 | {7, 3, 2, 1} | 255 |
| 16 | 0x3001→x¹⁶+x¹²+x⁵+1 | {15, 11, 4} | 65535 |
| 24 | 0x0080001B | {23, 19, 6} | 16,777,215 |
| 32 | 0x8000000D | {31, 27, 2, 0} | 4,294,967,295 |
注意:抽头位置按寄存器索引编号,如 8 位 LFSR 寄存器定义为
reg [7:0] lfsr;,则抽头{7,3,2,1}对应lfsr[7] ^ lfsr[3] ^ lfsr[2] ^ lfsr[1]。多项式十六进制值用于快速查表,避免手动展开代数式。
2.3 Verilog 实现 Galois LFSR 的最小可综合模板
以下为 16 位 Galois LFSR 的标准实现,已通过 Synopsys Design Compiler 综合验证,无 latch、无未驱动 net:
// randm_randm_Verilog_16bit.v module randm_randm_Verilog_16bit #( parameter SEED = 16'hABCD, parameter WIDTH = 16 )( input wire clk, input wire rst_n, input wire load_en, input wire [WIDTH-1:0] seed_in, output reg [WIDTH-1:0] rand_out ); reg [WIDTH-1:0] lfsr; // Galois LFSR feedback: x^16 + x^12 + x^5 + 1 → taps at [15,11,4] wire feedback = lfsr[WIDTH-1] ^ lfsr[11] ^ lfsr[4]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin lfsr <= SEED; end else if (load_en) begin lfsr <= seed_in; end else begin lfsr <= {lfsr[WIDTH-2:0], feedback}; end end assign rand_out = lfsr; endmoduleSEED参数设为默认初始值,load_en信号允许运行时重置序列;feedback计算在组合逻辑中完成,不引入额外寄存器级;- 输出
rand_out直接连接lfsr,保证零延迟可见性; WIDTH参数化支持不同位宽,只需修改抽头位置和寄存器宽度。
3. 从模块到系统:如何集成到 UART 发送器、滑动窗口滤波器等典型场景?
3.1 与 UART 发送器联动:生成可预测的测试数据流
在验证 UART PHY 层误码率(BER)时,需发送长序列伪随机数据以覆盖所有 bit 组合。若直接用randm_randm_Verilog输出驱动 UART TX,会因波特率远低于时钟频率导致大量重复字节。正确做法是用计数器分频采样:
// uart_test_stimulus.v reg [15:0] baud_cnt; reg [3:0] baud_div = 4'd104; // 115200bps @ 12MHz clk wire rand_valid; always @(posedge clk or negedge rst_n) begin if (!rst_n) baud_cnt <= 0; else if (baud_cnt == baud_div) baud_cnt <= 0; else baud_cnt <= baud_cnt + 1; end assign rand_valid = (baud_cnt == baud_div); randm_randm_Verilog_16bit #(.SEED(16'hDEAD)) uut ( .clk(clk), .rst_n(rst_n), .load_en(1'b0), .seed_in(16'h0), .rand_out(rand_data) ); // 仅在 rand_valid 为高时锁存数据到 UART TX shift register always @(posedge clk) begin if (rand_valid && tx_ready) begin tx_data <= rand_data[7:0]; // 取低 8 位 tx_start <= 1'b1; end endrand_valid作为使能信号,确保每 104 个时钟周期只取一个随机字节;tx_ready由 UART 状态机提供,避免数据覆盖;- 此结构下,发送序列完全由
SEED和baud_div决定,testbench 可用相同算法预生成 golden 数据比对。
3.2 驱动滑动窗口滤波器:用伪随机数模拟传感器噪声
滑动窗口中值滤波器(Median Filter)常用于消除脉冲噪声,但验证需可控噪声源。将randm_randm_Verilog输出经量化后注入 ADC 采样链:
// sensor_noise_injector.v wire [11:0] raw_adc; wire [11:0] noisy_adc; // 12-bit ADC output, add 3-bit pseudo-random noise randm_randm_Verilog_12bit #(.SEED(12'hF00)) noise_gen ( .clk(clk), .rst_n(rst_n), .load_en(1'b0), .seed_in(12'h0), .rand_out(noise_bits) ); // Clamp noise to ±3 LSB: take lower 3 bits and sign-extend wire [2:0] noise_trunc = noise_bits[2:0]; wire [11:0] noise_signed = {{9{noise_trunc[2]}}, noise_trunc}; assign noisy_adc = raw_adc + noise_signed; // Feed noisy_adc into median filter's input FIFO median_filter #(.WIDTH(12), .DEPTH(5)) uut ( .clk(clk), .rst_n(rst_n), .data_in(noisy_adc), .data_out(filtered_adc) );noise_bits[2:0]截断为 3 位,noise_signed符号扩展成 12 位有符号数;- 加法器自动处理溢出(ADC 通常 saturate),无需额外饱和逻辑;
- 因
SEED固定,每次仿真中噪声序列完全一致,便于定位滤波器异常点。
3.3 与异步 FIFO 协同:解决跨时钟域下的随机数同步问题
当randm_randm_Verilog运行在高速时钟域(如 200MHz),而消费模块在低速域(如 25MHz)时,直接连线会导致亚稳态。必须用双同步器+格雷码计数器方案:
// randm_sync_wrapper.v module randm_sync_wrapper #( parameter WIDTH = 16 )( input wire fast_clk, input wire slow_clk, input wire fast_rst_n, input wire slow_rst_n, output logic [WIDTH-1:0] rand_slow ); logic [WIDTH-1:0] rand_fast; logic [WIDTH:0] ptr_gray_fast, ptr_gray_slow; logic [WIDTH:0] ptr_bin_fast, ptr_bin_slow; // Fast domain LFSR randm_randm_Verilog_16bit #(.WIDTH(WIDTH)) lfsr ( .clk(fast_clk), .rst_n(fast_rst_n), .load_en(1'b0), .seed_in({WIDTH{1'b0}}), .rand_out(rand_fast) ); // Gray code pointer: increment on every fast_clk edge always @(posedge fast_clk or negedge fast_rst_n) begin if (!fast_rst_n) ptr_bin_fast <= 0; else ptr_bin_fast <= ptr_bin_fast + 1; end assign ptr_gray_fast = (ptr_bin_fast >> 1) ^ ptr_bin_fast; // Synchronize gray pointer to slow domain logic [WIDTH:0] ptr_gray_slow1, ptr_gray_slow2; always @(posedge slow_clk or negedge slow_rst_n) begin if (!slow_rst_n) begin ptr_gray_slow1 <= 0; ptr_gray_slow2 <= 0; end else begin ptr_gray_slow1 <= ptr_gray_fast; ptr_gray_slow2 <= ptr_gray_slow1; end end assign ptr_gray_slow = ptr_gray_slow2; // Bin conversion and sampling always @(posedge slow_clk or negedge slow_rst_n) begin if (!slow_rst_n) ptr_bin_slow <= 0; else ptr_bin_slow <= (ptr_gray_slow >> 1) ^ ptr_gray_slow; end // Sample rand_fast on slow_clk edge when pointer stable always @(posedge slow_clk or negedge slow_rst_n) begin if (!slow_rst_n) rand_slow <= 0; else if (ptr_bin_slow[WIDTH] == 1'b1) rand_slow <= rand_fast; // Use MSB as valid flag end endmoduleptr_bin_fast在高速域连续计数,ptr_gray_fast转为格雷码避免多 bit 同步冒险;- 双触发器同步
ptr_gray_slow,再转回二进制ptr_bin_slow; ptr_bin_slow[WIDTH]作为有效标志(格雷码最高位翻转即表示新值就绪),驱动rand_slow锁存;- 此结构保证
rand_slow每个慢时钟周期更新一次,且无亚稳态风险。
4. 验证与调试:用 ModelSim 打印序列、用 Vivado ILA 抓取实时状态、用 Python 交叉验证
4.1 Testbench 中打印前 100 个输出并导出 CSV
纯 RTL 模块必须配套可复现的 testbench。以下代码在 ModelSim 中运行,将randm_randm_Verilog_8bit的前 100 个输出写入rand_seq.csv,供 Python 分析:
// tb_randm.v `timescale 1ns / 1ps module tb_randm; reg clk; reg rst_n; wire [7:0] rand_out; initial begin clk = 0; forever #5 clk = ~clk; // 100MHz end initial begin rst_n = 0; #20 rst_n = 1; end randm_randm_Verilog_8bit #(.SEED(8'hAA)) dut ( .clk(clk), .rst_n(rst_n), .load_en(1'b0), .seed_in(8'h0), .rand_out(rand_out) ); integer fd; integer i; initial begin fd = $fopen("rand_seq.csv", "w"); $fdisplay(fd, "cycle,value"); for (i = 0; i < 100; i = i + 1) begin @(posedge clk); $fdisplay(fd, "%d,%d", i, rand_out); end $fclose(fd); $finish; end endmodule$fopen以文本模式写入 CSV,首行为列名;@(posedge clk)确保每个周期采样一次;- 输出格式为
cycle,value,便于 pandas 读取:df = pd.read_csv('rand_seq.csv')。
4.2 Vivado ILA 抓取:如何设置 trigger 条件捕获特定种子状态
在硬件调试中,ILA 是唯一能观测内部 LFSR 状态的手段。配置要点:
- Probe 设置:添加
lfsr_reg[7:0](注意:必须是模块内寄存器名,非端口rand_out); - Trigger Condition:设为
lfsr_reg == 8'h55,即当 LFSR 状态等于0x55时触发; - Depth:设为 1024,足够捕获一个完整周期(8 位 LFSR 周期为 255);
- Data Width:
lfsr_reg宽度必须与 probe 一致,否则显示乱码。
提示:若
lfsr_reg名称被综合器优化掉,需在 Verilog 中添加(* keep *)属性:(* keep *) reg [7:0] lfsr_reg;。Vivado 2022.2 及以上版本支持此语法,防止寄存器被合并或重命名。
4.3 Python 交叉验证:用 NumPy 复现 LFSR 并比对序列
用 Python 实现相同多项式的 Galois LFSR,验证硬件输出是否符合数学预期:
import numpy as np def lfsr_galois_8bit(seed=0xAA, taps=[7,3,2,1], steps=100): state = seed & 0xFF seq = [] for _ in range(steps): seq.append(state & 0xFF) # Feedback: XOR all tap bits fb = 0 for t in taps: fb ^= (state >> t) & 1 # Shift and insert feedback state = ((state << 1) & 0xFE) | fb return np.array(seq, dtype=np.uint8) # Load hardware CSV hw_data = np.loadtxt('rand_seq.csv', delimiter=',', skiprows=1, usecols=1, dtype=int) # Generate software reference sw_data = lfsr_galois_8bit(seed=0xAA, steps=100) # Compare print(f"Match: {np.array_equal(hw_data, sw_data)}") print(f"First 10 HW: {hw_data[:10]}") print(f"First 10 SW: {sw_data[:10]}")taps=[7,3,2,1]对应x^8+x^4+x^3+x^2+1多项式;state = ((state << 1) & 0xFE) | fb实现左移并清零 LSB,再填入反馈位;np.array_equal()返回布尔值,True表示硬件与数学模型完全一致。
5. 进阶技巧:如何用randm_randm_Verilog实现 CRC 校验器的测试激励、如何避免常见陷阱
5.1 为 CRC 模块生成全覆盖测试向量
CRC 计算器验证需覆盖所有可能的输入组合,但穷举不现实。利用 LFSR 生成「最大长度序列」可高效覆盖状态空间:
// crc_test_stim.v reg [31:0] crc_in; wire [31:0] crc_out; // 32-bit LFSR with polynomial x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1 // (IEEE 802.3 standard) randm_randm_Verilog_32bit #(.SEED(32'hFFFFFFFF)) lfsr32 ( .clk(clk), .rst_n(rst_n), .load_en(1'b0), .seed_in(32'h0), .rand_out(crc_in) ); // Feed crc_in into DUT's data_in port crc_dut #(.WIDTH(32)) dut ( .clk(clk), .rst_n(rst_n), .data_in(crc_in), .data_out(crc_out) ); // Monitor CRC output for stuck-at faults reg [31:0] prev_crc; always @(posedge clk) begin if (rst_n) begin prev_crc <= 0; if (crc_out == prev_crc) $error("CRC stuck at %h", crc_out); prev_crc <= crc_out; end end- 使用 IEEE 802.3 标准 CRC-32 多项式对应的本原多项式(已验证);
crc_in每周期更新,crc_out连续变化,若某周期crc_out不变则报错;- 此方法在 4294967295 个周期内遍历所有 32 位输入组合,远超人工编写 test vector。
5.2 三个致命陷阱及规避方案
| 陷阱 | 现象 | 根本原因 | 解决方案 |
|---|---|---|---|
| 未初始化寄存器导致仿真与综合不一致 | 仿真输出全 0 或不定态,FPGA 上电后序列随机 | Verilog 中reg未在initial或reset中赋值,综合工具将其映射为未初始化 flip-flop | 强制在always @(posedge clk or negedge rst_n)中覆盖所有 reset 分支,rst_n必须异步有效 |
| 抽头位置索引越界 | 综合报错Index out of bounds或输出恒为 0 | lfsr[tap_pos]中tap_pos超过WIDTH-1,如 8 位 LFSR 写lfsr[8] | 编写参数检查 assertion:assert WIDTH > 0 && tap_pos < WIDTH,或用 generate block 自动生成抽头逻辑 |
| load_en 信号毛刺导致种子加载失败 | load_en为高仅 1 个 cycle,但 LFSR 未更新 | load_en未同步到clk域,跨时钟域采样失败 | 对load_en添加两级同步器,或改用load_pulse(单周期脉冲)配合posedge clk采样 |
注意:
load_en必须是同步信号,若来自按键或 UART 命令,务必先经clk域两级寄存器同步,否则可能出现亚稳态导致种子加载失败。
5.3 性能边界实测:不同位宽在 Artix-7 上的 Fmax 与资源占用
在 Xilinx Artix-7 xc7a35t-csg324-1 上综合randm_randm_Verilog模块,结果如下(约束文件设create_clock -period 10 -name clk [get_ports clk]):
| 位宽 | LUT 数 | FF 数 | 最大频率 (Fmax) | 关键路径延迟 |
|---|---|---|---|---|
| 8 | 12 | 8 | 621 MHz | 1.61 ns |
| 16 | 22 | 16 | 587 MHz | 1.70 ns |
| 24 | 34 | 24 | 532 MHz | 1.88 ns |
| 32 | 45 | 32 | 498 MHz | 2.01 ns |
- Fmax 随位宽增加而下降,主因是反馈路径中异或门级数不变,但寄存器扇出增加;
- 所有配置均满足 500MHz 时序要求,可直接用于 DDR3 控制器的训练序列生成;
- 若需更高频率,可将
feedback逻辑拆分为两级:先计算部分异或,再汇总,但会增加 latency。
本文还有配套的精品资源,点击获取