目录
[2.1 硬件设计一些重要概念](#2.1 硬件设计一些重要概念)
[2.2 功能性仿真](#2.2 功能性仿真)
[2.3 简单的Verilog代码例子(4-bit的加法器)](#2.3 简单的Verilog代码例子(4-bit的加法器))
[3.1 Chisel基本概念](#3.1 Chisel基本概念)
[3.2 Chisel代码展示](#3.2 Chisel代码展示)
[3.3 Chisel转成Verilog代码](#3.3 Chisel转成Verilog代码)
[4.1 Scala基本概念](#4.1 Scala基本概念)
[4.2 变量和常量- var 和 val](#4.2 变量和常量- var 和 val)
[4.3 条件语句](#4.3 条件语句)
[4.3.1 用于根据条件执行不同的代码块](#4.3.1 用于根据条件执行不同的代码块)
[4.3.2 else if多条件分支](#4.3.2 else if多条件分支)
[4.3.3 if语句的返回值](#4.3.3 if语句的返回值)
[4.4 方法(函数)](#4.4 方法(函数))
[4.4.1 方法(函数)定义](#4.4.1 方法(函数)定义)
[4.4.2 函数重载](#4.4.2 函数重载)
[4.4.3 递归与嵌套函数](#4.4.3 递归与嵌套函数)
[4.4.4 命名参数与默认值](#4.4.4 命名参数与默认值)
[4.5 列表](#4.5 列表)
[4.6 for循环语句](#4.6 for循环语句)
[4.7 代码块](#4.7 代码块)
[4.7.1 基本概念](#4.7.1 基本概念)
[4.7.2 参数化代码块](#4.7.2 参数化代码块)
[4.7.3 简单函数定义](#4.7.3 简单函数定义)
[4.7.4 类定义](#4.7.4 类定义)
[4.7.5 使用map方法](#4.7.5 使用map方法)
[4.8 包和包的引入](#4.8 包和包的引入)
[4.8.1 定义包和类](#4.8.1 定义包和类)
[4.8.2 导入包中的类](#4.8.2 导入包中的类)
[4.8.3 包名的约定](#4.8.3 包名的约定)
[4.8.4 Chisel的常见导入](#4.8.4 Chisel的常见导入)
[4.9 Scala的面向对象](#4.9 Scala的面向对象)
[4.9.1 Scala的面向对象特性](#4.9.1 Scala的面向对象特性)
[4.9.2 类的定义](#4.9.2 类的定义)
[4.9.3 类的实例化](#4.9.3 类的实例化)
[示例:WrapCounter 类](#示例:WrapCounter 类)
一、芯片前端设计开发背景知识·
在一个完整的芯片从确定需求到制作完成,其中包含了许多的流程, 可以大致分为设计和制
造两大部分,其中设计通常是由开发芯片的公司完成,确定芯片的各种性能参数和规格,设计完成
后,将芯片的详细设计图纸送往芯片代工厂,例如中芯国际、台积电等,由他们完成芯片制造这部
分的工作。 芯片设计可以理解成一栋大楼的设计师,设计师绘制大楼的建筑图纸,确保这栋大楼
结构的稳固和功能的完备。而芯片制造则是负责真正一砖一瓦的将这栋大楼建造完成的工人。
芯片的设计部分又可以细分为前端设计(也称逻辑设计)和后端设计(也称物理设计)。
前端设计主要关注的是芯片如何实现需要的功能, 怎样达到性能要求,而后端设计主要关注的是
如何保证前端设计出来的电路在现实中能够制造出来并正常运行。 在本章节中,主要学习了解
的是芯片前端设计的流程,而后端设计流程会在后续章节介绍。
芯片前端设计的主要流程:
在芯片设计流程中,从需求分析到最终制造芯片,整个过程包括:
-
需求分析与架构设计:明确芯片的功能、性能和可行性要求,设计相关的架构,按功能划分模块,确定各部件规格,形成设计文档。
-
框架搭建:搭建整体项目的框架,搭建开发环境,配置项目构建工具,为后续模块的开发及验证做准备。
-
模块实现:按照设计文档,将整体功能划分为多个模块,再将多个模块划分为更细的各个子模块,逐一实现,并使用硬件描述语言(HDL)编写RTL代码。
-
单元测试:对每个模块进行单独的测试,验证其功能正确性,以快速定位并修复错误,提高开发效率。
-
整体仿真验证:将所有模块整合为整体系统进行仿真测试,验证系统功能的正确性和性能。
-
FPGA验证:使用FPGA进行硬件验证,检查设计的正确性和性能,特别是通信协议和复位信号等细节。
-
时序分析与后端设计:检查电路是否满足设计指标,将RTL代码转换为代工厂可接受的制造文件,进行物理布局和布线等设计。
二、Verilog介绍
Verilog是一种专门用于设计和模拟数字电路的硬件描述语言(Hardware Description Language, HDL)。Verilog强大之处在于它的多抽象层次建模能力,支持系统级到门级的各种设计。它允许色设计者以并行的方式思考问题,同时描述多个硬件组成的协同工作。Verilog代码可以被综合成实际的硬件电路,也可以用于仿真验证设计的正确性。
2.1 硬件设计一些重要概念
硬件设计和软件设计有很多不一样的地方,编写 HDL 代码需要考虑很多的硬件相关因素,作
为 HDL 代码编写人员同样需要了解一些硬件设计中的重要概念:
-
时序:硬件设计中需要考虑信号的时序,即信号在电路中传播的时间。这与软件设计中的执行时间不同,因为硬件电路是并行工作的,而软件通常是顺序执行的。这就需要设计者在设计时考虑信号的同步和延迟,确保电路在时钟边沿正确工作;
-
并行性:硬件电路天然是并行运行的,多个模块可以同时处理数据。这与软件的串行执行有本质的不同。在硬件设计中,设计者需要充分利用并行性来提高电路的性能;
-
资源限制:硬件设计需要考虑物理资源的限制,例如逻辑门的数量、布线的长度和面积等。这些限制决定了设计的复杂度和实现的可能性。在软件设计中,我们关注的是内存和计算资源的使用效率。
2.2 功能性仿真
在完成Verilog设计之后,我们进行功能性仿真,验证我们设计的行为是否正确。
功能性仿真时在不考虑时序的情况下验证设计的逻辑功能。主要用于验证Verilog代码行为是否正确,确保设计逻辑在不同的输入条件下都能按照预期工作。Verilog 提供了一些可综合的语法以及不可综合的语法,例如,initial 块和 task 函数是不可综合的语法,这些语法主要用于仿真测试,而不会被综合工具转化为硬件逻辑。
为了进行功能性仿真,我们需要编写一个测试平台(testbench),它的主要作用是为设计提供输入激励(stimulus),并监视设计的输出结果。一个典型的测试平台包括以下几个部分:信号声明、实例化待测模块(Device Under Test, DUT)、生成输入激励、监控输出结果及仿真控制。
2.3 简单的Verilog代码例子(4-bit的加法器)
主要包括三个部分的内容:
1)模块声明:使用 module 关键字定义模块,模块名为 adder4;
2)端口声明:定义模块的输入和输出端口,包括两个 4 位输入 A 和 B、一个进位输入 Cin、
一个 4 位的和输出 Sum 以及一个进位输出 Cout,位于 input / output 后面的则是这个端口
的硬件类型,这里定义的是 wire 类型;
3)逻辑描述:使用 assign 语句将输入 A、B 和 Cin 的和赋值给 {Cout, Sum}。
Scala
// 4-bit Adder Module
module adder4 (
input wire [3:0] A, // 4-bit input A
input wire [3:0] B, // 4-bit input B
input wire Cin, // Carry-in
output wire [3:0] Sum, // 4-bit Sum output
output wire Cout // Carry-out
);
assign {Cout, Sum} = A + B + Cin;
endmodule
以下则是一个简单的功能性仿真测试平台(testbench)的示例,用于验证一个 4 位加法器模块:
- 模块例化: 模块实例化是指在测试平台或其他模块中创建并连接一个模块的过程。通过实例化,可以在不同的上下文中重复使用模块。以下是一个实例化4位加法器(
adder4
)的示例,其中uut
代表"Unit Under Test"(被测试单元)。
Scala
adder4 uut (
.A(A),
.B(B),
.Cin(Cin),
.Sum(Sum),
.Cout(Cout)
);
- initial块:
initial
块是 Verilog 中的一种语句块,用于在仿真开始时执行一次。在测试平台中,initial
块通常用于设置初始条件、应用测试向量以及控制仿真结束。
Scala
initial begin
// 初始化测试条件
// 应用测试向量
// ...
// 仿真结束条件
#100 $finish; // 假设仿真在100个时间单位后结束
end
- **延迟:**在 Verilog 中,延迟语句用于控制仿真时间的推进。通过延迟语句,可以模拟信号随时间的变化。
Scala
#10; // 延迟10个时间单位
- **display 系统任务:** `display
是 Verilog 中的系统任务,用于在仿真控制台输出信息。它的功能类似于 C 语言中的
printf`。
Scala
$display("A=%b B=%b Cin=%b | Sum=%b Cout=%b", A, B, Cin, Sum, Cout);
Scala
1.// Testbench for 4-bit Adder
2. module tb_adder4;
3.
4. reg [3:0] A; // 4-bit input A
5. reg [3:0] B; // 4-bit input B
6. reg Cin; // Carry-in
7. wire [3:0] Sum; // 4-bit Sum output
8. wire Cout; // Carry-out
9.
10. // Instantiate the adder4 module
11. adder4 uut (
12. .A(A),
13. .B(B),
14. .Cin(Cin),
15. .Sum(Sum),
16. .Cout(Cout)
17. );
18.
19. initial begin
20. // Test cases
21. A = 4'b0001; B = 4'b0010; Cin = 0;
22. #10; // Wait for 10 time units
23. $display("A=%b B=%b Cin=%b | Sum=%b Cout=%b", A, B, Cin, Sum, Cout);
24.
25. A = 4'b0101; B = 4'b0110; Cin = 1;
26. #10;
27. $display("A=%b B=%b Cin=%b | Sum=%b Cout=%b", A, B, Cin, Sum, Cout);
28.
29. A = 4'b1111; B = 4'b0001; Cin = 0;
30. #10;
31. $display("A=%b B=%b Cin=%b | Sum=%b Cout=%b", A, B, Cin, Sum, Cout);
32.
33. $finish; // End simulation
34. end
35.
36. endmodule
三、Chisel简介
3.1 Chisel基本概念
Chisel 是一种由 UC Berkeley 在2012年推出的开源硬件描述语言(HDL),专为数字电路设计而生,尤其在集成电路和FPGA设计领域广受欢迎。它基于Scala编程语言,融合了高级编程语言的抽象能力和硬件描述语言的精确性,为设计者提供了高效、灵活且现代的硬件描述方式。
Chisel的设计理念强调模块化和可重用性,利用Scala的函数式编程、对象继承等特性,设计者能够创建参数化的硬件模块,这些模块在不同设计中可重复使用,提高了设计效率并减少了代码冗余和错误。其强大的类型系统和编译时检查功能,帮助设计者在早期发现并修正错误,降低了调试成本。
相较于传统的硬件描述语言如Verilog,Chisel在抽象层次和设计方法上提供了显著改进。Verilog通常关注于具体电路细节,而Chisel则允许设计者在更高层次上思考和构建复杂系统,通过函数式编程和参数化模块,简化了设计过程。
Chisel编写的代码可通过其编译器转换为Verilog代码,无缝对接现有的硬件综合工具链和EDA工具,既保留了高级语言带来的便利和效率,又兼容了传统的硬件设计流程。这种双重优势使得Chisel在硬件设计领域备受青睐,成为提升设计效率、灵活性和可维护性的重要工具。
3.2 Chisel代码展示
基于上一小节的 Verilog 代码例子,在这里我们给出一个实现相同逻辑功能的 Chisel 版本的代码例子,当然从目前这个例子来说还不能展示 Chisel 的强大,在这里只是对 Chisel 的语法做一个简单直观的展现。
Scala
1. // Import Chisel library
2. import chisel3._
3.
4. class Adder4 extends Module {
5. // Define input and output ports
6. val io = IO(new Bundle {
7. val A = Input(UInt(4.W)) // 4-bit input A
8. val B = Input(UInt(4.W)) // 4-bit input B
9. val Cin = Input(Bool()) // Carry-in
10. val Sum = Output(UInt(4.W)) // 4-bit Sum output
11. val Cout = Output(Bool()) // Carry-out
12. })
13.
14. // Perform the addition and assign to outputs
15. val result = io.A +& io.B + io.Cin
16. io.Sum := result(3, 0) // Lower 4 bits
17. io.Cout := result(4) // Carry-out bit
18. }
3.3 Chisel转成Verilog代码
Chisel生成Verilog代码时,使用了特有的中间代码表示:FIRRTL,生成的FIRRTL代码再经过FIRRTL编译器(firtool)后生成Verilog。
四、Scala入门
4.1 Scala基本概念
Scala,作为Chisel的底层语言,其选择并非偶然。Scala融合了面向对象和函数式编程的精髓,为嵌入式DSL(领域特定语言)提供了理想的宿主环境。Chisel正是利用了Scala的这些特性,构建了一个既灵活又强大的硬件描述语言。
Scala的库系统丰富且高效,特别是其数据集合操作,为硬件设计中的复杂数据处理提供了强大支持。此外,Scala的严格类型系统确保了代码在编译阶段就能捕获大量潜在错误,从而提高了设计的可靠性和稳定性。
函数式编程的引入,使得Scala代码更加简洁、易于理解和维护。在Chisel中,这种优势被进一步放大,设计者能够以更抽象、更高级的方式思考和构建硬件系统,而无需深陷于繁琐的底层细节。
4.2 变量和常量- var 和 val
在Scala中,var
和 val
分别用于声明可变变量和不可变常量。尽管var
提供了灵活性,但在大多数情况下,推荐使用val
来声明变量,主要原因如下:
-
减少错误 :使用
val
可以减少在代码中错误地重复修改变量的可能性,这有助于避免引入难以追踪的bug。 -
提高可读性:代码的可读性增强,因为读者可以明确知道哪些值在程序的执行过程中保持不变。
-
鼓励函数式编程 :Scala鼓励函数式编程风格,其中不可变性是一个核心概念。通过大量使用
val
,可以更轻松地编写出无副作用、更纯粹的函数。 -
类型安全和优化:Scala的类型推断系统在处理不可变值时更加高效,因为它可以在编译时确保这些值不会被意外修改,从而优化代码的执行和内存使用。
此外,Scala的语法特性还包括:
-
省略分号:与Java和C不同,Scala的语句末尾通常不需要分号。Scala编译器会根据上下文自动推断语句的结束。
-
自动分号推断:当存在换行符且当前行的末尾是一个需要后续代码的运算符时,Scala编译器会自动推断出分号,允许语句跨越多行。
-
单行多条语句:如果需要在一行中编写多条语句,则必须使用分号显式分隔它们。
以下是对您提供的代码段的精简和整理:
Scala
// 使用var声明可变变量
var numberOfKittens = 6
// 使用val声明不可变常量
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false
// 可变变量可以被重新赋值
numberOfKittens += 1
// 尝试修改val声明的常量会导致编译错误
// kittensPerHouse = kittensPerHouse * 2 // This would not compile; kittensPerHouse is not updatable
// 可变变量done被重新赋值
done = true
4.3 条件语句
4.3.1 用于根据条件执行不同的代码块
Scala
// 一个简单的条件语句
if (numberOfKittens > kittensPerHouse) {
println("Too many kittens!!!")
}
// 当所有分支都是单行时,可以省略大括号,但Scala风格指南建议在包含else子句时保留大括号
// (尽管下面的写法可以编译,但通常不推荐)
if (numberOfKittens > kittensPerHouse)
println("Too many kittens!!!")
// if语句包含else子句
if (done)
println("we are done")
else
numberOfKittens += 1
4.3.2 else if多条件分支
Scala
// 使用else if和else
if (done) {
println("we are done")
}
else if (numberOfKittens < kittensPerHouse) {
println("more kittens!")
numberOfKittens += 1
}
else {
done = true
}
4.3.3 if语句的返回值
Scala中的if-else
语句的一个重要特性是它们可以返回一个值,这个值由所选择分支的最后一行代码决定。这个功能在函数和类的值初始化时非常有用。例如:
Scala
// 使用if-else语句返回一个值
val likelyCharactersSet = if (alphabet.length == 26)
"english"
else
"not english"
println(likelyCharactersSet)
在这个例子中,if-else
表达式直接作为val
的初始值,这展示了Scala表达式语言的强大和灵活性。如果alphabet.length
等于26,则likelyCharactersSet
将被赋值为"english"
;否则,将被赋值为"not english"
。这种特性使得Scala代码更加简洁和表达力强。
4.4 方法(函数)
4.4.1 方法(函数)定义
在Scala中,方法通过关键字def
来定义,也可以称之为函数。函数参数以逗号分隔的列表形式指定,可以包括参数名称、类型以及可选的默认值。为了代码清晰,通常建议指定函数的返回类型。
短的单行函数:对于比较短的单行函数可以直接设省略花括号。
Scala
def times2(x: Int): Int = 2 * x
多参数函数:可以包含多个参数,每个参数都有自己的类型。
Scala
def distance(x: Int, y: Int, returnPositive: Boolean): Int = {
val xy = x * y
if (returnPositive) xy.abs else -xy.abs
}
4.4.2 函数重载
Scala支持函数重载,即同一个函数名可以定义多个版本,每个版本都有不同的参数列表。这允许开发者根据传入的参数类型或数量来调用不同版本的函数。
Scala
def times2(x: Int): Int = 2 * x
def times2(x: String): Int = 2 * x.toInt
times2(5) // 调用第一个版本
times2("7") // 调用第二个版本
4.4.3 递归与嵌套函数
Scala支持递归和嵌套函数。递归函数调用自身以解决问题,而嵌套函数是在另一个函数内部定义的函数,只能在外部函数的作用域内访问。
Scala
def asciiTriangle(rows: Int) {
def printRow(columns: Int): Unit = println("X" * columns)
if(rows > 0) {
printRow(rows)
asciiTriangle(rows - 1) // 递归调用
}
}
asciiTriangle(6) // 调用asciiTriangle函数
4.4.4 命名参数与默认值
Scala允许为函数参数命名,并且在调用函数时可以按名称传递参数,这可以忽略参数的顺序。此外,可以为参数指定默认值,当调用函数时如果未提供这些参数,则使用默认值。
Scala
def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... }
// 调用时可以指定参数名,忽略顺序
myMethod(count = 10, wrap = false, wrapValue = 23)
myMethod(wrapValue = 23, wrap = false, count = 10)
// 仅传递需要非默认值的参数
myMethod(wrap = false, count = 10) // wrapValue使用默认值24
4.5 列表
Scala 实现了多种聚合或序列对象,其中列表(List)与数组非常相似,但支持更多的函数操作,如附加和提取元素等。
Scala
// 定义几个变量
val x = 7
val y = 14
// 定义一个列表
val list1 = List(1, 2, 3)
// 使用另一种方式组装列表
val list2 = x :: y :: y :: Nil // 使用 :: 操作符和 Nil 作为空列表
// 合并两个列表
val list3 = list1 ++ list2 // 将第二个列表附加到第一个列表
// 获取列表的长度
val m = list2.length
val s = list2.size // length 和 size 在列表中是等价的
// 访问列表的头部和尾部
val headOfList = list1.head // 获取列表的第一个元素
val restOfList = list1.tail // 获取去除第一个元素后的新列表
// 通过索引访问列表元素
val third = list1(2) // 获取列表的第三个元素(索引从0开始)
4.6 for循环语句
Scala 的 for
语句类似于传统的 for 语句,但提供了更丰富的功能和灵活性,用于遍历一系列的值或集合中的元素。
Scala
// 使用 to 遍历 0 到 7
for (i <- 0 to 7) { print(i + " ") }
println() // 输出: 0 1 2 3 4 5 6 7
// 使用 until 遍历 0 到 6
for (i <- 0 until 7) { print(i + " ") }
println() // 输出: 0 1 2 3 4 5 6
// 使用 by 按固定增量递增,打印 0 到 10 之间的偶数
for (i <- 0 to 10 by 2) { print(i + " ") }
println() // 输出: 0 2 4 6 8 10
// 遍历列表并求和
val randomList = List(scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt())
var listSum = 0
for (value <- randomList) {
listSum += value
}
println("sum is " + listSum)
Scala 的 for
循环功能强大,可以满足广泛的迭代需求。然而,对于某些特定操作(如数组或列表求和),使用集合推导(comprehensions)或其他函数式编程技巧可能更为直观和高效。集合推导适用于多种不同类型的元素集合,提供了一种更声明式的方式来处理数据。
4.7 代码块
4.7.1 基本概念
在Scala中,代码块由花括号{}
分隔,它可以包含零行或多行Scala代码。通常,代码块的最后一行(如果存在)会被视为该代码块的返回值,但在某些情况下(如赋值给Unit
类型的变量时),这个返回值可能会被忽略。一个没有任何内容的代码块将返回Unit
类型的值,这类似于其他语言中的void
或null
(但Unit
是一个具体的类型,而null
在Scala中通常与Option
类型或可空引用类型相关联)。
代码块在Scala中非常常见且用途广泛。它们构成了类定义的主体、函数和方法定义的主体、if
语句的子句,以及for
循环和其他Scala操作符的主体。
4.7.2 参数化代码块
虽然直接提及"参数化代码块"可能不是Scala术语中的标准用法,但我们可以理解为在函数或方法定义中,参数列表之后跟随的代码块(实际上是函数的体)可以视为接受这些参数的"代码块"。在类和方法的上下文中,这些参数与大多数传统编程语言中的参数类似。
4.7.3 简单函数定义
一个单行函数定义,不需要显式地使用花括号
Scala
def add1(c: Int): Int = c + 1
4.7.4 类定义
类RepeatString
接收一个字符串参数s
,并在其内部定义了一个字段repeatedString
,该字段是s
的重复拼接。
Scala
class RepeatString(s: String) {
val repeatedString = s + s
}
4.7.5 使用map
方法
在Scala中,列表(List
)的map
方法接受一个函数作为参数,该函数会对列表中的每个元素进行转换,并返回一个新的列表,其中包含转换后的元素。这里,map
方法的参数是一个匿名函数(也称为lambda表达式或闭包),它接收一个整型参数i
并返回其字符串表示。
Scala
val intList = List(1, 2, 3)
val stringList = intList.map { i =>
i.toString
}
注意:在这个map
方法的调用中,花括号{}
内的部分是一个匿名函数,它接收一个参数i
并返回i.toString
的结果。这个匿名函数是对列表intList
中每个元素进行操作的函数。Scala的语法允许在这种情况下省略花括号和箭头(=>
),使其更简洁,例如:intList.map(i => i.toString)
,甚至更简化为intList.map(_.toString)
,其中_
是Scala中的占位符语法,用于指代匿名函数的唯一参数。
4.8 包和包的引入
在Scala中,特别是在使用Chisel这类硬件构造语言时,定义包(package)和正确地导入包内的类、方法或对象是非常重要的。下面展示了如何定义一个包及其内部的类,并解释了如何导入这些类。
4.8.1 定义包和类
首先,你可以定义一个包(package)并在其中声明一个类。例如
Scala
package mytools
class Tool1 {
// ... 类的定义和代码
}
在这个例子中,mytools
是一个包名,而Tool1
是该包内定义的一个类。
4.8.2 导入包中的类
当你需要在另一个Scala文件中引用mytools
包中定义的Tool1
类时,你需要使用import
语句来导入它。这样做可以通知编译器你正在使用某个特定的库或包中的类、方法或对象。
Scala
import mytools.Tool1
这行代码导入了mytools
包中的Tool1
类,之后你就可以在当前的Scala文件中使用Tool1
类了。
4.8.3 包名的约定
- 目录层次结构:通常,包名应该与你的源代码文件所在的目录层次结构相匹配,但这不是强制性的。
- 命名约定:按照惯例,包名通常全部使用小写字母,并且不使用像下划线这样的分隔符。这样做是为了便于创建可读性强的描述性名称。
4.8.4 Chisel的常见导入
导入chisel3包中的所有类和方法:
使用下划线(_
)作为通配符,可以导入chisel3
包中的所有公开类和方法。
Scala
import chisel3._
从chisel3.iotesters包中导入特定的类:
如果你只需要从chisel3.iotesters
包中导入几个特定的类,你可以明确地列出它们。
Scala
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
4.9 Scala的面向对象
4.9.1 Scala的面向对象特性
Scala 是一种强大的面向对象编程语言,了解这一特性对于充分利用 Scala 和 Chisel 至关重要。Scala 的面向对象特性可以通过多种方式描述:
- 变量是对象 :在 Scala 中,变量(包括通过
val
声明的常量)实际上是对对象的引用。 - 字面值也是对象:即便是简单的字面值(如数字、字符串等)在 Scala 中也被视为对象。
- 函数是对象:Scala 中的函数可以像对象一样被传递和赋值,这一特性在函数式编程中非常有用。
- 对象与实例:在 Scala 中,类的实例在面向对象编程的上下文中通常被称为实例。
4.9.2 类的定义
定义类时,程序员需要指定:
- 与类关联的数据(通过
val
或var
声明)。 - 类的实例可以执行的操作,即方法或函数。
类可以扩展其他类,形成继承关系:
- 被扩展的类是超类(父类)。
- 扩展的类是子类。
- 子类继承超类的数据和方法,并可以添加自己的数据和方法或重写继承的方法。
Scala 还支持从特质(trait)继承,特质可以看作是轻量级的类,允许以特定、有限的方式从多个超类继承。
4.9.3 类的实例化
创建类的实例通常使用 new
关键字,例如:
Scala
val x = new WrapCounter(2)
但 Scala 提供了更灵活的语法,有时可以省略 new
关键字,直接通过伴生对象(Scala 的 object
的 apply
方法)来创建实例,如:
Scala
val y = WrapCounter(6)
这种语法依赖于伴生对象中的 apply
方法实现。
示例:WrapCounter 类
Scala
// WrapCounter 类,根据位宽计数到最大值
class WrapCounter(counterBits: Int) {
// 成员变量
val max: Long = (1 << counterBits) - 1 // 最大值
var counter = 0L // 计数器
// 方法
def inc(): Long = {
counter = counter + 1
if (counter > max) {
counter = 0
}
counter
}
// 注意:这里的 println 语句会在类实例化时立即执行
println(s"counter created with max value $max")
}
// 类的实例化
val x = new WrapCounter(2)
x.inc() // 增加计数器
// 访问实例的成员变量(除非它们被声明为 private)
if (x.counter == x.max) {
println("counter is about to wrap")
}
// Scala 允许省略点操作符(.)进行方法调用
x inc() // 这会使代码在某些场景下看起来更像自然语言
请注意,在 WrapCounter
类的定义中,println
语句会在每个 WrapCounter
实例被创建时立即执行,这通常不是期望的行为。在构造函数中执行这类操作更为合适,尽管 Scala 没有传统的构造函数语法,但可以通过定义不带返回类型的 def this(...)
方法或使用初始化代码块来实现。