4、组合逻辑设计:与或非门、译码器、编码器、多路选择器、加法器——用CPLD实现。
各位同学,欢迎来到第四讲。
前面我们聊了CPLD的基本结构,也讲了怎么搭开发环境。今天咱们来点实在的——用CPLD实现最基本的组合逻辑电路。
你可能会想:“不就是与或非门吗?74系列芯片我闭着眼睛都能画。” 没错,用74芯片搭电路是基本功。但用CPLD实现,思路完全不一样。说白了,你是在用代码“画”电路,而不是拿烙铁焊电路。
4.1 基本逻辑门:从74芯片到CPLD
先看一个最简单的例子。用CPLD实现一个三输入与门。
在传统设计中,你会找一片74HC11(三输入与门),把三个输入引脚接好,输出接出去。但在CPLD里,我们这么写:
module basic_gates (
input wire a, b, c,
output wire y_and,
output wire y_or,
output wire y_nand,
output wire y_xor
);
assign y_and = a & b & c; // 与门
assign y_or = a | b | c; // 或门
assign y_nand = ~(a & b & c); // 与非门
assign y_xor = a ^ b; // 异或门
endmodule
你看,就这么几行代码。但背后发生了什么?CPLD内部的宏单元被配置成了查找表(LUT),每个逻辑门对应一个LUT。我刚开始用CPLD时,总觉得这玩意儿是“软”的,后来才明白——它本质上还是硬件,只是可配置的硬件。
4.2 译码器:3-8译码器的CPLD实现
译码器是数字系统里的“翻译官”。3-8译码器,说白了就是把3位二进制数翻译成8个输出中唯一的一个低电平(或高电平)。
我在项目中遇到过一个问题:用74HC138搭的译码器,输出驱动能力不够,还得加缓冲器。换成CPLD后,直接在内部把输出电流配置大一点就解决了。
看代码:
module decoder_38 (
input wire [2:0] sel,
output reg [7:0] y
);
always @(*) begin
case (sel)
3'b000: y = 8'b11111110;
3'b001: y = 8'b11111101;
3'b010: y = 8'b11111011;
3'b011: y = 8'b11110111;
3'b100: y = 8'b11101111;
3'b101: y = 8'b11011111;
3'b110: y = 8'b10111111;
3'b111: y = 8'b01111111;
default: y = 8'b11111111;
endcase
end
endmodule
这里我用的是always @(*)组合逻辑块。你可能会问:为什么不用assign?嗯,因为case语句更直观,可读性更好。我个人习惯是:简单逻辑用assign,复杂一点用always。
4.3 编码器:优先级编码器
编码器和译码器正好相反——把多个输入信号编码成二进制数。最常用的是优先级编码器,比如8-3线优先级编码器。
为什么需要优先级?你想想看,如果两个按键同时按下,编码器该输出哪个?优先级编码器会按照你设定的优先级,选择最高优先级的输入进行编码。
module priority_encoder (
input wire [7:0] req,
output reg [2:0] code,
output reg valid
);
always @(*) begin
if (req[7]) begin code = 3'b111; valid = 1'b1; end
else if (req[6]) begin code = 3'b110; valid = 1'b1; end
else if (req[5]) begin code = 3'b101; valid = 1'b1; end
else if (req[4]) begin code = 3'b100; valid = 1'b1; end
else if (req[3]) begin code = 3'b011; valid = 1'b1; end
else if (req[2]) begin code = 3'b010; valid = 1'b1; end
else if (req[1]) begin code = 3'b001; valid = 1'b1; end
else if (req[0]) begin code = 3'b000; valid = 1'b1; end
else begin code = 3'b000; valid = 1'b0; end
end
endmodule
这里valid信号很关键——它告诉后面的电路,当前输出的编码是否有效。没有这个信号,当所有输入都为0时,输出也是0,你就分不清是“请求0有效”还是“没有请求”。
4.4 多路选择器:数据通道的“开关”
多路选择器(MUX)在数字系统里太常见了。说白了,就是一个多路开关——从多个输入中选择一个送到输出。
用CPLD实现4选1多路选择器:
module mux4 (
input wire [1:0] sel,
input wire [3:0] data_in,
output wire data_out
);
assign data_out = data_in[sel];
endmodule
就这么简单?对,就这么简单。CPLD的宏单元可以直接实现这种“索引”操作。但要注意,如果数据位宽很大,比如32选1,建议拆成两级MUX,否则路径延迟会很大。
4.5 加法器:半加器与全加器
加法器是算术运算的基础。半加器只考虑两个加数,不考虑进位;全加器则考虑来自低位的进位。
先看半加器:
module half_adder (
input wire a, b,
output wire sum,
output wire carry
);
assign sum = a ^ b;
assign carry = a & b;
endmodule
全加器稍微复杂一点:
module full_adder (
input wire a, b, cin,
output wire sum,
output wire cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
你看,全加器的逻辑表达式其实很简单。但如果你要做一个8位加法器,直接用assign sum = a + b不就行了?为什么还要自己搭全加器?
嗯,这里要说明一下:在CPLD中,直接用+运算符,综合工具会自动调用内部的加法器宏单元。但如果你对面积有严格要求,或者想控制进位链的延迟,手动搭全加器反而更可控。
| 加法器类型 | 逻辑表达式 | CPLD实现方式 | 适用场景 |
|---|---|---|---|
| 半加器 | sum = a ^ b carry = a & b |
1个LUT | 最低位加法 |
| 全加器 | sum = a ^ b ^ cin cout = (a&b) | (a&cin) | (b&cin) |
2个LUT | 多位数加法 |
| 行波进位加法器 | 多个全加器级联 | N个全加器 | 对速度要求不高 |
| 超前进位加法器 | 并行计算进位 | 面积大但速度快 | 高速运算 |
我个人建议:在CPLD中,如果加法器位数不超过8位,直接用+运算符就好。综合工具优化的结果,往往比手搭的要好。超过8位时,再考虑手动优化进位链。
4.6 综合实战:一个简易ALU
好了,我们把今天学的知识串起来,做一个4位简易ALU(算术逻辑单元)。它能完成与、或、加、比较四种操作。
module simple_alu (
input wire [3:0] a, b,
input wire [1:0] op,
output reg [4:0] result
);
always @(*) begin
case (op)
2'b00: result = a & b; // 与运算
2'b01: result = a | b; // 或运算
2'b10: result = a + b; // 加法
2'b11: result = (a > b) ? 5'd1 : 5'd0; // 比较
default: result = 5'd0;
endcase
end
endmodule
这个ALU虽然简单,但麻雀虽小五脏俱全。它包含了我们今天讲的所有组合逻辑元素:与或门、加法器、比较器(本质上也是减法器),以及多路选择器(通过case实现)。
好了,这一讲的内容就到这里。总结一下:
- 基本逻辑门在CPLD中就是一行
assign语句的事 - 译码器和编码器用
case语句实现最直观 - 多路选择器本质上是“索引”操作,注意大位宽时的时序
- 加法器可以直接用
+,也可以手动搭,看需求 - 组合逻辑设计的关键是:想清楚你要什么电路,然后用代码描述出来
下一讲,我们会进入时序逻辑的世界——触发器、计数器、分频器。这些东西才是CPLD真正发挥威力的地方。到时候见!