type
status
date
slug
summary
tags
category
Property
Nov 30, 2023 02:47 PM
icon
password
属性
属性 1
描述
Origin
URL
1.UVM 验证方法学(1)(UVM 基础之面向对象)
UVM 方法学是基于 SystemVerilog 语法更通用、可重用性更好的一个验证平台,学好了 SystemVerilog,UVM 自然轻轻松松拿下,诸君拭目以待。
- 学习前提
- 熟悉使用 Linux 操作系统和文本编辑工具(gvim)
- 熟悉 RTL 设计代码
- 具有 SystemVerilog OOP 编程经验
- 熟练掌握逻辑仿真工具(VCS/Questasim)
- 学习目标
- 搭建 UVM 验证平台并编写测试用例 testcases
- 利用覆盖率驱动的带约束的随机化验证策略
- 对 Verilog/VHDL/SystemVerilog RTL 代码进行逻辑功能验证(EDA 阶段也不会验证时序)(还有种验证叫制造验证 ATE)
- 规划内容
- 1、SV 面向对象编程 OOP(Object Oriented Programmming)回顾
- 2、UVM 验证平台 TestBench Architecture(验证平台架构)
- 3、事务级建模 Transaction Modeling
- 4、产生激励序列 Stimulus Sequences
- 5、工厂模式和配置机制 Factory and Configuration Mechanism(UVM 独有)
- 6、UVM 组件之间的通信机制 Component Communication(TLM)
- 7、覆盖率和计分板 Coverage and Scoreboard
- 8、UVM Callback 机制(UVM 独有)
- 9、Virtual Sequencer and Sequence(激励发送机制)
- 10、UVM Component Phases 机制
- 11、寄存器抽象层 Register Abstraction Layer(RAL)
注:上面是不是很多东西都在之前的 AHB-SRAMC 项目中有所见识?没错,很多概念都是类似的!
注:寄存器在任何芯片里面都会有,一般是用来配置不同的场景和模式,这样不同的场景和模式就可以使用同一套电路了。寄存器是软件和硬件通讯的接口,通过地址译码访问不同的寄存器。
- OOP 封装(Encapsulation)
- 封装是数据 + 对数据的操作(function、task)放在一起
- OOP 继承(Inheritance)
- OOP 多态(Polymorphism)
- 多态是在继承的基础上,还可以继续定义原有的函数,但调用的时候是和定义的类相绑定的,互不干扰
- OOP 编码规则
- 对数据的操作(function 或 task )通常声明为 extern
- OOP 特殊类
- 参数化类
- 自定义类
- 单例类(Singleton Class)
- 代理类(Proxy Class)【UVM 常用】
- 工厂模式 Factory【UVM 里面比较特殊的模式】
2.1、OOP:Class 类
- 与 Verilog 的 Module 语法类似,SV 中行的 class:
- 变量(Variables/Properties)用于数据建模
- 子程序(Subroutines/Methods)用于操作数据
- 变量和子程序称为类的成员(Members of Class)
2.2、OOP:封装
- 过程化编程
- 数据和数据处理是独立的
- Verilog,VHDL,C
- 面向对象编程
- 数据和数据处理整合在一起
- SystemVerilog,C++
把数据的操作放到了 class 里面,display 就不需要传递什么参数,可以直接用。
相比于上一张图就不需要通过函数去传递了,直接可以打印调用,这就是封装的好处。
注:封装的好处是便于代码的管理。因为它针对的是同一个事务,所以放在一起可读性,可维护性更好。
2.3、OOP:继承
- 通过基类派生新类
- 新类会继承基类的所有数据和数据处理
注:继承的好处 - 减少代码的工作量,提高代码的可重用性,可维护性
- 重用:派生类(子类)兼容基类(父类)
注:理解记忆方法,继承类的方法 / 属性肯定大于等于基类,所以继承类可以直接赋值给基类,多的方法 / 属性直接被截取掉了;基类缺少继承类中的方法 / 属性,所以直接赋值会错误!
2.4、OOP:多态
- 1、下面这句代码,调用哪个子程序(程序实现如下图)?
tr.crc = tr.compute_crc();
- 调用哪个子程序,取决于变量类型 tr 是 “transaction”(调上面)还是 “error_trans”(调下面)。
- 不管子程序 compute_crc() 是否为 virtual
- 2、如果子程序 compute_crc() 不是 virtual methods。 下面代码中,调用哪个子程序?
- “I”和 “II” 通过显式声明对象是什么样的一个类,来确定调用哪个子程序。
- “III” 和 “IV” 需要先确定里面调用的函数是否为虚函数,如果不是,那么就按照形参的类型去调用!如果是虚函数,那么就按照实参的类型去调用!因为此处没有 virtual,故此处只要是 process,都是调用①!
- 3、如果子程序 compute_crc() 是 virtual methods。 下面代码中,调用哪个子程序?
- Virtual methods 会自动根据调用的句柄类型去自动适配是哪一个类的函数!
- 4、利用 virtual methods 开发通用型的类子程序
- BFM(Bus Function Model)
- 5、常见用法
- 不使用 virtual methods 的方式为边界测试案例注入错误(调用类 transaction 里面的 compute_crc 函数)
- 如果将
compute_crc
声明为 virtual,就没有问题了,如下图:
注:类比上面的 3/4 点理解!
- 6、多态总结:多态是在继承的基础上,还可以继续定义原有的函数,但调用的时候是和定义的类相绑定的,互不干扰。
2.5、OOP 编程规则:在 class 之外定义子函数
- 将所有的子函数声明在 class 内:extern
- 将数据和数据处理(子函数)放在一屏代码中,可以减少 BUG
- 在 class 文件之外实现函数体
- 作用域符号
::
- 函数放在 extern 内部进行声明,那么声明就都在 class 内部了。那么对函数的描述就可以放在 class 外面了,可读性更好。
- 我们自己新加一个类的时候也要按照这样的方式编写
注:UVM 源码大多也是采用该种形式的编码规则!
2.6、参数化的 class
- 为通用功能编写参数化的类
- 通用的代码重用
- 类似于参数的 module,在实例化时传递参数
- 参数化的格式:
类+类名+#+定义的参数
2.7、自定义的 class
- 应用场景 1:在声明之前使用类
- 2 个彼此使用对方的句柄
- 应用场景 2:简化参数类的编码
2.8、Static Property 静态变量 / 数据
- 如何创建一个非全局变量,但是可以被一个类的所有对象的共享的变量?
- 在 class 的定义中声明静态变量(static)
- 可以存储本地信息,例如生成的对象的数量(index)
- 类的所有对象都共享该静态变量
注:把对象和对象之间要共享的东西联系在一起。
2.9、Static Method 静态子函数 / 方法
- 不使用句柄就可以调用子函数 / 方法
- 只能处理静态变量(static properties)【静态函数只能处理静态变量!!!】
- 不能声明为 virtual !(因为它在编译阶段就会去分配内存空间!)
2.10、Singleton class 单例类
- 用于定义全局行为,比如打印、工厂
- 单例类不存在对象(no instance/object)
- 只包含静态变量和子函数(static properties & methods),即只包含静态成员!
2.11、Singleton object 单例对象
- 单例对象是一个全局可见 / 可操作性的对象,提供定制化服务 / 函数
- 有且仅有唯一的对象
- 在编译时创建
- 在仿真时全局可见 / 可操作性
- 可以有静态或者非静态类成员
注:UVM 的工厂对象就是一个单例对象。
2.12、Proxy class 代理类
- 提供 create() 等通用型的子函数
注:UVM 代理类机制与 tr.new() 这种区别后面也会详细研究,这里暂且按下不表!
2.13、小结
对于代理类、单例类这些机制第一次接触可能会比较难以理解,但是对于我们使用者来讲,可以先学会如何使用,学有余力再来研究其内部机制!
2.UVM 验证方法学(2)(UVM 基础之工厂机制(Factory Mechanism))
UVM 的工厂内部机制(Factory Mechanism)坦白来讲,对我们初学者确实有点深。故本篇文章各位读者可先做个了解,混个眼熟,学会使用方法,了解该机制的好处即可!
1、代理类
- 创建工厂,需要两层代理类
- 虚代理基类(Virtual Proxy Base Class)
- Virtual 不会被实例化,是一个架子,或者框图。虚代理类就是工厂,把需要的零部件定义在这。因为是虚的类,无法实现,所以需要派生,派生就是继承,类似于模具。
- 派生代理类,实现全部功能
- 类似于工厂中的模具
2、虚代理基类
- 虚代理基类(Virtual Proxy Wrapper Class)
- 代理基类要具有多态属性
- 派生类用方法的时候,因为有多态的属性,才会去用到它自己的方法
- 为代理服务提供抽象的接口应用程序
- 创建对象
- 获取创建类型名称
注:工厂机制最大的特点:新定义类的时候可以覆盖(Overrider)已有的类 ,先了解,后面有实例慢慢体会!
3、factory class 工厂类
前面定义了虚拟基类,接下来就要在它的基础上建立一个工厂!
- 按需创建对象
- 维护代理对象的登记表(registry)【注册表】
- 第一行定义一个登记表或者注册表
- 工厂机制的主要作用是把对象去做一个覆盖,那么要覆盖就需要知道新定义的对象和原来的对象覆盖的是否为同一个对象,所以需要去注册。
- 此处的注册表是一个联合数组(哈希数组)的形式,索引是字符串,用来传声明对象的名字(类),类型是 proxy_base。
- 因为注册表类型是 proxy_base,所以联合数组里面的内容是代理类。从代理类(proxy_base)的对象可以获取 type_name,通过 type_name 就可以进行注册。
- 哈希数组的注册就像学生注册一样,通过姓名(string value)找学号(key)。
- 简单理解为注册表存的是 proxy_base 代理类类型的数组。
- 注册函数 registry:注册就是把代理类 proxy 的类型(get_typename)和对应的句柄值一一映射起来,放在一个叫做 registery 的注册表里面。
4、proxy_class 代理类
- 完成抽象的接口函数
- 在工厂中登记
- 利用工厂创建对象
5、应用实例
调用流程分析(图放大看):
由上分析可知:通过工厂机制,可以实现 new_driver 替换 driver,同时不修改验证平台!
6、工厂机制优势
问:为什么要使用这么复杂的机制?
- 答:测试用例可以使用工厂机制替换验证平台中创建的任何对象(组件),但却不需要修改验证平台的代码,提高了代码的可重用性!
7、小结
本小节的工厂机制调用数据流对于初学者来说,确实不太友好,但是我们可以先学会怎么去用嘛。使用一段时间后,再回过头看这些内部实现机制,说不定就会豁然开朗。最后,再简单总结下工厂机制的优势及使用方法,我们使用工厂机制的好处是:不修改验证平台的前提下,在顶层替换验证平台的任何对象。即在顶层加入
factory::override_type("old", "new");
即可,另外要求在建立对象的时候,需要用create
函数建立,而不能使用原来的new
函数!create 函数的具体语法规则将会在后续 UVM 篇章中详细介绍,现在知道使用方法及整体的概念即可!3.UVM 项目实践之 APB_SPI(1)SPI 协议重点回顾,要点提炼
2.1、SPI 协议简介
SPI 是上世纪 80 年代中期由 Motorola 开发的一款,用于串行外设接口(Serial Peripheral Interface)通信的协议。主要应用于嵌入式系统的短距离通信,典型应用为闪存(Flash)或液晶显示接口。主要特点有:全双工,同步,串行,主从结构。并且该协议接口只需占用四根线,大大节约了芯片的管脚数量,同时为 PCB 的布局布线提供了方便。正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议接口。
SPI 总线的 传输速率 需要自定义,没有具体的规定,一般为:400KHz~10MHz(AHB:100MHz,APB:50MHz)
- 主要功能 :实现 CPU 与各种外接设备连接(芯片与芯片之间的连接),以串行的方式进行数据交换(串并转换)。
- 拓扑结构 :一个 SPI Master 可以通过共享数据线的方式可以连接多个 SPI Slave(一主多从);
- 接口配置 :作为芯片与芯片之间互连的接口,通过配置寄存器实现主控芯片(SPI Master)对外围芯片(SPI Slave)的配置;
2.2、单板中的 APB_SPI 模块简介
- Core 可以通过红色示意的数据通路配置寄存器,来让 AD/DA 模块工作在预期的模式!
- CPU 内部可以通过并行的 AMBA 总线(AHB/APB)来配置某个模块,但是如果外挂 AD/DA 模块在单板上,考虑到信号完整性等问题就不能使用 AMBA 总线来配置它,这里就需要将并行的 AMBA 总线转换成串行的 SPI 总线。这样 SPI Master 和 SPI Slave 之间就可以通过很少的物理走线连接!
- SPI(Serial Peripheral Interface,串行外设接口)
- 应用:主控芯片和外围芯片之间互联的接口
- 作用:主控芯片去配置外围芯片的接口协议
- 功能:并串转换,管脚就变少了,功耗和面积就变小了
- 拓扑结构:一个 master 可以配置多个 slave
单独提炼出 APB_SPI 模块,接口如下:
3.1、快速理解 SPI 接口协议
- ①、基本功能(概述):Feathures
- 并串转换,并行数据转换成串行的。因为是芯片与芯片之间的互联,如果走线多了,信号完整性就不好了。并且走线多了,会造成 IO 过多,芯片的 PPA(Performance(性能)、Power(功耗)、Area(尺寸))就得不到收益!
- ②、拓扑结构(空间):Architecture
- 一个 Master 多个 Slave
- ③、数据流图(时间):Data Path
- 见上上图
- ④、接口信号(空间):Interface
- 详见后文
- ⑤、接口时序(时间):Timing
- 详见后文
- ⑥、配置信息(空间,如寄存器):Configure
- 详见后文
- ⑦、配置过程(时间,启动 -> 正常工作):Flow
- 详见后文
3.2、SPI 协议特点
- SPI 特点:主从结构、同步、串行、全双工通信接口(需要记住)
- 主从结构:SPI 区分 Master 和 Slave(谁产生时钟,谁就是 Master)
- 同步:Slave 使用 Master 提供的 SCLK
- 串行:MOSI 和 MISO 为单 bit 串行信号
- 全双工:master 和 slave 收发可以同时进行(全双工类比手机;半双工类比对讲机)
1、同步方式(Synchronous)传输数据
Master 设备会根据将要交换的数据来产生相应的时钟脉冲 (Clock Pulse), 时钟脉冲组成了时钟信号 (Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的。
2、主 - 从结构(Master-Slave)的控制方式
SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制次设备 (Slave)。一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备, SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCLK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock。
3、串行、全双工方式数据交换(Data Exchanges)SPI 设备间的数据传输之所以又被称为数据交换(全双工通信) , 是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 “发送者 (Transmitter)” 或者 “接收者 (Receiver)”. 在 每个 Clock 周期内, SPI 设备都会发送并接收一个 bit 大小的数据(串行通信) (不管主设备好还是从设备),相当于该设备有一个 bit 大小的数据被交换了。
需要说明的是:SPI 只有主模式和从模式之分,没有读和写的说法,因为实质上每次 SPI 是主从设备在交换数据。也就是说,每发一个数据必然会收到一个数据;要收一个数据必须也要先发一个数据。
SPI 的 Master 和 Slave 连接如下图(一对一为例):
注:MOSI(Master Out Slave In)
3.3、SPI 接口信号
信号名 | 含义 | 描述 |
|–|–|–|–|
| SCLK | Serial Clock、时钟信号线 | Master 设备往 Slave 设备传输时钟信号, 同步控制数据交换的时机以及速率 |
| MOSI | Master Ouput Slave Input、主设备输出从设备输入数据线 | 在 Master 上面也被称为 Tx-Channel, 作为数据的出口, 用于 SPI 主设备发送数据 |
| MISO | Master Input Slave Ouput、主设备输入从设备输出数据线 | 在 Master 上面也被称为 Rx-Channel, 作为数据的入口, 主要用于 SPI 从设备接收数据 |
| SS_n | Slave Select、从机选择线,低电平有效 | Master 设备片选 Slave 设备, 使被选中的 Slave 设备能够被 Master 设备所访问 |
- 每个从设备都需要一个 SS_n 信号,SS_n 信号与从设备数相同
- 当有多个从设备时,最多只能有一个从设备被选中
- 未被选中的从设备输出一般体现为高阻,为了不影响被选中的从设备输出
- SS_n、SCK、MOSI 信号均由主设备产生,MISO 信号由从设备产生。在 SS_n 为低电平的前提下,MOSI 和 MISO 信号才有效,在每个时钟周期 MOSI 和 MISO 传输一位数据
3.4、SPI 多个从设备的输出信号
- SCLK 和 MOSI 直接广播到 Slave,为了避免走线过多,这里不再一一画出!
3.5、SPI 接口时序
3.5.1、CPOL 与 CPHA 信号详解
注:CPOL(时钟极性):Clock Polarity;CPHA(时钟相位):Clock Phase。
- CPOL 与 CPHA:CPOL 决定总线空闲电平状态;CPHA 决定哪个跳变沿采样。
- CPOL:时钟极性选择,为 0 时 SPI 总线空闲为低电平,为 1 时总线空闲为高电平
- CPHA:时钟相位选择,为 0 时在 SCLK 第一个跳变沿采样,为 1 时在 SCLK 第二个跳变沿采样
1)、CPHA=0,表示第一个边沿:
对于 CPOL=0,idle 时候的是低电平,第一个边沿就是从低变到高,所以是上升沿;
对于 CPOL=1,idle 时候的是高电平,第一个边沿就是从高变到低,所以是下降沿;
2)、CPHA=1,表示第二个边沿:
对于 CPOL=0,idle 时候的是低电平,第二个边沿就是从高变到低,所以是下降沿;
对于 CPOL=1,idle 时候的是高电平,第一个边沿就是从低变到高,所以是上升沿;
单纯看上述皱巴巴的文字可能还不好理解这两个信号,不妨结合下面的图再来理解下:
3.5.2、SPI 时序图
注:CPHA=0 - 时钟前沿采样;时钟后沿输出。CPHA=1 - 时钟前沿输出;时钟后沿采样。
Bit1 为 MSB,Bit8 为 LSB。假设 CPOL=0,CPHA=0。在 SCLK 的第一个时钟周期,在时钟的前沿采样数据(上升沿),在时钟的后沿输出数据。先看主器件,主器件的输出口(MOSI)输出数据 bit1,在时钟的前沿被从器件采样,那主器件是何时输出 bit1 的呢?bit1 的输出时刻实际上在 SCK 信号有效以前,比 SCK 的上升沿还要早半个时钟周期,bit1 的输出时刻与 SS_n 信号没有关系。再来看从器件,主器件的输入口 MISO 同样是在时钟的前沿采样从器件输出的 bit1 的,那从器件又是在何时输出 bit1 的呢?从器件实在 SS_n 信号有效后,立即输出 bit1,尽管此时 SCK 信号还没有生效。
CPOL/CPHA 的设定 | 第一位数据的输出 | 其他位的输出 | 数据采样 |
CPOL=0, CPHA=0 | 在第一个 SCLK 上升沿之前 | SCLK 下降沿 | SLCK 上升沿 |
CPOL=0, CPHA=1 | 第一个 SCLK 上升沿 | SCLK 上升沿 | SCLK 下降沿 |
CPOL=1, CPHA=0 | 在第一个 SCLK 下降沿之前 | SCLK 上升沿 | SCLK 下降沿 |
CPOL=1, CPHA=1 | 第一个 SCLK 下降沿 | SCLK 下降沿 | SCLK 上升沿 |
注:上表结合 3.5.1 中的图理解会更好!
3.5.2、其他
- 对于 32bit 的数据来讲,bit31 就是 MSB,bit0 就是 LSB
- MISO/MOSI 具有半周期采样(setup/hold)特性,如:10M 时钟,就得按 20M 去约束!(物理实现需要去考虑的)
思考:APB_SPI 项目的四种模式(CPOL 和 CPHA 的排列组合)怎么验证呢?
3.6、SPI 配置信息
编号 | 配置信息 | 描述 |
1 | SLCK 频率 | Master 选择 Slave 可接受的频率 |
2 | Slave 选择 | Master 选择需要操作的 Slave,通过 SS_n 信号 |
3 | 传输 bit 数 | 一般为 2 的幂次,也可以为任意正整数 |
4 | 传输位序 | 先发送高 bit(MSB,默认),还是先发送低 bit(LSB) |
5 | CPOL | 时钟极性(Polarity)0:SCLK 的前沿为上升沿,后沿为下降沿1:SCLK 的前沿为下降沿,后沿为上升沿 |
6 | CPHA | 时钟相位(Phase)0:发送端在后沿早一拍发送数据,接收端用前沿接收数据1:发送端在前沿发送数据,接收端用后沿接收数据Master 和 Slave 可以不一致 |
7 | 启动间隔 | 在启动传输时,Master 等待主从设备都准备好传输在停止传输时,Master 等待主从设备传输完成 |
- 后沿早一拍,即前沿早半拍!
Q:为什么要配置信息呢?
- A:因为只有配置好信息,才能工作在不同的模式!
3.7、SPI 配置过程
- 依次设置好配置信息(见上节)
- 这一步是软件通过 cpu 来做的
- SS_n 选择对应 Slave
- 这一步也在配置信息中
- 执行数据传输
- 这一步是硬件 DUT 来完成的
- SS_n 释放对应 Slave
- 这一步也是通过硬件完成的
3.8、SPI 数据交换过程
SPI 是一个环形总线结构,由 SS_n (CS)、SCLK、MOSI、MISO 构成,时序很简单,在 SCLK 的控制下,
SSPSR
是 SPI 设备内部的移位寄存器,根据 SPI 时钟信号状态,往SSPBUF
里移入或移出数据,每次移动的数据大小由 Bus-width 和 Channel-width 决定。在正常工作时,两个双向移位寄存器进行数据交换,寄存器中的内容全部移出时,相当于完成了两个寄存器内容的交换。在每个时钟周期内,Master 与 Slave 之间交换的数据其实都是 SPI 内部移位寄存器从SSPBUF
里面拷贝的,可以通过往SSPBUF
对应的寄存器(Tx-Data/Rx-Data register)里读写数据,间接操控 SPI 内部的SSPBUF
。SSPSR
控制数据移入移出SSPBUF
。Master 里面的 Controller 主要通过时钟信号以及片选信号来控制 Slave。Slave 会一直等待,直到接收到 Master 发过来的片选信号,然后根据时钟信号来工作。Master 的片选操作必须由程序实现。SSPBUF
:Synchronous Serial Port Buffer, 泛指 SPI 设备里面的内部缓冲区, 一般在物理上是以 FIFO 的形式, 保存传输过程中的临时数据;
SSPSR
:Synchronous Serial Port Register, 泛指 SPI 设备里面的移位寄存器 (Shift Regitser), 它的作用是根据设置好的数据位宽 (bit-width) 把数据移入或者移出 SSPBUF;
- Controller, 泛指 SPI 设备里面的控制寄存器, 可以通过配置它们来设置 SPI 总线的传输模式
3.9、SPI 内容总结
本文内容要点提炼如下:
- 快速理解接口协议的 7 个步骤
- SPI 接口协议,总结具备 4 个特点
- SPI 接口协议,接口包含 4 个信号
- SPI 接口协议,时序关注双工和采样(验证不去验时序,但是面试会考)
- SPI 接口协议,配置包含 7 个信息
另外,再多啰嗦两句。要做这个 APB_SPI 项目,必须要掌握 SPI 协议,这样才能搭建环境、写 interface、进行数据建模等操作!面试的时候,也会常问你做的这个项目用的协议理解!这个 APB_SPI 项目相对之前 AHB-SRAMC 可能会更复杂一点,工作量也会大一点。虽然 AHB 比 APB 复杂,但是我们在那个项目中实际使用的是 AHB-Lite!对于新人来讲,企业更多看中的是潜力!那么个人潜力如何展现给企业呢?需要结合每个人自身情况量身打造了…
4.UVM 项目实践之 APB_SPI(2)APB 协议重点回顾,要点提炼
1.1、AMBA 发展史
- AMBA1.0: ASB 协议和 APB 协议;
- AMBA2.0: AHB 协议、ASB 协议和 APB 协议;
- AMBA3.0: 增加了 AXI 协议(了解);
- AMBA4.0: ACE 协议(了解)
AMBA2.0 (Advanced Microcontroller Bus Architecture,先进微控制总线结构),主要定义了三种总线:
- AHB:Advanced High-performance Bus(先进高性能总线) 高速高性能总线;支持 2 级流水操作(优势)
- APB:Advanced Peripheral Bus(先进外围总线) 低速总线、低功耗;接口简单
- ASB:Advanced System Bus(先进系统总线)
1.2、典型的 AMBA 系统
AHB 总线的强大之处在于它可以将微控制器(CPU)、高带宽的片上 RAM、高带宽的外部存储器接口、DMA 总线 master、各种拥有 AHB 接口的控制器等等,连接起来构成一个独立的完整的 SOC 系统。不仅如此,还可以通过 AHB-APB 桥来连接 APB 总线系统。
AHB 可以称为一个完整独立的 SOC 芯片的骨架。
注:APB 总线是 AHB 系统总线的扩展,便于低速外设连接到系统总线上,AHB 和 APB 之间通过 AHB2APB Bridge 连接。
2.1、APB 总线概述
APB 的英文全称 Advanced Peripheral Bus,即先进外设接口 。AMBA 中的 APB 总线主要用在低速且低功率消耗的外围设备, 在 APB 总线中,唯一的 Master 为 APB Bridge ,其它一些低速和低功率的外围皆为 Slave。因此 APB 总线不需要有一个像 AHB 一样的仲裁器及其它复杂的线路,也就是说 APB 总线的整个架构较 AHB 简单许多。
为了使 APB 容易被整合进大部分的设计流程中, APB 规定所有信号必须在时钟上升沿触发时进行传递。通常 APB 总线的组成可看做是由 APB Bridge 和 APB 上的从设备两部分组成。
- 1、 APB Bridge 可以锁存总线所有地址、数据和控制信号;并进行二级译码来产生 APB 从设备选择信号。
- 2、APB 上所有的其他模块都是 APB 从设备。
2.2、APB 总线信号列表(重点)
信号名 | 含义 | IO | 源 | 描述 |
PCLK | 总线时钟 | / | / | 总线时钟,同步所有传输,时钟上升沿触发所有信号,所有信号时序与 PCLK 的上升沿有关 |
PRESET_n | 复位 | / | / | 总线复位信号低有效,复位系统与总线 |
PADDR[31:0] | 地址总线 | Input(M2S) | 主 | 32 位系统地址总线 |
PSELx | 从选择 | Input(M2S) | 主 | 选择信号:表示当前哪个 Slave 被选择在传输地址选择就是地址译码出来的 Slave 选择信号 |
PENABLE | APB 选通 | Input(M2S) | 主 | 指示 APB 操作的第 2 个周期(PENABLE=0:SETUP 阶段;PENABLE=1:ENABLE 阶段) |
PWRITE | 传输方向 | Input(M2S) | 主 | 该写操作:1 写 0 读 |
PRDATA[31:0] | 读数据总线 | Output(S2M) | 从 | 读操作时,Slave 返回给 Master 的数据总线推荐最少 32 位数据总线带宽,可以很方便地扩展到高带宽操作。 |
PWDATA[31:0] | 写数据总线 | Input(M2S) | 主 | 写操作时,Master 返回给 Slave 的数据总线推荐最少 32 位数据总线带宽,可以很方便地扩展到高带宽操作。 |
由上表不难看出:APB 信号主要由系统信号(PCLK、PRESETn)、地址(PADDR[31:0])和控制信号(PENABLE、PWRITE、PSELx)、数据信号(WDATA[31:0]、PRDATA[31:0])、状态信号(PREADY)四部分组成。而 PREADY 信号是在 AMBA3.0 中给出,AMBA2.0 及以下版本是没有的。
3.1、APB 状态转换图(FSM)
从状态机看,APB 对每一笔数据的传送,均需花 2 个周期的时间,且 APB 的数据传递不适用在有流水线架构的模块设计中。
状态 | 含义 | 跳转条件 | 描述 |
IDLE | 默认状态 | No Transfer:ENABLE -> IDLEIDLE -> IDLE | IDLE 是 APB 状态机的预设状态 |
SETUP地址阶段 | 准备状态 | Transfer:IDLE -> SETUPSETUP -> ENABLEENABLE -> SETUP | 当一笔数据需要传递时,进入 SETUP 阶段,在此状态中,PADDR[31:0] 地址线会经译码电路产生唯一的 Slave 使能信号 PSELx。APB 总线只会在 SETUP 停留一个时钟周期,而在下一个时钟周期上升沿触发时进入 ENABLE 状态 |
ENABLE数据阶段 | 使能状态 | Transfer:ENABLE -> SETUPNo Transfer:ENABLE -> IDLE | 从 SETUP 转换至 ENABLE 时,数据地址、读写控制和选择信号会保持稳定。该状态也只会持续一个时钟周期,之后如果无传递需求,则在下一个时钟上升沿触发时,转回 IDLE;如果有其他笔数据需要传送,就会转回 SETUP。在从 ENABLE 转回 SETUP 状态时,地址、写入和选择信号中存在毛刺是可以接受的。 |
3.2、写操作时序(重要)
注:这里默认 PREADY 为常高的情况,PREADY 为低顺延 ENABLE Cycle 即可,这里不做过多讨论!
- 在 T1 时,有限状态机进入预设的 IDLE 状态;
- 在 T2 时,数据地址、读写控制信号和写入的数据会在时钟上升沿触发时,开始做写数据传输准备,这个周期也就是上面所提及的 SETUP 状态。译码电路在此状态会根据数据地址去译码出所要写入的 APB Slave,此时所对应到 Slave 的 PSEL 信号将由 0 变 1;
- 在 T3 时,有限状态机会进入 ENABLE 状态,PENABLE 信号在此状态会被设成 1;
- 在 T4 时钟上升沿触发时,PENABLE 信号将由 1 变 0,而 PSEL 信号在若没有其它数据的写入动作时,也将由 1 变 0。为了减少功耗,APB 的数据地址和读写控制信号在下一笔数据传递前,将不会作任何改变。
3.3、读操作时序(重要)
注:这里默认 PREADY 为常高的情况,PREADY 为低顺延 ENABLE Cycle 即可,这里不做过多讨论!
- 由图中可发现除了写信号是低有效外,APB 读操作时序图和写操作时序图非常相似,在这里就不再作详细的解释。
- 要特别注意的是,在 T3 后,也就是在进入 ENABLE 周期后,APB 从必须要将 Master 所要读取的数据准备好,以便 Master 可以在 ENABLE 周期末被 T4 时钟上升沿触发时正确的将数据读取。
3.4、APB 模块接口(重要)
上文中已经提到,APB 由 APB Bridge 和 APB 上的从设备两部分 组成。
3.4.1、APB Bridge (APB Master)框图
APB 桥为 AHB 的一个从设备,但它在 APB 中是唯一的主设备,而 APB 中其它低速和低功率消耗的外设皆为 APB 桥的从设备。下图是 APB 桥的信号接口:
- APB Bridge 通过利用 HREADY 反压信号,打断 AHB 的流水传输,以达到时序转换的目的,将 AHB 的时序转换成 APB 的时序。
APB Bridge 将 AHB 总线传送转换成 APB 总线传送,它具有以下功能:
- 锁存地址,在传送过程中保持地址有效,锁存读写控制信号
- 对锁存的地址进行译码并产生选择信号 PSELx,在传送过程中只有一个选择信号可以被激活,也就是选择出唯一一个 APB 从设备以进行读写动作。
- 写操作时,负责将 AHB 送来的数据送上 APB 总线(注意理解数据流)
- 读操作时,负责将 APB 的数据送上 AHB 系统总线(注意理解数据流)
- 产生时序选通信号 PENABLE 作为数据传递时的启动信号
3.4.2、APB Slave 框图
- APB Slave 的数据方向同 APB Bridge(APB Master)的数据方向刚好相反
如前面所提及,APB 总线中除了 APB Bridge 为 Master 外,其他的外设均是 Slave。因此,APB 从设备比 AHB 从设备接口简单。主要特点有如下几个:
- 1、APB 少了仲裁器及复杂的译码电路,APB 进行写操作时,从设备可以决定:
- 在 PCLK 上升沿触发,且 PSEL 为高时锁存数据
- 或在 PENABLE 上升沿触发,且 PSEL 为高时锁存数据
- 2、PSELx,PADDR 和 PWRITE 三个信号的组合可以决定哪个寄存器会被写操作更新
- 3、在读操作的时候,数据可以 PWRITE=0,PSELx=1 和 PENABLE=1 的时候被送到总线上,而 PADDR 用于决定哪个寄存器会被读。
4.1、读操作
下图是批量读操作(非高频),每一组数据都只需要一个等待周期:
数据流:CPU <- AHB <- APB
- T1:AHB 总线开始传送地址与控制信号;
- T2:HREADY 信号拉低打断流水,此时地址信号被 APB 总线采样。如果该传送是针对外设的话,这个地址就会被译码成选择信号 PSELx 发往外设。T2 就是 APB 的 SETUP CYCLE。
- T3:在 APB 的 ENALBE CYCLE 阶段,PENABLE 拉高,数据被读出,同时拉高 HREADY 信号,返回数据。
- T4: 读出的数据直接映射到 AHB 总线上,在上升沿被 AHB 主采样 。这句需要结合前面将的 APB 总线读操作理解,主要是:“APB 从必须要将 Master 所要读取的数据准备好,以便 Master 可以在 ENABLE 周期末被 T4 时钟上升沿触发时正确的将数据读取。”
注:这里大家可能会有疑问了,从图上看,ENABLE CYCLE 中 APB PRDATA 读取到的数据跟 AHB HRDATA 几乎是同时的,那么在频率很高的情况下,情况会怎么样呢,按照这样的时序,会不会送不到 AHB 总线上?
答:在频率很高的情况下,ENABLE CYCLE 确实可能出现数据不能够直接映射到 AHB 总线。这时需要在 APB 桥中,即在 T4 的时候打一拍锁住,并在 T5 的时候被 AHB 采样。虽然这种做法需要多一个等待周期(一共 2 个,HREADY 反压两拍),但是由于频率提升了因此总的性能也提升了,所以整体来说系统还是有收益的!
4.2、写操作
数据流:CPU -> AHB -> APB
以下进一步说明 AHB 和 APB 之间数据传递的情形, 如图所示:
下图是批量写操作的图:
当批量写操作的时候,第一块数据不需要等待周期,之后的每一块数据都需要一个等待周期。 APB 桥中需要有 2 个地址寄存器,当处理一个数据块写操作时,可以寄存下一个数据块的地址。
APB 总线上的单块数据写操作不需要等待周期。APB 桥的责任是对地址和数据进行采样,并在写操作的过程中保持它们的值。
- T1:AHB 总线开始传送地址与控制信号 (HADDR 和 HWRITE);
- T2:AHB 总线开始传送数据信号(HWRITE);
- T3:APB Bridge 栓取住 AHB 送来的数据、地址及读写控制信号,同时进入到 APB 有限状态机的 SETUP 状态;
- T4:其后的读和写动作跟之前所介绍的 APB 读写动作一模一样,在这里我们不再加以详述。
4.3、读写交替传送
下图画出了读写交替传送的时序,先是写,再读,再写,再读。
如果写操作之后跟随着读操作,那么需要 3 个等待周期来完成读操作。通常的情况下,不会有读操作之后紧跟着写操作的发生,因为两者之间 CPU 会进行指令读取。 > 本文由简悦 SimpRead 转码
5.UVM 项目实践之 APB_SPI(3)APB_SPI 数据建模(Transaction Modeling)
从本节开始将使用 UVM 正式搭建我们的验证环境了,回顾一下 SystemVerilog 搭建 AHB-SRAMC 验证环境的步骤:
- ①、接口 interface
- ②、数据包 transaction
- ③、数据包生成器 generator
- ④、代理 agent
- ⑤、数据驱动 driver
- ⑥、验证环境 environment
- ⑦、测试用例 testcase
- ⑧、顶层连接 top
- ⑨、rtl.f
- ⑩、Makefile
而 UVM 搭建验证环境的步骤与此相比,也大差不差。首先编写接口(interface),接着根据 interface 要传的数据进行数据建模(transaction modeling),然后使用 sequencer 产生具体的数据内容,最后通过 driver 将数据按照 interface 的物理时序传递给 DUT!
- uvm_sequence_item
在 UVM 里面 transaction 的名字是 uvm_sequence_item,它在源码里面已经实现了基类,那么我们在建立新的 transaction 的时候就直接从它这派生。这样做的好处是可以使用其内置的处理函数,使用起来简单方便。
- uvm_sequence_item 处理函数(methods)
- 修改序列项(sequence item),编写测试用例(testcase)
- set_type_override_by_type
- set_inst_override_by_type
我们在 SV 里面如果要修改 transaction 的话,是在 generator 写了很多修改 transaction 的函数,比如 wirte32,write16,write8 等。然后在 testcase 里面去调用 write32,那么它就会产生 write32 的 transaction,会在 generator 里面去产生。而 UVM transaction 是直接在用例 testcase 里面产生的,而不是在用例 testcase 中调用 generator 里面的方法。产生 transaction 之后如果想改怎么办呢,就可以用到 override 这个函数,即通过工厂机制去覆盖。(定义 transaction 时会进行工厂注册,那么在 UVM 注册之后就可以使用 override 覆盖了!)
override 有两种类型,一类是根据 type 去 override:set_type_override_by_type;一类是根据实例化对象 inst 去 override:set_inst_override_by_type。具体应用,后面也有实例通过 type/inst 去 override 上面提到的 uvm_sequence_item,相当于构造新的测试用例或调整用例中的激励。
- 网络数据
- TCP/IP
- WIFI
- 3G/4G
- 总线数据
- AMBA-AHB/APB/AXI
- PCI/PCI-E
- SATA
- US
- SD
- 音视频编解码
- 指令集
- CISC - x86
- RISC - ARM/MIPS/RISC-V
- GPU
所谓事务就是跟你验的 DUT 业务相关的数据建模!如要验的 DUT 是处理网络数据的,那么 transaction 就可能是 TCP/IP 报文格式的数据包、或者是 WIFI 数据包、或者是 3G/4G 的数据包!
上述提到的术语不用纠结,只需记住重点:transaction 是针对我们不同的 dut 业务而言的,你验的 dut 的 interface 实际跑的是什么,那 transaction 就根据这个协议去创建相应的数据模型。即:对不同协议来讲,transaction 是不一样的。
- testcase 产生不同的 transaction,通过 sequencer 下发到 driver,sequencer 更多的是做一个传输的作用,它会把具体的数据产生出来。
- testcase 类似于做方案的项目经理,告诉 sequencer 要产生什么样的数据;而 sequencer 是执行层面的,把具体的数据产生出来。
- 数据产生出来是封装在一个包里面,不具备实际的属性。
- 上图中数据流是 UVM 通用数据流,agent 在实际项目中有 master 和 slave 之分,并且有的 agent 并不需要 driver 和 sequencer 组件!
- 数据建模 transaction 由
uvm_sequence_item
基类派生 - 支持激励的生成,打印和比较等操作(可以直接使用内置函数)
- 默认情况下,变量都是
public
的 - 可以在其他的 class 中添加约束信息(
constraints
) - 即:可以在 transaction 引用的更上级(如 sequence、sequencer 等类)改变约束,比较灵活。
- 默认情况下,变量必须都是随机变量(
rand
),但是不能是randc
- 使用
rand_mode
函数可以关闭随机变量
注:SV 产生 transaction 是不需要任何基类的;UV 需要去继承 uvm_sequence_item
4.1、数据建模的考虑因素
- 添加事务描述符
- UVM 组件可以解释这些可执行的事务
- 添加事务状态标志(status flag)
- UVM 组件可以设置事务执行的状态
4.2、事务:必须遵守的约束
- 定义必须遵守的约束
- 不能关闭
- 不能覆盖
- 命名规则:“class_name_valid”(这个实际并不是必须要满足的,只是个建议)
- 示例
- int 型变量 len 不能取负值
- 数组的大小范围
- 具体代码如下:
构造数据模型(transaction)有两部分:一部分是定义的各种类型数据;另一部分是对数据范围定义约束。
- 定义必须遵守的约束
- 可以关闭相关的约束,并添加容错处理的约束
- 为每一组相关的约束信息编写一个约束语句块
- 每个约束语句块都可以独立关闭或重新载入
- 命名:“class_name_rule”(不是必须遵守)
4.3、事务:如何添加约束
- 不能违反有效约束
- 当用户设置的约束信息不满足有效约束外,EDA 工具会报错
- 不能把约束范围之外的东西赋给它,否则会报错
4.4、uvm_sequence_item class
UVM 有两大类 uvm_object 和 uvm_component,而 uvm_sequence_item 属于 uvm_object。
- 具体的内置函数,这里并不需要去记,知道有这个东西,实际用的时候再去查博客即可!
4.4.1、数据处理函数
- 基类的数据处理函数(methods)
- 通过宏定义来创建这些数据处理函数
这里先知道有这些函数即可,具体如何使用,后面有具体的练习!
注:实际中,用到的最多的是拷贝和比较!
4.4.2、数据处理函数的功能描述
数据处理函数 | 功能描述 |
clone | 克隆函数会创建并返回一个对象 |
copy | 返回深度复制后的对象 |
print | 按照一定的格式将对象的数据显示在屏幕上 |
sprint | 格式化打印,返回一个字符串 |
compare | 数据比较 |
pack/unpack | pack 按照比特位将数据存放在数组中,unpack 是反向操作 |
record | 记录数据 |
4.4.3、UVM 提供自定义函数
- 使用 UVM 提供的
do_*
,用户可以自己定义数据处理函数
4.4.4、数据处理函数的使用方法
- 一个简单的示例:
注:使用 create 实例化的方式,才能使用上面提到的 UVM 内置的一些函数。
- 常见数据处理函数
- print、sprint、clone、copy、compare、record、pack/unpack
- UVM 中使用宏定义创建通用的数据处理函数(设置函数使用的域段权限)
uvm_object_utils_begin(transaction_class_name) *
uvm_field_(ARG, FLAG) 通过 field 来单独设置函数的使用权限和范围 *代表参数 argument(ARG)的类型 *
field
翻译成中文就是域 *FLAG
指参数具备的权限- `uvm_object_utils_end
4.4.5、`uvm_field_* 字段宏定义
- `uvm_filed_* 使用特定的标志(FLAG)(理解成权限)添加数据变量类型
- `uvm_field_* 宏主要用于对数据模型(transaction)中的一些变量内置函数的权限管理
- 支持如下多种数据变量类型(具体选哪一个取决于 ARG 这个参数的类型)
4.4.6、通过 FLAG 设置数据初始化函数
- FLAG 用于开关基类的数据处理函数
- 使用技巧:一般先都打开(
UVM_ALL_ON
),如果关闭某个,用或(|
)进行操作!
4.4.7、通过 FLAG 设置数制
- 在打印数据的数值格式时,可以使用响应的 FLAG 进行配置
4.5、uvm_object_utils macros
- `uvm_object_utils [_begin] (class_name) 展开
注:可辅助之前的《【数字 IC 验证快速入门】38、UVM 验证方法学(3)(UVM 基础之工厂机制(Factory Mechanism))》理解该宏定义源码
4.6、override 替换
override 分为两类
type_override
和inst_override
,作用都是替换,替换的目的就是一个环境多个用例,具体实例如下:4.6.1、修改数据约束并通过 type_override 进行替换
- uvm 里面定义一个类,如果要用到它里面的特性(如:内置函数、宏定义、gettype、override 等)就需要注册到工厂:
uvm_object_utils
和uvm_conponent_utils
! - 注册是必不可少的,注册是用
uvm_object_utils
还是uvm_conponent_utils
注册,取决于这个类是uvm_obiect
还是uvm_component
。 - testcase 属于是 component,所以用
uvm_component
;transaction 属于是 object,所以用uvm_obiect
! - 通常来讲,不占 runtime,即事前先定义好,编译的时候将其生成;
uvm_component
通常在 runtime 的时候会去运行。这是它们两个主要的区别! - 即:
uvm_component
(如:testcase)里面有build
和run
函数,而uvm_obiect
(如:transaction)里面只有build
函数!
- 不改变环境而更换 transaction 的方法,在 testcase 中 build phase(阶段)去做一个 override(覆盖),即:使用
set_type_override_by_type
函数! - 第一个参数是 old type;第二个参数是 new type
set_type_override_by_type(transaction::get_type(), transaction_da_3::get_type())
替换是全局行为,它将整个系统组件中的transaction
对象替换成transaction_da_3
对象。这样内存里面的 da 就固定是个 3,而不是随机值。- 使用该函数替换的前提是,替换类 B 和被替换类 A 是要继承的关系,即:
B extends A
- 当然,上述例子是在 build phase 去覆盖,当然也可以不在 build phase 进行!
- 为什么要用
get_type()
? - 首先需要知道:当使用 UVM 的工厂注册 class 后,就会用代理类去定义一个单例实体(对象)。同时,工厂注册好之后,才可以使用其中的
get_type
方法,而 get_type 的作用是获取单例实体(对象) 的名字。 - 而只写个
transaction
,它只是个类本身,没有分配内存空间(相当于只是个图纸),是没有什么意义的!真正类去作用,是去实例化之后(相当于根据图纸建造出的房子),指针里面的内容! get_type
相当于获取这些实例化后的内容,即获取单例类
4.6.2、type_override 数据替换结果
4.6.3、修改数据约束并通过 inst_override 进行替换
- inst 会多一个路径,这是个差异。这里是只会替换 sequencer 下面的 transaction,其它的不一定会替换(如:driver 不会替换),所以 inst 是局部的替换。
4.6.4、inst_override 数据替换结果
4.7、参数化的数据类
4.7.1、参数化的数据类型,UVM 使用不同的宏定义
- 通过宏定义做工厂的注册
- 工厂注册宏定义为(以 object 为例):
uvm_object_utils
;参数化的工厂注册宏定义为:uvm_object_param_utils
;如果需要进行变量权限管理,需要在宏定义后加上begin...end
:uvm_object_param_utils_begin
uvm 分为两大类:uvm_object(sequence、sequence_item、transaction) 事务和 uvm_component (driver、monitor、sequencer)组件
4.7.2、创建参数化的对象(实例化)是需要参数
- type_id 是 transaction 的代理类;create 是实例化,类似 new(实在不理解先记住!!!)
- 在 uvm 做对象创立的时候的语法:
类名 :: 代理类 ::create( (“实例的对象名”), this )
,this
主要是指明上一级和当前的关系。my_component 是 this ,在 my_component 下实例化了 transaction ,那么 my_component 就是 transaction 的 parent(这个父子是逻辑层面的父子,并非继承的父子!!!)
4.8、数据建模的重用性:给数据类分层
- 通过分层让模型的通用性、重用性更强一些,要注意培养自己的思维
4.9、UVM 知识回顾
- UVM 创建数据模型,使用 UVM 的基类 uvm_sequence_item
- 使用 UVM 的宏定义创建数据处理函数
- 通过 UVM 的 override 机制,在 testcase 中替换数据约束(后面还需再深入 override)
- set_
type_override
_by_type - set_
inst_override
_by_type
注:关于 sequence_item、sequence、sequencer 与 手枪各部件的类比,节选自《【数字 IC 验证快速入门】40、UVM 验证方法学(5)(UVM 基础之 TestBench 验证平台深入)》:“如果把发送激励比作手枪发射子弹,transaction(uvm_sequence_item)就相当于子弹,一个个子弹放到弹夹中,弹夹就相当于 Sequence。Sequencer 就相当于手枪后半部,包含着弹夹、扳机等部分。枪口可以理解成我们的 Driver,Driver 和 Sequencer 之间的弹道可以理解成 TLM。弹夹本身是发不出子弹的,需要手枪后半部 Sequencer 包含的扳机触发,这样子弹才能发到 Driver。Sequence(弹夹)只是把 Sequence_item(子弹)装到里面,不具备发射子弹的功能,因此需要 Sequencer 中扳机的协助才能发射!”
本节暂时还不能学习具体的数据建模代码,因为我们还未学习弹夹 Sequence 和手枪后半部 sequencer。本节只是学习了如何制造子弹 sequence_item,它的弹夹 sequence 是什么样的,弹夹 sequence 是如何放到手枪的后半部 sequencer 里面的,这些都还未学习。要想将子弹给装配到手枪中(数据建模),至少必须要有子弹 sequence_item、弹夹 sequence 以及手枪后半部 sequencer 才可以。 > 本文由简悦 SimpRead 转码
6.UVM 项目实践之 APB_SPI(4)APB_SPI 激励(Sequencer)产生【Sequence 机制】
- 通过 uvm_sequence 基类扩展用户 sequence
- 构建复杂的 sequence
- sequence 的嵌套:nested_sequence
- 配置 sequence
- 配置机制:uvm_config_db #(类型)::set(范围,路径,配置变量,配置值) 【获取用 get】
- 在不同的 phase 阶段执行不同的 sequence
- 启动 sequence 就是通过 config_db,所以可以通过 config_db 可以配置不同的 sequence 在不同阶段(phase)去启动
- sequence 的优先级和权重
- 多种 sequence 的调度
- sequence-driver 的乱序
- UVM 中使用 sequence 生成激励
- 通过 sequencer 将 sequence_item (对应 SV 中的 transaction)发送给 driver
产生激励的过程(数据流向):数据源头是通过继承 sequence_item 去建模生成数据类(transaction),然后再通过 sequence 封装起来,接着通过 Sequencer 将其发给 driver。
- uvm_sequence_item(包装数据):只对数据进行封装,不存在自动执行的函数
- uvm_sequence(生产数据):具有可自动执行的函数,可通过 body()函数进行可执行操作,产生数据激励
- uvm_sequencer(发送数据):将数据发送给 driver
2.1、uvm_sequence class 普通成员
- 事务(transaction)基类是 uvm_sequence_item
- 不存在任何自动执行的函数,仅仅是数据和对数据的处理,不能执行函数
- 在 uvm_sequence 中产生激励
- 具有可自动执行的函数
2.2、Sequence 执行流程
在配置阶段:
Sequence 的执行流程:
- 1、Sequencer:当 Sequencer 启动时,首先会检查自身的 default_sequence 是否配置,如果配置了就会创建实体,设置该 sequence 的 starting_phase 并随机化该 sequence,最后调用 sequence 的 start() 函数启动该 sequence。
- 2、Sequence:但 Sequence 的 start() 函数启动后,sequence 就会执行 body() 方法,产生事务,并将事务发送给 sequencer 放入 FIFO 中储存。Sequence 会等待一个 dirver 的完成响应,得到之后会退出该次事务的产生。
- 3、Driver:首先会向 Sequencer 发送一个事务请求,然后等待事务,sequencer 会将产生好的事务发送给 driver,driver 在处理完该事务后,返回一个完成响应!【UVM 中的 Sequencer 和 Driver 之间是握手的机制,这一点不同于 SV】
层次结构:
2.3、流程到代码的映射
- Driver 的 request 是一个阻塞的行为,通过 TLM Port 请求数据,请求到了之后才会继续下一步的数据处理
- Process 这里仅仅是举了个打印的例子,实际工程中大多会把 request 转成 interface 的信号
- 处理完毕之后,会通过 TLM Port 发送一个完成的信号
2.4、Sequence class 要求
- 验证工程师通过 uvm_sequence 基类派生所需要的 sequence
- body() 程序实现了 sequence 的代码
- `uvm_do 宏定义可以随机化创建并传递事务到驱动器(driver)
- sequence 使用 raise 和 drop phase objection 保证 sequence 满足整个仿真时间
- sequencer 默认执行 starting_phase
- 验证工程师默认显式配置 starting_phase
Sequence 的启动方式:
- 1、显式启动(直接启动)—— 调用 start() 方法启动
- 2、隐式启动 —— 使用 uvm_config_db 机制配置 default_sequence 启动
2.5、sequence class callbacks
- Sequence 执行 start() 程序时,会回调以下程序:
- callback 执行之前可以自己去写一些操作
- 比如在执行 start 之前,可以自己写一些操作放在 pre_start 里面,这样就不用改变代码的结构了
2.6、User Sequence with callback
- 在 base sequence 中 raise 和 drop objections
- 通过 base Sequence 扩展 user Sequence
2.7、工程师可以手动创建和发送 sequence_item
- `uvm_do* 宏定义实现以下功能(了解即可)
- 验证工程师可以手动执行这些内嵌的程序(了解,我们直接用 UVM 提供的即可)
- 也就是说宏定义可以直接用,也可以自己写(宏定义也是用 SV 写的,不能满足我们还可以直接拉出来修改)
2.8、Sequencer 与 driver 之间的通信机制
- 通信机制(伪代码)
- 通讯机制就是一个握手机制
- 1、driver 发 request 给 sequencer
- 2、sequencer 把 request 数据发给 driver
- 3、driver 处理数据
- 4、处理完后,driver 发一个 done 给 sequencer
- 层次结构
2.9、sequence 中的 randc 随机变量
- `uvm_do 宏定义不支持 randc 类型的随机变量
- 验证工程师可以执行实现
- 自己去写代码是可以支持 randc 的就是把 uvm_do 做一个展开
2.10、利用 rand sequence 数组实现不同的应用场景(scenario sequence)
- 不同场景(Scenario)的 sequence 是不一样的,可以定义一个动态数组的 transaction
2.11、嵌套 sequence
- sequence 可以嵌套
- 把三个场景通过 nested 变成一个场景,都实例化
- 也就是说 `uvm_do 宏定义不光应用于 sequence item,还可以应用于 sequences(它里面本质还是 sequence_item)
2.12、在测试用例中执行 user sequences
- 默认测试用例(test_base)执行 default sequence,该 sequence 是在验证环境(Environment)中配置的
- 默认是使用 default_sequence,通过 uvm_config_db (后续还会详细介绍)去配置的
default_sequence = transaction_sequence::get_type();
- 验证工程师可以在测试用例中 override 和 disable default sequence(override 执行 user sequence)
default_sequence = null;
- UVM 的两大机制:工厂(Factory)机制 和 配置(config_db)机制
- 工厂机制:主要作用是覆盖(override)
- 配置机制:config_db 中的 db 是 data base 缩写,主要作用是配置使用哪个 sequence
2.13、小结
要产生什么样的激励,把它定义好,后面引用不同的 sequence 即可!UVM 通信机制没有 SV 邮箱那么直接,而是进行了封装。整体流程和 SV 基本一样,UVM 我们先学会如何使用 API 就可以!(发送侧发送,接受侧接收)
注:工厂(Factory)机制 和 配置(config_db)机制。配置机制使整个环境连接变得丰富;工厂机制使用 override 可以修改组件而不改变环境。
3.1、基于 instance 配置 sequence
- 在测试用例中,通过 UVM 配置机制修改 sequence
- 通过 get_full_name() 获取配置的路径名称
发送方用
set
函数,把第④个参数赋给第③个参数,即:item_count = 20
。
接收方用get
函数,把第③个参数赋给第④个参数,即:item_count_1 = item_count = 20
3.2、基于 instance 配置 sequence 的好处
- 防止嵌入式 sequence 中有字段命名冲突
3.3、基于 sequencer 配置 sequence
- 在测试用例中配置 sequencer 中的变量
- 通过
get_sequencer()
获取配置字段的 sequencer
3.4、基于 sequencer 配置 sequence 的好处
- 在嵌套 sequence 中为所有的变量提供配置(common configuration)
- 基于 instance 配置 sequence ,一般用在配置不同值;基于 sequencer 配置 sequence ,一般用在配置相同值。
- 所有的 sequence 都是在 sequencer 里面的,所以基于 sequencer 配置 set 的时候只需要 set 一次,get 的时候分别在对应的 sequence 中 get 即可!
3.5、基于 agent 配置 sequence
- 通过 sequence 可以进行 agent 配置
- 只用于配置 agent
- 在测试用例中进行配置
- 通过 get_sequencer() 获取配置字段
配置一般都在 build_phase 阶段;有 set 必然要有 get;一个 set 可以对应多个 get,不会一个 get 对多个 set。
3.6、在 phase 中隐式执行 sequence
- 在不同的 phase 阶段可以执行不同的 sequence
- 通常在测试用例中进行配置
sequence 可以理解为 SV generator 里面的一个个场景,如 write32、write8 等等;get_type 可以理解为获取实例化名字。
3.7、显式执行 sequence
- 调用 start() 程序执行 sequence
3.8、sequence 的优先级 priority 和权重 weight(了解即可)
- UVM 可以设置 sequence 的优先级和权重
3.9、Sequencer-Driver 响应端口(response port)
- Driver 可以返回一个 response 给 sequence(握手机制)
- 层次结构
3.10、Sequencer - Driver 乱序端口
- Sequence
上述代码解释:通过 Sequencer 往 Driver 发一个 transaction 或者叫 sequence_item(即代码中的 req),就会在本地数组 pkt_in_drv 存储该 transaction,存储的时候是根据 transaction 设置的 ID 为索引的。Driver 响应回到 Sequencer 的时候也会将对应 transaction 对应的 ID 带回来,此时在将本地数组 pkt_in_drv 中删除对应 ID 的 transaction!这样就能保证所有的 transaction 发送完完毕后,pkt_in_drv 这个数组大小是为 0 的!
3.11、乱序 Driver
上述代码解释:Driver 收到 sequence_item 之后,不立即处理,而是把它放到一个队列中等待处理!队列处理时,是使用 urandom 这样一个随机函数去获取队列中存储的 sequen_item,通过这样来完成一个乱序的 Driver!
3.12、小结
本节还是我们把重心放在如何使用上,不要去深究源码!实际上将前面的 SV 项目做扎实之后,再理解起来本节内容至少在架构上会有个对照,学起来也不会太吃力!有些新的东西,初次学习可以先去记忆它的语法结构,后面再慢慢消化理解会好很多。
7.UVM 项目实践之 APB_SPI(5)配置机制(uvm_config_db)机制深入_8.UVM 项目实践之 APB_SPI(6)工厂机制(factory)机制深入
一、学习目标
- 理解 UVM 组件之间的逻辑层级关系【上一节】
- 利用逻辑层次关系实现对组件变量 / 字段的配置(set/get)【上一节】
- 利用工厂机制创建可以替换的事务和组件【本节】
二、工厂机制(factory)深入
2.1、管理测试用例的要求
- 测试用例需要能够修改类的变量(下面只是举的典型例子)
- eg:修改约束信息
- eg:插入错误条件
- eg:Driver 在发送数据前计算数据的冗余值 CRC
- 组件的实例可以根据变量或者全局信息进行配置
- 为全局配置或者特殊的对象,控制对象的位置
- 构造通用的功能
- 在仿真时创建精确的对象
注:工厂机制的目的是:一套验证环境支持各种测试场景和用例。
2.2、Testcase 要求:Transaction / Components
- 如何在不改变原始代码的条件下,为 transaction 的实例增加新的信息?
场景一:transaction 改成 new_transaction,且不改变 monitor,用 SV 的那种 new 方法做不到
场景二:driver 改成 new_driver,且不改变 agent,用 SV 的那种 new 方法做不到
2.3、解决方案:UVM factory【重点】
实现流程:
- 工厂的基础结构和寄存机制
- `uvm_object_utils(transaction_type)
- `uvm_component_utils(component_type)
- 通过宏定义创建代理类(type_id),并在 uvm_factory 中寄存这个代理类的一个实例
- 通过静态的代理类程序创建对象
class_name obj = class_name::type_id::create(...);
- 类的覆盖(下面两个函数的区别:在于覆盖的范围)【在 testcase 中使用 override 机制,可以创建所需的对象】
set_type_override_by_type(...)
;【全局覆盖】- eg:
set_type_override_by_type(transaction::get_type(), transaction_da_3::get_type());
- 第一个参数
transaction::get_type
得到的就是静态的代理类创建好的对象,是被替换的类。 - 第二个参数是替换的新类。
- 替换的本身不是对类进行替换,而是对类创建的对象进行替换,所以后面都会加
::get_type
。 set_inst_override_by_type(...)
;【局部覆盖】
使用工厂机制流程总结:
- 第一步做注册,用 uvm_object_utils 还是 uvm_component_utils 注册取决于定义的类本身的类型。【
sequence_item(transaction)、sequence
需要用 object;testcase、env、agent、sequencer、driver、monitor
属于 component】注册之后会对类创建一个叫type_id
的代理类。
- 第二步通过
type_id
代理类创建对象,即使用type_id
里面的create
函数创建单例类。
- 第三步是做类的覆盖(分两种覆盖方式,区别在于覆盖范围)。
2.4、Transaction Factory
- 在 Factory 中使用 create() 创建 transaction 的对象
2.4.1、UVM 工厂创建 object
2.4.2、在测试用例中替换 object
- 全局改变所有的事务类型
- 根据搜索路径改变实例(局部替换)
2.5、Component Factory
注:跟上面介绍的 Transaction Factory 基本都是一样的!
- 在 Factory 类中通过 create() 生成对象
2.5.1、UVM 工厂创建 component
2.5.2、在测试用例中替换 component
- 全局改变所有的 driver 类型
- 通过搜索路径改变实例
2.5.3、通过命令行进行替换
- 通过命令行,验证工程师可以替换工厂中的对象
- 必须使用工厂模式创建对象
- 作用类似于工厂中的替换机制
- set_type_override()
- set_inst_override()
- 示例
注:场景替换通常是针对部分用例,如果在命令行替换,Makefile 可能还要区分用例是否需要替换,Makefile 会变得复杂,不太通用!故,语法上提供的,并不一定在实战中好用!
2.6、检查 UVM 拓扑结构的正确性
- 利用
uvm_top.print(topology())
内置函数可以在测试用例中打印 uvm 的拓扑结构!
如下示例:
- 选择 UVM 拓扑结构的打印格式
- 以树形格式打印拓扑结构(参数:
uvm_default_tree_printer
)
如下示例:
注:上述拓扑结构打印代码可参考:https://recclay.blog.csdn.net/article/details/121331884
2.7、显式检查工厂的替换
- 必须利用
factory.print()
检查替换的对象
instance overrides
显示的是本地实例的替换
type overrides
显示的是全局类型的替换
set_type_override_by_type
覆盖打印信息覆盖打印信息
2.8、参数化的组件
- 必须使用不同宏定义来注册参数化的组件
注意其中带参数的工厂注册宏,有
param
关键字!另外,typedef 每次表示的 width 只有一种,不写 width,默认为 9,写的话直接填数字即可!eg: typedef new_driver #(3) new_driver_type;
常用的工厂注册的几个宏定义总结:
- 普通的工厂注册宏定义(以 component 为例):`uvm_component_utils
- 带权限管理的工厂注册宏定义(以 component 为例):`uvm_component_utils_begin
- 带参数的工厂注册宏定义(以 component 为例):`uvm_component_param_utils
- 带参数和权限管理的工厂注册宏定义(以 component 为例):`uvm_component_param_utils_begin
2.9、组件函数的最佳使用方法
- 通过派生的方式拓展组件的功能
- 使用小的虚方法实现类的操作,这些方法都可以利用多态的属性重载(OOP 多态)
可以回顾:【数字 IC 验证快速入门】37、UVM 验证方法学(2)(UVM 基础之面向对象)中的多态内容!
2.10、小结
本节又一次深入学习了工厂机制,使用工厂机制主要明白两点:第一步根据 object 还是 Component 使用不同的宏定义进行注册并自动创建 type_id 代理类;第二步通过 type_id 代理类的 create 函数创建对象;第三步进行替换(两种替换方式)。实现 override 主要是为了一套环境,实现多种测试用例场景。
9.UVM 项目实践之 APB_SPI(7)TLM(Transaction Level Modeling)通信机制
UVM TLM 通信机制是从 systemC 里面衍生出来的,在 UVM 里面用 event(线程与线程之间同步),semaphore(线程之间共享区域管理),mailbox(数据传输)进行通讯本身是没有任何问题的,只是 TLM 提供了更好的封装性。
- UVM 组件之间的通信使用 TLM port(TLM 1.0)/socket(TLM 2.0)
- TLM 1.0 一次传输只有一个 port;1.0 是自己定义的 transaction
- TLM 2.0 的 socket 一次传输有多个 port!2.0 提供数据格式内容
- TLM 2.0 generic payload(先定义好的 payload)
- UVM 组件之间的同步机制:uvm_event,uvm_barrier
- event 是 1 对 1 的,barrier 是 1 对多的
- uvm event 是在多个组件之间进行同步通信,而 SV 是一个组件内部多个线程同步通信!
- uvm_pool
- 多个组件之间同步,避免自行制作一个 event 在环境里面,具有更好的隔离性!
2.1、组件的通信接口
- 在验证平台各个组件之间需要交换事务(transaction/sequence_item)
- 【激励】Sequencer -> Driver
- 【响应】Monitor -> Collectors(Scoreboard,Coverage)
这张图是表明两个组件之间的通信是兄弟关系,即并行。
2.2、组件接口
2.2.1、method interface(1/3):TLM 提出背景
- 组件内嵌通信的任务
- 但是这种任务不能通过组件对象的句柄调用
- 对于验证平台而言,这种代码缺乏灵活性
- 只能与特定的 consumer 进行通信
- 产生数据:producer;获取数据:consumer
如果没有 TLM 机制,consumer 中的 put 函数要在 producer 调用的话,producer 必须引用 consumer,这样的话通信之间的组件要求存在父子关系依赖,这样代码的灵活性就大大降低。
2.2.2、method interface(2/3):避免依赖的中间方案 —— TLM 诞生
- 使用之间接口对象 TLM(UVM 快递)
通过 TLM 隔离了 Producer 和 Consumer!
2.2.3、method interface(3/3):TLM 的多用途
- 在测试用例中,可以使用中间接口组件 TLM,可以重新连接任意的组件
面试题:TLM 的好处?
- 可以让组件之间的通讯变得非常灵活,可以让任意一个组件进行通讯,而不改变组件之间的相互代码结构的关系,不改变验证环境的关系。
2.3、UVM 组件之间的通信:TLM1.0/2.0
- TLM Classes(中间的 * 表示端口可能含有很多类型,如 get/put(指明 port 的方向)、block/unblock 的组合)
- TLM 1.0
- uvm_*_imp
- uvm_*_export
- uvm_*_port
- uvm_*_fifo
- 如果一次想要传输多个数据,就需要 FIFO 做为中间缓存
- TLM 2.0
- uvm_*_socket
TLM 1.0 里面可以分成两类:非 FIFO(FIFO 深度为 1)和 FIFO,FIFO 在连接的中间起一个缓存的作用。非 FIFO (FIFO 深度为 1)又可分成三种端口:port、export、import。三种端口之间存在优先级,port 优先级最高(port 在图形中用方框表示)、export 优先级次高(export 在图形中用圆圈表示),import 优先级最低。端口优先级的用途:在端口与端口之间连接的时候,通常是优先级高的连接优先级低的!
port 所在一方,在上述图片中是属于发起通讯的一方,我们称之为
initiator
,通讯接收方我们称之为target
。(driver 通过 port 发送 request 到 sequencer,sequencer 通过 export 把数据送到 driver)。当组件之间不是传递一个 transaction 的时候,通常使用 FIFO 将其存储起来(使用 FIFO 将 port 和 export 连接起来),故 FIFO 在连接的中间起一个缓存的作用!数据传输路径一般理论上讲是从 port 到 export,再到 import 终结。(了解即可)最关键的是优先级高的端口连接优先级低的端口!
- 产生数据一方称为 producer,获取数据一方称为 consumer
- 发起通讯一方称为 initiator,通讯接收一方称为 target
- initiator 是 producer 还是 consumer 都是可以的,需要看实际的工作模式(push、pull),target 同理。
- push: producer(initiator)调用 put 函数,consumer(target)实现 put 函数
- pull:consumer(initiator)调用 get 函数,producer(target)实现 get 函数
- fifo:push + pull 两种模式
- 谁发起通讯,谁用 port;谁接收通讯,谁用 export/import。port 优先级高,用来连接 export/import。
2.4、UVM TLM 1.0
通讯方式:
- 一对一(点对点)通讯方式(peer-to-peer)【双向】
- push Mode
- pull Mode(常用)
- fifo Mode(push 和 pull 混合)
- 一对多广播通讯方式(one to many)/ 多对一通讯方式(many to one)【单向】
2.4.1、Push/put Mode 代码实现
2.4.2、Pull/get Mode 代码实现
2.4.3、FIFO Mode 代码实现
- 通过 uvm_tlm_fifo 连接 producer 和 consumer
2.4.4、Analysis Port(广播)【单向】
- 广播的 Subscriber 可以不连接
- 在 subscriber 中实现 write 函数,在 Producer 中调用 write 函数
上述示意是一对二,发送的是 port,接收的是 export!
注:上面讲的几个代码是 TLM 代码的内部实现示意,即 TLM 处于某种模式时,实现是哪一方编写,调用是哪一方完成。实际上 UVM 是封装好这些的,我们可以端口定义好拿来直接用!
2.4.5、Port pass-through
- 使用相同的端口类型
上述是 monitor 和 scoreboard 进行通信的示意图,monitor 需要通过 agent 才能与 scoreboard 进行通信,此时经过的 agent 要使用与 monitor 相同的端口类型!
2.5、UVM TLM 2.0
注:UVM TLM 2.0 是在 UVM TLM 1.0 基础上做的扩展,2.0 兼容 1.0,但现在业内 2.0 用的还是比较少,1.0 已经能够满足大部分使用要求!
UVM TLM 2.0 不叫 port 了,而是叫 socket,socket 实际是把 port 和 export 封装起来了,也就是说对于一个端口来讲既有 port 也有 export。
- Blocking(单向)
- Non-Blocking(双向)
相比于 1.0 来讲,2.0 增加了一个对通讯的时间控制 delay(EDA 逻辑验证,时序验证暂不关注),增加了几个 phase,其它的看不看都行,下面对于 TLM2.0 介绍也是了解即可,用的少。(提纲挈领,抓住主干学习)
没必要管时序,真正管时序是到了 driver 组件,这样 UVM 各个组件可以更加内聚一点(高内聚)。
2.5.1、Blocking transport initiator
2.5.2、Blocking transport target
2.5.3、Non-blocking transport initiator
- 具有 forward 和 backward 两个路径
initiator 中实现 backward(bw),调用 forward(fw);target 中实现 forward(fw),调用 backward(bw)。
阻塞的传输方式通过 blocking 的前缀来作为函数名的一部分,而非阻塞的方式则名为 nonblocking
2.6、TLM 2.0 常用负载
- TLM 2.0 常用负载时总线读写的数据
- 用于跨平台交互(比如 SystemC)
- 总线传的就是读写命令,空命令的作用是可以丰富场景;
- 属性 attributes
- 总线的核心要素是地址和数据以及一些控制
- 2.0 的 transaction 给了我们一个模板
- 处理函数 Accessors
有 set… 设置,和 get… 取过来,is 就是判断
- 可扩展
- 通过派生特性,可以在 TLM2.0 负载中添加新成员
- 需要使用
uvm_tlm_textension
类实现功能的扩展 - 知道有这么一个扩展功能就行,代码不用深究
2.7、组件同步(Component Synchronization)
- uvm_event(做同步)
- 等到一个触发信号,可以释放所有的 wait 语句
- uvm event 和 sv 的 event 的主要区别在于它还可以传数据,就不是一个简单的消息同步机制了,还可以 cancle,长时间等不到可以取消。uvm event 可以做组件之间的同步(配合 uvm pool 使用),SV 的 event 只能做组件内部的同步!
- uvm_event 相比 sv event 可以传数据
- uvm_barrier 阻止(新增加的,可以配置等待的数量)
- event 是 1 对 1 的,barrier 是 1 对多的!
- 等到多个 waiter 之后,释放所有的 wait 语句
- uvm_barrier 可以设置等待的数量
总结:uvm_event 可以传数据,barrier 可以设置等待的数量
2.8、UVM_POOL
注:POOL 定义是一些公共事件资源池。
- uvm_pool class
- uvm_pool 类用于创建自定义的 db pools
- 特殊的 string pools
- pool 最常见的格式是字符串索引的 pool
- 在 UVM 定义了两个同步 pools
- uvm_event_pool 是由 uvm_event 组成的 pool
- uvm_barrier_pool 是由 uvm_barrier 组成的 pool
10.UVM 项目实践之 APB_SPI(8)计分板(Scoreboard)和覆盖率(Coverage)
一、学习内容
- 自动比较的计分板 self-checking scoreboards(提供的类:uvm comparator)
- 使用 UVM 内建的 comparator 类,构建可重用的自动检查的计分板
- 功能覆盖率 functional coverage(老生常谈)
- 验证完备性
- 与 SV 类似
二、Scoreboard
2.1、Scoreboard 简介
- 挑战
- 自动比较的验证平台需要计分板(scoreboards)
- 1、提升效率;2、质量更高
- 开发完 scoreboard 之后,可以在不同的验证平台中重复使用
- 不同的应用场景使用不同的 scoreboard
- 必须知道 DUT 中的数据转换(相当于内嵌了 Reference Model)
- 解决方案
- UVM scoreboard class 扩展了函数功能,用于比较期望数据和观察到的数据
- 按照一定的顺序
- 数据转换
- 在 comparator 类中内建(built-in)了 analysis exports(TLM)
2.2、Scoreboard:transaction stream
- 数据序列可以是任意(Arbitrary)的
上图表达场景:不同场景的数据格式和时序千差万别!(不需要理解图中具体含义)
2.3、Scoreboard 实现方法
- 使用
uvm_in_order_class_comparator
做检查(in_order:按照输入顺序比较,等的话 match,不等就是 mismatch)
2.4、Scoreboard:monitor
- monitor 为 scoreboard 提供期望的数据和实际 DUT 数据
interface 是通过 config_db 机制在顶层 testcase 中 set ,然后在具体的组件中 get,比如上图中的 monitor!
2.5、Agent 中嵌入 monitor
- 通过 uvm_agent 派生 agent
- 包含 driver、sequencer 和 monitor
- 包含配置信息和其他参数
- Agent 的两种操作模式
- ACTIVE Mode:
- 模拟 DUT 的系统接口器件
- 例化了一个 driver、一个 sequencer 和一个 monitor。
- PASSIVE Mode:
- 被动操作
- 仅仅实例化了监视器和配置参数
上图 UVM 环境结构图要熟记!
2.6、uvm_agent 源代码
究竟是 ACTIVE 还是 PASSIVE 是在更上一层的 ENV 中决定的!
2.7、UVM agent 示例
2.8、验证平台中的 agent 的配置
- agent 简化了验证环境
- 分层次的验证平台更容易维护和调试(可重用 )
有 agent 一层的封装,避免一个 env 里有两个 monitor!
2.9、参数化的计分板
计分板可以参数化
- 必须使用 uvm 宏定义 uvm_component_param_utils
- 在 environment 中设置参数或者使用默认值
2.10、scoreboard:数据格式转换
- 如果数据需要转换格式
2.11、Scoreboard:Out-of-order 乱序比较
- 可以实现一个乱序比较的计分板
- 需要使用事务队列存储接收到的事务数据
- 使用标签搜索获取潜在的匹配对象
- 否则,性能大受影响
2.12、Scoreboard:multi-stream(了解)
- 同时比多个流,故这里参数多了个 num;comparator 有 num 个
三、Coverage
3.1、功能覆盖率
- 检查随机激励,跟踪验证进度是否满足验证目标
- 检查什么内容?
- 配置信息 Configuration
- 验证平台是否使用了所有的可能性
- 多个驱动 drivers,多个 slaves,总线地址
- 激励 Stimulus
- 验证平台是否产生了所有的数据事务,包括容错处理数据?
- 读、写、中断处理、长数据事务、短的连续传输、重叠操作等
- 正确性 Correctness
- 针对输入的激励,DUT 的输出响应是否正确?
- 读、写、中断处理、长数据事务、短的连续传输、重叠操作等
3.2、SystemVerilog 验证平台的结构
3.3、配置信息覆盖率
- 在测试用例中创建配置覆盖率的组件
显示数据传递总图
3.4、输入激励的覆盖率
- 覆盖 monitor 观察到的数据事务
3.5、覆盖率的正确性
- 在计分板中通过数据比较,保证了覆盖率的正确性
Coverage 放到 scoreboard 还是 env 里面都是可以的!
四、总结
本小节内容量还是有点多的,需要各位读者好好消化。虽然 UVM 提供的 Scoreboard 机制是挺好,但实际上里面的 Reference Model 仍需要自己写,因为不同项目不一样,无法做到通用。所以实际项目中,我们手动实现 Scoreboard 也是可以的。
对于 Scoreboard 这块内容,个人认为也并不是 UVM 最精髓的,最精髓的还是前面所述的 Factory 机制、Config_db 机制以及 TestBench 整体架构,所以这块知道如何使用即可!
11.UVM 项目实践之 APB_SPI(9)回调机制(CallBack)
1.1、学习内容
- 嵌入 UVM CallBack 函数
- 构建 Facade UVM CallBack 类(空壳)
- CallBack 应用
- 使用 UVM CallBack 机制注入错误(error injection)
- 使用 UVM CallBack 机制实现功能覆盖率
1.2、学习目标
- 掌握四步编写 UVM Callback 的方法
- 通过 callback 机制实现错误注入
- 通过 callback 机制实现覆盖率统计
2.1、改变 UVM 组件的功能行为
- 如何增加或者修改 UVM 组件的操作?
- 一种方式是嵌入 CallBack
- pre_send 做异常注入
- post_send 做覆盖率收集
2.2、编写 UVM CallBacks 代码(重要)
- 使用 UVM CallBack 机制增加新的功能,不需要创建复杂的 OOP 层次结构
- 四个步骤
- 在 UVM 组件中内嵌 CallBack 函数或任务
- 创建一个 UVM CallBack 空壳类(Facade Class)
- 从 UVM CallBack 空壳类扩展 UVM Callback 类
- 在验证环境中创建并登记 UVM CallBack 实例
2.2.1、第一步:嵌入 CallBack 函数 / 任务
- 在主操作函数或者任务之前或者之后嵌入 CallBack
2.2.2、第二步:声明空壳类(Facade Class)
- 在 uvm_do_callbacks 宏定义中创建空壳类
- 通常声明在同一个组件文件中
- 所有的方法都声明为 virtual
- 函数体为空
2.2.3、第三步:编写 CallBack 方法(Error Injection)
- 通过扩展 CallBack 空壳类创建错误类 error class
- 在 CallBack 任务中嵌入错误信息
- 需要自行编写具体内容
2.2.4、第四步:在测试用例中创建并登记 CallBack 的实例(对象)
- 在测试用例中例化 CallBack 对象
- 创建并登记 CallBack 对象
2.3、通过 CallBack 收集覆盖率
2.3.1、第一步:嵌入 CallBack 函数 / 任务
- 可以使用 callbacks 收集覆盖率
2.3.2、第二步:声明空壳类(Facade Class)并编写 CallBack 方法
- 通过空壳类创建覆盖率的类
- 在覆盖率类中定义 Covergroup
- 使用构造函数创建 Covergroup 实例
- 在 CallBack 任务中对覆盖率采样
2.3.3、第三步:在测试用例中创建并登记 CallBack 的实例(对象)
- 在验证环境中例化 CallBack 对象
- 在 Build_phase 创建并登记 CallBack 对象
2.4、CallBack 调试方法
- EDA 工具编译选项
- +UVM_CB_ TRACE_ON
callback 上面只是在 driver 中举例,但是它在所有组件中都可以使用!
2.5、UVM sequence 中的 callback
上面的这些 callback 通常是空的,我们实际使用是去做覆盖。
2.6、小结
callback 机制的核心目的是为了实现一个环境,能够支持不同的场景和用例。相当于安插些钩子在里面,可以在正常行为发生之前和之后去产生不同的东西,就可以让这个行为变得更加丰富。具体的语法点,能够参照例子写出来即可!
常见面试题:讲一下 callback 机制?
- 答:callback 机制针对组件的一些行为,可以在这些行为之前或者之后,可以先内置一些函数在里面,而这些函数具体实现什么功能,可以根据不同的用例进行一个重置。以期做到:不修改验证环境,可以实现不同的用例。当然除了 callback 的方式,还可以通过 override 的方式新增一个组件,override 环境里面旧的组件。具体使用哪种,看具体情况怎么取舍。如 driver 修改比较大,那就通过 override 的方式;如果 driver 只是小修小补,那么就可以用 callback 机制!
callback 更多的是增强我们 UVM 验证环境的功能,但并不是说只能使用 callback 实现某些功能(如:故障注入功能,实现方式并不唯一,还可以通过修改 sequence_item 实现),在实际工程中需要灵活使用。这里之所以写专门的一篇文章学习,是为了向读者展现 UVM 的全貌,了解这种机制的存在即可。callback 的作用是增强或修改组件的某些功能,如通过 callback 机制可以做到在某些用例或场景中增强。
12.UVM 项目实践之 APB_SPI(10)Component Phase(组件执行顺序)
2.1、UVM 中组件的执行顺序 Phase
- 同步 Phase 执行
- UVM 预定义的两个域 “Phase domain”
Common Phase
- 组件功能简单(driver/monitor)【具备以下顺序,但并不是说非得在这些顺序中做一些事情】- Build(不占用仿真时间)(phase 的设置,即里面要干的事,只是一个建议性的东西,不要死记硬背,当成葵花宝典,一定要灵活应用!)
- build_phase(做一些配置工作)
- connect_phase(组件之间连接工作,当然放到 build_phase 也可以,只是人为定义的一些思路或者叫验证的方法学)
- end_of_elaboration_phase
- Run(占用仿真时间)
- start_of_simulation_phase
- run_phase
- …(具体包含 phase 见下文)
- Clean up(不占用仿真时间)
- extract_phase
- check_phase
- report_phase
- final_phase
UVM Phase
- 组件功能复杂(test/environment/scoreboard)【上面的 Run_Phase 阶段,】- 核心 4 个 Phase:reset -> configure -> main -> shutdown
- 具有
pre_*/post_*
phases,一共 12 个 phase,重点记忆:reset、configure、main、shutdown
【前后的 pre(准备工作)和 post(善后工作)是回调】 - pre_reset
- reset
- post_reset
- pre_configure
- configure
- post_configure
- pre_main
- main
- post_main
- pre_shutdown
- shutdown
- post_shutdown
- 为什么要定义这些 phase(主要是
reset、configure、main、shutdown
)?模拟芯片正常工作的行为:reset -> configure -> main -> shutdown。简单的验证,phase 并不会分那么多,一定要灵活应用!(如:AHB-SRAMC 没有 configure phase)这里仅仅是提供一些思路或方法!
- 灵活性高
2.1.1、Common Phases
如上所讲,phase 的设置,即里面要干的事,只是一个建议性的东西,不要死记硬背将其当成葵花宝典,一定要灵活应用!
接下来了解一下 Phase 的各个阶段建议去做的事,没有按照 UVM 方法学的一些规范去做,只有好与坏之分,没有错与对之分!
阶段 | 建议设置 |
build phase | 创建并配置验证平台的架构 |
connect phase | 建立 UVM 验证平台各个组件之间的连接关系 |
end_of_elaboration phase | 检查验证平台架构的正确性 |
start_of_simulation phase | 在产生激励阶段设置配置信息 |
run | 运行仿真 |
extract | 从不同的节点提取数据,进行对比验证 |
check | 在验证环境中检查任何意外情况 |
report | 报告测试用例的仿真结果 |
final | 停止循环,结束仿真 |
2.1.2、Run Phases
- 作为一个方法学是一个大而全的,但是在实际项目中并不会各个 phase 都会使用,需要注意!
阶段 | 建议设置 |
pre_reset | 对 DUT 复位前,设置或等待复位条件 |
reset | 对 DUT 进行复位,其中复位信号有效 |
post_reset | 复位完成之后,等待 DUT 进入初始化的状态 |
pre_configure | 在配置 DUT 之前,设置或等待配置的条件 |
configure | 配置 DUT 的参数信息 |
post_configure | 等待 DUT 进入配置的状态 |
pre_main | 启动 DUT 执行核心流程前,设置或等待执行条件 |
main | 对 DUT 进行验证:输入激励,检查响应 |
post_main | 通常无任何操作 |
pre_shutdown | 通常无任何操作 |
shutdown | 处理验证平台中的数据 |
post_shutdown | 做最后的检查 |
2.2、uvm_phase 类中核心方法(函数)
- 主要知道常用有:raise_objection、drop_objection 以及 jump 三个即可,其他的用到(看别人代码)时再去查资料即可!
计数器为 0,结束 phase!
2.3、phase 同步 synchronization(重要 )
- 在运行仿真时,只有当所有组件的当前 phase 都执行了 drop objection 后,才会进入下一个 phase
重要知识点:
- 1、所有的组件都具备:reset -> configure -> main -> shutdown 这四个 phase,但并不意味着必须在每个 phase 都执行相应的动作!
- 2、当所有组件的当前 phase 都执行了 drop objection 后,才会进入下一个 phase。【同步】
- 如上图:Component A 执行 Reset Phase 需要 2ns,Component B 执行 Reset Phase 需要 1.7ns,Component C 执行 Reset Phase 需要 1.5ns,但是只有当最后一个组件的 Reset Phase 的 drop objection 执行完,三个组件才会进入到下一个 Configure Phase!【这就叫同步】
- 各个 Phase 可以比喻成组团旅游的景点,而各个 Component 可以比喻成组团的游客。只有当所有的游客把当前经景点都看完,才一起组团进入下一个景点。并不是所有的 Component 都有要在四个 Phase 执行具体的事,可以类比成某个景点,某个游客根本不感兴趣,不会去拍照打卡,只是在这个景点原地等待着其他游客打卡归来,才一起前往下一个景点。
- 3、Component 必须有这四个 Phase(同步其他 Component 对应的 Phase 阶段),但并不意味非得在的相应 Phase 中做事情!
2.4、Phase Objection 机制
2.4.1、Phase Objection 机制(1/6)
注:Objection 机制可以理解为举手、放下,控制内部的计数器加、减 count,默认加减 1!
- 通常在仿真开始时:raise objection
- 在 sequence 中 objection 机制的使用方法
starting_phase 实际上就是 run 里面的 main_phase!
下面那个程序是在 env 中配置 sequencer 的 main_phase,并设置好 sequence(替代 default_sequence),启动 sequence 传输!
2.4.2、Phase Objection 机制(2/6)
- 在组件的 run_phase 中没有 objection
- objection 的数量过多会影响仿真
- Driver 和 Monitor 的 Phase 都通过 Sequence 的 Objection 控制,不在它们里面控制。
在 sequence 的 pre_start 以及 post_start 中去做回调的处理
删除线是不建议这样使用!
2.4.3、Phase Objection 机制(3/6)
- 在测试用例中使用 drain time 设置明确的延迟时间
因为我们各个 Component 的 Phase 控制是在 Sequence 中进行的,而 Sequence 控制的是激励的发送开始和结束时刻,并不代表在 DUT 另一侧的接收也完毕,因为中间还有 DUT 的处理延迟,所以,会使用 drain time 设置明确的延迟时间!当然,此处的时间控制是绝对时间。但是,不同用例的处理和采集可能不一样,设置绝对时间并不是很好的选择,如何知道结束的准确时间呢?请看下一个小节!
2.4.4、Phase Objection 机制(4/6)
- 在 ScoreBoard 中必须考虑不确定的延迟时间
采用 callback 机制,在 scoreboard 组件里进行控制,要想结束 shutdown_phase 只有当 end_of_test 这个函数结束了才会 drop_objection。而 end_of_test 这个函数干什么事呢?下一小节。
2.4.5、Phase Objection 机制(5/6)
- 在测试用例结束时,CallBack 可以获取 DUT 的信号
这里的 end_of_test 里的内容只是举个例子,不同 DUT 当然采用不同处理手法。这里是采样 DUT 里面的 done 接口信号,等到 done 信号才会结束上面 scoreboard 的 shutdown_phase。
callback 实际上更多还是为了平台化,针对不同的项目只需要去改变 end_of_test 这个 task 实现。如果不考虑平台重用性,直接在 scoreboard 的 shutdown_phase 加一个 @(duv_if.done) 等待 done 信号也是可以的!加入 callback 机制是为了环境能够重用,在不同的项目中只需改变 end_of_test 的 task 实现。而 testcase 以及 scoreboard 中的代码结构不用改变!
2.4.6、Phase Objection 机制(6/6)
注:通过 raise_objection 启动 Phase,通过 drop_objection 结束 Phase。而实际如果仿真在 drop_objection 还没结束的话,可以通过 set drain time 设置绝对时间,让其延迟结束。或者也可通过握手机制,即采样 DUT 内部接口信号,得到真实的结束时刻,再进行结束仿真!本节将要介绍一下第三种,万一握手有错误或者 drain time 也有问题(如 raise_objection 中的 count 设置的是 2,而 drop_objection 设置的是 1,那么永远结束不了),可以设置一个超时(TimeOut 门限)。
超时机制
- 如果 objections 没有 drop 掉,phase 超时机制可以停止 phase 执行
- +UVM_TIMEOUT
- EDA 工具仿真时的命令行选项
- 在每一个 phase 中都可以设置超时
- phase 超时的默认是 9200 秒(将近 2 个半小时)
- 超过该值,打印错误或者警告信息
- uvm_top.set_timeout(.timeout(1ms), .overridable(1))
- 使用该语句可以覆盖 EDA 工具命令行中 +UVM_TIMEOUT
- 嵌入 TestCase 代码中
- 如果. overridable 设置为 1,那么不同的 phase 可以设置不同的值
- 在调用 set_timeout() 执行,phase 默认执行 + UVM_TIMEOUT
2.5、小结
重点掌握 Phase 管理机制,各个 Component 都有,但是有 Phase 并不一定非要做具体的事情。另外所有 Component Phase 转换是同步机制,所有 Phase 要转一起转(跳转到下一个 Phase),等到所有 Component 都结束当前 Phase 的时候,才跳转下一个 Phase。另外,Phase 的控制,启动是通过 raise_objection,结束时通过 drop_objection。另外还在 drop_objection 做了一个增强,去设置 set drain time,以此达到延迟结束时间。当然,还可以通过握手机制去采集 DUT 接口信号状态,来得到真实的结束时刻,进而 drop_objection 将其做得更保险一点!最后还通过一个超时机制来停止 Phase 执行,超时是为了异常保险,不建议所有都使用超时,因为 raise 和 drop 之间可能实际是 0.5us,因为设置超时通常会设置的大一点,那么对仿真时间是一种浪费,而且不真实。
学这么多机制的目的要时刻提醒自己,就是为了 UVM 平台通用化,可以让一个平台支持多种类型,多种规模的验证!
注:UVM 的进阶这里仅仅做一个了解即可,现阶段可不必深入学习
3.1、高级特性(了解)
注:对于 IC 验证,搭建验证平台的工作量可能只占 50% 左右,剩下 50% 左右的工作量属于项目自身的业务。
- Phase 域(domains)
- UVM 有一个默认的 phase 域
- 在同一个域中各个组件的 phase 是同步的(synchronized)
- 默认情况下,内部 phase 域不需要同步(不同的域之间)
- 验证工程师可以设置同步信息
- 验证工程师自定义 phases【了解】
- 可以混合使用 UVM 预定义的 phases
- Phase 跳转(jumping)【常用】(异常测试跳转到 reset,这个常用!)
- 往前跳
- 往后跳
注:只推荐验证平台开发工程师使用这些特性;组件开发工程师不需要使用这些高级特性!
3.2、Phase 域 domain
- 执行时的 phase 按照域进行组织
- 仿真时间有两个域:common 和 uvm
- 两个同步的线程并行执行(run phase 和 uvm phase)
- 工程师可以自定义域(了解)
3.4、工程师自定义 phase
- 验证工程师可以自定义 phase
3.5、Phase:往后跳 BackWard
- Phases 可以往后跳转
- 在仿真中间,reset 有效时,当前 phase 可以跳转到 reset phase
- 验证平台必须支持 phase 跳转
- 所有组件必须实现 reset_phase() 用于利用控制信号返回复位状态
- 必须清除残留的属性
- 必须杀掉所有残留的进程(下面有例子)
上图是从 main_phase 跳转到 reset_phase 的例子,这个在工程中常用!
3.6、Phase 跳转:向前跳 forward
- 在测试用例中,使用 Phase 往前跳,可以跳过一些操作
- 例如:如果覆盖率已经满足要求,就可以跳转 main phase 中的 reset phase
- 验证平台必须能够支持 phase 跳转
- 当调用 phase 进行跳转时,必须清除 objection(不清除的话内部计数器不为 0,不会进行 phase 切换)
3.7、Phase 跳转代码示例:driver
- Driver 中 Phase 跳转代码示例(1/2)
- Driver 中 Phase 跳转示例(2/2)
3.8、Phase 跳转示例:monitor
- Monitor 中 Phase 跳转代码示例
3.9、Phase 跳转代码示例:Scoreboard
- Scoreboard 中 Phase 跳转代码示例
3.10、Phase 回调机制
- phase_started
- 当 Phase 开始执行时调用 phase_started 函数
- phase_ready_to_end
- 当 phase 中所有的 objection drop 掉之时,调用 phase_ready_to_end 函数
- phase_ended
- 当 phase 终止时,调用 phase_ended
3.11、获取 Phase 执行数量
- get_run_count
- 该函数返回当前 phase 执行的次数
3.12、组件 phase 的编码原则:driver(参考)
- 驱动并采样寄存器
- 在 build_phase 阶段
- build_phase() // 获取配置信息
- 在 run_phase 阶段
- start_of_simulation_phase() // 打印配置信息
- run_phase() // 获取并处理事务数据
- 在 cleanup_phase 阶段
- 回调 callbacks
- phase_ended() // phase 的跳转
3.13、组件 phase 的编码原则:monitor(参考)
- 采样寄存器
- 在 build_phase 阶段
- build_phase() // 获取配置信息
- 在 run_phase 阶段
- start_of_simulation_phase() // 打印配置信息
- run_phase() // 报告通过 analysis_port 观察到的事务数据
- 在 cleanup_phase 阶段
- 回调 callbacks
- phase_ended() // phase 的跳转
3.13、组件 phase 的编码原则:agent(参考)
- Agent 是一个容器类,包含基于接口的组件
- 没有行为代码
- 在 build_phase 阶段
- build_phase() // 生成子组件,获取并设置子组件的配置
- connect_phase() // 连接各个子组件
- 在 run_phase 阶段
- start_of_simulation_phase() // 打印配置信息
- 在 cleanup_phase 阶段
3.14、组件 phase 的编码原则:scoreboard(参考)
- 跟踪操作的正确性
- 在 build_hase 阶段
- build_phase() // 创建子组件,获取配置信息
- connect_phase() // 通过 analysis ports 连接子组件
- 在 run_phase 阶段
- start_of_simulation_phase() // 打印配置信息
- shutdown_phase() // 实现结束测试用例的回调
- 在 cleanup_phase 阶段
- report_phase() // 报告自动比较的结果
- 回调 callbacks
- phase_ended() // phase 的跳转
3.15、组件 phase 的编码原则:environment(参考)
- 验证环境 environment 是一个容器,包含验证 DUV 所需要的所有组件
- 没有行为代码
- 在 build_phase 阶段
- build_phase() // 创建子组件
- connect_phase() // 连接子组件
- end_of_elaboration_phase() // 检查验证环境的结构,打印 UVM 整个组件信息
- print_pology()
- factory 注册情况也可以打印
- 在 run_phase 阶段
- start_of_elaboration_phase() // 显示配置信息
- 在 cleanup 阶段
- report_phase() // 报告验证环境的整体概述
3.16、组件 phase 的编码原则:test(参考)
- 对 phase 没有约束和限制
- 在 build_phase 阶段
- build_phase() // 创建子组件,设置验证环境的配置信息(环境配置,如激励数量)
- connect_phase() // 连接子组件
- 在 run_phase 阶段
- start_of_simulation_phase() // 显示配置信息(功能配置,如配置存器)
- run_phase() // 动态配置可变的配置信息
- reset_phase() // 执行 reset sequence
- configure_phase() // 执行 configure sequence
- main_phase() // 执行 stimulus sequence
- shutdown_phase() // 添加测试用例结束的条件
- 在 cleanup_phase 阶段
- final_phase() // 报告验证平台所有的结果
本节首先学习了 Phase domains,默认有两个,common phase 和 uvm phase 。另外,通过 raise_objection 启动 phase,通过 drop_objection 结束 Phase。时间控制的话,可以在 drop_objection 之后通过 drain time 控制 phase 绝对延迟时间,还可以通过与 DUT 握手采集它的一些信号进行时间控制,还有一种是设置超时时间。接着,我们还学习了自定义 phase 和 doamin,但实际工程中用的比较少,这里仅作了解即可。callback 机制也是作为拓展知识,了解即可。最后,我们学习了编码原则,该原则给出的知识建设性的意见,在实际使用中,我们可以灵活使用!当然,想要更好理解这些理论知识,最好的办法是结合我们后面的实践项目,所以本节只是学个概览,有个大致印象,后面忘记了,再来反复查阅即可! > 本文由简悦 SimpRead 转码
13.UVM 项目实践之 APB_SPI(11)Virtual sequence & sequencer(虚序列和虚序列器)
- 在特定的 phase 中,使用 Virtual Sequence 和 Virtual Sequencer 控制 Sequence 的执行顺序
- 不同 agent 中的 sequence 执行顺序控制
- 在 virtual Sequence 中管理 Sequence 执行的同步
- uvm_event
- uvm_barrier
2.1、Virtual Sequences
- 同一组序列如何在多个 agent 之间并行运行?
- 例如:DUT 中一部分逻辑的复位操作必须在另外一部分完成复位序列之前完成
- 解决方案:
Virtual Sequences
- 显性管理同一个 phase 在多个 agent 中执行的 sequence 顺序
- 可以很好的控制 sequence 执行的顺序
virtual Sequence
中没有自己的 sequence item,只用于管理序列的执行
- 当多个 agent 继承到更高层次的模块中时,如何管理每个 agent 中 sequencer?
- 输入到 DUT 多个接口的激励的时序可能需要同步
- 解决方案:
Virtual Sequences
- 同步不同接口的数据和时序
- 可以很好的控制验证环境
- 没有自己的数据,只用于控制其他序列或序列驱动器
2.2、Virtual Sequence/Sequencer
- 通常一个 Sequence 只跟 DUT 的一个接口交互
- 当 DUT 的多个接口需要同步时,就需要使用 Virtual Sequence
- 在验证环境或者测试用例中,使用 Virtual Sequences 同步不同接口之间的数据和时序
2.3、虚序列器 Virtual Sequencer
- 嵌入序列器,管理序列
只是声明了 virtual sequencer 中的两个 sequencer 句柄
2.4、虚序列 Virtual Sequence
- 不同的 agent 中具有自己的 Sequence
- 在 body() 任务中,控制这些序列
连接两个 virtual sequence 句柄到两个 virtual sequencer 句柄
2.5、将 sequencer 连接到 virtual sequencer(1/2)
- 在 build_phase 阶段创建 virtual sequencer
- 在 default_sequence 设置成 null,关闭响应的 sequence
- 配置 virtual sequencer,在需要的 pahse 阶段执行 virtual sequence
设置 virtual sequencer 的 default sequence
2.6、将 sequencer 连接到 virtual sequencer(2/2)
- 在 connect_phase ,将被控制的 sequencers 与 virtual sequencer 关联在一起
连接 virtual sequencer 和 sequencer(两个分别连接)
- 在 testcase 的 main_phase 阶段,配置 virtual sequencer 用于执行 virtual sequence
2.7、Sequence 执行管理
- 如果需要在一个 Sequencer 中间启动另外一个 Sequence,怎么处理?
- 通过 uvm_pool 使用 uvm_event(不同的组件之间需要有一个公众的资源池,uvm_pool)
2.8、同步机制:uvm_event
注:可以携带数据
- 等待一个触发器(one trigger)释放所有的等待
2.9、同步机制:uvm_barrier
注:等待次数可以配置
- 等待 waiters 的数量达到阈值,然后释放所有的等待
2.10、同步使用特定资源池
- 在 UVM 中使用两个特定的资源池,可以简化 uvm_event 和 uvm_barrier 的使用(多个组件之间)
- 通过资源池,events 和 barrier 都是全局可见的!
- 最常使用的 uvm_object_string_pool 类的成员
2.11、uvm_event_pool 触发 trigger
- 触发全局可见的 reset 事件
2.12、uvm_event_pool 等待触发 wait for trigger
- 全局可见
2.13、保护激励(独占 Grab / 释放 Ungrab)
- Sequence 可以显式占用 Sequencer
- 直到显式释放 sequencer
- 其他的激励请求(request)和注入(injection)将会被阻塞
2.14、资源池:uvm_pool
- uvm_pool 类可以用于创建自己的数据库资源,和上面的 uvm_event_pool 类似
- uvm_pool 是 UVM 定义好的资源池,可以自行往里面加东西,不用太细看,用到再查也是可以的!
- 资源库里面的内容是全局的,当然做成全局也有坏处,如一个变量放入资源池做全局,跑所有用例都是相同的值,或者不同组件赋不同值就有问题了,还是需要看实际需求。
2.15、小结
本小节重点是可以通过把不同组件的 Sequencer 放入到 Virtual Sequencer 里面控制他们的执行顺序, 控制顺序的主要手段是:uvm_event 和 uvm_barrier,通过事件触发去控制两个组件之间的顺序。接着后面学习了 uvm_pool 和 uvm_event_pool,作用是类似的,定义的一些公共变量可以放入里面,可以减少变量在环境之间的穿插。当然,还有一个是独占(Grab 和 Ungrab),但是这个在实际中用的不多,了解即可!
心里话:这个算是个增强的知识点,我们在实际中也有使用,但是使用的大多是简单的语法点,并没有上面学习的那么复杂。实际项目中发送的激励有不同的激励,激励之间也需要一些时间的控制,这个可以通过 Virtual Sequence 来把它放到一起,然后通过线程通信的控制语句来进行控制。
3.1、序列库 Sequence Library
- 当一个 Sequencer 需要执行多个 Sequence 时,如何进行管理?
- 比如,在执行随机选择 Sequence 时,如果执行直接测试的 Sequence(类似 SV 中的 write32 write8 等)
- 解决方法:序列库 Sequence Library
- 组织和管理多个 Sequence 的执行
- 默认随机执行序列库中的序列
- 允许验证工程师,通过编写特定的算法,抓取序列库中的特定序列
3.2、定义相关序列
- 定义相关序列
- 将所有序列几种放在库中
3.3、建立序列库包
- 建立序列库包
- 向包中添加普通序列
- 序列基本库为空
3.4、在验证环境中引用序列库
- 在 environment,为序列器(sequencer)配置序列库的默认序列
3.5、登记和执行序列
- 在 program 语句块中输入库包文件
- 在 testcase 中登记序列库中的序列
- 在测试用例中,使用 add_typewide_sequence() 将序列添加到序列库中
3.6、自定义序列库的对象
- 可以为每一个序列器定制序列
- 创建序列库对象
- 使用 add_sequence 添加 sequence
- 配置序列器使用序列库对象
- 在测试用例中会影响配置的序列器
上面是通过实例化对象然后将 scenario sequence 放入库中,这种方法的好处是:只针对使用的对象单独添加。而上一小节是使用 add_typewide_sequence() 直接操作类,也就说所有的对象都会添加一个 scenario sequence 进去!接着是通过 config_db 将 default sequence 覆盖掉,当然还有另外一种方式是使用 override!
3.7、配置问题
- 下面的代码编译没有问题,但是仿真会出错
- 当配置序列库对象时
- 类型必须是 uvm_sequence_base
- 以下代码不能编译
- 配置问题很难调试(difficult to debug)
- 必须检查类型和路径
3.8、配置序列库
- 通过 uvm_sequence_library_cfg 修改默认行为
- 使用 min_/max_/random_count 设置序列执行的数量
- selection_mode 有四种模式(在 testcase 中进行 sequence 配置)
- UVM_SEQ_LIB_RAND(随机序列)
- UVM_SEQ_LIB_RANDC(随机周期性序列)
- UVM_SEQ_LIB_ITEM(随机数据,没有序列)
- UVM_SEQ_LIB_USER(工程师自定义的序列)
3.9、配置序列库的例子
- 在 test program 中,设置序列库配置参数的值
3.10、工程师定义序列执行
- 对于 UVM_SEQ_LIB_USER 模式,工程师必须实现 select_sequence() 函数
3.11、小结
Sequence Library 主要是针对同一个 agent 下的多个 sequence 发送问题,而上一节所讲的 virtual sequence 和 virtual sequencer 更多的是针对不同 agent 下的多个 sequence 发送问题!当然,对于同一个 agent 组件下的多个 sequence 发送,可以单独赋值给 default_sequence,放在 Library 里面最大好处是节省了代码。
14.UVM 项目实践之 APB_SPI(12)寄存器抽象层(RAL,Register Abstraction Layer)
前面我们已经学习了 config_db、factory 这两个重要的机制,可以让 UVM 实现一套验证环境,多个场景。而今天要学习的 RAL 在 UVM 验证方法学中也是非常经典且重要的,它是用于管理寄存器的一种组件。寄存器通常的作用有两个:1、控制模式;2、状态上报!
- 创建代表 DUT 寄存器的模型文件(Register Model File),是一个比较结构化的东西
- 流程:excel -> .ralf -> reg_model.sv
- 使用 RAL Generator 创建 UVM 寄存器类
- 即通过 RAL Generator 读取. ralf 产生 reg_model.sv
- 在序列中使用 UVM 寄存器
- 丰富 sequence 激励发送类型
- 在用适配器 adapter 为驱动器正确传递 UVM 寄存器对象
- 寄存器的数据模型与 sequence 数据结构进行适配
- 运行 UVM 内建的寄存器测试用例
2.1、Register 和 Memory
- 每一个待测设计都有寄存器(有的甚至有存储器)
- 寄存器行为和 Memory 行为类似,不过在电路实现时,使用的是 D 触发器!存储器的电路实现是使用的定制单元,面积更小,功耗也更低!如果要存储的数据数量不多时,可以直接使用 D 触发器实现。
- CPU 角度看寄存器和存储器,就是一个个地址,然后往这一个个地址单元去写数据!只是硬件实现有差异!
- 第一个需要验证的对象就是寄存器(做验证的第一步)
- 复位值 Reset value(初始值)
- 每一比特的行为 Bits behavior
- 可维护性
- 测试用例可以修改寄存器
- 修改固件模型(firmware model)
- UT(Unit Test)中通常用不到固件,直接用测试用例即可
- ST(System Test)中通常需要使用固件(firmware)
注:firmware 用来配置寄存器,因为它与硬件耦合比较紧密!
2.2、不使用 UVM RAL 的验证平台
前门操作是通过接口(interface),按照真实的场景操作,模拟 CPU 通过总线访问的行为,且占用仿真时间。后门操作是在环境中得到寄存器的层级(Hierarchy)路径,直接对寄存器进行修改,且不占用仿真时间。后门操作较前门操作,优势就是效率高!不好地方是,没有按照真实的场景,那么接口处的寄存器路径就不能很好的覆盖到!各有优缺,在实际项目中需要结合灵活使用。初期需要验 RAL 模型时,先使用前门把数据通路打通;后期跑回归测试时,每次初始化如果前门浪费很多时间,那么比较大的用例可以考虑使用后门直接操作!传统的环境,如上图,只通过 interface 与 DUT 相连。
2.3、使用 UVM RAL 的验证平台
2.4、UVM RAL(Register Abstraction Layer)
- 对寄存器的字段(filed)或者存储器进行读写操作抽象
- 支持前门(front-door)和后门(back-door)读写操作
- 寄存器数据镜像(mirror),便于后期 scoreboard 数据比对,不用再到 DUT 去取了!
- UVM 内建的功能覆盖率
- 层次化的模型便于重用
- UVM 预定义好的寄存器和存储器测试用例
2.5、实现 UVM RAL 的八个步骤
- 1、不使用 RAL,只是创建和验证前门,比如使用 sequence 到 driver;先打通整体数据通路
- 2、使用寄存器模型文件(.ralf)描述寄存器的字段;用自动化工具生成(脚本生成)
- 3、使用 RAL Generator(ralgen)生成 RAL 类;生成. sv 文件
- 4、创建 RAL 适配器(adapter);变换 RAL 数据结构为 sequence 数据结构
- 5、在验证环境中实例化 RAL 模型;集成到环境中
- 6、编写并运行 RAL 的测试序列
- 7、运行 UVM 内建的测试用例
- 8、实现镜像预测期(mirror predictor)
2.6、小结
本小结主要介绍了一些基本概念,读者需要清楚以下几个问题:寄存器是什么?寄存器做验证需要验哪些东西?传统的验证方法是什么?(不使用 RAL)传统验证的方法有什么弊端?寄存器建模需要对它的一些基本概念非常清楚,尝试举例都需要知道哪些?(如:地址、寄存器名字、域段等)
RAL 在面试和工程上都经常出现,需要读者好好掌握!
3.1、DUT 的寄存器描述
寄存器的各个域都包含三个要素:1、位宽;2、模式;3、复位值。
w1c:写 1 清。r1c:读 1 清。在多线程系统中,通常使用 w1c。
3.2、STEP1:不使用 RAL 的发送激励环境搭建(打通前门数据通路)
3.2.1、STEP1-1:创建事务和组件
非 RAL 的环境建模(Sequence、Driver),打通数据通路,手动操作寄存器
3.2.2、STEP1-2:创建总线模型的序列
- 编写驱动器序列
3.2.3、STEP1-3:验证前门的功能是正确的
- 根据 UVM 的基本编码原则,完成验证平台和测试用例,并通过 EDA 工具进行编译和仿真
3.3、STEP2:使用寄存器模型文件(.ralf)描述寄存器的字段
3.3.1、STEP2-1:根据设计文档编写寄存器模型
- 结构化的东西,不需要工程师去手写(能看懂即可,下面也会介绍具体语法,先有个大概印象即可!)
注意,Memory 通常我们会预先写好初始值,而不是通过复位信号给它写一个值!
3.3.2、STEP2-2:创建寄存器模型文件(.ralf)
- 工具生成该文件(了解即可)
3.3.2.1、Field 语法介绍
- RAL:寄存器的字段语法(extend form uvm_reg_field)
- Field 包含:bits、access、reset value、constraints 和 coverage
- RAL:字段属性
字段 | 属性 |
bits | 字段中的比特数量 |
access | 参见下文 |
reset | 字段复位值 / 默认值 |
constraint | 字段随机时的约束 |
enum | 定义字段值的符号名称 |
cover | +b 表示在寄存器比特覆盖率模型中包含该子弹,-b 表示排除该字段。使用 + f 制定覆盖点的目标,如果没有使用 - f 覆盖率的权重是 0 |
coverpoint | 显示制定字段的 bins |
- RAL:字段操作策略(Access:模式)
字段 | 属性 |
RW | read-write |
RO | read-only |
WO | write-only;read error |
W1 | write-one |
WO1 | write-one;read error |
W0C/S/T | write a 0 to bitwise-clear/set/toggle matching bits |
W1C/S/T(常用在中断中) | write a 1 to bitwise-clear/set/toggle matching bits |
RC/S | read clear/set all bits |
WC/S | write celar/set all bits |
WOC/S | write-only clear/set matching bits;read error |
RC/S | read clear/set all bits |
WSRC[WCRS] | write sets all bits; read clears all bits [inverse] |
W1SRC[W1CRS] | write one set matching bits; read clears all bits [inverse] |
W0SRC[W0CRS] | write zero set matching bits; read clears all bits [inverse] |
3.3.2.2、Register 语法介绍
- RAL:Register 语法
- 包含 fields
- RAL:Register 属性
字段 | 属性 |
bytes | 寄存器中的 byte 数量 |
left_to_right | 如果制定该参数,寄存器中字段是按照从左到右的顺序处理比特位,将最高位调整为最低位 |
[n] | 字段数组 |
bit_offset | 从最低位计算比特位的偏移量 |
incr | 数组偏移量增加 |
hdl_path | 指定寄存器所在的模块实例的路径,用于后门操作 |
cover | 指定如果寄存器的地址从模块的地址映射覆盖率模型中排除时,不在统计该寄存器的地址的覆盖率 |
shared | 所有的例化该寄存器的模块共享空间 |
cross | 指定两个或多个字段间的交叉覆盖率,交叉覆盖率可以有标签。 |
3.3.2.3、Register File 语法介绍
- RAL:Register File 语法(类似 Register)
- 包含 Register
- RAL:Register File 属性
RAL 寄存器文件中的属性包含以下内容:
字段 | 属性 |
[n] | 寄存器数组 |
hdl_path | 指定包含寄存器的模块实例,用于后门操作 |
offset | 寄存器文件中的寄存器偏移量 |
shared | 所有包含该寄存器的模块共享空间 |
层次化的路径:
3.3.2.4、Memory 语法介绍
- 分支组件
- 存储器中的寄存器
- RAL:memory 属性
- RAL memories 的属性包含以下内容
字段 | 属性 |
size | 连续地址的数量(深度) |
bits | 每一个地址的比特数量(宽度) |
access | 操作类型 |
initial | 存储器的初始值(默认是 x 态) |
3.3.2.5、Block 语法介绍
- Block
- 在 System 中实例化
- 定义了模块的层次结构和内容
- 在域中定义了多个接口,这些接口可以操作同一个寄存器
- 包含寄存器和存储器
- Block 属性
字段 | 属性 |
bytes | 地址对应的 byte 的数量 |
endian | 寄存器的映射模式:大小端 |
3.3.2.6、System 语法介绍
- 顶层或子系统
- 在域中定义了多个接口,这些接口都可以操作同一个寄存器
- 顶层或子系统
- 包含其他系统或者模块 Systems or blocks
3.4、STEP3:使用 RAL Generator 生成 RAL 的寄存器模型
会通过原始的寄存器表单(Excel 居多),使用工具生成. ralf 的文件,这个文件是 UVM 统一的数据结构。
上述 master.ralf 是工具生成的,能看懂就行,明白寄存器的几个要素。它会把每一个寄存器都会生成一个 class,里面会定义 field。
常见面试题:ralf 怎么做的?
- 首先用 excel 表格把寄存器写好,然后通过工具脚本生成. ralf 文件。这个文件自动生成好后,再通过 ralgen 得到 regmodel.sv 文件,它可以直接用在环境里面。(面试问的更多的是 regmodel 的问题)【reg_model 是声明寄存器的所有域,reg_blcok 是声明所有的寄存器!】
3.5、STEP4:创建 RAL 适配器(adapter)
regmodel.sv 生成的东西是不能传到 driver 的,因为那里面的数据格式是 sequence_item 的类型,而它仅仅是寄存器结构,所以需要进行一个适配(数据结构转换)!
3.5.1、STEP4-1:创建 RAL 适配器 Adapter
- 验证环境中,使用 RAL 的适配器将 RAL 中的事务数据转换成 Agent/Sequencer 的总线事务
3.5.2、STEP4-2:创建 RAL 适配器 Adapter
- UVM RAL 寄存器 Sequence(RAL 也要建立自己的 sequence)
- RAL 寄存器 sequence
- 在总线 sequencer 上运行寄存器的 sequence
- 将寄存器序列当做 virtual sequence(发到不同的 agent 中,用的少)
3.6、STEP5:在验证环境中实例化 RAL 模型
3.6.1、STEP5-1:在验证环境中例化 RAL
XMR:Cross Module Reference ,跨层次的路径
另外,上面你的第四步,后门仿真是可选的,需要就加!
3.6.2、STEP5-2:在验证环境中连接 RAL
记住即可,一个比较死的语法规定
- 使用 RAL 寄存 sequencer 和对应的 adapter
- 将 sequence 的适配器与寄存器建立映射关系
- RAL 的映射代表了一个接口
- 寄存器只是用 default_map 作为接口
- 具有多个接口的寄存器使用工程师指定的映射名称
3.7、STEP6:编写并运行 RAL 的测试序列
3.7.1、STEP6-1:RAL 测试用例
3.7.2、STEP6-2:显式或隐式的运行 RAL 序列
3.7.3、STEP6-3:测试用例常见的 sequence
sequence name | description |
uvm_reg_hw_reset_seq | 测试寄存器的硬复位值(外部复位) |
uvm_reg_bit_bash_seq | 所有寄存器比特位 |
uvm_reg_access_seq | 验证所有寄存器的操作属性 |
uvm_mem_walk_seq | 使用遍历算法验证存储器 |
uvm_mem_access_seq | 使用前门或者后门方式验证寄存器的读写操作 |
uvm_reg_mem_build_in_seq | 运行所有的寄存器和存储器的测试用例 |
uvm_reg_mem_hdl_paths_seq | 验证寄存器和存储器的层次化路径 |
uvm_reg_mem_shared_access_seq | 验证共享寄存器和存储器的读写操作 |
UVM 做好的测试用例,直接执行即可!
3.7.4、STEP6-4:隐式执行镜像测试
- 当对寄存器进行读写操作时,更新寄存器的镜像值
- 优点
- 简单:自动调用读写操作更新镜像值,不需要验证工程师自己编写代码
- 缺点
- 镜像值的更新时序不一定是精确到时钟周期
- DUT 内部的更改可能不会影响镜像值
- 如果在测试用例中需要使用镜像值,使用隐式镜像预测是不可取的!
3.7.5、STEP6-5:在 sequence 中使用 auto predict 来运行 RAL 的测试用例
- 调用 set_auto_predict(1) 函数可以使能自动预测功能
3.7.6、STEP6-6:显式执行镜像预测
- 当监测器观察到寄存器的值有所改变时,更新镜像值
- 优点
- 通过观察总线协议更新镜像值
- DUT 内部的改动会影响镜像值
- 如果测试用例中需要使用镜像值,使用显式镜像预测是正确的选择
- 缺点
- 设置复杂
- 需要监视器
3.7.7、STEP6-7:显式镜像预测
3.7.8、STEP6-8:RAL 测试序列使用显式预测
- 测试用例基本与自动预测相同
- 不同之处是关闭了自动预测
RAL 创建了一个代表 DUT 寄存器的模型文件,通过 RAL Generator 去创建一个寄存器类,然后在 UVM Sequence 中去使用寄存器。另外,还需要一个适配器。除此之外,我们寄存器类还有一些内建的 sequence,可以直接在 testcase 中例化使用!
4.1、学习内容
- UVM RAL 寄存器读写操作接口(API)
- UVM RAL 存储器读写操作接口(API)
- EDA 逻辑仿真工具 VCS 的 RAL 生成器 ralgen
- UVM 寄存器类树
- UVM RAL 寄存器 / 存储器成员
- UVM RAL 回调 callbacks
- 改变一个域的偏移地址(offset)
- UVM RAL 覆盖率
- UVM RAL 共享寄存器
4.2、RAL 寄存器读写操作接口 API
- 前门操作:Frontdoor read/write(改变 DUT 的值)
- 后门操作:Backdoor read/write/peek/poke(改变 DUT 的值)(peek 等效于 read,poke 等效于 write)(后门操作不占仿真时间)
- 镜像操作:Mirror set/get/update(改变 DUT 和 Mirrored regs 的值)
注:存储器不能镜像
4.2.1、前门写操作
- 序列设置 uvm_reg 的值
- uvm_reg 的值传递给总线事务
- Driver 获取总线事务并对 DUT 的寄存器进行写操作
- 镜像值显式或者隐式更新
(图上标注:status 是返回的状态上报!;write_reg 是另一种函数操作,效果相同!)
4.2.2、前门读操作
- 序列(sequence)执行 uvm_reg 读操作
- uvm_reg 读操作转换成总线事务(transaction)
- Driver 读取总线事务并对 DUT 的寄存器进行读操作
- 读到的值转换成 uvm_reg 的数据,返回给序列
- 镜像值更新
4.2.3、后门写操作
- 序列可以对 uvm_reg 进行写操作,同时遵循一定的规则(写清零等)
- uvm_reg 使用 DPI/XMR 设置 DUT 寄存器的值(相当于寄存器一根连线)
- 隐式更新镜像值(后门只有隐式更新,因为斗不过 monitor 等组件)
4.2.4、后门读操作
- 序列执行 uvm_reg 读操作
- uvm_reg 使用 DPI/XMR 设置 DUT 的值
- 根据 uvm_reg 的操作修改 DUT 的寄存器的值
- rc:读清零
- 隐式更新镜像值
4.2.5、后门刺探:Backdoor poke
- 序列执行 uvm_reg 的写操作但是不遵循一定的规则(wc:写清零)
- uvm_reg 使用 DPI/XMR 设置 DUT 的值
- 隐式更新寄存器的值
4.2.6、后门窥视:Backdoor peek
- 序列执行 uvm_reg 窥视操作
- uvm_reg 使用 DPI/XMR 设置 DUT 寄存器的值,但是不遵循一定的规则(rc 等)
- 隐式更新寄存器的值
4.2.7、镜像值和期望值的更新
- 更新 DUT 的镜像值和预期值
- 默认不做检查:check 值为 UVM_NO_CHECK
- 如果 check 的值为 UVM_CHECK
- 将数据与镜像值比较
- Path 的值可以是
- UVM_FRONTDOOR
- UVM_BACKDOOR
4.2.8、设置期望的镜像值
regmodel.register.set(value);
- 通过 set 函数设置期望的镜像值
4.2.9、读取期望的镜像值
value = regmodel.register.get()
- 使用 get 函数获取期望的镜像值
4.2.10、更新镜像的值和 DUT 的值
- 如果镜像的值域期望的值不同,使用期望的值更新镜像的值和 DUT 的值
- path 可以是:
- UVM_FRONTDOOR
- UVM_BACKDOOR
4.2.11、使用 predict() 函数设置镜像的值
(决定隐式还是显式更新)
regmodel.register.predict(value);
- 验证工程师通过序列设置镜像的值
4.2.12、使用 get_mirrored_value() 函数读取镜像的值
value = regmodel.register.get_mirrored_value();
- 验证工程师可以通过序列获取镜像的值
4.2.13、创建期望的镜像值
register.randomize();
- 使用随机数值生成期望的镜像值
4.2.14、uvm_reg predict() 函数使用方法
- 显式使用预测器
- 监视器将观察到的事务数据传递给预测器
- 预测器调用 predict() 函数,更新镜像值
- 隐式使用预测器
- uvm_reg 调用 predict() 函数直接更新镜像值,而不需要观察实际的物理变化
4.3、存储器的读写操作 API
4.3.1、存储器前门写操作
- 序列设置 uvm_mem 的值
- uvm_mem 的内容转换成总线事务数据
- 驱动器获取事务数据后,写到 DUT 的存储器中
- 存储器的值不能镜像
4.3.2、存储器前门读操作
- 序列设置 uvm_mem 读操作
- uvm_mem 读操转换成总线事务
- 驱动器获得总线事务后,读取 DUT 的存储器数据
- 读取的数值转换成 uvm_mem 格式,并返还给序列
- 存储器的值不能镜像
4.3.3、存储器后门写操作
- 序列写 uvm_mem,遵循存储器的读写操作规则(ro 只读操作不会改变存储器的值)
- uvm_mem 使用 DPI/XMR 设置 DUT 存储器的值
- 存储器的值不能镜像
4.3.4、存储器后门读操作
- 序列实现 uvm_mem 读操作
- uvm_mem 使用 DPI/XMR 获取 DUT 存储器的值,并且遵循存储器的读写规则(wo:只写存储器不会返回数值)
- 存储器的值不能镜像
4.3.5、存储器后门刺探 poke 操作
- 序列实现 uvm_mem 写操作
- 不遵循存储器读写规则(ro:只读存储器将会修改存储器的值)
- uvm_mem 使用 DPI/XMR 设置 DUT 存储器的值
- 存储器的值不能镜像
4.3.6、存储器后门窥视 peek 操作
- 序列实现 uvm_mem PEEK 操作
- uvm_mem 使用 DPI/XMR 获取 DUT 的存储器值
- 存储器的值不能镜像
4.4、逻辑仿真工具 VCS 的 ralgen options(先了解)
4.4.1、ralgen 介绍
- ralgen [options] -uvm -t top_model file.ralf
- 命令行选项
- B
- 生成以 byte 为基础的地址
- b
- 生成 XMR 为基础的(non-DPI)后门操作代码
- c -a(coverage address)
- 生成 “地址映射” 功能覆盖率模型
- c -b(coverage bit)
- 生成 “寄存器比特位” 功能覆盖率模型
- c -f(coverage field)
- 生成 “字段值” 功能覆盖率模型
- 可以参阅 uvm_ralgen_ug.pdf 中提到的其他命令行参数
- $ ralgen -h
4.4.2、ralgen 地址粒度
4.4.3、ralgen XMR
后门仿真使用
重点:后门访问对于路劲的设置有两种方式:1、在环境中操作;2、在命令操作
4.4.4、ralgen 功能覆盖率
4.5、UVM RAL 类树(层次结构 Hierarchy)
4.6、UVM RAL 寄存器 / 存储器成员
4.6.1、RAL 寄存器类关键属性
4.6.2、RAL 存储器类关键属性
- uvm_reg_bus_op 定义
4.7、RAL 类关键回调成员(了解)
- 提示:简化代码
- 两种实现回调的方法:
- 简单的回调 - 扩展 uvm_reg 类
- UVM callback - 扩展 uvm_reg_cbs 类,然后寄存 cb
4.8、改变一个域的偏移地址(offset)(了解)
不太实用,验的 UT 本身都是偏移地址!
- 使用 set_base_addr() 函数修改 UVM 寄存器映射的基地址
- 在 lock_model 之前或之后可以调用该参数
- 如果实在模型锁住之后调用该函数,地址将会重新初始化
4.9、UVM RAL 覆盖率
- RAL coverage 模型
- 预定义的覆盖率模型标识符
标识符 | 描述 |
UVM_NO_COVERAGE | 没有覆盖率模型 |
UVM_CVR_REG_BITS | 寄存器中读写比特的覆盖率模型 |
UVM_CVR_ADDR_MAP | 一个地址映射中地址读写覆盖率模型 |
UVM_CVR_FIELD_VALS | 字段值的覆盖率 |
UVM_CVR_ALL | 所有覆盖率模型 |
- 覆盖率模型的构建
- 覆盖率模型采样(默认是关闭采样)
一般项目中,直接按照 ALL 收集即可!
15.UVM 项目实践之 APB_SPI(13)UVM 验证方法学总结
一、内容
- 统一的验证方法学
- 可扩展的 UVM 验证平台架构
- 标准化的组件通信机制 TLM
- 可定制的组件执行 Phase
- 灵活的组件配置机制 Configureation
- 可重用的寄存器测试方法 RAL
- 命令行调试选项
二、UVM 验证方法学的指导原则
- 自顶向下的实现方法
- 重点是覆盖率驱动的验证
- 设计质量最大化
- 更多的测试用例
- 更多的检查
- 更少的代码修改
- 验证策略和方法
- 重用性
- 测试用例
- 不同模块
- 不同系统
- 不同项目
- 一个验证环境,多个测试用例
- 特殊测试用例代码最小化
三、可扩展的 UVM 验证平台架构(图了解)
- 基于接口的 agent,可以在系统层次上复用模块层次的内容
四、标准化的组件间通信机制 TLM(图必须会画)
上图是示意图,可以认为 Reference Model 是放在 Scoreboard 里面!
五、定制化的组件执行顺序
- 组件的 phase 是同步的
- 确保了正确的配置和执行
- 验证工程师可以自己定义 phase
六、灵活的组件配置机制(重要)
uvm_config_db 两个重要函数,实例如下:
uvm_config_db #(type)::set(...)
uvm_config_db #(type)::get(...)
UVM 中的 inteface 必须要通过 配置机制(config_db) 传递,而 SV 可以初始化一层层传,面试常考!下面两个实例通过 phase 机制和配置机制来控制 sequence 执行 (两种方法效果一样)
- sequence execution phase can be specified
- 第一个程序:通过 phase objection 机制和回调机制,在 pre_start 中去 raise objection,在 post_start 中去 drop objection,相当于在 start 前后对 phase 进行控制。
- uvm_config_db #(type)::set(…) executes sequence in chosen phase
- 第二个程序:发包的时候通过 config_db 指定在哪个阶段去发!
override 工厂机制进行组件覆盖也非常重要,实例如下:
七、标准化的寄存器描述和测试(重要)
Adapter 作用是:寄存器 RW 形式的数据转换成 sequence_item 这种 tr 的数据结构
八、SoC UVM 验证平台架构(了解)
九、UVM TestBench
十、UVM Component Phase
十一、UVM Base Class Tree
十二、UVM 命令行选项
- +UVM_TESTNAME:运行仿真的测试用例的名称
- +UVM_VERBOSITY:过滤打印信息的开发
- +UVM_TIMEOUT:设置仿真超时时间
- +UVM_MAX_QUIT_COUNT:仿真退出时最大可以容忍的错误数量
- +UVM_PHASE_TRACE:跟踪 phase 的执行
- +UVM_OBJECTION_TRACE:跟踪 objection 的执行
- +UVM_CONFIG_DB_TRACE:跟踪配置机制的执行
- +UVM_RESOURCE_DB_TRACE:跟踪资源配置的执行
- +uvm_set_verbosity
- +uvm_set_action
- +uvm_set_severity
- +uvm_set_inst_override
- +uvm_set_type_override
十三、总结
本节就前面所学的 UVM 验证方法学中重点知识进行了一个提炼总结,对于 UVM 验证平台的基本架构图一定要了熟于心,这样才能在此基础确定各个组件之间的数据流向,出了问题才能快速定位到具体是哪一个组件的什么问题。除此之外,还需要灵活掌握组件的配置机制以及工厂机制,窃以为此乃 UVM 验证方法学的灵魂所在。其他更多的知识在实践中慢慢领悟即可,所以后续行文安排是先进行 UVM 各个组件的实践练习,一来可以巩固夯实前面所学的零散知识,将其在实践中串成一个属于自己的知识体系;二来可以为我们的 APB-SPI 实践项目打下扎实的基础,让理论与实践之间的跨度不至于太大。
最后再想唠叨一下,首先恭喜各位,可以一步一个脚印学到这个地方,所谓行百里者半九十,放到本专栏的学习过程亦是如此。到目前为止,我们已经完成了 SystemVerilog 基础理论学习、SystemVerilog 实践练习以及 UVM 基础理论学习三大板块,可以说是已经翻过了数字 IC 验证的三座大山,但是切不可就此松懈,因为还有一座非常重要且比较坎坷的大山在等着我们,那就是 UVM 实践练习。它比起 SystemVerilog 实践可能会没那么直观简单,因为它封装了很多 SystemVerilog 的细节,为我们呈现的更多是接口,我们要做的除了知道每个接口的使用方法,还需要理解接口为什么这样使用,唯有如此才能真正驾驭 UVM,才能真正入门数字 IC 验证。
- 作者:Conor
- 链接:https://www.xzhh.top/article/UVM2023
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。