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时,总觉得这玩意儿是“软”的,后来才明白——它本质上还是硬件,只是可配置的硬件。

小技巧: 在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。

注意: 在always块中,如果忘记写default分支,综合时会生成锁存器(latch)。我曾经因为这个原因,调试了一整个下午才发现问题。所以,组合逻辑的case语句,务必写全所有分支,或者加default。

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,否则路径延迟会很大。

实战经验: 我在做数据采集卡时,需要从16个ADC通道中选择一个进行采样。一开始用了一个16选1的MUX,结果发现采样率上不去。后来改成两级4选1,第一级选4个,第二级从4个中选1个,时序问题就解决了。

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实现)。

避坑指南: 注意result的位宽是5位。为什么?因为两个4位数相加,结果最大是30(15+15),需要5位才能表示。我曾经因为位宽不够,导致加法结果溢出,查了半天才发现是定义的问题。

好了,这一讲的内容就到这里。总结一下:

  • 基本逻辑门在CPLD中就是一行assign语句的事
  • 译码器和编码器用case语句实现最直观
  • 多路选择器本质上是“索引”操作,注意大位宽时的时序
  • 加法器可以直接用+,也可以手动搭,看需求
  • 组合逻辑设计的关键是:想清楚你要什么电路,然后用代码描述出来

下一讲,我们会进入时序逻辑的世界——触发器、计数器、分频器。这些东西才是CPLD真正发挥威力的地方。到时候见!