第一步:分析与整理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)

详解

  1. 打开文件:使用 $fopen 检查返回值,若为0则报错退出。
  2. 逐行读取$fgets 读取一行到 line 变量(足够宽)。循环直到 $feof 为真。
  3. 跳过注释和空行:检查行开头字符。
  4. 解析键值对$sscanf(line, "NUM_ITEMS=%d", num_items) 从字符串中提取整数。注意格式字符串中的 %d 匹配十进制数。
  5. 遇到 “DATA:” 行:我们不从该行解析剩余数据,而是使用 $fscanf 直接从文件读取指定个数的十六进制数(因为文件指针已经在 “DATA:” 行的下一行开头)。这里演示了混合使用 $fgets$fscanf
  6. 计算和并写入结果:用 $fdisplay 写入文件。
  7. 关闭文件:养成良好的习惯。

工作中应用场景

  • 加载测试向量:用 $readmemh$fscanf 从文本文件读取激励。
  • 保存参考输出:用 $fdisplay 将 golden 结果写入文件,供后续比较。
  • 自动生成报告:在仿真结束后,将覆盖率、错误统计等信息输出到日志。
  • 动态配置:仿真时通过修改配置文件来改变参数,无需重新编译。

常见陷阱与技巧

陷阱 解决方法
$fgets 读取行时包含换行符,用 $display 打印会多出空行 按需使用 $write 或手动去掉换行符
$fscanf 读取十六进制时,文件中的 0x 前缀可省略,但要确保数字是有效的十六进制字符 使用 %h 格式,文件内容如 12 ab CD 均可
$sscanf 的第一个参数必须是字符串型寄存器,不能是整数 若源是整数,用 $sformat 转换后再 $sscanf
$feof 在最后一行之后的一个空行仍可能返回0,导致多读一次 检查 $fgets 返回值,若为0则跳出循环
文件路径问题 建议使用绝对路径或将文件放在仿真运行目录下

学习建议

  1. 从简单开始:先尝试 $fdisplay 写一个文件,再用 $fgets 读回来。
  2. 练习 $fscanf 解析格式化文本(如 CSV)。
  3. 模拟配置文件读取:用 $fgets + $sscanf 实现键值对解析。
  4. 做一个完整的激励加载器:从文件读取测试用例,驱动 DUT,并将结果写回文件。
  5. 注意文件操作的系统任务只能在 initialalways 块中使用,不能在 function 中使用(除了 $fscanf 等少数)。

总结

Verilog 文件操作就像工具箱里的螺丝刀:

  • fopen/fopen/fopen/fclose:打开/关闭文件(借书/还书)。
  • fdisplay/fdisplay/fdisplay/fwrite:写入文件(记笔记)。
  • fgets/fgets/fgets/fscanf:按行或按格式读取(看笔记)。
  • $fread:二进制方式读取(拷贝文件)。
  • fseek/fseek/fseek/ftell:移动阅读指针(翻页)。
  • $readmemh:一键加载十六进制数组(批量导入)。

验证工程师经常用它们来构建自动化测试平台:把测试向量放在文件里,让仿真器读取并驱动 DUT,然后将输出和 golden 对比,最后生成报告。掌握这些任务,你就能写出更灵活、可复用、易维护的 testbench。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐