我在數(shù)字邏輯設(shè)計(jì)方面并沒有經(jīng)驗(yàn)。也就是說,直到最近我才決定嘗試設(shè)計(jì)自己的 CPU,并在 FPGA 上運(yùn)行!如果你也是一名軟件工程師,并對硬件設(shè)計(jì)有興趣,那么我希望這一系列關(guān)于我所學(xué)到的知識的文章能夠?qū)δ阌兴鶐椭?,并讓你感到有趣?/p>
什么是數(shù)字邏輯設(shè)計(jì)?
數(shù)字邏輯設(shè)計(jì)就是設(shè)計(jì)一個邏輯電路,對二進(jìn)制數(shù)值進(jìn)行運(yùn)算?;驹沁壿嬮T:例如,與門一樣,有兩個輸入和一個輸出。它的輸出為 1 或 iff,兩個輸入均為 1。
我們所設(shè)計(jì)的同步電路,一般都是利用觸發(fā)器來儲存狀態(tài),使電路運(yùn)行與共時鐘同步。觸發(fā)器由邏輯門組成。
模擬電路設(shè)計(jì)包括構(gòu)成邏輯門的電子元件,例如晶體管和二極管。這種抽象通常是用于直接處理來自模擬傳感器的信號的應(yīng)用,例如無線電接收器。在設(shè)計(jì) CPU 時,這種抽象水平是行不通的:現(xiàn)代的 CPU 有幾十億個晶體管!
相反,我們使用的工具可以將數(shù)字邏輯設(shè)計(jì)轉(zhuǎn)化為不同的有用格式:FPGA 的配置(見下文);模擬;晶片布局。
FPGA 是什么,為什么要用 FPGA?
上文中我們指出,不管我們是創(chuàng)建自定義 ASIC 芯片還是配置 FPGA,都可以使用相同的數(shù)字邏輯設(shè)計(jì)工具?,F(xiàn)場可編程門陣列(Field-Programmable Gate Array,F(xiàn)PGA)是一種集成集成電路,其中包含了可編程邏輯塊陣列。你可以把它想象成一個大型的邏輯門陣列,可以通過多種方式連接起來。
定制一款芯片動輒需要幾百萬美元,當(dāng)然,一旦芯片被生產(chǎn)出來,就無法對它進(jìn)行更改。所以 FPGA 通常用于下列情況:
由于缺乏資金,無法負(fù)擔(dān)制作定制 ASIC 的費(fèi)用(例如,如果你只是像我這樣的黑客,而不是 ARM 或英特爾)。
無法負(fù)擔(dān)制作定制 ASIC 的費(fèi)用,因?yàn)楫a(chǎn)量太低,不值得一次性支付高昂的費(fèi)用 (例如,如果你正在使用定制的數(shù)據(jù)采集硬件生產(chǎn)少量的 MRI 機(jī)器)。
需要靈活性。
缺點(diǎn)是什么?那就是 FPGA 的單芯片成本要高得多,并且由于它能夠以非常靈活的方式將邏輯塊連接在一起,因此速度通常要慢得多。與此相反,定制的設(shè)計(jì)可以減少晶體管的數(shù)量,而無需考慮靈活性。
在我看來,比較 ASIC 的定制設(shè)計(jì)過程和 FPGA 的設(shè)計(jì)過程是很有幫助的:
邏輯設(shè)計(jì):就像做 FPGA 一樣,ASIC 的邏輯設(shè)計(jì)也是用硬件描述語言來完成的。
驗(yàn)證:FPGA 設(shè)計(jì)可能會被驗(yàn)證,但是可以期待 ASIC!設(shè)計(jì)的過程更嚴(yán)格了。畢竟,設(shè)計(jì)一旦制造出來就不能更改!驗(yàn)證通常包括設(shè)計(jì)部分的正式驗(yàn)證。
合成:這將創(chuàng)建一個網(wǎng)表,一個邏輯塊及其連接的列表。連接被稱為網(wǎng),而塊被稱為單元。對于 FPGA 和 ASIC 來說,單元是特定于廠商的。
布局布線(Placement and routing,P&R):對于 FPGA 來說,它涉及到將網(wǎng)表中描述的邏輯塊映射到 FPGA 中的實(shí)際塊。由此產(chǎn)生的二進(jìn)制通常稱為比特流。對于 ASIC 來說,這涉及到?jīng)Q定在晶片上何處放置單元,以及如何將它們連接起來。這兩種應(yīng)用通常都要使用自動優(yōu)化工具。
我需要什么工具? 硬件描述語言:我使用的是 nMigen
你可能聽說過 Verilog 或 VHDL:這兩種流行的硬件描述語言(hardware description language,HDL)。這里我所說的“流行”,是指廣泛使用,而非廣受歡迎。
我不會假裝對這些工具很了解。我只知道那些比我更聰明的人,有著豐富的邏輯設(shè)計(jì)經(jīng)驗(yàn),卻對這些工具恨之入骨。由于 Verilog 和其他類似工具存在的問題,人們嘗試著開發(fā)出更有用、更友好的替代方法。就是在 Python 中創(chuàng)建一 門領(lǐng)域?qū)S谜Z言的項(xiàng)目。用它自己的話就是:
雖然用 Verilog 和 VHDL 進(jìn)行硬件設(shè)計(jì)比輸入原理圖的速度要快,但是由于一些原因,硬件設(shè)計(jì)還是很枯燥,而且效率也不高。對目前邏輯設(shè)計(jì)中占有重要地位的同步電路而言,事件驅(qū)動模型引入了不必要的問題,并引入了人工編碼。逆直覺的算術(shù)規(guī)則導(dǎo)致了更陡峭的學(xué)習(xí)曲線,并為設(shè)計(jì)上的微小缺陷提供了溫床。最后,通過“generate”語句來支持邏輯過程生成(元編程)非常有限,并且限制了代碼的通用、重用和組織方式。
針對這些問題,我們開發(fā)了 nMigen FHDL,該庫取代了事件驅(qū)動范例,它采用了組合語句和同步語句的概念,并采用了算術(shù)規(guī)則,使整型始終像數(shù)學(xué)整型一樣,最重要的是允許 Python 程序構(gòu)建所設(shè)計(jì)的邏輯。這一點(diǎn)使硬件設(shè)計(jì)人員能夠充分利用 Python 語言的豐富內(nèi)容:面向?qū)ο缶幊?、函?shù)參數(shù)、生成器、操作符重載、庫等,構(gòu)建組織良好、可重用的優(yōu)雅設(shè)計(jì)。
假如你和我一樣,從未使用過 Verilog,那么這些對你來說不僅僅是抽象的含義。但是聽起來確實(shí)很有前景,而且我可以證明,在沒有 Verilog 障礙的情況下,從邏輯設(shè)計(jì)開始就非常簡單。如果你對 Python 非常熟悉,我將推薦它!
我能想到的唯一缺點(diǎn)是,nMigen 仍然處于開發(fā)階段,特別是文檔還不完整。但你可以通過 chat.freenode.net 的 #nmigen 頻道找到有用的社區(qū)。
用于檢查模擬的波形顯示器:我使用的是 GTKWave
nMigen 提供了模擬工具。我將它用于用 pytest 編寫的測試。為了幫助調(diào)試,我記錄了這些測試中的信號,并在波形顯示器中觀察它們。
FPGA 開發(fā)板:我使用的是 myStorm BlackIce II
你不必使用 FPGA 開發(fā)板來創(chuàng)建自己的 CPU。在模擬中,你可以做任何事情。對于我來說,工作中使用板子的樂趣就是能閃爍 LED,看著自己的設(shè)計(jì)運(yùn)行。
當(dāng)然,如果你要創(chuàng)建的東西比我的最基本的 CPU 更有用,那么你可能需要一些硬件來運(yùn)行它,而這并非“可選”選項(xiàng)!
開始使用 nMigen
在 nMigen 系統(tǒng)中,我并沒有立刻嘗試設(shè)計(jì)一個 CPU,而是首先制作一個算術(shù)邏輯單元(Arithmetic Logic Unit ,ALU)。在我見過的所有 CPU 設(shè)計(jì)中, ALU 是一個關(guān)鍵部件:它執(zhí)行算術(shù)運(yùn)算。
為什么要從這里開始呢?我知道我的 CPU 需要一個 ALU;我知道我能做一個簡單的 ALU;我知道當(dāng)開始一個新的項(xiàng)目時,做事情的感覺是一種重要的動力!
我的設(shè)計(jì)看起來像這樣:
"""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
正如你所看到的,我們已經(jīng)創(chuàng)建了大量的 nMigen Signal 實(shí)例,以很好地表示定義 ALU 接口的信號!但這個復(fù)雜的方法是什么呢?這個 elaborate 方法又是什么呢?我的理解是,“elaboration”是合成網(wǎng)表的第一步的名稱(見上文)。在上面的 nMigen 代碼中,我們的想法是,已經(jīng)創(chuàng)建了一些可闡述的結(jié)構(gòu)(通過繼承 nm.Elaboratable),也就是用來描述想要合成的數(shù)字邏輯的東西。這個 elaborate 方法描述了數(shù)字邏輯。它必須返回一個 nMigen 模塊。
下面讓我們進(jìn)一步了解一下 elaborate 的方法的內(nèi)容。Switch 將創(chuàng)造某種形式的合成設(shè)計(jì)決策邏輯。但什么是 m.d.comb 呢?nMigen 提出了同步(m.d.sync)和組合(m.d.comb)控制域的概念。:
控制域是指在相同條件下改變其值的一組命名信號。
所有的設(shè)計(jì)都有一個預(yù)定義的組合域,其中包含所有的信號,當(dāng)用來計(jì)算這些信號的任何值發(fā)生變化時,這些信號也隨之發(fā)生變化。名稱 comb 是為組合域保留的。
一種設(shè)計(jì)還可以有任意數(shù)量的用戶定義的同步域,也稱為時鐘域,其中包含的信號在域的時鐘信號出現(xiàn)特定邊緣時會發(fā)生變化,或者,對于具有異步復(fù)位功能的域,域的復(fù)位信號會發(fā)生變化。大多數(shù)模塊只使用一個同步域。
在組合域和同步域中,信號的賦值的行為各不相同??偟膩碚f,同步域中的信號包含了設(shè)計(jì)的狀態(tài),而組合域中的信號并不能形成反饋回路或維持狀態(tài)。
下面以移位寄存器為例,說明要設(shè)計(jì)的邏輯。假定移位寄存器有 8 位,每個時鐘周期,該位值都會有一個移位(最左邊的值來自輸入信號)。這必然是同步的:不能通過簡單地將位連接在一起來創(chuàng)建這個功能,而在 nMigen 中,將位分配到組合域中將代表此功能。
我將在這個系列博客的下一部分詳細(xì)討論我的 CPU 設(shè)計(jì)。現(xiàn)在的情況是,我試圖在每個周期中只停用一個指令,而不使用流水線——這很不尋常,但是我希望這樣做可以簡化 CPU 的各個方面。其結(jié)果是,大多數(shù)邏輯是組合的,而非同步的,因?yàn)槲規(guī)缀鯖]有在時鐘周期之間維持這種狀態(tài)?,F(xiàn)在,我的寄存器文件設(shè)計(jì)有問題,為了解決這個問題,我可能需要重新考慮我的“無流水線”想法。
編寫測試
對于 Python 測試,我喜歡使用 pytest,當(dāng)然你也可以使用任何能吸引你的框架。以下是我在上面測試的 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 等波形顯示器來查看,以便調(diào)試。你會注意到,組合模擬固定運(yùn)行的時間段是任意小的,而同步模擬功能運(yùn)行的時間段是確定的時鐘周期數(shù)。
一個信號產(chǎn)生于一個測試函數(shù),它將從模擬器請求它的當(dāng)前值。對于組合邏輯,我們生成 nnmigen.sim.Settle() ,要求完成模擬。
對于同步邏輯,還可以開始新的時鐘周期,而不需要參數(shù)。
設(shè)計(jì)一個 CPU
在熟悉了 nMigen 之后,我開始嘗試?yán)L制一個框圖來顯示我的 CPU。在本系列博客的下一部分中,我將對這個問題進(jìn)行更詳細(xì)的討論,但我將簡單地說,我先繪制出一個指令所需要的邏輯,然后繪制出另一個指令的邏輯,然后找到如何將它們結(jié)合起來的方法。這里有第一個混亂的草圖:
在弄清楚不同元件的接口要求是什么時,這個框圖步驟非常有價值,但是在開始使用 nMigen 和在這個過程中學(xué)習(xí)數(shù)字邏輯設(shè)計(jì)之前,我不想這么做。修改后的框圖如下所示:
審核編輯:劉清
-
晶體管
+關(guān)注
關(guān)注
77文章
9682瀏覽量
138079 -
數(shù)字邏輯
+關(guān)注
關(guān)注
0文章
73瀏覽量
16645 -
RISC-V
+關(guān)注
關(guān)注
45文章
2270瀏覽量
46125
原文標(biāo)題:設(shè)計(jì)一個 RISC-V CPU,第 1 部分:軟件工程師如何學(xué)習(xí)硬件設(shè)計(jì)
文章出處:【微信號:FPGA研究院,微信公眾號:FPGA研究院】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論