前言
其实很早以前就想对这个话题展开来聊聊,但是对体系结构的理解也仅仅限于《计算机体系结构——量化研究方法》这一书,对底层实现也仅仅局限于做过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考虑到目前某些部件的硬件代码还不熟悉暂时先搁置,最近在赶进度,后期会分析这个问题。
1635

被折叠的 条评论
为什么被折叠?



