跳至正文

Verilog仿真模块–真随机数生成器

image-20250427182821513

前言

在进行功能仿真时,总是希望仿真条件能覆盖尽量多的情况,因此,经常需要产生随机数作为仿真的输入。Verilog 和 SV 中有能够产生随机数的系统函数$random,可惜的是此函数产生的随机数是伪随机数,重新再跑一次仿真,它还是产生那些数,这使得两次仿真没有什么区别,覆盖的测试条件是一样的。这种情况不便于测试出模块在某些特殊输入时可能出现的问题,我们希望的是每次仿真都不一样。因此,就有了在仿真文件中产生真随机数的需求。

一、随机数函数 urandom、random 和 srandom

本章参考:

  1. SystemVerilog IEEE 1800-2017 — 18.13 Random number system functions and methods
  2. SystemVerilog IEEE 1800-2017 — 20.15.1 $random function

SV 标准中提供了多个用于产生随机数的函数,简介如下。

1.1 $urandom

SystemVerilog 中加入了新的随机数生成函数 urandom(),此函数相较于$random()更加方便,函数原型:

function int unsigned $urandom[(int seed );

输入:可选参数 seed。

返回值:int unsigned 类型,无符号 32 位整数,数值大小为 0 ~ 2**32-1。

种子(seed)是一个可选参数,用于确定生成的随机数序列。种子可以是任何整数表达式。每次使用相同的种子时,随机数生成器(RNG)应生成相同的随机数序列。

RNG 是确定性的。每次程序执行时,它都会循环相同的随机序列。通过使用外部随机变量,如一天中的时间,来初始化$urandom 函数,可以使该序列变得非确定性。

1.1.1 $urandom_range(推荐使用)

更常用的函数为 $urandom_range,函数原型:

function int unsigned $urandom_range( int unsigned maxval,
                                      int unsigned minval = 0 );

输入 1:maxval,最大值,32 位无符号数,必须指定此参数;

输入 2:minval,最小值,32 位无符号数,默认为 0,即可省略此参数。

返回值:32 位无符号数,数值大小为 0 ~ 2**32-1。

另外,$urandom_range 函数内部有判断 maxval 和 minval 大小的逻辑,如果输入参数 maxval < minval,此函数会自动反转输入参数。例如:

$urandom_range(20100// 产生20 ~ 100的整数
$urandom_range(10020// 与上式等价

另一个例子:

$urandom_range(2652// 产生0 ~ 2562的整数
$urandom_range(02562// 与上式等价
$urandom_range(25620// 与上式等价

1.1.2 Vivado 中关于$urandom_range 的 BUG

理论上,$urandom_range 输入参数 maxval 的最大值应该是 2**32-1(4294967295)。但在,Vivado 仿真中 maxval 的值指定为 2^32-1(4294967295)会有问题,正常应该是 0~4294967295 范围内产生随机数,但实测时随机数只有两个,0 或者 4294967295,就好像 $urandom_range(0,-1) 一样,这显然是不符合 SV 标准中的描述的。

经过在 Vivado 2024.2 中的详细测试,maxval 的上限 是应该是 2**31-1,实测 $urandom_range(0,2**31-1) 可以正常产生随机数,而 $urandom_range(0,2**31) 就不太正常。调换 maxval 和 minval 的位置还是一样。

但是,在 modelsim SE-64 2020.4 中进行仿真时,$urandom_range(0,2**32-1) 能够正常工作,进行各种其它范围的测试都能够正常工作,完全符合 $urandom_range 在 SV 标准中的描述。

结论:函数 $urandom_range 在 Vivado 2024.2 的仿真工具 XSIM 中 存在BUG

建议,在 Vivado 中使用 $urandom_range 时,maxval 赋值不要大于 2**31-1,即 2147483647。

猜测,Vivado 中 $urandom_range 的内部实现逻辑就是给后文介绍的 $random 取绝对值,简单粗暴,但显然会造成错误。

1.1.3 不要使用带 seed 的$urandom(seed)函数

注意:不要使用带 seed 的 $urandom(seed) 函数,很容易造成问题,原本 urandom 是自动管理 seed,此函数又主动指定了 seed,可能导致 urandom_range 函数失去作用,总是产生同一个值。可以试着运行一下这段代码,就会发现每次$urandom_range 产生的值都是一样的,都是 823。

//++ 生成随机数 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
int num1;
int num2;
initial begin
  repeat(10begin
    num1 = $urandom(100);
    $display("num1 = ", num1);
    num2 = $urandom_range(01000);
    $display("num2 = ", num2);
    #1;
  end
  $stop;
end
//-- 生成随机数 ------------------------------------------------------------

并且必须使用 num1 = $urandom(seed); 这种形式,

直接用$urandom(seed),如下所示,运行仿真时会报错。

initial begin
  $urandom(100);
  $stop;
end

ERROR: [XSIM 43-3122] ” C:/_myOpenSource/verilog-simulation-module–RandomNum/SIM/genRandomNum.sv ” Line 57. urandom system task is not supported.

在 Vivado 2024.2 中仿真报 urandom 系统任务不被支持的错误。

综上,总是应该使用 $urandom_range,禁止使用 $urandom(seed),当需要固定种子时,请使用 $srandom(seed) 函数,本文后续会讲。

1.2 $random(不推荐)

SystemVerilog 中的 $random 是 Verilog 遗留的随机数生成函数,主要用于生成 32 位有符号整数 的随机数。其语法和行为与 $urandom 存在显著差异,需谨慎使用。函数原型:

function int $random([int seed]);

输入:seed,可选参数,手动指定随机数种子。若省略,使用内部状态生成随机序列。

返回值:返回 -2^31 ~ 2^31-1 之间的 有符号整数(即 int 类型)。

示例用法:

int num1;
initial begin
  num1 = $random// -2**31 ~ 2**31-1
  num1 = $random % 60// -59 ~ 59
  num1 = {$random} % 60// 0 ~ 59
end

带 seed 的用法示例:

int num2;
initial begin
  num2 = $random(100); // -2**31 ~ 2**31-1
  $display("00 num2 = ", num2);
  num2 = $random(100) % 60// -59 ~ 59
  $display("01 num2 = ", num2);
  num2 = {$random(100)} % 60// 0 ~ 59
  $display("02 num2 = ", num2);
end

带了种子,产生的值就是固定的。

注意

  1. 不要同时使用 $random$urandom,二者的种子管理逻辑冲突,可能导致随机序列异常。
  2. $random 在 SystemVerilog 中 不推荐用于新设计,优先使用更安全的 $urandom$urandom_range

1.3 $srandom

SystemVerilog 提供了 $srandom() 函数用于显式设置随机数生成器的种子,函数原型:

function void $srandom(int seed);

示例用法:

initial begin
    $srandom(100); //* 指定此initial块的种子
end

通过手动设置种子值,可以 精确控制随机数序列的起始点。相同种子会产生完全相同的随机序列,这对测试场景的复现至关重要。

设置的种子会影响后续所有 $urandom$urandom_range 的调用,直到再次调用 $srandom 改变种子。

注意事项:

  1. 不要同时使用 $srandom() 和带参数的 $urandom(seed),两者均会修改随机数生成器状态,可能导致序列混乱。
  2. SV 中有线程的概念,一个 initial 块为一个线程,每个线程都是独立的,$srandom(seed)只能改变所在 initial 块的种子,无法全局改变种子。

二、产生真随机数的方案

要产生真随机数,其实就是要改变每次仿真时,初始用到的种子,通常的做法是使用系统时间作为种子,有的方案里使用的是 $time 或者 $realtime 作为种子,实际上是不行的,这两个函数返回的是仿真时间,仿真初始时刻 $time0,但随仿真推进递增。若在初始阶段(如 initial 块开头)使用,其值可能相同,导致种子重复,如:

initial begin
  #1;
  seed = $time// 此时函数固定返回1
end

那么如何获取系统时钟呢?SV 中并没有函数可以直接使用,需要借助其它语言,有两种方案经测试可行。

2.1 使用 tcl 脚本获取系统时钟,并传参给仿真文件

使用 tcl 可以实现获取系统时钟,用到的函数为:

# 获取系统以us/ms/s为单位的时间戳,
set timestamp_us [clock microseconds]
set timestamp_ms [clock milliseconds]
set timestamp_s [clock seconds]

所有精度的时间戳均以 1970 年 1 月 1 日 00:00:00 UTC 为起点(即 UNIX 纪元)。

使用 tcl 可给 SV 传参,用的函数为:

# 将seed的值传递给目标仿真进程的SEED参数
set_property generic "SEED=$seed" $target_simset

此语句等价于在 Vivado 设置中进行如下配置。若后续不再使用此参数传递方式,需手动删除相关配置。

image-20250426210822493

要正常实现传参需要 在仿真顶层文件 中定义 parameter SEED

除此之外,在实际应用时,还考虑到,保存波形配置文件与恢复波形的问题。波形配置文件包括波形窗口中有哪些波形,顺序是什么,用什么颜色,用 16 进制/10 进制进行显示等等信息。

因为波形配置文件(如 genRandomNum_behav.wcfg),文件内部是保存了参数值 SEED 值,所以再次运行 tcl,SEED 值改变,原本的波形配置文件会因为 SEED 值改变而失效(会报警告),所以,在重启仿真之前,需要读取 wcfg 文件,替换 SEED 值为新值,这部分代码对应以下 tcl 文件的 修改波形配置文件中的 SEED 值部分

最终的 tcl 文件如下。

# ------------------------ Global Settings ------------------------
# 动态获取仿真顶层模块名
set TOP_MODULE [get_property TOP [current_fileset -simset]]
if {$TOP_MODULE eq ""} {
    puts "== ERROR: Simulation top module not set! =="
    return 1
}

# 波形配置文件(强制绝对路径)
set WAVE_CONFIG_FILE "[file normalize "${TOP_MODULE}_behav.wcfg"]"
puts "== WaveConfig Path: [file nativename $WAVE_CONFIG_FILE] =="

# ----------------- 动态种子生成(高随机性)-----------------
set timestamp_us [clock microseconds]
set pid [pid]
set seed [expr { ($timestamp_us ^ $pid) % 999983 + 1 }]  ;# 质数取模

# ----------------- 获取仿真文件集 -----------------
set sim_sets [get_filesets -filter {FILESET_TYPE == "SimulationSrcs"}]
if {[llength $sim_sets] == 0} {
    puts "== ERROR: No simulation fileset found! =="
    return 1
}
set target_simset [lindex $sim_sets 0]

# ----------------- 保存当前波形配置 关闭现有仿真 -----------------
if {[current_sim -quiet] ne ""} {
    # 保存当前波形配置
    save_wave_config $WAVE_CONFIG_FILE
    # 关闭仿真
    close_sim -force
}

# ----------------- 修改波形配置文件中的SEED值 -----------------
if {[file exists $WAVE_CONFIG_FILE]} {
    # 读取配置文件内容
    set fp [open $WAVE_CONFIG_FILE r]
    set content [read $fp]
    close $fp

    # 使用正则表达式精确替换SEED值
    # 匹配模式:(SEED=\d+) → 替换为当前种子值
    set updated_content [regsub -all {\(SEED=\d+\)} $content "(SEED=$seed)"]

    # 写回修改后的配置
    set fp [open $WAVE_CONFIG_FILE w]
    puts $fp $updated_content
    close $fp
    puts "== Updated SEED value in wave config: $seed =="
}

# ----------------- 传递新的SEED值 启动新仿真 -----------------
set_property generic "SEED=$seed" $target_simset
puts "== Starting simulation with SEED = $seed =="
launch_simulation -simset $target_simset

对应的仿真顶层文件中,相关代码如下:

//++ 随机种子 与 种子数组 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/*
* 通过外部TCL命令获取系统时间,再传递给SEED参数作为种子
* 需关闭仿真再启动仿真种子才会变化,重启仿真种子不变
*/

parameter int SEED = 0;
initial begin
  timestamp_seed = SEED;
  $display("the timestamp_seed is %d", timestamp_seed);
  $display("timestamp_seed initial success!!!!!!!!!!");
  -> timestamp_seed_ready;
end

int seeds [20];
event seeds_ready;
initial begin
  wait(timestamp_seed_ready.triggered); //* 等待系统时间种子初始化完成
  $srandom(timestamp_seed);
  foreach (seeds[i]) begin
    seeds[i] = $urandom();  //* 使用 $urandom() 初始化 seeds 数组
  end
  for (int i=0; i<20; i++) begin
    $display("seeds[%0d] = 0x%08x", i, seeds[i]);
  end
  $display("seeds initial success!!!!!!!!!!");
  -> seeds_ready; //* 种子数组初始化完成
end
//-- 随机种子 与 种子数组 ------------------------------------------------------------


//++ 生成随机数 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
int unsigned num1;
initial begin
  wait(seeds_ready.triggered); //* 等待种子数组初始化完成
  $srandom(seeds[0]); //* 指定此initial线程的初始种子
  repeat(10begin
    num1 = $urandom_range(02**31-1);
    $display("num1 = ", num1);
    #1;
  end
  $finish;
end
//-- 生成随机数 ------------------------------------------------------------

操作方法:直接运行 tcl 文件即可。

image-20250426201713187

仿真开始之后,点击 relaunch simulation 按钮重启仿真并不能更新 SEED,因为此时 tcl 文件没有运行,需要再运行 tcl 文件重新开始仿真,SEED 才会更新,相对来说没那么方便。

当然,tcl 文件还有很多玩法,可实现自动化仿真。

另外,点上图中的 Run Tcl Script…运行脚本,等价于在 Tcl Console 窗口输入命令:

source C:/_myOpenSource/verilog-simulation-module--RandomNum/SIM/run_sim.tcl

注意,修改路径和反斜杠

2.2 在仿真文件中调用 C 语言获取系统时间(推荐)

使用C语言是更推荐的方法,它的所有操作和原 SV 文件一样,只是通过 DPI-C 接口 调用 C 语言函数扩展了 SV 的功能,让 SV 可以获取到系统时间,这无疑更加方便。

SV 文件示例:

//++ 随机种子 与 种子数组 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/*
* 通过调用C语言函数获取系统时间作为种子,
* 重启仿真种子会变化
*/

import "DPI-C" function longint get_system_time();
longint timestamp_us;
int timestamp_seed;
event timestamp_seed_ready;
initial begin
  timestamp_us = get_system_time();
  $display("timestamp_us = ", timestamp_us);
  timestamp_seed = int'(timestamp_us ^ (timestamp_us >> 32)); // 高低位异或
  $display("timestamp_seed = ", timestamp_seed);
  $display("timestamp_seed initial success!!!!!!!!!!");
  -> timestamp_seed_ready;
end

int seeds [20];
event seeds_ready;
initial begin
  wait(timestamp_seed_ready.triggered); //* 等待系统时间种子初始化完成
  $srandom(timestamp_seed);
  foreach (seeds[i]) begin
    seeds[i] = $urandom();  //* 使用 $urandom() 初始化 seeds 数组
  end
  for (int i=0; i<20; i++) begin
    $display("seeds[%0d] = 0x%08x", i, seeds[i]);
  end
  $display("seeds initial success!!!!!!!!!!");
  -> seeds_ready; //* 种子数组初始化完成
end
//++ 随机种子 与 种子数组 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


//++ 生成随机数 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
int unsigned num1;
initial begin
  wait(seeds_ready.triggered); //* 等待种子数组初始化完成
  $srandom(seeds[0]); //* 指定此initial线程的初始种子
  repeat(10begin
    num1 = $urandom_range(02**31-1);
    $display("num1 = ", num1);
    #1;
  end
  $finish;
end
//-- 生成随机数 ------------------------------------------------------------

SystemVerilog 的 DPI-C(Direct Programming Interface – C) 是用于实现 SystemVerilog 与 C/C++ 代码直接交互的接口机制。它允许在 SystemVerilog 环境中无缝调用 C/C++ 函数,或在 C/C++ 中调用 SystemVerilog 函数,极大提升了系统级建模和验证的能力。

DPI-C 的核心特性:

  1. 双向交互
  • Import 函数:从 C/C++ 导入函数到 SystemVerilog 中调用。
  • Export 函数:将 SystemVerilog 函数导出到 C/C++ 中使用。
  1. 无中间层
  • 直接通过函数调用实现交互,无需 PLI/VPI 的复杂接口层,性能更高
  1. 数据类型映射
  • 支持基本数据类型(int, real 等)和复杂类型(结构体、数组)的自动转换。
  • SystemVerilog 与 C/C++ 之间的数据通过 值传递指针引用 交换。

C 文件示例:

#include <stdio.h>

#if defined(_WIN32)
#include <windows.h>
#else
#include <sys/time.h>
#endif

long long get_system_time() {
    #if defined(_WIN32)
        FILETIME ft;
        GetSystemTimeAsFileTime(&ft);
        ULARGE_INTEGER uli;
        uli.LowPart = ft.dwLowDateTime;
        uli.HighPart = ft.dwHighDateTime;
        // 转换为 1970-01-01 基准的微秒数
        const long long EPOCH_OFFSET = 116444736000000000LL; // 1601至1970的100ns间隔
        return (uli.QuadPart - EPOCH_OFFSET) / 10// 100ns -> μs
    #else
        struct timeval tv;
        gettimeofday(&tv, NULL);
        return (long long)tv.tv_sec * 1000000 + tv.tv_usec;
    #endif
}

利用 DPI-C 可实现各种复杂功能,这里仅使用了 C 语言中获取系统时间的函数。

在 Vivado 中进行仿真时,需要将 C 文件,如下图的 time.c 添加到仿真文件中,这样仿真器就能够找到相关 C 语言函数了。

image-20250426205237862

注意,添加仿真文件时,文件类型注意选择 all files,否则可能不显示后缀名为.c 的文件。

此方案,不需要先关闭仿真,再运行仿真,直接点击 Relaunch Simulation按钮(如下图所示),即可更新种子,实现真随机数。

image-20250426205926698

三、总结

推荐使用 SV 支持的 DPI-C 特性,通过 C 语言函数扩展 SV 的功能,获取到系统时间戳,再通过$srandom(seed)函数设置每个 initial 块的种子,即可实现每次仿真产生真随机数。

四、分享

相关源码分享,两平台同步。

Verilog 仿真模块–生成真随机数

zhengzhideakang/verilog-simulation-module–RandomNum

后续如有更改将不再更新文章,会直接推送到上面两个平台仓库中。


如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。

如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。


徐晓康的博客 持续分享高质量硬件、FPGA 与嵌入式知识,软件,工具等内容,欢迎大家关注。

0 0 投票数
文章评分
订阅评论
提醒
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x