AHB to APB Bridge验证项目
目录
3.3 AHB to APB sync-down bridge. 30
3.4 AHB to APB sync-down bridge 验证计划... 30
3.4.1 Metrics Driven Verification(指标驱动验证,MDV)... 30
3.4.2 AHB-to-APB Bridge 验证计划... 31
4.2 ahbl_master_agent环境搭建... 40
4.5.2 ahbl_mst_single_read32. 55
4.5.3 ahbl_mst_single_write32_apb_slv_nrdy. 55
4.5.5 ahbl_mst_burst_apb_slv_slverr 57
4.5.6 ahbl_mst_tight_transfer 57
4.6.2 ahbl_mst_single_read32_seq. 59
4.6.3 ahbl_mst_single_write32_seq. 59
4.6.4 ahbl_mst_tight_burst_seq. 60
第一章 APB协议
1.1 APB2协议
一个典型的APB总线架构如上图所示,APB协议是为了实现与外围低速、低功耗设备进行数据传输。在APB总线中,唯一的master是APB Bridge,其他的外围设备均为slave。
1.1.1 读操作
下图为APB的读操作,一共分为两拍:
T1时刻APB master将PSEL拉高,(PENABLE正常情况下此时为低),PWRITE拉低,APB master驱动PADDR。
T2时刻APB master将PENABLE拉高,APB slave采样PSEL、PENABLE、PADDR和PWRITE,如果满足写操作的时序要求,就驱动PRDATA(实际上只要在T3之前准备好即可)。
T3时刻APB master将PSEL和PENABLE都拉低(如果下面还要继续操作该slave,那PSEL也可以保持为高),并采样PRDATA,PADDR和PWRITE将保持(这是APB协议出于低功耗考虑要求的,减少额外的翻转,当下一个操作过来时这两个信号自然会变化)。
需要注意的是,通常片选信息包含在PADDR中。在RTL设计中会将不同的地址段对应到不同的slave,master发出一个地址,那么对应的slave的PSEL就会拉高。
1.1.2 写操作
下图为APB的写操作,过程和读操作非常类似,稍有区别的是APB master需要在T1时刻驱动PWDATA,并保持到写操作完成。
总的来说,APB的读写操作都需要两拍才能完成,通常将第一拍称为Setup phase,第二拍称为Access phase。
1.2 APB3协议
APB3相较于APB2新增了PREADY和PSLVERR两个信号。
1.2.1 PREADY信号
PREADY信号表示slave是否可以立即响应master要求的操作。PREADY为高表示slave可以响应,为低则表示还没准备好,master需要等待,并且在此期间master需要保持PADDR、PWRITE、PSEL和PENABLE。
举例说明:
T2时刻slave采样到PSEL=1、PENABLE=0,那么slave理应准备好对应的PRDATA,但slave无法立即准备好,于是把PREADY拉低,直到T4时刻PRDATA准备好,拉起PREADY。
T3时刻master采样到PREADY为低,于是保持PADDR、PWRITE、PSEL和PENABLE。直到T5时刻采样到PREADY为高,此时PRDATA有效,读数据完成,拉低PSEL和PENABLE。
1.2.2 PSLVERR信号
PREADY信号是让master进行等待,而PSLVERR信号则是通知master本次操作是否发生错误。PSLVERR信号只在传输的最后一拍有效,即PSEL、PENABLE和PREADY均为高时。当PSEL、PENABLE或PREADY为低时,建议将PSLVERR拉低,但这不是必需的。
需要知道的是,PSLVERR只是一个通知标识,它并不能阻止数据的读写,也就是说即使传输发生错误,数据也是会照样传过去,毕竟物理的连接是存在的,只不过master知道错误后,可以针对性的做出处理。
1.3 APB4协议
APB4相较于APB3增加了PPROT和PSTRB两个信号,其中PSTRB更常用些。
PPROT信号如上图所示,下面主要解释一下PRSTB信号。首先明确PRSTB信号只在写操作时有效,读操作时需要设置为4’b0000。PRSTB信号的出现是因为APB的写总线是32位的,但是有时候我们并不需要操作全部的位,并且需要保证不会误写了某些位,这时就需要PRSTB这样的信号。
它有4bit,每个bit对应8bit(1byte)写总线,为高则代表对应的byte上的数据是有效的,否则无效。
1.4 APB5协议
APB5相较于APB4增加了PWAKEUP信号和USER系列信号,USER系列是APB为用户提供的可定制的信号,一般情况下建议不使用USER信号。APB协议接口没有定义这些信号的功能,如果两个组件以不兼容的方式使用相同的USER信号,则会导致互操作性问题。
PWAKEUP用来指示本次传输是否有效,如果传输的时候PWAKEUP信号不使能,则slave会认为这一笔数据传输无效。设计上建议PWAKEUP先于PSEL一个周期拉高。也可以用PWAKEUP控制APB的时钟达到低功耗的目的。
第二章 AHB协议
2.1 版本说明
2.2 信号说明
2.2.1 简述
AHB总线的基本原理是采用主从结构,其中一个或多个主设备通过总线与一个或多个从设备通信。主设备可以是CPU、DMA控制器、图形处理器等高性能设备,从设备可以是存储器、外设控制器等低速设备。AHB总线采用了高度并行的传输模式和分时复用的地址和数据传输方式,从而实现了高带宽和低延迟的数据传输。
下图是 “多主从”的AHB总线结构,仲裁器、解码器、选择器以及互连线等通常称之为互连结构。这种结构的工作机制如下:
首先master会向仲裁器发送传输请求,仲裁器依据所设计的仲裁规则,选择合适的master获取总线访问的权限,之后master会将数据与控制信号发送到仲裁器,仲裁器通过地址解析判断对应的slave通路,然后将请求发送到对应的目的端。
同样响应的数据会通过Decoder解析,然后返回给对应的master。
通过这种多路复用的机制实现多对多的访问。但是这种结构有一个缺点就是一次能有一对maste与slave通信,无法实现多对多的同时通信,对于带宽需求比较大或者实时性要求较高的系统不太适合。
在本项目中,Bridge就是一个AHB slave,它在APB总线一侧是唯一的master,而在AHB一侧,它只是其中一个slave。
下图是“一主多从”的系统,从图中可以看到Decoder根据HADDR信号产生HSELx信号对slave进行片选,同时产生对多路选择器的控制信号,以选择哪个slave读出的数据。在多从机的系统中,唯一的、中央解码器和多路复用选择器是必须的。
2.2.2 全局信号
信号 | 信号源 | 描述 |
HCLK | 时钟源 | 总线时钟对所有的总线传输进行计时。所有的信号定时都与HCLK的上升沿有关。 |
HRESETn | 复位控制器 | 总线复位信号为低电平有效,复位系统和总线。这是唯一低电平有效的信号。 |
2.2.3 master信号
信号 | 接收方 | 描述 |
HADDR[31:0] | Slave and decoder | 32位系统地址总线 |
HBURST[2:0] | Slave | burst类型指示传输是单个传输还是形成burst的一部分。 支持4、8和16拍的固定长度的burst。burst可以是递增的,也可以是wrap的。还支持增加未定义长度的burst。 |
HMASTLOCK | Slave | 高电平时,表示当前传输是锁定序列的一部分。它与地址和控制信号具有相同的时序。 |
HPROT[3:0] | Slave | 保护控制信号提供关于总线访问的附加信息,并指示应该如何在系统内处理访问。 信号指示传输是操作码提取还是数据访问,以及传输是特权模式访问还是用户模式访问。 |
HPROT[6:4] | Slave | 添加扩展存储器类型的HPROT信号的3位扩展。 如果AHB5 EXTENDED_MEMORY_TYPE属性为True,则支持此信号扩展。 |
HSIZE[2:0] | Slave | 指示传输的大小,通常为字节、半字或字。该协议允许更大的传输大小,最大可达1024位。 |
HNONSEC | Slave and decoder | 指示当前传输为非安全传输或安全传输。 如果AHB5 SECURE_TRANSFERS属性为True,则支持此信号。 |
HEXCL | Exclusive Access Monitor | 独占传输。表示传输是独占访问序列的一部分。 如果AHB5 EXCLUSIVE_TRANSFERS属性为True,则支持此信号。 |
HMASTER[3:0] | Exclusive Access Monitor and slave | 主机标识符。如果主程序有多个独占能力的线程,则由主程序生成。 由互连修改以确保每个主设备被唯一标识。 如果AHB5 EXCLUSIVE_TRANSFERS属性为True,则支持此信号。 |
HTRANS[1:0] | Slave | 表示当前传输的类型。可以是: • IDLE • BUSY • NONSEQUENTIAL • SEQUENTIAL |
HWDATA[31:0] | Slave | 写入数据总线在写入操作期间将数据从主机传输到从机。建议的最小数据总线宽度为32位。但可以将其扩展以实现更高的带宽操作。 |
HWRITE | Slave | 指示传输方向。当为高时,该信号表示写入传输,而当为低时,则表示读取传输。它与地址信号具有相同的时序,但它必须在整个burst传输过程中保持稳定。 |
2.2.4 slave信号
信号 | 接收方 | 描述 |
HRDATA[31:0] | Multiplexor | 在读操作期间,读数据总线将数据从选定的从机传输到多路复用器。然后多路复用器将数据传输到主机。 建议的最小数据总线宽度为32位。但可以将其扩展以实现更高的带宽操作。 |
HREADYOUT | Multiplexor | 高电平表示总线上的传输已完成。该信号可以被驱动为低电平以延长传输(和APB的PREADY类似)。 |
HRESP | Multiplexor | 在通过多路复用器之后,传输响应向主设备提供关于传输状态的附加信息。 低电平表示传输状态正常。 高电平表示传输状态为错误。 |
HEXOKAY | Multiplexor | 指示独占传输的成功或失败。 如果AHB5 EXCLUSIVE_TRANSFERS属性为True,则支持此信号。 |
2.2.5 decoder信号
信号 | 接收方 | 描述 |
HSELx | Slave | 每个从机都有自己的从机选择信号HSELx,该信号表示当前传输是针对所选从机的。当最初选择从机时,它还必须监控HREADY的状态,以确保在响应当前传输之前,先前的总线传输已完成。 HSELx信号是地址总线的组合译码。 |
HSELx中使用的字母x必须更改为系统中每个从属设备的唯一标识符。例如,HSEL_S1、HSEL_S2和HSEL_MEMORY。
2.2.6 multiplexor信号
信号 | 接收方 | 描述 |
HRDATA[31:0] | Master | 读取数据总线,由解码器选择。 |
HREADY | Master and slave | 高电平时,HREADY信号向主机和所有从机表明前一次传输已完成。 |
HRESP | Master | 传输响应,由解码器选择。 |
HEXOKAY | Master | 由decoder选择 |
2.3 传输协议
2.3.1 基本传输
AHB的传输分为两个阶段:Address phase和Data phase,其流程为:
1)在第一个HCLK上升沿,master将地址和控制信号驱动到总线上;
2)在第二个HCLK上升沿,slave对地址和控制信号进行采样,并驱动相应的HREADYOUT信号和data。
3)在第三个HCLK上升沿,master对相应信号进行采样。
并且从图中可以看到,在Data phase,HADDR和HWRITE都发生了变化,也就是说不用像APB那样保持,可以为下一次操作做准备。地址和数据的这种重叠是一种流水线性质,使得传输不必强制分为两个周期,这也是AHB更快的原因之一。
每个slave都有一个HREADYOUT信号,在传输的Data phase驱动该信号。在多个slave的系统中,互连逻辑负责根据地址选择正确的slave输出的HREADYOUT,传递给master的HREADY。
下面是带有等待的读写操作,对于写操作,master在整个Data phase中都需要保持HWDATA稳定;而对于读操作,slave在传输即将完成之前不必提供有效的HRDATA。
同时可以看到,在Data phase扩展传输时,其实也同时扩展了下一笔传输的Address phase。
2.3.2 传输类型
HTRANS[1:0] | 类型 | 描述 |
00 | IDLE | 表示不需要数据传输。当主机不想执行数据传输时,它使用IDLE传输。建议主机使用IDLE传输终止锁定传输。 从机必须为IDLE传输提供零等待的OK响应(毕竟什么都不做),并且从机必须忽略该传输。 |
01 | BUSY | BUSY传输使主机能够在burst的中间插入空闲周期。表示master正在继续burst,但下一次传输不能立即进行。 当主机使用BUSY传输类型时,地址和控制信号必须反映burst中的下一个传输。 只有未定义长度的burst可以具有BUSY传输作为burst的最后一个周期。 从机必须始终对BUSY传输提供零等待状态OK响应,并且从机必须忽略该传输。 |
10 | NONSEQ | 表示单次传输或burst的第一次传输。 地址和控制信号与前面的传输无关。 |
11 | SEQ | 表示burst中的后续传输,并且地址与前一次传输相关。 控制信息与前一次传输相同。 该地址等于上一次传输的地址加上传输大小(以字节为单位),传输大小由HSIZE[2:0]信号确定。在wrap burst中,传输地址会在地址边界处回绕。 |
2.3.3 锁定传输
锁定传输是master通过拉高HMASTLOCK信号实现的,告知slave当前传输是不可分割的,必须要先处理完当前的传输才能去处理其他的。spec上建议在锁定传输后插入一个IDLE传输来终止锁定传输。要求锁定序列中的所有传输都指向相同的从机地址域。
2.3.4 传输size
注意:
HSIZE设置的传输大小必须小于或等于数据总线的宽度。
将HSIZE与HBURST结合以确定回卷burst的地址边界。
HSIZE信号与地址总线具有相同的时序。但必须在整个burst传输过程中保持不变。
2.3.5 burst操作
1)协议规定master不能启动一个跨越1KB地址边界的burst传输。也就是说master在想要发起一个传输之前,必须去检查这笔传输是否会跨越地址边界(1KB似乎是AHB协议中为每个slave分配的地址空间上限)。
2)在burst中所有的传输必须对齐到地址边界等于传输的大小。
3)在不定长的INCR传输中,master可以插入BUSY,然后决定是继续传输还是结束传输。插入NONSEQ或者IDLE都可以终止本次INCR传输。
4)协议不允许master用BUSY传输来结束下列burst传输:
INCR4、INCR8、INCR16,WRAP4、WRAP8、WRAP16,它们只能用SEQ作为结束。
5)single传输后面不能紧跟BUSY,它后面必须是IDLE或者NONSEQ。
6)当slave发出error响应,master可以提前结束burst传输,当然也可以继续完成。
如果master选择结束,那它必须在error响应的那两个周期内将HTRANS改为IDLE。
提前结束的传输就真的结束了,下一次访问这个slave时,也不会继续完成上一次未完成的传输。
7)首先要知道master没有提前结束传输的权力,在多主机系统中,互连逻辑可以决定提前结束一个传输。这要求当互连逻辑提前结束传输,并将另一个master的请求送到这个slave时,slave可以正常结束旧的传输,并响应新的。
这里举例说明一下WRAP操作,以下图3-10为例:WRAP8代表操作一共8拍,WORD是指每拍4byte数据,那么这个操作一次的数据量是8*4=32bytes,那么对这个操作来说地址边界是32byte的整数倍。
这个操作中,地址变化应该的这样的:0x00、0x04、0x08、0x0c、0x10、0x14、0x18、0x1c,0x20、0x24、0x28、0x2c、0x30、0x34、0x38、0x3c,0x40…
地址边界是0x00、0x20、0x40…
本例中从0x34开始,遇到0x40边界就会回绕到起点0x20。
2.3.6 waite传输
当slave通过拉低READYOUT使传输进入等待状态时,master是否能更改trans的类型和地址以及能做出怎样的更改,协议对此是有严格要求的。
1)在等待期间,允许master更改IDLE传输的地址,也允许master将传输类型从IDLE更改为NONSEQ。不过一旦HTRANS传输类型变为NONSEQ,master就必须保持HTRANS和HADDR不变,直到HREADY拉高。
上图中,master先对地址A发起一笔SINGLE传输,不过slave拉低了HREADY使传输进入等待状态。随后master又对地址Y、Z插入了IDLE传输,接着又对地址B发起INCR4传输,可以看到HTRANS改成了NONSEQ并且保持到HREADY拉高。HREADY拉高后完成了之前的SINGLE传输。
2)对于定长的burst传输,在进入等待状态后,允许master将传输类型从BUSY改为SEQ。不过一旦做出这种改变,master就必须保持HTRANS不变,直到HREADY拉高。其实也好理解,BUSY代表master目前比较忙不能处理传输,改为SEQ代表可以继续处理。这里地址是不能改变的,因为定长的操作前后地址都是有关系的。
同时注意到,因为BUSY只能在连续的burst之间插入,所以这不适用于SINGLE。
3)对于不定长的burst,在进入等待状态后,允许master从BUSY切换到任何其他传输类型。如果换到了SEQ传输,则当前的burst继续;如果换到了IDLE或NONSEQ传输,则当前的burst终止。
并且切换后master必须保持HTRANS不变,直到HREADY拉高。
4)在等待过程中,如果slave发出了ERROR响应,那么当HREADY为LOW时,允许master更改地址。
T3时刻slave发出ERROR,T4时刻master采样到响应,更改了地址。
2.3.7 Protection control
1)协议的版本B中将HPROT[3]名称改为Modifiable。
2)许多master不能生成准确的保护信息,这种情况本规范建议:
• master将HPROT设置为0b0011,以对应非缓存、非缓冲、特权的数据访问。
• 除非绝对必要,slave不使用HPROT。
3)HPROT信号必须在整个burst传输过程中保持不变。
2.4 总线互连
Spec规定分配给单个slave的最小地址空间为1KB,并且地址域的开始和结束必须在1KB边界上。
如果系统中包含不完全的内存映射,那么必须实现一个额外的default slave,以便在访问不存在的地址位置时提供响应。(大概就是说地址空间没有都分配给slave,空余出来的要指定给default slave)
default slave会向NONSEQ或SEQ传输提供ERROR响应,对IDLE或BUSY传输提供零等待的OKAY响应。
2.5 从机响应信号
对HRESP,0为OKAY,1为ERROR。
对于Transfer pending,一般来说slave会有一个预定的最大等待时间,防止总线在某次访问中锁死,并且不规定最坏情况不利于计算访问总线的最大延迟。建议slave插入的等待状态不要超过16个,但是对于某些设备(例如串口boot ROM),此建议不适用。此类设备通常仅在系统启动时访问,对系统性能的影响可以忽略不计。
OKAY响应可以在一个周期内完成,但是ERROR响应需要两个周期。第一个周期拉高HRESP表示出错,拉低HREADY延长transfer,第二个周期继续保持HRESP为高,拉高HREADY结束transfer。
之所以需要双周期,是因为AHB总线具有流水特性,当第一个周期拉高HRESP时,下一笔传输的信息也已经发到总线上了。两个周期的响应为master提供了足够的时间来取消下一次访问,并在下一次传输开始之前将HTRANS[1:0]更改为IDLE。
如果slave需要两个以上的周期来提供ERROR响应,那么可以在传输开始时插入额外的等待状态。在此期间HREADY为LOW,并且HRESP必须设置为OKAY。
如果slave提供了ERROR响应,那么master可以取消burst中剩余的传输,也可以继续剩余的传输。
slave只需要在传输完成并返回OKAY响应时提供有效数据,ERROR响应不需要有效的读取数据。
第三章 测试点分解
3.1 验证计划
描述DUT完整验证过程,以及支持该验证过程必要的验证平台、环境、方法、测试用例及预期结果的计划性文档。
3.1.1 验证计划的内容
1. 测试点分解(解决what?的问题)
2. 测试方案制定(解决how?的问题)
① 测试平台选定
② 仿真环境架构设计
③ 测试用例设计及期待结果描述
3. 资源估计(解决schedule?的问题)
4. 测试结果预期(解决result?的问题)
3.1.2 测试点
1.测试点的概念
针对一个给定的DUT,我们到底要“测什么?”和“仿什么?”,是我们测试点分解工作的输出对象。
2.测试点分解的概念
在对DUT有充分理解的基础上,以文字或者图标的形式明确地列出全部测试点的过程。
3.测试点分解的意义
测试点分解是制定验证计划过程中极其重要的和极具含金量的基础性步骤。测试点分解的正确性、完备性对后续的验证工作乃至芯片流片成功与否起着决定性的作用。
测试点分解与验证方案制定可以体现出一名验证工程师对DUT理解和把握的准确度和完整度,同时深刻反映出验证工程师的验证功底和经验。
4. 测试点分解依据
1) DUT的Spec
每一个待测的DUT都应当且原则上必须有一份合格的Spec文档
2) 标准、协议
若DUT符合某种标准、协议,则对应的标准和协议文档也是必须的
3) 其他文档
围绕DUT其他各种文档(产品需求、架构文档、算法说明、产品说明书、应用手册等)
4) 通用基本逻辑单元的常规测试点
某些常用的基本逻辑单元有基本的测试点要求,(比如fifo的空满、溢出)
5) 来自设计工程师的要求
DUT设计工程师的要求(比如有某种特殊功能要求)
6) 来自验证工程师的经验
5. 测试点分解的原则
1) 测点完全:若资源允许,DUT的全部功能都应当被测试。
2) 描述精准:测试点的描述应当清晰、准确。
3) 精细适度:测试点既要覆盖全面,又要避免过度验证。
4) 有先有后:资源有限的情况下,把握优先级。
5) 过程持续:测试点分解并非一蹴而就,需要反复迭代更新。
图中红色框选的两个步骤,在完整的验证流程中可能需要被迭代执行多次。
6. 测试点分解的步骤
7. 测试点分解的思路与方法
3.2 模块级测试点分解
1. 从spec中提取测试点
3.3 AHB to APB sync-down bridge
在某些应用中,为了实现低功耗,会对PCLK进行门控,产生一个门控PCLK——PCLKG,APBACTIVE就是这个门控开关。
PCLKEN是用来对HCLK进行分频的,产生同步的PCLK。简单的理解是PCLKEN和HCLK相与产生PCLK,那么PCLKEN不同的占空比就能产生不同的PCLK(不过实际上不会是简单的相与,有专门的无毛刺设计)。
3.4 AHB to APB sync-down bridge 验证计划
3.4.1 Metrics Driven Verification(指标驱动验证,MDV)
指标驱动验证是一种基于指标集合的方法。它用于提高验证工作的可预测性、生产力(效率)和质量。MDV是一种新的功能验证方法学。核心还是基于coverage,但是将coverage和验证计划进行了结合。将验证得到的metric反标到验证计划,得到可视化结果,从而确定验证到达了哪一个阶段,下一步的验证方向应该是什么,以及最终判断验证是否完备。保证了验证的快速收敛。metric其实就是覆盖率,在cadence工具中是这样称呼的。
简而言之,该方法基于连续执行的四个步骤,直到结果满足假定标准:
1)规划:制定验证计划,根据验证的testpoint(测试点),利用vplan工具,转化为vplan工程,供后续metric反标使用。
2)构建验证环境:在此阶段,工程师尝试重用现有的验证IP和基于约束随机的SystemVerilog UVM或OSVMM环境,以创建涵盖验证计划的验证方案。
3)测试方案执行:在此阶段,将针对项目的每个修订执行回归测试。代码或功能覆盖率等指标是使用 ACDB 技术捕获的。结果将映射到验证计划中。
4)测量和分析:coverage收集和分析,反标到vplan,确定验证是否signoff。
3.4.2 AHB-to-APB Bridge 验证计划
1. 阅读AHB to APB Bridge spec,规划设计验证环境
2. 阅读AHB to APB Bridge spec,测试点分解
列出测试点
设计测试用例
设计assertion和规划coverage目标
测试点不全可能会导致流片后还发现bug;有的时候时间紧、资源有限,某些检查并不能及时做或者不会去做,比如对于parameter,各种可能的取值以及不同取值参数之间的组合,往往需要大量测试,也许就不能测试所有组合。
测试用例应该从简单的、基本的开始,因为一开始的bug可能比较多(dut的或者test的),验证甚至可能都跑不起来,太复杂的case不利于debug。
代码覆盖率通常是不会达到100%的,有些代码确实不会执行,但是在覆盖率分析里是可以设置不去统计这些代码的。功能覆盖率一般就是自己定义的覆盖点和覆盖组,既然已经定义出来了,就应该都测试到,除非时间紧迫。
第四章 UVM环境搭建
4.1 apb_slave_agent环境搭建
4.1.1 apb_slv_tran
apb_slv_tran是apb_slave_agent内部TLM通信的载体(transaction),具体传输哪些信息要根据spec来决定。PSEL和PENABLE属于外部控制输入,不需要参与内部通信传输,其他的都是需要的。
具体代码如下图,其中op_kind是读写操作的枚举变量;pdata存放传输的数据,具体是读数据还是写数据由op_kind决定;nready_num是传输中插入not ready的个数(此处可以进一步添加一个not ready插入位置的变量)。
class apb_slv_tran extends uvm_sequence_item;
rand op_kind opkind;
rand logic [31:0] paddr;
rand logic [31:0] pdata;
rand logic [2:0] pprot;
rand logic [3:0] pstrb;
rand logic pready;
rand logic pslverr;
rand int unsigned nready_num;
`uvm_object_utils_begin(apb_slv_tran)
`uvm_field_enum(op_kind, opkind ,UVM_DEFAULT)
`uvm_field_int(paddr, UVM_DEFAULT)
`uvm_field_int(pdata, UVM_DEFAULT)
`uvm_field_int(pprot, UVM_DEFAULT)
`uvm_field_int(pstrb, UVM_DEFAULT)
`uvm_field_int(pready, UVM_DEFAULT)
`uvm_field_int(pslverr, UVM_DEFAULT)
`uvm_field_int(nready_num, UVM_DEFAULT)
`uvm_object_utils_end
function new(string name = "apb_slv_tran");
super.new(name);
endfunction
endclass: apb_slv_tran
4.1.2 apb_slv_if
在编写完apb_slv_tran后,紧接着可以编写apb_slv_drv,不过driver需要使用到interface,故需要先编写apb_slv_if。代码如下,参考代码中定义了时钟块,不过似乎并未用到,在其他的讨论中看到modport用得更多;此外,我们这里的apb是作为slave的,如果作为master,那么drv_ck中的输入输出方向应该相反。
import uvm_pkg::*;
`include "uvm_macros.svh"
`timescale 1ns/1ps
interface apb_slv_if(input pclk, input presetn);
logic [31:0] paddr;
logic penable;
logic psel;
logic pwrite;
logic [3:0] pstrb;
logic [2:0] pprot;
logic [31:0] pwdata;
logic pready;
logic pslverr;
logic [31:0] prdata;
clocking drv_cb @(posedge pclk);
default input #1ps output #1ps;
output pready, pslverr, prdata;
input presetn, paddr, penable, psel, pwrite, pstrb, pprot, pwdata;
endclocking
clocking mon_cb @(posedge pclk);
default input #1ps output #1ps;
input presetn, paddr, penable, psel, pwrite, pstrb, pprot, pwdata, pready, pslverr, prdata;
endclocking
/** APB Interface SVA **/
property p_paddr_no_x;
@(posedge pclk) disable iff(!presetn)
psel |-> !$isunknown(paddr);
endproperty: p_paddr_no_x
property p_psel_rose_next_cycle_penable_rise;
@(posedge pclk) disable iff(!presetn)
$rose(psel) |=> $rose(penable);
endproperty: p_psel_rose_next_cycle_penable_rise
property p_penable_rose_next_cycle_fall;
@(posedge pclk) disable iff(!presetn)
penable && pready |=> $fell(penable);
endproperty: p_penable_rose_next_cycle_fall
property p_pwdata_stable_during_trans_phase;
@(posedge pclk) disable iff(!presetn)
((psel && !penable) ##1 (psel && penable)) |-> $stable(pwdata);
endproperty: p_pwdata_stable_during_trans_phase
property p_paddr_stable_until_next_trans;
logic[31:0] addr1, addr2;
@(posedge pclk) disable iff(!presetn)
first_match(($rose(penable),addr1=paddr) ##[0:5] ((psel && !penable)[=1],addr2=$past(paddr,2))) |-> addr1 == addr2;
endproperty: p_paddr_stable_until_next_trans
property p_pwrite_stable_until_next_trans;
logic pwrite1, pwrite2;
@(posedge pclk) disable iff(!presetn)
first_match(($rose(penable),pwrite1=pwrite) ##[0:5] ((psel && !penable)[=1],pwrite2=$past(pwrite,2))) |-> pwrite1 == pwrite2;
endproperty: p_pwrite_stable_until_next_trans
// property p_prdata_available_once_penable_rose;
// @(posedge pclk) disable iff(!presetn)
// penable && !pwrite && pready |-> !$stable(prdata);
// endproperty: p_prdata_available_once_penable_rose
a_paddr_no_x: assert property(p_paddr_no_x) else `uvm_error("ASSERT", "PADDR is unknown when PSEL is high")
a_psel_rose_next_cycle_penable_rise: assert property(p_psel_rose_next_cycle_penable_rise) else `uvm_error("ASSERT", "PENABLE not rose after 1 cycle PSEL rose")
a_penable_rose_next_cycle_fall: assert property(p_penable_rose_next_cycle_fall) else `uvm_error("ASSERT", "PENABLE not fall after 1 cycle PENABLE rose")
a_pwdata_stable_during_trans_phase: assert property(p_pwdata_stable_during_trans_phase) else `uvm_error("ASSERT", "PWDATA not stable during transaction phase")
a_paddr_stable_until_next_trans: assert property(p_paddr_stable_until_next_trans) else `uvm_error("ASSERT", "PADDR not stable until next transaction start")
a_pwrite_stable_until_next_trans: assert property(p_pwrite_stable_until_next_trans) else `uvm_error("ASSERT", "PWRITE not stable until next transaction start")
// a_prdata_available_once_penable_rose:assert property(p_prdata_available_once_penable_rose) else `uvm_error("ASSERT", "PRDATA not available once PENABLE rose")
endinterface: apb_slv_if
把APB接口时序检查的SVA写在这里,需要注意的是这里关掉了对PRDATA的检查,在大量随机测试时发现,有时候两次读操作随机出来的PRDATA是相同的,这不符合我们这里的断言规则。
4.1.3 apb_slv_drv
driver的难度稍高,我是以MCDF实验为模板的。MCDF实验用的是set_interface函数,我这里用的是在build_phase里通过uvm_config_db获取接口,其实这也是MCDF实验可改进的一点。
class apb_slv_drv extends uvm_driver #(apb_slv_tran);
local virtual apb_slv_if intf;
`uvm_component_utils(apb_slv_drv)
function new(string name = "apb_slv_drv", uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual apb_slv_if)::get(this, "", "intf", intf)) begin
`uvm_fatal("NO VIF","vif is not found")
end
endfunction
task run_phase(uvm_phase phase);
fork
this.do_reset();
this.do_driver();
join
endtask: run_phase
task do_reset();
forever begin
@(negedge intf.presetn);
intf.pready <= 1;
intf.pslverr <= 0;
intf.prdata <= 32'd0;
end
endtask: do_reset
task do_driver();
apb_slv_tran req;
@(posedge intf.presetn);
forever begin
@(posedge intf.pclk);
if (intf.psel && !intf.penable) begin
seq_item_port.get_next_item(req);
this.drv_one_pkt(req);
seq_item_port.item_done();
end
end
endtask: do_driver
task drv_one_pkt(apb_slv_tran req);
int nready_cnt;
nready_cnt = req.nready_num;
while (nready_cnt != 0) begin
intf.drv_cb.pready <= 0;
nready_cnt--;
@(intf.drv_cb);
end
intf.drv_cb.pready <= 1;
intf.drv_cb.pslverr <= req.pslverr;
if (intf.drv_cb.pwrite) begin
drv_wr_pkt(req);
end
else begin
drv_rd_pkt(req);
end
endtask: drv_one_pkt
task drv_wr_pkt(apb_slv_tran req);
if (!req.pslverr) begin
end
endtask
task drv_rd_pkt(apb_slv_tran req);
if (!req.pslverr) begin
intf.drv_cb.prdata <= req.pdata;
end
else begin
intf.drv_cb.prdata <= req.pdata;
end
endtask
endclass: apb_slv_drv
run_phase里包含两个任务:用于复位的do_reset()和用于驱动激励的do_driver()。
这里关注一下if的条件,“intf.psel && !intf.penable”是第二个时钟上升沿,APB作为slave,当检测到这一条件时,开启驱动任务。
但是这里我有些疑问待后续调试时判断,根据时序要求,写操作应该在psel拉高时准备好pwdata,读操作才是在上面那个条件时准备好rdata。
解答:这里搞混了,APB作为slave它是接受master的读写任务,而不是发起者。在psel ==1、penable==0这个时钟沿,slave采样psel、penable、paddr、pwrite,并做出相应的动作。
激励的驱动具体由drv_one_pkt()完成,req是对应的sequence产生的(还没编写到),其中包括拉低pready的周期数nready_num,延迟结束后再把pready拉高。
可以看到这里写驱动没有内容,因为这里我尚未想好。作为salve,当接收到写操作命令时,应该把数据写入对应地址中,但这个项目是验证bridge的,APB并没有下行设备,所以有没有真的写入并不重要,检查AHB的各个指令经过bridge后到达APB一侧有无改变是更重要的。同理对于读操作,随机产生的req完全可以用于验证。
还需要注意的是,根据APB协议的规定pslverr信号不会影响读写,我这里去判断它只是希望在调试的时候更清除地看到pslverr的情况。调试结束后这里还是改成了req.pdata。
4.1.4 apb_slv_mon
monitor中通常使用analysis port来传输抽取到的transaction,在UVM中,对象的例化一般发生在build_phase阶段。
build_phase主要用于创建、连接和配置测试环境中的各个组件和数据结构。在build_phase中,会实例化测试环境中的组件对象,配置它们的参数,连接它们的端口等。通常在build_phase中执行一次性的、在整个测试过程中保持不变的设置和配置。
class apb_slv_mon extends uvm_monitor;
local virtual apb_slv_if intf;
uvm_analysis_port #(apb_slv_tran) mon_ap_port;
`uvm_component_utils(apb_slv_mon)
function new(string name = "apb_slv_mon", uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_slv_if)::get(this, "", "intf", intf)) begin
`uvm_fatal("NO VIF","vif is not found")
end
mon_ap_port = new("mon_ana_port", this);
endfunction
task run_phase(uvm_phase phase);
this.mon_tran();
endtask
task mon_tran();
apb_slv_tran pkt;
forever begin
@(intf.mon_cb);
if (intf.mon_cb.psel && intf.mon_cb.penable && intf.mon_cb.pready) begin
pkt = apb_slv_tran::type_id::create("pkt", this);
pkt.paddr = intf.mon_cb.paddr;
pkt.pprot = intf.mon_cb.pprot;
pkt.pstrb = intf.mon_cb.pstrb;
pkt.pslverr = intf.mon_cb.pslverr;
pkt.opkind = intf.mon_cb.pwrite ? WRITE : READ;
pkt.pdata = intf.mon_cb.pwrite ? intf.mon_cb.pwdata : intf.mon_cb.prdata;
mon_ap_port.write(pkt);
// $display("intf.mon_cb.pstrb = %4b", intf.mon_cb.pstrb);
// $display("pkt.pstrb = %4b", pkt.pstrb);
end
end
endtask
endclass: apb_slv_mon
注意monitor采样的条件,只有当psel、penable以及pready同时为高时,这一次的操作才是完成的(不管是否发生了错误),尤其是读操作,prdata在此之前并未准备好。这避免频繁采样,降低仿真效率。
不过这里没有采样pstrb和pready信号,暂时不知道为什么,等后面写到scoreboard了也许就知道了。
解答:pready是采样条件不需要再采样了,从scoreboard来看pstrb是漏了,复盘的时候得再加上。
这里还有一个问题,我们是利用pkt进行transaction传递的,可以看到这里是在循环里例化pkt的,而不是在循环外例化一次。每一次循环我们都创建一个新的pkt对象,装填本次的数据,然后通过write()操作把这次的对象传递给analysis port。System Verilog有内存自动回收机制,当传过去的对象被处理完后,对象就会被回收,所分配的内存也随即释放(SV中规定没有句柄指向的对象会被回收)。
如果我们只在循环外面例化一次,那么会导致传输的数据始终不变(这里不理解为什么不变,也许是说错了)。
解答:实际上传过去的只是pkt的句柄,所以如果只在循环外面例化一次,那么始终就只有一个pkt实例对象,每次循环都会产生新的句柄送出去,而这些句柄都指向这唯一的pkt,这样pkt的内容是一直在被覆盖修改的,始终只有最后一次的值。
4.1.5 apb_slv_sqr
sequencer的定义非常简单,这也是白皮书的原话。
4.1.6 apb_slv_agt
class apb_slv_agt extends uvm_agent;
`uvm_component_utils(apb_slv_agt)
apb_slv_drv driver;
apb_slv_mon monitor;
apb_slv_sqr sequencer;
local virtual apb_slv_if intf;
function new(string name = "apb_slv_agt", uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual apb_slv_if)::get(this, "", "intf", intf)) begin
`uvm_fatal("NO VIF","vif is not found")
end
monitor = apb_slv_mon::type_id::create("monitor", this);
uvm_config_db#(virtual apb_slv_if)::set(this,"monitor","intf",intf);
void'(uvm_config_db#(uvm_active_passive_enum)::get(this,"","is_active",is_active));
if (is_active == UVM_ACTIVE) begin
driver = apb_slv_drv::type_id::create("driver", this);
sequencer = apb_slv_sqr::type_id::create("sequencer", this);
uvm_config_db#(virtual apb_slv_if)::set(this,"driver","intf",intf);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (is_active == UVM_ACTIVE) begin
driver.seq_item_port.connect(sequencer.seq_item_export);
end
endfunction
endclass: apb_slv_agt
agent的重点在build_phase部分,注意config_db的接口传递。我们先在agent层get()到了intf,然后才例化monitor并把intf再set()到monitor层,这是因为在tb层我们只把接口发送到了agent层。其实发到哪一层并没有严格限制,这和代码重用性相关,有人需要整个env可重用,那么在tb层发到env也是可以的。这里提一下通配符的使用,通配符在多接收方的场景中很方便,这样不必为每个接收方都set()一次。但是不能滥用通配符,这会导致结构模糊,降低代码的可读性。
这里再提一下is_active,它是uvm_agent的一个成员变量,其默认值为UVM_ACTIVE,在UVM源码中可以看到其原型。UVM_ACTIVE模式下用于输入端口,是需要实例化driver和sequencer的;UVM_PASSIVE模式,用于输出端口,不需要驱动任何信号,只需要监测信号,故只需要实例化monitor。通过is_active变量,agent在build_phase()和connect_phase()等方法中通过选择语句来对driver和sequencer进行有条件的例化和连接。
区分两种模式的目的有两个:一是提高agent的可重用性,有时候一个agent既可以做输入,也可以做输出,那就不用编写两种agent了,直接复用,不过输出不需要激励,故通过模式选择来决定具体需要例化哪些组件;二是在验证的集成方面有需要,我们目前做的都是模块级的验证,当一个个模块验证完后,需要集成起来进行系统级的验证,那时候有些模块的输入就由别的模块产生了,而不像模块级验证时需要sequencer模拟产生,因此这时候driver和sequencer就没用了,故可以设置为passive模式。
等到后面我们还会编写一个uvm_config文件,它会负责验证平台的各种配置并通过config_db发送到指定位置。
4.1.7 ahbl_slv_pkt
4.2 ahbl_master_agent环境搭建
4.2.1 ahbl_mst_tran
class ahbl_mst_tran extends uvm_sequence_item;
rand logic hsel = 1'b0;
rand logic [31:0] haddr = 32'h0;
rand htrans_t htrans = NSEQ;
rand hsize_t hsize = BYTE;
rand hburst_t hburst = SINGLE;
rand logic [3:0] hprot = 4'h0;
rand logic hwrite = 1'b0;
rand logic [31:0] hrwdata = 32'h0;
rand logic hresp = 1'b0;
rand logic hreadyout = 1'b0;
protected rand int unsigned burst_beats;
protected rand logic [31:0] haddr_q[$];
protected rand logic [31:0] hrwdata_q[$];
protected rand htrans_t htrans_q[$];
protected int unsigned haddr_idx = 0;
protected int unsigned hrwdata_idx = 0;
protected int unsigned htrans_idx = 0;
`uvm_object_utils_begin(ahbl_mst_tran)
`uvm_field_int (hsel, UVM_DEFAULT)
`uvm_field_int (haddr, UVM_DEFAULT)
`uvm_field_enum (htrans_t,htrans, UVM_DEFAULT)
`uvm_field_enum (hsize_t,hsize, UVM_DEFAULT)
`uvm_field_enum (hburst_t,hburst, UVM_DEFAULT)
`uvm_field_int (hprot, UVM_DEFAULT)
`uvm_field_int (hwrite, UVM_DEFAULT)
`uvm_field_int (hrwdata, UVM_DEFAULT)
`uvm_field_int (hresp, UVM_DEFAULT)
`uvm_field_int (hreadyout, UVM_DEFAULT)
`uvm_field_int (burst_beats, UVM_DEFAULT)
`uvm_field_queue_int (haddr_q, UVM_DEFAULT)
`uvm_field_queue_int (hrwdata_q, UVM_DEFAULT)
`uvm_field_queue_enum (htrans_t,htrans_q, UVM_DEFAULT)
`uvm_field_int (haddr_idx, UVM_DEFAULT)
`uvm_field_int (hrwdata_idx, UVM_DEFAULT)
`uvm_field_int (htrans_idx, UVM_DEFAULT)
`uvm_object_utils_end
constraint haddr_constr{
(hsize == HWORD) -> (haddr[0] == 1'b0);
(hsize == WORD) -> (haddr[1:0] == 2'b0);
solve hsize before haddr;
}
constraint htrans_constr{
(htrans == IDLE) -> (hburst == SINGLE);
solve htrans before haddr;
}
constraint hburst_constr{
(hburst == SINGLE) -> (burst_beats == 1);
(hburst == INCR) -> (burst_beats == 10);
(hburst == WRAP4) -> (burst_beats == 4);
(hburst == WRAP8) -> (burst_beats == 8);
(hburst == WRAP16) -> (burst_beats == 16);
(hburst == INCR4) -> (burst_beats == 4);
(hburst == INCR8) -> (burst_beats == 8);
(hburst == INCR16) -> (burst_beats == 16);
solve hburst before burst_beats;
}
constraint queue_constr{
haddr_q.size() inside {1,4,8,16};
hrwdata_q.size() inside {1,4,8,16};
htrans_q.size() inside {1,4,8,16};
haddr_q.size() == burst_beats;
hrwdata_q.size() == burst_beats;
htrans_q.size() == burst_beats;
solve burst_beats before haddr_q;
solve burst_beats before hrwdata_q;
solve burst_beats before htrans_q;
}
function new(string name = "ahbl_mst_tran");
super.new(name);
hsel = 1'b0;
haddr = 32'h0;
htrans = NSEQ;
hsize = BYTE;
hburst = SINGLE;
hprot = 4'h0;
hwrite = 32'h0;
hresp = 1'b0;
hreadyout = 1'b0;
haddr_idx = 0;
hrwdata_idx = 0;
htrans_idx = 0;
endfunction
function void post_randomize();
int i;
haddr_q[0] = haddr;
htrans_q[0] = NSEQ;
hrwdata_q[0] = hrwdata;
for(i = 1; i < burst_beats; i++) begin
haddr_q[i] = haddr_q[i-1] + (2**hsize);
htrans_q[i] = SEQ;
// $display("!!! haddr_q[%0d] = %0h !!!", i,haddr_q[i]);
end
endfunction
virtual function logic [31:0] next_haddr();
haddr_idx++;
return haddr_q[haddr_idx-1];
endfunction
virtual function logic [31:0] next_hrwdata();
hrwdata_idx++;
return hrwdata_q[hrwdata_idx-1];
endfunction
virtual function logic [31:0] next_htrans();
htrans_idx++;
return htrans_q[htrans_idx-1];
endfunction
virtual function bit last_beat();
return (htrans_idx == htrans_q.size());
endfunction
virtual function int get_burst_beats();
return (burst_beats);
endfunction
endclass: ahbl_mst_tran
对照spec中master的接口图,可以发现在定义的transaction里没有hready,反而有一个hreadyout。
在实际应用中,slave会输出hreadyout信号,多个salve的hreadyout信号通过Multiplexor综合生成整体的hready,而hready信号会输入给所有的master和slave。
对照bridge的接口图,APB slave输出的pready接到左边的PREADY输入,经过bridge后从右边的HREADYOUT输出,因为我们这里只有当master和slave,所以HREADYOUT直接接到了AHB master的hready,而hready又是接到HREADY输入的。
4.2.2 ahbl_mst_if
import uvm_pkg::*;
`include "uvm_macros.svh"
`timescale 1ns/1ps
interface ahbl_mst_if(input hclk, input hresetn);
logic hsel;
logic [31:0] haddr;
logic [ 1:0] htrans;
logic [ 2:0] hsize;
logic [ 2:0] hburst;
logic [ 3:0] hprot;
logic hwrite;
logic [31:0] hwdata;
logic [31:0] hrdata;
logic hready;
logic hresp;
clocking drv_cb @(posedge hclk);
default input #1ps output #1ps;
// default input #10ns output #10ns;
input hresetn;
output hsel;
output haddr;
output htrans;
output hsize;
output hburst;
output hprot;
output hwrite;
output hwdata;
input hrdata;
input hready;
input hresp;
endclocking
clocking mon_cb @(posedge hclk);
default input #1ps output #1ps;
input hresetn;
input hsel;
input htrans;
input haddr;
input hburst;
input hprot;
input hsize;
input hwdata;
input hwrite;
input hrdata;
input hready;
input hresp;
endclocking
/** AHB Interface SVA **/
// 1. Check WRAP_4_BYTE
property p_check_wrap4_byte;
@(posedge hclk) disable iff(!hresetn)
((htrans==3) && (hburst==2) && (hsize==0) && ($past(hready)) && ($past(htrans)!=1))|->
(haddr == $past(haddr) + 1);
endproperty: p_check_wrap4_byte
// 2. Check WRAP_4_HALF_WORD
property p_check_wrap4_halfword;
@(posedge hclk) disable iff(!hresetn)
((htrans==3) && (hburst==2) && (hsize==1) && ($past(hready)) && ($past(htrans)!=1))|->
(haddr == $past(haddr) + 2);
endproperty: p_check_wrap4_halfword
// 3. Check WRAP_4_WORD
property p_check_wrap4_word;
@(posedge hclk) disable iff(!hresetn)
((htrans==3) && (hburst==2) && (hsize==2) && ($past(hready)) && ($past(htrans)!=1))|->
(haddr == $past(haddr) + 4);
endproperty: p_check_wrap4_word
// 4. Check WRAP_8_BYTE
property p_check_wrap8_byte;
@(posedge hclk) disable iff(!hresetn)
(htrans==3) &&( hburst==4) && (hsize==0) && ($past(hready)) && ($past(htrans)!=1)|->
(haddr == $past(haddr) + 1);
endproperty: p_check_wrap8_byte
// 5. Check WRAP_8_HALF_WORD
property p_check_wrap8_halfword;
@(posedge hclk) disable iff(!hresetn)
(htrans==3) &&( hburst==4) && (hsize==1) && ($past(hready)) && ($past(htrans)!=1)|->
(haddr == $past(haddr) + 2);
endproperty: p_check_wrap8_halfword
// 6. Check WRAP_8_WORD
property p_check_wrap8_word;
@(posedge hclk) disable iff(!hresetn)
(htrans==3) &&( hburst==4) && (hsize==2) && ($past(hready)) && ($past(htrans)!=1)|->
(haddr == $past(haddr) + 4);
endproperty: p_check_wrap8_word
// 7. Check_KB_boundary
property p_check_KB_boundary;
@(posedge hclk) disable iff(!hresetn)
(htrans==3) |-> haddr[10:0] != 11'b1000_0000;
endproperty: p_check_KB_boundary
// 8. Check NONSEQ SINGLE BURST NOT TERMINATED BY BUSY
property p_check_no_busy_follow_single;
@(posedge hclk) disable iff(!hresetn)
(hburst==0)|=> (htrans!=1);
endproperty: p_check_no_busy_follow_single
// 9. BUSY FOLLOWS IDLE
property p_check_busy_idle;
@(posedge hclk) disable iff(!hresetn)
(htrans==1) ##1 (htrans==0)|-> ($past(hburst,1)==1);
endproperty: p_check_busy_idle
// 10. Two cycle ERROR REPONSE
property p_two_cycle_error;
@(posedge hclk) disable iff(!hresetn)
(hresp==1) ##1 (hresp==1)|-> (hready==1) && ($past(hready) == 0);
endproperty: p_two_cycle_error
a_check_wrap4_byte: assert property(p_check_wrap4_byte) else `uvm_error("ASSERT", "The HADDR of WRAP_4_BYTE is not right")
a_check_wrap4_halfword: assert property(p_check_wrap4_halfword) else `uvm_error("ASSERT", "The HADDR of WRAP_4_HALF_WORD is not right")
a_check_wrap4_word: assert property(p_check_wrap4_word) else `uvm_error("ASSERT", "The HADDR of WRAP_4_WORD is not right")
a_check_wrap8_byte: assert property(p_check_wrap8_byte) else `uvm_error("ASSERT", "The HADDR of WRAP_8_BYTE is not right")
a_check_wrap8_halfword: assert property(p_check_wrap8_halfword) else `uvm_error("ASSERT", "The HADDR of WRAP_8_BYTE is not right")
a_check_wrap8_word: assert property(p_check_wrap8_word) else `uvm_error("ASSERT", "The HADDR of WRAP_8_BYTE is not right")
a_check_KB_boundary: assert property(p_check_KB_boundary) else `uvm_error("ASSERT", "The HADDR cross the 1KB boundary")
a_check_no_busy_follow_single: assert property(p_check_no_busy_follow_single) else `uvm_error("ASSERT", "BUSY follows SINGLE transfer")
a_check_busy_idle: assert property(p_check_busy_idle) else `uvm_error("ASSERT", "The HRESP do not offer zero wait resp")
a_two_cycle_error: assert property(p_two_cycle_error) else `uvm_error("ASSERT", "Incorrect hresp signal")
endinterface: ahbl_mst_if
4.2.3 ahbl_mst_pkt
pkt里比较重要的就是这三个枚举类型。
4.2.4 ahbl_mst_drv
class ahbl_mst_drv extends uvm_driver #(ahbl_mst_tran);
`uvm_component_utils(ahbl_mst_drv)
local virtual ahbl_mst_if intf;
protected ahbl_mst_tran pkt_aphase = null;
protected ahbl_mst_tran pkt_dphase = null;
function new(string name = "ahbl_mst_drv", uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual ahbl_mst_if)::get(this, "", "intf", intf)) begin
`uvm_fatal("NO VIF","vif is not found")
end
endfunction
task run_phase(uvm_phase phase);
fork
this.do_reset();
this.do_driver();
join
endtask
task do_reset();
forever begin
@(negedge intf.hresetn);
intf.hsel <= 1'b0;
intf.haddr <= 32'd0;
intf.htrans <= 2'd0;
intf.hsize <= 3'd0;
intf.hburst <= 3'd0;
intf.hprot <= 4'd0;
intf.hwrite <= 1'b0;
intf.hwdata <= 32'd0;
intf.hrdata <= 32'd0;
intf.hready <= 1'b0;
intf.hresp <= 1'b0;
end
endtask: do_reset
task do_driver();
@(posedge intf.hresetn);
forever begin
@(posedge intf.drv_cb);
if (intf.drv_cb.hready) begin
if(pkt_dphase != null) begin // 如果data phase的包不是空的,那之前一定有过address phase
drive_1cyc_pkt_dphase(pkt_dphase); // 驱动data phase
if((pkt_dphase.hburst == SINGLE) | pkt_dphase.last_beat()) begin
seq_item_port.item_done(); // 如果是SINGLE传输或者最后一拍
pkt_aphase = null; // 告诉sequence序列处理完毕
pkt_dphase = null; // 清空data phase和address phase的pkt
end
end
if(pkt_aphase != null && intf.drv_cb.hready == 1'b1) begin // 经过前面pkt_aphase还没清空,说明没发完
drive_1cyc_pkt_aphase(pkt_aphase); // 再驱动一个address phase
end
else begin
seq_item_port.try_next_item(pkt_aphase); // 如果pkt_aphase是空的,要么是发完了,要么是从没发过
if(pkt_aphase != null) begin // 那么就try一个,如果拿到了
drive_1cyc_pkt_aphase(pkt_aphase); // 就先驱动一个address phase
end
else begin
drive_1cyc_idle(); // 没拿到就驱动一个IDLE
end
end
end
end
endtask: do_driver
task drive_1cyc_pkt_dphase(ref ahbl_mst_tran pkt);
if(intf.drv_cb.hready) begin
intf.drv_cb.hwdata <= pkt.hwrite ? pkt.next_hrwdata() : (32'd0);
end
endtask
task drive_1cyc_pkt_aphase(ref ahbl_mst_tran pkt);
if(intf.drv_cb.hready == 1'b1) begin
intf.drv_cb.hsel <= pkt.hsel;
// intf.drv_cb.haddr <= ((pkt.htrans != IDLE) & (pkt.htrans != BUSY))? pkt.next_haddr() : intf.haddr;
intf.drv_cb.haddr <= pkt.next_haddr();
intf.drv_cb.htrans <= pkt.next_htrans();
intf.drv_cb.hsize <= pkt.hsize;
intf.drv_cb.hburst <= pkt.hburst;
intf.drv_cb.hprot <= pkt.hprot;
intf.drv_cb.hwrite <= pkt.hwrite;
this.pkt_dphase = this.pkt_aphase;
// `uvm_info("TEST", $sformatf("!!!pkt.htrans = [%0h]!!!", pkt.htrans), UVM_LOW)
// `uvm_info("TEST", $sformatf("!!!pkt.haddr = [%0h]!!!", pkt.haddr), UVM_LOW)
// `uvm_info("TEST", $sformatf("!!!intf.drv_cb.haddr = [%0h]!!!", intf.drv_cb.haddr), UVM_LOW)
end
endtask
task drive_1cyc_idle();
intf.drv_cb.hsel <= 1'b1;
intf.drv_cb.haddr <= intf.haddr;
intf.drv_cb.htrans <= IDLE;
intf.drv_cb.hsize <= intf.hsize;
intf.drv_cb.hburst <= intf.hburst;
intf.drv_cb.hprot <= intf.hprot;
intf.drv_cb.hwrite <= intf.hwrite;
endtask
endclass: ahbl_mst_drv
4.2.5 ahbl_mst_mon
class ahbl_mst_mon extends uvm_monitor;
`uvm_component_utils(ahbl_mst_mon)
local virtual ahbl_mst_if intf;
uvm_analysis_port #(ahbl_mst_tran) mon_ap_port;
ahbl_mst_tran pkt;
function new(string name = "ahbl_mst_mon", uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual ahbl_mst_if)::get(this, "", "intf", intf)) begin
`uvm_fatal("NO VIF","vif is not found")
end
mon_ap_port = new("mon_ap_port", this);
endfunction
task run_phase(uvm_phase phase);
this.mon_tran();
endtask
task mon_tran();
forever begin
@(intf.mon_cb);
if (!intf.mon_cb.hresetn) begin
pkt = null;
end
else begin
if (intf.mon_cb.hready) begin
if(pkt != null)begin // 如果包不是空的,那么已经采过地址了
sample_dphase(pkt); // 采样数据
mon_ap_port.write(pkt); // 把包发出去
// `uvm_info("TEST", $sformatf("!!!MON ADDR = [%0h]!!!", pkt.haddr[15:0]), UVM_LOW)
pkt = null; // 把包清空
end // 如果包是空的那么直接跳到下面采样地址
end
if (intf.mon_cb.hready && intf.mon_cb.hsel && intf.mon_cb.htrans[1]) begin
sample_aphase(pkt); // 不是IDLE传输,采样地址
// `uvm_info("TEST", $sformatf("!!!SAMPLE ADDR = [%0h]!!!", pkt.haddr[15:0]), UVM_LOW)
end
end
end
endtask
virtual task sample_dphase(ref ahbl_mst_tran pkt);
pkt.hrwdata = pkt.hwrite ? intf.mon_cb.hwdata : intf.mon_cb.hrdata;
pkt.hresp = intf.mon_cb.hresp;
endtask
virtual task sample_aphase(ref ahbl_mst_tran pkt);
pkt = ahbl_mst_tran::type_id::create("pkt");
pkt.haddr = intf.mon_cb.haddr;
pkt.htrans = htrans_t'(intf.mon_cb.htrans);
pkt.hsize = hsize_t'(intf.mon_cb.hsize);
pkt.hburst = hburst_t'(intf.mon_cb.hburst);
pkt.hprot = intf.mon_cb.hprot;
pkt.hwrite = intf.mon_cb.hwrite;
endtask
endclass: ahbl_mst_mon
4.2.6 ahbl_mst_agt
agent与APB的基本一致,注意这里添加了一个uvm_fatal检查,之前没有,也应该改成这样的。
class ahbl_mst_agt extends uvm_agent;
ahbl_mst_drv driver;
ahbl_mst_mon monitor;
ahbl_mst_sqr sequencer;
local virtual ahbl_mst_if intf;
`uvm_component_utils(ahbl_mst_agt)
function new(string name = "ahbl_mst_agt", uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual ahbl_mst_if)::get(this, "", "intf", intf)) begin
`uvm_fatal("NO VIF","vif is not found")
end
monitor = ahbl_mst_mon::type_id::create("monitor", this);
uvm_config_db#(virtual ahbl_mst_if)::set(this, "monitor", "intf", intf);
if(!(uvm_config_db#(uvm_active_passive_enum)::get(this,"","is_active",is_active))) begin
`uvm_fatal("is_active","is_active is not set!")
end
if (is_active == UVM_ACTIVE) begin
driver = ahbl_mst_drv::type_id::create("driver", this);
sequencer = ahbl_mst_sqr::type_id::create("sequencer", this);
uvm_config_db#(virtual ahbl_mst_if)::set(this, "driver", "intf", intf);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (is_active == UVM_ACTIVE) begin
driver.seq_item_port.connect(sequencer.seq_item_export);
end
endfunction
endclass: ahbl_mst_agt
4.3 顶层tb搭建
4.3.1 testbench
`timescale 1ns/1ps
import uvm_pkg::*;
`include "uvm_macros.svh"
import ahbl_mst_pkg::*;
import apb_slv_pkg::*;
import ahbl2apb_pkg::*;
import ahbl2apb_tc_pkg::*;
// tb
module tb;
logic hclk;
logic hresetn;
parameter HCLK_PERIOD = 100ns; // 10MHz
// pclk, presetn
wire pclk;
wire presetn;
reg [3:0] hclk_cnt;
reg pclken;
reg pclken_r;
bit [1:0] tmp_var;
int HCLK_PCLK_RATIO;
ahbl_mst_if ahbl_if_i(hclk, hresetn);
apb_slv_if apb_if_i(pclk, presetn);
// clock generation
initial begin
hclk <= 0;
forever begin
#(HCLK_PERIOD / 2) hclk <= !hclk;
end
end
// reset trigger
initial begin
// hresetn <= 1'b1;
#5 hresetn <= 1'b0;
repeat (100) @(negedge hclk);
#2 hresetn <= 1'b1;
end
initial begin
tmp_var = $urandom_range(0,3);
case(tmp_var)
0:HCLK_PCLK_RATIO = 1;
1:HCLK_PCLK_RATIO = 2;
2:HCLK_PCLK_RATIO = 4;
3:HCLK_PCLK_RATIO = 8;
endcase
end
always @(posedge hclk or negedge hresetn) begin
if(!hresetn)
hclk_cnt <= 4'd0;
else if(hclk_cnt == (HCLK_PCLK_RATIO - 1'b1))
hclk_cnt <= 4'd0;
else
hclk_cnt <= hclk_cnt + 1'd1;
end
always @(negedge hclk or negedge hresetn) begin
if(!hresetn)
pclken <= 1'b0;
else if(hclk_cnt == (HCLK_PCLK_RATIO - 1'b1))
pclken <= 1'b1;
else
pclken <= 1'b0;
end
always @(*) begin
#1ns;
pclken_r = pclken;
end
assign pclk = pclken_r & hclk;
assign presetn = hresetn;
wire apbactive;
cmsdk_ahb_to_apb #(
.ADDRWIDTH (16),
.REGISTER_RDATA (1) ,
.REGISTER_WDATA (0) )
dut(
.HCLK (hclk),
.HRESETn (hresetn),
.PCLKEN (pclken),
.HSEL (ahbl_if_i.hsel),
.HADDR (ahbl_if_i.haddr[15:0]),
.HTRANS (ahbl_if_i.htrans),
.HSIZE (ahbl_if_i.hsize),
.HPROT (ahbl_if_i.hprot),
.HWRITE (ahbl_if_i.hwrite),
.HREADY (ahbl_if_i.hready),
.HWDATA (ahbl_if_i.hwdata),
.HREADYOUT (ahbl_if_i.hready),
.HRDATA (ahbl_if_i.hrdata),
.HRESP (ahbl_if_i.hresp),
.PADDR (apb_if_i.paddr[15:0]),
.PENABLE (apb_if_i.penable),
.PWRITE (apb_if_i.pwrite),
.PSTRB (apb_if_i.pstrb),
.PPROT (apb_if_i.pprot),
.PWDATA (apb_if_i.pwdata),
.PSEL (apb_if_i.psel),
.PRDATA (apb_if_i.prdata),
.PREADY (apb_if_i.pready),
.PSLVERR (apb_if_i.pslverr),
.APBACTIVE (apbactive)
);
assign apb_if_i.paddr[31:16] = 16'd0;
initial begin
uvm_config_db#(virtual ahbl_mst_if)::set(uvm_root::get(), "uvm_test_top.env_t.ahbl_mst_agt_t", "intf", ahbl_if_i);
uvm_config_db#(virtual apb_slv_if)::set(uvm_root::get(), "uvm_test_top.env_t.apb_slv_agt_t", "intf", apb_if_i);
run_test("ahbl_mst_single_read32");
end
/** SVA **/
property p_psel_high_then_apbactive_high;
@(posedge pclk) disable iff(!hresetn)
apb_if_i.psel |-> apbactive;
endproperty
property p_apbactive_high_then_psel_high;
@(posedge pclk) disable iff(!hresetn)
$rose(apbactive) |=> apb_if_i.psel;
endproperty
a_psel_high_then_apbactive_high: assert property(p_psel_high_then_apbactive_high) else `uvm_error("ASSERT", "PSEL is low after APBACTIVE is high")
a_apbactive_high_then_psel_high: assert property(p_apbactive_high_then_psel_high) else `uvm_error("ASSERT", "APBACTIVE is low after PSEL is high")
endmodule
声明时间单位和精度,导入uvm库和自定义的包。
定义时钟周期,产生时钟和复位信号。因为AHB的时钟和复位使用的系统信号,所以这里也就直接定义了hclk和hresetn。
APB的prestn也直接使用系统信号,所以直接连接hrestn,比较麻烦的是pclk,根据bridge的spec,AHB到APB是同步下降bridge,所以APB的时钟频率不超过AHB的。根据spec要求,通过pclken来调节pclk。(可以理解为实现一个分频比可调的分频器,对hclk进行分频,产生pclk。)
这里通过将分频信号简单相与得到了pclk,实际上不会这么简单,容易有毛刺。注意pclken的翻转发生在hclk的下降沿,而不是上升沿。这是因为pclken的翻转肯定是在hclk时钟沿的后面,而我们需要把pclken和hclk相与,所以如果在上升沿做就容易有竞争冒险(结合波形图容易理解)。
4.3.2 SVA编写
这里把断言写在顶层tb里,一般会写在RTL里、module的接口里或者单独的文件里。对于AHB和APB有成熟的断言IP,可以直接拿来用。
下面的断言检查的是PSEL和APBACTIVE信号的关系,当PESL为高时,APBACTIVE必须为高;当APBACTIVE拉高时,PSEL在下一周期也必须为高。
下面的断言检查的是HRESP信号和HREADY信号的关系,按照spec的规定,AHB的OKAY信号(HRESP为0)只需一个周期,而ERROR信号(HRESP为0)则需要两个周期。第一个周期HRESP为高,HREADY为低;第二周期HRESP保持,HREADY拉高。
下面是进行断言,相当于启动。
下面是对断言开关的控制,在复位时会有许多不稳定的信号,需要关闭断言以避免不必要的错误。
4.4 ahbl2apb_pkg
4.4.1 env搭建
在env中声明、例化master agent、slave agent以及scoreboard(下面紧接着会编写),在connect_phase连接monitor中的analysis port和scoreboard中的uvm_tlm_analysis_export。
在MCDF实验中,env里还有virtual sequence和virtual sequencer,这个实验并没有用到,可能是因为本实验比较简单,后面可以自己改造一下,给加进去。
4.4.2 scoreboard搭建
首先需要声明两个uvm_tlm_analysis_fifo:ahbl_fifo和apb_fifo,这是和monitor的analysis port连接的。同时还需要两个uvm_blocking_get_port:ahbl_bg_port和apb_bg_port,这是和uvm_tlm_analysis_fifo的blocking_get_export连接的。
在UVM的FIFO通信中一共有两种TLM FIFO:uvm_tlm_analysis_fifo和uvm_tlm_fifo。uvm_tlm_analysis_fifo的接口示意图如下图,uvm_tlm_fifo没有analysis_export端口且没有write函数(不支持write操作),其他方面两种FIFO完全相同。
FIFO上的这些端口可以连接到对应的PORT上,完成相应的通信任务。必须清楚的是虽然FIFO提供的端口都叫做export,但是其实它们都是IMP。并且使用FIFO就不用再写write函数了。
比较地址和数据是否一致,这里由于我们在tb中例化DUT的时候地址宽度参数设置的是16位,所以这里不检查高16位,但是这里最低两位为什么不检查我还没搞明白。
接着比较读写操作命令、RESP信号以及Protection信号。
最后是hstrb信号、hsize信号和haddr之间的匹配关系,这里也还没弄明白。
2023.06.15更新
不论是APB还是AHB,地址、数据总线的宽度是属于硬件的设计,是固定的32位,不过在不同的传输中可能只用到了一部分位宽。
在AHB的spec中(6.2节)规定了不同位宽使用哪些字节,这来源于一个地址偏移(Address offset)计算公式,不过我们可以将其简单理解为haddr[1:0]的值。根据这个值就知道传输时哪些是有效位了,也就明确了和pstrb的关系。
报告最终比较结果。
4.4.3 功能覆盖率
class func_cov extends uvm_component;
covergroup cg with function sample(ahbl_mst_tran ahb_pkt,apb_slv_tran apb_pkt);
option.per_instance = 1;
haddr:coverpoint ahb_pkt.haddr[15:0] {bins a1 = {16'h0000};
bins a2 = {16'h0001};
bins a3 = {16'h0002};
bins a4 = {16'h0003};
bins a5 = {16'hffff};
bins a6 = {16'hfffe};
bins a7 = {16'hfffd};
bins a8 = {16'hfffc};
bins a9[4] = {[16'h0004:16'hfffb]};
}
hwrite:coverpoint ahb_pkt.hwrite;
hburst:coverpoint ahb_pkt.hburst;
hsize :coverpoint ahb_pkt.hsize;
cross haddr,hwrite,hburst,hsize {
ignore_bins not_care_a3 = binsof(haddr.a3) && binsof(hsize)intersect{2};
ignore_bins not_care_a2_a4 = binsof(haddr)intersect{16'h0001, 16'h0003} && binsof(hsize)intersect{3'b001, 3'b010};
ignore_bins not_care_a6 = binsof(haddr.a6) && binsof(hsize)intersect{2};
ignore_bins not_care_a5_a7 = binsof(haddr)intersect{16'hfffd, 16'hffff} && binsof(hsize)intersect{3'b001, 3'b010};
ignore_bins not_care_hsize = binsof(hsize)intersect{[3:7]};
}
endgroup
function new(string name="func_cov", uvm_component parent);
super.new(name, parent);
cg=new();
cg.set_inst_name({get_full_name,".",name,".cg"});
endfunction
endclass
在仿真过程中发现直接使用默认的交叉仓,会产生一些无效的仓,这会影响覆盖率提升。于是做出下面的修改,忽略掉无效仓。这里忽略的是不满足约束的。
4.4.4 env_pkg
4.5 测试用例
4.5.1 ahbl2apb_base_test
在env中声明、例化master agent、slave agent以及scoreboard(下面紧接着会编写),在connect_phase连接monitor中的analysis port和scoreboard中的uvm_tlm_analysis_export。set_drain_time()是在发送最后一个transaction后再延迟指定的时间后再drop_objection。
import ahbl2apb_pkg::*;
class ahbl2apb_base_test extends uvm_test;
`uvm_component_utils(ahbl2apb_base_test)
ahbl2apb_env env_t;
function new(string name = "ahbl2apb_base_test", uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env_t = ahbl2apb_env::type_id::create("env_t", this);
uvm_config_db#(uvm_active_passive_enum)::set(this, "env_t.ahbl_mst_agt_t", "is_active", UVM_ACTIVE);
endfunction
virtual task run_phase(uvm_phase phase);
phase.phase_done.set_drain_time(this,5us);
endtask
function int num_uvm_errors();
uvm_report_server server;
if(server == null) server = get_report_server(); begin
return server.get_severity_count(UVM_ERROR);
end
endfunction
function void report_phase(uvm_phase phase);
super.report_phase(phase);
if(num_uvm_errors == 0) begin
`uvm_info(get_type_name(),"Simulation PASSED!",UVM_NONE)
end
else begin
`uvm_info(get_type_name(),"SImulation FAILED!",UVM_NONE)
end
endfunction
endclass: ahbl2apb_base_test
4.5.2 ahbl_mst_single_read32
从最简单的case入手,先把验证环境跑通。这个测试是一个无等待状态的32位读操作,用到了ahbl_mst_single_read32_seq和apb_slv_rdy_seq两个sequence。
在MCDF实验里,raise_objection和drop_objection都是只放在base test里的,然后在两者之间定义一个空的虚方法,可以在具体的test case里定制内容。而本实验的base test里没有这两条语句,反而是放在了具体的test case和basic sequence里。
4.5.3 ahbl_mst_single_write32_apb_slv_nrdy
这个测试是一个无等待状态的32位写操作,用到了ahbl_mst_single_write32_seq和apb_slv_nrdy_seq两个sequence,也是一个简单的、基本的测试。与上一个测试相比几乎相同。
4.5.4 ahbl_mst_burst
这是测试burst功能的。
4.5.5 ahbl_mst_burst_apb_slv_slverr
这是测试burst传输中反馈slverr的情况。
4.5.6 ahbl_mst_tight_transfer
这个是后面大量测试所用的test case,核心是run_phase的编写,起初我是仿照MCDF实验里的写法,在uvm_do那边repeat想要的次数,但是测试中发现两个sequence难以实现同步,导致仿真无法停止。
同时还要理解,master和slave的sequence不一样,master那边每次随机化产生的item都有数拍,而slave每一个item都仅仅只有一拍,因此每一笔transaction,都需要一个while循环,通过get_burst_beats()获取本次的拍数,slave需要启动这么多次才行。
4.6 ahbl_mst_seqlib
4.6.1 ahbl_mst_basic_seq
可以看到这里定义了两个task:pre_body()和post_body(),里面也只放了控制语句。目前我认为这种做法没有MCDF实验中的做法更方便,但似乎这种做法代码重用性更高。
4.6.2 ahbl_mst_single_read32_seq
在这里我们只需要做好相应的约束即可,不过`uvm_do_with()里的req有些疑问,这里没有看到req的声明,在MCDF实验中,req是在base virtual sequence里声明过的。
4.6.3 ahbl_mst_single_write32_seq
与上一个相比只修改了读写操作约束。
4.6.4 ahbl_mst_tight_burst_seq
定义其实与上一个相同。
4.7 apb_slv_seqlib
4.7.1 apb_slv_basic_seq
4.7.2 apb_slv_rdy_seq
4.7.3 apb_slv_nrdy_seq
4.7.4 apb_slv_tight_seq
第五章 项目总结
5.1 项目成果
apb_slv_tran是apb_slave_agent内部TLM通信的载体(transaction),具体传输哪些信息要根据spec来决定。PSEL和PENABLE属于外部控制输入,不需要参与内部通信传输,其他的都是需要的。
5.2 面试经验
1、清晰描述整个项目的工作流程,体现出自己的专业性;
2、掌握APB协议,熟悉AHB协议的常用特点;
3、要自信,不要上来就说自己哪哪不会、不懂,面试是要表现自己的能力和优势,应该尽量讲这些;
4、准备一些可以证明自己的材料。比如证明你学习能力强、熟悉SV和UVM等,既然说了自己的优点,那就得证明,并且证明材料必须非常熟悉,否则会拉低面试官的期待;
5、面试时可能会被问及遇到了什么问题,如何解决的,要为每个项目提前准备一两个这样的回答。详细描述问题是什么,如何发现的,尝试了哪些办法,解决的思路是什么;
6、对于验证来说,面试非常注重思路,如何做验证,如何Debug,这远比对于语言的掌握更重要。