在线设计模仿平台StepFPGA应用实践

没有硬件设备下进行FPGA开发,在线设计模仿平台StepFPGA,即小脚丫,可以展开图形化设计和Verilog编程及其逻辑综合、管脚分配、FPGA映射、编程文件下载、波形仿真,对于学习和提升集成电路设计,是不错的选择。近期高校教培,本人实践和互动教学了一系列项目:译码/计数器、交通灯信号控制、UART通信、RISC-V片上系统,现在沉淀下来,供参考。其中,快速构造具体项目及其纠错时,借力了人工智能AI工具--IMA-Copilot-DS与MuleRun。美中不足:小脚丫平台,图形化设计不能仿真,可以针对的FPGA芯片虽然经典但不主流。

1 四-十六译码器

1.1 方案规划

1.2 项目创建

1.3 图形化设计

1.4 仿真测试

编制仿真测试文件,进行测试,如下图所示。在线StepFPGA系统,失于维护,不能有效完成仿真,此外略去仿真及其波形展示。仿真测试文件如下文本框所示。


`timescale 1ns / 1ps

module decoder_tb;

reg 3:0 d; // 4位地址输入

wire 15:0 o; // 16位输出低有效]

// 图形化设计的顶层模块名需与项目一致,实际为main

main uut ( .d (d), .o (o) );

initial begin

d = 4'd0; // 初始化

#100;

repeat(16) begin // 使能后遍历所有地址

#50;

d = d + 1;

end

d = 4'd0; // 禁用后验证输出全为1

#200;

$finish;

end

endmodule

1.5 综合与下载

展开一系列过程:逻辑综合à管脚分配àGPGA映射à文件下载。其中管脚分配过程如下图所示。

2 二十位计数器

2.1 方案规划

2.2 图形化设计

2.3 仿真测试

编制仿真测试文件,进行测试。在线StepFPGA系统,失于维护,不能有效完成仿真,此外略去仿真及其波形展示。仿真测试文件如下文本框所示。
`timescale 1ns / 1ps

module counter20_tb;

reg clk; reg rst_n;

wire 19:0 o;

main uut ( .clk (clk), .rst_n (rst_n), .o (o) );

initial begin

clk = 1'b0;

forever #5 clk = ~clk;

end

initial begin

rst_n = 1'b0;

#100;

rst_n = 1'b1;

// 20位计数器满量程约100万周期,仿真只观察部分

#50000;

$finish;

end

endmodule

3 简易交通灯

3.1 方案规划

实现一个"十字路口红黄绿交通灯",用两个RGB三色LED分别代表主路和支路。

|--------|--------|--------|----------|
| 状态 | 主路 | 支路 | 持续时间 |
| S1 | 🟢 绿灯 | 🔴 红灯 | 15 秒 |
| S2 | 🟡 黄灯 | 🔴 红灯 | 3 秒 |
| S3 | 🔴 红灯 | 🟢 绿灯 | 7 秒 |
| S4 | 🔴 红灯 | 🟡 黄灯 | 3 秒 |

状态转移:S1 → S2 → S3 → S4 → S1 → ...(倒计时归零触发跳转)

RGB LED编码:

|------------|
| out5:0 |

小脚丫 RGB LED 为 低电平点亮,6 位输出out5:0

|--------|--------|--------------|--------------|----------------|
| 状态 | 颜色 | 主路 R,G,B | 支路 R,G,B | out5:0 |
| S1 | 主绿支红 | 1,0,1 | 0,1,1 | 101_011 |
| S2 | 主黄支红 | 0,0,1 | 0,1,1 | 001_011 |
| S3 | 主红支绿 | 0,1,1 | 1,0,1 | 011_101 |
| S4 | 主红支黄 | 0,1,1 | 1,0,0 | 011_100 |

黄灯 = R+G 同时点亮(0,0,1 中 R=0 点红 + G=0 点绿 → 黄色混合)

3.2 项目创建

3.3 编码设计

新建两个编程文件:devide.v和traffic.v,编码如下文本框所示。设置顶层文件为traffic.v。

devide.v:

cpp 复制代码
module divide(
    input  wire clk,
    input  wire rst_n,
    output wire clkout
);
parameter WIDTH = 3; parameter N = 5;
reg [WIDTH-1:0] cnt_p, cnt_n;
reg             clk_p, clk_n;
assign clkout = (N == 1) ? clk : (N[0]) ? (clk_p & clk_n) : clk_p;
// 上升沿计数
always @(posedge clk) begin
    if (!rst_n)       cnt_p <= 0;
    else if (cnt_p == N - 1) cnt_p <= 0;
    else              cnt_p <= cnt_p + 1;
end
// 下降沿计数
always @(negedge clk) begin
    if (!rst_n)       cnt_n <= 0;
    else if (cnt_n == N - 1) cnt_n <= 0;
    else              cnt_n <= cnt_n + 1;
end
// 上升沿分频输出
always @(posedge clk) begin
    if (!rst_n)             clk_p <= 0;
    else if (cnt_p < (N >> 1)) clk_p <= 0;
    else                    clk_p <= 1;
end
// 下降沿分频输出
always @(negedge clk) begin
    if (!rst_n)             clk_n <= 0;
    else if (cnt_n < (N >> 1)) clk_n <= 0;
    else                    clk_n <= 1;
end
endmodule

traffic.v:

cpp 复制代码
module traffic(
    input  wire       clk,      // 12MHz 系统时钟
    input  wire       rst_n,    // 复位(低有效)
    output reg  [5:0] out       // {主R,主G,主B, 支R,支G,支B}
);
// 状态编码
parameter S1 = 2'b00,   // 主绿支红
          S2 = 2'b01,  // 主黄支红
          S3 = 2'b10,  // 主红支绿
          S4 = 2'b11;  // 主红支黄
// 各状态持续时间(秒)
parameter time_s1 = 8'd15,
          time_s2 = 8'd3,
          time_s3 = 8'd7,
          time_s4 = 8'd3;
// 各状态 LED 编码(低电平点亮)
parameter led_s1 = 6'b101_011,  // 主绿支红
          led_s2 = 6'b001_011,  // 主黄支红
          led_s3 = 6'b011_101,  // 主红支绿
          led_s4 = 6'b011_100;  // 主红支黄
reg [7:0]  timecont;            // 倒计时计数器
reg [1:0]  cur_state, next_state;  // 现态、次态
wire       clk1h;               // 1Hz 时钟
// 例化分频器:12MHz → 1Hz
divide #(.WIDTH(32), .N(12000000)) CLK1H (
    .clk(clk),
    .rst_n(rst_n),
    .clkout(clk1h)
);
// ============================================
// 第一段:同步时序 --- 状态寄存(现态 ← 次态)
// ============================================
always @(posedge clk1h or negedge rst_n) begin
    if (!rst_n)
        cur_state <= S1;
    else
        cur_state <= next_state;
end
// ============================================
// 第二段:组合逻辑 --- 状态跳转条件
// ============================================
always @(*) begin
    case (cur_state)
        S1: next_state = (timecont == 1) ? S2 : S1;
        S2: next_state = (timecont == 1) ? S3 : S2;
        S3: next_state = (timecont == 1) ? S4 : S3;
        S4: next_state = (timecont == 1) ? S1 : S4;
        default: next_state = S1;
    endcase
end
// ============================================
// 第三段:同步时序 --- 输出动作 + 倒计时
// ============================================
always @(posedge clk1h or negedge rst_n) begin
    if (!rst_n) begin
        out      <= led_s1;
        timecont <= time_s1;
    end else begin
        case (next_state)
            S1: begin
                out <= led_s1;
                if (timecont == 1)
                    timecont <= time_s1;
                else
                    timecont <= timecont - 1;
            end
            S2: begin
                out <= led_s2;
                if (timecont == 1)
                    timecont <= time_s2;
                else
                    timecont <= timecont - 1;
            end
            S3: begin
                out <= led_s3;
                if (timecont == 1)
                    timecont <= time_s3;
                else
                    timecont <= timecont - 1;
            end
            S4: begin
                out <= led_s4;
                if (timecont == 1)
                    timecont <= time_s4;
                else
                    timecont <= timecont - 1;
            end
            default: begin
                out      <= led_s1;
                timecont <= time_s1;
            end
        endcase
    end
end
endmodule

3.4 仿真测试

编制仿真测试文件,进行测试,运行波形如下图所示。仿真测试文件如下文本框所示。由于12MHz分频到1Hz 需要12000000个时钟周期,仿真非常慢。仿真前临时修改 traffic.v 中的分频参数:
// 实际下载到板卡时用:

divide #(.WIDTH(32), .N(12000000)) CLK1H (...)

// 仿真时临时改为(仿真完记得改回来):

divide #(.WIDTH(5), .N(10)) CLK1H (...)

这样1Hz变成约1.2MHz,只需10个时钟周期就翻转一次,仿真几微秒就能看到完整的状态切换。

cpp 复制代码
`timescale 1ns / 1ps
module traffic_tb;
reg  clk, rst_n;
wire [5:0] out;
// 例化交通灯(仿真用极小分频值,加速仿真)
traffic uut ( .clk(clk), .rst_n(rst_n), .out(out) );
// 生成 12MHz 时钟(周期 ≈ 83.3ns)
initial begin
    clk = 0;
    forever #42 clk = ~clk;
end
// 复位与仿真流程
initial begin
    rst_n = 0;
    #200;
    rst_n = 1;
    // 运行足够长时间
    // 实际 12MHz/12000000 = 1Hz,完整一轮需 28 秒
    // 仿真中 28s × 12000000 ≈ 3.36×10^8 个时钟周期
    // 仿真时间很长,建议缩小分频值
    #500000;
    $display("=== Traffic light simulation complete ===");
    $finish;
end
endmodule

4 UART通信监视

4.1 方案规划

4.2 编码设计

新建五个编程文件:

1 波特率节拍baud.v

cpp 复制代码
module baud #(
    parameter BPS_PARA = 1250   // 12MHz / 9600 = 1250
)(
    input      clk,              // 系统时钟 12MHz
    input      rst_n,            // 复位(低有效)
    input      bps_en,           // 接收使能
    output reg bps_clk           // 采样节拍脉冲(每位中点产生1个脉冲)
);
reg [12:0] cnt;
// 计数器:0 ~ BPS_PARA-1 循环
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 13'd0;
    else if ((cnt >= BPS_PARA - 1) || (!bps_en))
        cnt <= 13'd0;
    else
        cnt <= cnt + 1'b1;
end
// 在计数中点产生采样脉冲(此时数据最稳定)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        bps_clk <= 1'b0;
    else if (cnt == (BPS_PARA >> 1))  // 中点
        bps_clk <= 1'b1;
    else
        bps_clk <= 1'b0;
end
endmodule

2 接收uart_rx.v

cpp 复制代码
module uart_rx #(
    parameter BPS_PARA = 1250
)(
    input            clk,
    input            rst_n,
    input            uart_rx,          // 串口接收线(CP2102 TXD → FPGA RX)
    output reg [7:0] rx_data_out,         // 接收到的8位数据
    output reg       rx_data_valid      // 数据有效脉冲(1个时钟周期宽)
);
// ====== 亚稳态消除:打三拍 + 下降沿检测 ======
reg uart_rx0, uart_rx1, uart_rx2;
wire neg_uart_rx = uart_rx2 & ~uart_rx1;  // 起始位下降沿
always @(posedge clk) begin
    uart_rx0 <= uart_rx;
    uart_rx1 <= uart_rx0;
    uart_rx2 <= uart_rx1;
end
// ====== 接收使能控制 ======
reg       bps_en;
reg [3:0] num;        // 0~9: 0=等待, 1~8=数据位, 9=完成
reg [7:0] rx_data;
wire      bps_clk;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        bps_en <= 1'b0;
    else if (neg_uart_rx && (!bps_en))    // 检测到起始位且未在接收
        bps_en <= 1'b1;
    else if (num == 4'd9)               // 接收完成
        bps_en <= 1'b0;
end
// ====== 数据采样:在每个波特率中点采样 ======
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        num     <= 4'd0;
        rx_data <= 8'd0;
    end else if (bps_en) begin
        if (bps_clk) begin
            num <= num + 1'b1;
            if (num >= 1 && num <= 8)
                rx_data[num-1] <= uart_rx1;  // 采样当前位
        end else if (num == 4'd9) begin
            num <= 4'd0;
        end
    end else begin
        num <= 4'd0;
    end
end
// ====== 数据输出 ======
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_data_out   <= 8'd0;
        rx_data_valid <= 1'b0;
    end else if (num == 4'd9) begin
        rx_data_out   <= rx_data;
        rx_data_valid <= 1'b1;
    end else begin
        rx_data_valid <= 1'b0;
    end
end
// 例化波特率节拍模块
baud #(BPS_PARA) u_baud(
    .clk    (clk),
    .rst_n  (rst_n),
    .bps_en (bps_en),
    .bps_clk(bps_clk)
);
endmodule

3 数据解码decoder.v

cpp 复制代码
module decoder(
    input            clk,
    input            rst_n,
    input      [7:0] rx_data_out,
    input            rx_data_valid,
    output reg [31:0] seg_data,    // 8位数码管数据(每4bit一个BCD)
    output reg [7:0]  data_en      // 每位数码管使能
);
// ====== ASCII 模式(串口助手选 ASCII 发送)======
// PC 发送字符 '0'~'9',ASCII 码为 48~57,减去 48 得到数值
wire [3:0] bcd_val = rx_data_out[3:0];  // 取低4位作为BCD
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        seg_data <= 32'd0;
        data_en  <= 8'd0;
    end else if (rx_data_valid) begin
        // ASCII 模式:减48得到数值,左移推入
        if (rx_data_out >= 8'd48 && rx_data_out <= 8'd57) begin
            seg_data <= {seg_data[27:0], rx_data_out[3:0]};
            data_en  <= {data_en[6:0], 1'b1};
        end
        // Hex 模式:直接取数值,每2个hex占1个数码管位
        // 如需 Hex 模式,取消下方注释,注释上方 ASCII 逻辑
        // seg_data <= {seg_data[23:0], rx_data_out};
        // data_en  <= {data_en[5:0], 2'b11};
    end
end
endmodule

4 数码管扫描sesgmentscan.v

cpp 复制代码
module segmentscan(
    input         clk,
    input         rst_n,
    input    [31:0] seg_data,
    input    [7:0]  data_en,
    output reg [7:0]  seg_sel,     // 位选(高有效,哪一位亮)
    output reg [7:0]  seg_db      // 段选(共阳极,低电平点亮段)
);
// ====== 扫描计数器(约 1kHz 刷新)======
reg [15:0] cnt;
reg [2:0]  scan_idx;     // 0~7,当前扫描第几位
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        cnt <= 16'd0;
    else
        cnt <= cnt + 1'b1;
end
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        scan_idx <= 3'd0;
    else if (cnt == 16'd0)
        scan_idx <= scan_idx + 1'b1;
end
// ====== 位选信号 ======
always @(*) begin
    seg_sel = 8'd0;
    if (data_en[scan_idx])
        seg_sel[scan_idx] = 1'b1;
end
// ====== 当前位 BCD 值 ======
reg [3:0] bcd;
always @(*) begin
    case (scan_idx)
        3'd0: bcd = seg_data[3:0];
        3'd1: bcd = seg_data[7:4];
        3'd2: bcd = seg_data[11:8];
        3'd3: bcd = seg_data[15:12];
        3'd4: bcd = seg_data[19:16];
        3'd5: bcd = seg_data[23:20];
        3'd6: bcd = seg_data[27:24];
        3'd7: bcd = seg_data[31:28];
        default: bcd = 4'd0;
    endcase
end
// ====== BCD → 七段码(共阳极,低电平点亮)======
always @(*) begin
    case (bcd)
        4'd0: seg_db = 8'hC0;  // 0
        4'd1: seg_db = 8'hF9;  // 1
        4'd2: seg_db = 8'hA4;  // 2
        4'd3: seg_db = 8'hB0;  // 3
        4'd4: seg_db = 8'h99;  // 4
        4'd5: seg_db = 8'h92;  // 5
        4'd6: seg_db = 8'h82;  // 6
        4'd7: seg_db = 8'hF8;  // 7
        4'd8: seg_db = 8'h80;  // 8
        4'd9: seg_db = 8'h90;  // 9
        4'hA: seg_db = 8'h88;  // A
        4'hB: seg_db = 8'h83;  // B
        4'hC: seg_db = 8'hC6;  // C
        4'hD: seg_db = 8'hA1;  // D
        4'hE: seg_db = 8'h86;  // E
        4'hF: seg_db = 8'h8E;  // F
        default: seg_db = 8'hFF; // 熄灭
    endcase
end
endmodule

5 顶层显示display_ctl.v

cpp 复制代码
module display_ctl(
    input            clk,        // 12MHz 系统时钟
    input            rst_n,      // 复位(低有效)
    input            uart_rx,    // 串口接收线
    output     [7:0] seg_sel,     // 数码管位选
    output     [7:0] seg_db      // 数码管段选
);
wire [7:0]  rx_data_out;
wire        rx_data_valid;
wire [31:0] seg_data;
wire [7:0]  data_en;
// 例化 UART 接收模块
uart_rx #(.BPS_PARA(1250)) u_rx(   // 9600 baud
    .clk          (clk),
    .rst_n        (rst_n),
    .uart_rx      (uart_rx),
    .rx_data_out  (rx_data_out),
    .rx_data_valid(rx_data_valid)
);
// 例化数据解码模块
decoder u_dec(
    .clk          (clk),
    .rst_n        (rst_n),
    .rx_data_out  (rx_data_out),
    .rx_data_valid(rx_data_valid),
    .seg_data     (seg_data),
    .data_en      (data_en)
);
// 例化数码管扫描模块
segmentscan u_seg(
    .clk     (clk),
    .rst_n   (rst_n),
    .seg_data(seg_data),
    .data_en (data_en),
    .seg_sel (seg_sel),
    .seg_db  (seg_db)
);
endmodule

4.3 仿真测试

编制仿真测试文件,进行测试,运行波形如下图所示。仿真测试文件如下文本框所示。

cpp 复制代码
`timescale 1ns / 1ps
module uart_monitor_tb;
reg  clk, rst_n;
reg  uart_rx;
wire [7:0] seg_sel;
wire [7:0] seg_db;
// 例化顶层
display_ctl uut(
    .clk    (clk),
    .rst_n  (rst_n),
    .uart_rx(uart_rx),
    .seg_sel(seg_sel),
    .seg_db (seg_db)
);
// 12MHz 时钟生成
initial begin
    clk = 0;
    forever #42 clk = ~clk;    // 周期 ≈ 84ns → ~12MHz
end
// ====== UART 发送任务 ======
// 模拟 PC 串口发送一个字节:起始位(0) + 8位数据(低位在前) + 停止位(1)
task uart_send_byte;
    input [7:0] data;
    integer i;
    begin
        // 起始位
        uart_rx = 0;
        #104166;    // 1/9600 ≈ 104166ns
        // 数据位(低位在前)
        for (i = 0; i < 8; i = i + 1) begin
            uart_rx = data[i];
            #104166;
        end
        // 停止位
        uart_rx = 1;
        #104166;
    end
endtask
// ====== 主仿真流程 ======
initial begin
    // 初始化
    rst_n   = 0;
    uart_rx = 1;    // 空闲状态为高
    #500;
    rst_n = 1;
    #1000;
    // 发送字符 '5' (ASCII 0x35 = 53)
    $display("[%0t] Sending '5' (0x35)...", $time);
    uart_send_byte(8'h35);
    #500000;
    // 发送字符 '8' (ASCII 0x38 = 56)
    $display("[%0t] Sending '8' (0x38)...", $time);
    uart_send_byte(8'h38);
    #500000;
    // 发送字符 '2' (ASCII 0x32 = 50)
    $display("[%0t] Sending '2' (0x32)...", $time);
    uart_send_byte(8'h32);
    #500000;
    $display("[%0t] === UART monitor simulation complete ===", $time);
    $finish;
end
endmodule

5 RISC-V_SoC

5.1 方案规划

|--------|--------------------------|
| 参数 | 规格 |
| ISA | RV32I 子集(8 条指令) |
| 数据宽度 | 32 位 |
| 寄存器 | x0~x31(x0 恒为 0) |
| 指令ROM | 256 × 32 bit (1 KB) |
| 数据RAM | 256 × 32 bit (1 KB) |
| GPIO | 8 位输出(映射 LED) |
| 地址映射 | RAM: 0x0000,GPIO: 0x4000 |
| 主时钟 | 12 MHz(STEPFPGA 板载) |

5.2 编码设计

新建编程文件:risc_soc.v

cpp 复制代码
`timescale 1ns / 1ps   // 包含`timescale 1ns/1ps声明,仿真时不会出现 vsim-3009 警告
// =========================================================
//  RISC-V SoC 顶层模块
//  ISA: RV32I 子集 | ROM: 1KB | RAM: 1KB | GPIO: 8-bit
// =========================================================
module risc_soc (
    input      clk,       // 12 MHz
    input      rst_n,     // 按键复位 active-low
    output [7:0] gpio_out   // 8-bit GPIO → LED
);
// ---- CPU 信号 ----
wire [31:0] instr_addr, instr_data;
wire [31:0] data_addr, data_wdata, data_rdata;
wire        data_we;
// ---- 总线解码 ----
wire sel_ram  = ~data_addr[10];             // 0x0000~0x03FF
wire sel_gpio = data_addr[10];              // 0x0400+
wire [31:0] ram_rdata;
wire [31:0] gpio_rdata = {24'd0, gpio_out};
assign data_rdata = sel_gpio ? gpio_rdata : ram_rdata;
// ---- 实例化 CPU ----
rv32i_cpu u_cpu (
    .clk      (clk),
    .rst_n    (rst_n),
    .instr_addr (instr_addr),
    .instr_data (instr_data),
    .data_addr (data_addr),
    .data_wdata (data_wdata),
    .data_rdata (data_rdata),
    .data_we  (data_we)
);
// ---- 指令 ROM ----
instr_rom u_rom ( .addr (instr_addr[9:2]), .data (instr_data) );
// ---- 数据 RAM ----
data_ram u_ram ( .clk  (clk), .we   (data_we & sel_ram),
    .addr (data_addr[9:2]), .din  (data_wdata), .dout (ram_rdata) );
// ---- GPIO 寄存器 ----
gpio_reg u_gpio ( .clk (clk), .rst_n (rst_n), .we (data_we & sel_gpio),
    .din (data_wdata[7:0]), .gpio_out (gpio_out) );
endmodule
// =========================================================
//  RV32I CPU 核心(单周期实现)
// =========================================================
module rv32i_cpu (
    input           clk,
    input           rst_n,
    output reg [31:0] instr_addr,  // PC → ROM
    input      [31:0] instr_data,  // ROM → 指令
    output     [31:0] data_addr,   // 数据地址
    output     [31:0] data_wdata,  // 写数据
    input      [31:0] data_rdata,  // 读数据
    output          data_we      // 写使能
);
// ---- 指令解码字段 ----
wire [6:0]  opcode = instr_data[6:0];
wire [4:0]  rd     = instr_data[11:7];
wire [2:0]  funct3 = instr_data[14:12];
wire [4:0]  rs1    = instr_data[19:15];
wire [4:0]  rs2    = instr_data[24:20];
wire [6:0]  funct7 = instr_data[31:25];
// ---- 立即数生成 ----
wire [31:0] imm_i = {{20{instr_data[31]}}, instr_data[31:20]};
wire [31:0] imm_s = {{20{instr_data[31]}}, instr_data[31:25], instr_data[11:7]};
wire [31:0] imm_b = {{19{instr_data[31]}}, instr_data[31], instr_data[7],
                     instr_data[30:25], instr_data[11:8], 1'b0};
// ---- opcode 类型判断 ----
wire is_rtype = (opcode == 7'b0110011);  // ADD/SUB/AND/OR
wire is_itype = (opcode == 7'b0010011);  // ADDI
wire is_load  = (opcode == 7'b0000011);  // LW
wire is_store = (opcode == 7'b0100011);  // SW
wire is_branch= (opcode == 7'b1100011);  // BEQ
// ---- 寄存器堆 ----
reg [31:0] regfile [0:31];
wire [31:0] rs1_data = (rs1 == 5'd0) ? 32'd0 : regfile[rs1];
wire [31:0] rs2_data = (rs2 == 5'd0) ? 32'd0 : regfile[rs2];
// ---- ALU ----
reg [31:0] alu_result;
always @(*) begin
    alu_result = 32'd0;
    if (is_rtype) begin
        case ({funct7, funct3})
            10'b0000000_000: alu_result = rs1_data + rs2_data;  // ADD
            10'b0100000_000: alu_result = rs1_data - rs2_data;  // SUB
            10'b0000000_111: alu_result = rs1_data & rs2_data;  // AND
            10'b0000000_110: alu_result = rs1_data | rs2_data;  // OR
            default:         alu_result = 32'd0;
        endcase
    end else if (is_itype) begin
        alu_result = rs1_data + imm_i;                    // ADDI
    end else if (is_load || is_store) begin
        alu_result = rs1_data + (is_store ? imm_s : imm_i); // 地址计算
    end
end
// ---- 数据通路输出 ----
assign data_addr  = alu_result;
assign data_wdata = rs2_data;
assign data_we    = is_store;
// ---- 写回 + PC 更新 ----
wire branch_taken = is_branch & (rs1_data == rs2_data);
wire [31:0] rd_wdata = is_load ? data_rdata : alu_result;
wire        rd_we    = (is_rtype || is_itype || is_load) & (rd != 5'd0);
integer i;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        instr_addr <= 32'd0;
        for (i = 0; i < 32; i = i + 1)
            regfile[i] <= 32'd0;
    end else begin
        // 寄存器写回
        if (rd_we)
            regfile[rd] <= rd_wdata;
        // PC 更新
        if (branch_taken)
            instr_addr <= instr_addr + imm_b;
        else
            instr_addr <= instr_addr + 32'd4;
    end
end
endmodule
// =========================================================
//  指令 ROM(预装测试程序)
// =========================================================
module instr_rom (
    input  [7:0]  addr,
    output reg [31:0] data
);
always @(*) begin
    case (addr)
    //  地址   机器码         汇编                    注释
    8'd0:  data = 32'h00100093;  // addi x1,  x0,  1      x1=1
    8'd1:  data = 32'h00200113;  // addi x2,  x0,  2      x2=2
    8'd2:  data = 32'h00300193;  // addi x3,  x0,  3      x3=3
    8'd3:  data = 32'h00208233;  // add  x4,  x1,  x2     x4=3
    8'd4:  data = 32'h003202B3;  // add  x5,  x4,  x3     x5=6
    8'd5:  data = 32'h40128333;  // sub  x6,  x5,  x1     x6=5
    8'd6:  data = 32'h0032F3B3;  // and  x7,  x5,  x3     x7=2
    8'd7:  data = 32'h0012E433;  // or   x8,  x5,  x1     x8=7
    8'd8:  data = 32'h00502023;  // sw   x5,  0(x0)       RAM[0]=6
    8'd9:  data = 32'h00002483;  // lw   x9,  0(x0)       x9=6
    8'd10: data = 32'h40000513;  // addi x10, x0, 0x400   x10=0x400
    8'd11: data = 32'h00552023;  // sw   x5,  0(x10)      GPIO=6
    8'd12: data = 32'h00000063;  // beq  x0,  x0, 0       死循环
    default: data = 32'h00000013;  // NOP
    endcase
end
endmodule
// =========================================================
//  数据 RAM (256 x 32)
// =========================================================
module data_ram (
    input             clk,
    input             we,
    input  [7:0]      addr,
    input  [31:0]     din,
    output [31:0]     dout
);
reg [31:0] mem [0:255];
assign dout = mem[addr];
always @(posedge clk) begin
    if (we)
        mem[addr] <= din;
end
endmodule
// =========================================================
//  GPIO 输出寄存器 (8-bit)
// =========================================================
module gpio_reg (
    input            clk,
    input            rst_n,
    input            we,
    input  [7:0]     din,
    output reg [7:0] gpio_out
);
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        gpio_out <= 8'd0;
    else if (we)
        gpio_out <= din;
end
endmodule

5.3 仿真测试

编制仿真测试文件,进行测试,运行波形如下图所示。仿真测试文件如下文本框所示。

cpp 复制代码
`timescale 1ns / 1ps
module risc_soc_tb;
// ---- 信号 ----
reg       clk;
reg       rst_n;
wire [7:0]  gpio_out;
// ---- 实例化 SoC ----
risc_soc uut ( .clk (clk), .rst_n (rst_n), .gpio_out (gpio_out) );
// ---- 50 MHz 时钟 (周期 20ns) ----
initial clk = 0;
always #10 clk = ~clk;
// ---- 寄存器名称用于显示 ----
wire [31:0] x1  = uut.u_cpu.regfile[1];
wire [31:0] x2  = uut.u_cpu.regfile[2];
wire [31:0] x3  = uut.u_cpu.regfile[3];
wire [31:0] x4  = uut.u_cpu.regfile[4];
wire [31:0] x5  = uut.u_cpu.regfile[5];
wire [31:0] x6  = uut.u_cpu.regfile[6];
wire [31:0] x7  = uut.u_cpu.regfile[7];
wire [31:0] x8  = uut.u_cpu.regfile[8];
wire [31:0] x9  = uut.u_cpu.regfile[9];
wire [31:0] x10 = uut.u_cpu.regfile[10];
// ---- PC 和指令追踪 ----
wire [31:0] pc    = uut.u_cpu.instr_addr;
wire [31:0] instr = uut.u_rom.data;
// ---- 每周期打印执行跟踪 ----
always @(posedge clk) begin
    if (rst_n) begin
        $display("[%0t] PC=%h INSTR=%h | x1=%0d x2=%0d x3=%0d x4=%0d x5=%0d x6=%0d x7=%0d x8=%0d x9=%0d x10=%h | GPIO=%b",
        $time, pc, instr,
        x1, x2, x3, x4, x5, x6, x7, x8, x9, x10,
        gpio_out);
    end
end
// ---- GPIO 变化检测 ----
always @(gpio_out) begin
    $display("*** GPIO changed to: %0d (0x%h) at time %0t ***",
    gpio_out, gpio_out, $time);
end
// ---- 测试流程 ----
initial begin
    $display("======================================");
    $display("  RISC-V SoC Simulation Start");
    $display("======================================");
    // 复位
    rst_n = 0;
    #50;
    rst_n = 1;
    // 运行 20 个时钟周期(足够执行全部 13 条指令)
    #400;
    $display("======================================");
    $display("  Final Register State:");
    $display("  x1=%0d  x2=%0d  x3=%0d", x1, x2, x3);
    $display("  x4=%0d  x5=%0d  x6=%0d", x4, x5, x6);
    $display("  x7=%0d  x8=%0d  x9=%0d", x7, x8, x9);
    $display("  x10=0x%h", x10);
    $display("  GPIO = %0d (binary: %b)", gpio_out, gpio_out);
    $display("  RAM[0] = %0d", uut.u_ram.mem[0]);
    $display("======================================");
    // 验证结果
    if (x5 == 32'd6 && gpio_out == 8'd6)
        $display(">>> TEST PASSED <<<");
    else
        $display(">>> TEST FAILED <<<  Expected x5=6, GPIO=6");

    $finish;
end
endmodule
相关推荐
cjie22113 小时前
图像缩放需要哪些参数和端口
计算机视觉·fpga开发
思尔芯S2C13 小时前
FPGA Prototyping That Creates Useful Pre-Silicon Evidence
fpga开发
啄缘之间13 小时前
10.【学习】SPI & UART 验证环境与测试用例
开发语言·经验分享·学习·fpga开发·测试用例·verilog
liuluyang5301 天前
SV中|-> 和 |=>的区别与关系
fpga开发·sva
A000—ic测试座(陈佳鑫)1 天前
大电流FPGA芯片测试:特性、应用、测试条件与FPGA芯片测试座案例
fpga开发·测试用例
Saniffer_SH1 天前
【每日一题】不只是点亮画面:UniGraf 如何把 HDMI/DP 接口问题拆成可定位、可复现、可自动化验证的测试流程?
运维·人工智能·测试工具·fpga开发·性能优化·自动化·压力测试
liuluyang5302 天前
SV中#和##的区别与用法
fpga开发·sva
404是NotFound呀2 天前
[FPGA] Ubuntu 22.04 安装 Vivado 2023.1 和 PetaLinux 踩坑记录
linux·ubuntu·fpga开发
liuluyang5302 天前
SV中if与iff区别与用法
fpga开发·sv