small5的个人空间 https://blog.eetop.cn/nick [收藏] [复制] [分享] [RSS]

空间首页 动态 记录 日志 相册 主题 分享 留言板 个人资料

日志

从0到1,搭建SOC的testbench (part 1)

已有 2033 次阅读| 2016-11-1 21:37 |系统分类:芯片设计

很多童鞋接触到的testbench通常继承于之前的项目,往往有改动也不过是修修补补,反而对于testbench自底至上的搭建认识不多。读完这篇,妈妈再也不用担心了你的testbench了。


写这个文档的目的是让大家对搭建SoC项目的Testbench有一个比较清晰的认识,可以根据这个文档来一步一步的搭建起一个SoC项目的基本的testbench。

本文档重点是指导大家搭建基本环境,以及能解决搭建Testbench过程中容易遗漏的问题或者容易遇到的“地雷”。


我搭的SoC项目的testbench会有一些相对特殊的点:

  1. 要有嵌入式的软件。这里包括两部分,一是初始化的bootloader(一般是固化在rom或者存放在外部的flash里),一是boot起来以后放在外部易失性存储介质上的应用层的程序。

  2. 正常启动起来(一级boot可以切到应用程序了)以后,为了简化流程,我们要使用ISS的环境。 --- 这是比较特殊的一个点

  3. 环境主要脚本的维护和修改。主要是单个仿真和批量仿真(regression)核心脚本。

  4. 为了优化仿真和编译速度,我们要能把不用的模块dummy掉。 5 文件列表的处理。

  5. SoC软件与Testbench都能访问的“共享空间”的处理。

  6. 公用函数的准备,比如根据cpu看到的地址空间直接访问外部DRAM的数组,进行初始化写、数据写和数据读操作。

  7. 环境变量的维护。

  8. Define文件的维护。

  9. DDRC的替换(一个是AXI_SLV_VIP的替换,一个是简单AXI_SLV模型的替换)。


磨刀不误砍柴工,把需要的东西提前准备好,搭建Testbench就像搭积木一样简单快速了。


  • 环境变量维护

使用module工具来维护整个项目的环境变量。目的是为了让项目上的工程师都使用统一的环境(主要是工具版本和环境变量)。


  • 核心脚本的维护

两个脚本:run_sim 和regress。

run_sim负责提交单个仿真任务,regress负责提交批量仿真任务。

两个脚本已经使用了很多项目了,脚本的具体说明我以后专门开专题讲。

在这里只提醒一下,run_sim脚本通常需要根据不同的项目做微小的改变。

run_sim和regress都是比较大的perl脚本程序,大致描述一下功能。

  • run_sim脚本功能:

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

  2. 各种option的维护。比如不同仿真需要不同的define、编译和运行option、dump波形的scope以及层次

  • regress脚本功能:

    regress脚本比较简单,要吃一个由很多run_sim仿真命令组成的命令集文件。用regress脚本把这些仿真命令提交到工作站上去。需要注意的是:有时候可能会有一些公共的option或者define,比如打开coverage收集、某个define要应用到整个regression里。所以regress脚本要能支持对所有run_sim命令添加option的功能。


  • 产生dummy文件

使用gen_dummy_file脚本来产生dummy文件。设计工程师可能也要维护一个module_dummy.v的文件用于做integeration,验证工程师产生的dummy文件记得名字不要和设计自己维护的文件重复了。
为什么不使用设计维护的文件?因为一个是设计维护的文件在integration以后很可能就不再维护了;另一个是设计维护的文件可能output全是assign成0的,但是对于模块输出的pready\CEN等信号最好assign成1,否则可能导致问题(例如:sram使能信号CEN赋成0,可能导致后面的sram模型认为有读写行为;pready信号赋成0,可能导致SoC软件跑起来的时候对该模块寄存器操作的时候挂死apb总线)。这个脚本并不好写。因为verilog语法支持的模块声明实在是太多了,导致脚本很容易顾此失彼。


举例来说几个复杂的地方:
module声明后面可以跟parameter的就很复杂。

Module test #(
  parameter a = 1,
  Parameter b = 2,
  C = 3,
  D = 4
);

这些parameter很可能要用在端口位宽的声明里。更为麻烦的是parameter里可能会有function的使用。而function有可能是以define的形式写到代码中。这样就很难用parse RTL的方法来解决。
再比如: 端口声明里出现ifdef else endif这种编译宏的处理也比较麻烦。
也可以使用simulator或者debug工具提供的用户接口来编写tcl程序来获取各个端口的name、width信息。但是不同仿真(define不同)可能导致端口宽度和端口不一致,结果要针对不同define来维护不同的dummy也比较麻烦。
总之,产生dummy文件以后一定要记得检查一下。Dummy文件可以有效缩短编译的时间。


  • 文件列表处理的维护

上述几个事情是应该提前准备好的,接下来我们要开始编译RTL了。

Integration好的文件列表,首先要先编译该文件列表。有可能遇到的问题是加密文件的种类,有可能文件列表里的加密文件和你用的仿真器不一致。然后结合前面产生好的dummy文件,我们要处理出一个简化设计的mini-文件列表,一般里面只包括初始化必须的模块(Clkrst、PAD、CPU、总线拓扑、内存控制器),也就是把video系统、外围接口、存储系统这些模块统统dummy掉。


  • 产生mini-文件列表

可以用脚本来维护一个配置文件,在该配置文件中指明如何删改原有文件列表。

注意最终使用的文件列表里的文件路径应该是绝对路径。
使用绝对路径的好处在于可以让run_sim脚本指定仿真在任何目录下进行(比如regression要提交到别的硬盘上去跑,那么就必须使用绝对路径了)。
注意绝对路径里不要用$macro的结构,别人有可能用你的文件列表跑仿真或者debug,而别人的$macro很可能与你的不同,导致出问题。


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

`ifdef FPGA1
    module v_fpga1 (
`elsif FPGA2
    module v_fpga2 (
`elsif FPGA3
    module v_fpga3 (
`else
    module v (
`endif

那么在文件列表里就会是下面这种结构:

fpga1_def.v
a.v
fpga1_undef.v
fpga2_def.v
a.v
fpga2_undef.v
……

请注意文件列表处理脚本,有可能会有“去重”的处理。这个时候要去掉文件列表处理脚本的“去重”功能。
编译过mini-文件列表以后就可以开始真正的准备写testbench顶层模块了。


  • Define文件的维护

我们在搭建testbench过程中的interface、env、svtb等可能需要xmr(cross module reference)访问信号,这时候维护一个公共的define文件很重要。

该define文件中应该包括:

  1. 各个主要模块的xmr路径define ,记得按照asic/ FPGA/模块级 来分别区分define

  2. 地址空间上模型数组的路径。比如dram模型里数组的xmr路径、sram里memory数组的xmr路径

  3. 共享空间的部分地址的define,比如我们的软件打印的实现所用到的共享空间的define

  4. Dram基本define

 

  • 共享空间

SoC项目Testbench中的“共享空间”,是指的软件(嵌入式C程序)和Testbench(SV程序)都可以看到的空间。一般来说Testbench可以看到所有的内容,而软件只能看到CPU地址空间(寄存器、SRAM、ROM、Dram、外部IO空间等)。

共享空间需要的地址范围不算小(可能需要几十KB,所以一般是放在CPU可见的SRAM和Dram里),对于ISS会有所不同(后面会说明)。


  • 公共函数的维护

项目上大家都可能使用到的函数即为“公共函数”。

我个人认为最重要的是对CPU地址空间的访问(我们是xmr_read_mem和xmr_write_mem)。以及基于这几个函数(task)实现的文件存取等函数。
在实现xmr_read_mem和xmr_write_mem task(或function)的时候,主要模型数组的宽度会导致根据模型数组下标访问的地址的不同。比如,加入位宽是128bit,那么一个memory就对应着4个32bit的word。----- 各个项目会有所不同。
另外,对于Dram的处理是最复杂的,尤其是Dram是支持bank地址和Row地址 remap的,所以要特别注意remap时候的 地址和bank信号、row地址信号的对应关系。-----这个工作是可以继承前面项目的。
Xmr函数需要考虑“用SRAMC或者AXI_SLV_VIP替换DDRC”情况下的实现。

简单说一下vip_slave_write32函数的实现。 这个函数调的底层函数是:

env.axi_slave_subenv.do_write32(addr, data);

但是该函数在tb其他组件可能看不到,但是program可以看到。所以在program里做一个函数来调底层的env.axi_slave_subenv.do_write32,然后把program的这个do_write32用DPI export出去。在tb上维护一个xmr.c的c程序,里面实现vip_slave_write32。在xmr_wr_mem32里调用的就是这个vip_slave_write32。
使用xmr_wr_mem32 和 xmr_rd_mem32可以比较容易的实现:

  1. 装入初始化程序 --- 一级bootloader要装入到rom中,应用程序要装入到dram中。使用xmr_wr_mem32可以直接以访问cpu地址的形式来写入程序。

  2. 把激励数据灌入dram中,让被测模块从dram中取激励数据。或者从内存中读取成片的数据用来做比对(比如解码解完了一帧,从内存中一次性把整帧的yuv解压缩完的数据读出来)。

这里可以使用上面装程序类似的方法来实现。也可以利用xmr_rd_mem32和xmr_wr_mem32来实现一个通用的task。


把指定文件写入到内存中作为激励数据:
这里有一个小技巧,就是用fscanf来读取文件中的一行数据,然后判断字符串的长度,从而得到输入文件一行几个byte,然后根据一行几个byte来装入到dram中。


原文链接


点赞

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册

  • 关注TA
  • 加好友
  • 联系TA
  • 0

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 5

    粉丝
  • 0

    好友
  • 8

    获赞
  • 4

    评论
  • 2003

    访问数

小黑屋| 手机版| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-11-5 10:21 , Processed in 0.015021 second(s), 7 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
返回顶部