23.【Verilog】Verilog 文件操作
Verilog提供了丰富的文件操作系统任务,主要包括文件打开/关闭、读写操作、字符串处理、定位和存储器加载等功能。文件打开使用$fopen指定读写模式,关闭用$fclose。写入任务如$fdisplay和$fwrite支持格式化输出到文件,而$swrite和$sformat用于字符串处理。读取操作包括字符级($fgetc)、行级($fgets)和格式化读取($fscanf)。文件定位通过$fsee
第一步:分析与整理Verilog 文件操作
1. 概述
Verilog 提供大量文件操作的系统任务。常用类别:
- 文件开/闭:
$fopen,$fclose,$ferror - 文件写入:
$fdisplay,$fwrite,$fstrobe,$fmonitor - 字符串写入:
$sformat,$swrite - 文件读取:
$fgetc,$fgets,$fscanf,$fread - 文件定位:
$fseek,$ftell,$feof,$frewind - 存储器加载:
$readmemh,$readmemb
使用注意事项:需根据文件性质和变量类型选择合适的任务,避免字符串与多进制类型混淆。
2. 文件打开与关闭
| 系统任务 | 调用格式 | 描述 |
|---|---|---|
| 文件打开 | fd = $fopen("fname", mode); |
返回32位文件描述符(非零表示成功,零表示失败);mode指定打开方式(见下表) |
| 文件关闭 | $fclose(fd); |
关闭已打开的文件 |
| 文件错误 | err = $ferror(fd, str); |
正常时err=0, str=0;错误时err非零,str返回错误信息(建议str位宽640位) |
mode 类型(文本/二进制,读写追加):
"r"只读文本;"w"只写文本(覆盖);"a"追加文本"rb","wb","ab"对应二进制"r+","w+","a+"可读写;"rb+","wb+","ab+"二进制可读写
示例:
integer fd1, fd2;
reg [320:0] str;
initial begin
fd1 = $fopen("./DATA_RD.HEX", "r"); // 存在文件
$ferror(fd1, str); // 检查
$display("fd1=%h, error=%h, info=%s", fd1, err, str);
$fclose(fd1);
fd2 = $fopen("noexist.hex", "r"); // 不存在 -> fd2=0
$fclose(fd2);
end
3. 文件写入
对应显示任务的文件版本,多一个文件描述符参数:
| 任务 | 特点 |
|---|---|
$fdisplay(fd, args) |
自动换行 |
$fwrite(fd, args) |
不自动换行 |
$fstrobe(fd, args) |
选通写(同一时间步最后执行) |
$fmonitor(fd, args) |
监测写(变量变化时自动写) |
同样有 $fdisplayb, $fdisplayh, $fdisplayo 等格式变种。
示例(追加模式):
fd = $fopen("DATA_RD.HEX", "a+");
$fdisplay(fd, "New data1: %h", fd);
$fdisplay(fd, "New data2: %h", str);
$fclose(fd);
4. 字符串写入
向字符串变量(reg)写数据。
| 任务 | 格式 | 说明 |
|---|---|---|
$swrite(reg, list_of_args) |
顺序写,可多个字符串直接拼接 | 不自动换行,需手动加\n |
len = $sformat(reg, format_str, args) |
按format_str格式化写入,返回字符串长度 |
第二个参数必须为字符串格式,不能省略 |
注意事项:
$swrite写含变量的字符串时,建议指定格式(如%s %d),否则结果不可预测。$swrite可以一次写多个不包含变量的字符串,而$sformat只能接受一个格式字符串。$sformat的第二个参数可以是字符串寄存器,但要求存储的是正常字符串。
示例:
reg [299:0] str_swrite, str_sformat;
reg [63:0] str_buf = "runoob!";
integer age = 9;
$swrite(str_swrite, "%s age is %d", str_buf, age); // 正确
$sformat(str_sformat, "I have learnt in %s", str_buf);
len = $sformat(str_sformat, "for 4 years!"); // 不包含变量,可省略第三个参数
5. 文件读取

5.1 按字符读/写缓冲区
c = $fgetc(fd);读一个字符(8位),错误返回 EOF(-1)code = $ungetc(c, fd);向文件缓冲区压入一个字符(FILO),文件内容不变,正常返回0,错误返回EOF
5.2 按行读
code = $fgets(str, fd);读取一行(包括换行符)到str,直到str填满或行结束或文件结束。返回读取行数(正常为1),错误为0。
5.3 按格式读文件/字符串
code = $fscanf(fd, format, args);从文件按格式读取(如%h,%d,%s),一次读取直至空格或换行。code = $sscanf(str, format, args);从字符串变量按格式读取。
注意:$fscanf 读取十六进制数时,文件内容应为十六进制文本,不是二进制。$sscanf 的源变量必须是字符串类型,否则需要先用 $sformat 转换。
5.4 按二进制读文件
code = $fread(store, fd, start, count);以二进制数据流格式读取,将数据存入数组或寄存器store。start:起始位置(字节偏移)count:读取字节数(若未指定则填充整个store)- 若
store是寄存器变量,start/count无效,会填充寄存器直到满或文件结束。 - 返回实际读取的字节数。
6. 文件定位
| 任务 | 说明 |
|---|---|
pos = $ftell(fd); |
返回当前文件位置(字节偏移,从0开始) |
code = $fseek(fd, offset, type); |
移动文件指针:type=0绝对偏移,type=1相对当前位置,type=2相对文件尾(负值倒退) |
code = $rewind(fd); |
等价于 $fseek(fd, 0, 0); |
code = $feof(fd); |
检测是否文件尾(到达文件尾返回1,否则0) |
注意:$feof 对于最后一行换行符后的空行仍可能返回0,因为文件尚未真正结束,需要谨慎处理。
7. 加载存储器
$readmemh("fname", mem, start_addr, finish_addr);从文本文件读取十六进制数据填充存储器数组。$readmemb类似,读取二进制数据。- 文件内容可以包含注释(
//),数据之间用空白符分隔。
第二步:费曼教学法 – 通俗讲解 Verilog 文件操作
文件操作就是在仿真中读写外部文件,用于加载测试数据、保存结果、记录日志。这就像你去图书馆借书(打开文件)、看书(读数据)、还书(关闭文件)。Verilog 提供了丰富的“图书馆管理员”任务,我来逐一说明。
一、打开与关闭:借书证和还书
$fopen("文件名", "模式"):获得一个文件描述符(类似于借书证号码)。模式"r"只读,"w"写(会清空原内容),"a"追加。出错时返回0。$fclose(fd):还书,关闭文件。$ferror(fd, str):检查借书证是否有效,错误信息存在str中。
二、写入文件:写日记
$fdisplay(fd, ...):写一行,自动加换行。$fwrite(fd, ...):写数据,不自动换行。$fstrobe和$fmonitor同它们的显示版本,只是输出到文件。
用途:保存仿真结果、错误日志、波形数据。
三、字符串写入:把文字拼到变量里
$swrite(reg, ...):把多个字符串拼接后存入寄存器(类似于$sprintf)。$sformat(reg, format, ...):按格式(如"%s %d")格式化后存入寄存器,返回长度。
注意:格式化时不能省略格式字符串,否则结果不可预料。$swrite 可以一次写多个常量字符串,$sformat 不行。
四、读取文件:从文件取数据
4.1 字符级:一个一个字母读
$fgetc(fd):读一个字符(ASCII码)。$ungetc(c, fd):把字符“塞回”文件缓冲区(后进先出),不影响文件本身。
4.2 行级:读一整行
$fgets(str, fd):读一行(含换行符)到字符串变量str。行太长会截断。
4.3 格式化读取:按格式解析
$fscanf(fd, "%h", data):类似C语言的fscanf,从文件中按十六进制、十进制、字符串等读取,以空格或换行分隔。$sscanf(str, "%h", data):从字符串变量中按格式读取(常用于解析已读取的行)。
关键注意:$fscanf 读取十六进制时,文件里必须是文本形式的十六进制数(如 "c0dec0de"),不是二进制。$sscanf 的源必须是字符串,如果是数值变量,需先用 $sformat 转换。
4.4 二进制流读取:直接拷贝字节
$fread(store, fd, start, count):把文件中的原始字节复制到store(寄存器或数组)。适用于读取非文本数据,如二进制配置文件。
五、文件定位:移动阅读指针
$ftell(fd):返回当前文件位置(字节数)。$fseek(fd, offset, type):移动指针。type=0绝对移动,type=1相对当前,type=2相对末尾。$rewind(fd):回到文件开头。$feof(fd):判断是否文件末尾。
典型用法:解析复杂格式文件时,来回移动。
六、加载存储器:一键导入数据
$readmemh("file", mem_array):从文件读取十六进制数,按地址存入存储器数组。常用于初始化 ROM/RAM。$readmemb用于二进制。
第三步:详解示例 – 综合应用:解析配置文件并输出结果
下面设计一个完整的例子:读取一个包含配置参数和数据的文本文件,解析后计算和,并将结果写入日志。这个例子展示了 $fopen、$fgets、$sscanf、$fdisplay 以及 $feof 的配合使用。
示例说明
配置文件 config.txt 内容如下:
// This is a config file
NUM_ITEMS=4
DATA: 0x12 0x34 0x56 0x78
END
我们需要:读取 NUM_ITEMS 的值,然后读取指定数量的数据,计算它们的和,最后将结果写入 result.log。
RTL + Testbench 代码
`timescale 1ns/1ps
module file_operation_demo;
integer fd_in, fd_out;
integer scan_ret;
reg [255:0] line;
integer num_items, i;
reg [31:0] data, sum;
reg [31:0] data_array [0:15]; // 最多16个数据
initial begin
// 打开输入文件(只读)
fd_in = $fopen("./config.txt", "r");
if (fd_in == 0) begin
$display("ERROR: cannot open config.txt");
$finish;
end
// 打开输出文件(写入,覆盖)
fd_out = $fopen("./result.log", "w");
if (fd_out == 0) begin
$display("ERROR: cannot create result.log");
$finish;
end
// 逐行读取配置文件
num_items = 0;
while (!$feof(fd_in)) begin
scan_ret = $fgets(line, fd_in); // 读取一行
if (scan_ret == 0) begin
$display("Warning: read line error or EOF");
break;
end
// 忽略空行和注释行(以 // 开头)
if (line[0:7] == "//" || line[0] == "\n") continue;
// 解析 NUM_ITEMS=...
if ($sscanf(line, "NUM_ITEMS=%d", num_items) == 1) begin
$display("Parsed NUM_ITEMS = %0d", num_items);
continue;
end
// 解析 DATA: 行,例如 "DATA: 0x12 0x34 0x56 0x78"
if ($sscanf(line, "DATA: %s", line) == 1) begin
// 注意:line 现在变成了 DATA: 后面的部分(包括换行)
// 更健壮的方法:用 $fscanf 直接读,但这里演示 $sscanf 解析字符串
// 简化处理:手动逐个读取十六进制数
// 实际可以借助 $fscanf 从文件直接读取,但这里我们继续用 $sscanf 从行中解析
// 更好的方式:重新用 $fscanf 从文件读,或者使用循环 $sscanf
// 下面演示用 $fscanf 直接从文件读取指定数量的十六进制数
for (i = 0; i < num_items; i = i + 1) begin
scan_ret = $fscanf(fd_in, "%h", data_array[i]);
if (scan_ret != 1) begin
$display("ERROR: insufficient data items");
$finish;
end
end
// 计算和
sum = 0;
for (i = 0; i < num_items; i = i + 1)
sum = sum + data_array[i];
$display("Sum = %0d (0x%0h)", sum, sum);
// 写入结果文件
$fdisplay(fd_out, "Sum of %0d items = %0d (0x%0h)", num_items, sum, sum);
end
end
// 关闭文件
$fclose(fd_in);
$fclose(fd_out);
$finish;
end
endmodule
仿真输出
控制台显示:
Parsed NUM_ITEMS = 4
Sum = 288 (0x120)
result.log 文件内容:
Sum of 4 items = 288 (0x120)
详解
- 打开文件:使用
$fopen检查返回值,若为0则报错退出。 - 逐行读取:
$fgets读取一行到line变量(足够宽)。循环直到$feof为真。 - 跳过注释和空行:检查行开头字符。
- 解析键值对:
$sscanf(line, "NUM_ITEMS=%d", num_items)从字符串中提取整数。注意格式字符串中的%d匹配十进制数。 - 遇到 “DATA:” 行:我们不从该行解析剩余数据,而是使用
$fscanf直接从文件读取指定个数的十六进制数(因为文件指针已经在 “DATA:” 行的下一行开头)。这里演示了混合使用$fgets和$fscanf。 - 计算和并写入结果:用
$fdisplay写入文件。 - 关闭文件:养成良好的习惯。
工作中应用场景
- 加载测试向量:用
$readmemh或$fscanf从文本文件读取激励。 - 保存参考输出:用
$fdisplay将 golden 结果写入文件,供后续比较。 - 自动生成报告:在仿真结束后,将覆盖率、错误统计等信息输出到日志。
- 动态配置:仿真时通过修改配置文件来改变参数,无需重新编译。
常见陷阱与技巧
| 陷阱 | 解决方法 |
|---|---|
$fgets 读取行时包含换行符,用 $display 打印会多出空行 |
按需使用 $write 或手动去掉换行符 |
$fscanf 读取十六进制时,文件中的 0x 前缀可省略,但要确保数字是有效的十六进制字符 |
使用 %h 格式,文件内容如 12 ab CD 均可 |
$sscanf 的第一个参数必须是字符串型寄存器,不能是整数 |
若源是整数,用 $sformat 转换后再 $sscanf |
$feof 在最后一行之后的一个空行仍可能返回0,导致多读一次 |
检查 $fgets 返回值,若为0则跳出循环 |
| 文件路径问题 | 建议使用绝对路径或将文件放在仿真运行目录下 |
学习建议
- 从简单开始:先尝试
$fdisplay写一个文件,再用$fgets读回来。 - 练习
$fscanf解析格式化文本(如 CSV)。 - 模拟配置文件读取:用
$fgets+$sscanf实现键值对解析。 - 做一个完整的激励加载器:从文件读取测试用例,驱动 DUT,并将结果写回文件。
- 注意文件操作的系统任务只能在
initial或always块中使用,不能在function中使用(除了$fscanf等少数)。
总结
Verilog 文件操作就像工具箱里的螺丝刀:
- fopen/fopen/fopen/fclose:打开/关闭文件(借书/还书)。
- fdisplay/fdisplay/fdisplay/fwrite:写入文件(记笔记)。
- fgets/fgets/fgets/fscanf:按行或按格式读取(看笔记)。
- $fread:二进制方式读取(拷贝文件)。
- fseek/fseek/fseek/ftell:移动阅读指针(翻页)。
- $readmemh:一键加载十六进制数组(批量导入)。
验证工程师经常用它们来构建自动化测试平台:把测试向量放在文件里,让仿真器读取并驱动 DUT,然后将输出和 golden 对比,最后生成报告。掌握这些任务,你就能写出更灵活、可复用、易维护的 testbench。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)