媒介
其实很早从前就想对这个话题展开来聊聊,但是对体系结构的理解也仅仅限于《计算机体系结构——量化研究方法》这一书,对底层实现也仅仅范围于做过RISC-V RV32I根本指令子集的CPU筹划。别的实践深度远远不敷以支持我站在系统的角度思量题目!因此怕讲了出现太多错误,被技术老炮们炮轰。
现在之所以敢壮起胆子来谈这个话题有3点原因和1点动机!
1、已往三年时间内读了不少架构类、嵌入式开辟和操作系统类的书籍,或多或少已经对系统有了大致但还是有点朦胧的了解。由于曾经接触过超等demo的CPU筹划,所以对指令集的印象也相对深刻!
2、已往三年内分别体验过Vitis AI DPU(详情可以跳转之前写的Vitis AI DPU摆设的博客,后续有筹划补充对架构的理解,先赊个账)、自费1000+RMB购买的定制化加速器方案、读了NVIDIA开源的NVDLA加速器驱动代码和硬件代码(详情可以跳转之前写的内核态驱动、用户态驱动和架构分析的博客),对上述三个方案的SoC架构有所了解。从系统的角度对各层都有所观察、有所理解。
3、接触过Xilinx sdk开辟后,对怎么引入寄存器和寄存器读写都有些许经验和感悟。
至于动机,手头这个工作结束了,就开始思考如果要做下一个工作,到底是基于指令集开辟还是单纯使用寄存器开辟?不停以来接触最多的是在裸机上开辟寄存器,偶尔也会看到程序通过编译器得到汇编(其实就是01情势的指令集)后灌给主存来驱动硬件。不停觉得指令集开辟模式很酷,就像定义了一个新的果壳宇宙,而我是修建师。因为这样的中二病,所以手痒痒在所不免。
技术做多了,有点冷静下来了。我为什么须要指令集?、指令集酷的背后有什么代价?、天底下没有免费的午餐!。
回答这个题目之前就须要回答这么几个题目!
1、到底什么是指令集?
2、为什么当代CPU须要指令集?然而并非所有的硬件都须要指令集!简朴如自费1000+买的定制化极强的5层网络加速器根本不涉及指令集开辟,复杂如NVDLA也不存在指令集(至少到现在为止我读完NV开源的架构方案、开源的KMD和UMD代码,我都没有发现指令集的存在),那么究竟是为什么当代CPU须要开辟非常有条理的指令集呢?
3、开辟完指令集究竟有什么样的缺点?明明限制了逻辑寄存器的数量,明明通过多bit的比较逻辑根据操作码来确定操作类型,明明须要额外设置取指、译码两个看起来逻辑上很严密(好像没觉得奇怪,5级流水线嘛,教科书上都这么写)的环节但也会带来功耗和面积开销,以上种种我将会结合寄存器读写模式来谈为什么这些在相比之下是弊端。
4、为什么我会提到另一种完全不咋流行的硬件驱动模式(此驱动为drive而非driver)?说的就是你——“寄存器读写”,这个方式在嵌入式开辟非常流行,就是使用硬件筹划好的接口直接去读写寄存器,这一点还得感谢内存映射机制,还得感谢虚拟地址,感谢不完了,总之感谢整个体系结构。
基于以上四个题目,我思(xia)考(xiang)了好久,觉得是时候记录一下本身的想法了!
如果失言了,还请各位轻锤,指堕落误。
如果讲对了,欢迎大佬在评论区拓展!我权且抛砖引玉!
一、到底什么是指令集?
列举RISC指令集体系中的MIPS指令集来说明(先搬运点《超标量处置惩罚器》这本书上的知识):
MIPS指令集类型紧张分为三类。
- 1、和立即数相关的,rs为源寄存器,rd为目的寄存器。
- 2、和跳转相关的,其中26个bit用于立即数。
- 3、和寄存器指令相关的,rs和rt用作源寄存器,rd用作目的寄存器。由于R-Type指令种类繁多,因此需要funct域来进一步划分指令类别,同时sa专门用于移位指令。
复制代码 给若干张表详细解释高op是怎么指定指令类型的!
到这里为止,MIPS指令集的根本表面已经显现了。
那我们还是借助超标量这本书来解释“指令集架构ISA”的概念:
- ISA是规定处理器的外在行为的一系列内容的统称,它包括基本数据类型、指令、寄存器、寻址模式、存储体系、中断、异常与外部IO等内容。
复制代码 换句话说,指令集其实在用有限的32bit指定好了这么几件事:
- 1、让处理器做什么操作?(做什么的物理实现(也就是基于cmos的电路实现)已经搭建好了),这一条由op以及其他必要信息来指定
- 2、给了处理器做该操作的该有的信息,比如去读哪个寄存器,比如有什么样的立即数?
复制代码 第一条信息其实指明了指令集的op与其他须要的field来指定,粗糙地说是“做什么操作”,更加准确地说则是在已有的电路实现中选择哪条数据路径和控制路径来实现某条指令规定的功能,所以说白了是多选一的选择器信号或者某功能的使能信号,只不过在CPU筹划中该信号会在差别流水级中挨个用上。
第二条信息指明了实现该操作须要的数据信息,这些信息的泉源从指令的表现情势上来看要么是寄存器,要么是主存。但注意,不管是寄存器,还是主存,抑或是被掩盖掉看不到的3级Cache或者是磁盘和外存,得益于虚拟存储映射机制(意味着存储被同一)、页面替换、cacheline内的block替换等替换战略(意味着CPU执行程序不会碍于有限的物理内存)和中断非常(意味着能答应内核态、用户态线程的切换,同时答应中断现场的保存,保存所须要的资源都要从寄存器或者主存回到主存的临时空间)等的存在,物理存储在实际CPU运行(意味着这是个动态行为)中会被模糊。数据信息虽然泉源纷繁复杂,但是总之一句话,就是数据信息的泉源在没有操作系统的环境下是可以支持硬件工程师使用寄存器读/写的方式来获取或者赋予。
那么再回答一个题目?第二条信息我指明了是寄存器读/写,那么第一条信息的是不是也可以指明寄存器行为呢?我给一个RTL的verilog代码的模板(以一个简朴的加法器为例)
- module add #(
- parameter WIDTH = 4
- )
- (
- input clk,
- input rst_n,
- input [WIDTH-1:0] datain1,
- input [WIDTH-1:0] datain2,
- input enable_add,
- output [WIDTH :0] dataout
- );
- reg [WIDTH :0] dataout_reg;
- assign dataout = dataout_reg;
- always @(posedge clk or negedge rst_n)
- begin
- if (!rst_n) dataout_reg <= 0;
- else begin
- if (enable_add) dataout_reg <= datain1 + datain2;
- else dataout_reg <= dataout_reg;
- end
- end
- endmodule
复制代码 上述代码中,enable_add就是一个加法功能的使能信号,那我为什么又说这是多选一的选择信号呢?这基于CPU筹划了多功能的考量,给一个粗糙的例子:
- ......
- case (op_signal)
- 6'b001_001: begin
- ......
- operator_sel = <add_selection_signal>; // 赋予加法指令的控制信号
- ......
- 6'b001_???: begin
- ......
- operator_sel = <mul_selection_signal>; // 赋予乘法指令的控制信号
- end
- end
- ......
复制代码 以上给了加法和乘法的例子,加法就是被选出来的指令。
我们再来观察加法器的代码,接口中有这么几个信号:
- module add #(
- parameter WIDTH = 4
- )
- (
- input clk,
- input rst_n,
- input [WIDTH-1:0] datain1,
- input [WIDTH-1:0] datain2,
- input enable_add,
- output [WIDTH :0] dataout
- );
复制代码 clk时钟信号和rst_n复位信号是系统信号,datain1和datain2就是加法的2个加数,dataout就是加法的效果,enable_add就是选中了加法的行为。在verilog中如果须要把这个加法器接入到CPU中,会思量给这datain1、datain2、dataout和enable_add分配地址,对于地址的读写如果从ZYNQ的角度来思量,其实就是sdk中的reg_read和reg_write操作。
别的有没有觉得datain1和datain2就是加法的2个加数,dataout就是加法的效果,enable_add就是选中了加法的行为眼熟?这不就是指令嘛!该有的都有了
- op = &enable_add // 严格表述,&和C中一样,表示取指
- rs1 = &datain1
- rs2 = &datain2
- rd = &dataout
复制代码 那么回到题目本身,什么是指令集?
我给出的答案是携带了操作类型的信息以及执行该操作须要的数据信息,而这些信息中操作类型被明确指定,数据信息除了立刻数以外都是寄存器偏移,它的行为本质上和寄存器读写一致,不过是被高度抽象的!至于为什么须要高度抽象,看下一节。
二、为什么当代CPU须要指令集?
如果对硬件开辟不熟悉的软件工程师或者算法工程师而言,直接对基于RTL的verilog硬件操控寄存器来实现遥望着另有十万八千里距离的程序无疑是十分痛楚的,因为这个工作得把熟悉的基于python或者c的代码实现转换为基于寄存器的代码。
时代进步了,指令集把对控制信号和数据信号的信息做了个极大化的抽象,在RISC中被编码为32bit的宽度。利益在于,一次性给出2个或者3个或者4个寄存器信息,把基于python或者c的代码实现转为基于指令集的代码的工程量砍了至少一半以上,这个工程我们称之为基于指令集的汇编开辟。这个量说大也不大,究竟计科和微电子系的同行们试过直接手撸汇编,大名鼎鼎的雷军先生在金山开辟时用的也是汇编,说小还真不小,究竟用过c或者更高级的python的同志们想再回到汇编时代肯定是不乐意的。
时代接着进步,编译器和Runtime的出现极大化地促进高级编程语言的发展。编译器前端对接高级程序,后端对接指令集。用过c或者更高级的python的同志们可以无所畏惧地开辟软件或者算法是因为有了编译器和runtime这一层隔膜,究竟上也得感谢操作系统将须要的硬件抽象为内核态驱动(KMD,Kernel Mode Driver),而驱动是底层将寄存器行为进一步封装的高级方案。runtime和编译器在我熟悉的NVDLA的方案里面被同一归到用户态驱动(UMD,User Mode Driver)中,而在另一套我熟悉的Vitis AI DPU的方案中分别列举。因此关于UMD的归类各家有各家见解。
我们以上提到的所有,其实本质上是出于用户的考量,使用者怎么方便,计算机系统工作者就怎么思量。究竟使用者越多,整个框架卖得越多,当然得到的反馈也越多,同时促进了生产力的发展。所以,理所应当地,越是底层的,越不容易开源,嗐。
三、开辟完指令集究竟有什么缺点?
第二节我们讨论到基于指令集的工作,提到一个词用户。但究竟上,硬件筹划的指标并非远在天涯的用户,而是PPA (Performance, Power, Area),和针对详细场景而提出的成本、抗辐射、可用性等等。
我们还是从前述的加法操作为例。在CPU中五级流水线——取指、译码、执行、访存、写回中,取指和译码负责从ICache中取回指令和将指令破译。而在我提到的加法操作的verilog筹划中其实只须要思量将数据信息从寄存器中获取到就行,所以就只范围于当前这个操作,寄存器操作可以省略取指、译码的很大一部分工作。执行一条指令所须要的周期数减少、功能部件数量减少,接着功耗减少,在芯片上的占用面积也减少。
对于硬件加速器如是,我举一个Vitis AI DPU的架构:
在这个硬件加速器中还是照着CPU的流水线来开辟,从APU中以100MHz的频率给指令,指令通过Bus进入由时钟生成器生成325MHz高频时钟和100MHz低频时钟的PL,指令进入指令分配器中依次颠末取指、译码和分发,同时从片上Memory中获取须要的数据信息,或者在data miss时从片外Memory中获取数据信息。随后将数据和指令的控制信息传送到矩阵乘加的计算阵列中,将得到的中心效果通过BRAM读写控制器放到片上Memory或者Data Mover(这个应该是FIFO)中,一旦存储不下了,就把数据从片上调到片外(当然这个过程是隐藏在计算流程后面的,减少串行执行带来的耽误以提拔计算性能)。
如果从寄存器读写的角度来说,取指、译码和分发的功能部件可以做极大的简化。但是缺点也是明显的!就是debug Verilog的时候没有那么容易,在sdk开辟的时候也没那么容易!
这个方案还用到了中断,紧张起2方面作用:1、关照CPU,矩阵乘加计算完了,以便于系统发送新的矩阵乘加指令;2、关照CPU,所有有关矩阵乘加的操作都计算完了,为了保证基于CNN的图像分类可以接着执行,该把执行softmax的指令发送一下了,所以作者筹划了2个softmax的固化IP。
雷同基于五级流水的加速方案另有很多,比如Vortex GPGPU(基于RISC-V指令集扩展):
所以其实从硬件开辟的角度来说,全部功能模块都用指令集实现无疑是对PPA的损害,所以要思量到并非所有功能模块都使用指令集开辟。
我个人以为基于寄存器开辟的方案是PPA的大哥!但是如果从基于寄存器开辟的例子NVDLA来说,想接着做二次开辟的难度还是比较大,代码逻辑混乱,不容易理清晰。以下是NVDLA IP架构,从架构内容可以看出不具备指令开辟的特点,此中Configuration interface block用于以硬件方式配置寄存器,随后该模块接着被拉出寄存器在KMD层面抽象。
看完以上几个方案,我们如果从基于ZYNQ+FPGA的模式来思量,很多人可能觉得如果要开辟基于自定义指令集的方案应该怎么做?究竟前面提到和RISC-V指令集相关的GPGPU,这个可以让CPU来辨认是不是host (CPU)或者device (GPU)负责,雷同NVIDIA的方案。那是不是就没法儿搞自定义指令集这一工程了呢?不然!给个方案实现:
1、RTL层面依然使用自定义指令集来完成取指、译码、执行、访存、写回等五个步调的加速器;
2、RTL层面有了这么几个要素:1)取指令的起始地址;2)加速器开始运行的使能信号;
3、在2中提到的2个寄存器可以明显被拉出来(在完成axi类型的IP封装、和ZYNQ搭建SoC以后、经历综合、实现、bitstream导出后会给出寄存器在memory中的偏移地址),使用寄存器的偏移地址可以在Xilinx Vitis sdk中对起始地址处赋予指令的内容,也就是须要手写汇编。
4、至于手写汇编能不能被编译器取代,这得须要专门的编译器工具了。不过对CNN或者Transformer这类矩阵乘占据大多数的,往往开辟数据级并行DLP,因此指令数量在模型规模不大的环境下其实并不会很多。
四、寄存器读写怎么验证?
我以NVDLA为例,这个题目在回答如何在裸机层面和驱动层面去验证,驱动层面的细节太多,请移步我之前写过关于NVDLA的驱动代码解读。以下回答裸机层面:

最后封装的时候是这样的:

NVDLA IP的寄存器被拉到AMBA总线中,注意,给寄存器分配偏移地址是在SoC搭完以后,所以换句话说,没有AMBA总线来做中介是不可能在系统层面举行验证的。
我拿一段代码来说明(下面是axi-lite slave内的一段代码):
- `timescale 1 ns / 1 ps
- module ppv3_preprocess_accelerator_v1_0_S00_AXI #
- (
- // Users to add parameters here
- parameter DATA_WIDTH_AXI = 32,
- parameter PRIOR_DATA_WIDTH = 1024 , // 64prior * int8 * 2 (xc,yc)
- parameter GT_DATA_WIDTH = 256 , // 8gt * int8 * 4 (xleft,xright,yupper,ybottom)
- parameter NUMBER_OF_GT = 8 ,
- parameter NUMBER_OF_PRIOR = 64 ,
- parameter DATA_WIDTH_INT8 = 8 ,
- parameter HPIC = 160 , // 160 or 120
- parameter WPIC = 160 ,
- parameter NUMBER_OF_CLS = 32 , // cls number
- parameter DATA_WIDTH_INT4 = 4 ,
- parameter CLS_DATA_WIDTH = 8192 , // 64prior * int4 * 32(cls)
- parameter CONF_DATA_WIDTH = 512 , // 64prior confidence * int8
- parameter CONF_INT8 = 8 , // int8 confidence
- parameter CONF_THRESHOLD = 8 , // int-itize confidence threshold
- // alignment
- parameter DATA_WIDTH_INT12 = 12 ,
- // uppersum
- parameter DATA_WIDTH_INT16 = 16 ,
- // User parameters ends
- // Do not modify the parameters beyond this line
- // Width of S_AXI data bus
- parameter integer C_S_AXI_DATA_WIDTH = 32,
- // Width of S_AXI address bus
- parameter integer C_S_AXI_ADDR_WIDTH = 6
- )
- (
- // Users to add ports here
- output o_run,
- output [DATA_WIDTH_AXI-2:0] o_num_cnt,
- input i_idle,
- input i_read,
- input i_write,
- input i_done,
- output train_or_test,
- output nms_enable,
- output faster_enable,
- output dsla_enable,
- output yolo_enable,
- input end_signal,
- // User ports ends
- // Do not modify the ports beyond this line
- // Global Clock Signal
- input wire S_AXI_ACLK,
- // Global Reset Signal. This Signal is Active LOW
- input wire S_AXI_ARESETN,
- // Write address (issued by master, acceped by Slave)
- input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_AWADDR,
- // Write channel Protection type. This signal indicates the
- // privilege and security level of the transaction, and whether
- // the transaction is a data access or an instruction access.
- input wire [2 : 0] S_AXI_AWPROT,
- // Write address valid. This signal indicates that the master signaling
- // valid write address and control information.
- input wire S_AXI_AWVALID,
- // Write address ready. This signal indicates that the slave is ready
- // to accept an address and associated control signals.
- output wire S_AXI_AWREADY,
- // Write data (issued by master, acceped by Slave)
- input wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA,
- // Write strobes. This signal indicates which byte lanes hold
- // valid data. There is one write strobe bit for each eight
- // bits of the write data bus.
- input wire [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB,
- // Write valid. This signal indicates that valid write
- // data and strobes are available.
- input wire S_AXI_WVALID,
- // Write ready. This signal indicates that the slave
- // can accept the write data.
- output wire S_AXI_WREADY,
- // Write response. This signal indicates the status
- // of the write transaction.
- output wire [1 : 0] S_AXI_BRESP,
- // Write response valid. This signal indicates that the channel
- // is signaling a valid write response.
- output wire S_AXI_BVALID,
- // Response ready. This signal indicates that the master
- // can accept a write response.
- input wire S_AXI_BREADY,
- // Read address (issued by master, acceped by Slave)
- input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_ARADDR,
- // Protection type. This signal indicates the privilege
- // and security level of the transaction, and whether the
- // transaction is a data access or an instruction access.
- input wire [2 : 0] S_AXI_ARPROT,
- // Read address valid. This signal indicates that the channel
- // is signaling valid read address and control information.
- input wire S_AXI_ARVALID,
- // Read address ready. This signal indicates that the slave is
- // ready to accept an address and associated control signals.
- output wire S_AXI_ARREADY,
- // Read data (issued by slave)
- output wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA,
- // Read response. This signal indicates the status of the
- // read transfer.
- output wire [1 : 0] S_AXI_RRESP,
- // Read valid. This signal indicates that the channel is
- // signaling the required read data.
- output wire S_AXI_RVALID,
- // Read ready. This signal indicates that the master can
- // accept the read data and response information.
- input wire S_AXI_RREADY
- );
- // AXI4LITE signals
- reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_awaddr;
- reg axi_awready;
- reg axi_wready;
- reg [1 : 0] axi_bresp;
- reg axi_bvalid;
- reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_araddr;
- reg axi_arready;
- reg [C_S_AXI_DATA_WIDTH-1 : 0] axi_rdata;
- reg [1 : 0] axi_rresp;
- reg axi_rvalid;
- // Example-specific design signals
- // local parameter for addressing 32 bit / 64 bit C_S_AXI_DATA_WIDTH
- // ADDR_LSB is used for addressing 32/64 bit registers/memories
- // ADDR_LSB = 2 for 32 bits (n downto 2)
- // ADDR_LSB = 3 for 64 bits (n downto 3)
- localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
- localparam integer OPT_MEM_ADDR_BITS = 1;
- //----------------------------------------------
- //-- Signals for user logic register space example
- //------------------------------------------------
- //-- Number of Slave Registers 4
- reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
- reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
- reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
- reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;
- wire slv_reg_rden;
- wire slv_reg_wren;
- reg [C_S_AXI_DATA_WIDTH-1:0] reg_data_out;
- integer byte_index;
- reg aw_en;
- // I/O Connections assignments
- assign S_AXI_AWREADY = axi_awready;
- assign S_AXI_WREADY = axi_wready;
- assign S_AXI_BRESP = axi_bresp;
- assign S_AXI_BVALID = axi_bvalid;
- assign S_AXI_ARREADY = axi_arready;
- assign S_AXI_RDATA = axi_rdata;
- assign S_AXI_RRESP = axi_rresp;
- assign S_AXI_RVALID = axi_rvalid;
- // Implement axi_awready generation
- // axi_awready is asserted for one S_AXI_ACLK clock cycle when both
- // S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_awready is
- // de-asserted when reset is low.
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- axi_awready <= 1'b0;
- aw_en <= 1'b1;
- end
- else
- begin
- if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
- begin
- // slave is ready to accept write address when
- // there is a valid write address and write data
- // on the write address and data bus. This design
- // expects no outstanding transactions.
- axi_awready <= 1'b1;
- aw_en <= 1'b0;
- end
- else if (S_AXI_BREADY && axi_bvalid)
- begin
- aw_en <= 1'b1;
- axi_awready <= 1'b0;
- end
- else
- begin
- axi_awready <= 1'b0;
- end
- end
- end
- // Implement axi_awaddr latching
- // This process is used to latch the address when both
- // S_AXI_AWVALID and S_AXI_WVALID are valid.
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- axi_awaddr <= 0;
- end
- else
- begin
- if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
- begin
- // Write Address latching
- axi_awaddr <= S_AXI_AWADDR;
- end
- end
- end
- // Implement axi_wready generation
- // axi_wready is asserted for one S_AXI_ACLK clock cycle when both
- // S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_wready is
- // de-asserted when reset is low.
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- axi_wready <= 1'b0;
- end
- else
- begin
- if (~axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en )
- begin
- // slave is ready to accept write data when
- // there is a valid write address and write data
- // on the write address and data bus. This design
- // expects no outstanding transactions.
- axi_wready <= 1'b1;
- end
- else
- begin
- axi_wready <= 1'b0;
- end
- end
- end
- // Implement memory mapped register select and write logic generation
- // The write data is accepted and written to memory mapped registers when
- // axi_awready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted. Write strobes are used to
- // select byte enables of slave registers while writing.
- // These registers are cleared when reset (active low) is applied.
- // Slave register write enable is asserted when valid address and data are available
- // and the slave is ready to accept the write address and write data.
- assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- slv_reg0 <= 0;
- slv_reg1 <= 0;
- slv_reg2 <= 0;
- slv_reg3 <= 0;
- end
- else begin
- if (slv_reg_wren)
- begin
- case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
- 2'h0:
- for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
- if ( S_AXI_WSTRB[byte_index] == 1 ) begin
- // Respective byte enables are asserted as per write strobes
- // Slave register 0
- slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
- end
- 2'h1:
- for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
- if ( S_AXI_WSTRB[byte_index] == 1 ) begin
- // Respective byte enables are asserted as per write strobes
- // Slave register 1
- slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
- end
- 2'h2:
- for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
- if ( S_AXI_WSTRB[byte_index] == 1 ) begin
- // Respective byte enables are asserted as per write strobes
- // Slave register 2
- slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
- end
- 2'h3:
- for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
- if ( S_AXI_WSTRB[byte_index] == 1 ) begin
- // Respective byte enables are asserted as per write strobes
- // Slave register 3
- slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
- end
- default : begin
- slv_reg0 <= slv_reg0;
- slv_reg1 <= slv_reg1;
- slv_reg2 <= slv_reg2;
- slv_reg3 <= slv_reg3;
- end
- endcase
- end
- end
- end
- // Implement write response logic generation
- // The write response and response valid signals are asserted by the slave
- // when axi_wready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted.
- // This marks the acceptance of address and indicates the status of
- // write transaction.
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- axi_bvalid <= 0;
- axi_bresp <= 2'b0;
- end
- else
- begin
- if (axi_awready && S_AXI_AWVALID && ~axi_bvalid && axi_wready && S_AXI_WVALID)
- begin
- // indicates a valid write response is available
- axi_bvalid <= 1'b1;
- axi_bresp <= 2'b0; // 'OKAY' response
- end // work error responses in future
- else
- begin
- if (S_AXI_BREADY && axi_bvalid)
- //check if bready is asserted while bvalid is high)
- //(there is a possibility that bready is always asserted high)
- begin
- axi_bvalid <= 1'b0;
- end
- end
- end
- end
- // Implement axi_arready generation
- // axi_arready is asserted for one S_AXI_ACLK clock cycle when
- // S_AXI_ARVALID is asserted. axi_awready is
- // de-asserted when reset (active low) is asserted.
- // The read address is also latched when S_AXI_ARVALID is
- // asserted. axi_araddr is reset to zero on reset assertion.
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- axi_arready <= 1'b0;
- axi_araddr <= 32'b0;
- end
- else
- begin
- if (~axi_arready && S_AXI_ARVALID)
- begin
- // indicates that the slave has acceped the valid read address
- axi_arready <= 1'b1;
- // Read address latching
- axi_araddr <= S_AXI_ARADDR;
- end
- else
- begin
- axi_arready <= 1'b0;
- end
- end
- end
- // Implement axi_arvalid generation
- // axi_rvalid is asserted for one S_AXI_ACLK clock cycle when both
- // S_AXI_ARVALID and axi_arready are asserted. The slave registers
- // data are available on the axi_rdata bus at this instance. The
- // assertion of axi_rvalid marks the validity of read data on the
- // bus and axi_rresp indicates the status of read transaction.axi_rvalid
- // is deasserted on reset (active low). axi_rresp and axi_rdata are
- // cleared to zero on reset (active low).
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- axi_rvalid <= 0;
- axi_rresp <= 0;
- end
- else
- begin
- if (axi_arready && S_AXI_ARVALID && ~axi_rvalid)
- begin
- // Valid read data is available at the read data bus
- axi_rvalid <= 1'b1;
- axi_rresp <= 2'b0; // 'OKAY' response
- end
- else if (axi_rvalid && S_AXI_RREADY)
- begin
- // Read data is accepted by the master
- axi_rvalid <= 1'b0;
- end
- end
- end
- // Implement memory mapped register select and read logic generation
- // Slave register read enable is asserted when valid address is available
- // and the slave is ready to accept the read address.
- assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
- always @(*)
- begin
- // Address decoding for reading registers
- case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
- 2'h0 : reg_data_out <= slv_reg0;
- 2'h1 : reg_data_out <= {{27{1'b0}}, end_signal, i_done, i_idle, i_read, i_write};
- 2'h2 : reg_data_out <= slv_reg2;
- 2'h3 : reg_data_out <= slv_reg3;
- default : reg_data_out <= 777; // To debug
- endcase
- end
- // Output register or memory read data
- always @( posedge S_AXI_ACLK )
- begin
- if ( S_AXI_ARESETN == 1'b0 )
- begin
- axi_rdata <= 0;
- end
- else
- begin
- // When there is a valid read address (S_AXI_ARVALID) with
- // acceptance of read address by the slave (axi_arready),
- // output the read dada
- if (slv_reg_rden)
- begin
- axi_rdata <= reg_data_out; // register read data
- end
- end
- end
- // Add user logic here
- // tick gen o_run
- reg r_run;
- always @(posedge S_AXI_ACLK) begin
- if(!S_AXI_ARESETN) begin // sync reset_n
- r_run <= 1'b0;
- end else begin
- r_run <= slv_reg0[31];
- end
- end
-
- assign o_run = (r_run == 1'b0) && (slv_reg0[DATA_WIDTH_AXI-1] == 1'b1) ; // Posedge 1 tick
- assign o_num_cnt = slv_reg0[DATA_WIDTH_AXI-2:0];
- assign {dsla_enable, yolo_enable, faster_enable, nms_enable, train_or_test} = slv_reg3[4:0];
-
- // wire reset_n = S_AXI_ARESETN;
- // wire clk = S_AXI_ACLK;
- reg r_done; // to keep done status, i_done is a 1 tick.
-
- always @(posedge S_AXI_ACLK) begin
- if(!S_AXI_ARESETN) begin // sync reset_n
- r_done <= 1'b0;
- end else if (i_done) begin
- r_done <= 1'b1;
- end else if (o_run) begin
- r_done <= 1'b0;
- end
- // else. keep status
- end
- /*
- always @(posedge S_AXI_ACLK) begin
- if(!S_AXI_ARESETN) begin // sync reset_n
- slv_reg1 <= 32'b0;
- end else begin
- slv_reg1[0] <= i_idle;
- slv_reg1[1] <= i_read;
- slv_reg1[2] <= i_write;
- slv_reg1[3] <= r_done;
- end
- end
- */
- // User logic ends
- endmodule
复制代码 此中
- ......
- assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
- always @(*)
- begin
- // Address decoding for reading registers
- case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
- 2'h0 : reg_data_out <= slv_reg0;
- 2'h1 : reg_data_out <= {{27{1'b0}}, end_signal, i_done, i_idle, i_read, i_write};
- 2'h2 : reg_data_out <= slv_reg2;
- 2'h3 : reg_data_out <= slv_reg3;
- default : reg_data_out <= 777; // To debug
- endcase
- end
- ......
复制代码 和这段代码
- assign o_run = (r_run == 1'b0) && (slv_reg0[DATA_WIDTH_AXI-1] == 1'b1) ; // Posedge 1 tick
- assign o_num_cnt = slv_reg0[DATA_WIDTH_AXI-2:0];
- assign {dsla_enable, yolo_enable, faster_enable, nms_enable, train_or_test} = slv_reg3[4:0];
复制代码 把一个已经定义好的IP的接口信号怎么关联到axi-lite bus内做了一个样板操作,通过封装为axi-lite类型的IP后和ZYNQ接在一起以后就有了寄存器的偏移地址。
随后的验证在sdk层面可以移步我之前写的sdk代码解读。
总结
简朴谈了谈指令集和寄存器读写怎么驱动硬件,以及指令集开辟的优缺点。详细谈了寄存器读写和指令集开辟的流程与验证流程。其实原来另有一堆细节要讲,比如下一个工作应该怎么思量,but思量到目前某些部件的硬件代码还不熟悉暂时先搁置,最近在赶进度,后期会分析这个题目。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |