Linux, 工作, 生活, 家人

IC Design, RiscV

Run verilog Program on Lattice ECP5 Versa with OpenSource Tools

其實也不是我願意這樣做,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 板子上啦,輸出的燈號是反向的

發佈留言