多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

使用Chisel构建带CRC-8校验的UART收发器:从协议到FPGA实现

使用Chisel构建带CRC-8校验的UART收发器:从协议到FPGA实现 在实际 FPGA 开发中UART 通信因其简单可靠而被广泛使用但裸 UART 协议本身缺乏数据完整性校验在长距离或干扰环境下传输错误可能导致数据错乱而难以察觉。为提升通信可靠性在数据链路层增加 CRC 校验是常见的工程实践。Chisel 作为一种现代硬件构造语言其面向对象和函数式编程的特性使得构建一个带 CRC 校验的 UART 模块比传统 Verilog 更易于模块化和复用。本文将带你从零开始使用 Chisel 构建一个完整的、包含 CRC-8 校验的 UART 收发器涵盖从协议理解、模块设计、代码实现到仿真验证的全过程并重点解释如何在 Chisel 中优雅地实现 CRC 计算逻辑。本文适合已经了解 Chisel 基础语法和 Scala 编程并希望进行实际项目实践的 FPGA 开发者。通过本文你将掌握如何将一个通信协议分解为可组合的硬件模块如何在 Chisel 中实现状态机和控制逻辑以及如何为数据流添加校验机制。最终你将得到一个可综合、可仿真的 Chisel 项目并能将其部署到 FPGA 开发板上进行实际测试。1. 理解带 CRC 校验的 UART 通信协议栈在动手编码之前必须清晰地理解我们要构建的协议栈层次和数据流。一个简单的 UART 帧通常由起始位、数据位、可选的奇偶校验位和停止位组成。而我们要做的是在应用层数据与 UART 物理层之间插入一个 CRC 校验层。1.1 UART 基础帧格式与数据链路层扩展标准的 UART 一帧数据通常为 8 位或 9 位。在我们的设计中我们使用最常见的配置1 个起始位低电平8 个数据位无奇偶校验位1 个停止位高电平。这意味着每传输一个字节需要发送 10 个比特。CRC 校验的加入意味着我们不能直接将用户数据送入 UART 发送器。发送端需要先对用户数据例如一个 8 字节的数据包计算出一个 CRC 校验码然后将“数据CRC”作为一个整体通过 UART 帧逐个字节地发送出去。接收端则需要先接收所有字节然后重新计算接收数据的 CRC 值并与接收到的 CRC 值进行比较。如果一致则认为数据正确否则应产生一个错误标志。因此我们的系统需要两个核心模块一个负责 CRC 计算的模块以及一个负责组帧添加 CRC和解帧校验 CRC的顶层控制模块。UART 的收发器则作为最底层的物理层。1.2 CRC-8 算法选择与硬件实现原理CRC循环冗余校验有多种标准如 CRC-8、CRC-16、CRC-32等其区别在于生成多项式、初始值和输出异或值等参数。为了简化并保持文章聚焦我们选择常用的 CRC-8 算法其生成多项式为x^8 x^2 x 1对应的十六进制表示为0x07。硬件实现 CRC 通常有两种方式串行计算和并行计算。串行计算适合低速或资源受限的场景它在一个时钟周期内处理一位数据并行计算则在一个时钟周期内处理一个字节8位数据速度更快但逻辑稍复杂。考虑到 UART 波特率相对较低如 115200 bps而 FPGA 系统时钟通常很高如 50 MHz我们完全可以使用串行计算逻辑更清晰也便于理解。串行 CRC-8 的计算过程可以看作一个线性反馈移位寄存器LFSR。对于每个输入的数据位将其与寄存器最高位进行异或然后将结果反馈到生成多项式指定的位上同时寄存器整体左移一位。在 Chisel 中我们可以用一个 8 位的RegInit寄存器来优雅地描述这个状态机。2. 搭建 Chisel 开发环境与项目结构开始编码前需要确保你的开发环境已经就绪。Chisel 项目通常使用 sbt 进行构建。2.1 环境准备与依赖配置首先确保你的系统已安装 JDK 8 或 11 以及 sbt。然后创建一个标准的 sbt 项目目录结构。关键的配置文件是build.sbt。// build.sbt ThisBuild / scalaVersion : 2.13.10 ThisBuild / version : 0.1.0 lazy val root (project in file(.)) .settings( name : chisel-uart-crc, libraryDependencies Seq( edu.berkeley.cs %% chisel3 % 3.6.0, edu.berkeley.cs %% chiseltest % 0.6.0 % test ), scalacOptions Seq( -language:reflectiveCalls, -deprecation, -feature, -Xcheckinit, -P:chiselplugin:useBundlePlugin ), addCompilerPlugin(edu.berkeley.cs % chisel3-plugin % 3.6.0 cross CrossVersion.full), )2.2 项目模块划分一个清晰的项目结构有助于管理复杂度。我们建议按功能划分模块src/main/scala/uartcrc/ ├── Crc8.scala // CRC-8 计算模块 ├── UartTx.scala // UART 发送器 ├── UartRx.scala // UART 接收器 ├── UartCrcWrapper.scala // 顶层封装模块组帧/解帧 └── Top.scala // 最顶层模块可能包含时钟分频等 test/scala/uartcrc/ └── UartCrcSpec.scala // 测试文件3. 核心模块设计与实现我们将自底向上地实现各个模块从最基础的 CRC 模块开始。3.1 实现串行 CRC-8 计算模块 (Crc8.scala)CRC 模块是一个纯组合逻辑时序逻辑的模块。它提供一个接口输入一位数据bitIn和计算使能enable在每个时钟上升沿如果enable有效则根据当前寄存器值和输入位更新 CRC 值。同时它还应提供重置和读取当前 CRC 值的接口。// Crc8.scala package uartcrc import chisel3._ import chisel3.util._ class Crc8 extends Module { val io IO(new Bundle { val bitIn Input(Bool()) // 串行输入数据位 val enable Input(Bool()) // 计算使能高电平有效 val reset Input(Bool()) // 同步复位将 CRC 寄存器置为初始值通常为0 val crcOut Output(UInt(8.W)) // 当前的 CRC 值 }) // CRC-8 生成多项式: x^8 x^2 x 1 (0x07) val poly 0x07.U(8.W) // CRC 状态寄存器初始值为0 val crcReg RegInit(0.U(8.W)) when(io.reset) { crcReg : 0.U }.elsewhen(io.enable) { // 串行 CRC 计算逻辑 val msb crcReg(7) // 获取当前最高位 val feedback msb ^ io.bitIn // 计算反馈位 // 寄存器左移一位并根据多项式进行反馈 crcReg : (crcReg 1) ^ (Fill(8, feedback) poly) } io.crcOut : crcReg }关键解释val msb crcReg(7)取出当前 CRC 值的最高位第7位。val feedback msb ^ io.bitIn这是 CRC 串行计算的核心将输入位与寄存器最高位异或。crcReg : (crcReg 1) ^ (Fill(8, feedback) poly)这是状态更新。(crcReg 1)寄存器左移一位最低位补 0。(Fill(8, feedback) poly)如果feedback为 1则生成多项式poly的值被异或到移位后的寄存器上如果为 0则异或 0相当于不变。整个表达式用^连接完成了线性反馈移位寄存器的一次更新。3.2 实现 UART 发送器 (UartTx.scala)UART 发送器是一个经典的状态机。它接收一个 8 位数据dataIn和发送启动信号start然后按照设定的波特率依次输出起始位、8个数据位和停止位。// UartTx.scala package uartcrc import chisel3._ import chisel3.util._ class UartTx(baudRate: Int, clockFreq: Int) extends Module { val io IO(new Bundle { val dataIn Input(UInt(8.W)) val start Input(Bool()) // 高电平脉冲启动发送 val txd Output(Bool()) // 串行输出线 val busy Output(Bool()) // 发送忙标志 }) // 计算波特率分频系数 val BAUD_DIV (clockFreq / baudRate).U val tickGen RegInit(0.U(32.W)) val tick tickGen 0.U // 产生波特率时钟使能信号 // 波特率分频计数器 when(tickGen 0.U) { tickGen : BAUD_DIV - 1.U }.otherwise { tickGen : tickGen - 1.U } // 发送状态机 val sIdle :: sStart :: sData :: sStop :: Nil Enum(4) val stateReg RegInit(sIdle) val bitIndex Reg(UInt(3.W)) // 0 to 7 val dataReg Reg(UInt(8.W)) io.txd : true.B // 默认高电平空闲状态 io.busy : stateReg / sIdle switch(stateReg) { is(sIdle) { io.txd : true.B when(io.start) { stateReg : sStart dataReg : io.dataIn tickGen : 0.U // 复位分频计数器确保起始位对齐 } } is(sStart) { io.txd : false.B // 起始位为低电平 when(tick) { stateReg : sData bitIndex : 0.U } } is(sData) { io.txd : dataReg(bitIndex) // 从最低位开始发送 when(tick) { when(bitIndex 7.U) { stateReg : sStop }.otherwise { bitIndex : bitIndex 1.U } } } is(sStop) { io.txd : true.B // 停止位为高电平 when(tick) { stateReg : sIdle } } } }参数说明baudRateUART 波特率如 115200。clockFreq模块的时钟频率如 50000000 (50 MHz)。BAUD_DIV根据时钟频率和波特率计算出的分频系数用于生成波特率时钟。3.3 实现 UART 接收器 (UartRx.scala)接收器与发送器对称但逻辑更复杂一些因为它需要从异步的串行线中正确采样数据。我们采用过采样技术来提高抗干扰能力例如 16 倍过采样但为了简化本例使用在波特率时钟中点采样的经典方法。// UartRx.scala package uartcrc import chisel3._ import chisel3.util._ class UartRx(baudRate: Int, clockFreq: Int) extends Module { val io IO(new Bundle { val rxd Input(Bool()) val dataOut Output(UInt(8.W)) val dataValid Output(Bool()) // 高电平脉冲表示数据有效 }) val BAUD_DIV (clockFreq / baudRate).U val HALF_BAUD (BAUD_DIV 1).asUInt() // 用于在比特中点采样 val tickGen RegInit(0.U(32.W)) val tick tickGen 0.U when(tickGen 0.U) { tickGen : BAUD_DIV - 1.U }.otherwise { tickGen : tickGen - 1.U } val sIdle :: sStart :: sData :: sStop :: Nil Enum(4) val stateReg RegInit(sIdle) val bitIndex Reg(UInt(3.W)) val dataReg Reg(UInt(8.W)) val sampleReg Reg(Bool()) // 用于消除亚稳态的采样寄存器 // 对输入进行同步寄存消除亚稳态 sampleReg : io.rxd val rxdSync sampleReg io.dataOut : dataReg io.dataValid : false.B switch(stateReg) { is(sIdle) { when(!rxdSync) { // 检测到起始位低电平 stateReg : sStart tickGen : HALF_BAUD // 延时半个波特周期到起始位中点 } } is(sStart) { when(tick) { // 在起始位中点再次确认是低电平 when(!rxdSync) { stateReg : sData bitIndex : 0.U }.otherwise { stateReg : sIdle // 毛刺回到空闲 } } } is(sData) { when(tick) { // 在数据位中点采样 dataReg : Cat(rxdSync, dataReg(7,1)) // 右移并入 when(bitIndex 7.U) { stateReg : sStop }.otherwise { bitIndex : bitIndex 1.U } } } is(sStop) { when(tick) { // 在停止位中点期望为高电平 // 这里可以检查停止位是否正确本例仅输出数据 io.dataValid : true.B stateReg : sIdle } } } }关键点sampleReg用于对异步输入rxd进行同步这是 FPGA 设计中防止亚稳态的标准做法。HALF_BAUD用于在起始位和后续数据位的中间点进行采样以提高采样准确性。Cat(rxdSync, dataReg(7,1))是一个简洁的右移并入操作将新采样的位放在最低位原有数据右移。3.4 实现顶层封装模块 (UartCrcWrapper.scala)这是项目的核心控制器。它负责协调 CRC 计算与 UART 收发。其功能包括发送路径接收上游数据包例如 8 字节计算 CRC然后将“数据CRC”通过 UART 发送器依次送出。接收路径从 UART 接收器收集字节当收齐预期长度的数据包后重新计算 CRC 并与接收到的 CRC 比较输出校验结果和数据。// UartCrcWrapper.scala package uartcrc import chisel3._ import chisel3.util._ class UartCrcWrapper(packetSize: Int, baudRate: Int, clockFreq: Int) extends Module { val io IO(new Bundle { // 用户侧发送接口 val txDataIn Input(UInt((packetSize*8).W)) // 假设 packetSize 字节的数据包 val txStart Input(Bool()) val txBusy Output(Bool()) // 用户侧接收接口 val rxDataOut Output(UInt((packetSize*8).W)) val rxDataValid Output(Bool()) val rxCrcError Output(Bool()) // 高电平表示 CRC 校验失败 // 物理层 UART 信号 val txd Output(Bool()) val rxd Input(Bool()) }) // 实例化子模块 val uartTx Module(new UartTx(baudRate, clockFreq)) val uartRx Module(new UartRx(baudRate, clockFreq)) val crcCalc Module(new Crc8()) // 连接物理层信号 io.txd : uartTx.io.txd uartRx.io.rxd : io.rxd // --- 发送逻辑 --- val txState RegInit(0.U(2.W)) // 0: 空闲1: 发送数据字节2: 发送 CRC 字节 val txByteCount Reg(UInt(log2Ceil(packetSize1).W)) val txDataReg Reg(UInt((packetSize*8).W)) val txCrcReg Reg(UInt(8.W)) uartTx.io.start : false.B uartTx.io.dataIn : 0.U io.txBusy : txState / 0.U || uartTx.io.busy // CRC 计算用于发送路径 crcCalc.io.reset : false.B crcCalc.io.enable : false.B crcCalc.io.bitIn : 0.B switch(txState) { is(0.U) { // 空闲 when(io.txStart) { txState : 1.U txDataReg : io.txDataIn txByteCount : 0.U // 启动 CRC 计算复位后逐位计算 crcCalc.io.reset : true.B } } is(1.U) { // 发送数据字节 when(!uartTx.io.busy) { // 准备发送下一个字节 val currentByte txDataReg(7,0) // 取最低字节 uartTx.io.dataIn : currentByte uartTx.io.start : true.B // 将该字节移入 CRC 模块逐位 // 注意这里为了简化状态机可以预先计算 CRC或使用更复杂的控制逻辑。 // 本例采用简化先发送CRC计算在后台进行需另一个状态机。 // 更完善的实现需要并行处理或先计算后发送。 txDataReg : txDataReg 8 // 右移一个字节 txByteCount : txByteCount 1.U when(txByteCount (packetSize-1).U) { txState : 2.U // 数据发完准备发 CRC // 此时 crcCalc.io.crcOut 应该是数据包的 CRC 值 txCrcReg : crcCalc.io.crcOut } } } is(2.U) { // 发送 CRC 字节 when(!uartTx.io.busy) { uartTx.io.dataIn : txCrcReg uartTx.io.start : true.B txState : 0.U // 发送完成回到空闲 } } } // --- 接收逻辑 --- val rxState RegInit(0.U(2.W)) // 0: 空闲1: 接收数据字节2: 接收 CRC 字节3: 校验 val rxByteCount Reg(UInt(log2Ceil(packetSize1).W)) val rxDataReg Reg(Vec(packetSize, UInt(8.W))) val rxCRCReceived Reg(UInt(8.W)) val rxCRCCalculated Reg(UInt(8.W)) io.rxDataOut : 0.U io.rxDataValid : false.B io.rxCrcError : false.B // 另一个 CRC 实例用于接收校验或复用但需要控制 val crcCalcRx Module(new Crc8()) crcCalcRx.io.reset : false.B crcCalcRx.io.enable : false.B crcCalcRx.io.bitIn : 0.B when(uartRx.io.dataValid) { val receivedByte uartRx.io.dataOut switch(rxState) { is(0.U) { // 空闲收到第一个字节认为是数据包开始 rxState : 1.U rxByteCount : 1.U rxDataReg(0) : receivedByte // 启动接收端 CRC 计算 crcCalcRx.io.reset : true.B } is(1.U) { // 接收数据字节 rxDataReg(rxByteCount) : receivedByte rxByteCount : rxByteCount 1.U when(rxByteCount (packetSize-1).U) { rxState : 2.U // 数据收完下一个是 CRC } } is(2.U) { // 接收 CRC 字节 rxCRCReceived : receivedByte rxState : 3.U // 进入校验状态 rxCRCCalculated : crcCalcRx.io.crcOut // 获取计算出的 CRC } is(3.U) { // 校验 io.rxDataOut : rxDataReg.asUInt() // 将 Vec 转换为 UInt 输出 io.rxDataValid : true.B io.rxCrcError : rxCRCReceived / rxCRCCalculated rxState : 0.U // 回到空闲准备接收下一个包 } } } }设计说明与简化 这个顶层模块是一个简化版本旨在展示数据流和控制逻辑。其中发送路径的 CRC 计算与字节发送的协调、接收路径的 CRC 实时计算都需要更精细的状态机来控制 CRC 模块的enable和bitIn信号。一个更完整的实现需要发送端在txState为 1 时用一个子状态机控制将当前要发送的字节逐位从 LSB 或 MSB输入到crcCalc.io.bitIn并拉高crcCalc.io.enable一个时钟周期。接收端在rxState为 1 或 2 时同样将接收到的字节逐位输入到crcCalcRx模块。需要考虑字节序先发 LSB 还是 MSB与 CRC 计算顺序的一致性。为了保持文章核心脉络清晰上述代码省略了这部分逐位控制的细节但这是实际项目中必须实现的。你可以将其作为一个关键练习。4. 仿真测试与验证编写硬件代码后必须通过仿真验证其功能。我们将使用 ChiselTest 来编写测试。4.1 编写测试激励 (UartCrcSpec.scala)我们将测试整个链路发送一个数据包模拟接收端接收并检查 CRC 校验结果。// test/scala/uartcrc/UartCrcSpec.scala package uartcrc import chisel3._ import chiseltest._ import org.scalatest.flatspec.AnyFlatSpec class UartCrcSpec extends AnyFlatSpec with ChiselScalatestTester { val PACKET_SIZE 4 // 测试用数据包为4字节 val BAUD_RATE 115200 val CLOCK_FREQ 50000000 // 50 MHz it should transmit and receive a packet with correct CRC in { test(new UartCrcWrapper(PACKET_SIZE, BAUD_RATE, CLOCK_FREQ)) { dut // 初始化 dut.io.txStart.poke(false.B) dut.io.rxd.poke(true.B) // UART 空闲状态为高电平 dut.clock.step(10) // 准备测试数据 (0xDE, 0xAD, 0xBE, 0xEF) val testData (0xde 24) | (0xad 16) | (0xbe 8) | 0xef dut.io.txDataIn.poke(testData.U) // 启动发送 dut.io.txStart.poke(true.B) dut.clock.step() dut.io.txStart.poke(false.B) // 等待发送忙信号结束简化等待实际需根据波特率等待足够周期 // 这里我们模拟接收端行为从 txd 引脚读取数据并喂给 rxd 引脚 // 这是一个“回环”测试 var receivedBytes Seq.empty[Int] for (i - 0 until (PACKET_SIZE 1) * 10 * 20) { // 粗略估计时间 // 将发送端的 txd 连接到接收端的 rxd val txdVal dut.io.txd.peek().litValue dut.io.rxd.poke(txdVal) dut.clock.step(1) // 可以在这里监控接收端输出 if (dut.io.rxDataValid.peek().litValue 1) { println(sData received: 0x${dut.io.rxDataOut.peek().litValue.toInt.toHexString}) println(sCRC Error: ${dut.io.rxCrcError.peek().litValue}) } } // 简单断言发送忙结束 dut.io.txBusy.expect(false.B) } } it should detect CRC error if data is corrupted in { test(new UartCrcWrapper(PACKET_SIZE, BAUD_RATE, CLOCK_FREQ)) { dut // 这个测试更复杂需要干扰传输中的数据。 // 一种方法是直接实例化发送和接收模块并手动修改接收到的字节流。 // 由于篇幅这里仅描述思路 // 1. 驱动发送端发送一个已知数据包。 // 2. 在模拟接收过程中篡改其中一个数据字节例如在合适的时间点将 dut.io.rxd 强制拉高或拉低。 // 3. 检查接收端的 io.rxCrcError 是否变为高电平。 // 这需要精确的波特率时序控制是高级测试内容。 } } }测试要点第一个测试是“回环测试”将模块的发送端输出直接连到接收端输入验证整个链路在无错情况下能正常工作。第二个测试用于验证 CRC 检错能力需要模拟传输错误。这要求测试者能精确控制rxd输入信号在特定比特位上的值编写起来更复杂但对于验证 CRC 功能至关重要。由于 UART 波特率相对较慢仿真需要运行大量时钟周期例如发送一个字节在 50MHz 时钟下需要约 434 个周期。为了加速仿真测试中可以使用远高于实际值的“仿真波特率”或者使用 ChiselTest 的fork和join来并行驱动和监控信号。4.2 运行测试与结果分析在项目根目录下运行sbt test来执行测试。如果设计正确第一个测试应该能通过并且控制台会打印出接收到的数据。你需要观察发送忙信号txBusy是否正确地在发送期间拉高发送结束后拉低。接收有效信号rxDataValid是否在正确的时间点产生一个脉冲。接收到的数据rxDataOut是否与发送的数据txDataIn一致。在没有错误注入的情况下CRC 错误信号rxCrcError应始终保持为低。5. 常见问题、调试与最佳实践将 Chisel 代码转换为实际可用的 FPGA 比特流中间还会遇到许多问题。5.1 仿真通过但上板无输出问题现象可能原因检查方式处理建议程序编译综合成功下载到 FPGA 后串口无任何数据输出。1. 时钟频率参数错误。2. 复位信号未正确处理。3. UART 引脚约束错误。4. 波特率误差过大导致无法识别。1. 检查clockFreq参数是否与 FPGA 板载时钟一致。2. 检查顶层模块是否有未连接的复位信号确保系统在启动后能脱离复位状态。3. 检查约束文件 (.xdc 或 .qsf)确认txd、rxd引脚分配正确。4. 使用逻辑分析仪或示波器测量txd引脚波形看是否有符合波特率的信号。1. 确认系统时钟输入。在顶层模块可能需要对板载时钟进行分频或倍频以获得所需的工作时钟。2. 添加一个上电复位发生器确保所有寄存器被正确初始化。3. 仔细核对开发板原理图的引脚编号。4. 计算波特率分频器的实际分频系数看误差是否在 UART 可接受范围通常 3%。5.2 CRC 校验始终报错问题现象可能原因检查方式处理建议回环测试自发自收时rxCrcError信号总是为高。1. 发送端和接收端 CRC 计算顺序不一致LSB/MSB。2. CRC 初始值或最终异或值未对齐。3. 数据包长度计算错误。1. 打印发送端计算出的 CRC 值和接收端计算出的 CRC 值进行比对。2. 用软件 CRC 计算工具如在线 CRC 计算器计算同一数据的 CRC与硬件结果对比。3. 检查状态机确保计算 CRC 时包含了所有数据字节且没有多算或少算。1. 统一约定。UART 通常先发送 LSB。确保 CRC 模块也是按数据流的顺序即先接收/发送的位进行计算。可能需要调整bitIn的输入顺序。2. 确认 CRC 算法完全一致包括生成多项式、初始值本例为0、输入输出是否反转、结果是否与特定值异或等。3. 在发送和接收状态机中添加计数器确保字节数正确。5.3 资源占用与时序优化我们的设计是直接且未优化的。对于资源紧张或高速应用可以考虑以下优化并行 CRC 计算如果系统时钟频率不足以支持串行逐位计算在极高波特率下需要实现并行 CRC-8 计算即一个周期处理一个字节。这需要根据生成多项式推导出组合逻辑方程。状态机编码使用Enum生成的默认是二进制编码。对于小状态机没问题但也可以尝试使用OneHot编码可能对时序有帮助。跨时钟域处理如果 UART 时钟域与系统主时钟域不同需要在rxd输入处使用双寄存器同步器并且接收状态机可能需要使用异步 FIFO 来传递数据。封装为 BlackBox如果已有经过验证的 Verilog UART IP 核可以通过 Chisel 的BlackBox将其集成专注于 CRC 和协议逻辑。5.4 扩展方向支持可变长度数据包可以在顶层模块接口增加一个packetLen输入信号动态控制数据包大小。增加超时与错误重传在接收状态机中加入超时计数器如果在一定时间内未收齐数据包则复位状态。在 CRC 错误时可以通过反向通道如另一根 UART 线请求重传。多字节 CRC将 CRC-8 模块改为参数化模块支持 CRC-16 或 CRC-32只需改变多项式、寄存器宽度和反馈逻辑。集成到 SoC通过 AXI4-Stream 或 Wishbone 等总线接口将本模块封装为可被处理器访问的 IP使其能融入更大的片上系统。通过这个项目你不仅实现了一个带 CRC 校验的 UART 通讯模块更重要的是实践了如何使用 Chisel 进行层次化、模块化的数字系统设计。从协议分析、模块划分、具体实现到仿真验证这套流程适用于任何规模的硬件设计项目。在后续实践中可以尝试将文中简化的部分如 CRC 计算与字节流的精确同步补充完整这将是理解数据流控制的关键一步。
返回列表