我的西皮优学习笔记(一)->Hardware Description Language

硬件描述语言

当下比较流行的HDL(Hardware Description Language)分别是System Verilog 和 VHDL

只说明逻辑功能,同时引入计算机辅助设计工具来生成优化的门电路,可以获得更高的设计效率。

1)概述

模块

包括输入和输出的硬件块称为模块

分类:

  • 行为模块:
    • 描述一个模块做什么
  • 结构模块:
    • 应用层次化方法描述一个模块怎样由更简单的部件构造

示例:

表示一个 y =!a* !b * !c+a * !b *!c + a *!b *c 的模块

module silllyfunction(input logic a,b,c,
					output logic y);
assign y = ~a & ~b & ~c|
			a & ~b & ~c|
			a & ~b &  c;
		
endmodule : silllyfunction

理解:

一个SystemVerilog 模块以模块名,以及输入和输出列表开始,assign 语句描述组合逻辑

~代表非,&代表与,|代表或

输入输出等logic 信号是布尔变量(0和1)

logic 变量类型在Verilog中引用,取代reg变量类型,reg在Verilog语言中长期导致概念混淆,logic可以用于任何地方,除了具有多个驱动的信号外,具有多个驱动器的信号称为net

模块是模块化的一个好应用,很好的定义了由输入和输出组成的接口,并且它执行特定的功能,它编码的内容对于调用该模块的其他模块并不重要,只要它执行自己的功能。

HDL的起源

工业界倾向于使用Verilog 但是很多公司在使用VHDL,而VHDL作为一个委员会提出的语言,冗余而不够灵活。

SystemVerilog 作为一个逻辑模拟的专利语言开的,文件名一般以.sv 结束

VHDL 是美国国防部开发的,文件名以 .vhd结束

模拟和综合:

逻辑模拟:simulation

综合 :synthesis

综述:在模拟阶段会给模块增加输入,并检查输出以验证模块操作是否正确。在综合阶段会将模块的文字描述转换成逻辑门

模拟 simulation

我通过看波形发现模拟其实就是给模块添加的输入的信号,或者说

比如上面的abc ,在模拟的时候就会从000 开始二进制增一向输入端输入信号,本质是把电位置为高电平或者低电平

高电平的话看波形就是高位的,低电平的话就是低位的

同理同一时间段如果a是高位bc是地位就代表100 对应的逻辑单元过来是0的时候,就是低电平,或者说模拟就是提供信号,看看最后的输入和预想的是不是一样的。

看这个波形就能看出来

我自己运行这个模块的时候没有编写simulation文件也就是没有给这个模块以输入所以abc才会是高阻态,y是未知态

综合 synthesis

综合就是将HDL转换成网表

就是用逻辑门原理图的形式绘制出来,让电路可视化。

HDL的电路描述和编程语言中的代码很相似,但是要区分开这个代码是描述一个硬件的。
具体的是把HDL代码分成可综合模块(synthesizable)和测试模块(testbench)。可综合模块描述硬件(就是真正的去描述一个硬件的结果和功能),测试程序包含将输入应用于模块的代码,检测输出结果是否正确,并输出期望结果和实际结果的差别。

HDL中有一些特定的方法来描述各种逻辑,这些方法称为风格

2)组合逻辑

1、位运算符

位运算符对单位信号和多位总线进行操作。

示例:描述一个4连接到4位总线的反相器

module inv(
	input logic [3:0] a,
	output logic [3:0] y 
);

	assign y = ~a;
endmodule

分析:

a[3:0] 代表一个4位的总线,最高有效位到最低有效位是 a[3] ~ a[0]

这样书写,最低有效位的位号最小,称为小端

a [4:1] 同样是4位总线,最高有效位是a[4]

a[0:3] 最高有效位到最低有效位是a[0]~a[3] 称为大端

上面这串代码就是编写了一个模块,这个模块的输入a是4位总线,输出也是4位总线,功能是位取反

对于这一串的功能来说总线的字节顺序是任意选择的,字节顺序和运算符有关,只要前后保持一致,可以采用任意一种字节顺序。

2、注释和空白

给信号名和模块命名的时候使用大写字母和下划线,同时不能使用数字作为模块名和信号名的开头。

XOR是不可兼取或

NOR 是或完取反

module gates (
	input logic[3:0] a,b,
	output logic[3:0] y1,y2,y3,y4,y5		
);
	/*
	five different two-input logic gates
	acting on 4-bit busses
	*/
	assign y1 = a&b; //AND
	assign y2 = a|b; //or
	assign y3 = a^b; //xor
	assign y4 = ~(a&b); //NAND 
	assign y5 = ~(a|B); //NOR

endmodule

分析:

​ ~ ^ | 这些都是运算符

​ a、b、y1 这些都是运算数

运算符和运算数结合可以结合称为一个表达式 比如说:a&b

而assign y4 = ~(a&b) 称为是一条语句

而 assign out = in1 op in2; 这样的是一条连续赋值语句

右边一般以;号结尾,对应的连续赋值语句,右边的输入值如果改变那么左边输出值也会改变,因此一般用连续赋值语句来描述组合逻辑。

对于verilog来说:

​ 注释的方式就是/* */ 和 // 这和C语言是一样的,且大小写敏感。

3、缩位运算符

缩位运算符表示作用在一条总线上的多输入门,用于简化表达,对于或门,异或门,与非门,或非门和同或门也有类似的缩位运算符。

module and8 (
	input logic [7:0]a,
	output logic y	
);
assign y = &a;

//assign y = a[7]&a[6]&a[5]&a[4]&a[3]&a[2]a[1]&a[0]

endmodule

这里& a 就表示a内部八条线用与来连接,然后将与的结果赋值给y

4、条件赋值

就是描述多选几的复用器

module mux2 (
	input logic [3:0] d0,d1; //4位信号输入
	input logic s ; //一位二进制的选择信号
	output logic [3:0] y; //四位信号输出
);
	//Description 
	y = s ? d1 ? d0;
endmodul

描述出的电路为:

00

很明显这里二选一复用器就是通过三目运算符 ? : 来完成的

module mux4 (
	input logic [3:0] d0,d1,d2,d3,  //四位输入  
	input logic [1:0] s, //两位的选择信号,对应的可以四选一
	output logic [3:0] y	//四位输出
);
	assign y = s[1]?(s[0]?d3:d2)
				   :(s[1]?d1:d0);
endmodule

这样就完成了一个四选一的复用器,这里书上描述的复用器比较有意思,它是有多路数据d和独热使能e输入的,当有一有使能信号有效时就会传送到输出。

5、内部变量

一般的话把一共复杂的功能分为几个中间过程去完成就更加方便,比如说对于一个三输入(A,B,Cin)和两输出(S,Cout) 的全加器来说,可以将其功能转换为布尔代数

S = A (+)B(+)C 位运算加法进位置0,S表示本位

Cout = AB + ACin+BCin 满足两个输入为1就将进位置为1

这时如果定义中间变量 P G 用于表示:

P = A (+)B

G = AB

就可以重写全加器

S = P(+)Cin

Cout = G + PCin

这里的PG称为内部变量,因为他们既不是输入也不是输出,只在模块内部有效,相似于局部变量

对于HDL赋值语句assign 是并行执行的,所以在HDL中代码的顺序并不重要,和硬件一样右边的输入信号改变时 赋值语句就会被计算,而不考虑赋值语句在模块中的出现顺序

module fulladder (
	input logic a,b,cin,
	output logic s,cout	
);
logic p,g;
//赋值中间变量
assign p = a^b;   //在位运算逻辑上异或的效果和位加法是相同的
assign g = a&b;

assign s = p^cin;
assign cout = g | (p&cin);

endmodule

全加器

6、优先级

运算符含义
~NOT
* , / , %MUL,DIV,MOD
+ , -PLUS , MINUS
<<,>>逻辑左移,逻辑右移
<<< , >>>算数左移,算数右移
< , <= , >,>=相对比较
== ,!=相等比较
& ,~&AND,NAND
^ , ~^XOR, XNOR
| , ~ |OR, NOR
? :条件

7、数字

性质:

  • 数字一般使用二进制,八进制,十进制或者十六进制 来表示

  • 可以选择指定数字的大小,即位数

  • 也可以在数字的开头插入一些0来满足数字大小的要求

  • 数字中间的下划线会被忽略,所以可以通过下划线来划分较长的数字

常量:

通过N’Bvalue 的方式声明常量,N是位数,B是说明基数的字母,value是值

比如说:9’h25 就是声明了一个九位的十六进制数25等价于十进制数37

在verilog中通过**'b** 表示二进制数,'h 表示十六进制,'d表示十进制,'o 表示八进制

如果没有定义位数,那么就会当前表达式的位数赋予,0会自动的填补在数字的前面以达到满位,

示例:

比如说w 是六位总线

assign w = 'b11

则w = 000011

一般的说明位数肯定是更好的,但有一种特殊的使用就是,'0 和 '1 的用法用于将全 0 或者 全1 赋值给一条总线

8、Z和X

非法值X

符号X表示电路结点有未知或者非法的值,通常发生在这个结点同时被 0 / 1 驱动。

也就是这样的形态,结点Y同时被高电平和低电平驱动,这种情况称为竞争。一定要避免这种错误。

这时真实的电压可能处于0 ~ Vdd 之间,取决于驱动高电平和低电平两个门的相对强度,它经常但也不总是处于禁止区域内,竞争可能导致电路发热并可能被损坏

有时也被表示为没有始化的值

在真值表里表示不用关心的值

在电路种表示未知值或非法值

浮空值Z

符号Z表示结点没有被高电平驱动也没有被低电平驱动,这个结点称为浮空,表示高阻态

这里有一个典型的误解就是高阻态(浮空或者未驱动的结点)与逻辑0等同,而这个浮空结点可能是0也可能是1,也可能是0 ~ 1之间的电压,这取决于先前的状态

但出现浮空的状态并不意味着电路就出错了,一旦其他电路元件将这个结点驱动到有效电平,这个结点上的值就可以参与电路操作。

产生原因:

​ 忘记将电压连接到输入端。

结果:

​ 造成电路行为不确定。

示例:以三态缓冲器为例

在使能端为TRUE时,三态缓冲器只是作为一个简单的缓冲器,将值传往输出端,当使能端为FALSE时,三台缓冲器就是将输出端置为浮空(Z)

代码描述:

module tristate (
	input logic [3:0]a,
	input logic en,
	output tri [3:0]y	
);
	assign y = en ? a : 4'bz; //如果写使能为1 则 赋值为a的值,如果为0则赋值为高阻态

endmodule

分析:

​ 由于三态缓冲器有多个驱动信号,所以y应该是tri 变量类型而不能是logic 变量类型。

​ net 变量类型有tri 和 trireg两种类型

​ 对于net,一般来说,每次只有一个驱动信号源处于激活状态,net采纳该信号源的数值作为其信号参数,如果没有一个驱动信号处于激活状态,则tri信号将置为悬空态z,trireg 则保持先前的数值,

​ tri输出可以作为另一个模块的logic输入

小结:

看到这有些看不是很清晰了,那么非法值,或者说不定值X和高阻态的Z好像能理解他们的区别,那么他们都会展现出什么样的状态呢?

这边我找了一些网络上的理解:

高阻态可以理解为电阻无限大,当电路出现高阻态,对后续的电路是没有影响的,知识缺少了一个输入或者输出

而不定态就会造成可能采到低电平,也可能采到高电平,无法预估,这样因为输入错误对后续的电路的影响是不可逆的。

通俗点说,就是高阻态就是引脚没有接入电路,而不定态就是不知道当前的引脚是0 还是1

9、位混合

定义:

对于需要在总线的子集上操作,或者连接信号来构成总线的操作

这些操作称为位混合

assign y = {c[2:1],{3{d[0]}},c[0],3'b101}

这样一个语句表达出来的含义是九位数:C2 C1 d0 d0 d0 C0 1 0 1

分析:

{ }操作符用于连线总线

{3 {d [0] }} 表示d [0 ] 的三个拷贝

不同的部分之间通过使用 , 来连接

10、延迟

HDL的语句可以与任意单位的延迟相关联,这对于在模拟预测电路工作速度和调试需要知道原因和后果时就显得很有用。

比如说将之前的 y = a&b&~c + a&b&c + a&~b&c的原始功能上面增加延迟。

提前假定反相器的延迟是 1ns 3输入与门的延迟是 2ns ,3 输入或门的延迟是 4ns

module (input logic a,b,c,
        output logic y);
        logic ab,bb,cb,n1,n2,n3;  //定义中间变量
        assign #1 {ab,bb,cb} = ~{a,b,c};  //将ab,bb ,cb汇成总线 ,然后将对应的值取反赋值
        assign #2 n1 = ab & bb &cb;
        assign #2 n2 = a & bb &cb;
        assign #2 n3 = a & bb &c;
        assign #4 y = n1 | n2 |n3;
endmodule

11、结构化建模

二选一构建四选一
module mux2 (
	input logic [3:0] d0,d1; //4位信号输入
	input logic s ; //一位二进制的选择信号
	output logic [3:0] y; //四位信号输出
);
	//Description 
	y = s ? d1 ? d0;
endmodule
module mux4 (
	input logic[3:0]d0,d1,d2,d3,
	input logic[1:0] s,
	output logic[3:0] y
);
	logic[3:0] low,high;

	mux2 lowmux(d0,d1,s[0],low);
	mux2 highmux(d2,d2,s[0],high);
	mux2 finalmux(low,high,s[1],y);

endmodule

之前说的都是行为建模,根据需求需要的行为内容来构建模块,但是在模块之外也可以进行模块的调用,来建模。

分析:

​ 堆于模块的调用就必须将模块拷贝为一个实例,从上述代码中,lowmux hightmux finalmux 就都是mux2的实例

​ 同一个模的多个实例必须由不同的名字来命名区分,从而达到将 2:1 复用器重用了多次。

​ s[0] 未决定了d0 d1 选什么,同时把信号递给d2 d3 选什么

​ 最后选再从二者的信号中选出来最后的信号

逻辑图示:

s[0] 同时决定了两组选择

s[1] 决定一组选择

我怀疑这里本身并不是为了让第一步可以想选谁选谁,而是第一步同时选指定的两个,因为s[ 0]变化的话第一步是同时进行的,所以这里的选择并不自由。

布线图示:

三态缓冲器构建二选一
module tristate (
	input logic [3:0]a,
	input logic en,
	output tri [3:0]y	
);
	assign y = en ? a : 4'bz; //如果写使能为1 则 赋值为a的值,如果为0则赋值为高阻态
endmodule

module mux2(
	input logic[3:0] d0,d1,
	input logic s,
	output tri [3:0]y
);
	tristate t0(d0,~s,y);
	tristate t1(d1,s,y);
endmodule

分析:对于~s 这样的写法可以用在实例的端口列表中,虽然是合法的,但是不提倡。

访问部分总线
module mux2 (
	input logic [3:0] d0,d1; //4位信号输入
	input logic s ; //一位二进制的选择信号
	output logic [3:0] y; //四位信号输出
);
	//Description 
	y = s ? d1 ? d0;
endmodule
module mux2_8(
	input logic [7:0]d0,d1;
	input logic s,
	input logic [7:0]y
);
	mux2 lsbmux(d0[3:0],d1[3:0],s,y[3:0]);
	mux2 msbmux(d0[7:4],d1[7:4],s,y[7:4]);
endmodule

分析:

上述的这个module mux2_8 显示了一个模块如何访问部分总线,一个8位宽的2:1 复用器用两个已定义的四位2:1的复用构建,对字节的高半字节和低半字节分别进行操作。

图示:

一般来说,复杂的系统通过分层定义来实现。通过实例化主要的组件的方式来结构化的描述整个系统,而每一个组件又由更小的模块结构化的构成,然后进一步分解直到组件可以足够简单可以描述行为。

避免在一个模块中混合使用结果和行为描述是一种好的程序设计风格。

3)时序逻辑

1、寄存器

大多数的商业系统都是由寄存器构成的,这些寄存器使用正边沿触发的D触发器。

在Verilog中的always 语句中,信号保持他们原来的值直到敏感列表中的一个事件发生,该事件明显的引起他们的改变,因此具有合适敏感信号列表的代码可以用于描述有记忆能力的时序电路

比如说,触发器在敏感信号的列表中就只有clk,这说明下一个clk上升沿到来之前q都保持着原来的值,即使d在中途发生变化。

相反,在右边的任何一个输入发生变化时,verilog 的连续赋值语句assign 都会重新的计算值,因此这样的代码用于描述组合电路。

module flop(
    input logic clk,
    input logic [3:0]d,
    output logic [3:0]q
);
    always_ff @(posedge clk)
        q<=d;
endmodule

分析:

​ 一般来说verilog的always 语句写成下面的形式:

always @(sensitivity list)
	statement

对于sensitivity list 中说明的事件发生时才执行statement ,这里 q<= d 读作q 得到 d

简单来说就是 在时钟正边沿的时候将 d复制给q,否则q 保持原来的状态。

对于敏感信号列表也可指 激励信号列表

< = 称为非阻塞赋值,这时可以把它看成是一个普通的等号,在这里简单来说就是通过< =来代替了assign

always 可以表示触发器,锁存器或组合逻辑,取决于敏感信号列表和执行语句。而因为有这样的灵活性才会在不经意间制造出错误的硬件电路。

因此verilog 引入了 always_ff always_latch always_comb 来降低产生这些常见错误的风险,always_ff 的行为和always 一样,但是只用来表示触发器,并且如果他们用于表示其他器件时,允许工具生成警告信息。

2、复位寄存器

module floper(
    input logic clk,
    input logic reset,
    input logic [3:0]d,
    output logic [3:0]q
);
//asynchronous reset 异步复位
always_ff @(posedge clk,posedge reset)
    if (reset) q<=4'b0;
    else       q<=d;
endmodule

module floper(
    input logic clk,
    input logic reset,
    input logic [3:0]d,
    output logic [3:0]q
);
//synchronous reset 同步复位
always_ff @(posedge clk)
    if (reset) q<=4'b0;
    else       q<=d;
endmodul

分析:在always 语句敏感信号列表中的多个信号之间通过逗号去分隔,需要注意的是

posedge reset 只在异步触发器的敏感列表中,而不在同步复位触发器的列表中

所以异步触发器会马上响应reset的上升沿,但是同步复位触发器只在时钟的上升沿响应reset。

异步复位

从原理图上基本上很难去分别出来异步复位和同步复位。

3、带使能端的寄存器

module flopener(
    input logic clk,
    input logic reset,
    input logic en,
    input logic [3:0]d,
    output logic [3:0]q
);
    //asynchronous reset
    always_ff @(posedge clk,posedge reset)
        if (reset)
            q<= 4'b0;  //先给它复制使他不至于悬空
        else if(en)
            q<=d;  //使能有效时才保证具有正确的值

endmodule

a、多寄存器
module sync(
    input logic clk,
    input logic d,
    output logic q
);
    logic n1;
    
    always_ff @(posedge clk)
    begin
        n1 <= d;
        q <= n1;
    end
endmodule

分析:

因为有多条声明语句出现在了always 语句中,begin / end 的结构是必要的,类似于C中的{}

但是在上面floper 的例子中却不是必要的,因为if /else 是一条单独的语句

always 语句用于表示触发器,锁存器或组合逻辑,却决于它的敏感列表和执行语句。

因为 always_latch always_ff always_comb

分别代表的含义是:

  1. comb是combinational的缩写,代表作者想写一个组合逻辑电路,也没必要写敏感列表了,写组合逻辑电路的时候一不小心就会搞出一个latch

    比如说:描述一个组合逻辑电路

    always_comb //组合逻辑
      if(a > b)
        out = 1;
      else
        out = 0;
    

    但是如果使用always描述成:

    always_comb //错误
      if(a > b)
        out = 1;
    

    因为描述组合逻辑电路ifelse 必须完整,所以这里的always_comb 就会告知综合工具,这时就会收到警报,这里应该是组合逻辑但是写成了latch,但是使用always 语句的时候这个情况就不会报错

  2. always_latch(latch 就是锁存器)

     always_latch //latch
    
      if(clk)
        out = in;
    

    如果真的需要latch 那么这里always_latch 就是提供了一个latch关键字,表明这里是一共latch

    always_latch 是电平敏感的,它不需要敏感信号列表,它会告诉综合工具,这里我就是需要一个latch

    通过使用always_comb 和 always_latch 极大的减少了unintentional latch 的出现

  3. always_ff

    ff 是flip-flop触发器的缩写,而always_ff 就是对其配备的专用关键字

    它需要敏感列表,而且是边沿触发的,所以敏感信号列表中的信号的都加了关键字 posedge 或 negedge (分别表示时钟上升沿触发和时钟下降沿触发)

    一般情况下统一使用posedge 而不使用 negedge 进而降低设计的复杂性

关于同步器部分的内容,暂时搁置,后续需要学习状态机的相关内容

4、锁存器

moduel latch(
	input logic clk,
	input logic [3:0]d,
	output logic [3:0]q
);
	always_latch 
		if (clk) q<= d;
endmodule

不是所有的综合工具都支持锁存器,,除非直到工具支持锁存器,否原则最好使用边沿触发器。

总结:

到此为止verilog 的基本内容就已经学完了,还差一些复杂的组合逻辑,状态机和数据类型

但是在学时序逻辑的时候我发现我真正的问题处于对于时序逻辑电路的不扎实的基础,对于组合逻辑电路学了很多次所以还算是基本了解,但是对于时序逻辑电路,我发现基础很差,对于基本的时序逻辑电路的组件和组件使用的目的和特性都不理解,这可能也是我上学期做实验难以前进的最大一部分原因。

其实这部分原因我很早之前就知道,但是如今发现了这个问题,所以我下一阶段计划安排为期三天左右组合逻辑电路和时序逻辑电路的复习

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值