走出死胡同——由i2c_reg_adapter引发的思考 目录前言遵循传统惯例的思路找到现成的adapter的惊喜与失落重写虚函数越陷越深QA可以在reg2bus里将第一次的I2C_WRITE和第二次的I2C_READ融合到一个i2c_trans里吗QA可以在reg2bus()函数中返回两个trans吗QA为什么apb总线可以I2C总线不行走进了死胡同寄存器模型操作总线只有adapter这一条路吗打开新世界uvm_reg_frontdoor是什么任务的核心frontdoor的body()任务如何集成frontdoor后记前言寄存器配置是IC验证中绕不开的场景而通过挂载寄存器模型操作总线进行寄存器配置是验证环境中非常常用的一个手段。在绝大多数项目中使用APB总线配置寄存器最为常见使用寄存器模型的通用做法是通过svt_apb_vip的adapter将寄存器模型事务转换成apb_master_transaction类型然后通过vip的sequencer发送给driver进而操作APB总线。最近项目中用到一个i2c2axi的配置接口。整个芯片的寄存器配置软件可以通过I2C串口来进行下发。在以往的验证中并没有通过寄存器模型来操纵I2C总线而是封装了一些task通过RegWriteT(addr, data)来发送寄存器配置信息。不使用寄存器模型当寄存器的offsett发生改变下发的寄存器配置也要相应改变。对于寄存器的管理就不是那么方便。于是在这个项目中决定使用寄存器模型和svt_i2c_vip来操纵I2C总线。遵循传统惯例的思路在验证环境中使用寄存器模型操作I2C总线一个自然而然的想法是通过i2c_adapter将寄存器模型事务和svt_i2c_master_transaction事务进行转换。于是为了避免重复造轮子先去svt_i2c_vip的源码库里找一下有没有现成的svt_i2c_reg_adapter。还真有找到现成的adapter的惊喜与失落因为之前的环境apb的adapter是已经集成好的自己还从未写过adapter一直想借着i2c这个机会练一下手结果i2c vip里自带了这个还是有点失落不过也无所谓了。现成的轮子用起来。打开svt_i2c_reg_adapter一看发现写的有很大问题。在$DESIGNWARE/vip/svt/i2c_svt/U-2023.06/i2c_master_agent_svt/sverilog/src/vcs/目录下有两个文件以上是svt_i2c_reg_adapter的源码这里最大的问题是在reg2bus函数里将rw.addr赋值给了i2c_reg_trans.addr。这里犯了一个致命错误把寄存器地址和I2C slave设备地址混为一谈。在I2C协议中一笔完整的写协议格式如下START条件 7bit设备地址读写标志位应答位 8bit数据应答位 8bit数据应答位而i2c_reg_trans.addr表示的是要发送的目标slave设备地址。要下发的寄存器地址和数据信息应该全部存放在i2c_reg_trans.data[]数组中每8个bit为一个元素。正常32位寄存器访问data数组的size应该设为8.在发送完设备地址并应答后连续发送4个8bit作为寄存器地址再连续发送4个8bit作为要写入这个寄存器地址的数据。可以得出结论svt_i2c_vip自带的这个adapter完全是错的根本不可用。而且在svt_i2c_vip中这个类并没有在任何地方使用。只能自力更生由于reg2bus和bus2reg是在基类uvm_reg_adapter中声明的虚函数。所以我们可以写一个子类Csub_i2c_reg_adapter继承于svt_i2c_reg_adapter并重写这两个函数。重写虚函数越陷越深写一个子类重写虚函数的目的是因为知道原来的是错的并且这个也不难很好改于是很快就完成了寄存器写的reg2bus部分。当写到寄存器读的时候逐渐感觉到有些不对劲了。寄存器读和写不同读是一个双向的过程master首先要发送访问的寄存器地址这通过一笔I2C_WRITE的trans来完成然后master要发送I2C_READ的命令使得slave返回寄存器的数据。会发现调用一次uvm_reg.read()需要执行两次I2C trans的发送。调用uvm_reg.read()的流程如下uvm_reg.read() │ ▼ uvm_reg.do_read() │ ▼ uvm_reg.local_map.do_read() │ ▼ reg_map.do_bus_read() │ ├──► adapter.reg2bus() ─────────► [转换生成 bus_req] │ │ │ ▼ │ start_item(bus_req) │ │ │ ▼ │ finish_item(bus_req) ──► [发送读地址指令] │ │ │ ▼ │ [等待总线响应] │ │ ├◄── adapter.bus2reg(bus_rsp) ◄── get_response(bus_rsp) ◄────────┘ │ ▼ [更新数据到寄存器模型]事情的矛盾点在于调用一次uvm_reg.read()只会引发一次adapte.reg2bus()进行事务转换reg2bus()只能返回一个i2c_reg_trans。也就是在上述框架下一次寄存器读只能引发一次VIP的trans发送。这在I2C总线上是无法实现的。QA可以在reg2bus里将第一次的I2C_WRITE和第二次的I2C_READ融合到一个i2c_trans里吗不可以svt_i2c_vip不支持一个trans包含两个方向的指令不支持combined_trans。I2C_WRITE和I2C_READ必须分成两个trans发送。QA可以在reg2bus()函数中返回两个trans吗不可以在基类uvm_reg_adapter中reg2bus()函数只能返回一个句柄不能返回两个而且在返回后外侧接收函数返回值的也只有一个句柄。这些代码是uvm中写死的。即一次reg2bus()只能将寄存器事务转换为1个vip transQA为什么apb总线可以I2C总线不行对于apb总线一次寄存器读操作只对应一次apb trans的发送。apb发送读trans时地址通过apb_master的paddr发送给slaveapb_slave的数据通过prdata返回给apb_master。完美符合上述结构。而I2C总线是串行线数据只在一根线上不可能既发送地址又接收数据。i2c_master只能先发送I2C_WRITE发出寄存器地址。然后通过I2C_READ来接收数据。而这个在svt_i2c_vip中必须对应两次trans的发送。走进了死胡同事情到这里 已经走进了死胡同被限制死的原因在于使用adapter这套机制do_bus_read这个函数建立的是一次reg.read()调用只触发一次vip trans的转换和发送。这是已经写死的代码。无法改变。核心解决思路如何调用一次reg.read()时能触发两次trans的发送寄存器模型操作总线只有adapter这一条路吗回顾上面的框架图发现函数的嵌套调用一切都走的太直了仔细梳理整个函数的调用链条会发现这只是其中的一条路。只有当没有设置uvm_reg_frontdoor的时候才会走下面的分支通过adapter这套框架来进行事务转换。而上面的分支此前还从未注意到过。 但是一看就非常熟悉这是一次经典的sequence.start(sequencer)sequence启动。这就是一个思维惯性的问题也或许是遇到的太少以前以为要想通过寄存器模型操作总线就必须通过adapter来完成。但是这条路并不是万能的。遇到上述I2C这种情况其他串行总线应该也会是类似无解adapter这条路根本走不通。打开新世界uvm_reg_frontdoor是什么上面的分支值得仔细研究。这段代码的功能是检查map_info中是否有frontdoor实例如果有则拿到这个实例并将寄存器模型事务的句柄rw赋值给frontdoor.rw_info. 然后拿到system_map中的sequencer将fd发送出去。所以uvm_reg_frontdoor并不是什么特别的类它就是一个uvm_sequence。而在这个sequence的body()任务中我们可以发送任意个trans。再也不用局限于adapter框架下一次只能发送一个事务。uvm_reg.read() │ ▼ uvm_reg.do_read() │ ▼ [检查 map_info 中的 frontdoor 是否为空] │ ├─► (不为空) ──► frontdoor.start(sequencer) │ └─► (为空) ──► [执行默认的 do_read 逻辑 / do_bus_read]在新路径下完全不再使用adapter核心是在frontdoor的body()任务中处理rw_info于i2c_reg_trans的转换。并发送trans。任务的核心frontdoor的body()任务由此对于I2C等其他串行总线如果要挂载寄存器模型就不是考虑如何开发一个adapter了而是要从uvm_reg_frontdoor继承一个子类并完善其body()任务。最后将frontdoor在env中例化然后添加到每一个reg上。这是最早版本的i2c_reg_frontdoor代码存在很大的问题已经弃用class Cmt_i2c_reg_frontdoor extends uvm_reg_frontdoor; // uvm_sequence uvm_object_utils(Cev_i2c_reg_frontdoor) bit [9:0] target_slave_addr; function new(string name Cev_i2c_reg_frontdoor); super.new(name); uvm_info(get_name(), Constructed, UVM_LOW); endfunction virtual function void set_target_slave_addr(bit [9:0] addr); target_slave_addr addr; endfunction virtual task body(); svt_i2c_uvm_pkg::svt_i2c_master_transaction i2c_write_trans; svt_i2c_uvm_pkg::svt_i2c_master_transaction i2c_read_trans; if (rw_info.kind UVM_READ) begin //---------------------------------------------------- // 第一步发送 I2C WRITE写入 32 位寄存器地址 //---------------------------------------------------- uvm_create(i2c_write_trans) i2c_write_trans.addr target_slave_addr; i2c_write_trans.cmd I2C_WRITE; i2c_write_trans.sr_or_p_gen 1; // gen_restart condition i2c_write_trans.data new[4]; for (int i 0; i 4; i) begin i2c_write_trans.data[i] rw_info.offset[(i * 8) : 8]; end start_item(i2c_write_trans); finish_item(i2c_write_trans); //---------------------------------------------------- // 第二步发送 I2C READ读取 4 字节数据 //---------------------------------------------------- uvm_create(i2c_read_trans) i2c_read_trans.addr target_slave_addr; i2c_read_trans.cmd I2C_READ; i2c_write_trans.sr_or_p_gen 0; // gen_stop condition i2c_read_trans.data new[4]; // 预分配接收空间 start_item(i2c_read_trans); finish_item(i2c_read_trans); rw_info.value[0] {i2c_read_trans.data[0], i2c_read_trans.data[1], i2c_read_trans.data[2], i2c_read_trans.data[3]}; rw_info.status UVM_IS_OK; end else if (rw_info.kind UVM_WRITE) begin uvm_create(i2c_write_trans) i2c_write_trans.addr target_slave_addr; i2c_write_trans.cmd I2C_WRITE; i2c_write_trans.sr_or_p_gen 1; // gen_restart condition i2c_write_trans.data new[8]; for (int i 0; i 4; i) begin i2c_write_trans.data[i] rw_info.offset[(i * 8) : 8]; end for (int j 0; j 4; j) begin i2c_write_trans.data[j4] rw_info.value[0][(j * 8) : 8]; end start_item(i2c_write_trans); finish_item(i2c_write_trans); rw_info.status UVM_IS_OK; end endtask endclass修改过的i2c_reg_frontdoor的代码如下ifndef MT_I2C_REG_FRONTDOOR__SV define MT_I2C_REG_FRONTDOOR__SV class Cmt_i2c_reg_frontdoor extends uvm_reg_frontdoor; // uvm_sequence#(uvm_reg_item) import svt_i2c_uvm_pkg::*; uvm_object_utils(Cmt_i2c_reg_frontdoor) static semaphore m_semaphore; // to Prevent concurrent execution of the same sequence bit [9:0] target_slave_addr; uvm_reg_map_info map_info; uvm_reg m_reg; uvm_reg_addr_t m_reg_addr; uvm_declare_p_sequencer(svt_i2c_master_transaction_sequencer) svt_i2c_configuration i2c_cfg; function new(string name Cmt_i2c_reg_frontdoor); super.new(name); m_semaphore new(1); uvm_info(get_name(), Constructed, UVM_LOW); endfunction virtual function void set_target_slave_addr(bit [9:0] addr); target_slave_addr addr; endfunction virtual task body(); svt_i2c_uvm_pkg::svt_i2c_master_transaction i2c_write_trans; svt_i2c_uvm_pkg::svt_i2c_master_transaction i2c_read_trans; uvm_sequence_item seq_item; svt_i2c_uvm_pkg::svt_i2c_master_transaction i2c_rsp; svt_uvm_pkg::svt_configuration cfg; m_semaphore.get(1); //p_sequencer.lock(this); // this sequence lock sequencer, other sequence wait p_sequencer.get_cfg(cfg); /** Cast the SVT configuration handle on the local I2C configuration handle */ if (!$cast(i2c_cfg, cfg)) begin uvm_fatal(body, Unable to cast the configuration to a svt_i2c_configuration class); end $cast(m_reg, rw_info.element); map_info rw_info.local_map.get_reg_map_info(m_reg); m_reg_addr map_info.addr[0]; if (rw_info.kind UVM_READ) begin // ------------------------------------------------------------ // 第一步发送 I2C WRITE写入 32 位寄存器地址 // ------------------------------------------------------------ uvm_create(i2c_write_trans) i2c_write_trans.addr target_slave_addr; i2c_write_trans.cmd I2C_WRITE; i2c_write_trans.sr_or_p_gen 0; // not gen_restart condition i2c_write_trans.data new[5]; i2c_write_trans.data[0] 8h12; //$display(rw_info name is %s, rw_info.element.get_name()); //$display(reg addr is %0x, m_reg_addr); for (int i 0; i 4; i) begin i2c_write_trans.data[i1] m_reg_addr[(i * 8) : 8]; //$display(write trans data is 0x%0x, i2c_write_trans.data[i1]); end start_item(i2c_write_trans); finish_item(i2c_write_trans); if(i2c_cfg.enable_put_response 1) begin get_base_response(seq_item); $cast(i2c_rsp, seq_item); end // ------------------------------------------------------------ // 第二步发送 I2C READ读取 4 字节数据 // ------------------------------------------------------------ uvm_create(i2c_read_trans) i2c_read_trans.addr target_slave_addr; i2c_read_trans.cmd I2C_READ; i2c_read_trans.sr_or_p_gen 0; // gen_stop condition i2c_read_trans.data new[4]; // 预分配接收空间 start_item(i2c_read_trans); finish_item(i2c_read_trans); if(i2c_cfg.enable_put_response 1) begin get_base_response(seq_item); $cast(i2c_rsp, seq_item); end rw_info.value[0] {i2c_rsp.data[3], i2c_rsp.data[2], i2c_rsp.data[1], i2c_rsp.data[0]}; // I2C first send low byte on SDA lien and vip store on data[0] //$display(rw_info value[0] is 0x%x, rw_info.value[0]); rw_info.status UVM_IS_OK; end else if (rw_info.kind UVM_WRITE) begin uvm_create(i2c_write_trans) i2c_write_trans.addr target_slave_addr; i2c_write_trans.cmd I2C_WRITE; i2c_write_trans.sr_or_p_gen 0; // gen_restart condition i2c_write_trans.data new[9]; i2c_write_trans.data[0] 8h12; for (int i 0; i 4; i) begin i2c_write_trans.data[i1] m_reg_addr[(i * 8) : 8]; end for (int j 0; j 4; j) begin i2c_write_trans.data[j14] rw_info.value[0][(j * 8) : 8]; end start_item(i2c_write_trans); finish_item(i2c_write_trans); if(i2c_cfg.enable_put_response 1) begin get_base_response(seq_item); $cast(i2c_rsp, seq_item); end rw_info.status UVM_IS_OK; end //p_sequencer.unlock(this); m_semaphore.put(1); endtask endclass endif代码的修订是因为第一版在仿真调试过程中遇到了非常多的问题针对代码的修改过程将会在下文中详细描述如何集成frontdoor错误的旧版本virtual function void SetBlockFrontdoorF(uvm_reg_block m_block); Cmt_i2c_reg_frontdoor i2c_reg_frontdoor; uvm_reg_block sub_blocks[$]; i2c_reg_frontdoor Cmt_i2c_reg_frontdoor::type_id::create(i2c_reg_frontdoor); i2c_reg_frontdoor.set_target_slave_addr(h33); m_block.get_blocks(sub_blocks); foreach (sub_blocks[i]) begin uvm_reg sub_regs[$]; sub_blocks[i].get_registers(sub_regs); foreach(sub_regs[k]) begin sub_regs[k].set_frontdoor(i2c_reg_frontdoor); end end endfunction修订后的新版本virtual function void SetBlockFrontdoorF(uvm_reg_block m_block); Pi2c_vip_pkg::Cev_i2c_reg_frontdoor i2c_reg_frontdoor; uvm_reg_block sub_blocks[$]; m_block.get_blocks(sub_blocks); foreach (sub_blocks[i]) begin uvm_reg sub_regs[$]; sub_blocks[i].get_registers(sub_regs); foreach(sub_regs[k]) begin i2c_reg_frontdoor Cev_i2c_reg_frontdoor::type_id::create($sformatf(i2c_reg_frontdoor_%s, sub_regs[k].get_name()) ); i2c_reg_frontdoor.set_target_slave_addr(h56); sub_regs[k].set_frontdoor(i2c_reg_frontdoor); end end endfunction这里使用的方法是遍历uvm_reg_block下所有的uvm_reg并给每个reg执行set_frontdoor()。目前没有发现在uvm_reg_map中统一设定的方法。由此当case中调用某个reg.read()时将会按照如下流程执行。不再走adapter这条路了。uvm_reg.read() │ ▼ uvm_reg.do_read() │ ▼ [检查 map_info 中的 frontdoor 是否为空] │ ├─► (不为空) ──► frontdoor.start(sequencer) │ └─► (为空) ──► [执行默认的 do_read 逻辑 / do_bus_read]后记最早的时候觉得寄存器模型的核心就是adapter这是操作总线的关键所以视野一直局限在这条路上还自以为掌握了adapter就是掌握了寄存器模型的关键。但现在发现写uvm_reg这部分代码的人绝对是个大神这部分算是uvm中比较独立的一部分但是处理的却非常细致精妙。这次遇到了I2C这个问题一度进入了死胡同觉得i2c没有办法实现adapter。甚至想过放弃使用寄存器模型操作I2C总线。而我在历史的项目文档中发现其他项目组也遇到了同样的问题最终他们放弃了使用寄存器模型的做法。庆幸自己在这关键的时刻死磕了一下。在这个函数中钻的太深发现是一条看不到希望的死路。而退出这个函数向后看发现还有多个分支。这条路不是唯一的一条路。人生也是一样难免会走进死胡同有时候退一步真的是海阔天空。