verilog知识积累 - minichao9901/TangNano-20k-Zynq-7020 GitHub Wiki
1) verilog基础知识
- Verilog 中有一个特殊的运算符是 C 语言中没有的,就是位拼接运算符。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。拼接符出现在左边或者右边都可以,这非常的方便,有点像python一样。
assign a={b,c};
assign {a,b}=c;
- 位运算符的与、或、非与逻辑运算符逻辑与、逻辑或、逻辑非使用时候容易混淆,逻辑运算符一般用在条件判断上,位运算符一般用在信号赋值上。
- 关系(>,>=,<,<=,==,!=)+逻辑(&& || !)==>用于条件判断
- 位运算符(& | ~ ^)用于信号赋值
- 归约运算符(&, ~&, |, ~|, ^, ~^)使用非常方便
- 比较简单的组合逻辑推荐使用 assign 语句,比较复杂的组合逻辑推荐使用 always 语句。像if/else,case/endcase语句只能用在always中。
- 在组合逻辑中,if 或者 case 语句不完整的描述。注意,是组合逻辑!只有不带时钟的 always 语句 if 或者 case 语句不完整才会产生 latch,带时钟的语句if 或者 case 语句不完整描述不会产生 latch
- casex的用法:可以用?作为通配符。例如优先级编码中。
always @(*)
begin
casex ({d3,d2,d1,d0})
4'B1??? : out_temp=3'b0_11; //可用?来标识x或z。
4'B01?? : out_temp=3'b0_10;
4'B001? : out_temp=3'b0_01;
4'B0001 : out_temp=3'b0_00;
4'B0000 : out_temp=3'b1_00; //输入无效
endcase
end
- 当需要按照流程或者步骤来完成某个功能时,用状态机。实际应用中三段式状态机使用最多,因为三段式状态机将组合逻辑和时序分开,使代码看上去更加清晰易懂,提高了代码的可读性,推荐大家使用三段式状态机。(野火用的是新式2段,更为简洁)
- Verilog 语法中的 localparam 代表的意思同样是参数定义,用法和 parameter 基本一致,区别在于 parameter 定义的参数可以做例化,而 localparam 定义的参数是指本地参数,上层模块不可以对 localparam 定义的参数做例化。
- Verilog可以定义signed类型,这样可以直接使用负数和算术移位。这个是可以综合的。因此不需要手动费劲的去做负数补码的转换。例如如下的rom模块:
module rom_signed(
input clk,
input rst_n,
input [2:0] addr,
output reg signed [7:0] data,
output wire signed [7:0] data_half,
output wire signed [7:0] data_half2
);
always @(posedge clk or negedge rst_n)
if(rst_n==0)
data<=8'd0;
else
case(addr)
0: data<=-100;
1: data<=-90;
2: data<=-80;
3: data<=0;
4: data<=10;
5: data<=20;
6: data<=30;
7: data<=40;
endcase
assign data_half=data>>>1; //算法移位,因为是signed类型,高位会补符号位
assign data_half2=data>>1; //逻辑移位,高位补0
endmodule
- 注意Verilog的2位数组要这样写, 2个维度都要给出起始和结束
reg [7:0] rom[0:1023];
- 任务调用中的输出参数必须是寄存器类型的。任务的定义和调用必须在同一模块 module 中,且任务的的调用只能在过程中,不能出现在 assign 语句中。另外,在一个任务中可以调用其它的任务和函数,也可以调用该任务本身。
- 函数定义在函数内部隐式地声明一个寄存器变量,该变量与函数同名并且取值范围相同,故也称 “函数名变量”。函数调用时通过这个函数名变量”来传递函数值。函数至少要有一个输入。要注意在函数定义中,不允许出现 output 和 inout 端口。函数调用既要出现在过程语句中,也可出现在 assign 语句;函数可调用其它的函数,但不能调用任务。
module Reverse(date_in,date_out);
input [7:0] date_in;
output [7:0] date_out;
//函数定义
function [7:0] ReverseBits;
input [7:0] Byte;
integer i;
begin
for (i = 0; i < 8; i = i + 1)
ReverseBits[7-i] = Byte[i];
end
endfunction
//函数调用
assign date_out=ReverseBits(date_in);
endmodule
- 二段式可以准确地描述 Mealy 状态机和 Moore 状态机,是目前较为流行此种描述方法,推荐同学采用这种方法。二段式的另一种方法是把状转换逻辑和输出逻辑分隔开来。一个 always 过程块描述状态转换,即状态寄存器和下一个状态逻辑用一个 always 过程块描述;另外,一个 always 过程块描述输出逻辑。
例 4. 3 二段式的另外一种描述方式
module control2(clk,reset,gate,oe,clear);
input clk,reset,gate;
output oe,clear;
parameter RESET=0,WAIT=1,MEASURE=2,LATCH=3; //状态编码
reg [1:0] state;
//第一段:完成状态转换
always @(posedge clk)
if (reset) state=RESET;
else
case(state)
RESET: state=WAIT;
WAIT: if(gate) state=MEASURE;
MEASURE: if(~gate) state=LATCH;
LATCH: state=RESET;
endcase
//第二段:完成状态转换,下面两条(两段)语句,用 always 语句只需一段
assign clear=(state==RESET);
assign oe=(state==LATCH);
endmodule
几段式还是要从本质上(组合或时序)看问题,不是形式上几段。例我们用一个时序 always 过程块描述寄存器,用二个组合 always 过程块描述下一个状态电路和输出电路,形式上是三段式,但本质上是二段式,因为三段式输出要求时序输出。
2) verilog编程规范
- RTL 级别代码里面不使用 initial 语句,仿真代码除外;
- 由于不同的解释器对于 TAB 翻译不一致,所以建议不使用 TAB,全部使用空格。
- 一个 always 需要配一个 begin 和 end;
- 纯延迟打拍信号使用_dly 作为后缀。
3) inout端口
- inout端口只能被定义为wire型,只能用assign赋值。
- 以下是典型用法
inout i2c_sda;
assign i2c_sda=(sda_en==1'b1)? i2c_sda_reg: 1'bz; //写
wire sda_in;
assign sda_in=i2c_sda; //读
4) verilog写模块收发/激励数据产生的标准套路:
- 定义2个核心计数器+状态机
- 输出可以用组合逻辑,条件就是前面的(2个计数器+状态机+输入)
- 输出也可以用时序逻辑。其实时序逻辑和组合逻辑差别就是延一拍,其它没啥差别。
- 从Mealy型状态机的角度去看,所有的输出,应该都是组合逻辑产生的。
- 模块与模块之间,用start_pulse, end_pulse来同步
- 可以看野火的i2c_eeprom章节。思路,让cnt_bit与数据位对齐非常好,可以简化数据收发的逻辑,例如:
case(state)
SEND_D_ADDR: if(cnt_bit<=6)
i2c_sda_reg<=DEVICE_ADDR[6-cnt_bit];
else
i2c_sda_reg<=0;
W_DATA: i2c_sda_reg<=wr_data[7-cnt_bit];
endcase
- 低电平有效的信号,使用_n 作为信号后缀;异步信号,使用_a 作为信号后缀;
5)线性序列机的写法
div_cnt---基础分频计数器(计时单元)
lsm_cnt---线性序列机计数器
parameter CNT_MAX=50_000_000;
reg [31:0] div_cnt;
reg [5:0] lsm_cnt;
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n==0)
div_cnt<=0;
else if(div_cnt==CNT_MAX-1)
div_cnt<=0;
else
div_cnt<=div_cnt+1;
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n==0)
lsm_cnt<=0;
else if(div_cnt==CNT_MAX-1)
lsm_cnt<=lsm_cnt+1;
6)case语句扩展范围,以支持不定长度的或表达式label
- 用小梅哥的EDA_V3扩展版运行,正确!
`timescale 1ns / 1ps
module sw_led_flow(
input sys_clk,
input sys_rst_n,
input [7:0] sw,
output reg [7:0] led
);
parameter CNT_MAX=50_000_000;
reg [31:0] div_cnt;
reg [5:0] lsm_cnt;
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n==0)
div_cnt<=0;
else if(div_cnt==CNT_MAX-1)
div_cnt<=0;
else
div_cnt<=div_cnt+1;
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n==0)
lsm_cnt<=0;
else if(div_cnt==CNT_MAX-1)
lsm_cnt<=lsm_cnt+1;
always @(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n==0)
led<=8'b00000000;
else
case(lsm_cnt)
0: led<=8'b00000001;
1: led<=8'b00000010;
2: led<=8'b00000100;
3: led<=8'b00001000;
4: led<=8'b00010000;
5: led<=8'b00100000;
6: led<=8'b01000000;
7: led<=8'b10000000;
default: begin
if(lsm_cnt<sw/2) //8....sw/2-1
led<=sw;
else //sw/2....sw
led<=~sw;
end
endcase
endmodule
7) verilog定义字符串的方法
parameter N=14;
reg [0:8*N-1] str="hello world!\r\n";
使用字符串的方法
reg [3:0] index;
xfer_data<=str[(8*index) +:8];
8)verilog索引一个范围,其中索引为变量,需要使用+:或者-:符号。不能使用str[8index: 8index+7]
reg [3:0] index;
xfer_data<=str[(8*index) +:8]; //逐步递增
xfer_data<=str[(8*index) -:8]; //逐步递减
9)如何根据,当数据data[7:0]变化时,产生一个start_pulse?
很简单,将data延迟一拍后,再将data与data_s进行比较即可。
reg [7:0] cmd_s;
wire xfer_start;
reg xfer_end;
always @(posedge clk or negedge rst_n)
if(rst_n==0)
cmd_s<=8'h00;
else
cmd_s<=cmd;
assign xfer_start= (cmd!=cmd_s);
关于模块之间的握手:valid/ready/data_en
大佬们是这么说的
一般命令式的都是脉冲,状态式的都是电平
这里en是前级向下发数据,数据在en有效的这个时钟里面,所以是脉冲,很合理
rdy是后级向前级的信号,只是是否busy,只要后面没忙完,就要一直busy
你这个是cpu和外设通信的时序
上面mdy的那个我觉得更适合状态机和模块
因为模块内只有一个寄存器,en下来就得赶紧busy,所以用的组合逻辑
如果里面是fifo,那就不着急busy了