EGO-Link FPGA智慧教育社区介绍:用 leetcode 的方式练习 verilog 语言

介绍

本课程笔记是基于依元素科技公司与北京邮电大学开展的 FPGA 创新工坊课程学习 verilog 代码。

image-20230408080815380 image-20230408080713484

主要就是在其的社区平台上写 verilog 代码练习。平台链接:https://www.ego-link.com/#/problem-list 注册账号,练习题都在题库里,用类似 leetcode 刷题的方式学习。

FPGA

现场可编程门阵列。可以在芯片内部绘制用编程软件绘制和擦除电路。大概就是编程来决定内部电路结构的芯片。

FPGA 内部有各种各样可编程门电路(如与或非),其编程能力取决于可编程单元的数量。通过阵列的形式排列。

门电路断电后功能擦除,再上电重新写入,因此被称为可擦除。

image-20230408082158373

本次课程更多使用 Verilog 语言。

现在的 FPGA 芯片也不仅仅局限于这些功能,可以有一些网络接口,存储器,DPU(深度学习处理单元,学 AI 用的)等。

image-20230408083957295

如果感兴趣建议入门从 EGO1 开始学习,b站同名up就有相关学习课。

语法

电路主要分为组合逻辑电路(输出仅和现在的输入有关),时序逻辑电路(和输入以及过去电路的历史状态有关)。

首先熟悉一下程序架构:

image-20230409202319152

module:我们写的这一整个内容可以看做是一个函数/模块,module。

输入:input 类型。

输出:output 类型。

我们要做的是把 input 类型转换为想要的 output 结果。

例1:P1203 1输入1输出

题目描述

请构建一个具有一个输入和一个输出的模块,请用组合逻辑实现,其输出端口和输入端口信号关系如下:
out <- in(将in连续赋值给out)

与物理导线不同,Verilog中的导线(和其他信号)是定向的。这意味着信息只在一个方向上流动,从(通常是一个)源到接收器(源通常也称为将值驱动到导线上的驱动程序)。在Verilog的assign语句中,右侧信号的值被驱动到左侧的导线上,其赋值是“连续的”,因为即使右侧的值发生变化,赋值也会一直持续,注意连续分配不是一次性事件。

如下right_side的信号会连续赋值给left_side 。
assign left_side = right_side;

要求很简单,把输入赋值给输出原封不动即可。

主要是语法如何赋值。用 assign 关键词赋值。

assign:连续赋值,比如 in 的值更新了 out 也会实时更新。assign 类似 return。

// Verilog Solution:

module top_module(
    // 输入信号
    input in,
    // 输出信号
    output out
);


// write your code here

assign out=in;//把 in 的值赋给 out

endmodule

类似 leetcode,给定了一些数据结构和函数,我们只需要在需要写代码的地方补充代码即可。

例2:P1204 3输入4输出

请构建一个具有 3 个输入和 4 个输出的模块,请用组合逻辑实现,其行为类似于建立这些连接的导线:
w <- a
x <- b
y <- b
z <- c

在设计实现中你会使用到assign语句,当设计中有多个assign赋值语句时,它们在代码中的显示顺序无关紧要。需要理解的是assign赋值语句(“连续赋值”)是描述事物之间的联系,而不是将值从一个事物复制到另一个事物的操作。
这里还需澄清的一个潜在的混淆:上图的绿色箭头表示电线之间的连接,但本身不是电线。该模块本身已经声明了 7 根导线(命名为 a、b、c、w、x、y 和 z),assign语句不是在创建导线,而是在已存在的7根导线之间创建连接。

image-20230414082453700
和例1很像,主要就是如何正确的把多个 input 按要求赋值给多个 output。

assign w=a;
assign x=b;
assign y=b;
assign z=c;

例3:P1207 P1208 P1205 与或非门

与门(英语:AND gate)又称“与电路”、逻辑“积”、逻辑“与”电路。是执行“与”运算的基本逻辑门电路。有多个输入端,一个输出端。当所有的输入同时为高电平(逻辑1)时,输出才为高电平,否则输出为低电平(逻辑0)。

与门的真值表为:

输入in0 输入in1 输出out
0 0 0
0 1 0
1 0 0
1 1 1

或门(OR gate),又称或电路、逻辑和电路。如果几个条件中,只要有一个条件得到满足,某事件就会发生,这种关系叫做“或”逻辑关系。具有“或”逻辑关系的电路叫做或门。或门有多个输入端,一个输出端,只要输入中有一个为高电平时(逻辑“1”),输出就为高电平(逻辑“1”);只有当所有的输入全为低电平(逻辑“0”)时,输出才为低电平(逻辑“0”)。

或门的真值表:

输入in0 输入in1 输出out
0 0 0
0 1 1
1 0 1
1 1 1

非门(英文:NOT gate)又称非电路、反相器、倒相器、逻辑否定电路,简称非门,是逻辑电路的基本单元。非门有一个输入和一个输出端。当其输入端为高电平(逻辑1)时输出端为低电平(逻辑0),当其输入端为低电平时输出端为高电平。也就是说,输入端和输出端的电平状态总是反相的。非门的逻辑功能相当于逻辑代数中的非,电路功能相当于反相,这种运算亦称非运算。

非门的真值表:

输入in 输出out
0 1
1 0

这里就是简单的数电概念了,对于2个输入 in0 in1,求其与和或的结果;对于一个输入 in,求其非的结果。

语法:& | ! 是针对单位 bit 的逻辑运算。&& || ~ 是多位的逻辑运算。这里 in0 和 in1 都是单位 bit ,用 & 或 && 都行。(可以试一下 P1206 4位非门,令 out=!in; 就是错误的因为需要有多位取反,~ 就正确。)

assign out=in0&in1;
assign out=in0|in1;
assign out=!in;

还有几道与非,或非,同或的题是根据与或非的结合去计算的,也比较简单可以练手。

例4:P1200 半加器

半加器电路是指对两个输入数据位相加,输出一个结果位和进位,没有进位输入的加法器电路。 是实现两个一位二进制数的加法运算电路,即不考虑低位有无向本位的进位,只将两个本位数相加的运算。

以下是1位半加器的真值表,其中x和y是加数,c_out是向高位的进位信号,sum是和:

输入 输出
x y sum c_out
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1

也是很简单的电路,就是求x+y。如果进位了,c_out=1。sum =x+y的最低位。比如1+1=10,进位 c_out=1,sum 最低位=0。

可以令 sum=x+y; 因为 sum x y 都是1位的,即使 x=1 y=1,相加后也会溢出,sum 得到的值仍是0. 然后 c_out=x&y; 因为只有两者同时==1 时才会进位。

也可以采用异或运算 sum=x^y; 异或运算是当 x!=y 时,结果=1,否则结果=0,也符合半加器的进位机制。(可以尝试 P1211 异或题。)

例5:P1201 4位二进制转余3循环码

格雷码,又叫循环二进制码或反射二进制码,格雷码是一个有序的2的N次方个二进制码,格雷码是我们在工程中常会遇到的一种编码方式,格雷码的特点是从一个数变为相邻的一个数时,只有一个数据位发生跳变,由于这种特点,就可以避免二进制编码计数组合电路中出现的亚稳态。格雷码常用于通信,FIFO或者RAM地址寻址计数器中。

十进制数 binary[3:0](自然二进制数) gray[3:0](格雷码) 十进制数 binary[3:0](自然二进制数) gray[3:0](格雷码)
0 0000 0000 8 1000 1100
1 0001 0001 9 1001 1101
2 0010 0011 10 1010 1111
3 0011 0010 11 1011 1110
4 0100 0110 12 1100 1010
5 0101 0111 13 1101 1011
6 0110 0101 14 1110 1001
7 0111 0100 15 1111 1000

4位2进制转格雷码方法如下:

img
首先介绍一下 verilog 的多位寄存器定义。这次题目定义如下:

module top_module(
    // 4位二进制数输入
    input [3:0] binary,
    // 4位格雷码输出
    output [3:0] gray
);

前面接触的变量大多数变量都是一位的,比如 input in 就是定义了一个比特的 in 寄存器。如果我们给一位的寄存器赋值会自动截取最低位的值,比如 assign out=2; 2 的二进制是10,也就是 out 得到的值只是末尾的0.

如果 input[3:0] in 是定义了一个4位的寄存器,用法很像 C 语言的数组。4位从高到低分别是 in[3], in[2], in[1], in[0]。

我们可以单独给其中一个或者几个单元赋值,比如 output[3:0] outassign out[2]=1; assign out[0]=0;

也可以比如 out[3:2]=2; 给前两位赋值10.

貌似也可以 output[0:3] out 去定义,连续赋值的时候也必须从小到大,如 out[2:3]=2; .不过约定俗成大家一般都规定高位索引值高,低位索引值低。

这道题根据图片公式,我们给每个位和他左边相邻的位进行异或,求得本位的值。

assign gray[0]=binary[0]^binary[1];
assign gray[1]=binary[1]^binary[2];
assign gray[2]=binary[2]^binary[3];
assign gray[3]=binary[3];

也可以用一个大括号赋值,用逗号分割。

assign gray={binary[3],binary[2]^binary[3],binary[1]^binary[2],binary[0]^binary[1]};

这种赋值方式不是说必须局限于写4个 1bit 的值。可以 assign gray={binary[3:2],binary[1],binary[1]}; 形如这样,相当于把 binary[3:2] 赋值给 gray[3:2],binary[1] 赋值给 gray[1] gray[0]。总长度是一样的就行。

除此以外还可以复制重复多次赋值。

image-20230408144833865

对于常数,可能会看到 4'b1011 类似这样的形式。第一个数字表示:这个常数转换为二进制之后,一共有多少位。然后加一个’,然后是一个字母表示后面的数字是以二进制形式/十进制形式/十六进制形式传入。b是二进制,d是十进制,h是十六进制。

8’b1011_1011: 二进制数 1011 1011,即十进制187。_是分隔每四位数字的符号,写不写都行,就是看起来方便一点。

3’d7:十进制数7,二进制数111.

4’ha:十六进制数a,十进制数10,二进制数1010.

例6:P1215 2选1多路选择器

多路选择器是数据选择器的别称。在多路数据传送过程中,能够根据需要将其中任意一路选出来的电路,叫做数据选择器,也称多路选择器或多路开关。
2选1多路选择器的电路图为
image-20230414082728584>
以下为2选1多路选择器的真值表,其中s为控制信号,d0,d1为两个输入信号,y为输出信号。当s为低电平时,输出y=d0,当s为高电平时,输出y=d1。

输入d0 输入d1 输入s 输出y
0 0 0 0
0 1 0 0
1 0 0 1
1 1 0 1
0 0 1 0
1 0 1 0
0 1 1 1
1 1 1 1

请参考真值表构建一个二选一多路选择器,要求组合逻辑实现。

module top_module(
    // 输入信号d0
    input d0,
    // 输入信号d1
    input d1,
    // 选择信号s
    input s,
    // 输出信号y
    output y
);

题目大意:当 s1 时,y=d1. 当 s0 时,y==d0.

也就是说这里需要用到的语法是逻辑判断和条件结构。

先说逻辑判断,和c语言类似,==判断两个变量是否值相等,>= <= < > 判断大小比较,!= 判断是否不相等,表达式返回结果是1 或 0.

再说 if else 条件判断语句。

always @(*) begin	// always 这里是固定语法,if for case 都要嵌套在 always end 里面,先记住就好
    if(condition) begin
        
    end
    else if(condition) begin
        
    end
    else(condition) begin
        
    end
end

如果if 后面只有一个执行语句,可以省去 begin end。

always @(*) begin	
    if(condition) 
    	statement;
    else if(condition) 
        statement;
    else(condition) 
        statement;
end

然后还需要介绍两个数据类型:wire 和 reg。

我们前面赋值基本都是直接给 output 类型输出赋值的,这是没有问题的。但是如果我们需要自己定义一个中间变量,用 assign 赋值,必须定义为 wire 类型,因为规定如此,只有 wire 数据类型可以被 assign 赋值。

wire[3:0] out_temp;
assign out_temp[3]=1;

而在 always end 内部如果我们想给变量赋值,还不太一样。首先,always 内部不能用 assign 赋值。

然后,always 内部只有 reg 数据类型可以被赋值,output 变量都不行。

module top_module(output out);
always @(*) begin
	assign out=1;//wrong
end
endmodule

解决方法如下三种:

module top_module(output out);
reg out_temp;
always @(*) begin
	out_temp=1;//success, 不用 assign 直接赋值
end
    assign out=out_temp;//success
endmodule 
module top_module(output reg out);
    always @(*) begin
	out=1;//success,out 是 output (输出)类型,reg 数据类型的变量
end
endmodule 
module top_module(output out);
reg out;//和第二种方法等效
    always @(*) begin
	out=1;//success
end
endmodule 

所以此题我们可以在 always if 的条件判断里先赋给一个临时变量,always 结束后再赋值给 output y。

命名的话有人用 _r 表示是一个 reg 变量,有人用 _t 代表是一个临时的变量。

  reg y_r;
  always @(*) begin
    if(s==1)
      y_r=d1;
    else 
      y_r=d0;
  end
  assign y=y_r;

除了 if,还可以使用 case。case 重在对于一个数的不同的值的比较。

  reg y_r;
  always @(*) begin
      case(s)//根据s的值判断执行什么语句
      1:y_r=d1;//s==1要执行的内容
      0:y_r=d0;//s==0要执行的内容
      default:;//如果不满足以上情况需要执行的内容。default 必须写
      endcase//结束 case 语句,必须写
  end
  assign y=y_r;

还可以使用三目运算符,这是最简单的一个条件判断式。形如:(condition)?如果condition为true要执行的语句:如果condition为false要执行的语句; 这个语句也不用在 always 里。

assign y=(s)?d1:d0;

例7:P1236 D触发器

D触发器是一个具有记忆功能的,具有两个稳定状态的信息存储器件,是构成多种时序电路的最基本逻辑单元,也是数字逻辑电路中一种重要的单元电路。
因此,D触发器在数字系统和计算机中有着广泛的应用。触发器具有两个稳定状态,即"0"和"1",在一定的外界信号作用下,可以从一个稳定状态翻转到另一个稳定状态。
D触发器有集成触发器和门电路组成的触发器。触发方式有电平触发和边沿触发两种,前者在时钟脉冲=1时即可触发,后者多在时钟脉冲的前沿(正跳变0→1)触发。
D触发器的次态取决于触发前D端的状态,即次态=D。因此,它具有置0、置1两种功能。
对于边沿D触发器,由于在时钟脉冲=1期间电路具有维持阻塞作用,所以在时钟脉冲=1期间,D端的数据状态变化,不会影响触发器的输出状态。
D触发器应用很广,可用做数字信号的寄存,移位寄存,分频和波形发生器等等。

以下1bit D触发器的真值表:

输入 输出
d clk q
0 上升沿 0
1 上升沿 1
x 下降沿 保持不变
x 0 保持不变
x 1 保持不变

请根据以上真值表构建一个1比特时钟上升沿触发的D触发器。

这里主要是涉及到“上升沿”和“下降沿”信号的处理。

always @(*) begin 的意思是括号内的条件满足时,就会触发。如果是 * ,就是信号变化时就触发后跟的语句。

如果括号内换成上升沿或下降沿的条件,就可以在上升沿和下降沿触发某些指令。

语法:posedge clk,指时钟上升沿触发;negedge clk,指时钟下降沿触发。

module top_module(
    // D触发器数据输入端
    input d,
    // D触发器时钟输入端
    input clk,
    // D触发器数据输出端
    output q
);

	reg tmp;
  	always@(posedge clk) begin
    	tmp \<\= d;
  	end
	assign q = tmp;
    
endmodule

这里可以注意到使用了一个奇怪的赋值符号:<=。他的意思是这个符号赋值的变量不是按顺序赋值的,而是同一时间赋值的。这种赋值叫非阻塞赋值,而=叫阻塞赋值,即串行进行赋值。

例8:P1246 4位移位寄存器

移位寄存器内的数据可以在移位脉冲(时钟信号)的作用下依次左移或右移。移位寄存器不仅可以存储数据,还可以用来实现数据的串并转换、分频,构成序列码发生器、序列码检测器,进行数值运算以及数据处理等,它也是数字系统中应用非常广泛的时序逻辑部件之一。

移位寄存器按数据移位方向分类:可以分为左移寄存器和右移寄存器。请构建一个位宽为4位的移位寄存器,既能实现左移寄存器,又能实现右移寄存器。左移寄存器其功能为当时钟上升沿到达时,输入信号的最高位移位到最低位,其余各位依次向左移动一位。同理,右移寄存器其功能为当时钟上升沿到达时,输入信号的最低位移位到最高位,其余各位依次向右移动一位。

其中ena为移位方向使能信号,当ena值为1时,实现右移寄存器;当ena值为2时,实现左移寄存器。d为输入端数据信号,q为输出端数据信号。
image-20230414082351809

这道题,对于时钟信号的上升沿我们已经学过语法了,难的地方在于怎么实现“移位”。

移位不是单纯的 << >> ,因为这样不会把移溢出的位放到另一侧,而是直接补0或1. 如果想实现循环移位,需要我们手动赋值,比如d[15:0],新赋值后的寄存器值={d[3:0],d[15,4]},左移同理。

module top_module(
    // 输入时钟
    input clk,
    // 全局复位高有效
    input rst,
    // 移位方向使能信号
    input [1:0] ena,
    // 输入数据
    input [3:0] d,
    // 输出数据
    output [3:0] q
);


// write your code here

reg[3:0] q_r;
always @(posedge clk) begin
  	if(rst) q_r<=4'b0;
    else if(!ena) q_r<=4'b0;
    else if(ena==1) begin
    	q_r<={d[0],d[3:1]};
	end
    else if(ena==2) begin
    	q_r<={d[2:0],d[3]};
  	end
end
assign q=q_r;

endmodule

猜你喜欢

转载自blog.csdn.net/jtwqwq/article/details/130144498
今日推荐