日志分析技巧:日志级别设置、关键信息过滤、断言与打印的配合使用

做验证这么多年,我越来越觉得日志分析是门手艺活。很多人觉得仿真跑完看日志嘛,不就是搜几个关键词?其实没那么简单。日志里藏着大量信息,关键是你得知道怎么看、看什么。

今天我就把日志分析这块的经验掰开揉碎讲一讲。从日志级别设置,到关键信息过滤,再到断言和打印的配合使用,咱们一个一个来过。

日志级别设置:别让信息淹了你

刚开始做验证那会儿,我习惯把所有打印都打开。结果呢?仿真跑完一看日志,几十万行。找一条关键信息跟大海捞针似的。后来我学乖了——日志级别一定要分层设置。

常用的日志级别大概分这么几档:

级别 含义 使用场景
ERROR 致命错误 仿真必须停止的情况
WARNING 警告 可能有问题但不致命
INFO 普通信息 正常流程打印
DEBUG 调试信息 定位问题时才打开
TRACE 详细跟踪 最细粒度的打印

我个人习惯的做法是:默认只开ERROR和WARNING。INFO级别的打印只在关键节点用,比如「配置完成」「序列启动」这种。DEBUG和TRACE平时关掉,等出问题了再打开。

核心原则:日志级别不是越高越好,而是越精准越好。你想想看,如果所有信息都打印,那等于什么都没打印。

在SystemVerilog里,我一般这么设置:

// 定义日志级别
typedef enum {TRACE, DEBUG, INFO, WARNING, ERROR} log_level_t;

// 全局日志级别控制
log_level_t g_log_level = INFO;

// 打印函数
function void print_msg(log_level_t level, string msg);
  if (level >= g_log_level) begin
    case (level)
      ERROR:   $display("[ERROR] %0t: %s", $time, msg);
      WARNING: $display("[WARN]  %0t: %s", $time, msg);
      INFO:    $display("[INFO]  %0t: %s", $time, msg);
      DEBUG:   $display("[DEBUG] %0t: %s", $time, msg);
      TRACE:   $display("[TRACE] %0t: %s", $time, msg);
    endcase
  end
endfunction

这样跑回归测试的时候,日志文件小很多。出错了也能一眼看到ERROR和WARNING。

关键信息过滤:学会抓重点

日志级别设好了,但有时候还是会有大量INFO信息。这时候就需要过滤了。

我常用的过滤手段有几种:

  • 按模块过滤:只关注某个子模块的日志
  • 按时间戳过滤:只看某个时间段的日志
  • 按关键词过滤:搜「error」「fail」「timeout」这些
  • 按事务类型过滤:比如只看AXI的写操作

举个例子,我在项目中遇到过一个问题:DMA传输偶尔会卡住。日志文件有500MB,根本没法看。后来我写了个脚本,只提取包含「DMA_STATE」和「ERROR」的行,一下子就定位到了问题——状态机在某个条件下死循环了。

小技巧:用grep配合正则表达式,可以快速过滤出你关心的内容。比如 grep -E "ERROR|FATAL|TIMEOUT" sim.log,几秒钟就能找到关键信息。

还有一种做法是给每个模块分配一个ID,打印的时候带上模块名。这样过滤起来更方便:

// 模块ID定义
parameter MOD_CPU   = 8'h01;
parameter MOD_DMA   = 8'h02;
parameter MOD_MEM   = 8'h03;

// 打印时带上模块ID
$display("[%0h][INFO] CPU启动完成", MOD_CPU);
$display("[%0h][ERROR] DMA超时", MOD_DMA);

嗯,这里要注意:模块ID不要太多,否则反而增加管理成本。我一般控制在10个以内。

断言与打印的配合使用

断言是验证的利器,但很多人用不好。要么断言太多,仿真跑得慢;要么断言太少,该抓的bug没抓到。

我的经验是:断言和打印要配合使用。断言负责检查,打印负责记录上下文。

举个例子:

// 不好的写法:只有断言,没有上下文
assert (data_valid == 1'b1) else $error("data_valid不为高");

// 好的写法:断言+上下文打印
if (data_valid !== 1'b1) begin
  $error("[ASSERT] data_valid不为高");
  $display("[CONTEXT] 当前状态: %0h, 地址: %0h, 数据: %0h", 
           state, addr, data);
  $display("[CONTEXT] 上一个有效事务: addr=%0h, data=%0h", 
           last_addr, last_data);
end

为什么要这么做?因为断言失败的时候,你不仅要知道「哪里错了」,还要知道「为什么错了」。上下文信息就是帮你找「为什么」的。

注意:不要在断言里做太多打印。断言本身是性能敏感的,打印太多会影响仿真速度。我的做法是:断言失败时打印关键上下文,成功时什么都不打。

还有一种配合方式:用断言做检查,用打印做追踪。比如:

// 断言检查协议
property p_axi_valid_ready;
  @(posedge clk) 
  valid |-> ##[1:5] ready;
endproperty

// 打印追踪事务
always @(posedge clk) begin
  if (valid && ready) begin
    $display("[AXI_TRANS] addr=%0h, data=%0h, burst=%0d", 
             addr, data, burst_len);
  end
end

这样,断言负责告诉你「协议有没有违反」,打印负责告诉你「具体发生了什么」。两者配合,定位问题快很多。

我曾经遇到一个bug:AXI总线的burst传输偶尔会丢数据。断言报了ERROR,但光看断言根本不知道问题在哪。后来我打开了事务追踪打印,发现是某个地址对齐条件没满足,导致数据写到了错误的位置。如果没有打印,光靠断言,这个bug可能要查好几天。

日志分析的整体思路

说了这么多,其实日志分析的核心就三点:

  1. 分层管理:日志级别要分层,不要一股脑全打印
  2. 精准过滤:学会用工具和脚本快速过滤关键信息
  3. 断言+打印:断言负责检查,打印负责上下文,两者缺一不可

下面这张图是我自己总结的日志分析流程,你可以参考一下:

日志分析流程 日志级别设置 ERROR/WARNING/INFO/DEBUG 仿真运行 生成原始日志 关键信息过滤 grep/脚本/模块ID 是否有ERROR? 定位问题 断言+上下文打印 是 回归通过 检查覆盖率 否 调整日志级别,重新仿真

说白了,日志分析就是个「由粗到细」的过程。先看ERROR,再看WARNING,最后才看DEBUG。别一上来就扎进细节里,那样反而容易迷失方向。

我的习惯:每次跑完仿真,先花30秒扫一眼日志末尾。如果有ERROR,再回头去看。如果没有,那就说明这轮仿真基本没问题。别小看这30秒,它能帮你省下大量时间。

好了,日志分析这块就讲这么多。记住三点:级别分层、精准过滤、断言配合打印。做到这三点,你的日志分析效率至少提升一倍。

可可精品资料库