
前言
在进行功能仿真时,总是希望仿真条件能覆盖尽量多的情况,因此,经常需要产生随机数作为仿真的输入。Verilog 和 SV 中有能够产生随机数的系统函数$random,可惜的是此函数产生的随机数是伪随机数,重新再跑一次仿真,它还是产生那些数,这使得两次仿真没有什么区别,覆盖的测试条件是一样的。这种情况不便于测试出模块在某些特殊输入时可能出现的问题,我们希望的是每次仿真都不一样。因此,就有了在仿真文件中产生真随机数的需求。
一、随机数函数 urandom、random 和 srandom
本章参考:
-
SystemVerilog IEEE 1800-2017 — 18.13 Random number system functions and methods -
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(20, 100) // 产生20 ~ 100的整数
$urandom_range(100, 20) // 与上式等价
另一个例子:
$urandom_range(2652) // 产生0 ~ 2562的整数
$urandom_range(0, 2562) // 与上式等价
$urandom_range(2562, 0) // 与上式等价
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(10) begin
num1 = $urandom(100);
$display("num1 = ", num1);
num2 = $urandom_range(0, 1000);
$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
带了种子,产生的值就是固定的。
注意:
-
不要同时使用 $random
和$urandom
,二者的种子管理逻辑冲突,可能导致随机序列异常。 -
$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
改变种子。
注意事项:
-
不要同时使用 $srandom()
和带参数的$urandom(seed)
,两者均会修改随机数生成器状态,可能导致序列混乱。 -
SV 中有线程的概念,一个 initial 块为一个线程,每个线程都是独立的,$srandom(seed)只能改变所在 initial 块的种子,无法全局改变种子。
二、产生真随机数的方案
要产生真随机数,其实就是要改变每次仿真时,初始用到的种子,通常的做法是使用系统时间作为种子,有的方案里使用的是 $time
或者 $realtime
作为种子,实际上是不行的,这两个函数返回的是仿真时间,仿真初始时刻 $time
为 0
,但随仿真推进递增。若在初始阶段(如 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 设置中进行如下配置。若后续不再使用此参数传递方式,需手动删除相关配置。
要正常实现传参需要 在仿真顶层文件
中定义 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(10) begin
num1 = $urandom_range(0, 2**31-1);
$display("num1 = ", num1);
#1;
end
$finish;
end
//-- 生成随机数 ------------------------------------------------------------
操作方法:直接运行 tcl 文件即可。
仿真开始之后,点击 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(10) begin
num1 = $urandom_range(0, 2**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 的核心特性:
-
双向交互
-
Import 函数:从 C/C++ 导入函数到 SystemVerilog 中调用。 -
Export 函数:将 SystemVerilog 函数导出到 C/C++ 中使用。
-
无中间层
-
直接通过函数调用实现交互,无需 PLI/VPI 的复杂接口层,性能更高。
-
数据类型映射
-
支持基本数据类型( 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 语言函数了。
注意,添加仿真文件时,文件类型注意选择 all files,否则可能不显示后缀名为.c 的文件。
此方案,不需要先关闭仿真,再运行仿真,直接点击 Relaunch Simulation按钮
(如下图所示),即可更新种子,实现真随机数。
三、总结
推荐使用 SV 支持的 DPI-C 特性,通过 C 语言函数扩展 SV 的功能,获取到系统时间戳,再通过$srandom(seed)函数设置每个 initial 块的种子,即可实现每次仿真产生真随机数。
四、分享
相关源码分享,两平台同步。
zhengzhideakang/verilog-simulation-module–RandomNum
后续如有更改将不再更新文章,会直接推送到上面两个平台仓库中。
如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。
如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。

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