引言

上次课内容:

  • 指令集模拟器 = 指令集手册定义的状态机
  • C语言解释器 = C标准手册定义的状态机

 

本次课内容:

  • 处理器的状态机如何实现?

通过晶体管实现01

数字芯片由晶体管构成

数字芯片 = 处理数字信号的芯片 = 处理01的芯片

  • 晶体管是实现01的基本元件
  • 在数字芯片中, 通常使用金属-氧化物-半导体场效应晶体管(Metal-Oxide-Semiconductor Field-Effect Transistor, MOSFET)

  • 又分nMOS(上图)和pMOS两种, 都有栅极, 源极, 漏极

 

  • 根据电气特性, nMOS的功能为
    • \(V_G-V_S\)较大时, 源极和漏极导通 - 开关合上
    • \(V_G-V_S\)较小时, 源极和漏极截止 - 开关打开
  • pMOS的功能表现与nMOS类似
    • \(V_S-V_G\)较大时导通, \(V_S-V_G\)较小时截止

CMOS = 用MOS管的开关特性实现01

CMOS = Complementary MOS = nMOS + pMOS

最简单的CMOS电路:

  • A点加高电压时, n管导通, p管截止, 相当于Y点与地相连, 电压低
  • A点加低电压时, n管截止, p管导通, 相当于Y点与电源相连, 电压高

CMOS将nMOS和pMOS的开关特性转换成输出的高低电压

  • 将物理上的高电压定义为逻辑1(高电平), 低电压定义为逻辑0(低电平)

我们得到了数字电路中的两种基本状态!

通过晶体管搭建门电路

门电路 = 对状态进行运算

光有01还不够, 还要进行运算

  • 通过CMOS电路对01进行各种有意义的转换

 

  • 考虑之前的CMOS电路:
    • A点为1时, n管导通, p管截止, Y点为0
    • A点为0时, n管截止, p管导通, Y点为1

 

这正好是逻辑上的非运算!

  • 这个电路就是非门, 也称反相器

另一个门电路

  • P1和P2并联, 其中一者导通时, Y为1
  • N1和N2串联, 两者均导通时, Y为0
A B P1 P2 N1 N2 Y
0 0 导通 导通 截止 截止 1
0 1 导通 截止 截止 导通 1
1 0 截止 导通 导通 截止 1
1 1 截止 截止 导通 导通 0

 

这正好是逻辑上的与非运算!

  • 这个电路就是与非门

与门 = 与非门 + 非门

A B P1 P2 N1 N2 Y0 P3 N3 Y
0 0 导通 导通 截止 截止 1 截止 导通 0
0 1 导通 截止 截止 导通 1 截止 导通 0
1 0 截止 导通 导通 截止 1 截止 导通 0
1 1 截止 截止 导通 导通 0 导通 截止 1

或非门和或门

  • P1和P2串联, 两者均导通时, Y为1
  • N1和N2并联, 其中一者导通时, Y为0
A B P1 P2 N1 N2 Y
0 0 导通 导通 截止 截止 1
0 1 导通 截止 截止 导通 0
1 0 截止 导通 导通 截止 0
1 1 截止 截止 导通 导通 0

这正好是逻辑上的或非运算!

 

或非门的输出连一个非门, 可组成或门

三输入与非门

在门电路层面搭建

#T(x)为门电路x所需的晶体管数量, 则

#T(nand3) = #T(and) + #T(nand) = 6 + 4 = 10

可通过#T(x)粗略评估电路的面积

 

在晶体管层面搭建

#T(nand3) = 6
#T(nandN) = 2N
#T(andN) = #T(nandN) + #T(not) = 2(N + 1)

 

体现了ASIC设计中全定制电路的优势

  • 延迟低, 功耗低, 面积小

异或门

在门电路层面

  • Y=A^B=A*~B+~A*B
#T(xor) = 2#T(not) + 2#T(and) + #T(or) = 2 * 2 + 2 * 6 + 6 = 22

在晶体管层面搭建, 效果更优

  • A=1时, P2和N2相当于非门; P3和N3均截止; 故此时Y=~B
  • A=0时, P2和N2均截止; B=0时N3导通; B=1时P3导通; 故此时Y=B
#T(xor) = 6

通过门电路搭建基本组合逻辑电路

译码器

检测\(n\)位输入的值, 使\(2^n\)位输出中的相应位为1

  • 若输入为\(X\), 则输出的第\(X\)位为1, 其他位为0
    • 该输出也称独热码(One-hot)
  • 常用于寻址: 输入某地址, 生成相应内容的有效信号

 

例: 2-4译码器

\(A_1\) \(A_0\) \(|\) \(Y_3\) \(Y_2\) \(Y_1\) \(Y_0\)
0 0 \(|\) 0 0 0 1
0 1 \(|\) 0 0 1 0
1 0 \(|\) 0 1 0 0
1 1 \(|\) 1 0 0 0

#T(2-4 dec) = 2#T(not) + 4#T(and) = 2 * 2 + 4 * 6 = 28
#T(5-32 dec) = 5#T(not) + 32#T(and5) = 5 * 2 + 32 * 12 = 394
#T(10-1024 dec) = 10#T(not) + 1024#T(and10) = 10 * 2 + 1024 * 22 = 22548

编码器

\(2^n\)位输入, \(n\)位输出, 与译码器功能相反

  • 若输入为独热码, 且第\(X\)位为1, 则输出\(X\)
  • 常用于根据独热码生成地址: 找出1的位置

 

例: 4-2编码器

\(A_3\) \(A_2\) \(A_1\) \(A_0\) \(|\) \(Y_1\) \(Y_0\)
0 0 0 1 \(|\) 0 0
0 0 1 0 \(|\) 0 1
0 1 0 0 \(|\) 1 0
1 0 0 0 \(|\) 1 1
\(|\) 0 0

#T(4-2 enc) = 4#T(not) + 4#T(and4) + 2#(or) = 4 * 2 + 4 * 10 + 2 * 6 = 60

优先编码器

可支持独热码以外的输入, 但只编码优先级最高的位

  • 常用于找出第一个1的位置

 

例: 4-2优先编码器

\(A_3\) \(A_2\) \(A_1\) \(A_0\) \(|\) \(Y_1\) \(Y_0\)
0 0 0 1 \(|\) 0 0
0 0 1 X \(|\) 0 1
0 1 X X \(|\) 1 0
1 X X X \(|\) 1 1
\(|\) 0 0

#T(4-2 prioenc) = 2#T(not) + #T(and3) + #T(and) + 2#(or) = 2 * 2 + 8 + 6 + 2 * 6 = 30

多路选择器

根据选择端选择一路输入

  • 将选择端看作地址,则类似一次寻址操作

 

例: 2选1多路选择器

\(S\) \(|\) \(Y\)
0 \(|\) \(D_0\)
1 \(|\) \(D_1\)

#T(2-1 mux) = #T(1-2 dec) + 2#T(and) + #(or) = #T(not) + 2 * 6 + 6 = 20
#T(2-1 mux32) = #T(1-2 dec) + 32(2#T(and) + #(or)) = 2 + 32 * (2 * 6 + 6) = 578

对于数据位宽为\(M\)位的\(N\)选1多路选择器, \(\log_{2}{N}\)-\(N\)译码器的输出只有1位为1, 且可被\(M\)\(N\)选1多路选择器复用

比较器

检查两个输入的每一位是否完全一致

例: 4位比较器

#T(cmp4) = 4#T(xnor) + #T(and4) = 4 * 6 + (4 + 4 + 2) = 34
#T(cmp32) = 32#T(xnor) + #T(and32) = 32 * 6 + (32 + 32 + 2) = 258

1位加法器

  • 半加器(Half Adder, HA) - 输入无进位的加法器
S = A ^ B
C = A & B
#T(HA) = #T(xor) + #T(and) = 6 + 6 = 12
  • 全加器(Full Adder, FA) - 输入有进位的加法器
S = A ^ B ^ Cin
Cout = (A & B) | (Cin & (A ^ B))
\(A\) \(B\) \(|\) \(S\) \(C\)
0 0 \(|\) 0 0
0 1 \(|\) 1 0
1 0 \(|\) 1 0
1 1 \(|\) 0 1

可以用两个半加器组成一个全加器

#T(FA) = 2#T(HA) + #T(or) = 2 * 12 + 6 = 30

多位加法器

将低位FA的进位输出作为高位FA的进位输入

行波进位加法器(Ripple-Carry Adder, RCA)

#T(RCA4) = #(HA) + 3#T(FA) = 12 + 3 * 30 = 102
#T(RCA32) = #(HA) + 31#T(FA) = 12 + 31 * 30 = 942

时序逻辑电路

组合逻辑 vs. 时序逻辑

  • 组合逻辑电路的输出完全由当前输入决定
    • 可实现各种运算功能
  • 新需求: 如何实现PC = PC + 4?

 

正确理解PC = PC + 4: 次态的PC = 当前的PC + 4

  • 我们需要新的电路特性:
    • 可以读出电路的旧状态
    • 可以更新电路的状态
  • 组合逻辑电路没有新旧状态的概念

 

需要通过时序逻辑电路实现 - 可以存储状态的电路

交叉配对反相器(Cross-Coupled Inverters)

\(Q = A_{out} = \overline{A_{in}} = \overline{B_{out}} = \overline{\overline{B_{in}}} = \overline{\overline{Q}} = Q\) \(\overline{Q} = B_{out} = \overline{B_{in}} = \overline{A_{out}} = \overline{\overline{A_{in}}} = \overline{\overline{\overline{Q}}} = \overline{Q}\)

  • \(Q\)\(\overline{Q}\)都处于保持不变的稳定状态

  • 可稳定地存储1 bit信息(通过\(Q\)端输出)
    • \(Q=0, \overline{Q}=1\), 则认为存储0; \(Q=1, \overline{Q}=0\), 则认为存储1

设线延迟为\(T_w\), 反相器延迟为\(T_g\), 如果一开始\(Q=\overline{Q}=0\), 会怎样?

  • \(T_w\)后, \(A_{in}=B_{in}=0\); \(T_g\)后, \(A_{out}=B_{out}=1=Q=\overline{Q}\)
  • \(T_w\)后, \(A_{in}=B_{in}=1\); \(T_g\)后, \(A_{out}=B_{out}=0=Q=\overline{Q}\)
  • 电路处于震荡状态, 无法表示稳定的信息(也称亚稳态)

不过, 即使上述电路位于稳定状态, 也使无法更新\(Q\)\(\overline{Q}\)

更实用的存储单元 - SR锁存器

S R \(|\) Q
0 0 \(|\) 保持
0 1 \(|\) 0
1 0 \(|\) 1
1 1 \(|\) 禁止

S(et)R(eset)锁存器, 其中S和R用于控制锁存器的状态

#T(SR latch) = 2#T(nor) = 2 * 4 = 8
  • 不允许S=R=1
    • 或非门的特性使反馈功能失效, 此时输出均为0
    • 从S=R=1变为S=R=0时, 会进入亚稳态
      • 真实电路可能会受随机扰动而进入某个稳定状态, 但无法提前预知
      • 每次工作可能会产生不同的结果(类似包含UB的程序)

避免亚稳态 - D锁存器

思想: 额外添加两个与门, 将SR锁存器的4种输入限制成3种合法输入

  • D为输入数据
  • WE为写使能(Write Enable)
#T(D latch) = #T(SR latch) + 2#T(and) + #T(not) = 8 + 2 * 6 + 2 = 22
WE D \(|\) S R \(|\) Q
0 0 \(|\) 0 0 \(|\) 保持
0 1 \(|\) 0 0 \(|\) 保持
1 0 \(|\) 0 1 \(|\) 0
1 1 \(|\) 1 0 \(|\) 1

用与非门搭建的D锁存器

#T(D latch) = 4#T(nand) = 4 * 4 = 16

面积更小

WE D \(|\) \(\overline{S}\) \(\overline{R}\) \(|\) Q
0 0 \(|\) 1 1 \(|\) 保持
0 1 \(|\) 1 1 \(|\) 保持
1 0 \(|\) 1 0 \(|\) 0
1 1 \(|\) 0 1 \(|\) 1

同步电路 vs. 异步电路

  • 如果系统规模增大, 如何控制多个部件协同工作?
    • 例如, 如何在当前指令执行结束后再取下一条指令?
  • 需要正确实现一种同步关系
    • 事件A在事件B之后发生(happened after)

 

需要额外的机制来实现同步关系

  • 同步电路: 通过全局的周期性时钟信号实现
    • 存储单元仅在时钟边沿到达时写入数据
    • 且在该时钟周期中稳定读出该数据
    • 实现简单, 容易分析, 但功耗较大
  • 异步电路: 通过部件之间的局部通信信号实现
    • 无时钟开销, 功耗低, 但实现和分析较困难

仅靠D锁存器无法实现同步电路的特性

同步电路: 存储单元仅在时钟边沿到达时写入数据, 且在该时钟周期中稳定读出该数据

 

D锁存器的性质: WE有效时, 输入的变化马上传播到输出

将时钟连到D锁存器的WE端仍然无法实现

  • 假设上升沿触发

 

需要一种新的电路结构

D触发器(D Flip-Flop, DFF)

若将左上图的三输入与非门G(nand3)看成二输入与门(and2)和二输入与非门(nand2)的级联, 其行为等价于右上图

#T(DFF) = 5#T(nand) + #T(nand3) = 5 * 4 + 6 = 26

D触发器的工作原理

\(|\) \(\mathrm{sr_0}\) \(|\) \(\mathrm{sr_1}\) \(|\) \(\mathrm{sr_2}\)
clk D \(|\) \(\mathrm{\overline{S}}\) \(\mathrm{\overline{R}}\) \(\mathrm{\overline{Q}}\) \(|\) \(\mathrm{\overline{S}}\) \(\mathrm{\overline{R}}\) \(\mathrm{Q}\) \(\mathrm{\overline{Q}}\) \(|\) \(\mathrm{\overline{S}}\) \(\mathrm{\overline{R}}\) \(\mathrm{Q}\)
0 0 \(|\) 1 0 1 \(|\) 0 0 1 1 \(|\) 1 1 保持
0 1 \(|\) 0 0 1 \(|\) 0 1 1 0 \(|\) 1 1 保持
\(\uparrow\) 0 \(|\) 1 1 1(保持) \(|\) 1 0 0 1 \(|\) 1 0 0
1 1 \(|\) 1 1 1(保持) \(|\) 1 1 0(保持) 1(保持) \(|\) 1 0 0
\(\uparrow\) 1 \(|\) 0 1 0 \(|\) 0 1 1 0 \(|\) 0 1 1
1 0 \(|\) 1 1 0(保持) \(|\) 0 0 1 1 \(|\) 0 1 1

D触发器的核心思想:

  • 时钟低电平期间, 通过\(\mathrm{sr_2}\)的保持功能, 保持其输出不变
  • 时钟上升沿到来时, 根据输入D触发\(\mathrm{sr_2}\)的复位或置位功能, 以写入数据

  • 时钟高电平期间, 通过\(\mathrm{sr_0}\)\(\mathrm{sr_1}\)的保持功能, 使D的变化无法传播到\(\mathrm{sr_2}\)

带使能端的D触发器

 

#T(DFFE) = #T(DFF) + #T(2-1 mux) = 26 + 20 = 46

多加一个使能端, D触发器的晶体管数量增加77%!

寄存器 = 可同时读写多位的结构

由多个D触发器组成

 

在RV32中, PC寄存器所需的晶体管数量约为:

#T(RV32.PC) = 32#T(DFFE) = 32 * 46 = 1472

存储器

存储器 = 可寻址的存储单元的集合

可看成一个由比特构成的矩阵

  • 每一行是一个存储单元
    • 地址 = 行编号, 行的数量 = 存储器的深度(depth)
    • 地址的位宽 = \(\log_2(\mathrm{depth})\)
  • 一行可存储多位数据
    • 一行中数据的位宽 = 存储器的宽度(width)

例: 深度为2, 宽度为3的存储器

地址 内容
0 \(b_{(0, 2)}b_{(0, 1)}b_{(0, 0)}\)
1 \(b_{(1, 2)}b_{(1, 1)}b_{(1, 0)}\)

存储器又分只读存储器(ROM)和随机访问存储器(RAM), 后者可读可写

  • 我们下面讨论RAM

用D触发器实现存储器(读操作)

给定地址addr, 读出存储器中的相应内容

  • 左上方的译码器又称地址译码器
  • 地址译码器, 与门和或门共同构成一个3位的2选1多路选择器
    • 根据addr选出一行数据

用D触发器实现存储器(写操作)

给定地址addr和数据D, 将数据D写入存储器中的相应位置

  • 将数据D的每一位分别连接到每一行的D端
  • 根据地址译码器的结果, 将需要写入的行所在的使能信号置1, 其余行的使能信号为0

用D触发器实现存储器(完整结构)

#T(3x2 RAM) = #T(1-2 dec) + 6#T(DFFE) + 8#T(and) + 3#(or) = 2+6*46+8*6+3*6 = 344
#T(RV32.Reg) = #T(32x32 RAM) =
    #T(5-32 dec)
  + (32*32)#T(DFFE)
  + 32#T(and)       // 生成一行的使能信号, 共32行
  + (32*32)#T(and)  // 用于读出一行数据的与门, 其数量与D触发器相同
  + 32#T(or32)      // 用于选出一位数据的或门, 输入端口数量=存储器深度, 或门数量=存储器宽度
= 394 + 1024 * 46 + 32 * 6 + 1024 * 6 + 32 * 66 = 55946

面积更小的存储器 - SRAM

#T(32x32 RAM) = #T(5-32 dec) + 1024#T(DFFE) + 32#T(and) + 1024#T(and) + 32#T(or32)
              = 394          + 1024 * 46    + 32 * 6    + 1024 * 6    + 32 * 66

随着RAM的规模增加, 带使能端的D触发器的晶体管数量占主要部分

 

想法: 用面积更小的存储单元构成RAM

 

SRAM单元 - 在晶体管层面全定制!

  • 只需6根晶体管

SRAM单元工作原理

  • 空闲: 字线加低电压时, N1和N2截止, 无读写操作
  • 读出: 向两根位线加高电压(接近逻辑1), 再向字线加高电压, 此时N1和N2导通, 根据存储的信息, 其中一根位线的电压轻微下降, 可通过放大电路检测并确定哪一根位线, 从而得知存储的信息是逻辑0还是逻辑1
  • 写入: 将两根字线分别设置成逻辑0和逻辑1, 再向字线加高电压, 效果类似SR锁存器的复位和置位

可将SRAM单元的行为抽象成一个锁存器(假设高电平有效)

  • SEL有效时, Q端读出单元中数据
  • SEL有效且WE有效时, 将D写入单元

用SRAM单元实现存储器

#T(3x2 RAM) = 344
#T(3x2 SRAM) = #T(1-2 dec) + 6#T(SRAM latch) + #T(and) = 2 + 6 * 6 + 6 = 44
#T(32x32 RAM) = 55946
#T(32x32 SRAM) = #T(5-32 dec) + (32*32)#T(SRAM latch) + #T(and)
               = 394          + 1024 * 6              + 6
               = 6544

 

如何解决锁存器不满足同步电路特性的问题?

同步SRAM

提前用D触发器存放输入端

  • 使SRAM的输入受时钟控制

 

  • 好处: 能在同步电路中使用SRAM了
  • 代价:
    • D触发器带来额外的面积开销
      • 但与存储阵列相比可忽略不计
    • 读出数据时需要多等待一个周期
      • D触发器需要在下个时钟上升沿到来时才会把D端数据传播到Q端

存储器对比

存储器单元 #T 读延迟 处理器中的使用场景 典型容量
带使能端的D触发器 46 当前周期 通用寄存器堆 < 1KB
SRAM单元 6 下一周期 高速缓存 32KB~32MB

 

Q: AMD某型号处理器配备一个大小为384MB的高速缓存, 假设该高速缓存通过SRAM存储阵列实现, 问存储阵列的晶体管数量有多少?

A: 384*1024*1024*8*6=19,327,352,832, 约200亿!

YPC - 用数字电路搭建简单处理器

回顾: ISA手册定义了一个状态机

  • 状态集合\(S = \{<R, M>\}\)
    • \(R = \{PC, x_0, x_1, x_2, \dots\}\)
      • RISC-V手册 -> 2.1 Programmers’ Model for Base Integer ISA
      • \(PC\) = 程序计数器 = 当前执行的指令位置
    • \(M\) = 内存
      • RISC-V手册 -> 1.4 Memory
  • 激励事件\(E = \{指令\}\)
    • 执行PC指向的指令
  • 状态转移规则\(next: S \times E \to S\)
    • 指令的语义(semantics)
  • 初始状态\(S_0 = <R_0, M_0>\)

上次课我们用C程序实现了这个状态机并执行指令

现在让我们来用电路实现它!

用数字电路实现ISA状态机 = 处理器

程序 抽象计算机 CPU
状态 \(\{<V, PC>\}\) \(\{<R, M>\}\) \(\{时序逻辑电路\}\)
状态转移规则 C语言语句的语义 指令的语义 组合逻辑电路
FM C语言标准手册 指令集手册 架构设计文档

 

  • 用时序逻辑电路实现寄存器和内存
  • 用组合逻辑电路实现指令的语义
    • 电路只适合处理二进制信息, 不适合处理汇编指令
      • CPU = 指令集模拟器的 “加速器”
      • 指令集模拟器 = CPU的 “虚拟化”

用时序逻辑电路实现寄存器和内存

  • \(PC\) - 单个寄存器, 初值为0
  • \(R\) - 通用寄存器组, 可寻址, 用带使能的D触发器搭建存储器
  • \(M\) - 内存, 可寻址
    • 此处因容量小, 先用带使能的D触发器实现, 可不考虑读延迟
    • 真实的\(M\)一般较大, 后期我们会逐渐过渡到SRAM和DRAM

 

我们用Chisel代码来展示

val R = Mem(32, UInt(32.W))
val PC = RegInit(0.U(32.W))
val MSize = 1024
val M = Mem(MSize / 4, UInt(32.W))

用组合逻辑电路实现指令的语义

// instruction structure and helper functions
val Ibundle = new Bundle {
  val imm11_0 = UInt(12.W)
  val rs1     = UInt( 5.W)
  val funct3  = UInt( 3.W)
  val rd      = UInt( 5.W)
  val opcode  = UInt( 7.W)
}

// fetch
val inst = M(PC(31, 2)).asTypeOf(Ibundle)

// opcode decode
val isAddi = (inst.opcode === "b0010011".U) && (inst.funct3 === "b000".U)
val isEbreak = inst.asUInt === "x00100073".U
assert(isAddi || isEbreak, "Invalid instruction 0x%x", inst.asUInt)  // (*)
// operand decode
val rs1 = Mux(isEbreak, 10.U(5.W), inst.rs1)
val rs2 = Mux(isEbreak, 11.U(5.W), 0.U(5.W))
val rs1Val = Mux(rs1 === 0.U , 0.U(32.W), R(rs1))
val rs2Val = Mux(rs2 === 0.U , 0.U(32.W), R(rs2))
val immVal = Cat(Fill(20, inst.imm11_0(11)), inst.imm11_0)

// execute
when (isAddi) { R(inst.rd) := rs1Val + immVal }
when (isEbreak && (rs1Val === 0.U)) { printf("%c", rs2Val(7,0)) }  // (*)
io.halt := isEbreak && (rs1Val === 1.U)

// update PC
PC := PC + 4.U

(*)的代码并非电路, 需要仿真环境的辅助来实现功能

整理一下, 得到YPC(Ysyx Processor Core)的代码

import chisel3._
import chisel3.util._
import chisel3.util.experimental.loadMemoryFromFileInline

class YPC extends Module {
  val io = IO(new Bundle{ val halt = Output(Bool()) })
  val R  = Mem(32, UInt(32.W))
  val PC = RegInit(0.U(32.W))
  val M  = Mem(1024 / 4, UInt(32.W))
  def Rread(idx: UInt) = Mux(idx === 0.U, 0.U(32.W), R(idx))
  loadMemoryFromFileInline(M, "prog.hex")

  val Ibundle = new Bundle {
    val imm11_0 = UInt(12.W)
    val rs1     = UInt( 5.W)
    val funct3  = UInt( 3.W)
    val rd      = UInt( 5.W)
    val opcode  = UInt( 7.W)
  }
  def SignEXT(imm11_0: UInt) = Cat(Fill(20, imm11_0(11)), imm11_0)

  val inst = M(PC(31, 2)).asTypeOf(Ibundle)
  val isAddi = (inst.opcode === "b0010011".U) && (inst.funct3 === "b000".U)
  val isEbreak = inst.asUInt === "x00100073".U
  assert(isAddi || isEbreak, "Invalid instruction 0x%x", inst.asUInt)

  val rs1Val = Rread(Mux(isEbreak, 10.U(5.W), inst.rs1))
  val rs2Val = Rread(Mux(isEbreak, 11.U(5.W), 0.U(5.W)))
  when (isAddi) { R(inst.rd) := rs1Val + SignEXT(inst.imm11_0) }
  when (isEbreak && (rs1Val === 0.U)) { printf("%c", rs2Val(7,0)) }
  io.halt := isEbreak && (rs1Val === 1.U)
  PC := PC + 4.U
}

设计电路(硬件)和设计模拟器(软件)不同

软件设计 硬件设计
算法流程图 电路结构图
代码 描述算法流程 描述电路结构
多行代码间的关系 反映流程的先后顺序 反映部件之间的连接关系
代码的作用 生成程序来执行 生成电路版图来制造
模块的作用 调用子程序 实例化子部件
  • 硬件设计的本质 = 实例化 + 连线
    • 想象有一个空白的电路板, 还有一张设计图纸
    • 实例化 = 按照图纸拿一个元件(门电路, 触发器…)放在电路板上
    • 连线 = 按照图纸用导线将这些元件的引脚连起来

我们用晶体管展示各个电路的内部结构, 就是希望大家理解上述本质

给初学者的建议: 如果写电路时大脑蹦出 “执行”这个词, 你就输了

  • 买盒乐高玩一玩, 体会一下搭积木的过程

粗略估算电路晶体管数量, 避免电路面积爆炸

我们给出各个电路#T(x)的意义: 希望大家对常见电路的面积有大致的认识

  • 知道哪些电路的面积随规模指数增长
  • 避免在代码中大量使用这样的电路
    • 16-65536译码器, 32位的64选1多路选择器, 1024位加法器…
  • 了解写出的RTL代码背后意味着什么
    • 除了逻辑, 还有PPA: 延迟, 功耗, 面积

 

课后小作业:

  1. 画出YPC的电路结构图(忽略输出字符的功能)
  2. 浏览电路结构图中的每一处细节, 思考如何用晶体管搭建
  3. 估算YPC需要占用多少晶体管
  4. 不考虑\(M\), 找出YPC中最占面积的部分

编译成Verilog

module YPC( // <stdin>:3:10
  input  clock, // <stdin>:4:11
         reset, // <stdin>:5:11
  output io_halt    // playground/src/YPC.scala:6:14
);
  wire [31:0] _M_ext_R0_data;   // playground/src/YPC.scala:9:15
  wire [31:0] _R_ext_R0_data;   // playground/src/YPC.scala:7:15
  wire [31:0] _R_ext_R1_data;   // playground/src/YPC.scala:7:15
  reg  [31:0] PC;   // playground/src/YPC.scala:8:19
  wire        isAddi = _M_ext_R0_data[6:0] == 7'h13 & _M_ext_R0_data[14:12] == 3'h0;    // playground/src/YPC.scala:9:15, :22:35, :23:{29,47,63}
  wire        isEbreak = _M_ext_R0_data == 32'h100073;  // playground/src/YPC.scala:9:15, :24:30
  wire [4:0]  _rs1Val_T = isEbreak ? 5'hA : _M_ext_R0_data[19:15];  // playground/src/YPC.scala:9:15, :22:35, :24:30, :27:25
  wire [31:0] rs1Val = _rs1Val_T == 5'h0 ? 32'h0 : _R_ext_R0_data;  // playground/src/YPC.scala:7:15, :8:19, :10:{29,34}, :27:25, :28:25
  wire [4:0]  _rs2Val_T = isEbreak ? 5'hB : 5'h0;   // playground/src/YPC.scala:24:30, :28:25
  `ifndef SYNTHESIS // playground/src/YPC.scala:25:9
    always @(posedge clock) begin   // playground/src/YPC.scala:25:9
      if (~reset & ~(isAddi | isEbreak)) begin  // playground/src/YPC.scala:23:47, :24:30, :25:{9,17}
        if (`ASSERT_VERBOSE_COND_)  // playground/src/YPC.scala:25:9
          $error("Assertion failed: Invalid instruction 0x%x\n    at YPC.scala:25 assert(isAddi || isEbreak, \"Invalid instruction 0x%%%%x\", inst.asUInt)\n",
                 _M_ext_R0_data);   // playground/src/YPC.scala:9:15, :25:9
        if (`STOP_COND_)    // playground/src/YPC.scala:25:9
          $fatal;   // playground/src/YPC.scala:25:9
      end
      if ((`PRINTF_COND_) & isEbreak & rs1Val == 32'h0 & ~reset)    // playground/src/YPC.scala:8:19, :10:29, :24:30, :25:9, :30:{29,47}
        $fwrite(32'h80000002, "%c",
                {_rs2Val_T[3], _rs2Val_T[1:0]} == 3'h0 ? 8'h0 : _R_ext_R1_data[7:0]);   // playground/src/YPC.scala:7:15, :8:19, :10:{29,34}, :23:63, :28:25, :30:47
    end // always @(posedge)
  `endif // not def SYNTHESIS
  always @(posedge clock) begin // <stdin>:4:11
    if (reset)  // <stdin>:4:11
      PC <= 32'h0;  // playground/src/YPC.scala:8:19
    else    // <stdin>:4:11
      PC <= PC + 32'h4; // playground/src/YPC.scala:8:19, :32:12
  end // always @(posedge)
  R_combMem R_ext ( // playground/src/YPC.scala:7:15
    // ...
  );
  M_combMem M_ext ( // playground/src/YPC.scala:9:15
    .R0_addr (PC[9:2]), // playground/src/YPC.scala:8:19, :22:{15,18}
    .R0_en   (1'h1),    // <stdin>:3:10
    .R0_clk  (clock),
    .R0_data (_M_ext_R0_data)
  );
  assign io_halt = isEbreak & rs1Val == 32'h1;  // <stdin>:3:10, playground/src/YPC.scala:10:29, :24:30, :31:{23,34}
endmodule

仿真环境

有一些电路无法实现的功能, 需要在仿真环境(是个软件)中实现

  • 用仿真过程中的assert()实现RTL代码中的assert()功能
  • 用仿真过程中的stdio.h实现RTL代码中的printf()功能
  • 在仿真环境中将程序加载到\(M\)
#include <stdio.h>
#include "VYPC.h"
#include "VYPC___024root.h"
static VYPC *top = NULL;
void step() { top->clock = 0; top->eval(); top->clock = 1; top->eval(); }
void reset(int n) { top->reset = 1; while (n --) { step(); } top->reset = 0; }
void load_prog(const char *bin) {
  FILE *fp = fopen(bin, "r");
  fread(&top->rootp->YPC__DOT__M_ext__DOT__Memory, 1, 1024, fp);
  fclose(fp);
}
int main(int argc, char *argv[]) {
  top = new VYPC;
  load_prog(argv[1]);
  reset(10);
  while (!top->io_halt) { step(); }
  return 0;
}
sed -i -e '/define ENABLE_INITIAL_MEM_/d' YPC.v # 不通过生成的`readmemh`读入M
verilator --cc --exe main.cpp YPC.v  # 通过Verilator将Verilog文件编译成C++仿真文件
make -C obj_dir -f VYPC.mk           # 将C++仿真文件编译成可执行的仿真程序
./obj_dir/VYPC hello.bin             # 启动仿真程序, 在YPC的电路上运行hello程序

仿真程序: 用C++实现的电路状态机

状态: 包含时序逻辑电路(部分组合逻辑信号和端口也用C变量来表示)

// obj_dir/VYPC___024root.h
    // DESIGN SPECIFIC STATE
    VL_IN8(clock,0,0);
    VL_IN8(reset,0,0);
    VL_OUT8(io_halt,0,0);
    CData/*0:0*/ __Vtrigrprev__TOP__clock;
    CData/*0:0*/ __VactContinue;
    IData/*31:0*/ YPC__DOT___M_ext_R0_data;
    IData/*31:0*/ YPC__DOT__PC;
    IData/*31:0*/ YPC__DOT__rs1Val;
    IData/*31:0*/ __VstlIterCount;
    IData/*31:0*/ __VactIterCount;
    VlUnpacked<IData/*31:0*/, 32> YPC__DOT__R_ext__DOT__Memory;
    VlUnpacked<IData/*31:0*/, 256> YPC__DOT__M_ext__DOT__Memory;
    // ...

状态转移: 翻译Verilog中的组合逻辑电路

// obj_dir/VYPC___024root__DepSet_h5f434cf1__0.cpp
    vlSelf->YPC__DOT__PC = ((IData)(vlSelf->reset) ? 0U
                             : ((IData)(4U) + vlSelf->YPC__DOT__PC));
    // ...
    vlSelf->io_halt = ((0x100073U == vlSelf->YPC__DOT___M_ext_R0_data) 
                       & (1U == vlSelf->YPC__DOT__rs1Val));

更多的模拟器/仿真器

举例 效率 精确度
指令集模拟器 YEMU, NEMU, QEMU +++++ 指令集(行为正确)
体系结构模拟器 GEM5 +++ 性能(大致运行时间)
RTL仿真器 VCS, Verilator ++ 微结构(IPC)
晶体管仿真器 Spice + 晶体管(物理特性)

 

在处理器芯片设计企业中, 前三类都会使用:

  • 指令集模拟器 - 前期软件开发, 验证的行为参考模型
  • 体系结构模拟器 - 设计空间探索, 快速寻找可提升处理器性能的特性
  • RTL仿真器 - RTL测试验证

 

后面的课程会进一步讨论

总结

从晶体管到CPU

  • 用nMOS和pMOS的开关特性实现01
  • 通过晶体管搭建门电路进行01的简单计算
    • not, nand, and, nor, or, xor
  • 通过门电路搭建组合逻辑电路处理信息
    • 译码器, 编码器, 多路选择器, 比较器, 加法器
  • 通过门电路搭建时序逻辑电路存储信息
    • SR锁存器, D锁存器, D触发器, 寄存器, SRAM

 

  • YPC = 处理器 = 用上述电路实现指令集手册定义的状态机
    • 通过仿真环境实现自定义freestanding运行时环境
    • 支持两条指令的指令周期: 取指, 译码, 执行, 更新PC

 

  • 硬件设计的本质 = 实例化 + 连线