EDA365欢迎您!
您需要 登录 才可以下载或查看,没有帐号?注册
x
(信息来源:EETOPBBS 作者:lshj98115)/ {3 s3 v! `$ Y- F- W" Q
先啰嗦几句。其实老早就想写这个帖子,自己犯懒一直木有写。 前阵子写了一个初版,然后发给了几个做验证的朋友看了看,普遍反映没看明白 . 说是我写的东西和我搭的环境结合的太过紧密了,不结合代码,理解的不透彻。可惜代码是公司的,我不能把代码发出来。 我后来写了一个带很多代码截屏的版本,但是很抱歉没法发到论坛上来。2 j8 r3 D( W. C8 r; J
- T: B3 T6 x8 N3 t* i' t
我个人觉得下面的文字已经能表达我的想法和思想了,希望能对帖的有一点帮助吧5 E8 y: u2 z% k
- ?$ h$ ^4 J6 `% t" ? Z: J1 t; H
---------------------------------------------
$ e+ L/ _; \* ^; N$ w6 Q& m写这个文档的目的是让大家对搭建SoC项目的Testbench有一个比较清晰的认识,可以根据这个文档来一步一步的搭建起一个SoC项目的基本的testbench。本文档重点是指导大家搭建基本环境,以及能解决搭建Testbench过程中容易遗漏的问题或者容易遇到的“地雷”。
' F. o: d3 ]6 A( A% I9 A7 R
- f2 Q& ^* ]; U4 Z' H5 {" U我搭的SoC项目的testbench会有一些相对特殊的点: 8 J; f3 F& ?1 q0 i* j
1)要有嵌入式的软件。这里包括两部分,一是初始化的bootloader(一般是固化在rom或者存放在外部的flash里),一是boot起来以后放在外部易失性存储介质上的应用层的程序。 2 e+ z3 ? \& [+ @
2)正常启动起来(一级boot可以切到应用程序了)以后,为了简化流程,我们要使用ISS的环境。 --- 这是比较特殊的一个点
! K5 ]" P% p% d4 [ t G3)环境主要脚本的维护和修改。主要是单个仿真和批量仿真(regression)核心脚本
7 X. }) u/ s- U, ~0 |& z3 _4)为了优化仿真和编译速度,我们要能把不用的模块dummy掉。 8 N% H8 a* ]8 Q
5)文件列表的处理 5 f2 |3 P. H5 {7 c
6)SoC软件与Testbench都能访问的“共享空间”的处理 8 a3 _9 t( s& u' X7 m7 H2 g, Z, k& R2 L
7)公用函数的准备,比如根据CPU看到的地址空间直接访问外部DRAM的数组,进行初始化写、数据写和数据读操作。
8 y8 _6 Z* \5 J( _! p2 N8)环境变量的维护。
3 o7 x" s! k; _! x2 c9)Define文件的维护 g- @% c: r. L* g
10)DDRC的替换(一个是AXI_SLV_VIP的替换,一个是简单AXI_SLV模型的替换) 5 Q8 e/ C$ U- `4 A- Y: G) ~4 U) U
+ C Y, W! m8 I9 a/ R5 l* X
磨刀不误砍柴工,把需要的东西提前准备好,搭建Testbench就像搭积木一样简单快速了。
( |$ a' A/ F5 b" A3 x4 K! x
+ b, C/ y9 X7 H+ X4 |/ _3 E s* D环境变量维护
3 }5 R' J! T4 j- c. N. ^/ s( A" U使用module工具来维护整个项目的环境变量。目的是为了让项目上的工程师都使用统一的环境(主要是工具版本和环境变量)。
; a$ \& p2 E/ F- Z0 ]核心脚本的维护& x- U; K2 ^/ n+ U
两个脚本:run_sim 和regress。 run_sim负责提交单个仿真任务,regress负责提交批量仿真任务。两个脚本已经使用了很多项目了,脚本的具体说明我以后专门开专题讲。在这里只提醒一下,run_sim脚本通常需要根据不同的项目做微小的改变。
- a# c& V% {* K, Q3 \. Q) T9 nrun_sim和regress都是比较大的perl脚本程序,大致描述一下功能。 , d6 V, }3 X4 D9 O9 T
run_sim脚本功能
' }" ?& p2 f/ E' _3 V1)为每个仿真产生仿真目录。仿真的目录里应该包括文件列表(硬件和软件)、编译和仿真命令(注意包括嵌入式软件的MakeFile)、提前建立需要的子目录、和单个仿真对应的文件链接(比如维护的C的测试主函数、扩展的随机类的SV文件、一级bootloader文件的链接)、define文件、本仿真的重构命令(这是一个容易忽略的,一旦你跑regression的时候某个仿真失败,你又不想在出错的目录下重新仿真,用这个重构命令文件就可以直接提交)。 3 s3 D7 K/ Y) i9 o6 ], v3 z$ m- \
2)各种option的维护。比如不同仿真需要不同的define、编译和运行option、dump波形的scope以及层次
5 P1 j! w( Q; {% _. y: P' N# n- k# x/ Zregress脚本功能 9 j4 Q: V2 ]8 a2 L5 b% w
regress脚本比较简单,要吃一个由很多run_sim仿真命令组成的命令集文件。用regress脚本把这些仿真命令提交到工作站上去。需要注意的是:有时候可能会有一些公共的option或者define,比如打开coverage收集、某个define要应用到整个regression里。所以regress脚本要能支持对所有run_sim命令添加option的功能。 & b" }% q7 S- }# N
产生dummy文件
% Q+ N }1 t9 {, ?! y使用gen_dummy_file脚本来产生dummy文件。设计工程师可能也要维护一个module_dummy.v的文件用于做integeration,验证工程师产生的dummy文件记得名字不要和设计自己维护的文件重复了。
( S9 Q- D5 U: ^为什么不使用设计维护的文件?因为一个是设计维护的文件在integration以后很可能就不再维护了;另一个是设计维护的文件可能output全是assign成0的,但是对于模块输出的pready\CEN等信号最好assign成1,否则可能导致问题(例如:sram使能信号CEN赋成0,可能导致后面的sram模型认为有读写行为;pready信号赋成0,可能导致SoC软件跑起来的时候对该模块寄存器操作的时候挂死apb总线)。 ! H4 M/ I2 `/ c0 F- Y" f. B& f
这个脚本并不好写。因为verilog语法支持的模块声明实在是太多了,导致脚本很容易顾此失彼。举例来说几个复杂的地方:
% C) [5 `3 `- ^/ Z7 m. \6 f+ A. Jmodule声明后面可以跟parameter的就很复杂 4 o: b! Q* _5 E& w) d$ d. C6 R
Module test #(parameter a = 1,Parameter b = 2,C = 3,D = 4 ); 1 E4 @4 X- t( N n
这些parameter很可能要用在端口位宽的声明里。更为麻烦的是parameter里可能会有function的使用。而function有可能是以define的形式写到代码中。这样就很难用parse RTL的方法来解决。
( \% f+ {# {9 i d) w. u再比如: 端口声明里出现ifdef else endif这种编译宏的处理也比较麻烦。
, X4 I: b* j6 u
: U1 h, W/ ]9 `也可以使用simulator或者debug工具提供的用户接口来编写tcl程序来获取各个端口的name、width信息。但是不同仿真(define不同)可能导致端口宽度和端口不一致,结果要针对不同define来维护不同的dummy也比较麻烦。
0 `2 {% d, R+ L( L
0 I' v. A k7 X% c& k- h: @ z总之,产生dummy文件以后一定要记得检查一下。Dummy文件可以有效缩短编译的时间。
" x; V; Y9 B) b- @. o! O7 V文件列表处理的维护
0 \. _5 `. \- o上述几个事情是应该提前准备好的,接下来我们要开始编译RTL了。Integration好的文件列表,首先要先编译该文件列表。有可能遇到的问题是加密文件的种类,有可能文件列表里的加密文件和你用的仿真器不一致。然后结合前面产生好的dummy文件,我们要处理出一个简化设计的mini-文件列表,一般里面只包括初始化必须的模块(Clkrst、PAD、CPU、总线拓扑、内存控制器),也就是把video系统、外围接口、存储系统这些模块统统dummy掉。 M6 j. y J8 v" H5 G
0 g$ ], l1 c8 R/ o/ d0 l' X
产生mini-文件列表
& i& ~. t5 u6 Y* }5 \8 A 4 X" W R6 L( g2 ~
可以用脚本来维护一个配置文件,在该配置文件中指明如何删改原有文件列表。 6 o( B3 y2 H. E; Q8 j5 f K
# K, l' k- f+ `
" C$ P0 \ a7 t' g0 k( F' _ : P+ }) m/ t" @, ?( Z4 V, W
注意最终使用的文件列表里的文件路径应该是绝对路径。 " I0 r$ J4 ~4 q2 r, A
使用绝对路径的好处在于可以让run_sim脚本指定仿真在任何目录下进行(比如regression要提交到别的硬盘上去跑,那么就必须使用绝对路径了)。 , [; w, A, U) J7 n( K
注意绝对路径里不要用$macro的结构,别人有可能用你的文件列表跑仿真或者debug,而别人的$macro很可能与你的不同,导致出问题。( \" M5 Y. {2 s; ^
. n& J- D- }* ?7 f6 ^( D文件列表的产生有一个地方需要注意: 7 [7 d- R/ a. W+ a
通常来说一个文件(比如a.v)在一个文件列表里只允许出现一次。否则可能会有重复module的编译错误。但是有时候集成比较特殊(比如FPGA版本),为了改动的时候少调整code,会使用ifdef-elsif-else-endif这种结构来对同一个文件的module定义不同的module-name。比如文件a.v的内容如下:
; C4 t; g& Q( R0 v`ifdef FPGA1
3 Y3 [- w5 R8 U. T9 i
- o" J8 e! ?: Y2 smodule v_fpga1 ( : K2 [9 F6 T1 ^% k6 x2 x
`elsif FPGA2 1 w4 j* ~- b) \8 a
6 l6 N& c; ^1 |& Y# s6 q2 ?module v_fpga2 (
s; r$ G$ ^% {`elsif FPGA3 % b4 W7 l: h- W5 _" B7 T3 O
( T% s6 C: j9 h+ f
module v_fpga3 ( / r" h* V8 @/ `6 M) q% g6 g$ {
`else 4 Y& Q0 [8 g$ o9 J1 A- z7 V
* o+ q9 A/ ]5 K' Fmodule v (
. t! o- a) a F# j`endif
; Z L- Z3 N- }3 ~/ U / h! O7 R& J4 T
" s3 M, Y& Q8 \) G2 v9 W那么在文件列表里就会是下面这种结构: 8 S B- ~$ q0 }* U, O8 e* H& R( g
fpga1_def.v ! G0 F+ B1 _; ^6 x
a.v
, g$ S- w8 J2 z rfpga1_undef.v / V8 T$ T0 I3 s1 a' e7 q
fpga2_def.v 8 p) ?8 ~! g# v
a.v 8 G& e3 W( }2 t$ u* W" k
fpga2_undef.v
, F' Q( K6 @4 G7 f+ i! [1 x y……
. d3 k1 E* T% w+ e! I6 { ( v% I4 O* g. U P- q$ {
请注意文件列表处理脚本,有可能会有“去重”的处理。这个时候要去掉文件列表处理脚本的“去重”功能。 h; w$ q" M# w; f
编译过mini-文件列表以后就可以开始真正的准备写testbench顶层模块了。 2 J- g" J: U9 i: F0 P
# L0 S2 l, v5 [' Q
Define文件的维护& z, T3 l: `, X
我们在搭建testbench过程中的interface、env、svtb等可能需要xmr(cross module reference)访问信号,这时候维护一个公共的define文件很重要。该define文件中应该包括
: n0 h/ H/ K/ \1)各个主要模块的xmr路径define ,记得按照ASIC/ FPGA/模块级 来分别区分define
( E$ E- @5 |$ S2)地址空间上模型数组的路径。比如dram模型里数组的xmr路径、sram里memory数组的xmr路径
% `' J& B: f1 I4 G3)共享空间的部分地址的define,比如我们的软件打印的实现所用到的共享空间的define
& m6 Y2 u: Q" w9 J4)Dram基本define
% B% K2 R$ E1 |- j9 W0 d ! X" {8 W2 Z: }# x n# M1 u
共享空间 1 [' |( s! Z# _9 E, o
SoC项目Testbench中的“共享空间”,是指的软件(嵌入式C程序)和Testbench(SV程序)都可以看到的空间。一般来说Testbench可以看到所有的内容,而软件只能看到CPU地址空间(寄存器、SRAM、ROM、Dram、外部IO空间等)。共享空间需要的地址范围不算小(可能需要几十KB,所以一般是放在CPU可见的SRAM和Dram里),对于ISS会有所不同(后面会说明)。
! A8 J6 n: y# G; p; R# i: H4 A公共函数的维护
: b) \' p, U/ I1 h* M项目上大家都可能使用到的函数即为“公共函数”。我个人认为最重要的是对CPU地址空间的访问(我们是xmr_read_mem和xmr_write_mem)。以及基于这几个函数(task)实现的文件存取等函数。
0 h4 o* m% W0 Z; \在实现xmr_read_mem和xmr_write_mem task(或function)的时候,主要模型数组的宽度会导致根据模型数组下标访问的地址的不同。比如,加入位宽是128bit,那么一个memory就对应着4个32bit的word。----- 各个项目会有所不同。
9 S; c @+ V, y+ _4 f* R( ~另外,对于Dram的处理是最复杂的,尤其是Dram是支持bank地址和Row地址 remap的,所以要特别注意remap时候的 地址和bank信号、row地址信号的对应关系。-----这个工作是可以继承前面项目的。 ; v$ P1 k, O6 w' t2 z0 c
Xmr函数需要考虑“用SRAMC或者AXI_SLV_VIP替换DDRC”情况下的实现。 - U3 z$ c& e# H0 s* V0 X
- o E8 H- r! W7 U
简单说一下vip_slave_write32函数的实现。 这个函数调的底层函数是:
0 ~ i3 g% m) P" C |% ?8 h: Yenv.axi_slave_subenv.do_write32(addr, data); , A2 j& c3 \ t5 t2 X
但是该函数在tb其他组件可能看不到,但是program可以看到。所以在program里做一个函数来调底层的env.axi_slave_subenv.do_write32 9 e7 Y( C3 P# L& F8 ^! A% A
+ u* G" G) K- T9 R
然后把program的这个do_write32用DPI export出去。 7 w, z8 ~# T7 ?8 r2 b* P7 P
, S) _) R' ^# `" y4 v
在tb上维护一个xmr.c的c程序,里面实现vip_slave_write32。在xmr_wr_mem32里调用的就是这个vip_slave_write32. 2 A2 ]5 @3 k9 o) K9 B m! O8 C
+ S3 f6 M8 F& l' @5 n使用xmr_wr_mem32 和 xmr_rd_mem32可以比较容易的实现: , T2 B# F+ J$ x9 q9 |* V" p0 k
1)装入初始化程序 --- 一级bootloader要装入到rom中,应用程序要装入到dram中。使用xmr_wr_mem32可以直接以访问cpu地址的形式来写入程序。
0 r ~) L; \+ l) a0 R ; y ]$ w0 N5 U% }0 t" c
2)把激励数据灌入dram中,让被测模块从dram中取激励数据。或者从内存中读取成片的数据用来做比对(比如解码解完了一帧,从内存中一次性把整帧的yuv解压缩完的数据读出来)。 ( I: J4 F! J+ n9 A
这里可以使用上面装程序类似的方法来实现。也可以利用xmr_rd_mem32和xmr_wr_mem32来实现一个通用的task。 1 f7 V0 t' |$ V- e
把指定文件写入到内存中作为激励数据:
% i% c3 m/ `# F. H这里有一个小技巧,就是用fscanf来读取文件中的一行数据,然后判断字符串的长度,从而得到输入文件一行几个byte,然后根据一行几个byte来装入到dram中。 - z5 O, S9 r8 |+ \
T. [% q6 @0 y
Testbench顶层文件
7 e. u4 T( R( V, Z5 G7 w我们基于mini-文件列表来做Testbench顶层文件,是为了加速编译速度。 ( ?. Q+ s- U$ p2 \: y
Testbench顶层最主要的是例化DUT的顶层。Emacs用户做集成很容易;我是VI用户,稍微麻烦一些,使用vi的替换功能也可以比较快的集成起来:
) p* d! g3 | s& a1)把顶层模块的input output inout端口声明部分copy出来,把input-outpput-inout替换成“wire”,来实现信号的声明。个别信号,系统输入信号、系统reset信号需要改成reg,并产生reg信号的激励。 时钟的产生建议使用一个module来产生,目的是为了让代码简洁清晰一些。 7 N) \. c/ G% @3 i/ y. B, J! @
2 X4 W( @' I9 v- g) b. ^2)Copy一份wire声明的部分,然后处理成DUT的集成。
/ W+ _& q8 {/ {& Ps/\s*\[\d\+:\d\+\]// 去掉位宽声明; {3 w( _% Z3 X
wire [1:0] A; à wire A; D, y1 X0 u0 x' j; G
s/wire\s\+// 去掉wire声明+ p+ _% i# D: b! O$ D+ a
wire A; à A; # h: k' E& o) h
s/\(\w\+\)/.\1(\1)/ 产生集成3 ?% G, p+ D# {! M
A; à .A(A);
, R3 i& D5 s% T T8 ~" o3)处理一下模块名声明例化和分号。! _" m8 @- u6 s4 v$ Q: z! t
.A(A); à .A(A), : ~" N- Y$ m4 P B
' H$ v9 l+ q0 a5 J' n9 e( i
给顶层信号加pullup pulldown,一般来说顶层信号都要加pulldown,个别信号需要加pullup.总之是不希望让TB引入X状态。如果不知道哪些加pulldown pullup,至少要对 测试模式输入pin(TEST)、CPU Jtag口、初始化要读取的PAD状态或者标识PIN加入合适的pullup或者pulldown。 9 n# u' [1 o$ f6 B1 b' f% X
8 O% }. q* T$ c例化interface和program。
% Z! @0 D) D# M( i
( V, R8 R; B: ?6 mProgram通常就是简单例化SV的组件(比如VMM下的env),以及include每个testcase所不同的处理部分 ; \: e# V: ^- \2 x" H/ f7 s
: D m6 @. K. t0 R / O/ b3 l3 Z( t: z$ }' s% Y$ k7 B
在每个test.sv里通常是实现随机变量的扩展类。 2 R/ b8 |+ R$ }+ o/ [ V* J8 q
要注意Program如果结束的话,那么仿真也会结束,所以注意控制program的结束时间。
) P; t2 F5 m6 p* u- s$ G' D + @' l% x; S, N3 T
例化基本仿真模型。最主要的是Dram模型了。请注意,Dram模型的例化最好用define处理好,因为Dram有可能要做4bit 8bit 16bit等几种情况,不同大小、不同位宽的dram的地址信号宽度不同,外挂的片数也不同,这里集成的时候需要特别注意。 1 }* q" Q3 Y Y+ g) ?: p( H2 T& U
0 Q: @" t4 r; T: d W
5 a4 R' _" e! O
Dump波形的实现机制:
8 x7 E) [; U* e: r. P$ ? `$ t; P5 ^Dump波形的原则是“是否dump、修改dump的起始时间、修改dump的层次都不需要重新编译”。前两个要借助仿真的运行参数来控制,后一个使用verdi的pli。
, c6 q9 b- i2 U# M
8 [8 Y) m4 q8 W$ w* @通常Testbench顶层文件都比较复杂,建议多使用Include文件的方式维护,这样代码可读性较强。而且顶层文件里通常有比较多的ifdef-elsif-else-endif的编译结构,代码太复杂的话,可能有一些笔误造成的编译错误。
- i4 f4 W; m& p7 U2 ? ) H1 b; n: v! |3 ]' ]& w
Include前面准备好的公共函数文件和公共define文件。 9 D3 `2 \9 X' w. }: c% A! l
) n' Y, u u p: Y- k程序初始化load代码。SoC项目需要嵌入式软件代码,包括一级boot和Dram里的应用程序。这两段程序代码都需要load到对应的存储介质中去。这个load工作可以使用基于xmr_write_mem函数构造的写文件函数比较简单的实现。------具体实现前面已经贴过了。 ' {( k1 h5 w2 V
# d+ Q+ j3 e g+ H至此,testbench顶层基本完成。
9 b" s7 p: J" e" A/ j % N6 V5 N* p+ F$ q! P* G
初步debug设计和环境5 n# q1 H4 R" |: r
顶层testbench写好以后,编译通过后,dump整体波形,可以看一下各个模块端口上是否有高阻Z,有的话说明可能有漏接的内部信号,尤其是主总线上的各个master口和slave口的连接。 : `7 ^) u7 D! \* \, x2 z7 J; @
检查CPU PAD ROM控制器 SRAM控制器等初始化需要的基本模块是否有时钟和reset。如果没有的话,说明根据外部输入系统时钟和系统reset产生的基本模块的时钟和reset有问题。
2 S+ c# a& h1 t/ [8 y: [
9 Z, y) S) Y# p q一级Bootloader& g+ R" Z9 H, V& a
一级bootloader是为了做初始化的,系统实际使用的bootloader是比较复杂的,牵扯到外部存储介质上的参数搬运和配置。仿真用的一级bootloader要尽量的简单,因为一级bootloader所有的仿真都要用,这一步要是慢了会浪费时间。------- 当然,使用ISS的话,就不存在这个问题了,但是一样也要求初始化要尽量的快速。 7 l6 f% {! J* O9 L6 D# z
我个人建议仿真bootloader里就只设计如下几个步骤: & ?+ K& u- |* ]. _
1)系统上电初始配置
1 E2 V# u9 |4 w& B ) P5 }$ J; Z6 [5 q% R. E
2)初始化pll至目标频率(如果系统pll默认频率就是目标频率,那么这一步就省略) 9 ^8 u; g( q2 D, R
8 x+ G2 o1 P& P2 l
3)配置核心模块时钟频率以及切时钟,对必要模块进行软件reset * y2 F" i# S- z' Z
6 _7 x) J( D; C y( g# H8 l4)内存控制器初始化 6 h$ J8 b$ |5 D. N$ [" g9 y' s9 J
8 b$ y! o' u% z! {- s5)Remap到内存中准备执行内存中的应用程序。 ------ 一般汇编实现 2 ^* ]/ k* P: n1 K8 W+ G
$ ]' e. b, `% }3 \+ {. s! T
# ]: S' T7 r" V: L最基本的函数是对CPU空间的访问处理函数 / z6 {: ?( l4 b$ g
#define SETREG32(reg,val). a) K0 J5 q! n. Y% X5 y0 G
(*((volatile unsigned int *) (reg)) = ((unsigned int) (val))) + x( s) a, ]# g
#define GETREG32(reg)# l$ t0 N: S1 N, u
(*((volatile unsigned int *) (reg)))
, N: W% M3 p* A5 t$ m/ C% i& x# Zreg是寄存器地址,val是要配置的数值。 Volatile保证直接操作到内存。
) L8 }# O0 G3 K0 r( z9 q! l应用程序代码* Z+ K! F6 s+ L6 A% f) Y
应用程序代码里也要做一些初始化,主要是非核心模块的时钟配置以及非核心模块的软件复位操作。 8 p+ d+ }' {4 F$ K) h* C l
如果使用ISS的话,由于没有一级bootloader,所以要把一级bootloader的代码功能在应用程序初始化中实现。 ) s( W, R# o5 ~2 J; \
需要注意的是,使用ISS的时候,使能cache可能导致ISS行为异常。可以在cache使能的位置使用ifdef ISS。汇编代码中是:
! ~# M& c0 f$ B, U# {& f: h
K3 s M* Y. f' w7 }IF6 g+ V+ T" e' c- ^: q6 N7 b. `
( EF: ARM_ISS)
$ b2 r1 d: d1 c, A
& z" H8 h, w" s5 j( m* i% vNOP - ]6 v0 p3 w8 y5 n/ z
8 I/ @7 o' L$ l9 ~* d1 P8 K% m
NOP
2 s* e" Q- T, i! d% `& OELSE
& E; Z; q9 D3 b* L3 BCache-operation 5 z- P8 E. b8 @/ Y* C
ENDIF
& ^* `) B& F! {2 y0 f# A5 q F* F / h* ^. A x8 I- M; A2 K
汇编代码中include define文件是:
2 Y4 y9 t$ h# a; I; }, h; A
! ]2 K% U" }8 O; [ Z: A% F# u5 J2 c* z- \GET define.s (注意不能顶头写!)
" b" ^1 @+ \$ n+ V: R . X1 B* \$ N! L6 X ~5 x P0 S2 P( ?4 K
然后构造一个极简单的应用程序。一般就是访问一下ddr、sram、寄存器和打印。 & M# T9 f; S3 B- j3 e- e8 ]' e' A- N
% U# K% Q- L+ H; Aendsim()是结束仿真函数,如果希望让软件控制什么时候结束仿真,那么就可以在软件中的合适位置调用该函数。 函数的实现是利用共享空间,软件写入到共享空间指定位置一个标志,然后svtb中while(1)的去采样该标记就可以了。 ! k3 C5 L! A$ d9 [
/ N8 D: g- z4 J- j9 D6 E9 m ; i$ ~' z, K) M7 B% N( J
实现嵌入式代码在仿真平台上的打印软件代码里相对复杂一些的是“printf”的实现。重点是使用软件和testbench都能看得到的地方来存放要打印的内容。然后testbench里while(1)的根据“打印使能”、“打印开始”、“打印结束”标志来把内容$write出来。 & z5 m0 D( s( \9 n( ~ k
软件和Testbench都可以看到Sram空间(一级bootloader用来做数据存放和堆栈的sram)。注意bootloader的scatter文件里不要让stack-top覆盖了这部分空间。 6 h( R5 ~+ O* r% F( R o6 g. h) ^
Printf与实际C的printf的实现机制是一样的,都是利用“不定个数参数的函数”(实现机理:因为参数是从右向左压栈,所以最开始的那个参数在最接近栈顶的位置,这个参数在栈中的位置编译器可以知道)。 . s9 C1 Y% _2 f4 U d# z. x; r
. a3 l+ ?* Q! D2 g% q7 \9 {Debug整体环境至此,整个环境已经基本建立起来了。结合一级bootloader和简单的应用程序代码可以debug系统初始化流程和整体环境。通常这里会有一些集成、以及总线访问的小bug。
: q; H; V0 N7 k: V$ X 3 A1 X F0 w/ H1 n* g8 t# U
ISS替换为了加速编译和仿真速度,我们使用ISS来替换CPU-IP。ISS一些C程序代码。提前把这些代码编译成.so文件,然后编译的时候就不用编译ISS了,链接的时候link进来就可以用了。 ' X4 y5 i0 ~) I
使用ISS的优势:
. ^7 T% e! e; i1)可以dummy掉CPU-IP的代码
R4 h! x @+ ~ N4 L$ ]) T2)不需要一级bootloader
0 Y5 r/ ?' H; _% o) n! O& ]3)执行软件很快
" A: n z; ~9 V7 Q8 h4)Testcase依然可以基于嵌入式c程序来写 % ]; a+ C( h% o# D8 v2 Z
5)模块级的testcase也可以用C实现
+ X' i8 L$ U& X( N, u
: S; P* ]3 Y. |* bISS外面包一个AXI的wrapper,把这个模块例化到testbench顶层。Force到cpu的data总线的AXI口上(如果是Arm9的话,是AHB总线)。IO访问的task文件要include到testbench顶层中去。对于寄存器空间的IO访问,需要产生正常的时序;而对于内存空间的访问,可以调用前面介绍的xmr_wr_mem32和xmr_rd_mem32函数来加速。
7 |3 ], l _/ M- v& g$ mISS可能和CPU-IP不是同一个类型的CPU,这里要注意编译软件代码的时候需要加—cpu的区分,甚至可能导致软件代码的编译器都不同。这些不同可以体现在run_sim脚本里。
6 ~$ C* |5 i6 Y: T9 h( h
; l, K; A7 D `每个项目的CPU地址可见空间可能不同,需要注意ISS的空间配置文件的内容要根据项目的不同而不同。IO访问的task里的地址访问也会有所不同。
# y/ E, b+ I) D4 ~
" A1 T& r- s5 y: WISS下共享空间与实际CPU_IP不同,像实现打印这种功能,可以不必使用CPU地址空间。这是因为ISS的wrapper是testbench的一部分,可以直接在testbench上实现一个大数组来作为“共享空间”,这样更简单直观。
* H3 W' Y, c) E5 l9 [
H* k: t2 x5 o' x" e3 EDDRC的替换系统起来以后,我们可能需要替换掉DDRC。一般有两种情况: 5 I. m3 [3 K) ?( t, o* r
1)使用SLAVE_VIP替换DDRC,目的是为了随机控制slave的latency。实现模块访存的异常情况。------ 一般要结合ISS使用,因为没必要把应用代码初始化到slave-vip中。Slave-vip的读写可能会比较慢,对于大数据量的写入行为,仿真可以明显感觉到停顿。
8 M+ u, C& R0 z$ |! r, Q% \8 M把内部端口和slave-vip对应上:建议使用macro,方便阅读和简化代码。
( a+ [( }( R& K9 i: f7 R / O3 y2 }6 U. r5 \+ X$ I2 {
2)使用一个更为简单的Slave(SRAMC)来替换DDRC。目的是为了快速初始化(不用配寄存器做初始化),加快编译和仿真速度。
" y4 l* M- s: _) L* C& OSRAMC不是class,而是一个module。把它例化在顶层TB里,与上面的Slave-VIP一样也需要和内部端口对应上。
4 a7 U4 v- [0 f& z% ~) N
; d: ^" f3 Q; E2 f, JSLAVE_VIP和SRAMC都是参数化设计,可以方便的修改数据宽度等信息。 0 j2 T# X- a' C! _2 l
|