跳转到内容
新建笔记

01. 硬件思维、并行执行与模块

Verilog 代码描述电路的结构和行为,不是让 FPGA 按行执行的程序。

软件流程与硬件并行

在 C 语言中,处理器通常依次取指、执行。下面四行表达了先后关系:

a = input_a();
b = input_b();
sum = a + b;
output(sum);

在 Verilog 中,这三条连续赋值描述三组一直存在的连线/逻辑:

assign x = a & b;
assign y = c | d;
assign sum = p + q;

三条 assign 没有“第一条先运行”的含义。任一输入改变,对应的电路都会开始传播,三组电路可以同时工作。把三行上下调换,硬件功能不变。

组合逻辑:当前输入决定当前输出

跳转到“组合逻辑:当前输入决定当前输出”
assign max_value = (a > b) ? a : b;

它像一个二选一多路选择器。输入稳定后,经过传播延迟,输出跟着稳定。它没有时钟,也不需要记住过去。

时序逻辑:在时钟沿保存状态

跳转到“时序逻辑:在时钟沿保存状态”
always @(posedge clk) begin
q <= d;
end

它描述一个 D 触发器。只有 clk 上升沿到来时,q 才接收当时的 d;两个时钟沿之间,q 保持不变。“保持”就是状态,也是数字系统能够计数、分步骤工作的基础。

不同 always 块彼此并行;同一个过程块中的语句仍有过程语义。尤其是阻塞赋值 = 会影响块内后续语句,非阻塞赋值 <= 会把更新安排到当前仿真时刻的后续阶段。第 6 篇会用时间模型详细解释。

always @(posedge clk) begin
stage1 <= data_in;
stage2 <= stage1;
end

这里两条语句共同描述两级流水寄存器。一个时钟沿后,stage1 得到旧的 data_in,stage2 得到旧的 stage1,而不是新 stage1。

模块的组成

最小模块由模块名、端口和实现组成:

module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodule

逐行理解:

  • module and_gate:定义名为 and_gate 的硬件模块;
  • input wire:从模块外进入的连接;
  • output wire:离开模块的连接;
  • assign:持续驱动输出;
  • endmodule:模块定义结束。

文件本身不会“运行”。综合工具以某个模块为顶层,从顶层向下展开所有实例,生成完整硬件。

wire y0;
wire y1;
and_gate u_and0 (
.a(a0),
.b(b0),
.y(y0)
);
and_gate u_and1 (
.a(a1),
.b(b1),
.y(y1)
);

u_and0 和 u_and1 不是对同一函数的两次调用,而是两份同时存在的与门。命名端口连接 .a(a0) 的含义是:子模块端口 a 接到当前层信号 a0。

同一段 Verilog 可能用于两个目的:

  • 仿真:由模拟器按事件和时间规则计算波形,用于证明行为;
  • 综合:由工具把可综合语句转换成门、触发器、存储器等硬件。

例如 testbench 中常见的 #10 延时能被模拟器理解,却通常不能综合成普通 FPGA 逻辑:

initial begin
clk = 1'b0;
forever #10 clk = ~clk; // 仿真时钟,不属于待综合设计
end

因此要始终知道自己正在写 RTL 设计 还是 testbench。

  1. 认为代码越靠前越先执行。 模块级 assign/always 是并行的。
  2. 把延时当计时器。 FPGA 中的计时通常用时钟和计数器实现。
  3. 把模块实例当函数调用。 每个实例都会占用硬件资源。
  4. 只看仿真,不想硬件。 可仿真不等于可综合,更不等于时序能满足。
  1. 两个独立的 always 块谁先执行?答:没有固定先后,应该按并行过程设计。
  2. assign y = a ^ b; 可能对应什么硬件?答:异或逻辑。
  3. 计数器为什么需要时钟?答:它必须在离散时刻保存并更新过去的状态。

画出下面代码对应的电路,标出三个模块是否能同时工作:

assign n0 = a & b;
assign n1 = c | d;
assign out = n0 ^ n1;

答案应包含两个第一级逻辑门和一个第二级异或门;前两个门并行工作。

返回系列目录 · 下一篇:四态逻辑、位宽、进制与有符号数