由于帝国硬性规定的等级制度,根据种族、职业等划分等级。其目标是:“每个人都有自己的位置,每个人都在自己应在的位置上。”
——《沙丘》
有句电影台词说得好:“监狱生活充满了一段又一段的例行公事。”
老道晓得前几日晚间的这段书是和尚讲的“非RTL级的Verilog简介”,因此匿了。众所周知,唱戏的是“男怕夜奔,女怕思凡”,讲Verilog最怕的就是这个非RTL,多亏我长了个心眼。我倒是佩服加菲,真的符合那个章节号:第二章第二回,整个就是一个二。说起来呢,贫道也没闲待着。我是高抬腿轻落足,将身藏于房梁之上,期间还少不得学了几声耗子叫,终于听完了这段书。这个出家人竟然讲完了,还没有倒彩,也算是完满了。有说书人如此,大家夫复何求?
好了,书归正传,今天主要给大家说说Verilog里和模块有关的话题。要说模块,就要先说工程项目设计的流程。这也就是为什么要引用“例行公事”那句话了,从流程上看来,项目的工作无外乎“例行公事”的过程。
在《软件工程》这门课里,大家肯定被灌输了不少“瀑布模型”的概念。如是我闻(套一句和尚的术语):“瀑布模型(Waterfall Model)是一个项目开发架构,开发过程是通过设计一系列阶段顺序展开的,从系统需求分析开始直到产品发布和维护,每个阶段都会产生循环反馈,因此,如果有信息未被覆盖或发现了问题,则最好“返回”上一个阶段并进行适当的修改,项目开发进程从一个阶段“流动”到下一个阶段,这也是瀑布模型名称的由来。”这个概念,实际上不仅仅可以用于软件开发,我们的数字逻辑设计过程也可以借鉴一二。
不多聊软件开发的内容,这里只给一个结论:在需求明确并且变更很少的前提下,“瀑布模型”适用。这个要求倒是和数字逻辑设计——尤其是 ASIC 设计——的现实比较符合,因此在进行项目开发时,经常采用“瀑布模型”的思想。注意这里说的是思想,名字嘛,就不一定了。把猫叫作“Hello Kitty”,也不会改变其生物学本质。为什么会有这么多名字,鄙人没研究过,就不瞎说了。
为什么会有“瀑布模型”?教授们肯定讲了不少理由,贫道这里只有一个理由:个人能力不够。这儿的“个人”不是指某个特定的个体,也不是说“张三小学毕业,能力不成;李四是本科,就没问题了”。这是一个人脑生物学特性决定的事情,上帝没有把人类的大脑,设计成适合大的工程项目的样子。现在的工程项目已经巨大到了,任何一个工程师的注意力,在一个时刻不可能放到全部细节上的地步。
为什么大家在本科学习FPGA做实验时,不采用“瀑布模型”开发呢?因为这样太费力气了,完全属于出力不讨好。如果你设计一个“流水灯”,还写那么多报告,估计你的经理明天就“端茶送客”了。这能说您老不严谨?非也非也,是您老不识时务。换一个场景,设计一个基站接收机或CPU什么的,上手就敢写代码,老道预计贵施主离卷铺盖走人的日子也不远矣。没人——即使是天才——能把握如此复杂的系统。还是那句话,是您老不识时务。别想多了,这不是吹来吹去的那个寓言 的意思,老道才没那个时间和精力和大家打机锋呢。
“找啊找啊,找呀找”,找到了一个软件和数字逻辑设计均可用的“瀑布模型”示意图,如图2.9所示。忽略验证/测试部分及需求分析部分,关于开发设计的主要任务见表2.7。这两个属于前面一回的“余孽”,就是说还是属于“看热闹”的冷知识。但是也难说,万一有哪位听众升职做项目管理了呢?记住:“苟富贵,勿相忘!”
图2.9 “瀑布模型”开发流程示意图
表2.8 开发设计过程中“瀑布模型”各步骤的主要任务
(续表)
其实,这个开发过程与原来传统的PCB类电子系统的开发过程没有本质区别。PCB是自顶向下进行信息系统、板和板内设计,Verilog也是自顶向下设计各层模块的。按照一般人类能够处理的能力而言,建议最底层的模块设计到小规模集成电路的水平(就是《数字电子技术》实验课里面,什么74系列芯片的样子)比较合适。这个没有定论,不是强行规定。
在开发过程中,模块一般是按照逻辑功能进行划分的。因此模块的命名也是按照功能进行的,如通信模块、显示模块。PCB上的芯片用导线/传输线连接,类似的,Verilog语言也可以表达模块如何连接(“必须的”)。唯一的不同点在于:PCB 不能设计芯片里的功能,而我们设计的却正是芯片本身。
可能又有细心的听众质疑这种开发了:这样按照纯粹功能划分模块,会不会在逻辑上不是最优呢?这个担心是有道理的。但是在目前这么发达的电子设计自动化(Electronic Design Automation,EDA)的前提下,这样问难免有杞人忧天的嫌疑。简单说,就是您老太小看综合软件了。可以这么说,综合的第一步就是把所有模块打散,变成一个大的平铺的“电路”,然后才进行优化。对于时序设计,可能综合软件的帮助不大,但是对于组合逻辑的优化,一般人脑还真的比不过软件。
鄙人从来没做过管理工作,以上只是点到为止,说一说鄙人在这么多年工程实践里的感觉,难免挂一漏万,请诸位斧正。如果需要更加专业的介绍,请查考专门论述项目管理,尤其是硬件项目管理的专著。
要讲解Verilog语言的语法需要从最底层的模块讲起,这个和设计的顺序是恰好相反的。有点嘲讽意味的是,这看似截然不同的两种叙述方法的理由却是惊人的相同:人脑的局限性。
好了,书到紧关节要之处了,大家严肃点。现在遇到了一个关键的Verilog关键词:module,汉语翻译就是模块的意思。这个关键词module与endmodule配对,标记代码中的一个模块,也可以说,它是一个逻辑的“芯片”。一个抽象的简单模型如下:
module module_name(ports_table);
ports_description;
begin
module_behavior description;
end
endmodule;
其中
“module_name”(模块名称)的起名,请遵守有关的命名规则。总的原则是,让人一看到这个名字就大约知道这个模块是做什么用的。有些人总是喜欢U1、U2之类的名称,这样不好。这样说吧,如果将来挨了批评什么的,乃至因此丢了饭碗,别找贫道拼命。
“ports_table”(输入输出端口/管脚列表),起到罗列出芯片管脚(pin)作用。理论上说,一个有意义的模块,总是对输入数据进行某种处理(也即功能部分)然后输出出去。因此,输入输出总是难免的。在Verilog 2001中,在输入输出端口/管脚列表中也可以完成有关输入输出端口/管脚列表的定义。
“ports_decription”(输入输出端口/管脚定义),完成确定端口信号方向和位宽的作用。端口信号既可以是1 bit的,也可以是多位位宽的。如上所述,这部分可能被输入输出端口/管脚列表收编。端口名称不能重复定义,否则会引起混乱(这个不必细说,地球人都知道)。
“module_behavior description”是模块真正的功能部分(这部分超出了本章的范围,在下将会在后文书给大家详细介绍)。
最后值得注意的是module那句话后面也有分号“;”,很多人在最初做代码时容易忘掉它。然后嘛,自然综合器会提示语法错误,这个错误还比较难查。
图2.10中的模块(这里还是抽象模型,不详细说明。工程中的一个实例就是半加器,当然也可以是其他单元)具有两个输入信号in_1和in_2和两个输出信号out_1和out_2。例2.14和例2.15就是两种不同的描述方法(请先忽略信号的类型,紧接着会唠叨得您老吐了为止的)。
图2.10 抽象的双输入、双输出端口
【例2.14】具有端口定义部分的模块描述
module abstraction_module(in_1,in_2,out_1,out_2);
//Ports Description
//Inputs:
input in_1; //First input signal
input in_2; //Second input signal
//Outputs:
output reg out_1; //First output signal
output reg out_2; //Second output signal
//Module Operation
begin
module_behavior description;
end
endmodule
【例2.15】具有端口列表中完成定义的模块描述
module abstraction_module(input in_1,input in_2,output reg out_1,output reg out_2);
//Inputs:
//in_1: First input signal
//in_2: Second input signal
//Outputs:
//out_1: First output signal
//out_2: Second output signal
//Module Operation
begin
module_behavior description;
end
endmodule
这两个例子中的有关模块定义是等效的,工程师可以根据自己的喜好选择。理论上,无须强行规定。如果有公司有规定,则是项目管理的需要而非语法的要求。
空行部分按照标准的说法是:“空白包括空格符、制表符(tab)、回车,处理时刻均忽略。”代码中保留部分空行,可以有效分割各个功能部分,提高代码的可读性。在下有这个习惯,绝非为了多赚几个稿费,特此声明。
另外,以“//”开头的部分是有关注释,不参与有关综合和仿真。有些注释是为了标记不同功能区的分割,更多的是为了说明有关功能。当然,在正式的代码中,例子里的“First input signal”(第一个输入信号)基本属于废话,不建议出现。要注释就真的说明一下信号的性质,如“高电平有效的复位”等。以“/*”开头和“*/”结束的部分也是注释,这个中间的内容也不做任何处理。
例2.15里的同方向、同位宽的信号可以合并,写成例2.16的样子。但是对于这样的形式,鄙人十分的不推荐,其可读性不好。
【例2.16】同方向、同位宽的信号可以合并的模块描述
module abstraction_module(input in_1,in_2,output reg out_1,out_2);
//Inputs:
//in_1: First input signal
//in_2: Second input signal
//Outputs:
//out_1: First output signal
//out_2: Second output signal
//Module Operation
begin
module_behavior description;
end
endmodule
输入、输出信号也可以是多位位宽的,描述方法在前文书中已经介绍了,这里不再多说。例2.17是图2.9的一个扩展,其中in_1、in_2和out_1都是4比特信号,out_2保持1比特位宽。这是什么呢?这就是老道最喜欢忽悠的4比特半加器。
【例2.17】具有多位位宽端口的模块描述
module abstraction_module(in_1,in_2,out_1,out_2);
//Ports Description
//Inputs:
input[3:0]in_1; //First input signal,4 bits
input[3:0]in_2; //Second input signal,4 bits
//Outputs:
output reg[3:0]out_1; //First output signal,4 bits
output reg out_2; //Second output signal,1bit
//Module Operation
begin
module_behavior description;
end
endmodule
上面例子中的Verilog关键字input表示该信号是输入信号,类似的,output表示该信号是输出信号。
另外,还有一个非常特殊的关键词inout。这个关键词对应着很多计算机系统中的数据总线之类的信号。这个信号既可能是输入,也可能是输出,具体方向由系统中的“控制器”发出的控制信号(一般是flag--标志)决定。具体如何使用,将在后文书中给出,先存着这里不表。还需要说明的是,这个信号只能出现在电路系统最外层的模块的端口处,不建议用于内部模块的连接。也就是说,只有和真实物理芯片管脚连接的信号,才能被定义为inout类型。
不是每种信号类型和每种端口类型都匹配的,请注意:这个和输血一样,混乱了要死人的。表2.9给出了信号类型和端口类型的对应匹配关系。具体这么匹配的原因,请从电路角度思考。当然,不思考硬背也未尝不可,毕竟这个表中的内容不多。
表2.9 信号类型和端口类型
模块外的情况,就涉及模块的实例化和模块调用的问题了,请看下节分解。还有一种带参数的模块定义方法,属于高级层次了。目前听众还没有到达那个层次,为了使大家不混淆,暂时不讲,后面也有介绍。
正如上文书说到的,能够被一个模块完成全部功能的系统,了不起也就是学生练习作业的水准,估计连课程设计的难度都达不到。因此,这里套用一句广告词:学会模块之间的连接,很有必要。
一般把内部没有其他模块的模块叫作底层模块,同时,把包含其他模块的模块叫作顶层模块。在一般的系统架构图中,顶层模块是外面的大方块,底层模块则画在大方块的内部,如图2.11所示。顶层模块内可以包含多个其他模块(底层或顶层皆可能)。这些内部的模块既可能是相同的,也可能是不同的。再说一句放之四海而皆准的废话:模块之间的连接是要满足某种目的的,不能瞎连乱连。
图2.11 模块层次的系统架构图
顶层模块的存在,可能有两个作用:一是满足所谓前面提到的“自顶向下”设计的思路;二是为了“偷懒”。举一个例子,例如,系统需求是实现一个复数加法运算。可以如图2.12所示那样,先做一个实数加法的底层模块,然后用两个实数加法模块在顶层复数加法模块里并列,来完成系统的需求。当然,对于这个例子,直接写两个“+”也不难。但是,如果被重复的底层模块十分复杂,则这样的方法就几乎成为必然了。如果您老愿意笔耕不倦地敲字母,“All work and no play makes Jack a dull boy ”,鄙人只好学星爷的台词了:“I服了you(我服了你)。”(心里独白:陕西话——瓜娃,粤语——死蠢,重庆话——哈搓搓,吴语——港督)
图2.12 复数加法模块的层次结构
强调一点,您老可以“偷懒”式将模块例化,综合软件却绝对是一个“傻孩子”。每个例化的模块都会用独立的电路实现。换句话讲,就是你重复例化同一个模块几遍,就消耗几倍的这个模块的电路资源。
模块的被引用,在术语里叫作例化。模块的代码可以被视为一个芯片库,里面有很多可选用的芯片;模块的例化,就等于把需要的芯片焊接到PCB上面。只有例化的模块,才是系统真正的有机组成。
模块的例化的主要问题是上层信号与模块端口的连接,有两种写法:第一种,按照模块端口列表的顺序,直接写上层信号;第二种,采用端口引用的方法,格式是.port_name(signal_name),port_name 是模块端口的名称,signal_name 是上层信号的名称。如果采用第二种方法,姑且称为对点名,可以不考虑模块实现的端口列表的顺序。
以上面的附属加法模块为例,例2.18是实数加法模块的抽象实现,不涉及具体功能,例2.19和例2.20对应上面说到的模块例化的方法1和方法2。在例2.20中,故意改变了有关虚部加法的端口顺序,看起来有点怪怪的。这样写的目的是为了演示,请大家理解。
【例2.18】实数加法模块的抽象实现
module real_adder(in_1,in_2,out_result);
//Ports Description
//Inputs:
input[7:0]in_1; //Operate number 1,7 bits
input[7:0]in_2; //Operate number 2,7 bits
//Outputs:
output reg[8:0]out_result; //Result=in_1+in_2,8 bits
//Module Operation
begin
module_behavior description; //Result=in_1+in_2
end
endmodule
【例2.19】复数加法模块:顺序引用连接(方法1)
module real_adder(real_1,real_2,image_1,image_2,real,image);
//Ports Description
//Inputs:
input[7:0]real_1; //Real part of operate number 1,7 bits
input[7:0]real_2; //Real part of operate number 2,7 bits
input[7:0]image_1; //Image part of operate number 1,7 bits
input[7:0]image_2; //Image part of operate number 2,7 bits
//Outputs:
output[8:0]real_part; //Real part of result,8 bits
output[8:0]image; //Image part of result,8 bits
//Module Implement
real_adder realpart_adder(real_1,real_2,real_part);
//Real part operation
real_adder imagepart_adder(image_1,image_2,image);
//Image part operation
//Module Operation
begin
module_behavior description; //Result=in_1+in_2
end
endmodule
【例2.20】复数加法模块:点名引用连接(方法2)
module real_adder(real_1,real_2,image_1,image_2,real,image);
//Ports Description
//Inputs:
input[7:0]real_1; //Real part of operate number 1,7 bits
input[7:0]real_2; //Real part of operate number 2,7 bits
input[7:0]image_1; //Image part of operate number 1,7 bits
input[7:0]image_2; //Image part of operate number 2,7 bits
//Outputs:
output[8:0]real_part; //Real part of result,8 bits
output[8:0]image; //Image part of result,8 bits
//Module Implement
real_adder realpart_adder(.in_1(real_1),.in_2(real_2),.out_result(real_part));
//Real part operation
real_adderimagepart_adder(.out_result(image), .in_1(image_1), .in_2(image_2));
//Image part operation
//Module Operation
begin
module_behavior description; //Result=in_1+in_2
end
endmodule
如果调用自己的模块出现错误,请自行面壁。如果调用的是别人的模块,包含调用IP核(Intellectual Property core)的情况,请务必详细阅读该模块的说明——鄙人见过太多望文生义,附加打包错误连连的情况了。
最后,罗列一些不建议在代码里出现的冷知识。
· 允许模块例化时,上层信号与模块定义的类型不一致(这叫作位宽不匹配),这时会按照Verilog语言规定的类型信号赋值转化原则进行位宽调整。
· 上层信号的位宽可以与模块定义的位宽不一致(这叫作位宽不匹配),这时会按照Verilog语言规定的不同位宽信号赋值原则进行缩位或扩展。
· 点名例化连接方法允许模块端口悬空,格式是.port_name(),括号里是空的即可。仿真的处理原则是:输入管脚悬空,该管脚输入为高阻 Z;输出管脚悬空,该管脚废弃不用。
这正是:
“三环套月名冲霄,机关重叠人难料。自顶向下流程妙,模块层次系统晓。定义例程两路标,例化实现双线桥。系统架构芯志聊,电路设计下面教。”