[Verilog]模块实例化驱动的理解

笔者在复习刷题HDLBits时, 对模块实例化时, 接口的驱动有了更深理解.

问题描述

实现100位的带涟漪进位(ripple-carry)的全加器

处理过程

这是一个纯组合逻辑电路, 除了可能在Combinational Blocksalways @(*)中进行的赋值外, 无需reg,所以默认的wire类型不予显式.

  1. 首先实现 单位全加器full_adder
module fadd(
	input a;
	input b;
	input cin;
	output sum;
	output cout;
);
	assign sum = a ^ b ^ cin;
	assign cout = (a&b) || (a^b)&cin;
endmodule

这是非常简单的逻辑, sum的0/1取决于a, b, c的奇偶数;cout取1的情况只有ab = 11ab至少有一个1, 同时cin = 1. 如果无法理解进位输出的计算逻辑, 可以先去了解一下最小项, a&b代表着仅11组合可取1, a^b代表仅单1组合可取1, 此情况就需要cin = 1的进位输入了.
最小项的最小体现在仅一种情况下取1, 其他所有情况都为0, 也就是尽可能小.
此外, 一个更优雅的实现方法是通过加法运算的行为描述实现:
2. 在adder100中实例化100个fadd

//模块声明
module adder100( 
    input [99:0] a, b,
    input cin,
    output [99:0] cout,
    output [99:0] sum );

下面列出的两种方法皆为原网站提供的Hint

  • 方法一: 使用generate语句
//对于第一个全加器的进位是cin, 而最后一位用不到. 为了使用i, 创建向量carry,把需要用到的进位序号增1
wire [99:0] carry;
assign carry = {cout[98:0], cin};

genvar i;
generate
	for (i = 0; i < 100; i = i+1) begin: fa_block
		faad fa(
		.a(a[i]),
		.b(b[i]),
		.cin(carry[i]),
		.sum(sum[i]),
		.cout(cout[i])
		);
	end
endgenerate

访问模块2的cout:adder100.fa_block[i].fa.cout;

  • 方法二: 使用实例化数组
    照方法一的葫芦画瓢, 笔者进行了第一次尝试
wire [100:0] carry;
assign carry = {cout[99:0], cin};

faad fa[99:0](
    .a(a),
    .b(b),
    .cin(carry[99:0]),
    .sum(sum),
    .cout(carry[100:1])
    );

遗憾的是出现了编译错误, 最主要的三行:

Warning (10030): Net "cout" at top_module.v(4) has no driver or initial value, using a default initial value '0' File: Line: 4
Error (10028): Can't resolve multiple constant drivers for net "carry[100]" at top_module.v(15)  Line: 15
Error (10029): Constant driver at top_module.v(7) Line: 7

即:

  1. 主模块的输出端口cout没有驱动, 被悬空;
  2. 无法处理carry[100]的被多个信号持续驱动的情况;
  3. 向量carry被持续驱动

之后的多行都是错误2同类, 从carry[99]一直往后.

cout为什么会被悬空呢?我起初认为wire不过是电路中双向的连线, 在实例化单位全加器的过程中,实现了carry的逐位连线; 而在assign语句中通过位连接操作符也实现了carry和主模块输出端口的连线, 这样子模块和主模块的cout通过wire [100:1] carry这样一根"连线"连接起来, 怎么会没有驱动呢?

对于错误1, 首先主模块的cout没有驱动, 如果问题是在assign语句上, 那么法一为什么就没有错误呢?对比模块例化语句:法一.cout(cout[i]), 法二.cout(carry[100:1]), 我们可以知道: 在模块例化语句中, 例化模块的输出接口驱动着所关联的信号.
果然, 在将例化模块output cout的关联信号改成 cout[99:0]后, 通过了仿真验证.
在得到上面结论后, 分析错误2也就简单许多, carry[100]assign语句中, 被主模块的cout[99]驱动, 同时被例化模块fa[99].cout驱动. 错误3也是指carry被多个信号驱动.
那么为什么.cin(carry)此类表达没有错误呢? 事实上, 在模块例化语句中, 例化模块的输入接口被所关联的信号驱动, 还有显然地, 连续赋值语句和过程赋值语句是赋值符号右侧的信号驱动左侧信号.

所学所感

由此,我们可以对Verilog中, 所谓drive一词进行更深刻的理解:

  • 赋值语句中, 右侧的值驱动左侧的值, 在连续赋值语句assign中, 只能驱动wire, 在过程块(时序逻辑always @(posedge clk)非阻塞赋值和组合逻辑always @(*)阻塞赋值)中, 只能驱动reg. 而对驱动者没有特殊要求. 特别要指出, 对于组合逻辑(assign和always @(*)):

These types (wire vs. reg) have nothing to do with what hardware is synthesized, and is just syntax left over from Verilog’s use as a hardware simulation language.

  • 例化模块时, 关联信号是驱动者还是被驱动取决于它关联的是模块的输入还是输出端口. 关联输入端, 关联信号驱动输入, 关联输出端, 关联信号被驱动或者说Driver as Input-port, Driven by Output-port
  • 模块的内部和外部: 在例化模块时, 我们其实只能接触到模块的输入输出接口, 而模块的内部信号是不可见的. 因此称作外部. 在RTL文件中, 主模块的所有信号都可访问, 故称作内部. 在模块例化时, 实现的其实是电路的并发组合逻辑(Concurrent Combinational Logic), 故被驱动的值只能是wire, 而驱动者可以是wire或reg.
    笔者重新翻看《数字逻辑与设计》课程的课件, 终于真正理解了:

input: 可以由regnet连接驱动, 但本身只能驱动net连接
output: 可以由regnet连接驱动, 但本身只能驱动net连接
inout: 只能net连接驱动, 本身只能驱动net连接

问题回顾

法一中

//对于第一个全加器的进位是cin, 而最后一位用不到. 为了使用i, 创建向量carry,把需要用到的进位序号增1
wire [99:0] carry;
assign carry = {cout[98:0], cin};

genvar i;
generate
	for (i = 0; i < 100; i = i+1) begin: fa_block
		faad fa(
		.a(a[i]),
		.b(b[i]),
		.cin(carry[i]),
		.sum(sum[i]),
		.cout(cout[i])
		);
	end
endgenerate

通过中间量carry和例化模块,实现了 cout <- .cout <- .cin <- carry <- cout<< 1, 将cout除了无用的最高位外其他位用于作为cin计算下一位carry.
法二中
错误使用carry[]例化模块输出, 导致output cout悬空.

写在最后

此外, 一个更优雅的实现方法是通过加法运算的行为描述实现:

assign {cout, sum} = a + b + cin;

通过加法运算, 实质上是生成了一个忽略进位的全加器, 使用连接运算符{}拓展进位, 即可通过单行代码同时实现sumcout的计算.
+同样支持多维进位的运算, 如果adder100中, output cout是一位输出, 而非向量的话, 同样可以单行代码实现:

 assign {cout, sum} = a + b + cin;

对于本题, output [99:0] cout, 也无需通过多级模块结构化设计实现

module adder100( 
    input [99:0] a, b,
    input cin,
    output [99:0] cout,
    output reg [99:0] sum
    /*再次强调, sum是在always块中被赋值(被驱动的), 必须使用reg, 无关其本身是否真的是寄存器,
    而使用always块也仅仅是为了能使用for循环语句, 如开头所说, 这是一个纯组合逻辑电路. */
    );
	reg [100:0] carry;
	//同理carry需要在过程块中被赋值, 也被声明成reg变量, reg变量需要初始化, 所以在过程块入口处对其赋值.
	always @(*) begin
		carry = {99'b0, cin};
		for (int i=0; i<100; i=i+1)
		{carry[i+1], sum[i]} = a[i] + b[i] + carry[i];
	end
	//cout不需要用在过程块中, 仍是默认的wire, 所以直接用assign语句赋值	
	assign cout = carry[100:1];
endmodule

如果你完全理解了上述reg出现的原因, 那么你应该知道 regwire类型与被驱动的方式有关, 而驱动者(driver)的类型无关紧要这一道理, 写出这样简洁优雅的代码并非一蹴而就, 行百里者半九十, 此前模块结构设计过程中调试的经验和启示, 才是真正的宝物.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值