找回密码
 注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

巢课
电巢直播8月计划
查看: 2|回复: 0
打印 上一主题 下一主题

[硬件] 论坛推荐:如何搭建SoC项目的基本Testbench

[复制链接]

551

主题

1470

帖子

3万

积分

EDA365管理团队

Rank: 9Rank: 9Rank: 9Rank: 9Rank: 9

积分
39487
跳转到指定楼层
1#
发表于 2019-9-27 15:10 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

EDA365欢迎您!

您需要 登录 才可以下载或查看,没有帐号?注册

x
(信息来源:EETOPBBS     作者:lshj98115)
5 h. ~& y% k: K. z  [7 [8 F. s先啰嗦几句。其实老早就想写这个帖子,自己犯懒一直木有写。 前阵子写了一个初版,然后发给了几个做验证的朋友看了看,普遍反映没看明白 . 说是我写的东西和我搭的环境结合的太过紧密了,不结合代码,理解的不透彻。可惜代码是公司的,我不能把代码发出来。 我后来写了一个带很多代码截屏的版本,但是很抱歉没法发到论坛上来。
" z# a: \: r/ C8 _! V1 S! h) o
5 j: G/ d& |$ ?9 @2 A: Y5 W7 i我个人觉得下面的文字已经能表达我的想法和思想了,希望能对帖的有一点帮助吧* s$ O8 o5 L5 v# W' O( ?: r
1 F% z6 g; ?9 c
---------------------------------------------
' ~2 p. j6 Y  D" f8 M
写这个文档的目的是让大家对搭建SoC项目的Testbench有一个比较清晰的认识,可以根据这个文档来一步一步的搭建起一个SoC项目的基本的testbench。本文档重点是指导大家搭建基本环境,以及能解决搭建Testbench过程中容易遗漏的问题或者容易遇到的“地雷”。
" A3 _/ R. w, t& w$ O4 R

5 H3 A6 y1 k" K9 x9 B$ ~
我搭的SoC项目的testbench会有一些相对特殊的点:

( f3 c3 a# [- e7 U: y" p
1)要有嵌入式的软件。这里包括两部分,一是初始化的bootloader(一般是固化在rom或者存放在外部的flash里),一是boot起来以后放在外部易失性存储介质上的应用层的程序。
8 u& _5 Z4 U: Y: X# I; g3 w
2)正常启动起来(一级boot可以切到应用程序了)以后,为了简化流程,我们要使用ISS的环境。 --- 这是比较特殊的一个点
9 T4 a2 w, t+ Y. v8 w- J( w' Y- d0 X
3)环境主要脚本的维护和修改。主要是单个仿真和批量仿真(regression)核心脚本
: I7 g! c, i; E! K$ E
4)为了优化仿真和编译速度,我们要能把不用的模块dummy掉。

7 C& |1 K& x4 V/ g5 |& e) a; v
5)文件列表的处理

' _! ^2 t4 x" O
6)SoC软件与Testbench都能访问的“共享空间”的处理

  g' K- r7 j" Z1 t* j
7)公用函数的准备,比如根据CPU看到的地址空间直接访问外部DRAM的数组,进行初始化写、数据写和数据读操作。
8 ~! I3 b# O1 l& W
8)环境变量的维护。

/ s4 O0 w* M! l7 p9 p
9)Define文件的维护
, A7 x3 H1 [+ H; ^1 j
10DDRC的替换(一个是AXI_SLV_VIP的替换,一个是简单AXI_SLV模型的替换)
+ _1 q1 G6 N& c& l4 v7 ^
- T$ L( z# p0 v, F* ]
磨刀不误砍柴工,把需要的东西提前准备好,搭建Testbench就像搭积木一样简单快速了。

$ u' r# [+ l& R" B% Y

! v3 ?5 A7 d' w* H环境变量维护' Y# R2 v! q6 E+ o: `
使用module工具来维护整个项目的环境变量。目的是为了让项目上的工程师都使用统一的环境(主要是工具版本和环境变量)。
9 ?+ n7 j8 a# N( h: Z
核心脚本的维护
  }$ W  \; e. a4 S# {
两个脚本:run_sim regress run_sim负责提交单个仿真任务,regress负责提交批量仿真任务。两个脚本已经使用了很多项目了,脚本的具体说明我以后专门开专题讲。在这里只提醒一下,run_sim脚本通常需要根据不同的项目做微小的改变。
6 @4 R1 s2 A- k" c+ ]  D
run_simregress都是比较大的perl脚本程序,大致描述一下功能。

. K) s: g+ h. \6 Z$ ?1 K# ~7 ~3 t" l
run_sim脚本功能

: a+ x  r' p: N2 Y7 _0 r8 |
1)为每个仿真产生仿真目录。仿真的目录里应该包括文件列表(硬件和软件)、编译和仿真命令(注意包括嵌入式软件的MakeFile)、提前建立需要的子目录、和单个仿真对应的文件链接(比如维护的C的测试主函数、扩展的随机类的SV文件、一级bootloader文件的链接)、define文件、本仿真的重构命令(这是一个容易忽略的,一旦你跑regression的时候某个仿真失败,你又不想在出错的目录下重新仿真,用这个重构命令文件就可以直接提交)。

; v  M) P$ l6 c7 ?" V
2)各种option的维护。比如不同仿真需要不同的define、编译和运行optiondump波形的scope以及层次
- E( a$ k( Z+ P: e2 f  P3 f$ F# @" s
regress脚本功能
- R; m; P1 h6 \, h( ?% k
regress脚本比较简单,要吃一个由很多run_sim仿真命令组成的命令集文件。用regress脚本把这些仿真命令提交到工作站上去。需要注意的是:有时候可能会有一些公共的option或者define,比如打开coverage收集、某个define要应用到整个regression里。所以regress脚本要能支持对所有run_sim命令添加option的功能。
7 L$ N7 p; O3 b6 ]9 g& F4 v) ^
产生dummy文件
- u& C6 o+ w0 ?' e5 m* V: i
使用gen_dummy_file脚本来产生dummy文件。设计工程师可能也要维护一个module_dummy.v的文件用于做integeration,验证工程师产生的dummy文件记得名字不要和设计自己维护的文件重复了。

, A/ D$ `' G- y% Q: i, k
为什么不使用设计维护的文件?因为一个是设计维护的文件在integration以后很可能就不再维护了;另一个是设计维护的文件可能output全是assign0的,但是对于模块输出的pready\CEN等信号最好assign1,否则可能导致问题(例如:sram使能信号CEN赋成0,可能导致后面的sram模型认为有读写行为;pready信号赋成0,可能导致SoC软件跑起来的时候对该模块寄存器操作的时候挂死apb总线)。
% h" M) o% n3 O! L
这个脚本并不好写。因为verilog语法支持的模块声明实在是太多了,导致脚本很容易顾此失彼。举例来说几个复杂的地方:
1 ^) q4 c5 d' ?5 [1 L. b% \) y
module声明后面可以跟parameter的就很复杂
# Q" q) v1 r( b- k+ n  a
Module test #(parameter a = 1,Parameter b = 2,C = 3,D = 4 );

* e7 |; W% @- T% c: M% Y9 u/ T
这些parameter很可能要用在端口位宽的声明里。更为麻烦的是parameter里可能会有function的使用。而function有可能是以define的形式写到代码中。这样就很难用parse RTL的方法来解决。
0 O" R5 h( F& P1 t1 V- X
再比如: 端口声明里出现ifdef else endif这种编译宏的处理也比较麻烦。

! j) a6 b, B2 H' ~% n6 m9 \, l" L
+ Q9 ?. s2 B- H! K* @, S
也可以使用simulator或者debug工具提供的用户接口来编写tcl程序来获取各个端口的namewidth信息。但是不同仿真(define不同)可能导致端口宽度和端口不一致,结果要针对不同define来维护不同的dummy也比较麻烦。
8 v& D  c; ?, z; p

  ~: N; N4 t$ v" u1 P
总之,产生dummy文件以后一定要记得检查一下。Dummy文件可以有效缩短编译的时间。

: H* u1 u7 ~4 Y# m4 s) F文件列表处理的维护
$ V( {# M/ Y5 |
上述几个事情是应该提前准备好的,接下来我们要开始编译RTL了。Integration好的文件列表,首先要先编译该文件列表。有可能遇到的问题是加密文件的种类,有可能文件列表里的加密文件和你用的仿真器不一致。然后结合前面产生好的dummy文件,我们要处理出一个简化设计的mini-文件列表,一般里面只包括初始化必须的模块(ClkrstPADCPU、总线拓扑、内存控制器),也就是把video系统、外围接口、存储系统这些模块统统dummy掉。

6 [* i8 I- W2 |. F2 V1 L) r

8 F* _; b5 |0 h, A0 u9 C! y' C
产生mini-文件列表$ Z+ i. m$ O& W; f* W2 w" J
4 @. [% n# z: Q+ \/ N6 `
可以用脚本来维护一个配置文件,在该配置文件中指明如何删改原有文件列表。

0 @2 R+ Q  h4 t  l1 @  @# f) k' o; c) v- S* v

9 j$ H/ N. G' H& v6 [& w
  y8 I$ R% ]1 ?. S% n5 ]
注意最终使用的文件列表里的文件路径应该是绝对路径
7 g8 |' j3 q$ Z
使用绝对路径的好处在于可以让run_sim脚本指定仿真在任何目录下进行(比如regression要提交到别的硬盘上去跑,那么就必须使用绝对路径了)。

) P& }  A; |8 O  P/ h/ o2 L3 a
注意绝对路径里不要用$macro的结构,别人有可能用你的文件列表跑仿真或者debug,而别人的$macro很可能与你的不同,导致出问题。
: m  P# Z; [7 |, x

6 t. s& S! u% y( s
文件列表的产生有一个地方需要注意:

, [- \, [5 H9 L% U
通常来说一个文件(比如a.v)在一个文件列表里只允许出现一次。否则可能会有重复module的编译错误。但是有时候集成比较特殊(比如FPGA版本),为了改动的时候少调整code,会使用ifdef-elsif-else-endif这种结构来对同一个文件的module定义不同的module-name。比如文件a.v的内容如下:

' w/ n+ q  d# ~/ V
`ifdef FPGA1
0 F* {$ B! o: m; v+ ?

6 `7 w) y- E* C3 e5 Cmodule v_fpga1 (
7 n6 p5 F, v+ c# U, Q' P) b: n
`elsif FPGA2
. v% A4 U7 d' R; I+ W+ \. U

! a0 f8 C. A% K6 Lmodule v_fpga2 (

! T* f3 O2 E4 H  ~) }2 a
`elsif FPGA3
0 S. r1 |$ `, _  y& E4 F
' |1 F4 s# c" n- a; t
module v_fpga3 (
* [# h( X8 U! Y2 S6 k  _
`else
! E7 F$ \: k0 @6 G

9 `8 T8 A9 w- O7 Omodule v (
/ {2 B  B/ U5 j/ j" t
`endif

4 J) q# n% i2 A7 z
, P; s% E" K8 v9 }( l4 ?& J  P+ I

1 ]8 a  `# G' z1 X
那么在文件列表里就会是下面这种结构:
3 Y5 C! Z9 ^) A5 W6 ^
fpga1_def.v

- K. W- K$ `0 P) Z: q1 v9 R
a.v
. h$ g- D' W1 ?/ N% Q9 {
fpga1_undef.v

1 j/ y  G2 S% s$ k
fpga2_def.v

& o( _: v; Y. h) S
a.v

# t. \8 y1 U! w
fpga2_undef.v
! g  z9 K% F4 l8 T0 K
……

/ S' m8 v! U8 D, M$ W3 Y
5 G6 N6 Z- R1 r8 {3 f
请注意文件列表处理脚本,有可能会有“去重”的处理。这个时候要去掉文件列表处理脚本的“去重”功能。
# ]/ T8 h) v: u/ w. `# f
编译过mini-文件列表以后就可以开始真正的准备写testbench顶层模块了。
$ A$ `( `7 H, R, I+ |' y6 _. `
! L4 v7 {: P0 b& ^  P7 s  F
Define文件的维护
' a* s9 C$ d9 E. `( u5 Z
我们在搭建testbench过程中的interfaceenvsvtb等可能需要xmrcross module reference)访问信号,这时候维护一个公共的define文件很重要。该define文件中应该包括

+ j  M6 e% s1 ^; I; _% N: n
1)各个主要模块的xmr路径define ,记得按照ASIC/ FPGA/模块级 来分别区分define
, A1 \4 j8 A  M' s
2)地址空间上模型数组的路径。比如dram模型里数组的xmr路径、srammemory数组的xmr路径

& e9 j$ ^6 M4 X; M9 y+ y
3)共享空间的部分地址的define,比如我们的软件打印的实现所用到的共享空间的define

( \# \! k' h- u. I5 e* `4 n" k
4)Dram基本define

; {" }1 D* K1 H8 y
: q: V  k$ u! b# d
共享空间
1 y0 a3 |8 t- n5 S& _
SoC项目Testbench中的“共享空间”,是指的软件(嵌入式C程序)和TestbenchSV程序)都可以看到的空间。一般来说Testbench可以看到所有的内容,而软件只能看到CPU地址空间(寄存器、SRAMROMDram、外部IO空间等)。共享空间需要的地址范围不算小(可能需要几十KB,所以一般是放在CPU可见的SRAMDram里),对于ISS会有所不同(后面会说明)。
& H( h$ R: m% Z* u2 P' c' ~6 g
公共函数的维护
: J, M2 q4 Y' t
项目上大家都可能使用到的函数即为“公共函数”。我个人认为最重要的是对CPU地址空间的访问(我们是xmr_read_memxmr_write_mem)。以及基于这几个函数(task)实现的文件存取等函数。

2 j: ~0 j9 e& [
在实现xmr_read_memxmr_write_mem task(function)的时候,主要模型数组的宽度会导致根据模型数组下标访问的地址的不同。比如,加入位宽是128bit,那么一个memory就对应着432bitword----- 各个项目会有所不同。
; q, Y& V* Y" E
另外,对于Dram的处理是最复杂的,尤其是Dram是支持bank地址和Row地址 remap的,所以要特别注意remap时候的 地址和bank信号、row地址信号的对应关系。-----这个工作是可以继承前面项目的。
3 K& x7 U- {* s  q
Xmr函数需要考虑“用SRAMC或者AXI_SLV_VIP替换DDRC”情况下的实现。
0 B; f0 b$ S( s# y& @

# n6 L  _8 ]/ q! {0 Y/ m
简单说一下vip_slave_write32函数的实现。 这个函数调的底层函数是:

; K% }9 B. G$ B; K8 @
env.axi_slave_subenv.do_write32(addr, data);

3 l* q4 t+ M+ q; u* M
但是该函数在tb其他组件可能看不到,但是program可以看到。所以在program里做一个函数来调底层的env.axi_slave_subenv.do_write32
  _& N+ F& W& A- }

3 P$ T5 ]  u! Q4 [
然后把program的这个do_write32DPI export出去。

' \, q# m1 s8 D* W2 |- B
) V+ }) @$ u) x+ b& b3 @" q' M
tb上维护一个xmr.cc程序,里面实现vip_slave_write32。在xmr_wr_mem32里调用的就是这个vip_slave_write32.
0 _& a/ X: a4 ^' R8 Q! n
( `% v6 g. S+ b* n
使用xmr_wr_mem32 xmr_rd_mem32可以比较容易的实现:

/ e. G" F- G& i8 R4 r
1)装入初始化程序 --- 一级bootloader要装入到rom中,应用程序要装入到dram中。使用xmr_wr_mem32可以直接以访问cpu地址的形式来写入程序。

& Y; z$ M% F: X' f  Q. g3 g4 v  C

/ I- @* X0 q- {4 [8 M- E
2)把激励数据灌入dram中,让被测模块从dram中取激励数据。或者从内存中读取成片的数据用来做比对(比如解码解完了一帧,从内存中一次性把整帧的yuv解压缩完的数据读出来)。

$ q1 ^3 [9 H% L8 f, Q9 O
这里可以使用上面装程序类似的方法来实现。也可以利用xmr_rd_mem32xmr_wr_mem32来实现一个通用的task

# z, m! L- r% m' U" {) M2 \
把指定文件写入到内存中作为激励数据:

( Y  G8 ?% r: i1 h
这里有一个小技巧,就是用fscanf来读取文件中的一行数据,然后判断字符串的长度,从而得到输入文件一行几个byte,然后根据一行几个byte来装入到dram中。
2 k9 E8 X% x8 q  U
- M, [. J/ i' C/ J% P5 K
Testbench顶层文件) ]: j/ l- O/ D; t/ }+ ~
我们基于mini-文件列表来做Testbench顶层文件,是为了加速编译速度。
8 H5 P* |( o6 S' j2 I. @
Testbench顶层最主要的是例化DUT的顶层。Emacs用户做集成很容易;我是VI用户,稍微麻烦一些,使用vi的替换功能也可以比较快的集成起来:

: _) y/ |1 z! C
1)把顶层模块的input output inout端口声明部分copy出来,把input-outpput-inout替换成“wire”,来实现信号的声明。个别信号,系统输入信号、系统reset信号需要改成reg,并产生reg信号的激励。 时钟的产生建议使用一个module来产生,目的是为了让代码简洁清晰一些。

) D! L" W/ u* s/ F' c

# C; z8 w7 d7 G1 ^+ _
2)Copy一份wire声明的部分,然后处理成DUT的集成。
' z+ q  y! {) \1 U  c" Y; H: @( E& L+ }; x
s/\s*\[\d\+:\d\+\]// 去掉位宽声明$ b9 F! m) o4 r+ T! A7 |
wire [1:0] A;
à wire A;

  l- s" O9 K; X: v" I
s/wire\s\+// 去掉wire声明
) a% e$ v9 f2 r; Y! Pwire A;
à A;
* ?' [7 Z- u+ X4 l
s/\(\w\+\)/.\1(\1)/ 产生集成/ R0 K' i2 l! F0 \, s4 P! j& m8 n1 _
A;
à .A(A);

# T; E: |' u1 u! Q3 {; A1 f
3)处理一下模块名声明例化和分号。
) ^3 s" m' x0 b1 M.A(A);
à .A(A),

" j2 y$ s2 ?7 j0 ^/ g) q

; Y/ F: [* |0 \1 X7 P) D8 z
给顶层信号加pullup pulldown,一般来说顶层信号都要加pulldown,个别信号需要加pullup.总之是不希望让TB引入X状态。如果不知道哪些加pulldown pullup,至少要对 测试模式输入pinTEST)、CPU Jtag口、初始化要读取的PAD状态或者标识PIN加入合适的pullup或者pulldown

3 p$ P/ a, C' w; B* `5 R8 y

4 {0 Q: ^2 W! U% E
例化interfaceprogram
  Q: r5 H) x, `  G* N3 Q

1 `! k0 @% d3 r/ j- A# w
Program通常就是简单例化SV的组件(比如VMM下的env),以及include每个testcase所不同的处理部分

1 L" P/ ^( M% o2 p# J

# N# m* M5 C/ i" B  G
/ w& c! n% E2 `! w  H) m
在每个test.sv里通常是实现随机变量的扩展类。

+ N/ d& L# y0 x! D  _/ G) z
要注意Program如果结束的话,那么仿真也会结束,所以注意控制program的结束时间。
5 m+ g5 w& n: ^# I. h

6 E6 _# w. a$ E9 O1 a
例化基本仿真模型。最主要的是Dram模型了。请注意,Dram模型的例化最好用define处理好,因为Dram有可能要做4bit 8bit 16bit等几种情况,不同大小、不同位宽的dram的地址信号宽度不同,外挂的片数也不同,这里集成的时候需要特别注意。
1 H2 q; a2 i$ S+ N  {  _
7 N2 n/ i- @+ o4 x
7 d3 T4 f6 K  j# k
Dump波形的实现机制:

9 ]# N  ?/ t# @5 Y% @
Dump波形的原则是“是否dump、修改dump的起始时间、修改dump的层次都不需要重新编译”。前两个要借助仿真的运行参数来控制,后一个使用verdipli

5 x* ]& _$ x8 D6 H7 x9 u
( A: i! k0 S. \( s1 s
通常Testbench顶层文件都比较复杂,建议多使用Include文件的方式维护,这样代码可读性较强。而且顶层文件里通常有比较多的ifdef-elsif-else-endif的编译结构,代码太复杂的话,可能有一些笔误造成的编译错误。
- }' D4 F, D+ E0 d0 e1 m5 @- G& `

2 P1 ]$ K& }: G2 P: b% n# D
Include前面准备好的公共函数文件和公共define文件。

! I$ D9 o3 l" x% I" t5 K) j
# X) }8 @, x' O" f/ z& K& w, b
程序初始化load代码。SoC项目需要嵌入式软件代码,包括一级bootDram里的应用程序。这两段程序代码都需要load到对应的存储介质中去。这个load工作可以使用基于xmr_write_mem函数构造的写文件函数比较简单的实现。------具体实现前面已经贴过了。

9 C/ J3 i" I2 w0 W& e& q
6 O/ j& ]! b, H; V, k( ?
至此,testbench顶层基本完成。
! J8 y( W4 _/ A+ \# @: w& X- K

; R. U% ?6 Q7 _0 Q初步debug设计和环境% F2 a6 h/ X0 A! `/ O, u% }) d
顶层testbench写好以后,编译通过后,dump整体波形,可以看一下各个模块端口上是否有高阻Z,有的话说明可能有漏接的内部信号,尤其是主总线上的各个master口和slave口的连接。

% i$ J0 G! g* X3 h! M, w
检查CPU PAD ROM控制器 SRAM控制器等初始化需要的基本模块是否有时钟和reset。如果没有的话,说明根据外部输入系统时钟和系统reset产生的基本模块的时钟和reset有问题。
, d+ l& K, H# c6 V
2 H# Z1 |: X, }& J
一级Bootloader! \) U4 W: ~( e2 r& V+ c. N
一级bootloader是为了做初始化的,系统实际使用的bootloader是比较复杂的,牵扯到外部存储介质上的参数搬运和配置。仿真用的一级bootloader要尽量的简单,因为一级bootloader所有的仿真都要用,这一步要是慢了会浪费时间。------- 当然,使用ISS的话,就不存在这个问题了,但是一样也要求初始化要尽量的快速。
. _' }5 d% t- N  F- p
我个人建议仿真bootloader里就只设计如下几个步骤:
, p* Q& H: x7 x% m
1)系统上电初始配置
9 C( \  z6 P9 z3 ]0 a0 ~
8 h2 k4 x  B/ X- G9 T4 ]8 m: N
2)初始化pll至目标频率(如果系统pll默认频率就是目标频率,那么这一步就省略)

2 L# E1 A( s+ x. ^: ~

3 I. @1 \6 d6 t0 Q' M5 `
3)配置核心模块时钟频率以及切时钟,对必要模块进行软件reset
( [* D$ _! q; }
7 a: I# i: I1 D5 [0 c. L
4)内存控制器初始化
: W8 z" e; S+ v* K
+ M0 c# d4 y- [9 V7 f
5)Remap到内存中准备执行内存中的应用程序。 ------ 一般汇编实现
$ o7 U* z  m) b7 h3 w3 W2 d$ Y

1 X- L7 P0 r* E

2 p1 Z+ Q3 D, K
最基本的函数是对CPU空间的访问处理函数

$ l8 ?& M8 Z9 l- E7 x; H( \* h
#define SETREG32(reg,val)  t0 J: k3 M" z1 j5 C
(*((volatile unsigned int *) (reg)) = ((unsigned int) (val)))

5 a# L( l9 H, J6 X! H9 f
#define GETREG32(reg)
; E* g' @7 K2 Z5 t: @( \(*((volatile unsigned int *) (reg)))
9 [2 }; L9 H5 V
reg是寄存器地址,val是要配置的数值。 Volatile保证直接操作到内存。

; J# d6 N/ N1 f8 U# H9 U7 g) Y+ ~应用程序代码9 X5 Z  V6 H+ D/ K, i3 @
应用程序代码里也要做一些初始化,主要是非核心模块的时钟配置以及非核心模块的软件复位操作。
% X- t. R4 M. k+ U7 J4 Y. [
如果使用ISS的话,由于没有一级bootloader,所以要把一级bootloader的代码功能在应用程序初始化中实现。

: ?9 E- P' R* g1 r3 D0 V
需要注意的是,使用ISS的时候,使能cache可能导致ISS行为异常。可以在cache使能的位置使用ifdef ISS。汇编代码中是:

: R7 a2 j: h/ F! [& D
4 h' r  G  K8 @# ^
IF
, o: B) ?% S3 R3 g! \, l$ I) r( EF: ARM_ISS)
7 o. l3 ?8 V+ N" G* {

3 h4 t# S5 x" f3 h3 kNOP

* |3 [! Q2 p, U( `$ X% p3 {9 P- p
7 f  n8 P8 A. a1 p  W! J4 N
NOP
" h+ i' z+ C7 M7 q
ELSE
$ {1 G! T& a& j: v
Cache-operation

4 R) a; w; D# O
ENDIF
$ G0 z! `3 @& R
( F5 O! s) ^. [" |, }: G
汇编代码中include define文件是:

/ _% |/ M+ X- E
0 V; f9 ?# _2 S" D; p
GET define.s
(注意不能顶头写!)

8 D' o4 P! l: s6 I1 H# M9 N

& q) W" e% F3 P. f! @- p3 ^4 }0 y
然后构造一个极简单的应用程序。一般就是访问一下ddrsram、寄存器和打印。

4 {6 i' }# w* p+ s; w

8 V' t$ P2 P/ f/ ^
endsim()是结束仿真函数,如果希望让软件控制什么时候结束仿真,那么就可以在软件中的合适位置调用该函数。 函数的实现是利用共享空间,软件写入到共享空间指定位置一个标志,然后svtbwhile(1)的去采样该标记就可以了。

8 z2 l& r; g, S' _3 I7 f; d( q

$ j# F; ?% d5 D
1 ?6 I7 ^0 `7 g3 h3 [6 E
实现嵌入式代码在仿真平台上的打印
软件代码里相对复杂一些的是“printf”的实现。重点是使用软件和testbench都能看得到的地方来存放要打印的内容。然后testbenchwhile1)的根据“打印使能”、“打印开始”、“打印结束”标志来把内容$write出来。
( J3 Y7 T3 K" h7 W* \) z8 q% U
软件和Testbench都可以看到Sram空间(一级bootloader用来做数据存放和堆栈的sram)。注意bootloaderscatter文件里不要让stack-top覆盖了这部分空间。

+ W/ ]0 r$ f3 _+ U+ I
Printf与实际Cprintf的实现机制是一样的,都是利用“不定个数参数的函数”(实现机理:因为参数是从右向左压栈,所以最开始的那个参数在最接近栈顶的位置,这个参数在栈中的位置编译器可以知道)。

) R/ n) S( g4 K+ G# i# _
; Z1 _9 C. e* m; i) O
Debug整体环境
至此,整个环境已经基本建立起来了。结合一级bootloader和简单的应用程序代码可以debug系统初始化流程和整体环境。通常这里会有一些集成、以及总线访问的小bug
3 k# A4 C# W1 e5 w2 w6 Z7 g
8 o4 M9 q* q+ M5 l  W8 g5 ]
ISS替换
为了加速编译和仿真速度,我们使用ISS来替换CPU-IPISS一些C程序代码。提前把这些代码编译成.so文件,然后编译的时候就不用编译ISS了,链接的时候link进来就可以用了。

: l5 u# S0 a+ H6 i6 T0 r: ]9 t
使用ISS的优势:

, Q. C: W; }6 \& ]
1)可以dummyCPU-IP的代码

9 a: b3 a" V- F
2)不需要一级bootloader

5 H8 r# K# g7 M& c5 `* |% l  @  ]1 i
3)执行软件很快

% ^# u5 A& s4 r. i5 T
4)Testcase依然可以基于嵌入式c程序来写

! r& Y0 f' S( Y% @7 E
5)模块级的testcase也可以用C实现
! d' z1 P: a& r" d1 r1 v

9 a8 @$ T; u/ j* O+ N. ^5 b$ @! H
ISS外面包一个AXIwrapper,把这个模块例化到testbench顶层。Forcecpudata总线的AXI口上(如果是Arm9的话,是AHB总线)。IO访问的task文件要includetestbench顶层中去。对于寄存器空间的IO访问,需要产生正常的时序;而对于内存空间的访问,可以调用前面介绍的xmr_wr_mem32xmr_rd_mem32函数来加速。

3 G% A! ]4 m: ^) m5 r) N  M
ISS可能和CPU-IP不是同一个类型的CPU,这里要注意编译软件代码的时候需要加—cpu的区分,甚至可能导致软件代码的编译器都不同。这些不同可以体现在run_sim脚本里。
: _- ?* s/ Y# r% T
& K' y/ V1 B  }" ~* g# Y
每个项目的CPU地址可见空间可能不同,需要注意ISS的空间配置文件的内容要根据项目的不同而不同。IO访问的task里的地址访问也会有所不同。
& h9 j6 a% D! n* i! S
* T  x* |; l7 s; r
ISS下共享空间与实际CPU_IP不同,像实现打印这种功能,可以不必使用CPU地址空间。这是因为ISSwrappertestbench的一部分,可以直接在testbench上实现一个大数组来作为“共享空间”,这样更简单直观。

+ N/ i3 \1 o/ Q  G! G
5 x8 ]7 K" l5 c' i6 f% A1 k
DDRC的替换
系统起来以后,我们可能需要替换掉DDRC。一般有两种情况:

1 u" p: }, i% m; y
1)使用SLAVE_VIP替换DDRC,目的是为了随机控制slavelatency。实现模块访存的异常情况。------ 一般要结合ISS使用,因为没必要把应用代码初始化到slave-vip中。Slave-vip的读写可能会比较慢,对于大数据量的写入行为,仿真可以明显感觉到停顿。
1 C4 }" G* i' G; j
把内部端口和slave-vip对应上:建议使用macro,方便阅读和简化代码。
5 s! n6 c# E# y- x  t1 B* [

. Z# Y; y- S2 M  B  @; f% Q
2)使用一个更为简单的SlaveSRAMC)来替换DDRC。目的是为了快速初始化(不用配寄存器做初始化),加快编译和仿真速度。
+ F. C: n8 \: w+ a* _# ~% i/ b, A
SRAMC不是class,而是一个module。把它例化在顶层TB里,与上面的Slave-VIP一样也需要和内部端口对应上。
5 z4 Q. v* `5 W* Y) e8 x. y# P5 b

8 I' C! a8 u5 x& b/ O- P
SLAVE_VIPSRAMC都是参数化设计,可以方便的修改数据宽度等信息。

' N- j: x! p7 ]+ W, x( x
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
收藏收藏 支持!支持! 反对!反对!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

巢课

技术风云榜

关于我们|手机版|EDA365 ( 粤ICP备18020198号 )

GMT+8, 2025-4-9 20:39 , Processed in 0.079676 second(s), 32 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表