Verilog语言学习!
文章目录
目录
文章目录
前言
一、基本逻辑门代码设计和仿真
1.1 反相器
1.2 与非门
1.3 四位与非门
二、组合逻辑代码设计和仿真
2.1 二选一逻辑
2.2 case语句实现多路选择逻辑
2.3 补码转换
2.4 7段数码管译码器
三、时序逻辑代码设计和仿真
3.1 计数器
3.2 4级伪随机码发生器
3.3 秒计数器,0~9循环
3.4 相邻点累加
四、状态机代码设计和仿真
4.1 三角波发生器
4.2 梯形波发生器
4.3 串口数据接收
4.4 串口数据发送
4.5 串口指令处理器
总结
前言
本文主要介绍了Verilog代码的基础内容和应用实例。
一、基本逻辑门代码设计和仿真
1.1 反相器
代码示例:
//反相器设计
module inv(
A,
Y
);
//inv是模块名,A、Y是端口。
//定义端口属性
input A;
output Y;
//定义输入输出关系
assign Y=~A;
endmodule
//testbench of inv
`timescale 1ns/10ps
module inv_tb;
//因为inv_tb没有端口,所以不写括号。
//因为反向器的输入端要有变动,所以定义为reg型变量;反相器的输出定义为wire型变量。
reg aa;
wire yy;
//反相器例化(异名例化)
inv inv(
.A(aa),
.Y(yy)
);
//让输入端aa动来观察yy
//initial语句块可以按时间定义各个变量的值
initial begin
aa<=0;
#10 aa<=1;
#10 aa<=0;
#10 aa<=1;
#10 $stop;//停止
end
//仿真停止是用Verilog的系统任务$stop,#10表示过10个时间单位。
endmodule
波形图:
1.2 与非门
代码示例:
//与非门
module nand_gate(
A,
B,
Y
);
input A;
input B;
output Y;
//由于是组合逻辑,所以可以用assign语句
assign Y=~(A&B);
endmodule
//testbench of nand_gate
`timescale 1ns/10ps
module nand_gate_tb;
//输入都定义为reg型变量
reg aa;
reg bb;
wire yy;
nand_gate nand_gate(
.A(aa),
.B(bb),
.Y(yy)
);
initial begin
aa<=0; bb<=0;
#10 aa<=0; bb<=1;
#10 aa<=1; bb<=0;
#10 aa<=1; bb<=1;
#10 $stop;
end
//reg型变量赋值的时候一定用 <=
endmodule
波形图:
1.3 四位与非门
代码示例:
//与非门
module nand_gate_4bits(
A,
B,
Y
);
input [3:0] A;
input [3:0] B;
output [3:0] Y;
//由于是组合逻辑,所以可以用assign语句
assign Y=~(A&B);
endmodule
//testbench of nand_gate
`timescale 1ns/10ps
module nand_gate_4bits_tb;
//输入都定义为reg型变量
reg [3:0] aa;
reg [3:0] bb;
wire [3:0] yy;
nand_gate_4bits nand_gate_4bits(
.A(aa),
.B(bb),
.Y(yy)
);
initial begin
aa<=4'b0000; bb<=4'b1111;
#10 aa<=4'b0010; bb<=4'b0110;
#10 aa<=4'b0111; bb<=4'b0100;
#10 aa<=4'b0000; bb<=4'b1110;
#10 $stop;
end
//reg型变量赋值的时候一定用 <=
endmodule
波形图:
总结:
表中运算符叫位运算符,表示按位进行逻辑运算,位宽为1只是它们的特例。
8位反相器和4位与非门的简单画法。
二、组合逻辑代码设计和仿真
多路选择器逻辑设计。
2.1 二选一逻辑
模块 fn_sw 功能:
- 当 sel 为 0 时,y 是 a 和 b 的与;
- 当 sel 为 1 时,y 是 a 和 b 的异或;
代码示例1:
//二选一逻辑设计
//模块fn_sw功能:当sel为0时,y是a和b的与;当sel为1时,y是a和b的异或。
module fn_sw(
a,
b,
sel,
y
);
input a;
input b;
input sel;
output y;
assign y=sel?(a^b):(a&b);
//这里使用了问号冒号语句,其含义是:如果sel为1,则y等于(a^b),否则y等于(a&b)
endmodule
//testbench of fn_sw
`timescale 1ns/10ps
module fn_sw_tb;
reg a;
reg b;
reg sel;
wire y;
fn_sw fn_sw(
.a (a),
.b (b),
.sel (sel),
.y (y)
);
initial begin
a<=0; b<=0; sel<=0;
#10 a<=0; b<=0; sel<=1;
#10 a<=0; b<=1; sel<=0;
#10 a<=0; b<=1; sel<=1;
#10 a<=1; b<=0; sel<=0;
#10 a<=1; b<=0; sel<=1;
#10 a<=1; b<=1; sel<=0;
#10 a<=1; b<=1; sel<=1;
#10 $stop;
end
endmodule
代码示例2:
//二选一逻辑设计
//模块fn_sw功能:当sel为0时,y是a和b的与;当sel为1时,y是a和b的异或。
module fn_sw(
a,
b,
sel,
y
);
input a;
input b;
input sel;
output y;
//用always语句块实现组合逻辑
reg y;//always语句块里赋值的变量需要是reg型
always @(a or b or sel) begin//括号里为敏感变量,组合逻辑输入,敏感变量一定要写全。
if(sel==1)begin
y<=a^b;//reg类型的变量赋值用带箭头的等号 <=
end
else begin
y<=a&b;
end
end
endmodule
//testbench of fn_sw
`timescale 1ns/10ps
module fn_sw_tb;
reg a;
reg b;
reg sel;
wire y;
fn_sw fn_sw(
.a (a),
.b (b),
.sel (sel),
.y (y)
);
initial begin
a<=0; b<=0; sel<=0;
#10 a<=0; b<=0; sel<=1;
#10 a<=0; b<=1; sel<=0;
#10 a<=0; b<=1; sel<=1;
#10 a<=1; b<=0; sel<=0;
#10 a<=1; b<=0; sel<=1;
#10 a<=1; b<=1; sel<=0;
#10 a<=1; b<=1; sel<=1;
#10 $stop;
end
endmodule
波形图:
2.2 case语句实现多路选择逻辑
模块 fn_sw 功能:
- 当 sel 为 00 时,y 是 a 和 b 的与;
- 当 sel 为 01 时,y 是 a 和 b 的或;
- 当 sel 为 10 时,y 是 a 和 b 的异或;
- 当 sel 为 11 时,y 是 a 和 b 的同或;
代码示例:
//四选一逻辑
module fn_sw_4(
a,
b,
sel,
y
);
input a;
input b;
input [1:0] sel;
output y;
reg y;
always@(a or b or sel)begin
case(sel)
2'b00: begin y<=a&b; end
2'b01: begin y<=a|b; end
2'b10: begin y<=a^b; end
2'b11: begin y<=~(a^b); end
endcase
end
endmodule
//testbench of fn_sw_4
`timescale 1ns/10ps
module fn_sw_4_tb;
reg [3:0] absel;
wire y;
fn_sw_4 fn_sw_4(
.a (absel[0]),
.b (absel[1]),
.sel(absel[3:2]),
.y (y)
);
initial begin
absel<=0;
#200 $stop;
end
always #10 absel<=absel+1;
endmodule
波形图:
2.3 补码转换
补码转换逻辑:
- 正数补码与原码相同。
- 负数补码转换方法是符号位不变,幅度值按位取反加1.
代码示例:
//补码转换逻辑
//正数补码与原码相同
//负数补码转换方法是符号位不变,幅度值按位取反加1.
module comp_conv(
a,
a_comp
);
input [7:0] a;
output [7:0] a_comp;
wire [6:0] b;//按位取反的幅度位(原码的低7位按位取反得到)
wire [7:0] y;//负数的补码
assign b=~a[6:0];//原码的低7位按位取反
assign y[6:0]=b+1;//按位取反+1 y[6:0]=~a[6:0]+1;//若不定义b,就可以简化为这一句代码
assign y[7]=a[7];//符号位不变 y={a[7],~a[6:0]+1};//y的值可以简化为这一句代码
assign a_comp=a[7]?y:a;//二选一
//若原码的符号位(最高位)是1,那原码就是负数,补码就是y;
//若原码的符号位(最高位)是0,那原码就是正数,补码就是它自己;
//assign a_comp=a[7]?{a[7],~a[6:0]+1}:a;//最终可以不定义y和b,所有assign语句可简化为这一句代码。
endmodule
//testbench of comp_conv
`timescale 1ns/10ps
module comp_conv_tb;
reg [7:0] a_in; //原码的输入
wire [7:0] y_out;//输出结果
comp_conv comp_conv(
.a (a_in) ,
.a_comp(y_out)
);
initial begin
a_in<=0;
#3000 $stop;
end
always #10 a_in<=a_in+1;
endmodule
波形图:
2.4 7段数码管译码器
图中数码管是由a、b、c、d、e、f、g共7截杠组成。每一截杠按特定的亮灭可以组成数字。
数字电路一般只会只会按二进制产生数据,二进制数据让对应的数码管点亮还需要一定的逻辑来控制。
代码示例:
//七段码译码器
module seg_dec(
num,
a_g
);
input [3:0] num;
output [6:0] a_g;//a_g-->{a,b,c,d,e,f,g}
reg [6:0] a_g;//由于要用always语句赋值,所以要定义为reg型变量,虽然定义了reg型,但仍然是组合逻辑
always@(num)begin
case(num)
4'd0: begin a_g<=7'b111_1110; end
4'd1: begin a_g<=7'b011_0000; end
4'd2: begin a_g<=7'b110_1101; end
4'd3: begin a_g<=7'b111_1001; end
4'd4: begin a_g<=7'b011_0011; end
4'd5: begin a_g<=7'b101_1011; end
4'd6: begin a_g<=7'b101_1111; end
4'd7: begin a_g<=7'b111_0000; end
4'd8: begin a_g<=7'b111_1111; end
4'd9: begin a_g<=7'b111_1011; end
default: begin a_g<=7'b000_0001; end //中杠,num超出0~9时,用default来统一处理,显示为中杠。
endcase
end
endmodule
//testbench of seg_dec
`timescale 1ns/10ps
module seg_dec_tb;
reg [3:0] n_in;
wire [6:0] a_out;
seg_dec seg_dec(
.num(n_in),
.a_g(a_out)
);
initial begin
n_in<=0;
#120 $stop;
end
always #10 n_in<=n_in+1;
endmodule
波形图:
三、时序逻辑代码设计和仿真
3.1 计数器
电路图中,右边是一个触发器,左边是一个加法器,做加1运算。
组合逻辑+触发器=时序逻辑。像图中电路中由组合逻辑加触发器构成的逻辑电路就叫它时序逻辑电路。
代码示例:
//计数器
module counter(
clk,
res,
y
);
input clk;
input res;
output [7:0] y;
reg [7:0] y;//y是触发器,要定义为reg型变量。y虽然是输出,但是要在always里对它进行赋值,要做reg型变量的定义。
wire [7:0] sum;//+1运算的结果。assign赋值,wire型变量
assign sum=y+1;//组合逻辑部分
always@(posedge clk or negedge res)//敏感变量为时钟的上升沿和复位信号的下降沿,这是时序逻辑电路的特点
if(~res)begin//触发器复位时
y<=0;//res为低时 y寄存器复位
end
else begin //时钟触发,也就是触发器正常工作时
y<=sum;//y得到sum值
//y<=y+1;//可以不定义sum,也不用assign语句写组合逻辑部分,直接用这一个语句来实现逻辑。
end
//if里面是触发器复位时的动作,else是触发器正常工作时的动作
//always块在只有见到时钟的上升沿或者复位信号的下降沿的时候,always块里的逻辑才会执行
//只要定义了一个寄存器,也就是reg型变量,always的敏感列表里又是时钟和复位的话,在实际电路当中一定会对应一个触发器。复位的时候一定要对寄存器赋值。
endmodule
//testbench of counter
`timescale 1ns/10ps
module counter_tb;
//reg型是要变动的;wire型是要看的输出结果
reg clk;
reg res;
wire [7:0] y;
counter counter(
.clk(clk),
.res(res),
.y (y)
);
//用initial语句给clk和res赋初值为0,过几个时间单位拉高res复位信号
initial begin
clk<=0;
res<=0;
#17 res<=1;
#6000 $stop;
end
//用always语句让clk时钟信号每隔5ns高低电平翻转一次,也就是10ns一个时钟周期
always #5 clk<=~clk;
endmodule
波形图:
3.2 4级伪随机码发生器
4级伪随机码发生器的电路是由4个触发器构成的,4个触发器理论上最多是有16个状态,但是从电路图的结构也可以看出4个触发器是形成了一串,时钟信号一来数据是从左向右移动的,第一个数据,也就是D3的数据来自于D3和D0输出的模2加。
数据不能出现全0的状态,全0的话模2加之后还是0,所以这种状态下最大理论重复周期是15,就是在16个状态里把全0排除。即2^4-1=15
可以得到最大理论重复周期的这种电路被称为伪随机码发生器
GPS为10级伪随机码(C/A码),BDS北斗为11级。
代码示例:
//4级伪随机码发生器
module m_gen(
clk,
res,
y
);
input clk;
input res;
output y;
reg [3:0] d;//定义4位寄存器,也就是4个触发器
assign y=d[0];//输出连接,输出是y,让y接给d[0]
always@(posedge clk or negedge res)
if(~res)begin
d<=4'b1111;//不能复位成全0
end
else begin
d[2:0]<=d[3:1];//右移一位,寄存器d的高三位给低三位。(不推荐使用双箭头 >> 写右移)
d[3]<=d[3]+d[0];//模二加,自动舍去进位。d[3]+d[0]的值给d[3],d[3]+d[0]的值本来应该是2位,给d[3]只能存1位的值,高位自动溢出,只存下低位,数值结果上就相当于模2
end
endmodule
//testbench of m_gen
`timescale 1ns/10ps
module m_gen_tb;
reg clk;
reg res;
wire y;
m_gen m_gen(
.clk(clk),
.res(res),
.y (y )
);
initial begin
clk<=0;
res<=0;
#17 res<=1;
#600 $stop;
end
always #5 clk<=~clk;
endmodule
波形图:
3.3 秒计数器,0~9循环
把系统时钟clk进行分频,得到秒脉冲,然后对秒脉冲计数,这样就得到了一个秒计数器。
假设系统时钟clk是24MHz,秒分频产生秒脉冲s_pulse;
秒计数模块对秒脉冲计数,计数范围是0~9,计数结果为s_num(位宽4)。
设计波形图:
设一个计数器,计数器基于系统时钟计数,计数范围是0~(24000000-1),计数器每循环一圈就是1s
设一个寄存器也就是触发器,每次遇到计数器为0以后,寄存器就置1;计数器非0时,寄存器置0.这样就能得到一个秒脉冲尖。
对秒脉冲尖进行计数,秒脉冲的寄存器一置1,秒计数就加1,一直加到9。到9之后遇到的下一次秒脉冲就将秒计数清0.
代码示例:
module s_counter(
clk,
res,
s_sum
);
input clk ;
input res ;
output [3:0] s_sum;
parameter frequency_clk = 24;//24MHz
reg [24:0] con_t;//秒脉冲分频计数器,con_t位数计算:如代码中最高支持24MHz系统频率,24000000对应二进制是25位数。
reg s_pulse;//秒脉冲尖
reg [3:0] s_sum;//秒脉冲尖计数器
//秒脉冲分频计数器con_t赋值
always@(posedge clk or negedge res)
if(~res)begin//复位时
con_t<=0;//秒脉冲分频计数器为0
s_pulse<=0;//秒脉冲尖的寄存器为0
s_sum<=0;//秒脉冲尖计数器为0
end
else begin
if(con_t==frequency_clk*1000000-1)begin//计数到最大范围值时
con_t<=0;//秒脉冲分频计数器清0
end
else begin//还没计数到最大值时
con_t<=con_t+1;//秒脉冲分频计数器累加1
end
if(con_t==0)begin//秒脉冲分频计数器为0时
s_pulse<=1;//秒脉冲尖的寄存器置1
end
else begin //秒脉冲分频计数器不为0时
s_pulse<=0;//秒脉冲尖的寄存器置0
end
if(s_pulse)begin//当有秒脉冲尖s_pulse时,也就是s_pulse为1时,秒脉冲尖计数器s_sum进行0~9的循环计数
if(s_sum==9)begin//秒脉冲尖计数器计到9时清零
s_sum<=0;
end
else begin
s_sum<=s_sum+1;//秒脉冲尖计数器自增1
end
end
end
endmodule
//testbench of s_counter
`timescale 1ns/10ps
module s_counter_tb;
reg clk;
reg res;
wire [3:0] s_sum;
s_counter s_counter(
.clk (clk),
.res (res),
.s_sum(s_sum)
);
initial begin
clk<=0;
res<=0;
#17 res<=1;
#1000 $stop;
end
always #5 clk<=~clk;
endmodule
波形图:
3.4 相邻点累加
相邻16点累加
电路结构图:
data_in是采样信号,syn_in是采样时钟。
data_in在syn_in上升沿变化,syn_in采样时钟的频率比系统时钟频率低很多,对相邻16点相加得到data_out,并由syn_out同步,syn_out为一个系统时钟周期宽度的脉冲。
输入信号data_in为8位带符号位的原码,输出data_out为补码。
以上电路图对应的工作逻辑:
首先对输入采样信号求补码,然后再对信号进行升位,升位之后在进行16点的相加,最后输出。
为了保证相加的节奏和采样时钟的节奏相同,就需要对采样时钟的上升沿进行识别,同时为了得到输出同步脉冲,还需要对采样时钟进行16分频。
波形图设计:
采样时钟信号和采样时钟延时信号相与就得到采样脉冲尖,采样脉冲尖和采样数据同时变化。对采样脉冲尖进行0~15的循环计数也就是脉冲尖循环计数信号,用来控制累加的节奏,每当计数器等于15的时候就把累加结果传给data_out并产生一个累加和同步脉冲。
代码示例:
//相邻16点相加
module sigma_16p(
clk ,
res ,
data_in ,
syn_in ,
data_out,
syn_out
);
input clk ;
input res ;
input [7:0] data_in ; //采样信号
input syn_in ; //采样时钟
output [11:0] data_out ; //累加结果输出
output syn_out ; //累加结果同步脉冲
reg syn_in_n1; //syn_in的反向延时,延时一拍就是用寄存器,采样时钟延时信号延时一拍,也就是一个系统时钟的周期,所以会定义为reg型的变量,在aways语句中赋值时,会比采样时钟迟一个系统时钟的周期。
wire syn_pulse; //采样时钟上升沿识别脉冲(采样脉冲尖)
reg [3:0] con_syn ; //采样时钟循环计数器
wire [7:0] comp_8 ; //补码
wire [11:0] d_12 ; //升位结果,12位是因为要在8位的补码再上升4位,所以提前定义好12位
reg [11:0] sigma ; //累加计算
reg [11:0] data_out ; //累加和
reg syn_out ; //累加和同步脉冲
//从输入到补码再到升位的过程都是组合逻辑,因此comp_8和d_12都定义为wire
assign syn_pulse = syn_in & syn_in_n1;//采样脉冲尖 由 采样时钟信号 和 采样时钟延时信号 相与 得到
assign comp_8 = data_in[7]?{data_in[7],~data_in[6:0]+1}:data_in;//补码运算,{符号位不变,幅度位按位取反加一}
//补码运算:data_in的最高位为1,表示负数,最高位符号位不变,其它位按位取反加一;data_in的最高位为0,就是正数,补码与原码相同。
assign d_12 = {comp_8[7],comp_8[7],comp_8[7],comp_8[7],comp_8};//升位,直接用符号位扩展。
always @(posedge clk or negedge res)
if(~res)begin //寄存器复位
syn_in_n1 <= 0;
con_syn <= 0;
sigma <= 0;
data_out <= 0;
syn_out <= 0;
end
else begin
syn_in_n1 <= ~syn_in;//输入时钟的反向延时,因为syn_in_n1为reg型,在aways语句中赋值时会延时一个时钟周期,syn_in_n1就是syn_in反向的一个系统时钟clk周期的延时。
if(syn_pulse)begin //在采样脉冲尖来的时候,因为采样时钟循环计数器计数是基于采样脉冲尖syn_pulse而不是基于系统时钟clk,所以需要加采样脉冲尖等于1时的条件。
con_syn <= con_syn + 1; //采样时钟循环计数器加1,由于是16个点计数的反复循环,而con_syn又定义了4位,就形成了天然的16点循环计数。
end
//相邻16点相加:
//当con_syn累加到16点时(也就是从0加到15),需要把sigma的结果送出去给data_out。同时新一轮的第一个点d_12来时,要把第一个点d_12作为sigma的初始值。
//当con_syn还没累加到16点时,sigma累加升位结果。
if(syn_pulse)begin //在采样脉冲尖来的时候
if(con_syn == 15)begin //在累加到16点时
sigma <= d_12;//sigma累加计算的值是新一轮相邻16点的第一个点的值。
data_out <= sigma;//送出上一轮16点累加计算的值给data_out
syn_out <= 1;//产生累加和同步脉冲
end
else begin //con_syn还没累加到15时
sigma <= sigma + d_12;//升位结果的累加计算
end
end
else begin
syn_out <= 0; //相邻16点没有累加到16点的情况下,不产生累加和同步脉冲。这样可以在波形中保证syn_out累加和同步脉冲只冒一个尖。
end
end
endmodule
data_in = 1 时:
//testbench of sigma_16p
`timescale 1ns/10ps
module tb_sigma_16p;
reg clk ;
reg res ;
reg [7:0] data_in ;
reg syn_in ;
wire [11:0] data_out;
wire syn_out ;
sigma_16p sigma_16p(
.clk (clk ),
.res (res ),
.data_in (data_in ),
.syn_in (syn_in ),
.data_out(data_out),
.syn_out (syn_out )
);
initial begin
clk <= 0; res <= 0; data_in = 1; syn_in <= 0;
#17 res <= 1;
#500 $stop;
end
always #5 clk <= ~clk;
always #100 syn_in <= ~syn_in;
endmodule
波形图:
data_in = -1 时:
//testbench of sigma_16p
`timescale 1ns/10ps
module tb_sigma_16p;
reg clk ;
reg res ;
reg [7:0] data_in ;
reg syn_in ;
wire [11:0] data_out;
wire syn_out ;
sigma_16p sigma_16p(
.clk (clk ),
.res (res ),
.data_in (data_in ),
.syn_in (syn_in ),
.data_out(data_out),
.syn_out (syn_out )
);
initial begin
clk <= 0; res <= 0; data_in=8'b1000_0001; syn_in <= 0;//data_in=8'b1000_0001;
#17 res <= 1;
#500 $stop;
end
always #5 clk <= ~clk;
always #100 syn_in <= ~syn_in;
endmodule
波形图:
四、状态机代码设计和仿真
4.1 三角波发生器
设计三角波里向上的波为0状态,向下的波为1状态,这样就组成了两个状态的状态机。
要组成一段三角波就需要从0状态跳到1状态,然后从1状态跳到0状态,循环跳跃0-1-0-1-......就形成三角波的向上波-向下波-向上波-向下波-......
- 0状态的任务是波形计数器加1,越加越多,加到299时就不加了,跳转到1状态;
- 1状态的任务是波形计数器减1,越减越少,减到1时就不减了,跳转到0状态;
代码示例:
//最简单的状态机,三角波发生器
module tri_gen(
clk,
res,
d_out
);
input clk ;
input res ;
output [8:0] d_out ;
reg state ; //主状态机的寄存器
reg [8:0] d_out ; //波形计数器,因为输出d_out也会在always里赋值,所以也定义为寄存器
always @(posedge clk or negedge res)
if(~res)begin //复位时寄存器清零
state<=0;
d_out<=0;
end
else begin
case(state)
0://0状态时,d_out自增,三角波的上升部分
begin
d_out<=d_out+1;
if(d_out==299)begin
state<=1;
end
end
1://1状态时,d_out自减,三角波的下降部分
begin
d_out<=d_out-1;
if(d_out==1)begin
state<=0;
end
end
endcase
end
//这里0状态时d_out每见到时钟上升沿就会加1,当d_out=299的时候,状态就由0跳到1,在状态跳变的过程中,d_out的加1也完成了,所以真正到1状态时,d_out是以300的值进1状态来的。
//同理1状态时d_out一直在减1,当d_out=1的时候,状态就由1跳到0,在状态跳变的过程中,d_out的减1也完成了,所以跳到0状态时d_out是0.
//状态机就是一个case语句,在case语句里有0状态和1状态,每个状态里有执行的动作和状态跳转。
endmodule
注:
这里0状态时d_out每见到时钟上升沿就会加1,当d_out=299的时候,状态就由0跳到1,在状态跳变的过程中,d_out的加1也完成了,所以真正到1状态时,d_out是以300的值进1状态来的。
同理1状态时d_out一直在减1,当d_out=1的时候,状态就由1跳到0,在状态跳变的过程中,d_out的减1也完成了,所以跳到0状态时d_out是0.
状态机就是一个case语句,在case语句里有0状态和1状态,每个状态里有执行的动作和状态跳转。
//testbench of tri_gen
`timescale 1ns/10ps
module tb_tri_gen;
reg clk ;
reg res ;
wire [8:0] d_out ;
tri_gen tri_gen(
.clk (clk ),
.res (res ),
.d_out(d_out)
);
initial begin
clk<=0;res<=0;
#17 res<=1;
#8000 $stop;
end
always #5 clk<=~clk;
endmodule
波形图:
4.2 梯形波发生器
设计梯形波里向上的波为0状态,平顶为1状态,向下的波为2状态,这样就组成了三个状态的状态机。
要组成一段梯形波就需要从0状态跳到1状态,再从1状态跳到2状态,循环跳跃0-1-2-0-1-2-......就形成梯形波的向上波-平顶-向下波-向上波-平顶-向下波......
- 0状态的任务是波形计数器加1,越加越多,加到299时就不加了,跳转到1状态;
- 1状态的任务是波形计数器保持原值,一段时钟周期后,跳转到2状态;
- 2状态的任务是波形计数器减1,越减越少,减到1时就不减了,跳转到0状态;
代码示例:
//状态机,梯形波发生器
module trape_gen(
clk,
res,
d_out
);
input clk ;
input res ;
output [8:0] d_out ;
reg [1:0] state;//主状态机的寄存器
reg [8:0] d_out;//波形计数器,因为输出d_out也会在always里赋值,所以也定义为寄存器
reg [7:0] con ;//记录平顶周期个数计数器
always @(posedge clk or negedge res)
if(~res)begin //复位时寄存器清零
state <= 0;
d_out <= 0;
con <= 0;
end
else begin
case(state)
0://0状态时,d_out自增,梯形波的上升部分
begin
d_out<=d_out+1;
if(d_out==299)begin
state<=1;
end
end
1://1状态时,d_out保持原值,梯形波的平顶部分
begin
if(con==200)begin
state<=2;
con<=0;
end
else begin
con<=con+1;
end
end
2://2状态时,d_out自减,梯形波的下降部分
begin
d_out<=d_out-1;
if(d_out==1)begin
state<=0;
end
end
default://状态机最后最好使用default来控制没有列出的状态执行的动作,在此代码中就是状态3,因为state定义为两位,就是0,1,2,3四个数,这里状态3并没有设计执行动作,最后统一用default来控制没有设计执行动作的状态。
begin
state<=0;
con<=0;
d_out<=0;
end
endcase
end
//这里0状态时d_out每见到时钟上升沿就会加1,当d_out=299的时候,状态就由0跳到1,在状态跳变的过程中,d_out的加1也完成了,所以真正到1状态时,d_out是以300的值进1状态来的。
//同理1状态时d_out一直在减1,当d_out=1的时候,状态就由1跳到0,在状态跳变的过程中,d_out的减1也完成了,所以跳到0状态时d_out是0.
//状态机就是一个case语句,在case语句里有0状态和1状态,每个状态里有执行的动作和状态跳转。
endmodule
注:
这里0状态时d_out每见到时钟上升沿就会加1,当d_out=299的时候,状态就由0跳到1,在状态跳变的过程中,d_out的加1也完成了,所以真正到1状态时,d_out是以300的值进1状态来的。
同理1状态时d_out一直在减1,当d_out=1的时候,状态就由1跳到0,在状态跳变的过程中,d_out的减1也完成了,所以跳到0状态时d_out是0.
状态机就是一个case语句,在case语句里有0状态和1状态,每个状态里有执行的动作和状态跳转。
//testbench of trape_gen
`timescale 1ns/10ps
module tb_trape_gen;
reg clk ;
reg res ;
wire [8:0] d_out ;
trape_gen trape_gen(
.clk (clk ),
.res (res ),
.d_out(d_out)
);
initial begin
clk<=0;res<=0;
#17 res<=1;
#8000 $stop;
end
always #5 clk<=~clk;
endmodule
波形图:
4.3 串口数据接收
串口时序图:
- 串口发送端口空闲时为高;
- 发送端口拉低表示数据传送即将开始;
- 字节数据低位先发;
- 字节发送后拉高,表示字节传送结束;
- 字节位宽可以不为8;
- 常用的波特率有4800、9600、115200等
串口接收模块:
- RX为串口输入
- data_out为接收到的串口字节(8位);
- 每接受完成一个字节,en_data_out就产生一个同步脉冲;
- 用户见到en_data_out即可收数;
- 波特率为4800,系统时钟频率24MHz
状态规划:
- 状态1:等空闲,空闲识别
- 状态2:等起始位
- 状态3:收b0
- 状态4:收b1
- 状态5:收b2
- 状态6:收b3
- 状态7:收b4
- 状态8:收b5
- 状态9:收b6
- 状态10:收b7
收完之后再返回去等待起始位。
空闲时别:
空闲时别:空闲期间一直为1,能连续收到15个1,就说明一定是在空闲状态。
起始位识别:
起始位识别:找起始位的方法实际就是找空闲到起始位的下降沿,把这个下降沿RX做一个时钟周期的延时RX_delay,再把RX反向一下和RX_delay与就得到一个脉冲尖。以后一看到这个脉冲尖就认为起始位到了。
收数据方法:
收数据方法:假设接受1位数据的宽度为Tbit,在每次收数据的时候把采数的点设计在每一位数据的中间能最可靠的收到准确数据,因此在识别到起始位时也就是空闲到起始位的下降沿这个位置时,再等到1.5Tbit时长后再去收一下RX数据,就在b0数据位的中间采到了第一个数据,接下来一个Tbit收下一个数据位b1,然后b2、b3、b4、b5、b6、b7,当收完b7以后,就表示这串数据收完了。
收完数据以后,又会回到等起始位这个状态。空闲识别是每次复位只识别一次。
状态转换图:
状态转换图:
- 首先是一个等空闲的状态,也就是RX长时间为1;
- 然后就进入等起始位的状态,也就是下降沿识别起始位;
- 再然后就开始收数据,从b0到b7,收满8bit后再跳转到等起始位的状态。等空闲只需一次,这个状态就是在复位的时候取一次。
代码示例:
//串口数据接收
module UART_RXer(
clk,
res,
RX,
data_out,
en_data_out
);
input clk ;
input res ;
input RX ;
output [7:0] data_out ;//接收字节输出
output en_data_out ;//输出使能
reg [7:0] data_out ;
reg en_data_out ;//使能脉冲
reg [7:0] state ;//主状态机,状态机位数定义时建议多放几位。
reg [12:0] con ;//用于计算Tbit宽度,也就是一个数据位的时间有多少个系统时钟。con的位数计算:1个Tbit宽度:24000000(系统频率24MHz)/4800(波特率)=5000,那么1.5个Tbit宽度换算成二进制就给个13位。
reg [3:0] con_bits ;//用于计算Tbit个数
reg RX_delay ;//RX的延时
always@(posedge clk or negedge res)
if(~res)begin//一旦定义了寄存器就要马上记得给寄存器复位清零
state<=0;
con<=0;
con_bits<=0;
RX_delay<=0;
data_out<=0;
en_data_out<=0;
end
else begin
RX_delay<=RX;//开始工作就无条件将RX赋值给RX的延时
case(state)
0://等空闲
begin
if(con==5000-1)begin //con计完一圈数后,也就是1个Tbit的宽度后,con清零。清零后好进行第二圈的计数
con<=0;
end
else begin
con <= con + 1;//con一直累加
end
if(con==0)begin //con_bits用于计算con的圈数,当con计完一圈数清零时,con_bits就累加
if(RX)begin //con_bits的累加还需要加一个条件,就是在RX为1的时候。RX为0的情况表示空闲长度还不够时就有一个0,那就说明此时有可能正在一串数据的中间,而不是等空闲状态。此时需要给con_bits清零。这样就能保证con_bits累加了一个很大的数的话一定是连续的RX长时间都为1,一个0都没有。
con_bits<=con_bits+1;
end
else begin
con_bits<=0;
end
end
if(con_bits==12)begin//如果con_bits连续累加到12,可以肯定当前处于空闲状态。
state<=1;
end
end
1://等起始位
begin
en_data_out<=0;
if(~RX&RX_delay)begin//等到起始位识别的脉冲尖,就状态跳出给收数据最低位。
state<=2;
end
end
2://收最低位b0;
begin
if(con==7500-1)begin //等con计数到1.5个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[0]<=RX; //从RX采到b0位的数
state<=3; //状态往下跳转
end
else begin
con<=con+1;
end
end
3://收最低位b1;
begin
if(con==5000-1)begin //等con计数到1个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[1]<=RX; //从RX采到b1位的数
state<=4; //状态往下跳转
end
else begin
con<=con+1;
end
end
4://收最低位b2;
begin
if(con==5000-1)begin //等con计数到1个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[2]<=RX; //从RX采到b2位的数
state<=5; //状态往下跳转
end
else begin
con<=con+1;
end
end
5://收最低位b3;
begin
if(con==5000-1)begin //等con计数到1个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[3]<=RX; //从RX采到b3位的数
state<=6; //状态往下跳转
end
else begin
con<=con+1;
end
end
6://收最低位b4;
begin
if(con==5000-1)begin //等con计数到1个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[4]<=RX; //从RX采到b4位的数
state<=7; //状态往下跳转
end
else begin
con<=con+1;
end
end
7://收最低位b5;
begin
if(con==5000-1)begin //等con计数到1个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[5]<=RX; //从RX采到b5位的数
state<=8; //状态往下跳转
end
else begin
con<=con+1;
end
end
8://收最低位b6;
begin
if(con==5000-1)begin //等con计数到1个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[6]<=RX; //从RX采到b6位的数
state<=9; //状态往下跳转
end
else begin
con<=con+1;
end
end
9://收最低位b7;
begin
if(con==5000-1)begin //等con计数到1个Tbit就可以采数了,否则con继续累加
con<=0; //con清零
data_out[7]<=RX; //从RX采到b7位的数
state<=10; //状态往下跳转
end
else begin
con<=con+1;
end
end
10://产生使能脉冲;数据收全后产生一个信号
begin
en_data_out<=1;//产生使能脉冲
state<=1;//状态回到1状态
end
default://其他状态清零
begin
state<=0;
con<=0;
con_bits<=0;
en_data_out<=0;
end
endcase
end
endmodule
`timescale 1ns/10ps
//testbench of UART_RXer
module UART_RXer_tb;
reg clk ;
reg res ;
wire RX ;//因为把RX最低位作为发送端了,所以RX定义为wire
wire [7:0] data_out ;
wire en_data_out ;
reg [25:0] RX_send ;//定义寄存器里装有串口字节发送数据
reg [12:0] con;//计算Tbit宽度
assign RX=RX_send[0];//连接RX; 将RX_send最低位传给RX
UART_RXer UART_RXer(
.clk (clk ),
.res (res ),
.RX (RX ),
.data_out (data_out ),
.en_data_out (en_data_out)
);
initial begin
clk<=0;res<=0;RX_send<={1'b1,8'haa,1'b0,16'hffff};con<=0;
//RX_send触发器的数据连续右移,最开始先出很多1代表空闲状,然后发一个0,再把aa发出去,最后发一个结束位。
#17 res<=1;
#1000 $stop;
end
always #5 clk<=~clk;
always @(posedge clk) begin
if(con==5000-1)begin//con循环计数1个Tbit的宽度
con<=0;
end
else begin
con<=con+1;
end
if(con==0)begin//con计数到1个Tbit的宽度后,RX_send数据就右移
RX_send[24:0]<=RX_send[25:1];//将高25位的值赋给低25位,依次赋值就可以使数据右移
RX_send[25]<=RX_send[0];//最低位的值赋给最高位
//这样RX_send就会反复地右移循环。
end
end
endmodule
波形图:
4.4 串口数据发送
串口发送模块:
- TX为串口输出端口
- rdy为空闲标志,字节发送到rdy为高
- data_in为准备发送的字节
- en_data_in为字节发送使能端口,高使能
- 发送波特率4800,系统时钟频率24MHz
状态图:
状态图:第一个状态等待发送使能就是en_data_in为1;第二个状态填充发送寄存器,位拼接发送寄存器中放入{结束位,数据,起始位};第三个状态发送寄存器右移,右移发送寄存器,最低位连接TX,右移结束就代表发送完毕,然后再等待发送使能,接受新数据继续发送。
代码示例:
//串口发送模块
module UART_TXer(
clk,
res,
data_in,
en_data_in,
TX,
rdy
);
input clk ;
input res ;
input [7:0] data_in ; //准备发送的数据
input en_data_in ; //发送使能
output TX ;
output rdy ; //空闲标志,0表示空闲,1表示在发送数据,为1时不能灌数据。
reg [3:0] state ; //主状态机寄存器
reg [9:0] send_buf ; //发送寄存器
assign TX = send_buf[0]; //连接TX,send_buf最低位连接给TX,send_buf里的数据要按节奏(4800波特率)右移就相当于串口发送。
reg [9:0] send_flag; //用于判断右移结束
reg [12:0] con; //用于计算波特周期
reg rdy; //空闲标志,字节发送时rdy为高,表示正忙;rdy为0时表示空闲
always@(posedge clk or negedge res)
if(~res)begin
state <= 0;
send_buf <= 1; //send_buf最低位是接TX,所以最低位不能是0,最低位必须是1,因为TX在空闲的时候是1,所以要给send_buf值为1
con <= 0;
send_flag <= 10'b10_0000_0000;//
rdy<=0;
end
else begin
case(state)
0://等待使能信号
begin
if(en_data_in)begin//当发送使能一来,给send_buf一个位拼接
send_buf = {1'b1,data_in,1'b0};//在这个10位寄存器里面最低位应该是起始位0,最高位因该是结束位1,中间的8位才是需要的数据。
send_flag <= 10'b10_0000_0000;
rdy<=1;//一旦有发送数据rdy为高,表示正忙
state<=1;//状态就可以跳走
end
end
1://串口发送,寄存器右移;
begin
if(con == 5000-1)begin //con循环计数1个Tbit的宽度
con<=0;
end
else begin
con <= con + 1;
end
if(con == 5000-1)begin //con计数到1个Tbit的宽度后,send_buf数据就右移
send_buf[8:0] <= send_buf[9:1]; //右移,将高9位的值赋给低9位,依次赋值就可以使数据右移
send_flag[8:0] <= send_flag[9:1];//send_flag和send_buf一样一起右移,当send_flag里的数10'b10_0000_0000右移到最低位也为1时表示10位数据全部右移完,证明同时右移的send_buf里的10位数据也右移结束。
end
if(send_flag[0])begin//当send_flag的最低位为1时,就表示send_buf的右移可以结束了。
rdy<=0;//数据一旦发送结束,变为空闲状态
state<=0;//状态回到等待使能
end
end
endcase
end
endmodule
上述数据右移的方法为算术右移,空出高位补位为原来数据的符号位也就是:
- 原数据最高位为1则右移后空出的最高位补1;
- 原数据最高位为0则右移后空出的最高位补0.
数据右移:
- 10'b10_0000_0000
- 10'b11_0000_0000
- 10'b11_1000_0000
- 10'b11_1100_0000
- 10'b11_1110_0000
- 10'b11_1111_0000
- 10'b11_1111_1000
- 10'b11_1111_1100
- 10'b11_1111_1110
- 10'b11_1111_1111
`timescale 1ns/10ps
//testbench of UART_TXer
module UART_TXer_tb;
reg clk ;
reg res ;
reg [7:0] data_in ;
reg en_data_in ;
wire TX ;
wire rdy ;
UART_TXer UART_TXer(
.clk (clk ),
.res (res ),
.data_in (data_in ),
.en_data_in (en_data_in),
.TX (TX ),
.rdy (rdy )
);
initial begin
clk<=0; res<=0; data_in<=8'h0a; en_data_in<=0;//data_in<=8'h7f;
#17 res<=1;
#30 en_data_in<=1;
#10 en_data_in<=0;
#1000 $stop;
end
always #5 clk<=~clk;
endmodule
波形图:
发送数据 data_in 为 8'h0a
发送数据 data_in 为 8'h7f
4.5 串口指令处理器
结构图:
串口指令处理器:将指令处理模块和串口发送模块、串口接收模块合在一起可以得到一个串口指令处理器。
- 串口接收模块接收指令和数据,把接收到的指令和数据以并行输入的模式传给指令处理模块。
- 指令处理模块对指令和数据进行处理得到的结果并行输出给串口发送模块。
- 串口发送模块再将运算结果输出。
电路图:
- RX为串口输入端口
- TX为串口输出端口
- UART_RXer为串口接收模块
- UART_TXer为串口发送模块
- cmd_pro为指令处理模块
- 合成的UART_top为串口指令处理器
cmd_pro指令集格式:
每次连续接收三个字节,第一字节为指令CMD,第二字节为操作数A,第三字节为操作数B。
指令集如下:
CMD | 操作 |
8'h0a | A + B |
8'h0b | A - B |
8'h0c | A & B |
8'h0d | A | B |
状态图:
状态图:0状态接收指令和数据;1状态处理指令和数据;2状态返回指令执行结果;2状态结束后再回到0状态继续接收指令和数据。
代码示例:
UART_RXer串口接收模块为 4.3串口数据接收代码
//串口数据接收
module UART_RXer(
......
UART_TXer串口发送模块为 4.4串口数据发送代码
//串口发送模块
module UART_TXer(
......
cmd_pro指令处理模块:
//指令处理
module cmd_pro(
clk ,
res ,
din_pro ,
en_din_pro ,
dout_pro ,
en_dout_pro,
rdy
);
input clk ;
input res ;
input [7:0] din_pro ; //指令和数据输入端口
input en_din_pro ; //输入使能
output [7:0] dout_pro ; //指令执行结果
output en_dout_pro; //指令输出使能
output rdy ; //串口发送模块空闲标志,0表示空闲
parameter add_ab = 8'h0a;
parameter sub_ab = 8'h0b;
parameter and_ab = 8'h0c;
parameter or_ab = 8'h0d;
reg [2:0] state ; //主状态机寄存器
reg [7:0] cmd_reg ; //存放指令,第1个字节
reg [7:0] A_reg ; //存放指令,第2个字节,操作数A
reg [7:0] B_reg ; //存放指令,第3个字节,操作数B
reg [7:0] dout_pro ; //指令执行结果
reg en_dout_pro; //指令输出使能
always@(posedge clk or negedge res)
if(~res)begin
state <= 0 ;
cmd_reg <= 0 ;
A_reg <= 0 ;
B_reg <= 0 ;
dout_pro<= 0 ;
en_dout_pro<= 0 ;
end
else begin
case(state)
0: //等指令
begin
en_dout_pro <= 0;//拉低指令输出使能
if(en_din_pro)begin //输入使能一来,就说明串口接收模块已经收到完整的1个字节了
cmd_reg <= din_pro;//收指令,收到的字节放入存放指令寄存器,第1个字节
state <= 1;//状态跳转到状态1
end
end
1: //收A
begin
if(en_din_pro)begin
A_reg <= din_pro;//收指令,收到的字节放入存放指令寄存器,第2个字节,操作数A
state <= 2;//状态跳转到状态2
end
end
2: //收B
begin
if(en_din_pro)begin
B_reg <= din_pro;//收指令,收到的字节放入存放指令寄存器,第3个字节,操作数B
state <= 3;//状态跳转到状态3
end
end
3: //指令译码和执行
begin
state <= 4;
case(cmd_reg)
add_ab: //A+B
begin
dout_pro <= A_reg + B_reg ;
end
sub_ab: //A-B
begin
dout_pro <= A_reg - B_reg ;
end
and_ab: //A&B
begin
dout_pro <= A_reg & B_reg ;
end
or_ab: //A|B
begin
dout_pro <= A_reg | B_reg ;
end
endcase
end
4: //发送指令执行结果
begin
if(~rdy)begin //表示空闲时
en_dout_pro <= 1; //拉高指令输出使能,其目的是为了形成一个脉冲尖作为发送指令的信号。
state <= 0;//状态跳转到状态0,在rdy为1的空闲状态就表示发送指令已经结束了,就该跳到等指令的状态等待新的指令
end
end
default:
begin
state <= 0;//回到0状态
en_dout_pro <= 0;//拉低指令输出使能
end
endcase
end
endmodule
UART_top串口指令处理器:
//串口指令处理器
module UART_top(
clk ,
res ,
RX ,
TX
);
input clk ;
input res ;
input RX ;
output TX ;
//需要定义5个中间信号,就以cmd_pro中的5个信号名为主,由于这些信号在UART_top里只是一个连接关系,所以这些信号都是wire型。
wire [7:0] din_pro ;
wire en_din_pro ;
wire [7:0] dout_pro ;
wire en_dout_pro;
wire rdy ;
UART_RXer UART_RXer(
.clk (clk ),
.res (res ),
.RX (RX ),
.data_out (din_pro ),
.en_data_out (en_din_pro )
);
UART_TXer UART_TXer(
.clk (clk ),
.res (res ),
.data_in (dout_pro ),
.en_data_in (en_dout_pro),
.TX (TX ),
.rdy (rdy )
);
cmd_pro cmd_pro(
.clk (clk ),
.res (res ),
.din_pro (din_pro ),
.en_din_pro (en_din_pro ),
.dout_pro (dout_pro ),
.en_dout_pro (en_dout_pro),
.rdy (rdy )
);
endmodule
注意:
在做程序封装顶层的例化的时候,信号之间的连接不管对谁来说是输入还是输出,从顶层看都是纯连接关系就都是wire型。
在测试文件testbench中例化的时候会需要设计输入信号数据变动的情况下会将这个变动的信号定义为reg型。
在做程序顶层封装的文件的时候除了例化连接模块,不要有其他的逻辑语句在里面,顶层文件里是纯连接关系。
测试代码:
//testbench of UART_top
`timescale 1ns/10ps
module UART_top_tb;
reg clk ;
reg res ;
wire RX ;//因为把RX最低位作为发送端了,所以RX定义为wire
wire TX ;
reg [45:0] RX_send; 定义寄存器里面装有串口字节发送数据
assign RX = RX_send[0]; //连接RX; 将RX_send最低位传给RX
reg [12:0] con;//计算Tbit宽度
UART_top UART_top(
.clk (clk),
.res (res),
.RX (RX ),
.TX (TX )
);
initial begin
clk<=0;res<=0;RX_send<={1'b1,8'h09,1'b0,1'b1,8'h06,1'b0,1'b1,8'h0a,1'b0,16'hffff};con<=0;
//RX_send触发器的数据连续右移,最开始先出很多1代表空闲状,发一个起始位0,再把第一个数据8'h0a发出去(控制指令A+B),然后发一个结束位;发一个起始位0,再把第二个数据8'h06发出去,然后发一个结束位;发一个起始位0,再把第三个数据8'h09发出去,最后发一个结束位。
#17 res<=1;
#4000000 $stop;
end
always #5 clk<=~clk;
always @(posedge clk) begin
if(con==5000-1)begin//con循环计数1个Tbit的宽度
con<=0;
end
else begin
con<=con+1;
end
if(con==0)begin//con计数到1个Tbit的宽度后,RX_send数据就右移
RX_send[44:0]<=RX_send[45:1];//将高45位的值赋给低45位,依次赋值就可以使数据右移
RX_send[45]<=RX_send[0];//最低位的值赋给最高位
//这样RX_send就会反复地右移循环。
end
end
endmodule
波形图:
总结
以上就是今天要讲的内容,本文仅仅简单介绍了Verilog代码的基础内容和应用实例。