数字电路基础回顾(上):组合逻辑与时序逻辑
各位同学,欢迎来到第二章。这一章咱们要打打地基。别觉得基础枯燥,我做了十几年数字设计,见过太多项目翻车,根源往往就是基础概念没吃透。今天咱们就把组合逻辑、时序逻辑和Verilog基础语法捋一遍。
本章核心脉络:从最底层的与或非门,到多路选择器、加法器,再到D触发器、寄存器和锁存器,最后用Verilog把它们描述出来。说白了,就是让你知道“电路长什么样”以及“怎么用代码写出来”。
2.1 组合逻辑基础
组合逻辑,说白了就是“输入一变,输出立马跟着变”。没有时钟,没有记忆,纯粹的门电路拼出来的逻辑。
2.1.1 与或非门
这三个门是所有数字电路的基石。你想想看,再复杂的CPU,拆到最底层也就是这些门的组合。
| 门类型 | 逻辑符号 | Verilog运算符 | 真值表(2输入) |
|---|---|---|---|
| 与门 (AND) | Y = A & B | & | 00→0, 01→0, 10→0, 11→1 |
| 或门 (OR) | Y = A | B | | | 00→0, 01→1, 10→1, 11→1 |
| 非门 (NOT) | Y = ~A | ~ | 0→1, 1→0 |
我的经验:写Verilog时,与或非直接用 &、|、~ 就行。但要注意,综合工具对“多余的门”很敏感。我曾经在项目里为了图省事,把几个与门串在一起没用括号,结果综合出来的面积比预期大了30%。所以,写表达式时括号别省,清晰第一。
2.1.2 多路选择器 (MUX)
多路选择器,就是“数据选择开关”。从多个输入中选一个输出。2选1 MUX是最基本的,控制信号sel为0选a,为1选b。
// 2选1多路选择器 - 组合逻辑实现
module mux2to1 (
input wire a, // 输入0
input wire b, // 输入1
input wire sel, // 选择信号
output wire y // 输出
);
// 方式1:assign语句
assign y = sel ? b : a;
// 方式2:逻辑门实现(等价)
// assign y = (~sel & a) | (sel & b);
endmodule
为什么我要强调MUX?因为FPGA的查找表(LUT)本质上就是一个可编程的MUX。你写的任何组合逻辑,最终都会被映射成LUT+MUX的结构。理解MUX,你就理解了FPGA的一半。
2.1.3 加法器
加法器是算术运算的核心。半加器只算两个bit,不考虑进位;全加器则把进位也算进去。
| 类型 | 输入 | 输出 | 逻辑表达式 |
|---|---|---|---|
| 半加器 | A, B | S(和), Co(进位) | S = A ^ B; Co = A & B |
| 全加器 | A, B, Ci(进位入) | S, Co | S = A ^ B ^ Ci; Co = (A&B) | (A&Ci) | (B&Ci) |
// 4位加法器 - 行为级描述
module adder_4bit (
input wire [3:0] a,
input wire [3:0] b,
input wire cin,
output wire [3:0] sum,
output wire cout
);
assign {cout, sum} = a + b + cin;
endmodule
⚠️ 避坑指南:我曾经在项目里直接用“+”号做加法,结果综合后时序不满足。原因是工具默认生成了行波进位加法器,位宽一大,进位链太长。对于位宽超过16位的加法,建议用“超前进位加法器”或者直接例化DSP单元。别偷懒,该优化就得优化。
2.2 时序逻辑基础
时序逻辑和组合逻辑最大的区别是什么?它有记忆。组合逻辑的输出只看当前输入,时序逻辑的输出还看“之前的状态”。这个“记忆”靠的就是触发器。
2.2.1 D触发器
D触发器是时序逻辑中最基本的存储单元。每个时钟上升沿,它把输入D的值“抓”到输出Q上,然后保持住,直到下一个时钟沿。
// D触发器 - 上升沿触发,异步复位
module dff (
input wire clk,
input wire rst_n, // 异步复位,低有效
input wire d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0; // 复位
else
q <= d; // 采样
end
endmodule
注意这里的 always @(posedge clk or negedge rst_n),这是标准的时序逻辑写法。敏感列表里有时钟沿和复位沿。我个人的习惯是:所有时序逻辑都用非阻塞赋值 <= ,组合逻辑才用阻塞赋值 = 。这个习惯帮我避免了很多仿真时的竞争问题。
2.2.2 寄存器
寄存器其实就是一组D触发器。8位寄存器就是8个D触发器并排。在RISC-V处理器里,寄存器堆(Register File)就是由一堆寄存器组成的。
// 8位寄存器
module reg_8bit (
input wire clk,
input wire rst_n,
input wire [7:0] d,
output reg [7:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 8'h00;
else
q <= d;
end
endmodule
2.2.3 锁存器 (Latch)
锁存器,这是个“坑”。很多初学者不小心就综合出锁存器,然后发现电路行为不对。
锁存器和触发器的区别:
- 触发器:时钟沿触发,边沿敏感。
- 锁存器:电平触发,使能信号为高时透明(输出跟随输入),为低时保持。
⚠️ 我曾经踩过的坑:在always块里写组合逻辑时,if条件没写全,或者case没写default,综合工具就会“好心”地给你生成一个锁存器。结果仿真和实测对不上,查了两天才发现。记住:组合逻辑的always块里,所有分支都必须赋值,否则就是锁存器。
// 错误示例:会综合出锁存器!
always @(*) begin
if (en)
q = d; // 没有else分支,en=0时q保持,综合出latch
end
// 正确写法:给默认值
always @(*) begin
q = 1'b0; // 先给默认值
if (en)
q = d;
end
2.3 Verilog HDL基础语法
好了,电路概念有了,咱们看看怎么用Verilog把它们描述出来。我挑三个最核心的语法讲:module、assign、always。这三个你掌握了,80%的代码都能写。
2.3.1 module — 模块化设计
Verilog的基本单元就是module。一个module对应一个硬件模块。端口定义、参数传递、模块例化,这些都是基本功。
// 模块定义
module counter #(
parameter WIDTH = 8 // 参数化设计,方便复用
) (
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
// 功能描述
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
// 模块例化(在顶层模块中使用)
counter #(
.WIDTH(16) // 覆盖默认参数
) u_counter (
.clk (sys_clk),
.rst_n (sys_rst_n),
.en (count_en),
.count (counter_val)
);
我的习惯:模块例化时,端口连接用 .端口名(信号名) 的显式方式,别用位置关联。位置关联一旦端口顺序变了,代码全废。显式连接虽然多打几个字,但可读性和可维护性高得多。
2.3.2 assign — 连续赋值
assign用于描述组合逻辑。它右边的表达式一旦有变化,左边立刻更新。说白了,assign就是“连线”。
// 连续赋值示例
module comb_logic (
input wire [3:0] a, b,
input wire sel,
output wire [3:0] y,
output wire flag
);
// 组合逻辑:多路选择 + 比较
assign y = sel ? a : b;
assign flag = (a == b) ? 1'b1 : 1'b0;
endmodule
注意:assign左边必须是wire类型,不能是reg。这个很多初学者搞混。我刚开始学的时候也犯过这个错,报了半天错才发现类型不对。
2.3.3 always — 过程赋值
always块比assign灵活得多。它可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。
| always块类型 | 敏感列表写法 | 赋值方式 | 典型用途 |
|---|---|---|---|
| 组合逻辑 | always @(*) | 阻塞赋值 = | 译码器、状态机下一状态 |
| 时序逻辑 | always @(posedge clk) | 非阻塞赋值 <= | 计数器、移位寄存器 |
| 混合(带复位) | always @(posedge clk or negedge rst_n) | 非阻塞赋值 <= | 带复位的寄存器 |
// always块完整示例
module alu_example (
input wire clk,
input wire rst_n,
input wire [7:0] a, b,
input wire [1:0] op, // 00:加, 01:减, 10:与, 11:或
output reg [7:0] result,
output reg carry
);
// 组合逻辑:运算结果
always @(*) begin
case (op)
2'b00: {carry, result} = a + b;
2'b01: {carry, result} = a - b;
2'b10: {carry, result} = {1'b0, a & b};
2'b11: {carry, result} = {1'b0, a | b};
default: {carry, result} = 9'b0;
endcase
end
endmodule
核心原则:
- 组合逻辑用
always @(*)+ 阻塞赋值= - 时序逻辑用
always @(posedge clk)+ 非阻塞赋值<= - 千万别混用!否则仿真和综合结果不一致,查错查到哭。
好了,这一章的内容就到这儿。组合逻辑、时序逻辑、Verilog基础语法,这三块是后续所有章节的基石。你把这些吃透了,后面写RISC-V处理器的时候就会顺手很多。