IC note

好好学一学吧!

RISCV基础

  • FGPA(可编程逻辑阵列),ASIC(专用集成电路)
  • 设计ISA的七种衡量标准
    • 成本。处理器是通过集成电路实现的芯片,保持ISA的简洁性可以缩小处理器的尺寸,这样可以减少成本
    • 简洁性。能够缩短芯片设计和验证的时间,降低文档成本
    • 性能。CPU性能方程
    • 架构和具体实现的分离。MIPS的延迟槽设计
    • 提升空间。ISA必须保留操作码的空间(以便添加指令)。ARM32想使用16位指令来减少代码长度,但没有操作码的空间了,因此16位指令创建了isa(thumb1),16和32位来组成另外一个isa(thumb2),然后使用一个模式位在两种长度的指令之间切换。
    • 程序大小。程序越小(跟指令长度也相关),存储它所需的芯片面积就越小;还能减少指令cache未命中的次数(访问DRAM需要的时间比访问SRAM需要的时间更久)。
    • 易于编程/编译/链接。编译器对寄存器的分配;若ISA支持PIC(位置无关代码),即可加载而无需重定位的代码,就可以支持动态链接
  • 宏观融合。高端处理器通过将简单的指令组合在一起从而提高性能,从而减少更复杂的ISA给底端实现带来的负担
  • RV32I指令
    • 六种基本指令
      • R。寄存器-寄存器风格的算数逻辑运算指令
      • I。用于短立即数和访问load操作
      • S。用于访存store操作
      • B。用于条件跳转
      • U。用于长立即数
      • J。用于无条件跳转
    • 所有指令都是32-bit长,简化了指令译码
    • 指令编码格式中提供三个寄存器操作数字段
    • 为减少译码逻辑,指令编码格式中将读/写寄存器都放在了固定字段,因此可以在译码前就进行
    • 立即数字段总是符号拓展,可以在译码前就进行
    • 指令编码格式
    • B指令和J指令的地址必须向左移动一位(相当于乘以2),拥有更大的跳转范围
    • 乱序执行处理器(关键是寄存器重命名,将汇编程序中的寄存器映射到物理寄存器)
    • ABI(应用程序二进制接口)
    • risc-v没有条件码,可以通过slt(set on less than)指令来比较两个寄存器的大小,然后再将0或1写入具体的寄存器中,通过做一些逻辑运算即可判断出两个寄存器值的大小了(也有立即数版本的slti)
    • lui指令,和auipc主要是因为riscv的指令编码长度为32-bit(包括操作码等),存放不下32位的立即数,因此只能以逻辑的方式操作高20-bit到寄存器里,然后I指令的高12位刚好为数据的低12位,再使用ori搭配lui就可以将32位值加载到一个寄存器里了(需要两条指令)。auipc和jalr的搭配也可以实现用组合成完整的32位值来修改pc。
    • 与MIPS不同,不支持(delay load),即load指令在两个指令执行后才可用,与延迟分支的设计类似
    • 条件码使指令设置的状态,会使得乱序执行的依赖计算复杂化,因此risc-v没有条件码
    • 使用sltu实现32位数的加法,sltu可以通过加的结果和其中一个操作数进行一次无符号比较。如果出现溢出,那么加的结果就会比其中一个操作数要小,这时候stlu可以将其中一个进位位保存到其中一个操作数的寄存器里,然后在高32位相加的时候加上这个进位位
    • 获取当前的pc可以通过使用auipc,立即数字段设为全0,然后auipc会将结果写到指定的寄存器里,这时候就能获取到PC的值了
    • x0寄存器硬连线为0
    • 特性
      • 32位主存空间,8位寻址能力
      • 所有指令都为32位长
      • 31个寄存器,全部32位宽,寄存器0硬连线为0
      • 所有操作都在寄存器之间(寄存器-寄存器风格)
      • 使用存储器映射I/O
      • 立即数总是符号拓展
      • 只提供一种数据寻址模式(寄存器+立即数)和PC相对寻址
      • 无乘法或除法指令
      • 加载32位常量需要两条指令lui和ori
    • 软件检测加法溢出
      • 在无符号加法指令addu后可以加上一个bltu(如果条件满足则会跳转到一个label中)来对相加的结果和其中一个操作数进行比较,如果和小于操作数,说明发生了溢出。
      • 有符号加法指令后,可以加上两条指令,如果其中一个操作数为负数,那么和才能小于另一个操作数(无论当前操作数是正还是负),否则就发生了溢出(比如两个正数相加结果小于0)
    • jalr跳转并链接(链接返回地址)指令,将返回地址(PC+4)放到ra中可以实现过程调用,若使用x0寄存器替换ra,则可以实现无条件跳转
    • 注意到jal的立即数部分非常混乱,看过immgen的实现以后才发现指令的一部分是可以复用的,减少译码逻辑
  • 寻址模式。指的是操作数的来源(寄存器,立即数,指令编码中)
    • lc-3支持:数据寻址方式(寄存器,立即数),存储器寻址方式(PC相对寻址,间接寻址,基址+偏移量寻址),为寄存器-寄存器风格(也成为load/store)风格的ISA

超标量处理器设计相关

  • ISA是软件与硬件之间的接口
  • 摩尔定律。处理器性能每18个月翻一番
  • RTL,寄存器传输语言
  • 超标量处理器使得IPC大于1。n-way超标量处理器指的每个时钟周期执行n条指令
  • VLIW(Very Long Instruction Word)超长指令字处理器架构,是依靠编译器和程序员决定调度哪些指令并行执行,而超标量处理器是靠硬件自身决定哪些指令可以并行执行
  • 面向PC端处理器设计性能放在第一位,移动端处理器设计功耗放在第一位
  • 使用流水后周期时间变为D/n+S,S为流水线寄存器的延迟,D为没有流水所需的周期时间,n为流水线的级数;硬件面积变为G+n*L,G为没有流水所消耗的硬件面积,L为流水线寄存器附带的控制逻辑所消耗的硬件面积
  • 最长的流水段所需要的时间决定了整个处理器的周期时间
  • 五级流水线
    • IF。使用PC作为地址,从I-Cache中取出指令,并将指令存储在指令译码器中
    • ID。根据指令译码的结果找到对应操作的微状态号,当前微指令中有对寄存器组的控制信号(可以不需要通过总线直接传送到ALU),进行运算之后再写回寄存器组中
    • EX。执行算数运算指令或对访问存储器的地址进行计算
    • MEM。ld/st指令会访问D-Cache,其他指令在这个stage不会做任何事情
    • RB
  • 对流水线中各个stage进行平衡处理
    • 合并stage。这不符合高性能处理器对高时钟频率的要求
    • 细分stage。较深的流水线会导致硬件消耗的增大(需要更多的流水线寄存器和控制逻辑),同时分支预测失败的惩罚也增大了
  • 超标量处理器的指令相关性
    • 数据相关性:RAW先写后读(可以通过转发来解决), WAR先读后写, WAW先写后写
      • ld/st指令比较难判别,通常需要带上地址判别相关性
    • 控制相关性:分支指令到跳转发生时与后面跟着的指令存在控制相关
  • 超标量处理器执行指令的方式
    • Frontend阶段(也就是取指令和译码阶段),这两个阶段很难实现乱序执行(实际上也没有意义)。
    • 在Issue阶段(也就是ex和mem阶段)乱序执行,issue表示将指令送到(Function Unit, FU)中执行,只要指令的源操作数准备好了,就可以将其先于其他指令执行
    • 在Write back阶段处理器内部使用寄存器重命名,将逻辑寄存器(Architecture Register File)映射到物理寄存器(Physical Register file)中,从而实现乱序方式的写回寄存器。寄存器重命名通常放在一个流水段里(时间比较长)。
    • Commit阶段表示一条指令允许更改处理器的状态(如D-Cache与SB相关的更新),为了保证实现精确异常然后retire,这个阶段需要顺序执行。
  • 顺序执行
    • 为了保证wb阶段是顺序执行的,所有的FU都需要经历同样周期数的流水线(即流水段的个数要一致),这样就会出现有的FU已经完成相关功能了,但还得执行几个什么都不做的stage。写回阶段统一了,因此WAW和WAR都不会对顺序执行的流水线产生影响
  • 乱序执行
    • 发射队列(Issue Queue, IQ),每个FU都有一个IQ,仲裁(select)电路会从这个IQ部件中挑选出合适的指令送到FU执行。乱序的仲裁电路会很复杂,顺序发射只需要判断IQ中最旧(即队列最前端)那条指令是否就绪就可。IQ中还存在唤醒电路,需要和仲裁电路相互配合进行工作
    • 每个FU都有自己的流水线级数(不像顺序执行那样都一致)
    • 重排序缓存(ReOrder Buffer, ROB),流水线中所有指令按照程序中规定的顺序存储在重排序缓存中。使用ROB来实现程序对处理器状态的顺序更新,这个阶段也称作Commit阶段。ROB会配合异常的处理,如果不存在异常这条指令就可以离开流水线了(也成为retire),指令一旦retire就不可能再回到之前的状态了
    • Store Buffer(SB),指令没有retire之前,结果会被写到SB中(防止store结果到主存中之后发生异常,但这时候主存里的数据没办法恢复的情况)。只有指令真正的retire的时候才会将SB中的值写到存储器中。
      • 如果在store指令执行后执行load这时候就会出现数据相关,因为加了一个commit stage这时候数据并未写入存储器中,而是在SB中,此时还需要从SB中找数据(增加旁路)
  • 恢复电路&预测技术
  • 经典九级流水线
    • Fetch。从I-Cache中取指令。主要有两大部件构成:I-Cache和分支预测器(决定下一条指令的PC值)
    • Decode。用来识别出指令的类型指令需要的操作数以及指令的一些控制信号(通过对指令编码字段进行译码)。
    • Register Renaming。解决WAW和WAR这两种”伪相关性”
    • Dispatch。被重命名之后的指令会按照程序中规定的顺序,写到发射队列、ROB和SB中。如果这些部件中没有空闲的空间可以容纳当前的指令,那么这些指令就需要在流水线重命名阶段进行等待,这就相当于暂停了寄存器重命名以及之前的所有流水线,直到这些部件中有空闲的空间位置
    • Issue。仲裁(select)电路会从这个部件中挑选出合适的指令送到FU中执行。对于顺序发射仲裁电路只需要判断IQ中最旧那条指令是否准备好就可以了
    • Register File Read。被仲裁电路选中的指令需要从物理寄存器组(PRF)中读取操作数
    • Execute。指令得到操作数之后马上就可以送到对应的FU中执行
    • Write Back。这个阶段会将FU计算的结果写到物理寄存器组(PRF)中,同时这个阶段还有一个重要的功能,就是通过转发将这个计算结果送到需要的地方。
    • Commit。这个阶段起主要作用的部件时ROB,它会将乱序执行的指令拉回到程序中规定的顺序,还有SB部件,防止出现异常的指令写入存储部件无法恢复

Cache

  • 组相联和全相联的对比
    • 全相联需要更多的硬件(如多路选择器、比较器等),包括LRU所需的控制位(Use bit),但冲突不命中的影响降低了。
    • 组相联存在冲突不命中的影响,但需要的硬件更少了。
  • 3’C不命中
    • 冷启动不命中/强制不命中。指的是指令和数据第一次被访问
      • 缓解方法:指令预取(放入硬件stream buffer中,避免分支指令的污染,但分支预测失败会导致浪费预取的功耗和性能。又分为硬件预取和软件预取)
    • 冲突不命中。为了解决多个数据映射到Cache中的同一个位置的情况
      • 缓解方法:Victim Cache(本质上增加了way的个数),将被替换的块放入其中
    • 容量不命中。比如数据属于5个不同的set,但Cache中只有4个set
  • 串行访问和并行访问Cache
    • 对于I-Cache来说,流水线结构不会有太大影响;对于D-Cache来说,使用流水线会增大load指令的延迟(会导致比如分支的Penalty增加)
    • 串行访问
      • 优点:只需要访问Tag比较正确的那个SRAM,其他的SRAM都不需要被访问,可以将它们的使能信号置为无效,节约功耗
      • 缺点:延迟增大(对Cache的访问增加了一个时钟周期)
    • 并行访问
      • 优点:较低的时钟频率,比较Tag和读取SRAM可以并行
      • 缺点:较大的功耗,SRAM都需要访问,还需要MUX进行选择
  • Cache的写入
    • 写入地址不在Cache中
      • Write Allocate。将数据写到当前一级存储器中,但下一级存储器并未改写
      • Non-Write Allocate。将数据写到下一级存储器而不写到当前一级存储器中
    • 写入地址在Cache中
      • Write Back
      • Write Through。加入写缓存很重要
    • 一般Write Back搭配Write Allocate, Write Through搭配Non-Write Allocate。因为写回和写分配都是直接将数据写入到当前一级的存储器中;写通和写不分配都是直接将数据写到当下一级的存储器中
    • 在替换的时候将脏数据写回,可以提高性能,不必每次脏都写回
    • 加入write buffer,可以在Cache不命中,将脏数据写回时将数据先写到write buffer里,而不是低一级存储器(时间太长);或者是write through时将数据写到write buffer里;如果Cache发生缺失先要去write buffer里找(里面有最新的数据,通过地址比较)
  • TLB。加快了虚拟地址->物理地址的转换速度,存储着PTE(PPN+访问控制位)
    • 现代处理器采用两级TLB,第一级哈佛结构(I-TLB, D-TLB。全相联)和第二级组相联
    • TLB中记录的所有的页都不允许在物理内存中被替换(即被替换到磁盘中)
  • Cache的设计
    • PIPT。用物理地址访问Cache
    • VIVT。用虚拟地址访问Cache,会出现同义(Synonyms)和同名(Homonyms)的问题。
      • 同义。不同的虚拟地址映射到同一个物理地址,可以利用bank结构解决(使用物理地址的[12]作为mux的信号选择对应的bank)
      • 同名。相同的虚拟地址映射到不同的物理地址,通过设置ASID寄存器(表示进程,若有256个进程,该寄存器需要8位宽。进程共享时使用G位,此时ASID无效),或者清空TLB和虚拟Cache(开销大)来解决。ASID相当于扩展了虚拟存储器的空间,用ASID来进行页表的寻址(相当于VPN的一部分)
    • VIPT。充分利用并行性,在访问TLB的同时访问Cache

分支预测

  • 分支指令包含两个要素:方向和目标地址。对于RISC-V目标地址在指令中可以有两种存在形式:PC相对寻址和间接跳转
  • 快速解码/预解码:只需要辨别解码指令是否是分支指令,找到分支指令对应的PC值送到分支预测器中。如果使用ASID寄存器,就不需要在进程切换时清空TLB、虚拟Cache和分支预测器了
  • 分支预测都是以PC值(可以是部分)为基础进行的,每个PC值都对应一个两位饱和计数器的值
基于两位饱和计数器的分支预测
  • 四种状态:Strongly Taken, Weakly Taken, Weakly Not Taken, Strongly Not Taken
  • 使用部分PC值来寻址(索引)PHT(Pattern History Table),找到对应的两位饱和计数器的值,在流水线提交阶段,当指令要离开流水线时,更新PHT
  • 别名问题使用部分PC值必然会导致别名的问题,即两个PC值索引到同一个两位饱和计数器的值,如果这两个PC对应的不是同一条分支指令必然会产生干扰。使用哈希算法可以解决别名的问题。
基于局部历史的分支预测
  • 使用一个寄存器BHR(Branch History Branch)分支历史寄存器来记录一条分支指令在过去的历史状态,再用BHR去寻址PHT,因此BHR的位宽决定了PHT的大小,BHR的宽度由分支指令序列的循环周期决定(如果BHR的宽度超过循环周期就会浪费PHT的存储空间)
  • 分支指令序列:一般规律,如果一个指令序列中连续相同的数最多有P位,那么这个指令序列的循环周期就是P
  • 该分支预测的大前提:每条分支指令都有自己的BHR和PHT
  • 使用PC来寻址BHT和PHT,相当于很多PC值公用一个BHR,但通过PC的低位字段从PHTs中索引到相应的PHT(但是使用多个PHT会占空间)
  • 使用哈希算法解决PC索引BHR的别名,使用拼接或者异或方法,解决不同分支指令的BHR的内容是一样,对应的PHT中的饱和计数器的值也一样的问题
基于全局历史的分支预测
  • 使用一个寄存器GHR(Global History Register)来记录所有分支指令在过去的执行情况

寄存器重命名

  • 重命名映射表(Register Alias Table, RAT)
    • 用来保存已经存在的映射关系(例如一个逻辑寄存器映射到了哪个物理寄存器)。源寄存器通过读取重命名映射表,就可以得到它的物理寄存器了
  • 空闲寄存器列表(Free Register List, FRL)用来完成这个过程
    • 用来记录哪些物理寄存器是空闲的。在寄存器重命名时会通过这个表格来获得空闲的物理寄存器的编号
  • 寄存器重命名方式
    1. 将逻辑寄存器(ARF)扩展来实现寄存器重命名
    2. 使用统一的物理寄存器(PRF)来实现寄存器重命名
    3. 使用ROB来实现寄存器重命名

Verilog

  • 模块(module):包含输入和输出的硬件模块。如与门、多路选择器、优先级别编码器
  • 模块的功能的主要形式
    • 行为模型(behavioral)。描述一个模块做什么
    • 结构模型(structural)。应用层次化方法描述一个模块怎么由更简单的部件构造
  • 运算符优先级
  • 数字。声明常量的方法N'Bvalue(如9'h25表示9个bit的数十六进制值25), N表示位数,B表示基数(如h表示十六进制,b表示二进制)。注意这里的N标识bit数
    • 若不写N,如'b11,则在assign中赋值时将会自动进行拓展满足左值的位数
    • 若不写B则默认为十进制数
  • Z和X
    • z表示浮空值,如三态门使能位为0时,输出值为浮空
    • x表示无效逻辑电平
  • 位混合。assign y = {c[2:1], {3{d[0]}}, c[0], 3'b101},将y赋予9位值$c_2c_1d_0d_0d_0c_0101$。{}用于连接总线,{3{d[0]}}表示d[0]的3个拷贝
  • assign描述组合逻辑
  • vector(向量)。对相关的信号进行分组
    • 声明。注意与C语言不同的是,向量索引写在向量名称之前
      1
      2
      3
      4
      5
      6
      7
      wire [7:0] w;         // 8-bit wire
      reg [4:1] x; // 4-bit reg
      output reg [0:0] y; // 1-bit reg that is also an output port (this is still a vector)
      input wire [3:-2] z; // 6-bit wire input (negative ranges are allowed)
      output [3:0] a; // 4-bit output wire. Type is 'wire' unless specified otherwise.
      // 可以以这种方式切换大小端的表示方式
      wire [0:7] b; // 8-bit wire where b[0] is the most-significant bit.
    • 如果不用[high:low]方式进行声明,则称为隐式网络(implicit nets)会默认声明为1bit变量,注意隐式网络可能带来的错误。比如若向量赋值双方长度不匹配,则进行零拓展截断
    • 访问向量元素(类似C语言数组),如w[3:0], w[0]
    • 完成一个四输入的与门可以用=&技巧,对所有bit进行与,相当于C的&=
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      module top_module( 
      input [3:0] in,
      output out_and,
      output out_or,
      output out_xor
      );
      assign out_and =& in;
      assign out_or =| in;
      assign out_xor =^ in;
      endmodule
    • 向量连接运算符{},可以将多个bit连接到一起
    • 复制运算符。重复一个向量多次并连接到一起{num{向量}}这将向量复制num次并连接到一起,num必须是常数。如{2{a,b,c}}{a,b,c,a,b,c}一致。复制运算符能完成符号位拓展
  • 模块。类似C语言的函数,可以进行传参
    • 构建实例。目的,通过传参将子模块的端口和模块端口连接
      • 通过位置模块名 实例名 (wa, wb, wc); // 按照参数顺序。缺点,如果端口位置变化则需要重新编写
      • 通过名字模块名 实例名 (.当前模块端口名(wc), .当前模块端口名(wa), .当前模块端口名(wb)); // 可不按照参数顺序传参
  • 过程
    • always块。**具有更丰富的语句集(如if-thencase)**。always中要写多条语句必须得有beginend
      • 组合always@(*),使用阻塞赋值。等同于assign语句。区别是always块赋值的对象必须是变量类型如(reg);assign赋值的对象必须是**net类型**如(wire);
        1
        2
        3
        4
        5
        6
        7
        8
        9
        module top_module(
        input a,
        input b,
        output wire out_assign,
        output reg out_alwaysblock
        );
        always@(*) out_alwaysblock = a & b;
        assign out_assign = a & b;
        endmodule
      • 时序always@(posedge clk),使用非阻塞赋值。逻辑块的输出端创建一组触发器,逻辑块的输出不是立即可见的,而是仅在下一个(posedge clk)之后立即可见。括号里的posedge关键字指的是上升沿有效,clk为传入的时钟变量
    • if-else语句。如果嵌套中包含多于一条语句,则要搭配beginend
      1
      2
      3
      4
      5
      6
      7
      8
      always @(*) begin
      if (condition) begin
      out = x;
      end
      else begin
      out = y;
      end
      end
      • if-else语句,如果没有else分支,然后if也不满足的情况下,会生成一个锁存器存储当前的状态
    • case语句。caseendcase结尾,其中case项中的语句后都需要加一个;
      1
      2
      3
      4
      5
      6
      7
      8
      9
      always @(*) begin     // This is a combinational circuit
      case (in)
      1'b1: begin
      out = 1'b1; // begin-end if >1 statement
      end
      1'b0: out = 1'b0;
      default: out = 1'bx;
      endcase
      end
      • 总是以case开头,没有switch语句,case语句的项和C语言一样以:结尾
      • 每个case项只能执行一条语句,如果需要使用多个语句必须使用beginend
      • 允许重复(和部分重叠)case的项(C语言不允许)
    • 阻塞分配和非阻塞分配。广义上说,表示组合逻辑内部无执行顺序(称为非阻塞)
      • verilog三种类型的赋值
        • 连续赋值(assign x = y;)。不能在过程内部使用(始终阻塞)
        • 过程阻塞赋值(x=y;)。只能在过程内部使用
        • 过程非阻塞赋值(x <= y;)。只能在过程内部使用
  • 三目元运算符实现四选一多路选择器assign q = sel[1]? (sel[0]? c: b): (sel[0]? a: d);。注意在括号中不需要;
  • 对于时序电路硬件设计师应当具备逆向思维,先从输出思考,再考虑输入

问题

  • 为什么需要发射队列?
    • 若想在双发射超标量流水线中同时执行4条指令,就需要用到IQ,因为一些指令可能涉及到写回时相关性的停顿。