前言
Makefile有一个经典教程,相信大部分人认识和学习Makefile都是通过这篇文章入手的:
不过之前和朋友聊,觉得这篇文章很完美但是篇幅有点长,同时教程以c语言编译环境为基础讲解,实际上比我们常用的编译环境还是要复杂挺多的;
感觉我们用不了这么多但是不学有怪可惜的,那么不如做一个极简版硅农专用的Makefile教程,本文内除vcs相关的内容外,基本所有信息均可以在上文中查阅(因此有引用部分就不在单独标注),属于n手资料吧~~~
指令和变量
当前目录如果存在Makefile或makefile文件时,那么代表我们能在这一文件夹内通过make xxx来执行命令,比如我们常用的仿真指令:
make run tc=sanity seed=0 wave=on ccov=on
或者lint检查指令:
make full_lint
Makefile中命令的组织极为简单,标准的形式为:
target ... :prerequisites ...
command ...
target为目标文件,prerequisites为依赖文件,command为执行命令,执行过程即在终端中的当前目录内键入 make target,则系统检查prerequisites内的文件是否有比target更新的文件,有的话则执行对应的command;当然了这个过程类似于一个链式反应,如何知道prerequisites内的文件是不是比target新呢,makefile会去找prerequisites的依赖文件或依赖项看一下prerequisites本身时候需要先更新,这样一级一级的找直到找到最底下的文件有修改,那么就处理整天线路;
而我们做编译仿真环境时最长使用的是伪目标的组织形式。在伪目标的组织形式下,prerequisites中的项相当于当前目标的前提条件,即要执行target那么要先执行prerequisites中的伪目标,之后再无脑执行command中设定好的命令,这样一来呢对于各种显式推导规则隐式推导规则啥的就没必要学的那么清了;
命令如同血液,变量如同骨骼,Makefile中变量定义与使用与sh语法基本一致,同时sh环境中的变量也可以在Makefile中直接使用:
创建与赋值
export seed ?= random
使用
SEED := $(seed)
Makefile中的变量近似于于systemverilog中的宏,在解释时候会在调用处进行原地展开为字符串,区别在于systemverilog中的宏是编译到了就直接展开,Makefile变量展开的时间点是在这个变量在宏观解释完成后进行的,而不是执行到这一句就展开了,比如下面这句:
x = aaa
y = $(x) bbb
x = ccc
test:
@echo $(y)
x = ddd
执行结果为:
[xiaotu@xiaotu-eda ~/my_work/uvm_demo/sim]$make test
ddd bbb
变量大小写敏感,可以包含数字字符下划线,赋值时不用加$等号前后可以有空格(这个我喜欢),调用时需要加上$并且最好以()或{}作为分隔;
创建变量时加export是为了传递变量的值到下级Makefile中,不过目前我们常用的Makefile形式中一般通过include的方式来(至少目前在前端设计和验证人员使用和调整的维度来看)来展开多层次的Makefile,而不是层层传递的方式,因此export在我看来意义不是太大,当然加上肯定是没有问题的,关于加export的具体行为很多地方都有说明,因为感觉用不到所以不赘述了~
关于赋值方式,Makefile中有四种等号:= / ?= / := / +=,他们之间的差别也是很明显的:
- =
b = $(a)的方式赋值,那么$(a)这个变量在全局任何位置被修改含义了,$(b)的值也会使用跟着修改,比如刚刚上面的例子,又或者下面这样写,其实对$(y)效果还是一样的:
x = aaa
y = $(x) bbb
x := ccc
test:
@echo $(y)
x := ddd
这样导致的问题就是,你在前面写了一个赋值后,这个变量随时都有被后面“篡改”的可能性,除非你想清楚了就是要这样做哈,否则这种不确定性就搞得人很心慌,因此呢基本上我们避免这种赋值方式;
- :=
这个赋值方式就非常主流非常友好了,表示把编译到这句代码为止时该变量的值展开在此处,比如下面这种写法:
x = aaa
y := $(x) bbb
x = ccc
test:
@echo $(y)
x = ddd
执行结果:
[xiaotu@xiaotu-eda ~/my_work/uvm_demo/sim]$make test
aaa bbb
- +=
在当前值的基础上继续加上后面的值,一般用来叠buff,比如vcs的指令那么多,总不能一口气写完吧,所以就分多次分多情况来写:
CMP_OPTIONS += -top $(TOP_MOD)
CMP_OPTIONS += -timescale=1ns/1ps -unit_timescale=1ns/1ps
CMP_OPTIONS += +vcs+initreg+random
#CMP_OPTIONS += -xprop=tmerge
ifeq ($(ccov), on)
CMP_OPTIONS += -cm line+fsm+cond+tgl+assert+branch
CMP_OPTIONS += -cm_cond allops+for+tf -cm_libs yv -cm_cond obs -cm_tgl portsonly -cm_glitch 0
CMP_OPTIONS += -cm_dir $(SIM_PATH)/cov/simv.vdb
endif
当然了,+=本身具有=的性质,这以为着如果在其中使用了变量,那么要提防变量的值在后面被修改:
x = aaa
y = bbb
y += $(x)
x = ccc
test:
@echo $(y)
x = ddd
执行结果:
[xiaotu@xiaotu-eda ~/my_work/uvm_demo/sim]$make test
bbb ddd
使用的时候心里时刻想着这个事就行了;
- ?=
?=的含义是如果这个值没有在其他地方被赋值(注意不只是在之前),那么就使用?=的赋值结果,通常把要外部传参的那些信号习惯用?=来赋值:
export seed ?= random
export tc ?= sanity_case
export wave ?= off
export ccov ?= off
export mode ?= sim_base
当然了,?=还是具有=的特性,如果在其他地方有赋值行为,那么?=即使在前面也无法生效:
x ?= aaa
test:
@echo $(x)
x = ddd
执行结果:
[xiaotu@xiaotu-eda ~/my_work/uvm_demo/sim]$make test
ddd
因此呢,使用+=和?=的时候,一定要留心其中使用变量的行为;
实操
了解了伪指令和变量后,我们就可以开始通过实操来进一步学习Makefile了,那么不如就做一个make run的仿真执行指令吧;先来确定下Makefile是放在哪个位置呢,显然必须放在sim仿真目录下,但是有时我们需要多个sim目录,而Makefile文件都是一样的,那么我们在Makefile中使用include将文件直接建立在cfg目录就好了:
include ../cfg/cfg.mk
好,那么进入正题,分析下make run指令的行为实际就是编译 + 用例仿真,因此实际上这个指令我们可以拆解为两个更小粒度的指令make cmp + make ncrun:
.PHONY: cmp ncrun run verdi clean clean_all
run: cmp ncrun
OK,这种写法使用了prerequisites区域来设置了两个依赖项,由于cmp和ncrun是另外两个伪指令,因此这样写的含义就是:make run等价于先执行make cmp,再执行make ncrun,再后面就结束了,因为command项中;
.PHONY中罗列了所有的伪指令,一般来说伪指令都会加入到.PHONY中,不过不放在里面并不会影响伪指令的执行,但是否有其他场景或者其他情况有影响我不确认,因此保险起见所有的伪指令请都加入进去吧;
好的,那么run指令拆解为cmp+nurun了,下一步当然是分别组织这两个伪指令;
cmp
先来想一下,执行编译工作一共分几步?
- clean一下当前的目录,把之前那些没用的中间文件啊删一删;
- 建立一下编译目录,主要是建立log和exec目录,当然了如果wave=on的时候呢把wave目录也要建立一下,如果ccov=on的时候呢要把cov目录建立一下;
- 启动vcs通过命令行来进行文件编译;
那么,我们的cmp指令的大概形式就出来了,注意command如果不和target一行的话,前面一定要用tab键缩进:
cmp: clean
@$(PRE_PROC)
@vcs $(CMP_OPTIONS)
在这种组织方式下呢,clean作为一个指令是cmp的前提指令,他的形式其实是很简单的:
export SIM_PATH := ./$(mode) #这里不用要,就是给仿真子目录根据mode起个名字
clean:
@-rm -rf $(SIM_PATH)/exec ucli.key csrc vc_hdrs.h novas.conf novas_dump.log novas.rc verdiLog
可以看到clean的作用就是删除各种中间文件以及exec文件夹,这里的rm加了一个-rm,作用是如果遇到了没有执行成功的时候,不要停继续执行下去,在这里还看不清楚,下面可能会看的更明白;rm前面还加了一个@符号,原因是makefile会将其执行的命令行在执行前输出到屏幕上,@做前缀可以使这个命令不在屏幕上打印出来;
完成删除操作后呢,下面就是建立各种文件夹的$(PRE_PROC)操作了,这个操作的初始组织形式如下:
export PRE_PROC:= mkdir $(SIM_PATH)/log $(SIM_PATH)/exec
ifeq ($(wave), on)
PRE_PROC += $(SIM_PATH)/wave
endif
ifeq ($(ccov), on)
PRE_PROC += $(SIM_PATH)/cov
endif
然后执行的时候就会发现这个问题:
[xiaotu@xiaotu-eda ~/my_work/uvm_demo/sim]$make cmp
mkdir: cannot create directory `./sim_base/log': File exists
make: *** [cmp] Error 1
什么意思呢?我们在clean的时候不是没有把log目录删除嘛(本来就不应该删除,不能跑一次仿真就把之前case的log都干掉吧),然后这里执行的时候mkdir文件夹就报了一个无伤大雅的error:log目录已存在。那么要如何解决这个问题呢?
一种方法是使用mkdir -p xxx,这样mkdir本身就会忽略已经存在的目录;
另一种方法就是使用-mkdir xxx,在makefile中主动忽略这个linux执行过程中的报错;
用哪个都行所以最后我就用的-p的方式来做的;
然后下一个知识点,ifeq($(ccov), on)这句看着就是条件判断,makefile里还是提供了几种条件判断语句如ifeq-else-endif,ifneq,ifdef这几个了,当然了还有其他一些函数啥的,可以在开头的教程中看看,这里不做。于是乎呢$(PRE_PROC)呢就被组织完成,只要在cmp中直接调用既可以;
最后一步,组织vcs $(CMP_OPTIONS),也就是vcs真正的编译命令,这步的难点不在于makefile中,在于你对vcs命令行的熟悉程度,因此看一下就可以了,哦对了顺便说一句makefile中使用#来做注释,使用\来换行,同时可以直接使用环境变量:
SIM_LOG ?= $(SIM_PATH)/log/sim.log
RUN_LOG ?= $(SIM_PATH)/log/$(tc)_$(SEED).log
RUN_WAVE ?= $(SIM_PATH)/wave/$(tc)_$(SEED).fsdb
EXEC_SIMV ?= $(SIM_PATH)/exec/simv
RUN_COV ?= $(tc)_$(SEED)
FILELIST ?= ../cfg/tb.f
TOP_MOD ?= harness
VERDI_P := $(NOVAS_HOME)/share/PLI/VCS/LINUX64/verdi.tab \
$(NOVAS_HOME)/share/PLI/VCS/LINUX64/pli.a
##############################################################
##
##vcs cmp command
##
##############################################################
export CMP_OPTIONS :=
CMP_OPTIONS += -f $(FILELIST) -P $(VERDI_P) -l $(SIM_LOG) -o $(EXEC_SIMV)
CMP_OPTIONS += +libext+.sv+.v +indir+/home/xiaotu/my_work/code_lib
CMP_OPTIONS += +v2k +define+RTL_SAIF +notimingcheck +nospecify +vpi +memcbk +vcsd +plusarg_save +nospecify +udpsched
CMP_OPTIONS += +vcs+lic+wait
CMP_OPTIONS += -sverilog -full64 -sverilog -debug_all -ntb_opts uvm-1.2
CMP_OPTIONS += -sv_pragma -lca -kdb
CMP_OPTIONS += -top $(TOP_MOD)
CMP_OPTIONS += -timescale=1ns/1ps -unit_timescale=1ns/1ps
CMP_OPTIONS += +vcs+initreg+random
#CMP_OPTIONS += -xprop=tmerge
ifeq ($(ccov), on)
CMP_OPTIONS += -cm line+fsm+cond+tgl+assert+branch
CMP_OPTIONS += -cm_cond allops+for+tf -cm_libs yv -cm_cond obs -cm_tgl portsonly -cm_glitch 0
CMP_OPTIONS += -cm_dir $(SIM_PATH)/cov/simv.vdb
endif
其实可以看得出来,组织makefile的过程就是把工具命令行搬到makefile文件中来,伴随着if else把工具命令行往上堆,你如果不愿意做makefile其实做个脚本一样能实现同样的功能的,只不过make提供了更标准更便捷的途径;
ncrun
ncrun顾名思义就是no-cmp run,不用编译直接仿真了,然后在不知道这个nc是啥意思的时候我一直称之为“脑残run”,反正就啥也不要管楞仿吧~那么在ncrun里我们做了几件事呢:
ncrun:
@$(EXEC_SIMV) $(RUN_OPTIONS)
@$(WORK_ENV)/check_fail.pl $(RUN_LOG)
@echo "[Note] report log path: $(RUN_LOG)"
- 用刚刚编译好的exec执行文件来执行用例仿真;
- 检查仿真的log,有没有设定好的关键字,如果有则反馈error/warning等信息在屏幕上;
- 最后打印log的路径;
对于第一步$(EXEC_SIMV)刚刚已经定义过了:
EXEC_SIMV ?= $(SIM_PATH)/exec/simv
只需要看下$(RUN_OPTIONS),那么先看下我们有意通过命令行来配置的几个量:
##############################################################
##
##user cfg
##
##############################################################
export seed ?= random
export tc ?= sanity_case
export wave ?= off
export ccov ?= off
export mode ?= sim_base
这几个配置是在键入make ncrun时可以通过外部修改的,因此在这里我使用了?=来赋默认值,其实呢这里不用?=用:=也是可以的,因为命令行输入是有最高优先级的:
x ?= aaa
test:
@echo $(x)
x := ddd
测试结果:
[xiaotu@xiaotu-eda ~/my_work/uvm_demo/sim]$make test x=aaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaa
好的,在有了上面的配置之后呢,我们就可以组织$(RUN_OPTIONS)了,核心工作还是vcs的命令行添加:
##############################################################
##
##vcs run command
##
##############################################################
export RUN_OPTIONS :=
RUN_OPTIONS += +ntb_random_seed=$(SEED) +tc_name=$(tc) -l $(RUN_LOG)
RUN_OPTIONS += -assert nopostproc
RUN_OPTIONS += +vcs+lic+wait
RUN_OPTIONS += +vcs+initreg+$(SEED)
RUN_OPTIONS += +UVM_TESTNAME=$(tc)
ifeq ($(wave), on)
RUN_OPTIONS += +fsdbfile+$(RUN_WAVE) -ucli -do ../cfg/run.do
endif
ifeq ($(ccov), on)
RUN_OPTIONS += -cm line+cond+tgl+fsm+branch+assert
RUN_OPTIONS += -cm_dir $(SIM_PATH)/cov/simv.vdb -cm_name $(RUN_COV)
endif
还有这里面用了一个run.do文件,这个文件是用来产生波形的,附在文章的最后;执行完这一步实际就已经把仿真工作完成了,后面两步只不是就是调用脚本检查仿真中的error关键字以及打印log,没有必要赘述;
好的,那么到这里一个简单的仿真编译环境就完成了;
最有一个要提的小点,在sim目录下直接键入make,会执行Makefile中设定的第一个指令,比如我的设置中就会执行cmp指令,因此千万不要不clean_all命令放在第一个位置,否则哭都来不及了。
附:
完整的Makefile:
##############################################################
##
##user cfg
##
##############################################################
export seed ?= random
export tc ?= sanity_case
export wave ?= off
export ccov ?= off
export mode ?= sim_base
##############################################################
##
##env path
##
##############################################################
export SIM_PATH := ./$(mode)
ifeq ($(seed), random)
SEED := $(shell python -c "from random import randint; print randint(0,99999999)")
#SEED := $(shell perl -e "print int(rand(100000000))")
else
SEED := $(seed)
endif
export PRE_PROC:= mkdir -p $(SIM_PATH)/log $(SIM_PATH)/exec
#export PRE_PROC:= -mkdir $(SIM_PATH)/log $(SIM_PATH)/exec
ifeq ($(wave), on)
PRE_PROC += $(SIM_PATH)/wave
endif
ifeq ($(ccov), on)
PRE_PROC += $(SIM_PATH)/cov
endif
SIM_LOG ?= $(SIM_PATH)/log/sim.log
RUN_LOG ?= $(SIM_PATH)/log/$(tc)_$(SEED).log
RUN_WAVE ?= $(SIM_PATH)/wave/$(tc)_$(SEED).fsdb
EXEC_SIMV ?= $(SIM_PATH)/exec/simv
RUN_COV ?= $(tc)_$(SEED)
FILELIST ?= ../cfg/tb.f
TOP_MOD ?= harness
VERDI_P := $(NOVAS_HOME)/share/PLI/VCS/LINUX64/verdi.tab \
$(NOVAS_HOME)/share/PLI/VCS/LINUX64/pli.a
##############################################################
##
##vcs cmp command
##
##############################################################
export CMP_OPTIONS :=
CMP_OPTIONS += -f $(FILELIST) -P $(VERDI_P) -l $(SIM_LOG) -o $(EXEC_SIMV)
CMP_OPTIONS += +libext+.sv+.v +indir+/home/xiaotu/my_work/code_lib
CMP_OPTIONS += +v2k +define+RTL_SAIF +notimingcheck +nospecify +vpi +memcbk +vcsd +plusarg_save +nospecify +udpsched
CMP_OPTIONS += +vcs+lic+wait
CMP_OPTIONS += -sverilog -full64 -sverilog -debug_all -ntb_opts uvm-1.2
CMP_OPTIONS += -sv_pragma -lca -kdb
CMP_OPTIONS += -top $(TOP_MOD)
CMP_OPTIONS += -timescale=1ns/1ps -unit_timescale=1ns/1ps
CMP_OPTIONS += +vcs+initreg+random
#CMP_OPTIONS += -xprop=tmerge
ifeq ($(ccov), on)
CMP_OPTIONS += -cm line+fsm+cond+tgl+assert+branch
CMP_OPTIONS += -cm_cond allops+for+tf -cm_libs yv -cm_cond obs -cm_tgl portsonly -cm_glitch 0
CMP_OPTIONS += -cm_dir $(SIM_PATH)/cov/simv.vdb
endif
##############################################################
##
##vcs run command
##
##############################################################
export RUN_OPTIONS :=
RUN_OPTIONS += +ntb_random_seed=$(SEED) +tc_name=$(tc) -l $(RUN_LOG)
RUN_OPTIONS += -assert nopostproc
RUN_OPTIONS += +vcs+lic+wait
RUN_OPTIONS += +vcs+initreg+$(SEED)
RUN_OPTIONS += +UVM_TESTNAME=$(tc)
ifeq ($(wave), on)
RUN_OPTIONS += +fsdbfile+$(RUN_WAVE) -ucli -do ../cfg/run.do
endif
ifeq ($(ccov), on)
RUN_OPTIONS += -cm line+cond+tgl+fsm+branch+assert
RUN_OPTIONS += -cm_dir $(SIM_PATH)/cov/simv.vdb -cm_name $(RUN_COV)
endif
##############################################################
##
## PHONY order
##
##############################################################
.PHONY: cmp ncrun run verdi clean clean_all
cmp: clean
@$(PRE_PROC)
@vcs $(CMP_OPTIONS)
ncrun:
@$(EXEC_SIMV) $(RUN_OPTIONS)
@$(WORK_ENV)/check_fail.pl $(RUN_LOG)
@echo "[Note] report log path: $(RUN_LOG)"
run: cmp ncrun
verdi:
@verdi -simflow -dbdir $(SIM_PATH)/exec/simv.daidir
clean:
@-rm -rf $(SIM_PATH)/exec ucli.key csrc vc_hdrs.h novas.conf novas_dump.log novas.rc verdiLog
clean_all: clean
@rm -rf $(SIM_PATH)
run.do文件:
call \$fsdbDumpvars 0 "harness"
run
check_fail.pl文件:
#!/usr/bin/perl -w
my $pass = 1;
while(<ARGV>){
if($_ =~ /error|Error|failed|Failed|UVM_FATAL|UVM_ERROR/){
$pass = 0;
last;
}
if($_ =~ /Report counts by severity/){
last;
}
}
if($pass == 1){
print "\033[42;37m SIMULATION PASS! \033[0m \n";
print_pass();
} else {
print "\033[41;37m SIMULATION FAIL! \033[0m \n";
print_fail();
}
sub print_pass{
print "\n";
print "############# # ############## ##############"; print "\n";
print "############# ### ############## ##############"; print "\n";
print "## ## # # # ## ##"; print "\n";
print "## ## ## ## ## ##"; print "\n";
print "## ## ## ## ## ##"; print "\n";
print "## ## ## ## ## ##"; print "\n";
print "############# ############## ############## ##############"; print "\n";
print "############# ############## ############## ##############"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ############## ##############"; print "\n";
print "## ## ## ############## ##############"; print "\n";
print "\n";
}
sub print_fail{
print "\n";
print "############# # ############## ##"; print "\n";
print "############# ### ############## ##"; print "\n";
print "## # # # ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "############# ############## ## ##"; print "\n";
print "############# ############## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ## ##"; print "\n";
print "## ## ## ############## ##############"; print "\n";
print "## ## ## ############## ##############"; print "\n";
print "\n";
}