《UVM实战》学习笔记——第七章 UVM中的寄存器模型1——寄存器模型介绍、前门/后门访问

news2025/4/12 16:24:57

文章目录

  • 前言
  • 一、寄存器模型简介
    • 1.1 带寄存器配置总线的DUT
    • 1.2 参考模型如何读取寄存器的值
    • 1.3 寄存器模型的基本概念
  • 二、简单的寄存器模型
    • 2.1 只有一个寄存器的寄存器模型
    • 2.2 将寄存器模型集成到验证平台
    • 2.3 在验证平台中使用寄存器模型
  • 三、前门访问和后门访问
    • 3.1 前门访问
    • 3.2 后门访问
    • 3.3 前门访问VS后门访问
    • 3.4 前门和后门混合应用的场景
  • 四、常见面试题
    • 4.1 为什么需要寄存器模型


前言

2023.3.8 热
2023.4.23 小雨


一、寄存器模型简介

1.1 带寄存器配置总线的DUT

最简单的DUT:只有一组数据输入输出端口,而没有行为控制口
带寄存器配置总线的DUT:通过总线来配置寄存器,DUT根据寄存器的值来改变其行为

这里的例子是DUT中有一个1bit的寄存器invert,分配地址为16‘h9,如果值为1,DUT在输出时把输入数据取反,如果值为0,直接输出输入的数据。

在这里插入图片描述

1.2 参考模型如何读取寄存器的值

  • 全局事件:在参考模型触发事件,virtual sequence等待事件,启动sequence去采样值(尽量避免使用全局事件)
  • 非全局事件:分别设置object,在object里面去设置事件,再去触发事件,等到这个事件触发就启动sequence去读取寄存器,利用的是config机制
  • 寄存器模型:上面方法都比较麻烦,所以引出了寄存器模型。

UVM寄存器模型的本质就是重新定义了验证平台与DUT的寄存器接口,使验证人员更好地组织及配置寄存器,简化流程、减少工作量。

task my_model::main_phase(uvm_phase phase);
	reg_model.INVERT_REG.read(status, value, UVM_FRONTDOOR);  //一句话来完成读取寄存器的操作
endtask

在这里插入图片描述

  • 任何消耗时间的phase:可以通过寄存器模型以前门或后门的方式来读取寄存器的值(前门访问是需要消耗时间的)

  • 某些不消耗时间的phase:如check_phase,使用后门访问来读取寄存器的值

1.3 寄存器模型的基本概念

一个寄存器一般是32bit位
在这里插入图片描述

uvm_reg_field:寄存器模型中的最小单位,具体存储寄存器数值的变量。针对寄存器功能域来构建的比特位,单个域可能由多个/单一比特位构成

reserved域:表示的是该域包含的比特位暂时保留以作日后功能的扩展使用,无法写入,读出来是复位值

在这里插入图片描述

uvm_reg:与寄存器匹配,内部可以例化和配置多个uvm_reg_field对象,一个寄存器至少包含一个uvm_reg_field

uvm_mem:匹配硬件存储模型

uvm_reg_map:用来指定寄存器列表中各个寄存器的偏移地址访问属性以及对应的总线。当寄存器模型使用前门访问方式来实现读或写操作时,uvm_reg_map就会将地址转换成绝对地址,后启动一个读或写的sequence。并将读或写的结果返回。在每个uvm_reg_block内部,至少有一个(通常也只有一个) uvm_reg_map

uvm_reg_block:比较大的单位,可以容纳多个寄存器(uvm_reg)、存储器(uvm_mem)和存储器列表(uvm_reg_map)。一个寄存器模型中至少包含一个uvm_reg_block

二、简单的寄存器模型

2.1 只有一个寄存器的寄存器模型

(1)为前文提到的invert寄存器创建寄存器模型,从uvm_reg派生出一个类

  • build函数:不同于build_phase,不是自动执行,需要手动调用,例化所有的uvm_reg_field,再调用configure函数进行配置
  • new函数:三个参数,名称,寄存器宽度,覆盖率相关
class reg_invert extedns uvm_reg;
	`uvm_object_utils(reg_invert)
	rand uvm_reg_field reg_data;

	virtual function void build();
		reg_data = uvm_reg_field::type_id::create("reg_data");
		//parent,size,lsb_pos,access,volatile,resetvalue,has_reset,is_rand,
		reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);
	endfunction
	
	function new(input string name = "reg_invert");
		//name size:整个寄存器的宽度,一般和总线宽度相同,不是实际使用的宽度 has_coverage:是否加入覆盖率的支持
		super.new(name, 16, UVM_NO_COVERAGE);
	endfunction
endclass

uvm_reg_field的configure函数的九个参数:

  • 此域的父辈:也就是这个域位于哪个寄存器,此处填this
  • 此域的宽度:这个寄存器宽度为1
  • 此域的最低位在整个寄存器中的位置:从0开始算
  • 此字段的存取方式:有25种,RW的意思是尽量写入,读取时对此域无影响
  • 是否易失,很少使用
  • 上电复位的默认值
  • 是否复位,一般都有复位默认值
  • 是否可以随机化,主要用于对寄存器进行随机写测试,如果是0,则不能随机,是复位值;且这个参数当且仅当第四个参数为RW(读写)、WRC、WRS、WO(只写)、RO(只读)、W1、WO1时才有效
  • 此域是否可以单独存取

(2)定义好这个寄存器后,再从uvm_reg_block派生一个类将其实例化

class reg_model extends uvm_reg_block;
	`uvm_object_utils(reg_model)
	rand reg_invert invert;
	
	virtual function void build();
		default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0); //用来存储不同reg在reg block中的地址
		invert = reg_invert::type_id::create("invert", , get_full_name());
		invert.configure(this, null, "invert");  //配置这个寄存器
		invert.build();  //手动调用,实例化各个域
		default_map.add_reg(invert, 'h9, "RW");  //要加入的寄存器,寄存器地址,寄存器的存取方式
	endfunction

	function new(input string name = "reg_model");
		super.new(name, UVM_NO_COVERAGE);
	endfunction
endclass
  • build函数:例化所有的寄存器。
    一个uvm_reg_block中一定要对应一个uvm_reg_map,系统已经有一个声明好的default_map,只需要在build中将其实例化,通过调用uvm_reg_block的create_map来实现。
    随后实例化invert并调用它的configure函数。再手动调用invert的build函数,实例化invert里面的域。
    最后一步是把此寄存器加入default_map,否则无法进行前门访问。
  • new函数:名字,是否支持覆盖率

uvm_reg_block的create_map函数的五个参数:

  • 名字
  • 基地址
  • 系统总线的宽度,单位为byte
  • 大小端
  • 是否按照byte寻址

uvm_reg的configure函数的三个参数:

  • 此寄存器所在的uvm_reg_block的指针,这里填this
  • reg_file的指针,这里暂时填null
  • 此寄存器的后门访问路径

2.2 将寄存器模型集成到验证平台

寄存器模型的前门访问操作:分为读和写两种

读和写都会通过sequence产生一个uvm_reg_bus_op的变量,里面存储着操作类型和操作地址以及写的数据。通过转换器adapter交给bus_sequencer,随后给bus_driver,由bus_driver来实现最终的前门访问读写操作。

图中的虚线(driver指向adapter):表示并没有实际的transaction的传递

在这里插入图片描述

class my_adapter extends uvm_reg_adapter;
	string tID = get_type_name();
	`uvm_object_utils(my_adapter)  //adapter是object类型
	
	function new(string name = "my_adapter");
		super.new(name);
	endfunction
		
	function uvm_sequence_item reg2bus (const ref uvm_reg_bus_op rw);
		bus_transaction tr;
		tr = new ( "tr" ) ;
		tr.addr = rw.addr;
		tr.bus_op = (rw.kind == UVM_READ) ? BUS_RD : BUS_WR;
		if(tr.bus_op == BUS_WR)
			tr.wr_data = rw.data;
		return tr;
	endfunction : reg2bus
	
	function void bus2reg(uvm_sequence_item bus_item,ref uvm_reg_bus_op rw);
		bus_transaction tr;
		if(!$cast(tr, bus_item))begin  //使父类句柄指向子类对象,才能进行访问
			`uvm_fatal (tID,"Provided bus_item is not of the correct type. Expecting bus_trans actiion")
			return;
		end
		rw.kind = (tr.bus_op == BUS_RD) ? UVM_READ : UVM_WRITE;
		rw.addr = tr.addr ;
		rw.byte_en = 'h3;
		rw.data = (tr.bus_op == BUS_RD) ? tr.rd_data : tr.wr_data;
		rw.status = UVM_IS_OK;
	endfunction : bus2reg
endclass
  • reg2bus函数:将寄存器模型通过sequence发出的uvm_reg_bus_op类型变量转换为bus_sequencer接受的类型
  • bus2reg函数:当检测到总线上有操作时,将收集到的transaction转换为寄存器模型接受的类型,以便寄存器模型去更新相应的寄存器的值

转换器写好后,再base_test里面加入寄存器模型:

class base_test extends uvm_test;
	my_env env;
	my_adapter reg_sqr_adapter;
	my_vsqr v_sqr;
	reg_model rm;

	function void build_phase (uvm_phase phase) ;
		super.build_phase (phase) ;
		env = my_env::type_id::create ("env", this) ;
		v_sqr = my_vsqr::type_id::create ("v_sqr", this ) ;
		rm = reg_model::type_id::create ( "rm", this) ;
		rm.configure ( null, "") ;  //parent block:由于是最顶层的,所以null;后门访问路径
		rm.build ( ) ;  //例化所有的寄存器
		rm.reset ( ) ;  //调用后,所有寄存器的值变成复位值,不调用,则全为0
		rm.lock_model ( );  //调用后不能再加入新的寄存器
		rm.set_hdl_path_root("top_tb.my_dut");  //设置后门访问的绝对路径
		
		reg_sqr_adapter = new ( "reg_sqr_adapter" ); //实例化adapter
		env.p_rm = this.rm;

	endfunction
	
	function void connect_phase (uvm_phase phase);
		super.connect_phase ( phase ) ;
		v_sqr.p_my_sqr = env.i_agt.sqr;
		v_sqr.p_bus_sqr = env.bus_agt.sqr;
		v_sqr.p_rm = this.rm;
		rm.default_map.set_sequencer (env.bus_agt.sqr,reg_sqr_adapter) ; //将sqr和adapter连接起来
		rm.default_map.set_auto_predict (1);  //设置为自动预测状态,意味着reg model中的镜像值时刻和DUT中对应的reg值一样
	endfunction
endclass

寄存器模型的前门访问操作最终都将由uvm_reg_map完成﹐因此在connect _phase中,需要将转换器和bus_sequencer通过set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态。

2.3 在验证平台中使用寄存器模型

可以在sequence和其他component中使用。

以参考模型使用寄存器模型为例,需要在参考模型中有一个指向寄存器模型的指针。

class my_model extneds uvm_component;  //这个model指的是参考模型
	reg_model p_rm;
	...
	
	task my_model::main_phase (uvm_phase phase) ;
		my_transaction tr;
		my_transaction new_tr;
		
		uvm_status_e status;  
		uvm_reg_data_t value;
		
		super.main_phase (phase) ;
		p_rm.invert.read (status, value, UVM_FRONTDOOR);
		while (1) begin
			port.get (tr);
			new_tr = new ( "new_tr");
			new_tr.copy(tr);
			`uvm_info ("my_model""get one transaction,copy and print it:",UVM_LOW)
			new_tr.print ( );
			if (value)
				invert_tr (new_tr) ;
			ap.write (new_tr);
		end
	endtask
endclass

//my_env把p_rm传递给参考模型
	mdl.p_rm = this.p_rm;	

read任务:

  • uvm_status_e:表明读操作是否成功
  • uvm_reg_data_t:读取的数值
  • 读取的方式:前门或者后门

参考模型一般不会写寄存器,因此在virtual sequence里面进行写操作。

class case0_cfg_vseq extends uvm_sequence;
	virtual task body();
		uvm_status_e status;  
		uvm_reg_data_t value;
		
		p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR)	;
	endtask
endclass

寄存器模型对transaction类型没有要求。因此可以在一个发送my_transaction的sequence中使用寄存器模型来对寄存器进行读写操作。

三、前门访问和后门访问

uvm_reg_sequence:继承于uvm_sequence,所以包含之前预定义的宏,还具有寄存器操作的方法

3.1 前门访问

定义:通过寄存器配置总线来对DUT进行读写操作。在这个过程中,仿真时间($time函数得到的时间)是一直往前走的。是消耗仿真时间的。有两者操作方法。

  • uvm_reg::read()/write():传递时需要注意将参数path指定为UVM_FRONTDOOR。uvm_reg::read()/write()方法可传入的参数较多,除了status和value两个参数需要传入,其它参数如果不指定,可采用默认值。
  • uvm_reg_sequence::read_reg()/write_reg():在使用时,也需要将path指定为UVM_FRONTDOOR。

读操作的完整流程:

  • 参考模型调用寄存器模型的读任务
  • 寄存器模型产生sequence,并产生uvm_reg_item:rw
  • 产生driver能够接受的transaction:bus_req=adapter.reg2bus(rw)
  • 把bus_req交给bus_sequencer
  • driver得到bus_req后驱动它得到读取的值,并将读取值放入bus_req中,调用item_done
  • 寄存器模型调用adapter.bus2reg(bus_req, rw)将bus_req中的读取值传递给rw
  • 将rw中的读数据返回参考模型

如果driver一直发送应答而sequence不收集应答,那么将会导致sequencer的应答队列溢出。因此在adapter中设置了provide_responses选项。

provides_responses:将读入的数据写到rsp并且返回sequencer,要进行设置(调用put_response和item_done的时候要返回rsp),否则两个值默认为0

3.2 后门访问

定义:与前门访问相对的操作,它并不通过总线进行读写操作,而是直接通过层次化的引用来改变寄存器的值。

  • 所有后门访问操作都是不消耗仿真时间(即$time打印的时间)而只消耗运行时间的
  • 从广义上来说,所有不通过DUT的总线而对DUT内部的寄存器或者存储器进行存取的操作都是后门访问操作。
  • 通过设置好每个寄存器的路径进行访问的。配置reg的configur函数以及在base_test里面set_hdl_path_root,两者合在一起就是寄存器的绝对路径。

在这里插入图片描述

后门访问有三种操作方法:

  • uvm_reg::read()/write()
  • uvm_reg_sequence::read_reg()/write_reg():在调用该方法时需要注明UVM_BACKDOOR的访问方式
  • uvm_reg::peek()/poke():分别对应了读取寄存器(peek)和修改寄存器(poke)两种操作,而用户无需指定访问方式为UVM_BACKDOOR,因为这两个方法本来就只针对于后门访问。
reg_model.INVERT_REG.peek(status, value, UVM_BACKDOOR);
// 通过后门访问方式读取寄存器的值,不关心DUT的行为,即使寄存器的读写类型是不能读,也可以将值读出来
reg_model.INVERT_REG.poke(status, 16'h1, UVM_BACKDOOR);
//通过后门访问方式写入寄存器的值,不关心DUT的行为,即使寄存器的读写类型是不能写,也可以将值写进去

3.3 前门访问VS后门访问

前门访问后门访问
通过总线访问需要消耗时间,总线访问结束时才能结束前门访问通过UVM DPI关联硬件寄存器信号路径,直接读取或修改硬件,不需要访问时间,零时刻响应
一般读写只能按字word读写(总线为32位),无法直接读写寄存器域直接读写寄存器或寄存器域
正确反映时序关系不受时序控制,可能访问时发送冲突
依靠监控总线来对寄存器模型内容做预测依靠auto prediction方式对寄存器内容做预测
有效捕捉总线错误,进而验证总线访问路径不受总线时序功能影响

3.4 前门和后门混合应用的场景

(1)只能写一次的寄存器:用物理访问的方式去反应硬件的真实情况
(2)先用前门访问去判断物理通路是否正常,遍历所有寄存器,然后再用后门访问去节约时间
(3)寄存器随机设置:考虑日常不可预测的场景,先通过后门访问随机化整个寄存器列表(在一定的随机限制下),随后再通过前门访问来配置寄存器。
(4)解决地址映射到内部错误寄存器的问题:前门写后门读,或者后门写前门读的方式
(5)状态寄存器:有延时,有时外界的激励条件修改会依赖这些状态寄存器。了需要前门和后门来访问寄存器,也需要映射一些重要的信号来反映第一时间的信息。

四、常见面试题

4.1 为什么需要寄存器模型

DUT的寄存器可以对DUT的行为进行配置,一般通过发送sequence对寄存器进行读写操作,但是当寄存器数目较多时,这样使用起来不方便且容易出错,因此使用寄存器模型,内部分别定义了不同的寄存器,直接对寄存器名字进行索引,就可以读写。而且可以修改访问的方式,是前门还是后门。

下面是通过sequence来读写寄存器,当个数较多是比较复杂

`uvm_do_with(m_trans, {m_trans.addr == 16'h9;
                       m_trans.bus_op == BUS_RD;
                      })//对地址为16'h9的reg发起一笔读操作
 
`uvm_do_with(m_trans, {m_trans.addr == 16'h9;
                       m_trans.bus_op == BUS_WR;
                       m_trans.wr_data == 16'h1;
                      })//对地址为16'h9的reg发起一笔写操作,写入1
reg_model.INVERT_REG.read(status, value, UVM_FRONTDOOR); //对名字是INVERT_REG的寄存器执行读操作
reg_model.INVERT_REG.write(status, 16'h1, UVM_FRONTDOOR); //对名字是INVERT_REG的寄存器执行写操作,写1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/456027.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年淮阴工学院五年一贯制专转本应用文写作考试大纲

2023年淮阴工学院五年一贯制专转本应用文写作考试大纲 一、考核对象 本课程的考核对象是五年一贯制高职专转本秘书学专业普通在校生考生。 二、考核目的 通过课堂教学,学生应当能够识记、理解和应用有关应用文写作的基本理论和基本技能。其中,识记指…

TortoiseSVN使用-TortoiseSVN更换或重置登录用户

文章目录 3.4.9 TortoiseSVN更换或重置登录用户 本人其他相关文章链接 3.4.9 TortoiseSVN更换或重置登录用户 1,打开SVN的settings 2,找到Saved Data栏,右侧Authentication data项点击清除按钮clear 3,再次打开SVN,会要…

AgentGPT已成气候

AgentGPT之前也有介绍过,它最主要的功能是在ChatGPT的功能基础上,允许你自己自定义配置部署,根据你给出的命令,它将尝试通过思考,和执行,不用重复的给它发送指令,直接给你汇总好结果。 安装步骤…

牛客网刷题总结

1.利用%符号获取特定位数的数字。 2.强制类型转换 (将float转换为int ) 3.计算有关浮点型数据时,要注意你计算过程中所有的数据都是浮点型 4.0/3.0 ! 4/3 4.通过位操作符实现输出2的倍数(对于位操作符不熟悉的小伙伴可以看看我…

StringBuffer类详解

StringBuffer 定义 1.java.lang.StringBuffer代表可变的字符序列,可以对字符串内容进行增删 2.很多方法与String相同,但StringBuffer是可变长度的。 3.StringBuffer是一个容器。 String和StringBuffer的不同 1.String保存的是字符串常量&#xff0c…

机器学习——用KNN解决非线性回归问题

问:k最近邻分类模型是非线性模型。 答:正确。k最近邻分类模型是非线性模型,因为它的决策边界是由最近邻居点的类别决定的,而最近邻居点的分布通常是不规则的,因此决策边界也就不是线性的。因此,k最近邻分类…

继续【Stable-Diffusion WEBUI】方方面面研究(内容索引)

文章目录 (零)前言(一)绘图(1.1)模型(1.1.1)基础模型(Stable-diffusion模型)(1.1.2)人物模型(LoRA模型) &…

我在公司彻夜撸码,老板天天开X6夜店蹦迪,到头来工资还拖欠

讲道理,我的学历远达不到BAT等名企大厂的要求,去不了好公司我认了,大专毕业的我在找工作的时候发现留给自己的机会并不多,最后去了一家不知名的小公司。入职后才发现这家公司其实就是个外包公司,里面的业务部门和制度相…

【RPA开发】Selenium 实现网页自动化

开发时有时会遇到网页爬取限制的情况,那么此时可以通过 Selenium 来解决这个问题,因为 Selenium 是模拟浏览器执行网页爬取,相比 Request/API 操作更安全,服务器会完全认为是用户在用浏览器进行操作,如此可以实现网页自…

centos7环境下:DolphinScheduler3.1.5简介和伪集群模式安装部署

centos7环境下:DolphinScheduler3.1.5简介和伪集群模式安装部署 DolphinScheduler简介 Apache DolphinScheduler是一个分布式、易扩展的可视化DAG工作流任务调度平台。致力于解决数据处理流程中错综复杂的依赖关系,使调度系统在数据处理流程中开箱即用…

Quartz定时任务

基本介绍 Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个、百个、甚至是好几万个Jobs这样复杂的日程序表,Jobs可以做成标准的Java组件或EJBs Qua…

android framework-ActivityManagerService(AMS)下

一、ActivityThread \frameworks\base\core\java\android\app\ActivityThread.java 1.1、main public static void main(String[] args) {Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");// Install selective syscall interceptionAnd…

java native 方法编写

目录 前言 1、创建 java native 方法 2、创建洞态链接库项目 3、加载 dll 文件 前言 Java 提供了调用 C 或 C 函数的方法,这种方法就是 native 方法,全称 Java Native Interface (JNI) 1、创建 java native 方法 1)新建 java 类文件 …

【CMake】如何使用CMake构建一个工程

1.如何使用CMake构建一个工程 1. 使用 CMakelists.txt 构建工程 一个最简单 CMake 的项目是将某个源文件构建成为可执行文件,使用CMake 构建项目时,你需要创建一个 CMakeLists.txt 文件,通常情况下,下面三条命令在每个 CMakeLis…

四结4.20

这俩天实现了分离客服端和服务端, 先将对象序列化,通过socket从客户端发送到服务端,反序列化 进行数据库操作,最后向客户端返回结果,渲染javafx的画面 中途遇到俩种异常,因为一报错弹出一大段红色的英语…

力扣sql中等篇练习(九)

力扣sql中等篇练习(九) 1 电影评分 1.1 题目内容 1.1.1 基本题目信息1 1.1.2 基本题目信息2 1.1.3 示例输入输出 a 示例输入 b 示例输出 1.2 示例sql语句 # 只要一个 可以考虑order by加上 limit的组SELECT子句中子查询的结果 # 电影名称和姓名不可能重复,所以直接Union a…

【RecyclerView】同时刷新和滚动导致,滚动位置异常(一)

前置: 被选中item高度与非选中item高度不一致,且硬件有点卡,运行会有一定卡顿。 可视界面的item为三个,总数据为十个。 期望效果: 》 实际上效果: 代码: mListAdapter.setSelectedPosition(…

学成在线笔记+踩坑(6)——【媒资模块】视频处理。FFmpeg+XXL-JOB

导航: 【黑马Java笔记踩坑汇总】JavaSEJavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线牛客面试题 目录 1 视频转码需求 1.1 视频编码格式和文件格式 1.2 windows使用编码工具FFmpeg 1.3 视频处理工具类 1.3.1 拼装FFmpeg命令的各工具类 1…

复旦MOSS大模型开源了「中国版ChatGPT」,Github和Hugging Face同时上线

最近,ChatGPT非常火,从ChatGPT3到ChatGPT4,都非常火。无论是否为互联网行业的,多少都听到过关于ChatGPT的消息。虽然百度、阿里等互联网巨头都已经宣布将会推出相关的类ChatGPT产品。但目前还未有成型的产品上线。 而昨日&#x…

94. 二叉树的中序遍历【119】

难度等级:容易 上一篇算法: 102. 二叉树的层序遍历【206】 力扣此题地址: 94. 二叉树的中序遍历 - 力扣(Leetcode) 1.题目:94. 二叉树的中序遍历 给定一个二叉树的根节点 root ,返回 它的 中序…