去年在FPGA上实现RISC-V CPU时,我选择了五级流水线架构。这种架构在性能与复杂度之间取得了很好的平衡——取指(IF)、译码(ID)、执行(EX)、访存(MEM)和写回(WB)五个阶段并行工作,理论上每个时钟周期都能完成一条指令。使用Quartus Prime 18.1作为开发平台,整个设计包含约8000行Verilog代码,支持RV32I基础指令集,并扩展了乘除法指令。
这个项目的独特之处在于完整实现了教学用CPU的所有关键组件:从最基础的流水线控制,到Cache一致性维护,再到通过AHB总线连接UART外设。在Xilinx Artix-7 FPGA上实测达到75MHz主频,比同类非流水线设计性能提升3-4倍。下面我将详细解析各模块的设计要点和调试过程中积累的实战经验。
流水线CPU最棘手的问题就是三种冒险:结构冒险、数据冒险和控制冒险。我的解决方案如下:
数据冒险:采用两级前递(Forwarding)机制。在EX阶段比较源寄存器与MEM/WB阶段的目的寄存器,若匹配则直接使用ALU结果而非寄存器文件值。对于Load-Use冒险,额外加入流水线停顿机制。Verilog关键代码如下:
verilog复制// EX阶段前递判断
always @(*) begin
if (MEM_reg_write && (MEM_rd != 0) && (MEM_rd == ID_rs1))
forwardA = 2'b10; // 前递MEM阶段结果
else if (WB_reg_write && (WB_rd != 0) && (WB_rd == ID_rs1))
forwardA = 2'b01; // 前递WB阶段结果
else
forwardA = 2'b00; // 无冒险
end
控制冒险:对分支指令采用"预测不跳转"策略。在ID阶段提前解析BEQ、BNE等指令,一旦检测到分支立即清空IF/ID流水线寄存器。为减少性能损失,我优化了分支目标地址计算电路,使其能在ID阶段完成计算。
实际调试中发现:前递路径的时序非常关键。建议将比较逻辑放在时钟上升沿前的一个周期内完成,否则可能导致setup time违例。
存储器层次结构对性能影响巨大。我的设计包含三级存储:
指令Cache:2路组相联,每路4KB,采用LRU替换策略。关键参数:
verilog复制parameter ICACHE_INDEX_BITS = 8; // 256个组
parameter ICACHE_TAG_BITS = 20; // 32位地址中高20位为Tag
数据Cache:写回策略,总线宽度32位。通过AHB-Lite接口与主存通信,支持突发传输。特别处理了Store指令的字节/半字写入,确保小端模式正确性。
片上RAM:使用FPGA的Block RAM实现,作为Boot ROM和调试用内存。在Quartus中通过altsyncram宏实现,避免使用厂商特定原语。
存储器访问的时序是调试重点。我建立了严格的检查表:
采用单层AHB总线连接所有外设,仲裁器采用固定优先级策略(CPU > DMA > 调试接口)。关键信号包括:
HADDR[31:0]:32位地址总线HWDATA[31:0]:写数据总线HRDATA[31:0]:读数据总线HWRITE:读写控制总线时钟域处理特别重要。所有外设都使用HCLK(50MHz)同步,复位信号HRESETn异步有效。在跨时钟域处插入两级同步器:
verilog复制always @(posedge HCLK or negedge HRESETn) begin
if (!HRESETn) begin
sync_reg0 <= 1'b0;
sync_reg1 <= 1'b0;
end else begin
sync_reg0 <= async_signal;
sync_reg1 <= sync_reg0;
end
end
UART模块支持可编程波特率,核心是波特率生成器:
verilog复制// 波特率计算示例:50MHz时钟,115200波特率
localparam BAUD_DIVISOR = (CLK_FREQ / (BAUD_RATE * 16)) - 1;
reg [15:0] baud_counter;
always @(posedge HCLK) begin
if (baud_counter == 0) begin
baud_tick <= 1'b1;
baud_counter <= BAUD_DIVISOR;
end else begin
baud_tick <= 1'b0;
baud_counter <= baud_counter - 1;
end
end
寄存器映射如下表:
| 地址 | 名称 | 功能描述 |
|---|---|---|
| 0x00 | UART_DATA | 数据寄存器,低8位有效 |
| 0x04 | UART_STAT | 状态寄存器:bit0=RX标志 |
| 0x08 | UART_CTRL | 控制寄存器:bit0=TX使能 |
调试UART时遇到一个典型问题:在连续发送时出现数据丢失。最终发现是状态机未正确处理TX忙状态,添加如下判断后解决:
verilog复制if (!tx_busy && tx_en) begin
tx_state <= TX_START;
tx_busy <= 1'b1;
end
采用三级验证体系:
重点测试案例包括:
在Artix-7板卡上遇到的主要问题及解决方案:
时序违例:关键路径在数据Cache的Tag比较环节。通过重定时(Retiming)将比较逻辑分散到两个周期完成。
复位不稳定:部分寄存器未正确初始化。添加全面的复位值设置:
verilog复制always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pc <= 32'h8000_0000; // 复位向量地址
state <= IDLE;
end else begin
// 正常逻辑
end
end
UART数据错误:因时钟抖动导致采样偏移。将波特率容错范围从±3%放宽到±5%。
通过以下手段将主频从初始的50MHz提升到75MHz:
关键路径优化:
存储器分区:
流水线平衡:
实测性能数据对比:
| 优化措施 | 最大频率提升 | 面积代价 |
|---|---|---|
| 流水线重平衡 | +12% | 5% |
| 加法器结构优化 | +8% | 3% |
| Cache预取策略改进 | +15% | 10% |
这个项目最让我自豪的是完整实现了从CPU核心到外设的整个系统。通过亲自处理流水线冒险、Cache一致性和总线仲裁等复杂问题,对计算机体系结构的理解达到了新的层次。建议后续开发者可以尝试添加MMU支持或超标量执行,这将带来更大的挑战和收获。