Verilog语法概述(一)

(一)这是我的第一篇blog,有一点激动捏!

  第一次写这类文章,如有不当之处,欢迎大家批评指正!感谢wwd同学的指正!

关于开发工具

  推荐使用Vivado + ModelSim 或 Quartus Ⅱ + ModelSim,这基本上就可以完成大部分工程。

关于练习

  推荐一个Verilog练习网站:HDLBits

(二)为什么选择Verilog?

  目前主流的HDL(Hardware Description Language)有VHDL和Verilog。VHDL语法比较严谨;而Verilog类似C语言,风格相对自由。本质上,语言之间其实并不存在优劣之分,只有适合与否。如果你有一定的C语言基础,不妨先学习Verilog,毕竟谁不想轻松的度过语法适应期呢?不过呢,当你掌握一门语言之后,最好还是了解一下另一门语言,毕竟搬砖也是一项技术活!

(三)HDL语言不是软件语言!

  相信大家或多或少有利用C语言或其它软件语言进行编程,程序是顺序执行的。但是HDL语言描述的是硬件电路,而硬件电路是并行处理的,这一转变或许会成为许多初学者的绊脚石。因此我们在学习语法的同时要做到心中有电路,而不仅仅是一串代码。那么,怎么判断代码描述的电路是否正确呢?写完一段代码后,多花一些精力对比实际逻辑电路(切忌偷懒哦),必要时可以做一些仿真,条件允许的话,可以在实验板上做一些测试。假以时日,代码与电路定会成为相伴而行的“老夫妻”。

(四)可综合语法与行为级语法

  可实现为硬件电路的语法称为可综合语法;不能实现到硬件电路中,却常常可作为仿真验证的高层次语法称为行为级语法。这一区分,使得入门的难度降低了许多,因为可综合语法只是一个很小的子集,或许当你完成一个实际系统设计时,你会惊奇的发现也就十几条语法。
  行为级语法很无奈的说:“要不你试试用我做仿真吧。”
在这里插入图片描述
  值得注意的是,本篇将只会对基本语法进行简单的介绍。详细的讲解带来的多半是困倦,实战中踩下的每一个坑都将促使你成长。那么,让我们开始一段新的学习之旅吧!

可综合的语法子集

  下面是一些常用的RTL级的Verilog语法及其简单的用法描述。我们可以将Verilog与C语言类比记忆,但是,一定要牢记Verilog描述的是并行处理的硬件电路。请注意,代码示例中的<>符号是为了标识中文,并无其它特殊含义。

(1)模块声明类语法:module…endmodule

  该语法在每一个Verilog文件中都会出现,是一个相对固定的用法,格式如下:

module module_name(<端口信号列表>...);
	<逻辑代码>...
endmodule

  值得注意的是,module_name是模块的名称,可以是数字、下划线和字母的组合。为了后续维护方便,尽量利用模块英文名称的缩写命名或者根据用途差分模块命名。端口信号列表中应当罗列出该模块所有的输入/输出端口信号名。

(2)端口声明:input, output, inout(inout用法比较特殊,将在后续实例中详细介绍)

  我么可以将硬件电路分为许多的部分,每个部分完成指定的功能,也就是我们前面提到的module。很显然,不同的module之间需要通过信号线连接。对于某一个module来说,这些信号便可以分为输入(input)信号、输出(output)信号和双向(inout)信号。
  你肯定还没有忘的是:你可以在module语法的端口信号列表中声明该模块用于与外部接口的信号。你将会面临两个选择:

  1. 在端口信号列表中直接声明输入输出类型。
  2. 在逻辑代码部分声明端口的输入输出类型。

  具体如下:

// 1. 在端口信号列表中直接声明输入输出类型。
module module_name(input clk, input rst_n, output [7:0] data_out);
	<逻辑代码>...
endmodule

// 2. 在逻辑代码部分声明端口的输入输出类型。
 module module_name(clk, rst_n, data_out);
	input clk;
	input rst_n;
	output [7:0] data_out;
	...
endmodule

  第一个和第二个声明均表示1bit的输入信号端口,而第三个表示8bits的名称为data_out的输出信号。另外,对于我来说,我更加习惯于第一种选择,即在端口信号列表中直接声明输入输出类型,因此之后的例子都将以第一种方式呈现。

(3)参数定义:parameter

  parameter主要用来声明一些常量(虽说是常量,但可以利用defparam修改,我们可以先不考虑),使得模块移植和升级变得更加方便,同时,可读性大大提高。反正我才不想因为一个参数的修改去翻看我那又臭又长的代码!
  仍然以前面的代码为例:

// 2. 在逻辑代码部分声明端口的输入输出类型。
 module module_name(clk, rst_n, data_out);
	input clk;
	input rst_n;
	output [7:0] data_out;

	parameter PERIOD = 1000000;
endmodule

  可以看到,此处我采用大写字母进行参数定义,这是为了与普通内部信号进行区分,另外,参数命名时仍然建议体现参数的含义。

(4)信号类型:wire, reg 等

  这是一个比较重要的概念。累了的话,大可以休息一下,再接着战斗。其实是我累了,哈哈哈哈哈。
  如下图RTL电路所示,在时钟上升沿到来时,reg将会锁存最新的输入数据,而wire就是这两个reg之间的直接连线。
reg与wire示意图
  值得注意的是:作为 input 或 inout 的信号端口只能是wire类型,而output则可以是wire也可以是reg。
  一些常见用法如下:

// 定义一个wire信号
wire <变量名>;

// 给一个定义的wire信号直接连接赋值
wire <变量名> = <常量或变量赋值>;
// 这实际上等价于如下形式:
wire <变量名>;
assign <变量名> =  <常量或变量赋值>;

// 定义一个多bit的wire信号
wire [<最高位>,<最低位>] <变量名>;

// 定义一个reg信号
reg <变量名>

// 对reg信号赋初值
// 这与前面所说的wire信号赋值是不同的
// wire的赋值将在右边操作数发生变化时便重新执行
reg <变量名> = <初始值>

// 定义一个多bit的reg信号
reg [<最高位>,<最低位>] <变量名>;

// 定义一个赋初值的多bit的reg信号
reg [<最高位>,<最低位>] <变量名> = <初始值>;

// 定义一个二维的多bit的reg信号,可以想象成一个矩阵
reg [<最高位>,<最低位>] <变量名> [<最高位>,<最低位>];
(5)多语句定义:begin…end

  这其实很好理解,就类似与C语言中的“{ }”。主要有两种形式:

// 含有命名的begin语句
begin : <块名>
	// 逻辑
end

// 基本的begin语句
begin : 
	// 逻辑
end
(6)比较判断:if…else, case…default…endcase

  这与C语言也是非常类似的,但是我们仍然需要特别注意硬件电路是并行执行的。那么,该语法应该对应到什么样的电路呢?直接举个例子吧。

// 以下代码实现了在clk上升沿到来时,输出两个输入中的较小值
module test(
    input din1,
    input din2,
    input clk,
    output reg dout
    );
    always@(posedge clk)
    begin
        if(din1 < din2)
            dout <= din1;
        else if(din1 > din2)
            dout <= din2;
        else
            dout <= dout;
    end
endmodule

  相对应的RTL级电路如图所示,我们可以发现if…else语句可通过多路复用器实现,并实现并行计算。同时,我们还可以发现,两个复用器的控制信号均为RTL_LT的输出,这是由于上述代码中的小于判断具备更高的优先级。大家可以自行与C语言中的if…else语句作一个比较,相信大家会对并行执行与顺序执行有更加深入的理解。

在这里插入图片描述
  
  
  case语句与if…else的硬件实现并没有什么本质不同。与C语言中类似的是:若分支较多则可以考虑采用case语法,原因何在?将上述代码做一些修改,并查看其RTL级电路:

module test(
    input din1,
    input din2,
    input clk,
    output reg dout
    );
    wire temp;
    assign temp = (din1<din2);
    always@(posedge clk)
    begin
        case(temp)
            1'b1:dout <= din1;
            1'b0:dout <= din2;
            default:dout <= dout;
        endcase
    end
endmodule

在这里插入图片描述
  我们发现使用case语句占用的资源变少了,这是因为case语句中的各分支不具有优先级。
  因此,如果你的设计中对各分支条件并没有优先级的要求,完全可以考虑采用case语句;反之,if…else语句将会成为较优选择。

(7)循环语句:for

  for循环用的比较少,且结构与C语言类似,其示例如下:

for{<变量名> = <初始值>; <判断表达式>; <变量名> = <新值>}
begin
	<具体逻辑>
end

  读者可以尝试编写一段简单的for循环代码,并查看其RTL级电路。

(8)任务定义:task…endtask

  为了便于理解,我们可以将task与C语言中的子函数类比。task中也可以有input、output 和 inout端口作为出入口参数,并可以实现时序控制。值得注意的是:C语言子函数可以有返回值,而task是没有返回值的。但其出口参数可以实现参数返回,类似于C语言中的指针。基本用法如下:

task <task_name>
	// 可选声明部分,例如localparam
	begin
	// 具体逻辑
	end
endtask

  在这里依然是简要介绍,将在具体实例中展示其具体用法。

(9)连续赋值:assign 和 A?B:C

  assign,按照我浅薄的知识理解,我们可以把等号两边的信号用导线相连接,这与你在现实中拿导线把两个端口连接起来没有区别,这便是赋值的概念。也因此,这样的连接跟时序无关,只要电压的值改变了另一端的值也会同时改变,这便是连续的概念。所以只能利用assign对wire类型变量进行赋值,而不能对reg变量赋值。用法如下:

assign <wire变量名> = <变量或常量>;

  对于“?:”,这本质上就是一个简单的if…else语句。区别在于:“?:”主要用在组合逻辑中(可搭配assign使用),而if…else时常用在时序逻辑中。例如:

assign dout = (din1>din2)?din1:din2;

我们可以看看其RTL级电路与(6)有什么区别?
?:电路图
  可以发现该电路与(6)中case语句对应的电路仅相差了一个触发器,这说明“?:”语句也不具有优先级。另外,虽然只相差一个触发器,但实现的功能却相差甚远,读者可自行思考(可考虑赋值的时刻)。

(10)always模块:

  用法如下:

always@(<敏感表>)
begin
	<具体逻辑>
end

  敏感表可以为电平、边沿(上升沿posedge/下降沿negedge),也可以是“ * ”,这代表所有输入信号。
  一般来说,如果敏感表为电平,则表示电平发生变化时,将执行此块逻辑;如果敏感表中有边沿信号,将在到达指定边沿时执行逻辑,因此多为时序逻辑。

(11)运算操作符:

  各种逻辑操作符、移位操作符、算术操作符大多是可综合的。如下所示:

+				//加
-				//减
!				//逻辑非
~				//取反,注意与!区别
&				//位与
~&				//与非
|				//位或
~|				//或非
^				//异或
^~				//同或
~^				//同或
*				//乘,是否可综合需要看综合工具
/				//除,是否可综合需要看综合工具,一般要求除数为整数,且为2的指数
%				//取模
<<				//逻辑左移
>>				//逻辑右移,高位补零
<				//小于
<=				//小于等于
>				//大于
>=				//大于等于
==				//逻辑相等,仅能判断0与1是否相等,若存在高阻态或不定态,则结果为不定态。若存在不定态或高阻态,在仿真中可以考虑使用全等符号===,但其不可综合。
!=				//逻辑不等,可与==类比
&&				//逻辑与
||				//逻辑或
{}              //位拼接符,可将两个或多个信号的某些位拼接起来进行运算。
(12)赋值符号:= 和 <=

  “=”和“<=”分别表示阻塞和非阻塞赋值,将在后续实例中进一步介绍。

(五)结束语

  可综合语法只是Verilog中很小的一个子集,硬件设计的精髓是用最简单的语句描述最复杂的硬件。因此,对于做RTL级设计来说,掌握好上面这些基本语法是非常非常重要的!
  下一篇将谈谈代码风格与书写规范,这一问题并没有标准答案,但好的代码风格和书写规范可以有效的提高FPGA设计的效率。

  • 9
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: Verilog是一种用于描述数字电路的硬件描述语言,它可以用于描述UART(通用异步收发器)。下面是一个简单的Verilog代码模型: ``` module UART (input clk, input rst, input rx, output reg tx, output reg busy); parameter BAUD_RATE = 9600; reg [7:0] rx_data; reg [7:0] tx_data; reg [11:0] counter; reg [11:0] baud_counter; reg start_bit; reg [3:0] bit_counter; reg rx_done; reg tx_done; always @(posedge clk) begin if (rst) begin counter <= 0; baud_counter <= 0; start_bit <= 0; bit_counter <= 0; rx_done <= 0; tx_done <= 0; busy <= 0; end else begin if (baud_counter == BAUD_RATE-1) begin baud_counter <= 0; counter <= counter + 1; end else baud_counter <= baud_counter + 1; if (start_bit) begin if (bit_counter == 0) tx <= 1'b0; else if (bit_counter == 9) tx_done <= 1; else tx <= tx_data[bit_counter-1]; bit_counter <= bit_counter + 1; end if (rx & ~rx_done & (counter == 0)) begin rx_data <= 0; start_bit <= 1; bit_counter <= 1; end else if (start_bit & (counter == 1)) begin rx_data <= {rx_data, rx}; bit_counter <= bit_counter + 1; end else if (bit_counter == 9) rx_done <= 1; busy <= ~rx_done | ~tx_done; end end endmodule ``` 这是一个简单的UART Verilog代码示例,它可以作为你开始写自己的UART模块的基础。注意,这仅仅是一个示例,可能不适用于所有的应用场景 ### 回答2: Verilog是一种硬件描述语言,可以用它来编写各种数字逻辑电路的描述。要编写一个UART(通用异步收发器),我们可以使用Verilog来描述其功能和行为。 UART是一种串行通信接口,用于在计算机和外部设备之间传输数据。它通常用于与外部设备(如传感器、显示器、打印机等)进行数据交换。 一个简单的UART模块可以包含以下主要模块: 1. 预分频器(Baud Rate Generator):根据设定的波特率(Baud Rate)来生成时钟信号。时钟信号用于控制数据位的发送和接收。 2. 发送数据位(Transmitter):根据发送数据缓冲区中的数据,按照指定的波特率和格式发送数据。 3. 接收数据位(Receiver):监听接收数据线上的数据,并将其存储到接收数据缓冲区中。 4. 状态机(State Machine):根据传输的状态,决定UART的操作模式、数据传输流程,以及错误处理。 5. 缓冲区(Buffer):用于暂存要发送或接收的数据。 在Verilog中,我们可以使用模块实例化、时钟控制、状态机和数据传输等Verilog语法来描述UART的行为。我们需要定义模块的输入输出接口,定义状态机的状态和状态转移,以及实现数据的发送和接收。 一个UART模块的Verilog代码可能会类似于以下示例: ```verilog module uart ( input clk, input reset, input data_in, output data_out ); //其他端口和内部模块的实例化和代码实现 endmodule ``` 这只是一个简单的示例,实际上UART模块的实现较为复杂,需要处理时钟同步、数据格式、错误处理等。具体的实现细节需要根据具体的需求和功能来定义。 总结来说,使用Verilog语言编写一个UART模块需要定义模块的输入输出接口、状态机、缓冲区以及实现数据的发送和接收功能。这些功能可以通过合理的状态机设计和时钟控制来完成。编写Verilog代码需要具备一定的数字电路设计和Verilog语言的基础知识。 ### 回答3: 用Verilog语言编写UART(通用异步收发传输器)是一种常见的硬件设计任务。UART是一种支持串行通信的芯片,可以将并行数据转换为串行数据,以便在电路之间传输。下面是使用Verilog语言实现UART的概述。 首先,需要定义UART的输入和输出接口。输入接口包括一个时钟信号、一个复位信号和一个用于接收数据的串行输入信号。输出接口包括一个时钟信号、一个复位信号和一个用于发送数据的串行输出信号。 接下来,需要定义一个状态机来实现UART的收发功能。状态机应该包括各种状态,例如接收数据、发送数据等。根据输入信号和当前状态,状态机将转换到相应的下一个状态。 在接收数据的状态下,需要使用一个移位寄存器来接收串行输入数据。每当收到一个有效的数据位时,将数据位移至寄存器中。当所有数据位都接收完毕后,可以进行数据处理或者发送到其他模块。 在发送数据的状态下,需要使用一个移位寄存器来将串行输出数据逐位地传输到串行输出信号上。当所有数据位都发送完毕后,可以转换到其他状态。 最后,在顶层模块中实例化所有的子模块,并将它们连接起来。顶层模块还应该处理复位信号、时钟信号以及其他与系统相应的信号。 这只是一个基本的框架,实际的实现中还需要考虑很多细节,例如错误检测、波特率控制等。通过编写合适的Verilog代码,可以实现一个功能齐全的UART模块,用于数据的收发。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值