其實也不是我願意這樣做,Lattice Diamond 明明支援我手上的 FPGA ,實際上就是找不到我這一塊 ECP5 Versa Board ,IC 型是 LFE5UM-45F ,而 Lattice Diamond 只支援 LEF5U-45F 。
所以只能用之前的 Open Source 開發環境編 Verilog 程式。首先請參照Build Risc-V on Ubuntu Linux設定開發環境
最難的部份是找到 FPGA 的 clock input pin ,每一家 LLM 給的值都是錯的。實際上就是 User Manual 以下這段,不過我找到是從 litex-board 抓出來才知道原來是這一段,給個 table 啊,這文件不行,其它的 LED dip switch 都給了,clock 不給.
An on-board 100 MHz LVDS oscillator is provided for general purpose use. This clock source is connected to differential inputs P3 and P4 and must be used as LVDS inputs to the FPGA. This pin pair also provides optimal interface to the FPGA PLL for customized use.
P3 and P4 pin 利用 LVDS 當 input. 這樣就可以寫 lpf 檔案,只能能抓出 clk ,而且知道FPGA ball number 要跟 verilog 對應。這樣就可以將LED 也抓進來了
而且看起來目前 FPGA 主要就是靠這兩根 pin 做 clk 輸入,100Mhz,如果是當 PCIe device 時也可以從 PCIe 那邊接 PCIe 的 clock 進來

接下來我們就可以編 mapping file ,像是 ecp5evn.lpf 內容如下
LOCATE COMP "clk100" SITE "P3";
IOBUF PORT "clk100" IO_TYPE=LVDS;
LOCATE COMP "rst" SITE "H2";
IOBUF PORT "rst" IO_TYPE=LVCMOS33 PULLMODE=DOWN;
# 將 Verilog 中的 "led" port 定位到 FPGA 的 LED output
LOCATE COMP "led[0]" SITE "F16";
IOBUF PORT "led[0]" IO_TYPE=LVCMOS33;
LOCATE COMP "led[1]" SITE "E17";
IOBUF PORT "led[1]" IO_TYPE=LVCMOS33;
LOCATE COMP "led[2]" SITE "F18";
IOBUF PORT "led[2]" IO_TYPE=LVCMOS33;
LOCATE COMP "led[3]" SITE "F17";
IOBUF PORT "led[3]" IO_TYPE=LVCMOS33;
LOCATE COMP "led[4]" SITE "E18";
IOBUF PORT "led[4]" IO_TYPE=LVCMOS33;
LOCATE COMP "led[5]" SITE "D18";
IOBUF PORT "led[5]" IO_TYPE=LVCMOS33;
LOCATE COMP "led[6]" SITE "D17";
IOBUF PORT "led[6]" IO_TYPE=LVCMOS33;
LOCATE COMP "led[7]" SITE "E16";
IOBUF PORT "led[7]" IO_TYPE=LVCMOS33;
然後寫一個簡單的 verilog 程式驗證我們的 code 是不是對的,像是一顆簡單的 CPU,然後寫了一個簡單的程式計算 1+10
module aCPU (
input clk,
input rst,
output reg [7:0] led, // LED output
output reg [7:0] debug_led // LED output
);
// command
parameter OP_NOP = 4'b0000 ;
parameter OP_LI = 4'b0001 ;
parameter OP_ADD = 4'b0010 ;
parameter OP_ADDI= 4'b0011 ; // copy s_reg + imm [1:0] reg to d_reg
parameter OP_BNER0 = 4'b0100 ;
parameter OP_OUT = 4'b0101 ;
parameter OP_HALT = 4'b0110 ;
reg [3:0] pc;
reg [7:0] register [0:3];
reg [15:0] ir; // ir = 指令暫存
wire [3:0] opcode /* verilator public */ = ir[15:12];
wire [1:0] s_reg = ir[11:10];
wire [1:0] d_reg = ir[9:8];
wire [7:0] value = ir[7:0];
reg [15:0] program_memory [0:15];
// 初始化程式記憶體
initial begin
// 程式: 計算 1 + 10 並輸出
program_memory[0] = {OP_LI, 2'b00, 2'b00, 8'd1}; // LI only load to d_REG. s_reg no work
program_memory[1] = {OP_LI, 2'b01, 2'b01, 8'd11}; //
program_memory[2] = {OP_ADDI, 2'b10, 2'b10, 8'b0000}; // R3 = R2 + IMM
program_memory[3] = {OP_ADD, 2'b00, 2'b00, 8'b0001}; // R2 = R0 + IMM => R0
program_memory[4] = {OP_BNER0, 2'b00, 2'b01,8'b00000010}; // R0 跟 R1 比若不相同跳到 2
program_memory[5] = {OP_OUT, 2'b10, 2'b00, 8'b00000000}; // 輸出 s_reg to serial
program_memory[6] = {OP_HALT, 2'b10, 2'b01, 8'b00000000}; // 停止
end
always @(posedge clk or posedge rst) begin
if (rst) begin
pc <= 0;
register[0] <= 0;
register[1] <= 0;
register[2] <= 0;
register[3] <= 0;
led <= 8'b11111111;
debug_led <= 8'b11111111;
end
else begin
pc <= pc + 1;
ir <= program_memory[pc];
// $display("pc[%d] ir:%x reg[0]:%d reg[1]:%d reg[2]:%d value: %x \n", pc, ir, register[0], register[1], register[2], value);
case (opcode)
OP_NOP: begin
end
OP_LI: begin
register[s_reg] <= value;
end
OP_ADD: begin
register[d_reg] <= register[s_reg] + value;
end
OP_ADDI: begin
register[d_reg] <= register[s_reg] + register[value[1:0]];
end
OP_BNER0: begin
if (register[s_reg] != register[d_reg]) begin
pc <= value[3:0];
end
end
OP_OUT: begin
led <= register[s_reg];
end
OP_HALT: begin
pc <= pc - 1;
end
default: begin
// $display("Error No this command OP_CODE: %x ir: %x ", opcode, ir);
end
endcase
end
end
endmodule
當然為了這個程式就需要寫一個 test bench 檔案,就叫 ${PRJNAME}_tb.cpp
這不僅僅是當 test bench ,後來在弄的過程才知道可以靠 $display 顯示電路內的狀態,比幻想簡單太多了
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "VsCPU.h"
#include "verilated.h"
#include "verilated_vcd_c.h"
int main (int argc, char** argv, char** env){
VerilatedContext* contextp = new VerilatedContext;
contextp->commandArgs(argc, argv);
VsCPU* top = new VsCPU{contextp};
VerilatedVcdC* tfp = new VerilatedVcdC;
contextp->traceEverOn(true); // open trace function
top->trace(tfp, 99);
tfp->open("wave.vcd");
int clk = 0;
int cycles = 0;
while (!contextp->gotFinish() && (cycles < 100) ) {
clk = !clk;
top->clk100 = clk;
top->eval();
tfp->dump(contextp->time()); // dump wave
contextp->timeInc(1); // move time to next clock
if (cycles > 50) {
// 檢查 LED 輸出是否為預期值 (1 + 10 = 11)
if (top->led == (u_int8_t) ~0xc8) {
printf("SUCCESS: 1 + 10 = %d\n", top->led);
break;
}
}
cycles++;
if (cycles >= 99) {
printf("TIMEOUT: Expected result not reached\n");
}
}
//
if (tfp) {
tfp->close();
delete tfp;
}
delete top;
return 0;
}
然後撰寫一個 Makefile ,可以直接 Compile Code 變 bitstream 而且可以上傳到 FPGA 上.
DEVICE = um-45k
PACKAGE = CABGA381
PRJNAME=aCPU
all: ${PRJNAME}.bit
${PRJNAME}.json: ${PRJNAME}.v
yosys -p "synth_ecp5 -json ${PRJNAME}.json" ${PRJNAME}.v
${PRJNAME}_out.config: ${PRJNAME}.json
nextpnr-ecp5 --$(DEVICE) --package $(PACKAGE) --json ${PRJNAME}.json \
--lpf ecp5evn.lpf --textcfg ${PRJNAME}_out.config
${PRJNAME}.bit: ${PRJNAME}_out.config
ecppack --svf ${PRJNAME}.svf ${PRJNAME}_out.config ${PRJNAME}.bit
sim:
verilator -Wall --cc --exe --build --trace ${PRJNAME}.v ${PRJNAME}_tb.cpp
program:
openFPGALoader -b ecp5_evn ${PRJNAME}.bit
clean:
rm -f *.json *.config *.bit *.svf
make sim 之後執行 ./obj_dir/VaCPU 就可以看到程式輸出的結果,果然還是印出來比較簡單 debug

同時也會產生 wave.vcd 這時可以用 gtkwave 看波形

make program 之後就可以燒到 FPGA 板子上啦,輸出的燈號是反向的

發佈留言