使用Verilog搭建一個單週期CPU
阿新 • • 發佈:2020-12-04
# 使用Verilog搭建一個單週期CPU
## 搭建篇
### 總體結構
其實跟使用logisim搭建CPU基本一致,甚至更簡單,因為完全可以照著logisim的電路圖來寫,各個模組和模組間的連線在logisim中非常清楚。唯一改變了的只有GRF和DM要多一個input PC埠,用來display的時候輸出PC值;IFU同理多了一個output PC,用來把PC的值傳給GRF和DM。其他的模組我都是直接對著logisim原封不動地用Verilog重新實現了一遍。目前支援指令集{addu、subu、ori、lw、sw、beq、jal、jr、nop、lui、sb、lb、sh、lh、jalr、addi}。
### IFU
埠如下圖所示,僅多了一個output PC,實現應該是非常簡單的。
![](https://images.cnblogs.com/cnblogs_com/BUAA-YiFei/1892914/o_2012031454222020-11-18_23-10-33.jpg)
但是要注意一點,如果進行了初始化(如下),那麼一定不能用非阻塞賦值,否則你會發現你的IM根本讀不進code.txt裡的內容(非阻塞賦值會在initial後進行賦值0,讀的code.txt又被清成0了,所以啥也讀不到)
```verilog
initial begin
pc = 32'h00003000;
for(i=0;i<1024;i = i + 1) begin
im[i] = 32'h00000000;//正確寫法
//im[i] <= 32'h00000000;//錯誤寫法
end
$readmemh("code.txt",im);
end
```
### GRF
埠多了一個input PC,用來display的時候可以獲取到PC值以輸出。
![](https://images.cnblogs.com/cnblogs_com/BUAA-YiFei/1892914/o_2012031454272020-11-18_23-10-38.jpg)
這個需要注意的是0號暫存器,他不能被寫入,特判一下就可以。也可以和我一樣`reg [31:0] rf[31:1];`,根本沒有0號暫存器自然也寫不了他,然後輸出暫存器值的時候`assign RD1 = (A1 == 0) ? 32'b0 : rf[A1];`,判斷是否為輸出0號暫存器的值。
### EXT
用位拼接寫,非常簡單,符號擴充套件就將最高位複製就可以了。
```verilog
assign ext_imm16 = (EXTOp == 2'b00) ? {{16{1'b0}},imm16[15:0]} :
(EXTOp == 2'b01) ? {{16{imm16[15]}},imm16[15:0]} :
(EXTOp == 2'b10) ? {imm16[15:0],{16{1'b0}}} :
{{16{1'b0}},imm16[15:0]};
```
但是**注意不要在位拼接裡出現沒位數的數**,Verilog會預設成32位,而不是你想象中的1位。錯誤示範如下:
```verilog
assign ext_imm16 = (EXTOp == 2'b00) ? {{16{0}},imm16[15:0]} :
(EXTOp == 2'b01) ? {{16{imm16[15]}},imm16[15:0]} :
(EXTOp == 2'b10) ? {imm16[15:0],{16{0}}} :
{{16{0}},imm16[15:0]};
```
總之寫Verilog的時候養成好習慣吧,數字都加上位數和進位制,防止在奇怪的地方出錯還找不到bug。
### ALU
這個就更簡單了。沒啥寫的就說說上次那個奇偶校驗跳轉的指令吧。
首先Verilog裡是有異或運算的:`^`,這個就是異或,不要課上用`|`和`~`手寫一個異或出來。然後我們知道縮減運算子`^A`就等於`A[31]^A[30]^A[29]^...^A[1]^A[0]`。那麼A中有奇數個1就相當於`^A == 1 `,A中有偶數個1就相當於`^A == 0`,然後就跟beq一樣跳轉就可以了(beq的Zero是`A == B`,這個指令的Zero是`^A`)
### DM
這個也比較簡單,我依然加了lb、sb、lh、sh指令,在Verilog裡寫只需要用位拼接來寫就可以了,比logisim方便不少。
對寫入資料進行處理:(SSel為2'b00即原資料,SSel為2'b01即sb時,SSel為2'b10即sh時)
```verilog
always @(*) begin
if(SSel == 2'b00) begin
WData = WD;
end
else if(SSel == 2'b01) begin
WData = (A[1:0] == 2'b00) ? {dm[A[11:2]][31:8],WD[7:0]} :
(A[1:0] == 2'b01) ? {dm[A[11:2]][31:16],WD[7:0],dm[A[11:2]][7:0]} :
(A[1:0] == 2'b10) ? {dm[A[11:2]][31:24],WD[7:0],dm[A[11:2]][15:0]} :
{WD[7:0],dm[A[11:2]][23:0]};
end
else if(SSel == 2'b10) begin
WData = (A[1] == 1'b0) ? {dm[A[11:2]][31:16],WD[15:0]} : {WD[15:0],dm[A[11:2]][31:16]};
end
end
```
對讀出資料進行處理:(LSel為2'b00即原資料,為2'b01即lb時,2'b10即lh時)
```verilog
always @(*) begin
case(LSel)
2'b00:begin
RD = dm[A[11:2]];
end
2'b01:begin
RD = (A[1:0] == 2'b00) ? {{24{dm[A[11:2]][7]}},dm[A[11:2]][7:0]} :
(A[1:0] == 2'b01) ? {{24{dm[A[11:2]][15]}},dm[A[11:2]][15:8]} :
(A[1:0] == 2'b10) ? {{24{dm[A[11:2]][23]}},dm[A[11:2]][23:16]} :
{{24{dm[A[11:2]][31]}},dm[A[11:2]][31:24]};
end
2'b10:begin
RD = (A[1] == 1'b0) ? {{16{dm[A[11:2]][15]}},dm[A[11:2]][15:0]} : {{16{dm[A[11:2]][31]}},dm[A[11:2]][31:16]};
end
default:RD = 32'h00000000;
endcase
end
```
話說回來我的測評點生成機好像忘記了測lb、sb、lh、sh(逃
### MUX
這個一定不要按高老闆ppt裡的那個寫。我一開始按他的寫然後de了半天才找到原來是MUX的錯誤。
高老闆寫法:
![](https://images.cnblogs.com/cnblogs_com/BUAA-YiFei/1892914/o_20120403330090A89AE5-6DD3-48D0-807E-EAF6007C3BD6.jpeg)
後來改成了三目運算子就AC了。。。現在也沒看懂他的是什麼原理(也可能是對的?
```verilog
assign Out = (S0 == 0 && S1 == 0) ? D0 :
(S0 == 1 && S1 == 0) ? D1 :
(S0 == 0 && S1 == 1) ? D2 :
D3 ;
```
### Controller
先寫巨集定義
```verilog
`define ADDU 6'b100001
`define SUBU 6'b100011
`define ORI 6'b001101
`define LW 6'b100011
`define SW 6'b101011
`define BEQ 6'b000100
`define JAL 6'b000011
`define JR 6'b001000
`define LUI 6'b001111
`define LB 6'b100000
`define SB 6'b101000
`define LH 6'b100001
`define SH 6'b101001
`define RTYPE 6'b000000
`define ADDI 6'b001000
`define JALR 6'b001001
`define J 6'b000010
```
之後每一個指令都用一個wire表示,注意用巨集定義加`,以及R型指令是Rtype與funct的與。
```verilog
wire addu,subu,ori,lw,sw,beq,jal,jr,lui,lb,sb,lh,sh,addi,jalr,j;
assign RType = (opcode == `RTYPE);
assign addu = RType&(funct == `ADDU);
assign subu = RType&(funct == `SUBU);
assign ori = (opcode == `ORI );
assign lw = (opcode == `LW);
assign sw = (opcode == `SW);
assign beq = (opcode == `BEQ);
assign jal = (opcode == `JAL);
assign jr = RType&(funct == `JR);
assign lui = (opcode == `LUI);
assign lb = (opcode == `LB);
assign sb = (opcode == `SB);
assign sh = (opcode == `SH);
assign lh = (opcode == `LH);
assign addi = (opcode == `ADDI);
assign jalr = RType&(funct == `JALR);
assign j = (opcode == `J);
```
之後對每個控制訊號根據真值表加指令,兩位的和一位的各舉了一個例子。
```verilog
assign NPCOp[0] = beq | jr | jalr;
assign NPCOp[1] = jal | jr | jalr | j;
assign RFWr = addu | subu | ori | lw | jal | lui | lb | lh | addi | jalr ;
```
### datapath
這個和Controller作為mips的子模組,datapath用來把所有除了controller的模組連線起來,然後在mips裡與controller連線。
結構圖如下
![image-20201204114817321](https://images.cnblogs.com/cnblogs_com/BUAA-YiFei/1892914/o_201204034833image-20201204114817321.png)
## 加指令篇
跟P3一樣分析即可。eg:加addi(不考慮溢位)
### 分析資料通路
判斷是否需要增加新的通路以實現該指令,如ALU是否要增加計算功能之類的。addi不需要因此直接改控制訊號即可。
![](https://images.cnblogs.com/cnblogs_com/BUAA-YiFei/1892914/o_201204035712image-20201127111601031.png)
### 確定控制訊號
對於NPCOp,這不是一個跳轉指令,因此NPCOp取00
對於RFWr,要回寫到R[rt],因此RFWr為1
對於EXTOp,要進行符號擴充套件,所以取01
對於ALUOp,加法,所以取00
對於DMWr,不用寫入DM,所以取0
對於WRSel,由於寫入的是R[rt],所以取01
對於WDSel,由於寫入的資料來自ALU的計算結果,所以取00
對於BSel,由於參與ALU計算的第二個數來自EXT,所以取1
對於SSel和LSel,由於不涉及半字或位元組,都取00
### 新增指令訊號
先定義ADDI
```verilog
`define ADDI 6'b001000
```
再新增wire addi
```verilog
assign addi = (opcode == `ADDI);
```
### 修改控制訊號
在addi控制訊號為1的地方加上addi。
如RFWr為1,則在` assign RFWr = addu | subu | ori | lw | jal | lui | lb | lh ;`最後或addi。
即變成` assign RFWr = addu | subu | ori | lw | jal | lui | lb | lh | addi;`
其他控制訊號依次新增即可,加完所有控制訊號後,addi的新增