找回密码
 注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

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

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

[复制链接]

551

主题

1470

帖子

3万

积分

EDA365管理团队

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

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

EDA365欢迎您!

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

x
(信息来源:EETOPBBS     作者:lshj98115)( O0 u' |, {% ~0 o) s
先啰嗦几句。其实老早就想写这个帖子,自己犯懒一直木有写。 前阵子写了一个初版,然后发给了几个做验证的朋友看了看,普遍反映没看明白 . 说是我写的东西和我搭的环境结合的太过紧密了,不结合代码,理解的不透彻。可惜代码是公司的,我不能把代码发出来。 我后来写了一个带很多代码截屏的版本,但是很抱歉没法发到论坛上来。
$ D) H, j7 v4 B. p( g* D8 Z8 H/ ^) f3 F" D6 \2 c+ _- e
我个人觉得下面的文字已经能表达我的想法和思想了,希望能对帖的有一点帮助吧/ L+ D2 b" ^- c' O0 N& N+ d& s; a
2 G6 b- W" O$ m
---------------------------------------------
* s- i' _; A1 p; D* C
写这个文档的目的是让大家对搭建SoC项目的Testbench有一个比较清晰的认识,可以根据这个文档来一步一步的搭建起一个SoC项目的基本的testbench。本文档重点是指导大家搭建基本环境,以及能解决搭建Testbench过程中容易遗漏的问题或者容易遇到的“地雷”。
' K- F$ E8 {. i. {5 O

. f2 l0 [8 S, F7 g& k
我搭的SoC项目的testbench会有一些相对特殊的点:
' e" f$ t/ H1 {) Z5 d0 }
1)要有嵌入式的软件。这里包括两部分,一是初始化的bootloader(一般是固化在rom或者存放在外部的flash里),一是boot起来以后放在外部易失性存储介质上的应用层的程序。

4 I0 t  K) E" i. q
2)正常启动起来(一级boot可以切到应用程序了)以后,为了简化流程,我们要使用ISS的环境。 --- 这是比较特殊的一个点
* f8 L$ Z/ ~( ~1 p- o
3)环境主要脚本的维护和修改。主要是单个仿真和批量仿真(regression)核心脚本
* `: k* {1 H! b' N: D
4)为了优化仿真和编译速度,我们要能把不用的模块dummy掉。

8 V- y4 o  b/ k2 U' g7 u
5)文件列表的处理

5 A- ]5 r* _. i8 \3 J* O* p
6)SoC软件与Testbench都能访问的“共享空间”的处理
: M$ b% S; K1 w6 l3 i
7)公用函数的准备,比如根据CPU看到的地址空间直接访问外部DRAM的数组,进行初始化写、数据写和数据读操作。

/ J/ }% A* L. A
8)环境变量的维护。
8 z/ ^) n0 K4 ~5 A* ]
9)Define文件的维护
2 d9 @+ H1 [1 ?$ Y1 M9 C  d
10DDRC的替换(一个是AXI_SLV_VIP的替换,一个是简单AXI_SLV模型的替换)
0 P5 x' L# Y7 t2 }3 D% x
) A" m  Q: }% f
磨刀不误砍柴工,把需要的东西提前准备好,搭建Testbench就像搭积木一样简单快速了。

+ q4 Y7 u. \% j

: i  d5 s! {+ x& T- ?5 o环境变量维护
3 |  e* A3 c+ I
使用module工具来维护整个项目的环境变量。目的是为了让项目上的工程师都使用统一的环境(主要是工具版本和环境变量)。
4 a4 U8 C( \- V+ W4 w
核心脚本的维护4 g/ \: ^( |1 O$ u
两个脚本:run_sim regress run_sim负责提交单个仿真任务,regress负责提交批量仿真任务。两个脚本已经使用了很多项目了,脚本的具体说明我以后专门开专题讲。在这里只提醒一下,run_sim脚本通常需要根据不同的项目做微小的改变。

) L* v! K8 s+ Y( t$ Q
run_simregress都是比较大的perl脚本程序,大致描述一下功能。

- J, f8 }$ n8 x6 _/ v9 g2 p% l5 ]
run_sim脚本功能
8 S# p3 R9 ]7 ~; O6 I% b1 O
1)为每个仿真产生仿真目录。仿真的目录里应该包括文件列表(硬件和软件)、编译和仿真命令(注意包括嵌入式软件的MakeFile)、提前建立需要的子目录、和单个仿真对应的文件链接(比如维护的C的测试主函数、扩展的随机类的SV文件、一级bootloader文件的链接)、define文件、本仿真的重构命令(这是一个容易忽略的,一旦你跑regression的时候某个仿真失败,你又不想在出错的目录下重新仿真,用这个重构命令文件就可以直接提交)。

  Q  _- B9 n1 U- {8 [6 ?
2)各种option的维护。比如不同仿真需要不同的define、编译和运行optiondump波形的scope以及层次

) t; l3 b" w8 b& b/ r, {- f" l6 I  _
regress脚本功能
9 Y2 \; K# J* i+ V( s8 L
regress脚本比较简单,要吃一个由很多run_sim仿真命令组成的命令集文件。用regress脚本把这些仿真命令提交到工作站上去。需要注意的是:有时候可能会有一些公共的option或者define,比如打开coverage收集、某个define要应用到整个regression里。所以regress脚本要能支持对所有run_sim命令添加option的功能。

' u+ l& e* G% p6 L产生dummy文件
- i  h' P. e- C* {2 x, M% J
使用gen_dummy_file脚本来产生dummy文件。设计工程师可能也要维护一个module_dummy.v的文件用于做integeration,验证工程师产生的dummy文件记得名字不要和设计自己维护的文件重复了。
, r* m: ?2 E/ i' m- p: {
为什么不使用设计维护的文件?因为一个是设计维护的文件在integration以后很可能就不再维护了;另一个是设计维护的文件可能output全是assign0的,但是对于模块输出的pready\CEN等信号最好assign1,否则可能导致问题(例如:sram使能信号CEN赋成0,可能导致后面的sram模型认为有读写行为;pready信号赋成0,可能导致SoC软件跑起来的时候对该模块寄存器操作的时候挂死apb总线)。

" R1 K# {1 ]/ Y  r  P8 H5 z
这个脚本并不好写。因为verilog语法支持的模块声明实在是太多了,导致脚本很容易顾此失彼。举例来说几个复杂的地方:

5 g6 b( o3 T' W8 A. V" O/ _8 T
module声明后面可以跟parameter的就很复杂
+ l' C0 T( @  N
Module test #(parameter a = 1,Parameter b = 2,C = 3,D = 4 );

! D2 _" T! A! f8 b
这些parameter很可能要用在端口位宽的声明里。更为麻烦的是parameter里可能会有function的使用。而function有可能是以define的形式写到代码中。这样就很难用parse RTL的方法来解决。

5 ~8 d( P6 A$ L& B6 e, u
再比如: 端口声明里出现ifdef else endif这种编译宏的处理也比较麻烦。
* P9 Z7 R/ W, D% O# Y

) Z( H/ ^+ ^! i' M# e9 C
也可以使用simulator或者debug工具提供的用户接口来编写tcl程序来获取各个端口的namewidth信息。但是不同仿真(define不同)可能导致端口宽度和端口不一致,结果要针对不同define来维护不同的dummy也比较麻烦。
! f5 r, Z+ f: s. \3 J/ k
) m  Q5 H: t/ Y6 [( n
总之,产生dummy文件以后一定要记得检查一下。Dummy文件可以有效缩短编译的时间。
4 D0 I5 p9 o  A
文件列表处理的维护. z  K$ n- ], G, [- R/ e5 i" j# x
上述几个事情是应该提前准备好的,接下来我们要开始编译RTL了。Integration好的文件列表,首先要先编译该文件列表。有可能遇到的问题是加密文件的种类,有可能文件列表里的加密文件和你用的仿真器不一致。然后结合前面产生好的dummy文件,我们要处理出一个简化设计的mini-文件列表,一般里面只包括初始化必须的模块(ClkrstPADCPU、总线拓扑、内存控制器),也就是把video系统、外围接口、存储系统这些模块统统dummy掉。

& ]2 h$ c6 A/ z9 l; H: t# J! r3 v
: h: T8 S6 p7 K8 {( n, x0 d8 v
产生mini-文件列表
! |  A1 Q6 ^( Q6 i4 I

  Z2 I, |; X4 R% [+ ]2 b# X9 k
可以用脚本来维护一个配置文件,在该配置文件中指明如何删改原有文件列表。

5 ]% f# @% Z3 o+ `( N; g' G
+ {  s2 K3 g+ G3 U  d) _! w: i# i$ t0 ^- n$ a0 x- z0 y
, _2 F* `: t0 Z$ o& T
注意最终使用的文件列表里的文件路径应该是绝对路径

& s: k7 ^# u$ v6 a  k8 N6 M' ?
使用绝对路径的好处在于可以让run_sim脚本指定仿真在任何目录下进行(比如regression要提交到别的硬盘上去跑,那么就必须使用绝对路径了)。

+ ~# L. O# H. F. q* H, c2 K9 s; X
注意绝对路径里不要用$macro的结构,别人有可能用你的文件列表跑仿真或者debug,而别人的$macro很可能与你的不同,导致出问题。. E& Z- `% `* J! v0 h- l: _
  U& O; ]" c( e, W* n. R
文件列表的产生有一个地方需要注意:

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

7 p9 x; \# x+ O# S
`ifdef FPGA1
8 v  g/ k7 J% v( e8 m1 Q( T
# J0 r2 l  L; k& e, w- _- e. _
module v_fpga1 (
$ D' m! D, p$ b+ M1 o/ @
`elsif FPGA2
7 b/ w" Q: e  ^% Z
9 P% \; R7 @* P4 I. |
module v_fpga2 (
2 I+ f5 E. `$ q/ R1 Z
`elsif FPGA3
0 x  V: I+ b0 d$ t9 s

* u) {' m+ _7 smodule v_fpga3 (

. F5 y) ~. {) x% ^' z5 p2 q
`else
: u6 M0 X" x4 r# c' }  d

% d' z  G+ [! D3 N- _2 nmodule v (

6 z& k! e5 b( t4 g
`endif
0 h( x6 u6 x% ^1 E9 a

: `( _( Y* ]) E, Y" H6 F3 {, O. o
0 l  p( E; \$ K3 o2 K! S' }
那么在文件列表里就会是下面这种结构:

% T, ?6 t! z9 @
fpga1_def.v

) }7 @' F0 \# J9 K$ p) ?
a.v

% V3 y6 k3 ~. @" O. A4 _+ H
fpga1_undef.v
: V6 b4 j9 U/ }; F, M, X, b" k
fpga2_def.v

5 j5 a9 K, B$ M! Q$ u0 b
a.v

. t7 l3 g$ N5 r9 j3 I5 S! l3 S
fpga2_undef.v

$ z" o4 |. y, K  k' b
……

. i( M" I2 ~* T" {1 D

0 F* i0 Z$ k0 E/ \4 z
请注意文件列表处理脚本,有可能会有“去重”的处理。这个时候要去掉文件列表处理脚本的“去重”功能。
& {6 a4 ?* O$ y
编译过mini-文件列表以后就可以开始真正的准备写testbench顶层模块了。

+ S1 ~; Q0 e3 e4 l
! B+ W" ]3 ~! ^  `; m( s
Define文件的维护
* [; `+ [! G5 y0 ~. y9 p
我们在搭建testbench过程中的interfaceenvsvtb等可能需要xmrcross module reference)访问信号,这时候维护一个公共的define文件很重要。该define文件中应该包括
# A. `- h9 ~6 Z' P5 ~% M3 ^
1)各个主要模块的xmr路径define ,记得按照ASIC/ FPGA/模块级 来分别区分define
8 ?2 |2 B0 t# t  B8 M8 i' R
2)地址空间上模型数组的路径。比如dram模型里数组的xmr路径、srammemory数组的xmr路径
( i+ e2 ]/ ^" }8 o/ h* O
3)共享空间的部分地址的define,比如我们的软件打印的实现所用到的共享空间的define
/ H3 f/ E2 v6 O3 T+ A* F
4)Dram基本define

# G7 n# M: t) M% Q1 i

( a+ r/ e. x/ u! {
共享空间
4 n8 g4 N, r6 `4 n& D3 W
SoC项目Testbench中的“共享空间”,是指的软件(嵌入式C程序)和TestbenchSV程序)都可以看到的空间。一般来说Testbench可以看到所有的内容,而软件只能看到CPU地址空间(寄存器、SRAMROMDram、外部IO空间等)。共享空间需要的地址范围不算小(可能需要几十KB,所以一般是放在CPU可见的SRAMDram里),对于ISS会有所不同(后面会说明)。
& W6 {5 U) _. P* [- \. T) h
公共函数的维护
) G1 K! L2 F2 X
项目上大家都可能使用到的函数即为“公共函数”。我个人认为最重要的是对CPU地址空间的访问(我们是xmr_read_memxmr_write_mem)。以及基于这几个函数(task)实现的文件存取等函数。

* t; q' v; X( X- `
在实现xmr_read_memxmr_write_mem task(function)的时候,主要模型数组的宽度会导致根据模型数组下标访问的地址的不同。比如,加入位宽是128bit,那么一个memory就对应着432bitword----- 各个项目会有所不同。
& @7 ]) ?0 W" q/ I9 E
另外,对于Dram的处理是最复杂的,尤其是Dram是支持bank地址和Row地址 remap的,所以要特别注意remap时候的 地址和bank信号、row地址信号的对应关系。-----这个工作是可以继承前面项目的。

) J4 a8 J6 n' d2 {
Xmr函数需要考虑“用SRAMC或者AXI_SLV_VIP替换DDRC”情况下的实现。

) k, V$ y3 t$ `

( d1 g9 ]& [+ e3 r3 Y
简单说一下vip_slave_write32函数的实现。 这个函数调的底层函数是:

. f. [2 T! r! l6 n- ]# U
env.axi_slave_subenv.do_write32(addr, data);
: n9 Q/ Q; W, f0 R) i- ~' r
但是该函数在tb其他组件可能看不到,但是program可以看到。所以在program里做一个函数来调底层的env.axi_slave_subenv.do_write32
  E" `5 L* `  l$ Z  h7 N3 @" c
8 \8 x, |# l5 N. G6 z6 W7 C6 s
然后把program的这个do_write32DPI export出去。
* L+ X# i  t- A" a7 g
! U- t& a+ ]7 q2 [/ P
tb上维护一个xmr.cc程序,里面实现vip_slave_write32。在xmr_wr_mem32里调用的就是这个vip_slave_write32.

, h9 ^5 J# l" ]8 c

# v  V/ s6 b1 T3 m# m  N, {, h
使用xmr_wr_mem32 xmr_rd_mem32可以比较容易的实现:

; q% A; h" Z$ c- F5 k
1)装入初始化程序 --- 一级bootloader要装入到rom中,应用程序要装入到dram中。使用xmr_wr_mem32可以直接以访问cpu地址的形式来写入程序。

7 a/ f  u2 u2 Y
( f2 X1 p/ d, w8 B
2)把激励数据灌入dram中,让被测模块从dram中取激励数据。或者从内存中读取成片的数据用来做比对(比如解码解完了一帧,从内存中一次性把整帧的yuv解压缩完的数据读出来)。
6 y5 W5 S6 ~7 C' ?, M, B5 F
这里可以使用上面装程序类似的方法来实现。也可以利用xmr_rd_mem32xmr_wr_mem32来实现一个通用的task

5 T& {3 c1 b5 p/ D' w  D
把指定文件写入到内存中作为激励数据:
3 h! F2 _/ L# J
这里有一个小技巧,就是用fscanf来读取文件中的一行数据,然后判断字符串的长度,从而得到输入文件一行几个byte,然后根据一行几个byte来装入到dram中。
1 T6 ^( R. K1 w* k2 V& V3 K8 q

# i  q1 B5 O+ ATestbench顶层文件
% ^2 Z  T/ g: Q7 I, ?' w
我们基于mini-文件列表来做Testbench顶层文件,是为了加速编译速度。

' L0 Z5 O8 ^0 T) F1 g- ~4 O' S
Testbench顶层最主要的是例化DUT的顶层。Emacs用户做集成很容易;我是VI用户,稍微麻烦一些,使用vi的替换功能也可以比较快的集成起来:

6 p' b  Y2 g" p4 O' I% @' V. d8 V
1)把顶层模块的input output inout端口声明部分copy出来,把input-outpput-inout替换成“wire”,来实现信号的声明。个别信号,系统输入信号、系统reset信号需要改成reg,并产生reg信号的激励。 时钟的产生建议使用一个module来产生,目的是为了让代码简洁清晰一些。

  q' Y: \) q: z4 G
! t3 Y9 p, _$ {+ Z
2)Copy一份wire声明的部分,然后处理成DUT的集成。

' q- u  m& E) w3 B8 a# u: f
s/\s*\[\d\+:\d\+\]// 去掉位宽声明
3 @& m4 }' f/ }* p) ?/ owire [1:0] A;
à wire A;
4 |& {  g1 [3 H5 M" O3 G
s/wire\s\+// 去掉wire声明
5 ~7 q" ~( W5 m% Xwire A;
à A;

% O# p: \* r7 ]9 R
s/\(\w\+\)/.\1(\1)/ 产生集成' n) j9 b1 v, z/ E* y
A;
à .A(A);
' X% j" p( I( j  v4 L
3)处理一下模块名声明例化和分号。% W& R# h5 e  g0 n3 Q; S0 w
.A(A);
à .A(A),

- c) e. i  e9 y1 k5 L; ~
) w. G' i* _) I2 M" Y
给顶层信号加pullup pulldown,一般来说顶层信号都要加pulldown,个别信号需要加pullup.总之是不希望让TB引入X状态。如果不知道哪些加pulldown pullup,至少要对 测试模式输入pinTEST)、CPU Jtag口、初始化要读取的PAD状态或者标识PIN加入合适的pullup或者pulldown

* a; w3 i8 j" q3 g2 j% N( o
' K0 L: V8 s# M. P# a( F0 o- t
例化interfaceprogram
) L. O( U: F6 H7 _
5 i8 t; P$ Y5 G7 ^$ p
Program通常就是简单例化SV的组件(比如VMM下的env),以及include每个testcase所不同的处理部分
% {; |* q7 u  e! T: a
% k! Z' S4 E/ J: ]

) B% K. F# T6 h+ U
在每个test.sv里通常是实现随机变量的扩展类。
# E$ `6 J( }" |' ~
要注意Program如果结束的话,那么仿真也会结束,所以注意控制program的结束时间。
7 d  v! U+ e7 ?- q

) {) a2 Y4 ^5 L* V- x# a7 K
例化基本仿真模型。最主要的是Dram模型了。请注意,Dram模型的例化最好用define处理好,因为Dram有可能要做4bit 8bit 16bit等几种情况,不同大小、不同位宽的dram的地址信号宽度不同,外挂的片数也不同,这里集成的时候需要特别注意。
+ d' T/ l( Y5 _* }# G) f1 M- d

% U6 Z* @. E9 l9 K' e( o) J
+ D5 z9 ~9 R2 F6 u: Z
Dump波形的实现机制:

6 \/ c7 g) J9 f8 a% d9 g+ N
Dump波形的原则是“是否dump、修改dump的起始时间、修改dump的层次都不需要重新编译”。前两个要借助仿真的运行参数来控制,后一个使用verdipli

: P: p5 [  A( o$ B+ H: x
- G" ^# t/ v9 l5 L+ ]# V
通常Testbench顶层文件都比较复杂,建议多使用Include文件的方式维护,这样代码可读性较强。而且顶层文件里通常有比较多的ifdef-elsif-else-endif的编译结构,代码太复杂的话,可能有一些笔误造成的编译错误。

; Q" b" B" u5 N

9 i* ?8 M8 W. n0 S' H. Y
Include前面准备好的公共函数文件和公共define文件。

0 t6 q# P! z6 p- U

, E. p( U5 {9 z$ |
程序初始化load代码。SoC项目需要嵌入式软件代码,包括一级bootDram里的应用程序。这两段程序代码都需要load到对应的存储介质中去。这个load工作可以使用基于xmr_write_mem函数构造的写文件函数比较简单的实现。------具体实现前面已经贴过了。

+ Z1 D$ [6 C' g. E" U

3 o- ^* N7 z+ Y* F9 l
至此,testbench顶层基本完成。

7 G  P: p9 Y( ?

- Q+ o4 [3 F/ p# F6 c% y. i初步debug设计和环境$ g+ y: E2 o! L. o5 _3 r
顶层testbench写好以后,编译通过后,dump整体波形,可以看一下各个模块端口上是否有高阻Z,有的话说明可能有漏接的内部信号,尤其是主总线上的各个master口和slave口的连接。

& X; D# ?. L' }. i  o- L
检查CPU PAD ROM控制器 SRAM控制器等初始化需要的基本模块是否有时钟和reset。如果没有的话,说明根据外部输入系统时钟和系统reset产生的基本模块的时钟和reset有问题。
, T8 _) X) w% U: D

( p2 g5 B  M5 j( f一级Bootloader/ u$ B1 x8 d! W% U: i% Y4 v4 K
一级bootloader是为了做初始化的,系统实际使用的bootloader是比较复杂的,牵扯到外部存储介质上的参数搬运和配置。仿真用的一级bootloader要尽量的简单,因为一级bootloader所有的仿真都要用,这一步要是慢了会浪费时间。------- 当然,使用ISS的话,就不存在这个问题了,但是一样也要求初始化要尽量的快速。

9 n1 |; [8 t! O
我个人建议仿真bootloader里就只设计如下几个步骤:

, H! d0 n6 A  H* l# G  o8 f
1)系统上电初始配置

' V. u8 ~0 s+ K5 n' ]" K' P2 _. N
* x+ ]& _. {6 X' o7 ~4 X
2)初始化pll至目标频率(如果系统pll默认频率就是目标频率,那么这一步就省略)

- {. S$ _+ A1 B* R5 V
1 B& o: M' _3 t: C* y
3)配置核心模块时钟频率以及切时钟,对必要模块进行软件reset

) g. F. N' R# Y6 n$ \/ X

) p: x6 j6 W, e# Z- U
4)内存控制器初始化
  V% c7 B$ u) n9 ~: S
/ I% s$ X5 T  p1 R3 m4 V4 U( w
5)Remap到内存中准备执行内存中的应用程序。 ------ 一般汇编实现
+ q9 C) d( V" F# L2 }9 ?3 U: I

. {# V" n* Z7 E# n  w8 O

: a0 r' F8 E2 z
最基本的函数是对CPU空间的访问处理函数

  Y; i; U  G$ U! q% N& t
#define SETREG32(reg,val)
4 o3 h7 y2 L5 w. @% l(*((volatile unsigned int *) (reg)) = ((unsigned int) (val)))

# \2 q/ o- d1 Z6 C' p6 i6 V
#define GETREG32(reg)' r, b1 W/ [- |5 g' M4 e+ O0 U
(*((volatile unsigned int *) (reg)))

% W& i) Z4 }/ l, M2 F% ]
reg是寄存器地址,val是要配置的数值。 Volatile保证直接操作到内存。

1 |9 C2 [! M2 Y% u5 j4 Y  r0 `应用程序代码
* ^3 X  Z* b! O  L* O3 V
应用程序代码里也要做一些初始化,主要是非核心模块的时钟配置以及非核心模块的软件复位操作。
, I# e7 A2 x/ K4 r& C* H
如果使用ISS的话,由于没有一级bootloader,所以要把一级bootloader的代码功能在应用程序初始化中实现。

( }3 f; |9 F/ G2 ^1 ?$ e
需要注意的是,使用ISS的时候,使能cache可能导致ISS行为异常。可以在cache使能的位置使用ifdef ISS。汇编代码中是:

# f& `$ K7 Y" u! m' v  Q5 w0 I

7 B# M. N1 o6 \6 M8 m- yIF
- U' p  L1 T4 _: ?7 N  {8 ^( EF: ARM_ISS)

$ y  f% W, r0 `: d9 y
% h! {6 ?/ i+ v' F
NOP

! p  i0 P% v9 v; W2 r0 J  D
& P! s0 R* W% A. P/ [6 _+ Y
NOP

1 W; K& f! m/ l
ELSE
0 A! {4 u$ \- z7 |- K; S1 M3 G
Cache-operation
2 L* o7 p; [; x0 v
ENDIF

$ A3 }2 {6 h- k
: ]& G  U* X( C+ a: L4 {- P
汇编代码中include define文件是:
/ m# |0 u8 k( M& }8 A; N+ }& a* D3 F

. \4 v/ v% Y  D9 w) }* }4 jGET define.s
(注意不能顶头写!)
# M, @; w# K1 `
9 t6 g) k1 l4 Y4 q" N; R
然后构造一个极简单的应用程序。一般就是访问一下ddrsram、寄存器和打印。

& S( W1 W: z7 I5 i" \1 W0 P: e0 ^

7 \$ T' s) W, N6 r. C, P1 `$ E- N
endsim()是结束仿真函数,如果希望让软件控制什么时候结束仿真,那么就可以在软件中的合适位置调用该函数。 函数的实现是利用共享空间,软件写入到共享空间指定位置一个标志,然后svtbwhile(1)的去采样该标记就可以了。

$ J: x) _4 U9 A6 \

  G: ]. T6 ^. h3 R' c/ b7 e+ e

; ]% q) X, C+ O, l) k' Y: H' x实现嵌入式代码在仿真平台上的打印
软件代码里相对复杂一些的是“printf”的实现。重点是使用软件和testbench都能看得到的地方来存放要打印的内容。然后testbenchwhile1)的根据“打印使能”、“打印开始”、“打印结束”标志来把内容$write出来。

+ S* e! s" g) |# K8 F8 w5 K
软件和Testbench都可以看到Sram空间(一级bootloader用来做数据存放和堆栈的sram)。注意bootloaderscatter文件里不要让stack-top覆盖了这部分空间。
+ I+ g$ C( q& \8 _  B1 `
Printf与实际Cprintf的实现机制是一样的,都是利用“不定个数参数的函数”(实现机理:因为参数是从右向左压栈,所以最开始的那个参数在最接近栈顶的位置,这个参数在栈中的位置编译器可以知道)。

5 n2 g! p4 A$ j# d" g' M5 J

. q  T% |, H0 J1 ?Debug整体环境
至此,整个环境已经基本建立起来了。结合一级bootloader和简单的应用程序代码可以debug系统初始化流程和整体环境。通常这里会有一些集成、以及总线访问的小bug

* ?! W5 W6 H: w2 l* n* s
( x4 X6 m9 O" B3 }
ISS替换
为了加速编译和仿真速度,我们使用ISS来替换CPU-IPISS一些C程序代码。提前把这些代码编译成.so文件,然后编译的时候就不用编译ISS了,链接的时候link进来就可以用了。

; c1 z4 ?2 r3 x& u0 W5 g
使用ISS的优势:
) o) A! W4 \" ~: K& n1 e
1)可以dummyCPU-IP的代码
5 \$ J  A( _6 k( x7 b! h( H* o
2)不需要一级bootloader
( @" }  o8 \3 m% L. f
3)执行软件很快

' q' Y( n3 f# Z6 K9 Y* U8 g
4)Testcase依然可以基于嵌入式c程序来写
5 e+ N% X2 P* D6 Y" q2 d0 Y" z
5)模块级的testcase也可以用C实现
9 o% z. g2 M6 A. ~- d
9 U. H& D9 J3 i1 @. r) W9 ?' |7 d
ISS外面包一个AXIwrapper,把这个模块例化到testbench顶层。Forcecpudata总线的AXI口上(如果是Arm9的话,是AHB总线)。IO访问的task文件要includetestbench顶层中去。对于寄存器空间的IO访问,需要产生正常的时序;而对于内存空间的访问,可以调用前面介绍的xmr_wr_mem32xmr_rd_mem32函数来加速。
8 `1 G6 l% [8 L2 ?4 G" S/ a
ISS可能和CPU-IP不是同一个类型的CPU,这里要注意编译软件代码的时候需要加—cpu的区分,甚至可能导致软件代码的编译器都不同。这些不同可以体现在run_sim脚本里。

$ N" ~+ ^& Y* b$ g

, F  P/ y& K7 o5 q1 v6 s
每个项目的CPU地址可见空间可能不同,需要注意ISS的空间配置文件的内容要根据项目的不同而不同。IO访问的task里的地址访问也会有所不同。

8 t2 r% P+ a: X( {5 j3 f% u
# A2 Q/ z* a3 u7 [' H+ k
ISS下共享空间与实际CPU_IP不同,像实现打印这种功能,可以不必使用CPU地址空间。这是因为ISSwrappertestbench的一部分,可以直接在testbench上实现一个大数组来作为“共享空间”,这样更简单直观。
: G9 b2 Z+ z! \9 A; \

9 B$ }5 M( P% n3 Y* lDDRC的替换
系统起来以后,我们可能需要替换掉DDRC。一般有两种情况:
# j: Y8 G6 D5 I& X) o. J
1)使用SLAVE_VIP替换DDRC,目的是为了随机控制slavelatency。实现模块访存的异常情况。------ 一般要结合ISS使用,因为没必要把应用代码初始化到slave-vip中。Slave-vip的读写可能会比较慢,对于大数据量的写入行为,仿真可以明显感觉到停顿。

: r) D/ C) z- t7 g9 G$ x
把内部端口和slave-vip对应上:建议使用macro,方便阅读和简化代码。
8 }5 [* v6 e9 C# o. z

- r0 [5 _6 h; K- x3 S3 n
2)使用一个更为简单的SlaveSRAMC)来替换DDRC。目的是为了快速初始化(不用配寄存器做初始化),加快编译和仿真速度。
* M1 F6 x: n3 H- G
SRAMC不是class,而是一个module。把它例化在顶层TB里,与上面的Slave-VIP一样也需要和内部端口对应上。
( ]3 b: c4 t# U+ ~6 [' q8 [; Y+ E
( |/ _$ W! U  [0 v7 M5 c6 ?
SLAVE_VIPSRAMC都是参数化设计,可以方便的修改数据宽度等信息。

& `: a2 l9 ?) H$ ?" m# F/ z0 V3 Z! `
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
收藏收藏 支持!支持! 反对!反对!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

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

巢课

技术风云榜

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

GMT+8, 2025-4-7 09:47 , Processed in 0.078571 second(s), 32 queries , Gzip On.

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

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

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