2.1 wire 与 reg:两条不同的证据来源 本节摘要:wire 与 reg 的分野不在「是不是寄存器」,而在驱动方式:wire 是被持续驱动的连线,reg 是被过程块赋值的变量。判断用哪个,只看一句——赋值语句写在 always 里还是外面。 上一章把模块骨架立了起来,这一章开始往里登记信号。先审最基础也最被误读的一对类型:wire 与 reg。把它们分清,类型系统就赢了一大半。 为什么会有这个分法 Verilog 描述的是电路,电路里信号只有两种存在方式:要么是一根被逻辑持续驱动的导线,输出变了它立刻跟着变;要么是一个被时钟沿「定格」的存储单元,值只在特定事件后更新。语言用 net 类(以 wire 为代表)和 variable 类(以 reg 为代表)分别对应这两种存在。
本节摘要:wire 与 reg 的分野不在「是不是寄存器」,而在驱动方式:wire 是被持续驱动的连线,reg 是被过程块赋值的变量。判断用哪个,只看一句——赋值语句写在 always 里还是外面。
上一章把模块骨架立了起来,这一章开始往里登记信号。先审最基础也最被误读的一对类型:wire 与 reg。把它们分清,类型系统就赢了一大半。
Verilog 描述的是电路,电路里信号只有两种存在方式:要么是一根被逻辑持续驱动的导线,输出变了它立刻跟着变;要么是一个被时钟沿「定格」的存储单元,值只在特定事件后更新。语言用 net 类(以 wire 为代表)和 variable 类(以 reg 为代表)分别对应这两种存在。
net 类成员不止 wire 一个:tri 是多驱动场景下的三态线,wand、wor 带线与线或语义,tri0、tri1 带默认上拉下拉含义。variable 类里 reg 是主力,integer 存整数、real 存实数、time 存仿真时间,后三个只活在仿真与辅助计算里,综合器对它们另眼相看。
赋值目标声明成什么类型,规则唯一:assign 与实例化输出驱动的目标用 wire(net),always、initial 块内赋值的目标用 reg(variable)。与电路里是不是触发器毫无关系。
module type_demo( input wire clk, input wire [3:0] a, b, input wire sel, output wire [3:0] y_w, // assign 驱动:wire output reg [3:0] y_r, // always 驱动:reg output reg [3:0] hist // 时序逻辑:也是 reg ); // 数据流:assign 只能驱动 net assign y_w = sel ? a : b; // 组合逻辑:reg 关键字,综合出来是纯组合电路,没有触发器 always @(*) begin if (sel) y_r = a; else y_r = b; end // 时序逻辑:reg 关键字,综合出触发器 always @(posedge clk) hist <= y_r; endmodule
上面 y_r 的例子就是那个流传最广的误读现场:它声明为 reg,综合后却没有触发器。reg 在这里只表示「赋值方式是过程性的」,是否生成存储单元取决于过程块的敏感形式——电平敏感的 @(*) 出组合逻辑,边沿敏感的 @(posedge clk) 出寄存器。把「reg 等于寄存器」这句话从脑子里删掉,很多综合报告就能看懂了。
wire 和 reg 的每一位都有四种取值:0、1、x、z。x 是未知态——冲突、未初始化、未驱动的位;z 是高阻态——没有人驱动这根线。这两个值是调试时的重要证词:上电后寄存器输出 x,说明复位没覆盖到它;总线读出 z,说明三态门全关着。
x 的传染性极强,任何含 x 的逻辑运算结果通常还是 x,于是一个没复位的寄存器能把「未知」扩散给下游一片。比较运算同样中招:if (x_val) 走 else 分支(x 被当假),仿真器看不出异常,硬件里却可能是任意值。经验做法是让复位覆盖所有时序状态寄存器,并在仿真初期用波形确认 x 在复位后全部清干净。
案例展开:reg 外赋值的编译现场。 背景:初学者常把组合逻辑一半写在 always 里、一半图省事用 assign 接着写。操作:写出下面这段代码并跑仿真器——
module bad_case(input wire [1:0] sel, output reg [3:0] y); always @(*) y = 4'b1111; assign y = {2'b00, sel}; // 两次驱动同一目标,非法 endmodule
结果:仿真器直接报错,提示对 variable 类信号使用了连续赋值(或多驱动冲突,措辞因工具而异)。解读:reg 声明了「只有过程块有权给我赋值」,assign 是另一个驱动源,类型系统在编译期就拦下了这种双头管理。修法是把逻辑合并进同一个 always 块,或者把目标拆成两个信号再 assign 汇总。变式:反过来,把 assign 的目标声明成 wire 再在 always 里赋值,同样报错——类型系统是双向把关的,这正是它有价值的地方:错误在编译期暴露,而不是流片后。
wire 只允许一个持续驱动源,但 tri 这类 net 允许多个驱动共存,由强度与取值决议最终值:两个驱动一个给 1 一个给 0,结果是 x;都给 1,结果 1;有驱动给 z,另一个驱动接管。这套裁决规则是三态总线建模的语言基础,第五章 5.3 节会专门开庭审它。
顺带提 strength(强度)的概念:驱动可以分强度等级,strong、pull、weak 依次减弱,门原语和 assign 默认 strong。日常 RTL 设计几乎用不到显式强度,知道「同值低强度让位于高强度、异值冲突出 x」就够了。
用软件经验映射一下:reg 更像「只能被特定代码段写的变量」,wire 更像「绑定到表达式的外部观测点」。但有一处对照必然出错——软件变量不赋值就保持旧值,Verilog 的 wire 没人驱动就是 z,reg 没人赋值在上电后是 x。所以硬件代码里「初始化」是显式责任:复位逻辑、复位值、上电行为,都要自己写、自己验。这个思维转换没有捷径,只能靠每份代码都问一遍「这个信号的第一拍值从哪来」来养成。
历史给出了答案:net 与 variable 的分立对应着硬件里「连线」与「存储」两种真实存在,类型系统把这层物理区别带进了语言。合并成单一类型的尝试(后来标准里的统一类型)最终仍要靠上下文区分驱动语义——区别无法消失,只能从声明挪到用法里,而挪走的后果是编译器少了一类可检查的错误。类型分立的价值就在这:它让「双驱动」「驱动来源错误」这类问题在编译期现形。
能互相赋值,但语义有差。integer 自带符号且固定 32 位,reg 默认无符号且位宽自定——混算时符号与位宽两条规则同时生效,结果常与直觉不符。实用建议:integer 只做循环变量与仿真计算,参与数据通路的值一律用显式位宽的 reg 或 wire,让类型系统的另一半(位宽规则)也在你的掌控里。
信号的类型定了,下一节解决「多宽」的问题——向量、位宽与数组,那里有全语言最静默也最凶险的截断陷阱。