Verilog语法

$signed()

这里写一下关于 $signed() 的理解。

$signed() 的真正功能是决定数据如何进行补位。一个表达式(特别注意三目运算符)中如果存在一个无符号数,那么整个表达式都会被当作无符号数。

signedness

  • self-determined expression:指一个位宽可以由该表达式本身独立决定的 expression
  • context-determined expression:指一个位宽由其本身以及其所属的表达式共同决定的 expression(例如一个阻塞赋值语句右侧表达式的位宽同时受左右两侧位宽的影响)

Verilog 在计算表达式前,会先计算表达式的 signedness。计算方式如下:

  • 仅由操作数决定,不由左侧决定(如 assign D = expexp 符号与 D 无关。这一点区别于位宽,位宽由左右两侧所有表达式的最大位宽决定)
  • 小数是有符号的,显式声明了进制的数无符号,除非用修饰符 s 声明了其有符号(如 'd12 无符号,'sd12 有符号
  • 位选_多位选择_位拼接的结果无符号(如 b[2]b[3:4]{b} 均无符号,这也是隔壁 bit extender 有同学用三目运算符结果 WA 了的原因)
  • 比较表达式的结果无符号(要么是 0,要么是 1
  • 由实数强转成整型的表达式有符号
  • 一个 self-determined expression 的符号性仅取决与其操作数
  • 对于 context-determined expression,只有所有操作数均为有符号数时表达式才有符号

在计算表达式时,先由以上规则得出最外层表达式的符号性,再向表达式里的操作数递归传递符号性。

$signed() 函数的机制是计算传入的表达式,返回一个与原表达式的值和位宽完全相同的值,并将其符号性设为有符号。该函数可以屏蔽外部表达式的符号性传递。

另外就是算术右移运算符(>>>)的有操作数不影响结果的符号,所以没必要加上 $signed()

一些例子

下面是一些例子:

  • assign C = (ALUOp==3'b101) ? $signed(A)>>>B : $signed(A+B); 正确
  • assign C = (ALUOp==3'b101) ? $signed(A)>>>B : A+B; 错误
  • assign C = (ALUOp==3'b101) ? $signed(A)>>>B : 0; 正确(0 被看作有符号数)
  • assign C = (ALUOp==3'b101) ? $signed(A)>>>B : 32'b0; 错误(显式声明了进制,被当成无符号数)

特别的,还有:

  • assign C = (ALUOp==3'b101) ? $signed(signed(A)>>>B) : $signed(A+B); 正确

因为外层推导出是无符号,但是外面的 $signed 阻止了 unsignedness 向内层转移。

位扩展

$signed() 用法太玄妙了,不如直接用位扩展替代。

{{16{imm[15]}}, imm} // 将 [15:0] 的 imm 扩展到 32 位

常用运算符

Verilog HDL 中有相当多的运算符都与 C 语言基本相同,如:

  • 基本运算符:+, -, *, /, %
  • 位运算符:&, |, ~, ^, >>, <<
  • 逻辑运算符:&&, ||, !
  • 关系运算符:>, <, >=, <=
  • 条件运算符:? :

这些运算的运算规则与 C 语言相同,只是在操作数中出现了不定值 x 和高阻值 z 的话最终结果可能也是带 xz 的。另外 Verilog 中没有自增、自减运算符。下面主要介绍其他与 C 不同的部分。

  • 逻辑右移运算符 >> 与算术右移运算符 >>>

    它们的区别主要在于前者在最高位补 0,而后者在最高位补符号位

  • 相等比较运算符 =====!=!==

    ==!= 可能由于不定值 x 和高阻值 z 的出现导致结果为不定值 x,而 ===!== 的结果一定是确定的 0 或 1xz 也参与比较)。

  • 阻塞赋值 = 和非阻塞赋值 <=

    不同于 assign 语句,这两种赋值方式被称为过程赋值,通常出现在 initialalways 块中,reg 型变量赋值。这种赋值类似 C 语言中的赋值,不同于 assign 语句,赋值仅会在一个时刻执行。由于 Verilog 描述硬件的特性,Verilog程序内会有大量的并行,因而产生了这两种赋值方式。这两种赋值方式的详细区别我们会在之后的小节内介绍,这里暂时只需记住一点:为了写出正确、可综合的程序,在描述时序逻辑时要使用非阻塞式赋值 <=

  • 位拼接运算符 {}

    这个运算符可以将几个信号的某些位拼接起来,例如 {a, b[3:0], w, 3'b101};;可以简化重复的表达式,如 {4{w}} 等价于 {w,w,w,w};还可以嵌套,{b, {3{a, b}}} 等价于 {b, {a, b, a, b, a, b}},也就等价于 {b, a, b, a, b, a, b}

  • 缩减运算符

    运算符 &(与)、|(或)、^(异或)等作为单目运算符是对操作数的每一位汇总运算,如对于 reg[31:0] B; 中的 B 来说,&B 代表将 B 的每一位与起来得到的结果。

宏定义

  • ``define 标识符(宏名) 字符串(宏内容)`
  • 引用宏名时也必须在宏名前加上符号 `
`define WORDSIZE 8
// 省略模块定义
reg[1:`WORDSIZE] data;
// 相当于定义 reg[1:8] data;

integer 型

  • int型,默认为有符号数

parameter 型

  • 常量,用于在模块实例化时传递参数

  • parameter 标识符 = 表达式; 如:parameter width = 8;

  • 在模块内部使用,parameter可以被重新赋值

wire

线网 nets 型数据

  • 一般使用 assign 语句对 wire 型数据进行驱动
  • 要按照组合逻辑的规则进行操作。如,对于 wire 型变量 aassign a = a + 1是不合法的
  • 一般在使用 wire 型数据前应先声明它。但如果在模块实例的端口信号列表中使用了一个未声明的变量,则会将其默认定义为 1 位的 wire 变量

reg

寄存器数据类型

  • reg 型变量不能使用 assign 赋值
  • 访问存储器型数据,可以用类似于位选择操作的方式。如 mem[2] 就是访问 mem 中的第 3 个元素
  • 信号定义好之后,不仅决定了位宽还决定了方向,在使用中只能正向接,不能反向接
  • reg型和always 关键字配合,建模组合逻辑

eg.

module ext(
input [15:0] imm,
input [1:0] EOp,
output reg [31:0] ext
);
always@(*) begin
case(EOp)
2'b00: ext <= {16{imm[15]},imm[15:0]};
2'b01: ext <= {16'b0,imm[15:0]};
2'b10: ext <= {imm[15:0],16'b0};
2'b11: ext <= ({16{imm[15]},imm[15:0]}<<2);
default: ext<= 0;
endcase
end
endmodule

数字字面量

  • <位宽>'<进制><值>,如 10'd100
  • 二进制(b 或 B)、八进制(o 或 O)、十六进制(h 或 H)、十进制(d 或 D)
  • 默认位宽:32位;默认进制:十进制;前导0可以省略;
  • 值部分可以用下划线分开提高可读性,如 16'b1010_1011_1111_1010

两个特殊的值:xz

x 为不定值,当某一二进制位的值不能确定时出现,变量的默认初始值为 x

z 为高阻态,代表没有连接到有效输入上

数据拼接与拆分

  • 拼接
reg [15:0] regA;
reg [7:0] regB = 8'd12;
reg [7:0] regC = 8'd34;

regA <= {regB ,regC }; //把regB和regC拼接成regA
  • 拆分
wire[15:0] A;
wire[7:0] B;
wire[7:0] C;
assign B = A [15:8];
assign C = A [7:0]; //就是把高8位和低8位拆分输出

有符号数$signed()

  • wirereg 等数据类型默认是无符号的
  • 用 **$signed()**使之成为有符号数
  • Verilog 会自动地做数据类型匹配,将符号数向无符号数转化

image-20231011235907762

阻塞赋值 = 和非阻塞赋值 <=

  • 当前一句阻塞赋值完成后(即 = 左边的变化为右边的值后),下一条阻塞赋值语句才会被继续执行
  • 处在一个 always 块中的非阻塞赋值是在块结束时同时并发执行的,在描述时序逻辑时要使用非阻塞式赋值 <=

逻辑右移运算符 >> 与算术右移运算符 >>>

  • 它们的区别主要在于前者在最高位补 0,而后者在最高位补符号位

相等比较运算符 =====!=!==

  • ==!= 可能由于不定值 x 和高阻值 z 的出现导致结果为不定值 x
  • ===!== 的结果一定是确定的 0 或 1xz 也参与比较)。

缩减运算符

  • 运算符 &(与)、|(或)、^(异或)对操作数的每一位进行相应运算
  • 如对于 reg[31:0] B; 中的 B 来说,&B 代表将 B 的每一位与起来得到的结果。

组合逻辑建模

assign

  • assign 语句不能在 alwaysinitial 块中使用
  • assign 语句经常与三目运算符配合使用建模组合逻辑
  • 在一个时钟周期里,对一个wire型变量,只能assign一次

法一:

assign语句和三目运算符配合,建模组合逻辑

module ALU(
input [3:0] inA,
input [3:0] inB,
input [1:0] op,
output [3:0] ans
);

// 利用三目运算符完成运算
assign ans = (op == 2'b00) ? inA + inB :
(op == 2'b01) ? inA - inB :
(op == 2'b10) ? inA | inB :
(op == 2'b11) ? inA & inB :
4'b0; // error

endmodule

法二:

reg型和always 关键字配合,建模组合逻辑

module ext(
input [15:0] imm,
input [1:0] EOp,
output reg [31:0] ext
);
always@(*) begin
case(EOp)
2'b00: ext <= {{16{imm[15:15]}},imm[15:0]};
2'b01: ext <= {{16{1'b0}},imm[15:0]};
2'b10: ext <= {imm[15:0],{16{1'b0}}};
2'b11: ext <= ({{16{imm[15:15]}},imm[15:0]}<<2);
default: ext<= 0;
endcase
end
endmodule

时序逻辑建模

always

  • always 之后紧跟 @(...),表示当括号中的条件满足时,将会执行 always 之后的语句
always @(posedge clk)  // 表示在 clk 上升沿触发后面的语句块
begin
// 一些操作
end

posedge ,上升沿触发

negedge ,下降沿触发

每个条件使用逗号 ,or 隔开,只要有其中一个条件被触发,always 之后的语句都会被执行

  • always 之后紧跟 @ *@(*),则表示对其后紧跟的语句或语句块内所有信号的变化敏感。这种用法主要用于与 reg 型数据和阻塞赋值配合,建模组合逻辑

同步复位与异步复位

同步复位:

always@(posedge clk)begin
if(reset == 1'b1)begin
//进行复位操作
end
else begin
//进行其他操作
end
end

异步复位:

always@(posedge clk or posedge reset)begin
if(reset == 1'b1)begin
//进行复位操作
end
else begin
//进行其他操作
end
end

initial 块

  • reg 型变量的取值进行初始化
reg a;
initial begin
a = 0;
end

if 语句

  • 只能出现在always块内
always @ * begin
if (a > b) begin
out = a;
end
else begin
out = b;
end
end

case 语句

  • 和C略有不同
always @(posedge clk) begin
case(data)
0: out <= 4;
1: out <= 5;
2: out <= 2;
3: begin
out <= 1;
end
default: ;
endcase
end

for 语句

  • integerreg 类型的变量均可作为循环变量
module test(
input [1:0] x,
output reg [3:0] ans
);
integer i;
always @(*) begin
ans = 0;
for (i = 0; i < 3; i = i + 1) begin
ans = ans + x;
end
end
endmodule

时间控制语句

#3;         // 延迟 3 个时间单位
#5 b = a; // b 为 reg 型,延迟 5 个时间单位后执行赋值语句
always #5 clk = ~clk; // 每过 5 个时间单位触发一次,时钟信号反转,时钟周期为 10 个时间单位
assign #5 b = a; // b 为 wire 型,将表达式右边的值延时 5 个时间单位后赋给 b