十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从Verilog到Chisel:硬件描述语言进阶与高效开发环境搭建

从Verilog到Chisel:硬件描述语言进阶与高效开发环境搭建 1. 从Verilog到Chisel为什么我们需要新的硬件描述语言如果你和我一样是从Verilog或者VHDL开始接触数字电路设计的那么第一次看到ChiselConstructing Hardware In a Scala Embedded Language时心里多半会犯嘀咕Verilog不是用得好好的吗为什么还要学这个这玩意儿是不是又是个“玩具语言”只适合学术界离工业界太远我最初也是这么想的。直到几年前我参与了一个规模不小的SoC项目模块间的接口协议复杂参数化配置项多达几十个每次需求变更修改一个顶层参数就要手动去更新十几个模块的端口定义和实例化稍有不慎就漏掉一个仿真跑起来全是X态排查起来苦不堪言。那时我才真切地感受到用传统的HDL硬件描述语言做大规模、高可配置性的设计维护成本有多高。后来接触到基于Chisel的设计比如开源的RISC-V处理器项目看到他们用寥寥数百行代码就描述了一个高度可配置的CPU核并且能通过参数化生成针对不同场景高性能、低功耗、小面积的优化版本我才意识到这不仅仅是语法糖而是一种设计范式的转变。简单来说Chisel不是要取代RTL寄存器传输级设计而是要让你更高效、更安全地生成RTL。它是一门嵌入在Scala语言中的硬件构造语言。这意味着你写的是Scala代码但最终编译或者说“生成”出来的是标准的Verilog。你获得了Scala这个强大的高级编程语言的全部能力——面向对象、函数式编程、类型系统、丰富的集合库——来帮助你构建硬件。而Verilog更像是这个过程的“汇编输出”。所以学习Chisel的基本语法第一步是转变心态你不是在写一行行描述连线的代码而是在编写一个“生成硬件”的程序。理解了这一点再去看那些Bundle、Vec、when语句就会清晰很多。接下来我们就从最基础的构建块开始看看Chisel是如何用代码来“说话”的。2. 搭建你的第一个Chisel开发环境在开始写任何Chisel代码之前一个稳定、可复现的构建环境是重中之重。我见过不少初学者在环境配置上就打了退堂鼓问题五花八门Scala版本不对、SBTScala构建工具下载依赖超时、IDE插件不兼容等等。这里我会分享一个我个人验证过、最稳定且易于管理的方案使用sbt配合Mill作为备选并强烈推荐基于容器的开发环境来保证一致性。2.1 核心工具链选型为什么是SBTChisel官方构建工具是sbt。选择它不是因为它完美事实上它的学习曲线和初始速度常被吐槽而是因为整个Chisel生态系统包括Firrtl编译器、各种测试库都围绕它构建能确保最大的兼容性和最及时的支持。基础环境准备Java Development Kit (JDK)Chisel 3.x 需要 JDK 8, 11, 或 17。我推荐使用JDK 11 LTS版本它在稳定性和社区支持上取得了很好的平衡。你可以通过包管理器安装如apt install openjdk-11-jdk或者从Adoptium等网站下载。Scala这里有一个关键点你不需要单独安装Scalasbt会自动为你管理项目所需的Scala版本。在项目配置文件中指定即可这避免了全局Scala版本冲突的问题。sbt去其官网下载安装包或使用包管理器安装。安装后在终端输入sbt sbtVersion应该能显示版本号。2.2 项目目录结构与build.sbt解析一个标准的Chisel项目目录结构如下my_chisel_project/ ├── build.sbt # 项目构建定义核心文件 ├── src/ │ ├── main/ │ │ └── scala/ │ │ └── mypackage/ │ │ └── MyModule.scala # 你的Chisel代码 │ └── test/ │ └── scala/ │ └── mypackage/ │ └── MyModuleSpec.scala # 你的测试代码 └── project/ └── build.properties # 指定sbt版本让我们仔细看看build.sbt这个文件它是项目的“大脑”// 指定项目名称和版本 ThisBuild / scalaVersion : “2.13.10” // Chisel 3.6 通常使用Scala 2.13.x ThisBuild / version : “0.1.0” ThisBuild / organization : “com.example” // 添加Chisel编译器插件必须 addCompilerPlugin(“edu.berkeley.cs” % “chisel3-plugin” % “3.6.0” cross CrossVersion.full) // 声明库依赖 libraryDependencies “edu.berkeley.cs” %% “chisel3” % “3.6.0” // 推荐添加测试框架依赖 libraryDependencies “edu.berkeley.cs” %% “chiseltest” % “0.6.0” % “test”关键解读与避坑指南addCompilerPlugin这一行至关重要。Chisel为了提供诸如Bundle中自动生成io端口等便利特性需要使用Scala的宏Macro和编译器插件。漏掉这一行代码编译时会报各种奇怪的错误例如“not found: value chisel3”或者“Auto-clonetype failure”。版本对齐确保chisel3-plugin和chisel3的版本号严格一致。混合使用不同版本是常见错误来源。%%与%%%意味着sbt会自动为你加上当前Scala版本的后续如_2.13而%则不会。对于Chisel主库我们通常用%%。2.3 终极省心方案使用Dev Container (VSCode Docker)这是我目前最推荐的方式尤其对于团队协作或新手。它通过一个配置文件定义整个开发环境包括JDK、sbt、常用工具任何克隆项目的人都能在几分钟内获得一个完全一致、开箱即用的环境彻底告别“在我机器上是好的”这类问题。在项目根目录创建.devcontainer文件夹。在其中创建devcontainer.json文件{ “name”: “Chisel Development”, “image”: “mcr.microsoft.com/devcontainers/java:1-11-bullseye”, “features”: { “ghcr.io/devcontainers/features/scala:1”: {} }, “customizations”: { “vscode”: { “extensions”: [ “scalameta.metals” // Scala语言支持对Chisel代码提示极佳 ] } }, “postCreateCommand”: “sbt sbtVersion” // 容器创建后自动运行预拉取sbt }用VSCode打开项目文件夹它会提示你“在容器中重新打开”。点击后VSCode会自动构建并连接到一个Docker容器里面一切就绪。注意首次构建可能会花费一些时间下载镜像和依赖但一劳永逸。此后你只需关注代码本身。3. Chisel世界的基石数据类型Data Types与硬件原语在Verilog里我们最熟悉的数据类型是reg和wire以及[7:0]这样的位宽表示。在Chisel中这一切被抽象为更丰富、更安全的类型系统。理解这些类型是写出正确、高效Chisel代码的基础。3.1 标量类型UInt,SInt,Bool这是最基础的硬件类型对应Verilog中的无符号数、有符号数和单比特线网。UInt(width: Int)无符号整数。例如UInt(8.W)表示一个8位无符号数。这里的.W是一个将Scala整数转换为Chisel宽度Width类型的方法必不可少。SInt(width: Int)有符号整数二进制补码表示。例如SInt(16.W)。Bool()布尔类型单比特。相当于UInt(1.W)但语义更清晰。创建与连接import chisel3._ class BasicTypes extends Module { val io IO(new Bundle { val in Input(UInt(8.W)) val out Output(UInt(8.W)) val flag Output(Bool()) }) // 直接连接 io.out : io.in // 创建常量/字面量 val constantEight 8.U(4.W) // 创建一个4位宽值为8的UInt。注意8需要小于2^4 val highFlag true.B // 创建一个值为真的Bool // 类型转换 val uintVal: UInt 5.U val asSInt: SInt uintVal.asSInt // 无符号转有符号 reinterpret val asBool: Bool uintVal(0) // 取最低位得到Bool io.flag : (io.in 100.U) // 比较操作产生Bool类型 }提示:是Chisel中的连接操作符表示将右侧的硬件节点连接到左侧。它和Scala的赋值有本质区别。:发生在硬件构造时Elaboration time而发生在Scala运行时。3.2 聚合类型Bundle与Vec这是Chisel提升抽象能力的关键让你能定义复杂的数据结构。Bundle自定义结构体想象一下Verilog里的struct或者软件中的class。Bundle用于将一组相关的信号捆绑在一起形成一个逻辑单元。class Packet extends Bundle { val header UInt(8.W) val addr UInt(32.W) val data UInt(64.W) val valid Bool() } class MyModule extends Module { val io IO(new Bundle { val packetIn Input(new Packet) val packetOut Output(new Packet) }) // 可以整体操作也可以访问成员 io.packetOut : io.packetIn val isHeaderZero (io.packetIn.header 0.U) }使用Bundle能极大提升代码可读性和维护性。当你需要增减Packet的字段时只需在一个地方修改。Vec硬件向量相当于Verilog中的多维reg或wire数组但功能更强大。Vec的元素必须是相同的Chisel硬件类型。class VectorExample extends Module { val io IO(new Bundle { val index Input(UInt(3.W)) // 0-7 val dataIn Input(UInt(8.W)) val dataOut Output(UInt(8.W)) val vecOut Output(Vec(8, UInt(8.W))) }) // 创建一个包含8个8位UInt寄存器的向量 val regVec Reg(Vec(8, UInt(8.W))) // 通过索引进行读写索引可以是硬件信号 regVec(io.index) : io.dataIn io.dataOut : regVec(io.index) // 整体连接 io.vecOut : regVec // 初始化使用VecInit这在Verilog中需要写循环才能实现 val initVec RegInit(VecInit(Seq.fill(8)(0.U(8.W)))) }这里有一个非常重要的概念Vec的索引可以是动态的硬件信号io.index。这意味着你可以描述诸如“根据当前状态选择第N个寄存器输出”这样的行为而这在生成Verilog时会正确地被综合成多路选择器MUX。这是Chisel“生成硬件”能力的直接体现。3.3 类型转换与位操作硬件设计离不开精确的位操作。Chisel提供了丰富且安全的操作符。位提取与拼接val word 0x1234.U(16.W) val highByte word(15, 8) // 提取第15位到第8位得到UInt(8.W) val lowByte word(7, 0) val singleBit word(4) // 提取第4位得到Bool() val concatenated Cat(highByte, lowByte) // 拼接得到0x1212.U(16.W) val replicated Fill(4, 0xAB.U(8.W)) // 重复4次得到0xABABABAB.U(32.W)类型转换.asUInt,.asSInt: 在位宽不变的情况下重新解释位。谨慎使用需确保语义正确。.asBool: 将单比特UInt转为Bool。.asTypeOf[T : Data]: 强大的通用转换方法要求位宽匹配。val myPacket Wire(new Packet) val rawBits 0xdeadbeefca.U(64.W) myPacket : rawBits.asTypeOf(myPacket) // 将rawBits按Packet的布局解析注意Chisel是强类型的。你不能直接将一个UInt赋值给一个SInt即使位宽相同。必须显式转换。这虽然增加了些许代码量但避免了大量隐蔽的硬件设计错误。4. 描述硬件行为组合逻辑与寄存器有了数据类型我们就可以用它们来构建有行为的电路了。Chisel将硬件描述分为组合逻辑和时序逻辑寄存器概念清晰。4.1 组合逻辑Wire与表达式在Chisel中你不需要显式声明wire。当你使用一个val来保存一个Chisel硬件类型的表达式结果时它就代表了一条连线或组合逻辑。class Combinational extends Module { val io IO(new Bundle { val a, b, c Input(UInt(8.W)) val sum, comp Output(UInt(8.W)) }) // 直接使用表达式。这里会生成一个加法器和一个比较器 val sumTemp io.a io.b // sumTemp是一个代表加法结果的硬件节点Wire val isGreater io.a io.c // isGreater是一个Bool类型节点 io.sum : sumTemp io.comp : Mux(isGreater, io.a, io.c) // 三目运算符生成一个8位MUX // 更复杂的组合逻辑块可以使用Wire显式声明 val complexWire Wire(UInt(8.W)) complexWire : (io.a io.b) | (~io.c) // 注意Wire必须在使用前被驱动连接否则在仿真中会是未定义值。 }关键理解val sumTemp io.a io.b这一行在Scala运行时它并没有进行加法计算而是构建了一个代表加法操作的硬件节点。这个节点会在后续生成Verilog时变成assign sumTemp io.a io.b;。4.2 时序逻辑Reg——硬件寄存器的核心寄存器是存储时序逻辑状态的根本。Chisel中的Reg用法多样但核心是Reg,RegNext, 和RegInit。class Sequential extends Module { val io IO(new Bundle { val in Input(UInt(8.W)) val out Output(UInt(8.W)) val en Input(Bool()) }) // 1. 最基本的寄存器下一拍值等于in无复位值上电后为未知态X val regBasic Reg(UInt(8.W)) regBasic : io.in // 注意在同一个when/otherwise块中对同一个寄存器的赋值必须有且仅有一次否则会报“多重驱动”错误。 // 2. 带使能的寄存器使用RegNext val regWithEnable RegNext(io.in, 0.U) // 当时钟上升沿且io.en为真时采样io.in否则保持。第二个参数是复位值。 // 更明确的写法 val regNextExplicit Reg(UInt(8.W)) when(io.en) { regNextExplicit : io.in } // 3. 带同步复位和初始值的寄存器使用RegInit val resetVal 0xff.U(8.W) val regInitSync RegInit(resetVal) when(io.en) { regInitSync : io.in }.otherwise { // 如果不写.otherwise寄存器在使能无效时将保持原值。 // 这里演示同步复位非标准通常复位有单独信号 when(reset.asBool) { // reset是Module内置的复位信号 regInitSync : resetVal } } // 4. 寄存器向量 val shiftReg Reg(Vec(4, UInt(8.W))) for (i - 3 to 1 by -1) { shiftReg(i) : shiftReg(i-1) } shiftReg(0) : io.in io.out : shiftReg(3) // 将regBasic连接到输出观察 io.out : regBasic }关于复位的深度解析Chisel模块默认有一个reset信号高有效或低有效取决于编译选项。使用RegInit定义的寄存器其初始值就是在复位信号有效时被赋予的值。在真实的芯片中这通常对应同步复位或异步复位触发器。一个重要经验对于复杂的复位逻辑如上电后从某个接口加载初始值建议使用显式的when (reset.asBool) { ... } .otherwise { ... }块来控制而不是依赖RegInit这样逻辑更清晰。4.3 条件语句when,.elsewhen,.otherwise这是描述硬件条件逻辑的主要方式对应Verilog的if-else。但Chisel的when是可级联的并且最后必须用.otherwise闭合所有可能路径以确保生成的组合逻辑是完备的无锁存器除非你故意要生成。class WhenExample extends Module { val io IO(new Bundle { val sel Input(UInt(2.W)) val a, b, c, d Input(UInt(8.W)) val out Output(UInt(8.W)) val state Output(UInt(8.W)) }) val regState RegInit(0.U(8.W)) // when-elsewhen-otherwise 结构 when(io.sel 0.U) { io.out : io.a regState : 1.U } .elsewhen(io.sel 1.U) { io.out : io.b regState : 2.U } .elsewhen(io.sel 2.U) { io.out : io.c // 这里没有对regState赋值则regState保持原值 } .otherwise { // sel 3.U 或其他值 io.out : io.d regState : 0.U } // 嵌套的when val temp Wire(UInt(8.W)) when(io.a 10.U) { when(io.b 5.U) { temp : 1.U } .otherwise { temp : 2.U } } .otherwise { temp : 3.U } }注意when块内部赋值的信号必须在所有可能的条件分支包括otherwise中都被赋值否则会推断出锁存器Latch。在大多数同步数字设计中锁存器是需要避免的因为它会带来时序和验证的复杂性。Chisel编译器会对此给出警告。5. 模块化设计定义与实例化Module任何稍具规模的设计都需要模块化。在Chisel中每个模块都是一个继承自Module的Scala类。5.1 如何定义一个模块import chisel3._ import chisel3.util._ // 包含一些常用工具如Counter, Decoder等 // 定义一个加法器模块 class MyAdder(width: Int) extends Module { // 参数化width是一个Scala Int参数在实例化时传入 val io IO(new Bundle { val in_a Input(UInt(width.W)) val in_b Input(UInt(width.W)) val out_sum Output(UInt(width.W)) val out_carry Output(Bool()) // 溢出进位 }) val sum io.in_a io.in_b // 是带进位加的运算符 io.out_sum : sum(width-1, 0) // 取和值的低width位 io.out_carry : sum(width) // 取进位位 } // 定义一个顶层模块使用多个子模块 class TopLevel extends Module { val io IO(new Bundle { val x, y Input(UInt(16.W)) val result Output(UInt(16.W)) finalCarry Output(Bool()) }) // 实例化两个加法器 val adder1 Module(new MyAdder(8)) val adder2 Module(new MyAdder(8)) // 连接逻辑将16位输入拆分成两个8位部分进行级联加法 adder1.io.in_a : io.x(7, 0) adder1.io.in_b : io.y(7, 0) // 第二个加法器的输入是高位部分并加上第一个加法器的进位 val high_a_with_carry io.x(15, 8) adder1.io.out_carry.asUInt adder2.io.in_a : high_a_with_carry adder2.io.in_b : io.y(15, 8) // 输出拼接 io.result : Cat(adder2.io.out_sum, adder1.io.out_sum) io.finalCarry : adder2.io.out_carry }要点解析class MyAdder(width: Int) extends Modulewidth是构造参数允许你创建不同位宽的加法器实例。这是Chisel参数化能力的冰山一角。val io IO(...)每个模块必须有且仅有一个名为io的端口集合且其类型必须是Bundle的子类。这是Chisel编译器的硬性规定用于自动识别和连接端口。val adder1 Module(new MyAdder(8))使用Module()来实例化一个子模块。new MyAdder(8)创建的是Scala对象而Module()包裹则告诉Chisel编译器将其视为一个硬件模块实例。连接使用:操作符方向是从右到左驱动源在右侧。5.2 模块端口的方向Input,Output,Flipped在Bundle内部每个字段都可以指定方向。class DecoupledIO[T : Data](gen: T) extends Bundle { val ready Input(Bool()) // 目标端准备好接收 val valid Output(Bool()) // 源端数据有效 val bits Output(gen) // 数据 } // 但是如果我想用这个Bundle作为模块的输入接口呢所有方向都要反过来。 // 手动改很麻烦而且容易出错。这时可以用Flipped()。 class MySender extends Module { val io IO(new Bundle { // 这里我们希望ready是输出valid和bits是输入 val channel Flipped(new DecoupledIO(UInt(32.W))) }) // 在内部io.channel.ready是Outputio.channel.valid和io.channel.bits是Input }Flipped()会递归地将一个Bundle内部所有信号的方向取反。这在定义标准接口如AXI、APB时极其有用可以保证接口定义的一致性。5.3 实战避坑命名与调试默认情况下Chisel生成的Verilog信号名是_T_1,_T_2这样的匿名名称调试起来如同噩梦。有两种主要方式改善使用suggestName在信号定义时建议一个名字。val criticalSignal (io.a io.b).suggestName(“sum_before_reg”)使用printf进行仿真时调试Chisel内置了类似C语言的printf在仿真时会打印出来。when(io.en) { printf(p“At clock cycle, in$io.in, reg value$regState\n”) // p”…”是Scala的字符串插值这里被Chisel重载用于安全打印硬件信号 }启用更好的命名在sbt运行时添加选项-Dchisel3.debug1或者在代码中导入import chisel3.experimental.chiselName并注解你的模块chiselName可以尝试根据Scala变量名生成Verilog名但效果有时不理想。我的经验是对于关键的中间信号和子模块实例养成使用suggestName的习惯能节省大量后期调试时间。6. 生成Verilog与测试让设计跑起来设计写完了怎么看到实际的Verilog代码并验证它是否正确呢这就需要用到Chisel的测试器Testers和生成器。6.1 生成Verilog代码在你的项目里可以创建一个简单的对象来驱动编译。// 在 src/main/scala/ 下创建一个文件比如 GenerateVerilog.scala object GenerateVerilog extends App { // 导入chisel3的驱动 (new chisel3.stage.ChiselStage).emitVerilog(new TopLevel(), Array(“—target-dir”, “generated”)) println(“Verilog for TopLevel has been written to ./generated/TopLevel.v”) }然后在sbt中运行sbt “runMain GenerateVerilog”这会在./generated目录下生成TopLevel.v文件。打开它你就能看到由Chisel转换而来的、可综合的Verilog代码。第一次看可能会觉得冗长因为Chisel生成了非常明确的、结构化的代码但它是完全符合标准的。6.2 使用ChiselTest进行硬件单元测试写硬件不测试等于造飞机不试飞。ChiselTest是一个强大的测试框架允许你直接在Scala层面编写测试驱动时钟、施加激励、检查输出而无需手动编写Verilog测试平台。首先确保build.sbt中已经添加了chiseltest依赖。 然后在src/test/scala/下创建测试规范import chisel3._ import chiseltest._ import org.scalatest.flatspec.AnyFlatSpec class TopLevelSpec extends AnyFlatSpec with ChiselScalatestTester { behavior of “TopLevel” it should “perform correct cascaded addition” in { test(new TopLevel).withAnnotations(Seq(WriteVcdAnnotation)) { dut // dut: Device Under Test即被测模块实例 // 初始化poke是驱动输入 dut.io.x.poke(0x1234.U) dut.io.y.poke(0x5678.U) // 时钟前进一个周期让组合逻辑稳定对于纯组合逻辑模块可能不需要 dut.clock.step(1) // expect是检查输出 // 0x1234 0x5678 0x68AC 无进位 dut.io.result.expect(0x68ac.U) dut.io.finalCarry.expect(false.B) // 测试带进位的情况 dut.io.x.poke(0x00ff.U) dut.io.y.poke(0x0001.U) // 低8位 0xff0x01 0x100产生进位 dut.clock.step(1) // 期望结果低8位0x00高8位 (0x000x00进位1)0x01最终0x0100 dut.io.result.expect(0x0100.U) dut.io.finalCarry.expect(false.B) // 整体16位未溢出 // 测试整体溢出 dut.io.x.poke(0xffff.U) dut.io.y.poke(0x0001.U) // 0xffff 0x0001 0x10000 (17位) dut.clock.step(1) dut.io.result.expect(0x0000.U) // 低16位为0 dut.io.finalCarry.expect(true.B) // 最终进位为1 } } it should “handle reset correctly” in { test(new TopLevel).withAnnotations(Seq(WriteVcdAnnotation)) { dut // 先让模块运行一下 dut.io.x.poke(0x1111.U) dut.io.y.poke(0x2222.U) dut.clock.step(5) // 然后复位 dut.reset.poke(true.B) dut.clock.step(2) // 保持复位至少一个周期取决于复位极性 dut.reset.poke(false.B) dut.clock.step(1) // 复位后输出应该是不确定或初始值取决于设计 // 这里我们主要看波形所以不设具体断言 } } }运行测试sbt test如果测试通过你会看到成功的提示。WriteVcdAnnotation注解会生成VCD波形文件通常位于test_run_dir/测试名目录下可以用GTKWave等工具查看这对于调试复杂时序问题不可或缺。测试经验谈从简单开始先测试边界条件0最大值进位/借位。使用fork和join进行并发激励可以模拟多个并发的输入事件。善用poke和expect的延迟poke是立即生效的而expect检查的是当前时刻的值。对于时序逻辑需要在clock.step()之后检查。波形是你的朋友遇到诡异的问题第一时间打开波形看信号跳变。7. 超越基础参数化、高阶函数与生成器掌握了基本语法和流程你已经可以完成很多设计了。但Chisel真正的威力在于其“生成器”特性。这里简要提一下进阶方向为你后续学习铺路。7.1 利用Scala进行高级参数化模块参数不仅仅是位宽。你可以传递函数、其他模块类型、甚至复杂的配置case class。case class CPUConfig(xLen: Int, useFPU: Boolean, nCores: Int) class CPU(config: CPUConfig) extends Module { val io IO(new Bundle { /***/ }) if (config.useFPU) { // 实例化浮点单元 val fpu Module(new FPU(config.xLen)) // ... } // 使用for循环生成多核 val cores Seq.tabulate(config.nCores) { i Module(new Core(i, config)) } }7.2 使用for循环和集合操作生成硬件这是Chisel相比Verilog最革命性的优势之一你可以用编程语言的所有能力来生成硬件结构。// 生成一个交叉开关Crossbar class Crossbar(nInputs: Int, nOutputs: Int, dataWidth: Int) extends Module { val io IO(new Bundle { val ins Vec(nInputs, Input(UInt(dataWidth.W))) val sel Input(Vec(nOutputs, UInt(log2Ceil(nInputs).W))) // 每个输出选择一个输入 val outs Vec(nOutputs, Output(UInt(dataWidth.W))) }) for (iOut - 0 until nOutputs) { // 为每个输出生成一个多路选择器 io.outs(iOut) : Mux1H(io.sel(iOut), io.ins) // Mux1H 是“one-hot”选择器更高效 } }这段代码简洁地描述了一个任意大小的交叉开关。在Verilog里你需要写冗长的generate块且容易出错。7.3 探索标准库chisel3.util这个包里有很多“开箱即用”的常用硬件组件能极大提升效率Counter计数器带使能和溢出标志。Decoupled/Queue就绪/有效接口及其FIFO队列。PriorityEncoder/PriorityMux优先级编码器和选择器。ShiftRegister移位寄存器。Arbiter仲裁器。例如实现一个带使能和饱和计数的计数器只需一行val (count, wrap) Counter(io.en, maxCount10)count是计数值wrap在计数到最大值时拉高一个周期。从Verilog转向Chisel初期最大的挑战是思维模式的转换。你不再是在“描述”一个固定的硬件电路而是在“编写程序”来“生成”最适合当前需求的电路。这种元编程的能力使得应对复杂性、实现高度可配置性、提高代码复用率成为了可能。虽然学习曲线存在尤其是需要熟悉一部分Scala语法但长远来看这对于构建稳健、可维护的数字系统是值得的投资。
返回列表