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

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

日志

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

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

  • Testbench顶层文件

我们基于mini-文件列表来做Testbench顶层文件,是为了加速编译速度。
Testbench顶层最主要的是例化DUT的顶层。Emacs用户做集成很容易;我是VI用户,稍微麻烦一些,使用vi的替换功能也可以比较快的集成起来:
把顶层模块的input output inout端口声明部分copy出来,把input-outpput-inout替换成“wire”,来实现信号的声明。个别信号,系统输入信号、系统reset信号需要改成reg,并产生reg信号的激励。 时钟的产生建议使用一个module来产生,目的是为了让代码简洁清晰一些。


Copy一份wire声明的部分,然后处理成DUT的集成。

  1. s/\s*\[\d\+:\d\+\]// 去掉位宽声明

  2. wire [1:0] A; à wire A;

  3. s/wire\s\+// 去掉wire声明

  4. wire A; à A;

  5. s/\(\w\+\)/.\1(\1)/ 产生集成

  6. A; à .A(A);

处理一下模块名声明例化和分号。
.A(A); à .A(A),


给顶层信号加pullup pulldown,一般来说顶层信号都要加pulldown,个别信号需要加pullup.总之是不希望让TB引入X状态。如果不知道哪些加pulldown pullup,至少要对 测试模式输入pin(TEST)、cpu Jtag口、初始化要读取的PAD状态或者标识PIN加入合适的pullup或者pulldown。


  • 例化interface和program:

Program通常就是简单例化SV的组件(比如VMM下的env),以及include每个testcase所不同的处理部分。
在每个test.sv里通常是实现随机变量的扩展类。
要注意Program如果结束的话,那么仿真也会结束,所以注意控制program的结束时间。
例化基本仿真模型。最主要的是Dram模型了。请注意,Dram模型的例化最好用define处理好,因为Dram有可能要做4bit 8bit 16bit等几种情况,不同大小、不同位宽的dram的地址信号宽度不同,外挂的片数也不同,这里集成的时候需要特别注意。


  • Dump波形的实现机制:

Dump波形的原则是“是否dump、修改dump的起始时间、修改dump的层次都不需要重新编译”。前两个要借助仿真的运行参数来控制,后一个使用verdi的pli。

通常Testbench顶层文件都比较复杂,建议多使用Include文件的方式维护,这样代码可读性较强。而且顶层文件里通常有比较多的ifdef-elsif-else-endif的编译结构,代码太复杂的话,可能有一些笔误造成的编译错误。
Include前面准备好的公共函数文件和公共define文件。
程序初始化load代码。SoC项目需要嵌入式软件代码,包括一级boot和Dram里的应用程序。这两段程序代码都需要load到对应的存储介质中去。这个load工作可以使用基于xmr_write_mem函数构造的写文件函数比较简单的实现。------具体实现前面已经贴过了。


至此,testbench顶层基本完成。

 

  • 初步debug设计和环境

顶层testbench写好以后,编译通过后,dump整体波形,可以看一下各个模块端口上是否有高阻Z,有的话说明可能有漏接的内部信号,尤其是主总线上的各个master口和slave口的连接。
检查CPU PAD ROM控制器 SRAM控制器等初始化需要的基本模块是否有时钟和reset。如果没有的话,说明根据外部输入系统时钟和系统reset产生的基本模块的时钟和reset有问题。


  • 一级Bootloader

一级bootloader是为了做初始化的,系统实际使用的bootloader是比较复杂的,牵扯到外部存储介质上的参数搬运和配置。仿真用的一级bootloader要尽量的简单,因为一级bootloader所有的仿真都要用,这一步要是慢了会浪费时间。------- 当然,使用ISS的话,就不存在这个问题了,但是一样也要求初始化要尽量的快速。
我个人建议仿真bootloader里就只设计如下几个步骤:

  1. 系统上电初始配置

  2. 初始化pll至目标频率(如果系统pll默认频率就是目标频率,那么这一步就省略)

  3. 配置核心模块时钟频率以及切时钟,对必要模块进行软件reset

  4. 内存控制器初始化

  5. Remap到内存中准备执行内存中的应用程序。 ------ 一般汇编实现

最基本的函数是对CPU空间的访问处理函数:

  1. #define SETREG32(reg,val) (*((volatile unsigned int *) (reg)) = ((unsigned int) (val)))

  2. #define GETREG32(reg) (*((volatile unsigned int *) (reg)))

reg是寄存器地址,val是要配置的数值。 Volatile保证直接操作到内存。


  • 应用程序代码

应用程序代码里也要做一些初始化,主要是非核心模块的时钟配置以及非核心模块的软件复位操作。
如果使用ISS的话,由于没有一级bootloader,所以要把一级bootloader的代码功能在应用程序初始化中实现。
需要注意的是,使用ISS的时候,使能cache可能导致ISS行为异常。可以在cache使能的位置使用ifdef ISS。汇编代码中是:

IF2 ( EF: arm_ISS)3 NOP4 NOP5 ELSE6 Cache-operation7 ENDIF

汇编代码中include define文件是:

GET define.s (注意不能顶头写!)

然后构造一个极简单的应用程序。一般就是访问一下ddr、sram、寄存器和打印。
endsim()是结束仿真函数,如果希望让软件控制什么时候结束仿真,那么就可以在软件中的合适位置调用该函数。 函数的实现是利用共享空间,软件写入到共享空间指定位置一个标志,然后svtb中while(1)的去采样该标记就可以了。


  • 实现嵌入式代码在仿真平台上的打印

软件代码里相对复杂一些的是“printf”的实现。

重点是使用软件和testbench都能看得到的地方来存放要打印的内容。

然后testbench里while(1)的根据“打印使能”、“打印开始”、“打印结束”标志来把内容$write出来。

软件和Testbench都可以看到Sram空间(一级bootloader用来做数据存放和堆栈的sram)。注意bootloader的scatter文件里不要让stack-top覆盖了这部分空间。
Printf与实际C的printf的实现机制是一样的,都是利用“不定个数参数的函数”(实现机理:因为参数是从右向左压栈,所以最开始的那个参数在最接近栈顶的位置,这个参数在栈中的位置编译器可以知道)。


  • Debug整体环境

至此,整个环境已经基本建立起来了。

结合一级bootloader和简单的应用程序代码可以debug系统初始化流程和整体环境。

通常这里会有一些集成、以及总线访问的小bug。


  • ISS替换

为了加速编译和仿真速度,我们使用ISS来替换CPU-IP。

ISS一些C程序代码。提前把这些代码编译成.so文件,然后编译的时候就不用编译ISS了,链接的时候link进来就可以用了。

使用ISS的优势:

  1. 可以dummy掉CPU-IP的代码

  2. 不需要一级bootloader

  3. 执行软件很快

  4. Testcase依然可以基于嵌入式c程序来写

  5. 模块级的testcase也可以用C实现

ISS外面包一个AXI的wrapper,把这个模块例化到testbench顶层。Force到cpu的data总线的AXI口上(如果是Arm9的话,是AHB总线)。

IO访问的task文件要include到testbench顶层中去。对于寄存器空间的IO访问,需要产生正常的时序;而对于内存空间的访问,可以调用前面介绍的xmr_wr_mem32和xmr_rd_mem32函数来加速。

ISS可能和CPU-IP不是同一个类型的CPU,这里要注意编译软件代码的时候需要加—cpu的区分,甚至可能导致软件代码的编译器都不同。这些不同可以体现在run_sim脚本里。

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

ISS下共享空间与实际CPU_IP不同,像实现打印这种功能,可以不必使用CPU地址空间。这是因为ISS的wrapper是testbench的一部分,可以直接在testbench上实现一个大数组来作为“共享空间”,这样更简单直观。


  • DDRC的替换

系统起来以后,我们可能需要替换掉DDRC。一般有两种情况:

  1. 使用SLAVE_VIP替换DDRC,目的是为了随机控制slave的latency。实现模块访存的异常情况。------ 一般要结合ISS使用,因为没必要把应用代码初始化到slave-vip中。Slave-vip的读写可能会比较慢,对于大数据量的写入行为,仿真可以明显感觉到停顿。

  2. 把内部端口和slave-vip对应上:建议使用macro,方便阅读和简化代码。

  3. 使用一个更为简单的Slave(SRAMC)来替换DDRC。目的是为了快速初始化(不用配寄存器做初始化),加快编译和仿真速度。
    SRAMC不是class,而是一个module。把它例化在顶层TB里,与上面的Slave-VIP一样也需要和内部端口对应上。
    SLAVE_VIP和SRAMC都是参数化设计,可以方便的修改数据宽度等信息。





点赞

评论 (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.014072 second(s), 7 queries , Gzip On, Redis On.

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