“人气腹语术师天愿在现场披露了被人偶搭档夺取灵魂的腹语术师将妻子杀害的表演节目。天愿真的陷入了多重人格,命令自己杀害妻子和子的人偶的人格出现了。为了不(让自己)杀害和弟子登川有外遇的妻子,天愿提出委托想要监视,然而第二天早上,和子真的被杀害的事件发生了。天愿坦白很可能是在自己的意识失去的时候杀害的……”(----“真相只有一个”《名侦探柯南》一向是老衲喜欢的动画片)这个是第 806 回《腹语師的错觉》的介绍。
人有双重人格,或者叫人格分裂,那么语言呢?Verilog 语言还真的是人格分裂的语言。前回书已经说到了,不能简单地把 wire 类型映射为组合逻辑,同时把 reg 类型映射为时序逻辑。事实上,这两个概念会交叉的。也就是说,wire 类型极可能被综合为组合逻辑也可能综合为时序逻辑,reg 类型也是这样。
“‘reg’是什么?”最微软的回答是:注册表文件。这个自然没错,但是违背了“到哪座山,唱哪里歌”的原则。一般的“标准”答案是:寄存器型变量。看看‘reg’,不就是‘register’(寄存器)的缩写吗?大多数中文教材中都是这样说的。
下面为了说明白这桩事情,请允许老僧引用 IEEE 有关 Verilog 语言里面的原文:
“Assignments to a reg are made by procedural assignments (see 6.2 and 9.2). Since the reg holds a value between assignments, it can be used to model hardware registers. Edge-sensitive (i.e., flip-flops) and level sensitive (i.e., RS and transparent latches) storage elements can be modeled. A reg need not represent a hardware storage element since it can also be used to represent combinatorial logic.”
为了强调,表 1 里面给出了 wire、reg 类型和组合逻辑、时序逻辑之间的映射关系。
表 1 wire、reg 类型和组合逻辑、时序逻辑之间的映射关系
|
wire |
reg |
组合逻辑 |
可 |
可 |
时序逻辑 |
可 |
可 |
可见 reg 是“双面间谍”的工作性质,为了能够“左右逢源”,自然用法要比 wire 来的复杂。类型 wire 被综合为时序逻辑一般还真是写错了,不必细表。
1. 电平触发,组合实现
和 reg“孟不离焦,焦不离孟”的是关键词 always,这个要记清楚。人家 wire 和 assign 是夫妻,reg 和 always 是一对,千万不要搞混了,这不是能拉郎配的季节。
“always”的语法结构是:
always @(sensitive_tabel)
其中,sensitive_tabel 称作敏感列表,其中包含 always 内部操作的一个或者多个触发条件。字符“@”发音是“艾特”(at),大伙儿发电子邮件(e-mail)的时候常用,不罗嗦。
正如上面提到的、标准立面的说法可以是边沿敏感和电平敏感两种。对于组合逻辑电路,这个敏感列表里面所有条件均为电平敏感。逻辑上,当敏感列表里面的条件符合的时候,always 内部的操作可以进行。但是,很多逻辑上可行的代码,由于没有实际电路的支持,是无法实现的。
在 Verilog 语言 95 版本里面,电平触发的敏感列表的写法是
triger1 or triger2 or triger3……
其中,triger1 等为触发信号。当触发信号电平变化时,说明敏感列表里面条件符合。如果触发信号是向量,则其中一个比特的信号变化,就认为信号电平有变化。到了 Verilog 2001 版本,这个写法被更加简化了:“,”和“or”都可以用来分割敏感事件;并且,可以用“*”代表所有输入信号,这可以防止遗漏。例 1 给出了一些符合语法的 always 的例子。
【例 1】always 与敏感列表
always @ (triger1 or triger2 or triger3) //Version 95 and Version 2001
always @ (triger1 , triger2 , triger3) //Version 2001
always @ (*) //Version 2001
具有完全电平敏感列表的 always 模块,总叫人觉得就是组合逻辑了。再次强调数字电路是并行工作的,注意不要用“执行”这个词,不准确。对应的所有 assign 以及 always 带领的快都是并行的,其在代码中前后顺序与输出结果无关。也就是说例 2 里面的两段代码是等效的。代码中,敏感列表 sensitive_table1 和 sensitive_table2 对应操作 Operation_A 和操作 Operation_B。
【例 2】always 所带操作顺序与输出结果无关
always @(sensitive_table1) Operation_A always @( sensitive_table2) Operation_B |
always @( sensitive_table2) Operation_B always @(sensitive_table1) Operation_A |
理论上,assign 后面只有一行,对于这个并行工作的理解不难,不会产生误解。到了 always 这里,一般其后的代码就有很多行了,一不注意就会出错。
2. 条件判断,分枝多多
“用 C 语言的标准评价 Verilog,如同用水果的标准评价蜜饯。”,但是他们的确很多写法有类似,这是很容易误导学习者的地方。
前文书关于“? :”选择操作哪里介绍过,选择与分枝在一般系统中是不可少的。那里介绍的代码方法,显然会产生阅读困难 ---- 尤其是在条件比较多的时候。为了改善这一问题,也同时可以更加符合大家以前的习惯,这一讲书老朽给贵客介绍“if”和“case”这两位大家熟悉的陌生人。
先看眼里的代码,条件语句 if 的形式有如表 2 中的三种。其中,condition 等表示选择的条件,operation 等表示对应的操作。请注意,这里的表达式“选择”,目的是和电路对应,不是故意和别人不一样的哗众取宠。
表 2 条件语句 if 的格式
|
无分枝 |
单级分枝 |
多级分枝 |
形式 |
if (condition ) begin operations end |
if (condition ) begin operations_1 end else begin operations_2 end |
if (condition_1 ) begin operations_1 end else if (condition_2 ) begin operations_2 end else if…… …… begin operations_m end |
对应电路 |
时序电路 |
时序电路 |
时序电路 组合电路 |
表 .2 中“对应电路”一行也请施主们注意,if 语句中条件的所有路径覆盖不全面,可能会产生时序电路的。对于 reg 类型的变量,需要满足“条件不满足的时候,保持原值”;同时,组合电路不可“自赋值”(也就是类似“a <= a”的形式)。当需要“保持”的时候,纯组合电路是无法满足的。所以,综合器会引入“锁存器”。不是综合器自作主张,这是代码的要求。“天作孽犹可恕,自作孽不可活”,只能怪你自己了,哭吧!这个对应的器件是锁存器,不是这里的重点,会在以后介绍。这里要说的是:要产生组合逻辑,if 的条件路径必须全覆盖。
if 语句中条件的所有路径覆盖不全面,可能会产生时序电路的。这个对应的器件是锁存器,不是这里的重点,以后介绍。这里要说的是:要产生组合逻辑,if 的条件路径必须全覆盖。
例 3 是一个单级条件语句 if 应用的例子,其功能是求有符号数绝对值。其中,输入为 8 比特有符号数,编码方式为补码;输出是输入数值的绝对值。具体算法是:
【例 3】绝对值运算模块
module abs
(
input[7:0] signed_value,
output reg[6:0] result
);
//Definition for Variables in the module
//Load other module(s)
//Logical
always @(signed_value)
begin
if ( signed_value[7])
//Negative number input
begin
result <= (~signed_value[6:0]) + 7'h01;
end
else
//Positive number or zero input
begin
result <= signed_value[6:0];
end
end
endmodule
3. 多种情况,并列判决
在条件很多的时候,用 if 语句来写还是很麻烦的,搞不好就是是一个条件路径覆盖不完全。这个时候,可以选择 case 套餐。case 语句是一种多分支选择语句, Verilog 语言提供的 case 语句直接处理多分支选择。多分支的 case 有三种形式,如表 3 所示。
表 3 条件语句 case 的形式
|
case |
casex |
casez |
比较方式 |
敏感表达式与各项值之间的比较,是一种全等比较 |
如果分支表达式某些位的值为高阻 z,那么对这些位的比较就会忽略,不予考虑,而只关注其他位的比较结果。 casez 会把 z/?匹配成任意,也会把任意匹配成 z/? |
在 casex 语句中,则把这种处理方式进一步扩展到对 x 的处理,即如果比较双方有一方的某些位的值是 z 或 x,那么这些位的比较就不予考虑。 casex 会把 z/?x 匹配成任意,也会把任意匹配成 z/?/x,即直接忽略 z/?/x |
形式 |
case (variable) costant_1: begin operations_1 end costant_2: begin operations_2 end …… default: begin operations_m end endcase |
case (variable) costant_1: begin operations_1 end costant_2: begin operations_2 end …… default: begin operations_m end endcase |
case (variable) costant_1: begin operations_1 end costant_2: begin operations_2 end …… default: begin operations_m end endcase |
constant 项 |
各个 constant 项为确定宽度的常数值,不包含 x 和 z; 可以用“?”表示不关心该位数值 |
各个 constant 项为确定宽度的常数值,可包含 x 但不能包含 z |
各个 constant 项为确定宽度的常数值,可包含 z 但不能包含 z |
constant 例子 |
3’b000:3 比特宽度全 0; 3’b0?0:3 比特宽度第二比特不关心,其他比特为 0 |
3’b000:3 比特宽度全 0; 3’b0?0:3 比特宽度第二比特不关心,其他比特为 0; 3’b00x:3 比特宽度最低比特为 x,其他比特为 0 |
3’b000:3 比特宽度全 0; 3’b0?0:3 比特宽度第二比特不关心,其他比特为 0; 3’b00z:3 比特宽度最低比特为不关心,其他比特为 0 |
可综合性 |
可综合 |
依赖综合软件 |
依赖综合软件 |
case 括弧内的变量称为控制表达式,case 分支项中的常数称为分支表达式。控制表达式通常表示为控制信号的某些位,分支表达式则用这些控制信号的具体状态值来表示,因此分支表达式又可以称为常量表达式。当控制表达式的值与分支表达式的值相等时,就执行分支表达式后面的语句。如果所有的分支表达式的值都没有与控制表达式的值相匹配的,就执行 default 后面的语句。
default 项可有可无,一个 case 语句里只能有一个 default 项。 当分支表达式可以覆盖控制表达式全部分枝路径时,default 可以不写。但是,有时候这个全覆盖不是那么容易看出来的,所以建议最好写上 default,哪怕有冗余这个 default 永远不可能被实现。也请大家放心,这种冗余综合软件会大伙儿去掉的,不必担心浪费电路资源。
每一个 case 分项的分支表达式的值必须互不相同,否则就会出现矛盾现象(对表达式的同一个值,有多种执行方案)。
执行完 case 分项后的语句,则跳出该 case 语句结构,终止 case 语句的执行。(精通 C 语言的大虾们请特别注意这点,这里 case 操作执行完之后不必写 break 了。)
在用 case 语句表达式进行比较的过程中,只有当信号的对应位的值能明确进行比较时,比较才能成功,因此要详细说明 case 分项的分支表达式的值。
case 语句的所有表达式的值的位宽必须相等,只有这样控制表达式和分支表达式才能进行对应位的比较。一个经常犯的错误是用'bx、'bz 来替代 n'bx、n'bz,这样写是不对的,因为信号 x、z 的缺省宽度是机器的字节宽度,通常是 32 位(此处 n 是 case 控制表达式的位宽)。
当分支表达式不完全覆盖控制表达式全部分枝路径时,您老有偷懒没有写 default 的情况下,可能产生时序逻辑的锁存器的,这点和条件 if 语句类似。例 4 是一个例子,说明了 default 的重要性。但是,图 1 中的“ld”是锁存器已经是时序电路的元件了,超越了本章的范围。
【例 4】case 语句条件覆盖不全产生会综合出锁存器
代码 1:组合逻辑电路写法
module case_full
(
input[7:0] number,
input[1:0] select,
output reg[7:0] result
);
//Load other module(s)
//Definition for Variables in the module
//Logical
always @(*)
begin
case (select)
2'b00:
begin
result <= number + 8'b0000_0001;
end
2'b01:
begin
result <= number;
end
2'b10:
begin
result <= number - 8'b0000_0001;
end
default:
begin
result <= 8'b0000_0000;
end
endcase
end
endmodule
图 1 例 4 综合出的电路图(全部为组合逻辑)
4. 多路选择,一个例子
数据选择器(也称为:多路复用器,英文:multiplexer,简写:MUX),是一种从多路输入信号中选择一个信号作为输出的器件。电器符号如图 2 所示。
图 2 数据选择器的电气符号
数据选择器的的逻辑功能是:
注意,其中输入 I0、I1 和 SEL 以及输出 O 都是 1 比特位宽的信号。对应布尔逻辑表达式是
对应 Verilog 代码为:
1) 利用? :表达式
input SEL;
input I0;
input I1;
output O
assign O =(SEL) ? (I0) : (I1);
代码中关键的部分是? :表达式,其语法结构为 (逻辑表达式) ? (值 0) : (值 1);作用是
所以,以上代码满足了数字电路里面数据选择器的功能。
2) 利用 if 关键词
If (SEL == 1’b0)
begin
O = I0;
end
else
begin
O = I1;
end
3) 利用 case 关键词
case (SEL)
1’b0:
begin
O = I0;
end
1’b1:
begin
O = I1;
end
endcase
在很多情况下,需要选择的输入位宽大于 1,这个时候只要两个待选择的输入与输出的位宽一致,照样可以实现功能(以下按照 8 比特输入为例)。此时 Verilog 代码除了模块的接口位宽,其他部分几乎没有变化:
input SEL;
input[7:0] I0;
input[7:0] I1;
output[7:0] O
assign O =(SEL) ? (I0) : (I1);
当然用 if 或者 case 语句也可以实现,相信读者举一反三的能力,就不罗列了。
很多读者或许会感觉到笔者十分啰嗦,实则不然,图 3 是多位输入的数据选择器的电气原理图。
图 3 多位数据选择器的电路原理图
上图看起来是顺理成章的。这里之所以笔者还不厌其烦的画出来,是为了叫读者看到多位与 1 比特实现上的区别。如果眼睛还没有贵恙的话,可以看出来多位数据选择器就是若干 1 比特数据选择器的并行排列。考虑到前面内容介绍的时延问题,需要提醒读者注意的是这个位数很高(当然不是例子里面的 8 比特)的时候,输入和输出信号的 skew(线间时延)可能会给设计带来麻烦。
另一种常见的情况是输入不止有两个信号,或者说需要在不仅仅两个信号里面进行选择,这个叫做高阶数据选择器(一般吧 SEL 的比特数称为数据选择器的阶数)。通常输入个数是 2 的幂,此时选择信号 SEL 也就不仅是 1 比特信号了,这个很容易理解。在理论上,可以通过展开布尔逻辑表达式的方法,完成高速的高阶数据选择器。例如,2 阶(也就是有 4 个输入信号,SEL 为 2 比特变量)的随机选择的布尔逻辑表达式为:
其中,I0、I1、I2 和 I3 为器件的输入,S0 和 S1 为 SEL 信号的低比特和高比特。
这个式子已经不简单了,如果是 10 阶神马的数据选择器,那样的式子的长度不难想象。所以,在工程上,一般利用低阶数据选择器的串联来实现高阶数据选择器。图 4 是一个用 3 个 1 阶数据选择器实现 2 阶数据选择器的例子。
图 4 利用低阶数据选择器实现高阶数据选择器
对于高阶数据选择器的 Verilog 代码,一般建议利用 case 的形式。例如图 3 里面的 2 阶数据选择器可以用以下代码实现:
case (SEL)
2’b00:
begin
O = I0;
end
2’b01:
begin
O = I1;
end
2’b01:
begin
O = I2;
end
2’b11:
begin
O = I3;
end
endcase
这正是:
“
组合逻辑大融合,关键语法有心得。不论理论数学河,电路优化靠综合。
鄙人说书自有乐,撬行老僧沙弥哥。报告整理嫉妒惹,大乘渡人笑呵呵。
”
与非网原创内容,谢绝转载!
系列汇总: