Menu

硬件与软件:Verilog 和 C、Python 的思维差异

学过软件语言再学 Verilog,为什么会觉得摸不着头脑:默认并发,时间是一等概念,语句也不按顺序执行。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

软件的时间与硬件的时间

在程序中,时间一条指令一条指令地向前推进。CPU 执行完第 1 行,再执行第 2 行。想让两件事同时发生,你得用线程、异步,或者再加一台机器。

在硬件中,一切都同时发生。电路不会轮流工作。加法器一直在加,多路选择器一直在选,触发器一直在盯着时钟。没有程序计数器,也没有“当前行”。

Verilog 必须用文本来描述这样一个世界。它的描述方式,就是本章所有困惑的来源。

两层并发

读一个 Verilog 模块时,你同时在看两种不同的东西:

  1. 电路的静态描述。 线网、寄存器、门实例、子模块实例、连续赋值。它们全都同时存在,在文件中的先后顺序无关紧要。
  2. 过程块,即 initial 和 always,看起来像仿真器逐步执行的小程序。在这样的块内部,语句确实按某种顺序执行。但多个 always 块可以同时处于活动状态,各自在自己那条小小的仿真时间线上运行。
module example(input wire a, input wire b, output wire y, output wire z);
    assign y = a & b;        // exists at all times
    assign z = a | b;        // also exists at all times, in parallel

    always @(posedge a) begin
        // a separate "always-on" reactor that wakes up on each
        // rising edge of `a`
    end
endmodule

两行 assign 不是先后关系。它们描述的是两块组合逻辑,综合工具可以把它们并排布置。always 块则是第三个并行发生的东西。

“信号”是什么意思

在软件中,变量会一直保存一个值,直到你修改它。在 Verilog 中,信号持续地保持一个值,而且每一刻都取决于驱动它的东西。

wire 由外部驱动(一条 assign、子模块的输出端口、一个 inout 连接)。reg 由过程块内部驱动。两者在任何时刻都有值。Verilog 中不存在 C 语言意义上的“未初始化”:信号要么被驱动为确定的值,要么是特殊的未知值 x,要么是高阻态 z。后两种会在 X 与 Z 值 中讲解。

时钟改变一切

一旦引入时钟,时间就开始变得重要。触发器是一小块硬件,它在时钟的上升沿(或下降沿)捕获输入值,并一直保持到下一个时钟沿。正是时钟让你能够构建计数器、状态机、流水线,以及任何具有记忆的东西。

注意这里写的是 q <= d,而不是 q = d。这是非阻塞赋值,时序逻辑的主力。它的意思是“在下一个时钟沿,安排 q 变成 d 此刻的值”。具体规则会在 阻塞与非阻塞赋值 中深入讲解;现在你只需认识到,这条赋值并不是在假装成一条软件语句。

RTL:寄存器传输的思维模型

大多数可综合的 Verilog 都采用一种叫寄存器传输级(Register Transfer Level,RTL)的风格。思路很简单:

  • 确定电路需要哪些状态(寄存器)。
  • 对每个寄存器描述两件事:什么会让它复位,以及哪段组合逻辑计算它的下一个值。
  • 把组合逻辑的输出连到寄存器的输入,一个能工作的电路就完成了。
always @(posedge clk) begin
    if (reset) state <= IDLE;
    else       state <= next_state;
end

always @(*) begin
    case (state)
        IDLE:   next_state = start ? RUNNING : IDLE;
        RUNNING: next_state = done  ? IDLE    : RUNNING;
        default: next_state = IDLE;
    endcase
end

这是一个两状态的状态机。第一个 always 块由时钟驱动,它就是一个触发器。第二个是纯组合逻辑,只是一个方程。你将来写的几乎每个状态机、计数器和流水线,都遵循这种结构。

让你卡住的习惯

如果你有软件背景,下面这几个习惯需要放下:

  • “给变量赋值时它就会更新。” 在时钟沿上不是这样:非阻塞赋值会把更新安排到当前时间步的末尾。
  • “语句从上到下执行。” 在过程块之外,不是。在时钟驱动的块内部,算是吧,但阻塞与非阻塞赋值会改变“按顺序”的实际含义。
  • “需要时我再分配。” 硬件没有分配这回事。每个寄存器和门在综合时就必须存在,每个向量的大小都是固定的。
  • “这个循环很快,只是一个操作而已。” 可综合 Verilog 中的 for 循环会被展开成并行硬件。一个 64 次迭代的循环会变成 64 份循环体,而不是一条执行 64 次的 CPU 指令。

你学的不是怎么写程序,而是怎么描述电路。从上往下读的直觉恰恰是错的,需要花一段时间才能扭转过来。

接下来

接下来的文档会介绍如何搭建本地工具链(可选,浏览器编辑器就够用),然后从零开始编写你的第一个模块。每当有什么看起来很奇怪,我们都会回到硬件与软件的对比上来,因为大多数“奇怪”之处都源于同一个原因:这不是软件。

常见问题

从 Verilog 的角度看,硬件和软件有什么区别?

软件是一串由 CPU 依次执行的指令。硬件(也就是 Verilog 描述的东西)是一张由门和导线组成的网络,所有导线同时传输信号。Verilog 文件描述的就是这张网络。仿真器模拟它的并行行为;综合工具则把它变成真正的芯片。

Verilog 代码会像 C 一样从上到下运行吗?

不会,而把它当成从上到下运行,正是初学者最常犯的错误。顶层语句(assign、模块实例、always 块)全都同时“存在”。只有在过程块 initial 和 always 内部,才会发生类似顺序执行的事情,而且即便在那里,非阻塞赋值也会打破这种错觉。

Verilog 中的“并发”是什么意思?

意思是多条语句描述的是电路中同时工作的各个部分。同一个模块里的两条 assign 语句并不是“先第 1 行再第 2 行”,而是两块并行运行的硬件,都在持续地响应各自的输入。

RTL 设计是什么?

RTL 是 Register Transfer Level(寄存器传输级)的缩写。这是一种编写 Verilog 的风格:把电路描述成寄存器(触发器)以及计算它们下一个值的组合逻辑。大多数可综合的 Verilog 都是 RTL。比它高一层的是行为级,低一层的是门级。

Coddy 编程语言插图

和 Coddy 一起学编程

开始