数字逻辑设计设计和RISC-V架构(上文中)

我没有数字逻辑设计的经验。也就是说,直到最近我才决定尝试设计自己的 CPU 并在 FPGA 上运行它!如果你也是一名软件工程师并且对硬件设计感兴趣,那么我希望这一系列关于我所学的文章对你有所帮助和有趣。在本系列文章的第一部分,将回答以下问题:

我将在以后的系列文章中详细讨论我的 CPU 设计和 RISC-V 架构,并将回答以下问题:

您可以看到我在此处编写的 CPU 代码,或在此处查看最新版本。

什么是数字逻辑设计?

数字逻辑设计是对二进制值进行操作的逻辑电路的设计。基本元素是逻辑门:例如,像与门一样,它有两个输入和一个输出。它的输出是 1 或 iff,两个输入都是 1。

我们设计的同步电路一般使用触发器来存储状态,并使电路操作与公共时钟同步。触发器由逻辑门组成。

模拟电路设计包括构成逻辑门的电子元件,例如晶体管和二极管。这种抽象通常用于直接处理来自模拟传感器(例如无线电接收器)的信号的应用程序。在设计 CPU 时,这种抽象级别是行不通的:现代 CPU 有数十亿个晶体管!

相反,我们使用将数字逻辑设计转换为不同有用格式的工具:FPGA 配置(见下文);模拟; 晶圆布局。

什么是 FPGA,为什么要使用 FPGA?

我们在上面指出,无论我们是创建定制 ASIC 芯片还是配置 FPGA,都可以使用相同的数字逻辑设计工具。现场可编程门阵列 (FPGA) 是一种集成集成电路,包含一组可编程逻辑块。您可以将其视为可以以多种方式连接的大量逻辑门。

定制一个芯片很容易花费数百万美元,当然,芯片一旦生产出来,就无法更改。所以FPGA通常用在以下几种情况:

有什么缺点?也就是说,FPGA 的单芯片成本要高得多,而且由于它能够以非常灵活的方式将逻辑块连接在一起,因此速度通常要慢得多。相比之下,定制设计可以在不考虑灵活性的情况下减少晶体管的数量。

在我看来,比较 ASIC 的定制设计过程和 FPGA 的设计过程是有帮助的:

我需要什么工具?

硬件描述语言:我正在使用 nMigen

您可能听说过 Verilog 或 VHDL:两种流行的硬件描述语言(HDL)。当我在这里说“流行”时,我的意思是广泛使用d触发器是同步还是异步,而不是广泛流行。

我不会假装对这些工具了解很多。我只认识比我聪明的人,有丰富的逻辑设计经验,但讨厌这些工具。由于 Verilog 和其他类似工具的问题,已经尝试开发更有用和更友好的替代方案。nMigen 是一个在 Python 中创建特定领域语言的项目。用它自己的话来说:

尽管 Verilog 和 VHDL 中的硬件设计比输入原理图更快,但由于多种原因,硬件设计仍然乏味且效率低下。事件驱动模型为同步电路引入了不必要的问题和手动编码,目前在逻辑设计中发挥着重要作用。违反直觉的算术规则会导致更陡峭的学习曲线,并为设计中的微小缺陷提供温床。最后,通过“generate”语句对逻辑过程生成(元编程)的支持非常有限,并且限制了代码的泛化、重用和组织方式。

针对这些问题,我们开发了 nMigen FHDL,一个替代事件驱动范式的库,采用组合和同步语句的概念,并采用使整数始终表现得像数学整数的算术规则,最重要的是允许 Python 程序构建设计的逻辑。这使硬件设计人员能够利用 Python 语言的丰富性:面向对象的编程、函数参数、生成器、运算符重载、库等,以构建组织良好且可重用的优雅设计。

如果您像我一样从未使用过 Verilog,那么这些对您来说不仅仅是抽象的含义。但这听起来确实很有希望,而且我可以证明,没有 Verilog 的障碍,从逻辑设计开始非常简单。如果你对 Python 非常熟悉,我会推荐它!

我能想到的唯一缺点是 nMigen 仍在开发中,尤其是文档不完整。但是您可以通过 chat.freenode.net 上的#nmigen 频道找到一个有用的社区。

用于检查模拟的波形显示:我正在使用 GTKWave

nMigen 提供仿真工具。我将它用于用 pytest 编写的测试。为了帮助调试,我记录了这些测试的信号并在波形显示中观察它们。

FPGA 板:我正在使用 myStorm BlackIce II

您不必使用 FPGA 开发板来创建自己的 CPU。在模拟中,您可以做任何事情。对我来说,在工作中使用该板的乐趣在于能够使 LED 闪烁并观看我的设计运行。

当然,如果你正在构建比我最基本的 CPU 更有用的东西,你可能需要一些硬件来运行它,这不是一个“可选”选项!

开始使用 nMigen

在 nMigen 系统中,我没有尝试立即设计 CPU,而是首先制作了算术逻辑单元 (ALU)。在我见过的所有 CPU 设计中,ALU 都是一个关键组件:它执行算术运算。

为什么从这里开始?我知道我的 CPU 需要 ALU;我知道我可以做一个简单的 ALU;我知道做事的感觉是开始一个新项目的重要动力!

图片[1]-数字逻辑设计设计和RISC-V架构(上文中)-老王博客

我的设计如下所示:

"""Arithmetic Logic Unit"""import enum
import nmigen as nm
class ALUOp(enum.IntEnum):
"""Operations for the ALU"""
 ADD = 0
 SUB = 1
 
 
class ALU(nm.Elaboratable):
"""
 Arithmetic Logic Unit
 * op (in): the opcode
 * a (in): the first operand
 * b (in): the second operand
 * o (out): the output
 """
def __init__(self, width):
"""
 Initialiser
 Args:
 width (int): data width
 """
 self.op = nm.Signal()
 self.a = nm.Signal(width)
 self.b = nm.Signal(width)
 self.o = nm.Signal(width)
def elaborate(self, _):
 m = nm.Module()
with m.Switch(self.op):
with m.Case(ALUOp.ADD):
 m.d.comb += self.o.eq(self.a + self.b)
with m.Case(ALUOp.SUB):
 m.d.comb += self.o.eq(self.a - self.b)
return m

复制代码

如您所见,我们创建了大量 nMigen Signal 实例来很好地表示定义 ALU 接口的信号!但是这个复杂的方法是什么?这是什么精巧的方法?我的理解是“细化”是合成网表的第一步的名称(见上文)。在上面的 nMigen 代码中,想法是创建了一些可详细说明的结构(通过从 nm.Elaboratable 继承),即描述我们想要综合的数字逻辑的东西。详细方法描述了数字逻辑。它必须返回一个 nMigen 模块。

让我们仔细看看详细方法的内容。Switch 将创建某种形式的综合设计决策逻辑。但是什么是 mdcomb?nMigen 提出了同步(mdsync)和组合(mdcomb)控制域的概念。从 nMigen 文档中:

控制域是一组在相同条件下改变其值的命名信号。

所有设计都有一个预定义的组合域,其中包含当用于计算它们的任何值发生变化时发生变化的所有信号。名称 comb 是为组合域保留的。

设计还可以具有任意数量的用户定义同步域,也称为时钟域,它们包含在域时钟信号的特定边沿发生时发生变化的信号,或者,对于具有异步复位能力的域,域的复位信号变化。大多数模块只使用一个同步域。

信号分配在组合域和同步域中表现不同。一般来说,同步域中的信号包含设计状态,而组合域中的信号不形成反馈回路或保持状态。

下面以移位寄存器为例说明所要设计的逻辑。假设移位寄存器有 8 位,该位的值每个时钟周期移位一位(最左边的值来自输入信号)。这必然是同步的:不能通过简单地将位连接在一起来创建此功能,而在 nMigen 中,将位分配到组合字段中将表示此功能。

我将在本博客系列的下一部分详细讨论我的 CPU 设计。现在发生的情况是,我试图在没有流水线的情况下每个周期只停用一条指令——这很不寻常,但我希望这能简化 CPU 的各个方面。结果d触发器是同步还是异步,大多数逻辑是组合的,而不是同步的,因为我几乎不保持时钟周期之间的状态。现在,我的寄存器文件设计有问题,为了解决它,我可能需要重新考虑我的“无流水线”想法。

编写测试

对于 Python 测试,我喜欢使用 pytest,但当然您可以使用任何吸引您的框架。这是我在上面测试的 ALU 代码:

"""ALU tests"""
import nmigen.sim
import pytest
from riscy_boi import alu
@pytest.mark.parametrize( 
"op, a, b, o", [ 
(alu.ALUOp.ADD, 1, 1, 2), 
(alu.ALUOp.ADD, 1, 2, 3), 
(alu.ALUOp.ADD, 2, 1, 3),
(alu.ALUOp.ADD, 258, 203, 461), 
(alu.ALUOp.ADD, 5, 0, 5), 
(alu.ALUOp.ADD, 0, 5, 5), 
(alu.ALUOp.ADD, 2**32 - 1, 1, 0), 
(alu.ALUOp.SUB, 1, 1, 0), 
(alu.ALUOp.SUB, 4942, 0, 4942), 
(alu.ALUOp.SUB, 1, 2, 2**32 - 1)])
def test_alu(comb_sim, op, a, b, o): 
alu_inst = alu.ALU(32)
def testbench():
yield alu_inst.op.eq(op)
yield alu_inst.a.eq(a)
yield alu_inst.b.eq(b)
yield nmigen.sim.Settle()
assert (yield alu_inst.o) == o
 comb_sim(alu_inst, testbench)

复制代码

和我的conftest.py:

"""Test configuration"""
import os
import shutil
import nmigen.sim
import pytest
VCD_TOP_DIR = os.path.join(
 os.path.dirname(os.path.realpath(__file__)),
"tests",
"vcd")
def vcd_path(node):
 directory = os.path.join(VCD_TOP_DIR, node.fspath.basename.split(".")[0])
 os.makedirs(directory, exist_ok=True)
return os.path.join(directory, node.name + ".vcd")
@pytest.fixture(scope="session", autouse=True)
def clear_vcd_directory():
 shutil.rmtree(VCD_TOP_DIR, ignore_errors=True)
@pytest.fixture
def comb_sim(request):
def run(fragment, process):
 sim = nmigen.sim.Simulator(fragment)
 sim.add_process(process)
with sim.write_vcd(vcd_path(request.node)):
 sim.run_until(100e-6)
return run
@pytest.fixture
def sync_sim(request):
def run(fragment, process):
 sim = nmigen.sim.Simulator(fragment)
 sim.add_sync_process(process)
 sim.add_clock(1 / 10e6)
with sim.write_vcd(vcd_path(request.node)):
 sim.run()
return run

复制代码

每个测试都会生成一个 vcd 文件,我可以通过 GTKWave 等波形显示查看该文件以进行调试。您会注意到组合仿真运行固定的时间段是任意小,而同步仿真函数运行的时间段是一定数量的时钟周期。

一个信号由一个测试函数生成,该函数将向模拟器请求其当前值。对于组合逻辑,我们生成完成仿真所需的 nnmigen.sim.Settle()。

对于同步逻辑,也可以在没有参数的情况下启动一个新的时钟周期。

设计一个 CPU

在熟悉了 nMigen 之后,我开始尝试画一个框图来展示我的 CPU。在本系列博客的下一部分中,我将更详细地讨论这个问题,但我会简单地说,我先绘制一条指令所需的逻辑,然后再绘制另一条指令的逻辑,然后找出方法将它们结合起来。这是第一个凌乱的草图:

这个框图步骤对于确定不同组件的接口要求是非常宝贵的,但我不想这样做,直到我开始使用 nMigen 并在此过程中学习数字逻辑设计。修改后的框图如下所示:

请继续关注本博客系列的下一部分,我将深入研究 RISC-V 和 CPU 设计。我想使用第三部分来重新设计我的设计以处理我想要实现的完整指令集 (RV32I)

关于作者:

lochsh 是一名住在英国牛津的软件工程师,在 Perspectum Diagnostics 工作,为医学图像诊断工具编写 C++。之前在 CMR Surgical 工作,在那里他为下一代手术机器人编写了裸金属嵌入式 C。对Rust感兴趣,写过很多Python代码,愿意尝试更多的函数式编程。

原文链接:

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论