GCD 硬件加速器设计
GCD 硬件加速器
- 在这次实验中, 我们将设计一个 GCD(最大公约数)硬件加速器, 通过硬件实现辗转相减算法, 让 CPU 只需要写入两个数, 等待计算完成后读取结果即可.
为什么需要硬件加速器?
软件 vs 硬件
假设我们要计算 GCD(48, 18), 用软件实现需要 CPU 执行多次循环, 每次循环包含比较、跳转、减法等指令. 而硬件加速器可以在每个时钟周期完成一次比较和减法, 大大提升计算效率. 更重要的是, 在硬件计算期间, CPU 可以去做其他事情!
GCD 算法回顾
辗转相减法的思路很简单:
- 两个数比大小
- 大的减去小的
- 重复上述过程, 直到两数相等
- 此时的值就是最大公约数
举个例子, 计算 GCD(48, 18):
| 步骤 | A | B | 操作 |
|---|---|---|---|
| 1 | 48 | 18 | A > B, A = 48 - 18 = 30 |
| 2 | 30 | 18 | A > B, A = 30 - 18 = 12 |
| 3 | 12 | 18 | A < B, B = 18 - 12 = 6 |
| 4 | 12 | 6 | A > B, A = 12 - 6 = 6 |
| 5 | 6 | 6 | A == B, 搞定! GCD = 6 |
硬件模块设计
寄存器地址分配
我们给 GCD 加速器分配基地址 0x40030000, 内部有 4 个寄存器:
| 地址 | 寄存器名 | 读/写 | 说明 |
|---|---|---|---|
| 0x40030000 | DATA_A | R/W | 输入数据 A |
| 0x40030004 | DATA_B | R/W | 输入数据 B(写入后自动开始计算) |
| 0x40030008 | RESULT | R | 计算结果 |
| 0x4003000C | STATUS | R | 状态寄存器, bit0=1 表示计算完成 |
GCD 硬件代码组成
AHBlite_GCD.v
- 功能: 最大公约数 (GCD) 硬件加速器的 AHB 接口包装器。
- 寄存器映射:
0x00: 输入 A0x04: 输入 B0x08: 控制寄存器 (Start)0x0C: 状态与结果寄存器 (Done, Result)
GCD.v
- 功能: 实现欧几里得算法计算两个数的最大公约数。
- 状态机:
IDLE->CALC->DONE。
GCD 核心模块
GCD 模块的代码位于 /rtl/GCD.v, 你需要在其基础上补全关键逻辑.
代码补全
根据辗转相减法的算法原理, 完成状态机的设计.
case(state)
IDLE: begin
done <= 1'b0;
if(start) begin
/********** 需要补充的代码 **********/
// 提示: 加载输入数据到 a_reg 和 b_reg, 切换到 CALC 状态
/************************************/
end
end
CALC: begin
if(reg_b == 32'd0) begin
result <= reg_a;
done <= 1'b1;
state <= DONE;
end else if(reg_a > reg_b) begin
/********** 需要补充的代码 **********/
end else begin
/********** 需要补充的代码 **********/
end
end
完成补全后, 你的代码应该如下所示:
case(state)
IDLE: begin
done <= 1'b0;
if(start) begin
reg_a <= a;
reg_b <= b;
state <= CALC;
end
end
CALC: begin
if(reg_b == 32'd0) begin
result <= reg_a;
done <= 1'b1;
state <= DONE;
end else if(reg_a > reg_b) begin
reg_a <= reg_a - reg_b;
end else begin
reg_b <= reg_b - reg_a;
end
end
AHB-Lite 总线接口
接下来我们需要给 GCD 核心加上 AHB-Lite 总线接口, 让 CPU 能够访问它。
AHB-Lite 接口的作用
AHB-Lite 接口是 GCD 加速器与 CPU 之间的桥梁。CPU 通过总线向 GCD 模块写入操作数, 并读取计算结果和状态。接口需要实现地址译码、读写控制等功能。
读操作的地址译码
在 AHB-Lite 总线中, CPU 读取外设数据时, 需要根据地址选择返回对应寄存器的值。以下代码实现了读操作的地址译码逻辑:
// Read operations - use latched address
reg [31:0] hrdata_reg;
always@(*) begin
case(addr_reg)
/********** 需要补充的代码 **********/
endcase
end
补全提示
addr_reg 是锁存的地址低位 (通常取 HADDR[3:2]), 用于区分 4 个寄存器:
2'b00(偏移 0x00): DATA_A 寄存器2'b01(偏移 0x04): DATA_B 寄存器2'b10(偏移 0x08): 控制/启动寄存器2'b11(偏移 0x0C): 状态和结果寄存器
补全数据读取阶段的代码, 完成补全后, 你的代码应该如下所示:
// Read operations - use latched address
reg [31:0] hrdata_reg;
always@(*) begin
case(addr_reg)
2'b00: hrdata_reg = reg_a;
2'b01: hrdata_reg = reg_b;
2'b10: hrdata_reg = {31'b0, start};
2'b11: hrdata_reg = {gcd_done, gcd_result[30:0]}; // Done in bit[31], result in bit[30:0]
default: hrdata_reg = 32'h00000000;
endcase
end
代码功能说明
| 地址 | 表达式 | 功能说明 |
|---|---|---|
2'b00 |
hrdata_reg = reg_a |
返回操作数 A 的值, CPU 可以回读之前写入的数据 |
2'b01 |
hrdata_reg = reg_b |
返回操作数 B 的值, CPU 可以回读之前写入的数据 |
2'b10 |
hrdata_reg = {31'b0, start} |
返回启动信号状态, bit[0] 表示是否正在启动计算 |
2'b11 |
hrdata_reg = {gcd_done, gcd_result[30:0]} |
关键寄存器: bit[31] 为完成标志 gcd_done, bit[30:0] 为计算结果 |
default |
hrdata_reg = 32'h00000000 |
非法地址返回 0, 保证总线不会读到未知值 |
状态寄存器的设计巧思
注意 2'b11 地址的设计: 将 gcd_done 放在最高位 (bit[31]), 结果放在低 31 位。这样 CPU 可以通过一次读操作同时获取完成状态和结果:
uint32_t status = GCD->STATUS;
if (status & 0x80000000) { // 检查 bit[31] 是否为 1
uint32_t result = status & 0x7FFFFFFF; // 取低 31 位作为结果
}
思考题
- 如果输入的两个数中有一个是 0, 会发生什么? 如何改进?
- 辗转相减法效率不高 (想想 GCD(1000000, 1)), 如果改用辗转相除法, 硬件该怎么设计?
- 能否让 GCD 加速器支持中断, 计算完成后通知 CPU?