1.3 模块与实例化:出庭的基本单元 本节摘要:module 是 Verilog 唯一的顶层组织单位:端口是对外契约,内部装着三种建模视角的描述;实例化让模块互相装配成层次化设计。这一节把模块语法、词法规则、编译单元一次讲全,读完你就能独立搭出规范的工程骨架。 前两节解决了「语言从哪来」「有哪几种讲法」,这一节解决「代码怎么组织」。Verilog 没有 main 函数,没有全局入口,一切代码都活在 module 里——它是语言的基本容器,也是综合与验证的操作单元。 模块的 anatomy 一个模块由五段组成:模块名与端口列表、端口与信号声明、内部描述(assign、always、实例化)、可选的任务函数、endmodule 收尾。看一个最小但完整的骨架: 几个声明细节值得停一下。
本节摘要:module 是 Verilog 唯一的顶层组织单位:端口是对外契约,内部装着三种建模视角的描述;实例化让模块互相装配成层次化设计。这一节把模块语法、词法规则、编译单元一次讲全,读完你就能独立搭出规范的工程骨架。
前两节解决了「语言从哪来」「有哪几种讲法」,这一节解决「代码怎么组织」。Verilog 没有 main 函数,没有全局入口,一切代码都活在 module 里——它是语言的基本容器,也是综合与验证的操作单元。
一个模块由五段组成:模块名与端口列表、端口与信号声明、内部描述(assign、always、实例化)、可选的任务函数、endmodule 收尾。看一个最小但完整的骨架:
module counter #( parameter WIDTH = 8 // 参数列表:实例化时可改 )( input wire clk, // 时钟 input wire rst_n, // 低有效复位 input wire enable, // 计数使能 output wire [WIDTH-1:0] count, // 计数值输出 output wire overflow // 溢出标志 ); reg [WIDTH-1:0] cnt; // 过程块内部赋值用 reg always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= {WIDTH{1'b0}}; // 复位清零 else if (enable) cnt <= cnt + 1'b1; // 计数递增 end assign count = cnt; // 组合输出 assign overflow = enable & (cnt == {WIDTH{1'b1}}); endmodule
几个声明细节值得停一下。端口方向只有 input、output、inout 三种;方向后面跟类型(wire 或 reg),行为级赋值对象必须声明成 output reg。位宽写在端口名前面,[WIDTH-1:0] 这种用参数表达的位宽是参数化的关键手法。参数列表用 #( ) 挂在模块名后,实例化时可以覆盖,这是第六章生成语句的前置知识。
标识符:字母、数字、下划线和美元符组成,不能以数字开头,区分大小写。惯例是信号用小写下划线风格(data_valid),参数用大写(WIDTH),这属于第九章编码规范的范畴,但从第一行代码就养成习惯最省事。
关键字:module、input、always 这些是语言保留字,不能挪用。完整的保留字表在标准附录里,日常常踩的坑是把 reg、wire、in、out 之类当变量名用——in 和 out 不是保留字,但很多老工具对它们过敏,避开最稳。
数字字面量的完整格式是「位宽'进制值」:4'b1010 是四位二进制,8'hFF 是八位十六进制,'d10 是未指定位宽的十进制。三段任何一段可省略,省位宽时工具按表达式上下文取宽度——这是位宽对不齐事故的常见源头,第二章细说。x 表未知态、z 表高阻态,也可以写进字面量,如 4'b00x1。
注释与 C 完全一致:// 行注释,/* */ 块注释。编译指令用反引号开头(`timescale、`define、`include),它们不属于任何模块,作用于编译单元。
Verilog-2001 正式引入编译单元概念:一次编译所处理的全部文件构成一个编译单元,反引号指令、任务函数声明在单元内全局可见。工程上的含义很直接——`define 定义一次,全编译单元有效,所以宏名冲突是多文件工程的高发事故;而 parameter 是模块私有的,实例化时按需覆盖,作用域干净得多。第九章的规范清单里会再次强调:能 parameter 就别 define。
实例化就是把一个模块「装」进另一个模块。两种写法必须分清:
module top( input wire clk, input wire rst_n, input wire [7:0] sw, output wire [7:0] led, output wire ovf ); wire [7:0] cnt_a; // 命名关联:端口名显式对应,顺序无关,推荐 counter #( .WIDTH(8) ) u_counter ( .clk (clk), .rst_n (rst_n), .enable (sw[0]), .count (cnt_a), .overflow(ovf) ); // 位置关联:按端口声明顺序对应,端口一改全盘错,不建议 // counter u_counter2 (clk, rst_n, sw[1], led, ); endmodule module counter #(parameter WIDTH = 8)(...); // 定义同前
命名关联的好处是抗修改:子模块加一个端口,父模块没接的端口保持悬空,工具发警告但不报错;位置关联则会整体错位,而且错位往往「看起来还能跑」。悬空端口用 .port() 空着表示,故意不用某个输出时就这样写,比删掉它更显意图。
案例展开:悬空端口引发的连环排查。 背景:某板级调试现场,计数器使能开关无效。操作:从顶层往下查——顶层实例化处 .enable (sw[0]) 写着没毛病,但往上一层发现顶层本身是被更上层实例化的,而上一层的实例化把 sw 悬空了(.sw()),sw 恒为 z,sw[0] 判为假,使能永远无效。结果:补上连接,功能恢复。解读:悬空输入读出的是高阻 z,z 在条件判断里当假处理,电路不报错、只是「安静地不工作」,这类案子在层次深的设计里极难目视发现——仿真波形里盯住 z 和 x 的传播是最快的侦破手段。变式:把悬空输出接一个常量或 LED 观测位,配合 Lint 工具的未连接端口检查,可以在编译期就拦下大半。
模块划分没有算法可依,但有两条经验纪律。其一,模块边界尽量落在「功能职责」上而不是「行数」上——一个模块回答一个问题(这里是计数器,那里是译码器),比按长度切割好维护得多。其二,跨模块只传必要信号,接口信号越少,层次间的耦合越松,复用越容易。层次图要能徒手画出来:拿一张纸,每个模块一个框,端口连线标清方向,画不顺的地方通常就是接口设计有问题的信号。
顺带交代一个常见疑问:仿真的「顶层」和综合的「顶层」可以不是同一个文件。仿真顶层是测试台(第七章展开),综合顶层是要落到硅片上的那个模块,工具分别从各自顶层往下遍历。不在层次里的模块不会被综合,测试台里的延迟与显示语句也不会进网表——两个世界通过「顶层」这个开关切换,各取所需。
output reg 是组合 always 输出的标准声明;骨架立好了,容器里的信号该是什么类型、位宽怎么算,下一章「物证登记」开庭。