0%

3.5 Functional Programming

Functions as Objects

  • Functions in Scala are first-class objects. That means we can assign a function to a val and pass it to classes, objects, or other functions as an argument.
1
2
3
4
5
6
// These are normal functions.
def plus1funct(x: Int): Int = x + 1
// These are functions as vals.
// The first one explicitly specifies the return type.
val plus1val: Int => Int = x => x + 1 // val plus1val: (Int => Int) = { x => x + 1 }
val times2val = (x: Int) => x * 2

val vs. def

  • (?) Why would you want to create a val instead of a def? With a val, you can now pass the function around to other functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// create our function
def plus2(x: Int): Int = x + 1
val plus1 = (x: Int) => x + 1
val times2 = (x: Int) => x * 2

// pass it to map, a list function
val myList = List(1, 2, 5, 9)
val myListPlus = myList.map(plus1)
val myListTimes = myList.map(times2)

// create a custom function, which performs an operation on X N times using recursion
def opN(x: Int, n: Int, op: Int => Int): Int = {
if (n <= 0) { x }
else { opN(op(x), n-1, op) }
}

opN(7, 5, plus1)
opN(7, 5, plus2) // O ?
opN(7, 3, times2)
  • Functions are evaluated every time they are called, while vals are evaluated at instantiation.
1
2
3
4
5
6
7
8
9
10
11
12
13
import scala.util.Random

// both x and y call the nextInt function, but x is evaluated immediately and y is a function
val x = Random.nextInt
def y = Random.nextInt

// x was previously evaluated, so it is a constant
println(s"x = $x")
println(s"x = $x")

// y is a function and gets reevaluated at each call, thus these produce different results
println(s"y = $y")
println(s"y = $y")

FIR Filter with Window Function Passed

  • window function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// get some math functions
import scala.math.{abs, round, cos, Pi, pow}

// simple triangular window
val TriangularWindow: (Int, Int) => Seq[Int] = (length, bitwidth) => {
val raw_coeffs = (0 until length).map( (x:Int) => 1-abs((x.toDouble-(length-1)/2.0)/((length-1)/2.0)) )
val scaled_coeffs = raw_coeffs.map( (x: Double) => round(x * pow(2, bitwidth)).toInt)
scaled_coeffs
}

// Hamming window
val HammingWindow: (Int, Int) => Seq[Int] = (length, bitwidth) => {
val raw_coeffs = (0 until length).map( (x: Int) => 0.54 - 0.46*cos(2*Pi*x/(length-1)))
val scaled_coeffs = raw_coeffs.map( (x: Double) => round(x * pow(2, bitwidth)).toInt)
scaled_coeffs
}
  • FIR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// our FIR has parameterized window length, IO bitwidth, and windowing function
class MyFir(length: Int, bitwidth: Int, window: (Int, Int) => Seq[Int]) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(bitwidth.W))
val out = Output(UInt((bitwidth*2+length-1).W)) // expect bit growth, conservative but lazy
})

// calculate the coefficients using the provided window function, mapping to UInts
val coeffs = window(length, bitwidth).map(_.U)

// create an array holding the output of the delays
// note: we avoid using a Vec here since we don't need dynamic indexing
val delays = Seq.fill(length)(Wire(UInt(bitwidth.W))).scan(io.in)( (prev: UInt, next: UInt) => {
next := RegNext(prev)
next
})

// multiply, putting result in "mults"
val mults = delays.zip(coeffs).map{ case(delay: UInt, coeff: UInt) => delay * coeff }

// add up multiplier outputs with bit growth
val result = mults.reduce(_+&_)

// connect output
io.out := result
}

visualize(() => new MyFir(7, 12, TriangularWindow))

3.6 Object Oriented Programming

Abstract Classe

  • Abstract Classes define unimplemented values that subclasses must implement.

  • Abstract class can’t be instantiated.

  • A class can only directly inherit from one parent abstract class.

    1
    class MyClass extends HasTrait1
1
2
3
4
5
6
7
8
9
10
abstract class MyAbstractClass {
def myFunction(i: Int): Int
val myValue: String
}
class ConcreteClass extends MyAbstractClass {
def myFunction(i: Int): Int = i + 1
val myValue = "Hello World!"
}
val abstractClass = new MyAbstractClass() // Illegal! Cannot instantiate an abstract class
val concreteClass = new ConcreteClass() // Legal!

Trait

  • Define unimplemented values like abstract class.

  • Traits class can’t be instantiated.

  • Difference:

    • A class can inherit from multiple traits.
    1
    class MyClass extends HasTrait1 with HasTrait2 with HasTrait3
    • (?) A trait cannot have constructor parameters.
1
2
3
4
5
6
7
8
9
10
11
12
13
trait HasFunction {
def myFunction(i: Int): Int
}
trait HasValue {
val myValue: String
val myOtherValue = 100
}
class MyClass extends HasFunction with HasValue {
override def myFunction(i: Int): Int = i + 1
val myValue = "Hello World!"
}
val myTraitFunction = new HasFunction() // Illegal! Cannot instantiate a trait
val myClass = new MyClass() // Legal!

Object

  • You can simply directly reference it
  • Object can’t be instantiated**. **(no need to call new)

Companion Object

  • Companion object and class share the same name and defined in the same file.

  • When you use new before the class/object name, it will instantiate the class.

    If you don’t use new, it will reference the object.

1
2
3
4
5
6
7
8
object Lion {
def roar(): Unit = println("I'M AN OBJECT!")
}
class Lion {
def roar(): Unit = println("I'M A CLASS!")
}
new Lion().roar() // instantiate class
Lion.roar() // companion ojbect
  • Companion objects are usually used for the following reasons:

    • to contain constants related to the class

    • to execute code before/after the class constructor

    • to create multiple constructors for a class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object Animal {
val defaultName = "Bigfoot"
private var numberOfAnimals = 0
def apply(name: String): Animal = {
numberOfAnimals += 1
new Animal(name, numberOfAnimals)
}
def apply(): Animal = apply(defaultName)
}
class Animal(name: String, order: Int) {
def info: String = s"Hi my name is $name, and I'm $order in line!"
}

val bunny = Animal.apply("Hopper")
val yeti = Animal()
  • Chisel uses many companion objects, like Module. When you write the following you are calling the Module companion object.
1
val myModule = Module(new MyModule)

Case Class

  • Allows external access to the class parameters.
  • Eliminates the need to use new when instantiating the class.

This is because the Scala compiler automatically creates a companion object for every case class in your code, which contains an apply method for the case class.

  • Automatically creates an unapply method that supplies access to all of the class Parameters.
  • Cannot be subclassed from.
1
2
3
4
5
6
7
8
9
10
11
class Nail(length: Int) // Regular class
val nail = new Nail(10) // Requires the `new` keyword
// println(nail.length) // Illegal! Class constructor parameters are not by default externally visible

class Screw(val threadSpace: Int) // By using the `val` keyword, threadSpace is now externally visible
val screw = new Screw(2) // Requires the `new` keyword
println(screw.threadSpace)

case class Staple(isClosed: Boolean) // Case class constructor parameters are, by default, externally visible
val staple = Staple(false) // No `new` keyword required
println(staple.isClosed)

Inheritance with Chisel

  • Every Chisel module you make is a class extending the base type Module.

  • Every Chisel IO you make is a class extending the base type Bundle (or, in some special cases, Bundle‘s supertype Record).

  • Chisel hardware types like UInt or Bundle all have Data as a supertype.

Module

3.7 Type

Scala vs. Chisel Type

  • Int vs. UInt
1
2
3
val a = Wire(UInt(4.W))
a := 0.U // legal: chisel UInt
a := 0 // illegal: scala Int
  • Boolean vs. Bool
1
2
3
4
5
6
7
8
val bool = Wire(Bool())
val boolean: Boolean = false
// legal
when (bool) { ... }
if (boolean) { ... }
// illegal
if (bool) { ... }
when (boolean) { ... }

Type Coercion

Scala

x.asInstanceOf[T] casts the object x to the type T. It throws an exception if the given object cannot be cast to type T.

1
2
3
4
5
6
7
8
9
val x: UInt = 3.U
// We can't cast UInt to Int
try {
println(x.asInstanceOf[Int])
} catch {
case e: java.lang.ClassCastException => println("As expected, we can't cast UInt to Int")
}
// But we can cast UInt to Data since UInt inherits from Data.
println(x.asInstanceOf[Data])

Chisel

Chisel has a set of type casting functions. The most general is asTypeOf(). Some chisel objects also define asUInt() and asSInt() as well as some others.

1
2
3
4
5
6
7
class TypeConvertDemo extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(SInt(4.W))
})
io.out := io.in.asTypeOf(io.out)
}

Match

Type Match

1
2
3
4
5
6
7
8
9
10
class ConstantSum(in1: Data, in2: Data) extends Module {
val io = IO(new Bundle {
val out = Output(chiselTypeOf(in1)) // in case in1 is literal then just get its type
})
(in1, in2) match {
case (x: UInt, y: UInt) => io.out := x + y
case (x: SInt, y: SInt) => io.out := x + y
case _ => throw new Exception("I give up!")
}
}

Value Match

It is good to remember that Chisel types generally should not be value matched. Scala’s match executes during circuit elaboration, but what you probably want is a post-elaboration comparison. The following gives a syntax error:

1
2
3
4
5
6
7
8
9
10
11
class InputIsZero extends Module {
val io = IO(new Bundle {
val in = Input(UInt(16.W))
val out = Output(Bool())
})
io.out := (io.in match {
// note that case 0.U is an error
case (0.U) => true.B
case _ => false.B
})
}

Unapply

value matching with case classes

1
2
3
4
5
6
case class Something(a: String, b: Int)
val a = Something("A", 3)
a match {
case Something("A", value) => value
case Something(str, 3) => 0
}

Scala unapply methods are another form of syntactic sugar that give match statements the ability to both match on types and extract values from those types during the matching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case class SomeGeneratorParameters(
someWidth: Int,
someOtherWidth: Int = 10,
pipelineMe: Boolean = false
) {
require(someWidth >= 0)
require(someOtherWidth >= 0)
val totalWidth = someWidth + someOtherWidth
}

def delay(p: SomeGeneratorParameters): Int = p match {
case sg @ SomeGeneratorParameters(_, _, true) => sg.totalWidth * 3
case SomeGeneratorParameters(_, sw, false) => sw * 2
case sg @ SomeGeneratorParameters(_, _, true) if sg.pipelineMe => sg.totalWidth * 3
}

All these syntaxes are enabled by a Scala unapply method contained in a class’s companion object. If you want to unapply a class but do not want to make it a case class, you can manually implement the unapply method. The following example demonstrates how one can manually implement a class’s apply and unapply methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Boat(val name: String, val length: Int)
object Boat {
def unapply(b: Boat): Option[(String, Int)] = Some((b.name, b.length))
def apply(name: String, length: Int): Boat = new Boat(name, length)
}

def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter { b =>
b match {
case Boat(_, length) if length < 60 => true
case Boat(_, _) => false
}
}

val boats = Seq(Boat("Santa Maria", 62), Boat("Pinta", 56), Boat("Nina", 50))
println(getSmallBoats(boats).map(_.name).mkString(" and ") + " are small boats!")

Partial Functions

为行为模型添加延迟的方法

结论:

  • always 块内部的语句中添加任何延迟都不能准确仿真真实硬件的行为。
    • 唯一的例外是非阻塞赋值 LHS delay, 这能准确仿真传输延迟(Transport delay model),但通常会牺牲模拟器性能。
  • 在连续赋值中添加延迟,或通过无延迟 always 块的输出驱动带延迟的连续赋值,都能准确仿真惯性延迟(Inertial delay model)

1 延迟模型

  • 惯性延迟模型 Inertial delay model

    • 仅在输入保持稳定的时间大于模型延迟时,将信号传播到输出。
    • 若两次输入变化之间的时间短于过程赋值延迟、连续赋值延迟或门延迟,则先前的输出事件会被覆盖。
  • 传输延迟模型 Transport delay model

    • 在任何输入信号发生变化后,都将按顺序传传播到输出端。
  • 剔除和误差模型 Reject & Error delay model

    • 传播大于误差设置的信号,传播介于剔除和误差设置之间信号的未知值,不传播低于剔除设置的信号。

    • 剔除和误差的设置:

2 阻塞赋值的延迟模型

image-20230730095706100
  • LHS delay:输入第一次变化后,经过 #delay 延迟后输出,且输出时采用 #delay 期间最后更新的输入值。
  • RHS delay:输入第一次变化后,经过 #delay 延迟后输出,期间无视输入的变化。

阻塞赋值 LHS delay

  • 缺陷1:若在 #delay 期间输入再次发生改变,输出的值由后续改变的输入决定,但输出仍会根据初次改变的时间延迟输出。
image-20230730100520219 image-20230730100213917
  • 缺陷2:输入第一次变化,结果赋值到临时值 tmp ,在 #delay 期间输入再次发生改变,输出仍会根据旧的临时值 tmp 输出错误的结果。
image-20230730100552661

Guideline

  • RTL:不要将延迟放在阻塞赋值的 LHS 侧来建模组合逻辑。
  • Testbench:可以将延迟放在阻塞赋值的 LHS 侧。

阻塞赋值 RHS delay

image-20230730101248351 image-20230730101936223 image-20230730101945733
  • 缺陷:输入第一次变化后,经过 #delay 后输出,期间无视输入的变化。

Guideline

  • RTL:不要将延迟放在阻塞赋值的 RHS 侧来建模组合逻辑。
  • Testbench:不要将延迟放在阻塞赋值的 RHS 侧。

3 非阻塞赋值的延迟模型

image-20230730102144370

非阻塞赋值 LHS delay

  • 缺陷:(与阻塞赋值 LHS delay 相同)若在 #delay 期间输入再次发生改变,输出的值由后续改变的输入决定,但输出仍会根据初次改变的时间延迟输出。
image-20230730102332209

Guideline

  • RTL:不要将延迟放在非阻塞赋值的 LHS 侧来建模组合逻辑。
  • Testbench:非阻塞赋值仿真效率更低,因此一般不建议将延迟放在非阻塞赋值的 LHS 侧。

非阻塞赋值 RHS delay(传输延迟)

  • 能准确仿真具有传输延迟(Transport delay model)的组合逻辑。
image-20230730103703322 image-20230730103652203

Guideline

  • RTL:
    • 建议在非阻塞赋值的 RHS 添加延迟以模拟行为线延迟逻辑。(?)
    • 只有在模拟传输延迟行为时,才这么做,它会导致仿真速度变慢。
  • Testbench:当必须在未来时钟边沿或设定延迟后调度激励时,通常会用这种编码方式,同时不会阻碍同一程序块中后续激励事件的分配。(?)

多个非阻塞赋值 RHS delay

image-20230730111015601
  • 缺陷:所有 RHS 都必须在灵敏度列表中,包括中间值。
    • #delay 更新 tmp 后,由于 tmp 也在灵敏度列表中,将再次触发 always 块,用正确的值更新 {co, sum}

Guideline

  • RTL:
    • 一般情况下,不要在非阻塞赋值的 RHS 添加延迟来模拟组合逻辑。
    • 在非阻塞赋值的 RHS 添加延迟来模拟时序逻辑的时钟到输出行为,是常见的做法。
  • Testbench:一般不建议

4 连续赋值的延迟模型

image-20230730111550149

连续赋值 LHS delay(惯性延迟)

  • 可以准确仿真具有惯性延迟(Inertial delay model)的组合逻辑。
image-20230730112609339 image-20230730112623580

混合使用

image-20230730113012636 image-20230730113021690

Guideline

  • RTL:
    • 连续赋值 LHS delay可以仿真具有惯性延迟的组合逻辑。
    • 使用无延迟 always 块建模复杂的组合逻辑,而将其输出驱动到带延迟连续赋值,可以仿真具有惯性延迟的组合逻辑。
  • Testbench:连续赋值可用于任何位置。

AMBA APB

1 APB 协议

  • APB 接口是无流水线同步协议,每次传输至少需要两个周期
  • APB 接口用于访问外设的控制寄存器。
  • APB 传输由 APB 桥/请求方(Requester)初始化,由 APB 外设/完成方(Completer)响应请求。

2 APB 接口信号

  • PCLK
  • PRESETn
  • Requester
    • PADDR
    • PSELx
    • PENABLE
    • PWRITE
    • PWDATA
    • PSTRB
    • PPROT PNSE
    • PWAKEUP
    • PWUSER
  • Completer
    • PREADY
    • PRDATA
    • PSLVERR
    • PRUSER
    • PBUSER

地址总线

  • APB 接口有一条地址总线 PADDR
  • 允许 PADDR 与数据宽度不对齐,但结果无法预测。(?)

数据总线

  • APB 接口有两条独立的数据总线 PRDATAPWDATA
  • 数据总线宽度可以是 8 位、16 位、32 位,且读写总线的宽度必须相同。
  • 数据传输不能同时进行,因为读/写没有独立的握手信号。

3 APB 传输

写传输

无等待状态

image-20230727101959667
  • Setup(T1):PSEL 断言,这表示 PADDR PWRITE PWDATA 是有效的。
  • Access(T2):PENABLE 断言,PREADY 在上升沿断言,这表示将在 T3 处接收数据。
  • 传输结束时, PSELPENABLE 取消断言。

有等待状态

image-20230727111042572
  • 在 Access 阶段,PENABLE 为高电平,Completer 将 PREADY 设为低电平,以延长传输。
  • 期间,下列信号保持不变:
    • PADDR PWRITE PSELx PENABLE PWDATA PSTRB PPROT PAUSER PWUSER

写选通

image-20230727112124939
  • PSTRB 表示写数据总线的相应通道包含有效数据。
  • 对于读传输,必须将 PSTRB 所有位置低。

PSTRB

  • PSTRB 是一个可选信号。
image-20230727112618422

读传输

无等待状态

image-20230727112757276
  • 与写传输相同。

有等待状态

image-20230727112840501
  • 与写传输相同。
  • 期间,下列信号保持不变:
    • PADDR PWRITE PSELx PENABLE PPROT PAUSER

错误响应

  • PSLVERR 指示错误信息。
  • PSLVERR 仅在传输的最后一个周期有效,此时 PSEL PENABLE PREADY 都为高电平。
  • 建议但不要求在 PSEL PENABLE PREADY 都为低电平时将PSLVERR 设为低电平(清除 flag)。
  • 外设收到错误信号不一定就会改变外设的状态,这与外设本身有关。
  • 写入事务时出现错误,但外设中的寄存器可能已经更新。
  • 读取事务出错时会返回无效数据(不要求在这种情况下全设为 0 ),该数据仍可使用。
  • Completer 不要求支持 PSLVERR 。若 Completer 不支持 PSLVERR ,则 Requester 的相应输入端置低。

写错误

image-20230727114203989

读错误

image-20230727114257626

PSLVERR 的映射

  • 从 AXI 到 APB:读取时出错,映射回 RRESP ;写入时出错,映射回 BRESP
  • 从 AHB 到 APB:映射回 HRESP 进行读写。

保护

4 状态机

image-20230729100123257
  • IDLE
  • SETUP
    • 需要传输时,进入 SETUP ,PSELx 被断言,期间一些信号不得发生变化。
    • 在下一个周期进入 ACCESS 。
  • ACCESS
    • PENABLE 被断言,期间一些信号不得发生变化。
    • 若 Completer 将 PREADY 置低,则保持 ACCESS 。
    • 若 Completer 将 PREADY 置高,则退出 ACCESS ;若不再需要传输,进入 IDLE ,若还需要传输,进入 SETUP 。

5 接口奇偶校验保护

Full & Parallel Case

1 case

1
2
3
4
5
6
case/x/z (expression)	// synopsys full_case, parallel_case
item1: item1_statement;
item2: item2_statement;
...
default: default_statement;
endcase
  • case header
    • casez
      • z ? 视作不关心
      • Guideline:谨慎使用
      • Coding Style:用 ? 而非 z 匹配
    • casex
      • z ? x 视作不关心
      • Guideline:不要使用
  • case expression
    • 可以是常量,或求值为常量的表达式
  • case item
    • 可以是表达式
    • 具有隐含的 break

2 Full Case

  • “full”指 case expression 中所有可能的二进制 pattern 都能与 item/default 匹配。

  • synopsys case report

    • 完整

      image-20230730120712405
    • 不完整

      image-20230730120728115
    • 用户指定了 full_case

      image-20230730120751547
  • 对于 HDL 的“full”

    • case expression 中所有可能的二进制(0 1)、非二进制(z x)和混合 pattern 都能与 item/default 匹配。
  • 对于综合的“full”

    • case expression 中所有可能的二进制 pattern 都能与 item/default 匹配。
  • 仿真时 sel == 2'b11y = 1'bx ,而综合将 sel == 2'b11 情况视作不关心,从而导致不匹配。可将该情况的输出 y 赋值为常数或与其他 item 相同。

image-20230730121955371

1 连续赋值

1.1 延时

  • #(上升沿延时 1,下降沿延时 0,关闭延时 Z,输出变成 X 延时)
1
assign #(a, b, c, d) x = y;
  • #(Min: Typ: Max)
1
assign #(1: 2: 3) x = y;
  • 惯性延时:任何小于其延时的信号变化脉冲将被滤除掉,不体现在输出端口上。

1.2 多驱动源线网

多重驱动

1
2
assign x = y;
assign x = z; // Error

线与、线或

  • 线与 wand
1
2
3
wand x;
assign x = y;
assign x = z;
  • 线或 wor
1
2
3
wor x;
assign x = y;
assign x = z;
  • 实际综合仍为与门、或门。
  • 延时在综合时会被忽略。

三态总线

1
2
3
tri x;
assign x = a ? 1'bz : b;
assign x = c ? 1'bz : d;

2 过程赋值

注:定义为 reg 不一定是寄存器

2.1 语句组

  • begin - end

  • fork - join

  • 语句组标识符

    当一个语句组有标识符时,在语句组内部可以定义局部变量,而不会传递到语句组外部。它的值在整个仿真周期中是不变的,但是不会与另一个语句组中的同名变量冲突。

    1
    2
    3
    4
    5
    always @ (...) begin: sort
    integer i;
    for (i = -; i < 7; i = i + 1)
    ...
    end

2.2 条件

if - else

  • 优先级

    可根据关键路径设计优先级编码,提高性能。

  • 锁存器

    条件不全,可能产生锁存器。

  • 寄存器

    在描述时序逻辑时,常用 if 语句的隐式条件对带时钟、使能的 D 触发器建模。在这里综合器会将其总合成一个带时钟使能的寄存器。

    1
    2
    3
    4
    5
    6
    7
    always @ (posedge clk or negedge rst_n) begin
    if(~rst_n)
    q <= 0;
    else if (en)
    q <= a + b;
    // ? else
    end

case

  • 所有分支具有一样的优先级

  • 条件不全可能产生锁存器

  • casez

    将条件中的 z 看做不关心的值。

    1
    2
    casez (x)
    4'b1??? : ...
  • casex

    将条件中的 z & x 看做不关心的值。

    1
    2
    casex (x)
    4'b1xxxx : ...

2.3 循环

forever

1
2
3
4
initial begin
clk = 0;
forever #5 clk = ~clk;
end

repeat

1
2
3
4
if (...)
repeat (8) begin
data = {data[6:0], data[7]};
end

while

1
2
3
while(...) begin
...
end

for

1
2
3
4
5
6
reg[3:0] mem[8:0];
always @ (...) begin : init
integer i;
for (i = 0; i < 8; i = i + 1)
mem[i] = 4'b0;
end

3 结构化描述

3.1 实例化模块

端口

  • 模块内部
    • input 默认是线网。
    • output 是线网或者寄存器。
    • inout 默认是线网,一般定义为 tri 。
  • 实例化模块
    • input 由线网或寄存器驱动。
    • output 驱动线网。
    • inout 输入时由线网驱动,输出时驱动线网。

实例化

1
2
3
4
5
6
7
adder u_adder (
.in1 (in1),
.in2 (in2),
.cin () // 无对应信号,留空
.out (out),
);
adder u_adder (in1, in2, /* 无对应信号,留空 */ , out);

参数化模块

  • 实例化时带入

    所有参数都要按照顺序列出,不能颠倒顺序或有遗漏。(?)

1
2
3
4
5
6
7
8
9
adder #(
2, // INPUT_WIDTH
3 // OUTPUT_WIDTH
) u_adder (
.in1 (in1),
.in2 (in2),
.cin ()
.out (out),
)
  • defparam

    未重新定义的参数保留原模块缺省值。

1
2
3
4
5
6
7
8
9
adder u_adder (
.in1 (in1),
.in2 (in2),
.cin ()
.out (out),
);
defparam
u_adder.INPUT_WIDTH = 2,
u_adder.OUTPUT_WIDTH = 3;

RTL 级的基本要素和设计步骤

  • ds

非阻塞赋值、阻塞赋值、连续赋值

  • 时序逻辑:always 为边沿敏感,使用 <=

  • 组合逻辑:always 为电平敏感,使用 =

  • 组合逻辑:assign 使用 =

    1
    2
    assign x1 = x + 1;
    assign x = x + 1; // 组合逻辑环

寄存器电路建模

  • 寄存器信号声明

    定义为 reg 不一定是寄存器,还需要 always 为边沿敏感。

  • 时钟

    1
    always @ (posedge clk)
  • 异步复位、置位

    1
    2
    3
    4
    5
    always @ (posedge clk or negedge rst)
    if (rst)
    ...
    else
    ...
  • 同步复位、置位

    1
    2
    3
    4
    5
    always @ (posedge clk)
    if (rst)
    ...
    else
    ...
  • **同时使用时钟的 posedgenegedge **

    相当于使用了倍频时钟,但是 PLD 内的 PLL/DLL 往往只能对一个时钟边沿保证很好的指标,因此不推荐使用。

组合逻辑建模

  • always
  • assign

双向端口和三态信号建模

建议仅在顶层模块定义双向总线和例化的三态信号,禁止在除顶层外的其他层次赋值高阻态 Z ,而是在顶层将双向信号分为输入信号和输出信号,然后根据需要分别传递到不同的子模块中。

1
2
3
4
inout[7:0] data_bus;
wire [7:0] data_in, data_out;
assign data_in = data_bus;
assign data_bus = sel ? data_out : 8'bz;
  • assign

    1
    2
    3
    assign data_bus =   sel1 ? data_out1 :
    sel2 ? data_out2 :
    8'bz;
  • case

    1
    2
    3
    4
    5
    6
    7
    8
    reg data_out;
    always @ (data_out1 or data_out2)
    case ({data_out1, data_out2})
    2'b10 : data_out = data_out1;
    2'b01 : data_out = data_out2;
    default : 8'bz;
    endcase
    assign data_bus = data_out; // data_bus 是 wire 或 tri ,不能在 always 块中被赋值

MUX 建模

  • ? :

  • case if - else

    根据综合工具不同,可能综合出优先级或无优先级 MUX 。一般情况下综合工具会优化成无优先级结构。

存储器建模

  • reg [REG_WIDTH - 1 : 0] mem [MEM_WIDTH - 1 : 0]
  • 调用 IP

时钟分频

  • 偶数分频

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    reg [2:0] cnt;
    always @ (posedge clk_200k or negedge rst_n) begin
    if (!rst_n)
    cnt <= 0;
    else
    cnt <= cnt + 1;
    end
    assign clk_100k = cnt[0];
    assign clk_50k = cnt[1];
    assign clk_25k = cnt[2];
  • 奇数分频

    ds

串并转换建模

ds

复位

同步复位

  • always 敏感表中仅有时钟边沿。
1
2
3
always @ (posedge clk)
if (!rst_n) ...
else ...
  • 如果目标器件有同步复位端口,可直接调用。如果没有同步复位端口,则与输入信号组成某种组合逻辑达成同步复位效果。

  • 同步复位优点

    • 利于时序分析,综合结果可达到更高的频率。
    • 仅在时钟沿生效,可避免因复位电路毛刺造成的亚稳态和错误。(复位组合电路产生的毛刺很难被时钟沿采集到)
  • 同步复位缺点

    • 必须保证复位信号足够长(至少要大于设计的最长时钟周期 + 同步复位信号穿过的组合逻辑路径延时 + 时钟偏移),才能保证所有时钟的有效沿都能采集到复位信号。

      1
      T_syn_rst > PeriodMax + (t1 + t2) + (clk2 - clk1)
    • 有的触发器没有同步复位端口,会增加逻辑资源消耗。

异步复位

  • always 敏感表中包含复位信号边沿。
1
2
3
always @ (posedge clk or negedge rst_n)
if (!rst_n) ...
else ...
  • 大多数触发器都有异步复位端口。
  • 异步复位优点
    • 设计简单
    • 节约逻辑资源
    • FPGA GSR
  • 异步复位缺点
    • 异步复位释放时间若与时钟有效沿接近,易造成触发器输出为亚稳态,造成逻辑错误。
    • 如果异步复位逻辑树的组合逻辑产生了毛刺,则会使触发器误复位,造成逻辑错误。

异步复位,同步释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module top_module( 
input clk, reset_n,
output rst_n
);
reg rst_nr1, rst_nr2;

always @(posedge clk or negedge reset_n) begin
if(!reset_n) begin
{rst_nr2, rst_nr1} <= 2'b00;
end else begin
{rst_nr2, rst_nr1} <= {rst_nr1, 1'b1};
end
end
assign rst_n = rst_nr2;
endmodule
1
2
3
4
5
reset_n = 1		->		rst_nr1 = 1, rst = 1
reset_n = 0 -> rst_nr1 = 0, rst = 0 // 异步复位
reset_n = 1 -> rst_nr1 = 0, rst = 0 // 保持
posedge clk -> rst_nr1 = 1, rst = 0 // 两级:防止第一级 reset_n 释放时间与 clk 有效沿接近,造成亚稳态
posedge clk -> rst_nr1 = 1, rst = 1 // 同步释放(2个周期)

Single Period

  • R: add, slt, sltu
  • I: ori, lw
  • U: lui
  • S: sw
  • B: beq
  • J: jal

数据通路

立即数扩展器

1
assign immI = 

ALU

ALUctr 与 func3 存在较强关系,对 ALUctr 编码时可根据 func3 字段进行优化。

1
assign SUBctr = 

IFU

R 型指令数据通路

I 型指令数据通路

U 型指令数据通路

Load/Store 型指令数据通路

B 型指令数据通路

J 型指令数据通路

完整数据通路

控制器