Lec 3 用Bluespec描述组合电路
算术计算和布尔算术有很强的联系。数字可以用二进制(base 2)来表示,并且能对其进行算术运算。更进一步,二进制的算术运算和布尔代数中的逻辑运算之间存在一一对应关系,比如,二进制加法可以用 XOR 和 AND 实现(比如半加器、全加器)
因此,我们可以把所有的算术操作(像加、减、乘等)用布尔表达式来表示,而这些布尔表达式就可以被实现成组合逻辑电路(combinational circuits)
Outline
- 加法器
- 多路复用器
- 移位器
加法器实现

加法器的组合逻辑:
- 半加器(Half addr):将两个输入,两个输出,其中一个是sum、另外一个是carry
- 全加器(Full addr): 三个输入(多了一个carry位),产生两个输出。
- 可以由两个半加器组合成
- 级联全加器可以执行二进制加法

描述一个32位的加法器
半加器
| A | B | S | C |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
布尔表达式为, s = ~a·b + a·(~b) = a ⊕ b(异或),c = a · b
电路组合逻辑为

function Bit#(2) ha(Bit#(1) a, Bit#(1) b);
Bit#(1) s = a ^ b;
Bit#(1) c = a & b;
return {c, s};
endfunction
Bit#(2) t = ha(1, 0);
%%eval t[0]
%%eval t[1]其中 {c, s} 表示比特的连接。注意输出t[0] = 'h1,t[0] = 'h0
全加器

// 全加器
function Bit#(2) fa(Bit#(1) a, Bit#(1)b, Bit#(1) c_in);
Bit#(2) ab = ha(a, b);
Bit#(2) abc = ha(ab[0], c_in);
Bit#(1) c_out = ab[1] | abc[1];
return {c_out, abc[0]};
endfunctionLet语法
强类型语言,当编译器能推断的时候,不需要指定变量类型,
// 全加器
function Bit#(2) fa(Bit#(1) a, Bit#(1)b, Bit#(1) c_in);
let ab = ha(a, b);
let abc = ha(ab[0], c_in);
let c_out = ab[1] | abc[1];
return {c_out, abc[0]};
endfunction2-bit 行波进位加法器
2位行波进位加法器(2-bit Ripple-Carry Adder),实现两个2位二进制数的加法,输出2位和可能的进位。通过级联两个FA构建,利用行波进位(Ripple-Carry)传递机制。

// 级联FA, 自己的实现
function Bit#(3) add2(Bit#(2) x, Bit#(2) y);
let t = fa(x[0], y[0], 'h0);
let t2 = fa(x[1], y[1], t[1]);
return {t2[1], t2[0], t[0]};
endfunction
// 完全根据图上的话
function Bit#(3) add2(Bit#(2) x, Bit#(2) y);
Bit#(2) s = 0;
Bit#(3) c = 0;
c[0] = 0;
let cs0 = fa(x[0], y[0], c[0]);
s[0] = cs0[0];
c[1] = cs0[1];
let cs1 = fa(x[1], y[1], c[1]);
s[1] = cs1[0];
c[2] = cs1[1];
return {c[2], s};
endfunction多比特字赋值规则
整体赋值
Bit#(3) c = 0;部分赋值
c[0] = c0;初始化要求:多比特字的每一位都必须有初始值。使用未初始化的比特会:
- 编译器发出警告 ⚠️
- 程序行为不可预测 ❗️
多路复用器
背景: 选择某一位x[i]
假设x是4-bit宽,若使用常量选择器(如 x[2])这仅仅是引用了一个具体的线网,并不生成额外的硬件;当使用变量选择器(如 x[i])时,生成的硬件通常是一个多路选择器(例如 4 位宽的 x 就会生成一个 4 路 mux),因为它需要在运行时动态决定选哪一位。

2路多用复用器
对于bluespec
(s == 0)? a: b;对于python
a if s == 0 else b
踩坑:硬件没有“条件执行”。软件里
s ? foo(x) : bar(y)会先算s,再只执行其中一个分支(以避免做无用的昂贵计算)。但在组合硬件里做不到这一点——foo(x)和bar(y)两个电路都会被实例化并并行求值,mux 只是最后在两个已经算好的结果中选一个。因此?:、if、case在组合逻辑里统统综合成 mux,所有分支的硬件都真实存在。(真正的“条件执行”要到时序逻辑才有,见 lec6。)
如果你构造一个2路多路复用器,用于两个n位信号a 和 b 之间进行选择,那么这个多路复用器的硬件实际上会被 按位复制 n 次。
举个例子,
Bit#(4) a = 4'b1010;
Bit#(4) b = 4'b1100;
Bit#(1) s = 1'b1;
Bit#(4) out = s == 0 ? a: b;背后其实是4个1-bit mux
out[0] = s? b[0]: a[0];
out[1] = s? b[1]: a[1];
out[2] = s? b[2]: a[2];
out[3] = s? b[3]: a[3];每一位都独立地选择,但共享相同的选择信号 s
4路多路复用器
case ({s1, s0})
0: a;
1: b;
2: c;
3: d;
endcase
def mux(a, b, s):
if s == 0:
return a
elif s == 1:
return b
elif s == 2:
return c
else:
return dn路多路复用器可以用 n-1 个两路多路复用器实现。
移位器
固定长度的移位操作在硬件中非常廉价,只需完成接线操作(纯布线操作)

循环移位和算术移位也差不多。循环移位就是把最高位接回最低位即可,仍然是接线;算术右移为例,保留符号位,最左边补原来的符号位,其他仍然通过布线完成。算术移位对于,乘除

逻辑右移n位
暴力法

假设我们想要实现一个把x左移n位(
一个简单的方法,就是准备32个移位器,通过n路的多路复用器连接。
这种方式需要多少个2路1位的多路复用器实现该结构?
Solution: n * (n-1)。 意思是把所有可能的移位结果(不需要 mux,只要移位连线)都准备好,然后用n路的大 mux 选择,对于每一位而言,例如y[0], 可能来自x[0], x[1]...x[7]中的任何一个,换句话说,需要8:1mux,因此最终结果就是 n * (n-1) 个mux。
分级移位(barrel)法
我们把 "移 k 位" 拆成几个固定移位的“步骤”,每个步骤只处理一种移位大小(比如移 1 位、移 2 位、移 4 位……),为什么可行? 任何整数 k 都可以分解为 2 的幂的和(,例如移位 x 5位 ,5的二进制是101,所以先左移4位,再左移1位。

结构上来说,假如我们处理8位数据:
- shift by 1:通过 2:1 mux 决定是否左移 1 位
- **shift by 2:再通过一层 mux 决定是否左移 2 位
- shift by 4:再通过一层 mux 决定是否左移 4 位
这样就可以组合出 0~7 的任何移位值 。一般地,对
条件移位: 移位 vs. 不移位对比
我们需要一个多路复用器(mux)来从两个输入中选择合适的线路:如果选择信号s == 1,mux会选择左边的线路,否则将选择右边的线路
成本对比(重点):
- 暴力法(
个固定移位器 + 一个 :1 大 mux):每个输出位需要 个 2:1 mux,共约 个 1 位 mux( 时约 992 个),即 。 - barrel shifter:只有
层,每层 个 1 位 mux,共 个( 时 160 个),即 。 N=4 的 Minispec barrel shifter 示例:
bluespecfunction Bit#(4) barrelShifter(Bit#(4) x, Bit#(2) s); Bit#(4) r1 = (s[1] == 0) ? x : {2'b00, x[3:2]}; // 是否右移 2 位 Bit#(4) r0 = (s[0] == 0) ? r1 : {1'b0, r1[3:1]}; // 是否再右移 1 位 return r0; endfunction