文章目录
- 介绍
- FPGA
- 语法
- 例1:P1203 1输入1输出
- 例2:P1204 3输入4输出
- 例3:P1207 P1208 P1205 与或非门
- 例4:P1200 半加器
- 例5:P1201 4位二进制转余3循环码
- 例6:P1215 2选1多路选择器
- 例7:P1236 D触发器
- 例8:P1246 4位移位寄存器
介绍
本课程笔记是基于依元素科技公司与北京邮电大学开展的 FPGA 创新工坊课程学习 verilog 代码。
主要就是在其的社区平台上写 verilog 代码练习。平台链接:https://www.ego-link.com/#/problem-list 注册账号,练习题都在题库里,用类似 leetcode 刷题的方式学习。
FPGA
现场可编程门阵列。可以在芯片内部绘制用编程软件绘制和擦除电路。大概就是编程来决定内部电路结构的芯片。
FPGA 内部有各种各样可编程门电路(如与或非),其编程能力取决于可编程单元的数量。通过阵列的形式排列。
门电路断电后功能擦除,再上电重新写入,因此被称为可擦除。
本次课程更多使用 Verilog 语言。
现在的 FPGA 芯片也不仅仅局限于这些功能,可以有一些网络接口,存储器,DPU(深度学习处理单元,学 AI 用的)等。
如果感兴趣建议入门从 EGO1 开始学习,b站同名up就有相关学习课。
语法
电路主要分为组合逻辑电路(输出仅和现在的输入有关),时序逻辑电路(和输入以及过去电路的历史状态有关)。
首先熟悉一下程序架构:
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根导线之间创建连接。
和例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进制转格雷码方法如下:
首先介绍一下 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] out
,assign 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]。总长度是一样的就行。
除此以外还可以复制重复多次赋值。
对于常数,可能会看到 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多路选择器的电路图为
>
以下为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为输出端数据信号。
这道题,对于时钟信号的上升沿我们已经学过语法了,难的地方在于怎么实现“移位”。
移位不是单纯的 << >> ,因为这样不会把移溢出的位放到另一侧,而是直接补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