主体内容摘自:https://blog.csdn.net/qq_34291505/article/details/87714172
在Verilog里,模块内部主要有“线网(wire)”和“四态变量(reg)”两种硬件类型,它们用于描述数字电路的组合逻辑和时序逻辑。在Chisel里,也按这个思路定义了一些硬件类型,包括基本的线网和寄存器。
在verilog中我们经常会对线网和寄存器赋值,以完成信号的传递或者电路的连接。在chisel中也是,线网和寄存器最常见的操作都是被赋值,因为只有这样我们才能实现各种我们想要的电路逻辑,所以,下面也会对在chisel中如何对线网和寄存器赋值做一下说明!!!
一、Chisel是如何赋值的
我们可以使用用赋值操作来进行信号的传递或者电路的连接。只有硬件赋值才有意义,单纯的数据类型的对象进行赋值并不会被编译器转换成实际的电路,因为在Verilog里也是对wire、reg类型的硬件进行赋值。那么,赋值操作需要什么样的操作符来完成呢?
在Chisel里,所有对象都应该由val类型的变量来引用,因为硬件电路的不可变性。因此,一个变量一旦初始化时绑定了一个对象,就不能再发生更改。但是,引用的对象很可能需要被重新赋值。例如,输出端口在定义时使用了“=”与端口变量名进行了绑定,那等到驱动该端口时,就需要通过变量名来进行赋值操作,更新数据。很显然,此时“=”已经不可用了,因为变量在声明的时候不是var类型。即使是var类型,这也只是让变量引用新的对象,而不是直接更新原来的可变对象。
为了解决这个问题,几乎所有的Chisel类都定义了方法“:=”,作为等号赋值的代替。所以首次创建变量时用等号初始化,如果变量引用的对象不能立即确定状态或本身就是可变对象,则在后续更新状态时应该用“:=”。从前面讲的操作符优先级来判断,该操作符以等号结尾,而且不是四种逻辑比较符号之一,所以优先级与等号一致,是最低的。例如:
val x = Wire(UInt(4.W))
val y = Wire(UInt(4.W))
x := "b1010".U // 向4bit的线网x赋予了无符号数10
y := ~x // 把x按位取反,传递给y
二、线网(类似于verilog中组合逻辑用的wire)
Chisel把线网作为电路的节点,通过工厂方法“Wire[T <: Data](t: T)
”来定义。可以对线网进行赋值,也可以连接到其他电路节点,这是组成组合逻辑的基本硬件类型。例如:
- 可以自动推断位宽:
val w0 = Wire(UInt()) // width is inferred
- 也可以指定位宽:
val w1 = Wire(UInt(8.W)) // width is set to 8
注1:
- 里面包裹Vec或者MixedVec也是可以的(注意,不能包裹Vecinit或者MixedVecinit,因为它们传入的是硬件类型,返回的也是硬件类型,而Wire必须传入数据类型):
val w2 = Wire(Vec(4, UInt())) // width is inferred
val w3 = Wire(Vec(4, UInt(8.W))) // width of each element is set to 8
但需要注意的是,一旦被Wire包裹,必须提供初始值,也即必须被驱动,至少要赋个DontCare,否则会报错。不过这个检查机制是可以关闭的!!!
注2: 赋值覆盖性理解
因为Scala作为软件语言是顺序执行的,定义具有覆盖性,所以如果对同一个线网多次赋值,则只有最后一次有效。例如下面的代码与上面的例子是等效的:
val myNode = Wire(UInt(8.W))
myNode := 10.U
myNode := 0.U
需要注意的是,如果存在多条赋值语句,那么要考虑覆盖性;如果只有一条赋值语句,那么该条语句一定是有效的,不能认为软件语言是顺序执行的就觉得该条语句执行过一次之后就没有了。因为对线网类型的赋值操作,在生成的verilog代码中一定会将其变成
assign
语句,那么该语句是一直有效的,你也可以认为一直在执行,只要右边的驱动源发生变化,左边的目标信号就一定会改变!!!下面举一个例子:
这是一个0-1上升沿检测的状态机,可以看到在switch里面,并没有对io.risingEdge进行false的赋值。这是因为,当需要进行true赋值的时候,它会覆盖一开始的false赋值语句;但是当条件不满足,不进行true赋值的时候,最上面的false赋值语句就会起作用,所以不在switch里面进行false赋值也是可以的。总之,总会有一个赋值语句起作用,否则就有可能出现未驱动的情况!!!
注3: 未驱动的线网
Chisel的Invalidate API支持检测未驱动的输出型IO以及定义不完整的Wire定义,在编译成firrtl时会产生“not fully initialized”错误。换句话说,就是组合逻辑的真值表不完整,不能综合出完整的电路。如果确实需要不被驱动的线网,则可以赋给一个DontCare对象,这会告诉Firrtl编译器,该线网故意不被驱动。转换成的Verilog会赋予该信号全0值,甚至把逻辑全部优化掉,所以谨慎使用。例如:
val io = IO(new Bundle {
val outs = Output(Vec(10, Bool()))
})
io.outs <> DontCare
检查机制是由
CompileOptions.explicitInvalidate
控制的,如果把它设置成true就是严格模式(执行检查),设置成false就是不严格模式(不执行检查)。
- 开关方法有两种,第一种是定义一个抽象的模块类,由抽象类设置,其余模块都继承自这个抽象类。例如:
// 严格
abstract class ExplicitInvalidateModule extends Module()(chisel3.core.ExplicitCompileOptions.NotStrict.copy(explicitInvalidate = true))
// 不严格
abstract class ImplicitInvalidateModule extends Module()(chisel3.core.ExplicitCompileOptions.Strict.copy(explicitInvalidate = false))
- 第二种方法是在每个模块里重写compileOptions字段,由该字段设置编译选项。例如:
// 严格
class MyModule extends Module {
override val compileOptions = chisel3.core.ExplicitCompileOptions.NotStrict.copy(explicitInvalidate = true)
...
}
// 不严格
class MyModule extends Module {
override val compileOptions = chisel3.core.ExplicitCompileOptions.Strict.copy(explicitInvalidate = false)
...
}
三、寄存器(类似于verilog中时序逻辑中用的reg)
寄存器是时序逻辑的基本硬件类型,它们都是由当前时钟域的时钟上升沿触发的。如果模块里没有多时钟域的语句块,那么寄存器都是由隐式的全局时钟来控制。对于有复位信号的寄存器,如果不在多时钟域语句块里,则由隐式的全局复位来控制,并且高有效。目前Chisel所有的复位都是同步复位,异步复位功能还在开发中。如果需要异步复位寄存器,则需要通过黑盒引入。
有五种内建的寄存器:
- 第一种是跟随寄存器“
RegNext[T <: Data](next: T)
”,在每个时钟上升沿,它都会采样一次传入的参数(跟随),并且没有复位信号;它的另一个版本的apply工厂方法是“RegNext[T <: Data](next: T, init: T)
”,也就是由复位信号控制,当复位信号有效时,复位到指定值,否则就跟随。
注4:
注意,在定义时传入next,后面可以无需再显式的对寄存器赋值,它会自动跟随next;但也可以在后面再对其进行无条件或者有条件赋值,这样会覆盖掉之前的定义或者赋值,然后开始跟随新的变量。这里的覆盖,比如
RegNext[T <: Data](next: T, init: T)
,一开始如果给了init参数,那么是会有复位信号的, 但如果被后面的无条件或者有条件赋值给覆盖,那么复位信号的有无就要取决了后面的赋值逻辑!!!
- 第二种是复位到指定值的寄存器“
RegInit[T <: Data](init: T)
”或者“RegInit[T <: Data](t: T, init: T)
”,可以进行条件赋值,也可以无条件赋值;默认有复位信号;参数t
的意思是提供一个数据类型模板,也即指定数据的类型,不用给确切的值。
定义之后可以不显式赋值,但是会被当做一个常量,常量的值取决于你定义时给的init参数的值。
- 第三种是普通的寄存器“
Reg[T <: Data](t: T)
”,可以进行条件赋值,也可以进行无条件赋值,默认没有复位信号;参数t
的意思是提供一个数据类型模板,也即指定数据的类型,不用给确切的值。
注5:
定义之后必须显式赋值,否则报错!!!
- 第四种是util包里的带一个使能端的寄存器“
RegEnable[T <: Data](next: T, init: T, enable: Bool)
”,如果不需要复位信号,则第二个参数需要省略给出;有使能信号。
注6:
注意,在定义时传入next,后面可以无需再显式的对寄存器赋值,它会自动跟随next;但也可以在后面再对其进行无条件或者有条件赋值,这样会覆盖掉之前的定义或者赋值,然后开始跟随新的变量。一开始如果给了init参数,那么是会有复位信号的,也有使能信号; 但如果被后面的无条件或者有条件赋值给覆盖,那么复位信号和使能信号的有无就都要取决了后面的赋值逻辑了!!!
- 第五种是util包里的移位寄存器“
ShiftRegister[T <: Data](in: T, n: Int, resetData: T, en: Bool)
”,其中第一个参数in是待移位的数据;第二个参数n是需要延迟的周期数;第三个参数resetData是指定的复位值,可以省略,省略之后无复位信号;第四个参数en是使能移位的信号,默认为true.B。
从以上内容可以总结出:
- 如果寄存器有next参数,那么就可以无任何显式赋值操作,寄存器会跟随next参数,如RegNext、RegEnable;如果无next参数,而是t参数,那么就必须显式的赋值,如RegInit、Reg;ShiftRegister因为有in参数,也可以不用显式赋值。
- 如果有init或者resetData参数,那么就不需要显式的将reset信号作为when的条件进行赋值,这样也会有复位信号,当然也可以通过省略init参数使得生成的verilog代码中没有复位信号(RegInit除外);如果没有init参数,就只有通过将reset信号作为when的条件进行赋值才会有复位信号。
- 无条件赋值就是直接赋值,有条件就是使用when语句。
- 如果寄存器被赋值为常量,那么在生成的verilog中就不会出现该寄存器变量;如果被赋值为变量,那么才会在跟随该变量,跟随就是每个时钟上升沿时对变量进行采样。
- 除了Reg,其余寄存器传入的参数都必须是硬件类型,而Reg传入的参数是数据类型。
- 关于t参数的理解:提供一个参数模板的意思就是指定该寄存器的数据类型,不能指定为确切值,并且在以后赋值的时候所提供的值必须是该类型,如下所示:
class MyBundle extends Bundle {
val unknown = UInt()
val known = UInt(8.W)
}
val myreg = Reg(new MyBundle)
val myReg = RegInit(UInt(12.W), 0.U)//对应RegInit[T <: Data](t: T, init: T)
val myReg = RegInit(0.U(12.W))//对应RegInit[T <: Data](init: T)
假如有如下代码:
// reg.scala
package test
import chisel3._
import chisel3.util._
class REG extends Module {
val io = IO(new Bundle {
val a = Input(UInt(8.W))
val en = Input(Bool())
val c = Output(UInt(1.W))
})
val reg0 = RegNext(io.a)
val reg1 = RegNext(io.a, 0.U)
val reg2 = RegInit(0.U(8.W))
val reg3 = Reg(UInt(8.W))
val reg4 = Reg(UInt(8.W))
val reg5 = RegEnable(io.a + 1.U, 0.U, io.en)
val reg6 = RegEnable(io.a - 1.U, io.en)
val reg7 = ShiftRegister(io.a, 3, 0.U, io.en)
val reg8 = ShiftRegister(io.a, 3, io.en)
reg2 := io.a.andR
reg3 := io.a.orR
when(reset.asBool) {
reg4 := 0.U
} .otherwise {
reg4 := 1.U
}
io.c := reg0(0) & reg1(0) & reg2(0) & reg3(0) & reg4(0) & reg5(0) & reg6(0) & reg7(0) & reg8(0)
}
对应生成的主要Verilog代码为:
// REG.v
module REG(
input clock,
input reset,
input [7:0] io_a,
input io_en,
output io_c
);
reg [7:0] reg0;
reg [7:0] reg1;
reg [7:0] reg2;
reg [7:0] reg3;
reg [7:0] reg4;
wire [7:0] _T_1;
reg [7:0] reg5;
wire [8:0] _T_2;
wire [8:0] _T_3;
wire [7:0] _T_4;
reg [7:0] reg6;
reg [7:0] _T_5;
reg [7:0] _T_6;
reg [7:0] reg7;
reg [7:0] _T_7;
reg [7:0] _T_8;
reg [7:0] reg8;
wire [7:0] _T_9;
wire _T_10;
wire _T_11;
wire _GEN_8;
wire _T_13;
wire _T_14;
wire _T_15;
wire _T_16;
wire _T_17;
wire _T_18;
wire _T_19;
wire _T_20;
wire _T_21;
wire _T_22;
wire _T_23;
wire _T_24;
wire _T_25;
wire _T_26;
wire _T_27;
wire _T_28;
assign _T_1 = io_a + 8'h1;
assign _T_2 = io_a - 8'h1;
assign _T_3 = $unsigned(_T_2);
assign _T_4 = _T_3[7:0];
assign _T_9 = ~ io_a;
assign _T_10 = _T_9 == 8'h0;
assign _T_11 = io_a != 8'h0;
assign _GEN_8 = reset ? 1'h0 : 1'h1;
assign _T_13 = reg0[0];
assign _T_14 = reg1[0];
assign _T_15 = _T_13 & _T_14;
assign _T_16 = reg2[0];
assign _T_17 = _T_15 & _T_16;
assign _T_18 = reg3[0];
assign _T_19 = _T_17 & _T_18;
assign _T_20 = reg4[0];
assign _T_21 = _T_19 & _T_20;
assign _T_22 = reg5[0];
assign _T_23 = _T_21 & _T_22;
assign _T_24 = reg6[0];
assign _T_25 = _T_23 & _T_24;
assign _T_26 = reg7[0];
assign _T_27 = _T_25 & _T_26;
assign _T_28 = reg8[0];
assign io_c = _T_27 & _T_28;
always @(posedge clock) begin
reg0 <= io_a;
if (reset) begin
reg1 <= 8'h0;
end else begin
reg1 <= io_a;
end
if (reset) begin
reg2 <= 8'h0;
end else begin
reg2 <= {{7'd0}, _T_10};
end
reg3 <= {{7'd0}, _T_11};
reg4 <= {{7'd0}, _GEN_8};
if (reset) begin
reg5 <= 8'h0;
end else begin
if (io_en) begin
reg5 <= _T_1;
end
end
if (io_en) begin
reg6 <= _T_4;
end
if (reset) begin
_T_5 <= 8'h0;
end else begin
if (io_en) begin
_T_5 <= io_a;
end
end
if (reset) begin
_T_6 <= 8'h0;
end else begin
if (io_en) begin
_T_6 <= _T_5;
end
end
if (reset) begin
reg7 <= 8'h0;
end else begin
if (io_en) begin
reg7 <= _T_6;
end
end
if (io_en) begin
_T_7 <= io_a;
end
if (io_en) begin
_T_8 <= _T_7;
end
if (io_en) begin
reg8 <= _T_8;
end
end
endmodule
四、寄存器组(可以一次性生成多个reg)
上述构造寄存器的工厂方法,它们的参数可以是任何Data的子类型。如果把子类型Vec[T]作为参数传递进去,就会生成多个位宽相同、行为相同、名字前缀相同的寄存器。当然如果Vec[T]或者Vecinit[T]中的参数不一样,那么生成的多个reg也不一样,其实它们本质上是没啥必然联系的,具体咋用取决于你想怎么操作它们!!!。同样,寄存器组在Chisel代码里可以通过下标索引。例如:
// reg2.scala
package test
import chisel3._
import chisel3.util._
class REG2 extends Module {
val io = IO(new Bundle {
val a = Input(UInt(8.W))
val en = Input(Bool())
val c = Output(UInt(1.W))
})
val reg0 = RegNext(VecInit(io.a, io.a))
val reg1 = RegNext(VecInit(io.a, io.a), VecInit(0.U, 0.U))
val reg2 = RegInit(VecInit(0.U(8.W), 0.U(8.W)))
val reg3 = Reg(Vec(2, UInt(8.W)))
val reg4 = Reg(Vec(2, UInt(8.W)))
val reg5 = RegEnable(VecInit(io.a + 1.U, io.a + 1.U), VecInit(0.U(8.W), 0.U(8.W)), io.en)
val reg6 = RegEnable(VecInit(io.a - 1.U, io.a - 1.U), io.en)
val reg7 = ShiftRegister(VecInit(io.a, io.a), 3, VecInit(0.U(8.W), 0.U(8.W)), io.en)
val reg8 = ShiftRegister(VecInit(io.a, io.a), 3, io.en)
reg2(0) := io.a.andR
reg2(1) := io.a.andR
reg3(0) := io.a.orR
reg3(1) := io.a.orR
when(reset.asBool) {
reg4(0) := 0.U
reg4(1) := 0.U
} .otherwise {
reg4(0) := 1.U
reg4(1) := 1.U
}
io.c := reg0(0)(0) & reg1(0)(0) & reg2(0)(0) & reg3(0)(0) & reg4(0)(0) & reg5(0)(0) & reg6(0)(0) & reg7(0)(0) & reg8(0)(0) &
reg0(1)(0) & reg1(1)(0) & reg2(1)(0) & reg3(1)(0) & reg4(1)(0) & reg5(1)(0) & reg6(1)(0) & reg7(1)(0) & reg8(1)(0)
}
五、用when给线网或者寄存器进行条件赋值
在Verilog里,可以使用“if…else if…else”这样的条件选择语句来方便地构建电路的逻辑。由于Scala已经占用了“if…else if…else”语法,所以相应的Chisel控制结构改成了when语句,其语法如下:
when (condition 1) { definition 1 }
.elsewhen (condition 2) { definition 2 }
...
.elsewhen (condition N) { definition N }
.otherwise { default behavior }
注意,“.elsewhen”和“.otherwise
”的开头有两个句点。所有的判断条件都是返回Bool
类型的传名参数,不要和Scala的Boolean
类型混淆,也不存在Boolean和Bool之间的相互转换。对于UInt、SInt,甚至是Clock和Reset
类型,可以用方法asBool转换成Bool
类型来作为判断条件。
when语句不仅可以给线网赋值,还可以给寄存器赋值,但是要注意构建组合逻辑时不能缺失“.otherwise
”分支。 下面是一个简单的寻找三个数中的最大值的模块:
class Max3 extends Module {
val io = IO(new Bundle {
val in1 = Input(UInt(16.W))
val in2 = Input(UInt(16.W))
val in3 = Input(UInt(16.W))
val out = Output(UInt(16.W))
})
when(io.in1 >= io.in2 && io.in1 >= io.in3) {
io.out := io.in1
}.elsewhen(io.in2 >= io.in3) {
io.out := io.in2
}.otherwise {
io.out := io.in3
}
}
when有很多场合可以使用:
- 比如使用when给带使能信号的寄存器更新数据;
- 还可用于状态机的状态转换;
除了when结构,util包里还有一个与之对偶的结构“unless”,如果unless的判定条件为false.B则一直执行,否则不执行:
import chisel3.util._
unless (condition) { definition }