如果你正在学习 Chisel 硬件设计语言,可能会遇到一个关键问题:如何将抽象的硬件描述转化为实际可用的功能模块?特别是当涉及到像 UART 这样基础但至关重要的通信接口时,很多教程要么停留在理论层面,要么直接给出最终代码而缺乏完整的工程化视角。
这篇文章将带你完成一个完整的 Chisel UART 设计实战,重点不是简单实现功能,而是展示如何构建一个工程可用的 UART 模块。你将学会如何从协议分析开始,逐步设计发送器和接收器,处理常见的时序问题,最终在 FPGA 上验证整个通信链路。更重要的是,我们会讨论 Chisel 相比传统 Verilog 在模块化设计和验证方面的独特优势。
1. 为什么 Chisel + UART 是硬件设计入门的绝佳组合
UART(Universal Asynchronous Receiver/Transmitter)作为最基础的串行通信协议,几乎是每个嵌入式系统和 FPGA 项目的必备模块。但传统的 Verilog 实现往往面临几个痛点:状态机难以维护、参数化配置繁琐、验证代码冗长。这正是 Chisel 发挥价值的地方。
Chisel 通过高级语言特性让硬件设计更接近软件开发体验。比如,你可以用 Scala 的面向对象特性构建可复用的 UART 组件,用函数式编程简化状态机设计,用类型系统在编译期捕获硬件描述错误。更重要的是,Chisel 的测试框架让验证工作变得系统化。
在实际项目中,一个设计良好的 UART 模块需要解决几个关键问题:波特率自适应、帧错误处理、流量控制支持。我们将从最简单的版本开始,逐步添加这些工程化特性。
2. UART 协议核心要点与设计目标
2.1 UART 帧格式详解
UART 通信的基本单位是"帧",每帧包含:
- 1个起始位(低电平)
- 5-9个数据位(通常为8位)
- 可选的奇偶校验位
- 1-2个停止位(高电平)
以最常见的 8N1 配置(8数据位、无校验、1停止位)为例,一帧数据的时序如下:
TEXT
1
空闲状态(高电平) → 起始位(低电平) → D0 → D1 → ... → D7 → 停止位(高电平)
2.2 波特率与采样时机
波特率决定了数据传输速度,如 115200 表示每秒传输 115200 位。关键设计挑战在于接收端需要在每个数据位的中间点采样,以避免边沿抖动带来的误码。
计算采样点的方法:
SCALA
1
// 假设系统时钟频率为 100MHz,波特率为 115200
2
val clockFreq = 100000000 // 100 MHz
4
val cyclesPerBit = clockFreq / baudRate // 约 868 个时钟周期每比特
5
val samplePoint = cyclesPerBit / 2 // 在比特中间采样
2.3 设计目标分解
我们的 Chisel UART 模块需要实现以下功能:
- 可配置的波特率(支持常见值:9600, 115200 等)
- 可配置的数据位宽(5-9位)
- 完整的错误检测(帧错误、溢出错误)
- 发送和接收缓冲机制
- 易于集成的 Wishbone 或 AXI 接口
3. 环境准备与项目结构规划
3.1 开发环境要求
在开始编码前,需要准备以下环境:
- Scala 2.12+ 和 sbt 构建工具
- Chisel 3.5+
- FPGA 开发工具(Vivado 或 Quartus)
- 串口调试工具(如 minicom、putty)
3.2 项目目录结构
一个规范的 Chisel 项目应该这样组织:
TEXT
3
├── src/main/scala/uart/
4
│ ├── UARTParams.scala # 参数配置类
5
│ ├── UARTTx.scala # 发送器模块
6
│ ├── UARTRx.scala # 接收器模块
7
│ ├── UART.scala # 顶层模块
8
│ └── UARTTop.scala # 测试顶层模块
9
├── src/test/scala/uart/
10
│ └── UARTUnitTests.scala # 单元测试
11
└── generated/ # Chisel 生成的 Verilog
3.3 build.sbt 基础配置
SCALA
4
scalaVersion := "2.12.13"
6
val chiselVersion = "3.5.4"
8
libraryDependencies ++= Seq(
9
"edu.berkeley.cs" %% "chisel3" % chiselVersion,
10
"edu.berkeley.cs" %% "chiseltest" % "0.5.4" % "test"
13
addCompilerPlugin("edu.berkeley.cs" % "chisel3-plugin" % chiselVersion cross CrossVersion.full)
4. UART 参数化配置设计
在 Chisel 中,参数化配置是提升代码复用性的关键。我们首先设计一个配置类:
SCALA
3
dataBits: Int = 8, // 数据位宽度
4
stopBits: Int = 1, // 停止位数量
5
hasParity: Boolean = false, // 是否使能奇偶校验
6
oddParity: Boolean = false, // 奇校验(true)还是偶校验(false)
7
baudRate: Int = 115200, // 波特率
8
clockFreq: Int = 100000000 // 系统时钟频率(100MHz)
10
require(dataBits >= 5 && dataBits <= 9, "数据位宽必须在5-9之间")
11
require(stopBits == 1 || stopBits == 2, "停止位只能是1或2")
14
val cyclesPerBit: Int = clockFreq / baudRate
15
require(cyclesPerBit > 0, "波特率不能超过系统时钟频率")
这种设计允许我们在实例化 UART 时灵活配置参数:
SCALA
2
val standardParams = UARTParams(baudRate = 115200)
5
val lowSpeedParams = UARTParams(baudRate = 9600, dataBits = 7, hasParity = true)
5. UART 发送器 (UARTTx) 详细实现
发送器负责将并行数据转换为符合 UART 协议的串行数据流。核心是一个状态机,控制数据帧的各个阶段。
5.1 发送器接口定义
SCALA
2
class UARTTx(params: UARTParams) extends Module {
3
val io = IO(new Bundle {
4
val txd = Output(Bool()) // 串行数据输出
5
val valid = Input(Bool()) // 数据有效信号
6
val bits = Input(UInt(params.dataBits.W)) // 待发送数据
7
val ready = Output(Bool()) // 发送器就绪信号
11
val sIdle :: sStart :: sData :: sParity :: sStop :: Nil = Enum(5)
12
val state = RegInit(sIdle)
15
val bitCount = RegInit(0.U(log2Ceil(params.dataBits).W))
17
val cycleCount = RegInit(0.U(log2Ceil(params.cyclesPerBit + 1).W))
19
val shifter = RegInit(0.U(params.dataBits.W))
21
val parityBit = if (params.hasParity) {
22
val dataParity = Mux(params.oddParity,
23
~(io.bits.xorR()).asBool(),
24
io.bits.xorR().asBool())
5.2 发送器状态机实现
SCALA
2
io.ready := (state === sIdle)
6
io.txd := true.B // 空闲状态为高电平
12
if (params.hasParity) {
13
parityBit := Mux(params.oddParity,
14
~(io.bits.xorR()).asBool(),
15
io.bits.xorR().asBool())
21
io.txd := false.B // 起始位
22
when(cycleCount === (params.cyclesPerBit - 1).U) {
27
cycleCount := cycleCount + 1.U
34
when(cycleCount === (params.cyclesPerBit - 1).U) {
36
shifter := shifter >> 1
37
when(bitCount === (params.dataBits - 1).U) {
39
if (params.hasParity) {
45
bitCount := bitCount + 1.U
48
cycleCount := cycleCount + 1.U
54
when(cycleCount === (params.cyclesPerBit - 1).U) {
58
cycleCount := cycleCount + 1.U
63
io.txd := true.B // 停止位
64
when(cycleCount === (params.cyclesPerBit * params.stopBits - 1).U) {
68
cycleCount := cycleCount + 1.U
这个状态机完整实现了 UART 发送的所有阶段,关键设计点包括:
- 使用
cycleCount 精确控制每个比特的持续时间
- 支持可配置的停止位数量
- 灵活的奇偶校验生成逻辑
6. UART 接收器 (UARTRx) 详细实现
接收器设计比发送器更复杂,需要处理异步信号同步和精确的采样时机判断。
6.1 接收器接口与亚稳态处理
SCALA
2
class UARTRx(params: UARTParams) extends Module {
3
val io = IO(new Bundle {
4
val rxd = Input(Bool()) // 串行数据输入
5
val valid = Output(Bool()) // 数据接收完成信号
6
val bits = Output(UInt(params.dataBits.W)) // 接收到的数据
7
val error = Output(Bool()) // 帧错误信号
8
val ready = Input(Bool()) // 数据已读取确认
12
val rxdSync = RegNext(RegNext(io.rxd))
15
val sIdle :: sStart :: sData :: sParity :: sStop :: Nil = Enum(5)
16
val state = RegInit(sIdle)
19
val rxdPrev = RegNext(rxdSync)
20
val startEdge = !rxdSync && rxdPrev
6.2 接收器状态机与多数表决采样
为了抗干扰,我们在每个数据位的中间点附近进行多次采样,采用多数表决机制:
SCALA
3
val sampleCount = RegInit(0.U(2.W))
4
val sampleBits = Reg(Vec(3, Bool()))
13
cycleCount := (params.cyclesPerBit / 2).U // 起始位中间点开始采样
19
when(cycleCount === (params.cyclesPerBit - 1).U) {
21
val startBitValid = !sampleBits.asUInt().orR() // 多数表决应该是0
28
state := sIdle // 起始位无效,返回空闲
32
when(cycleCount === (params.cyclesPerBit / 2 - 1).U ||
33
cycleCount === (params.cyclesPerBit / 2).U ||
34
cycleCount === (params.cyclesPerBit / 2 + 1).U) {
35
sampleBits(sampleCount) := rxdSync
36
sampleCount := sampleCount + 1.U
38
cycleCount := cycleCount + 1.U
43
when(cycleCount === (params.cyclesPerBit - 1).U) {
45
val bitValue = sampleBits.asUInt().popCount() > 1.U
46
shifter := Cat(bitValue, shifter(params.dataBits-1, 1))
49
when(bitCount === (params.dataBits - 1).U) {
51
if (params.hasParity) {
57
bitCount := bitCount + 1.U
62
when(cycleCount === (params.cyclesPerBit / 2 - 1).U ||
63
cycleCount === (params.cyclesPerBit / 2).U ||
64
cycleCount === (params.cyclesPerBit / 2 + 1).U) {
65
sampleBits(sampleCount) := rxdSync
66
sampleCount := sampleCount + 1.U
68
cycleCount := cycleCount + 1.U
接收器的关键设计特点:
- 两级同步器消除亚稳态
- 多数表决采样提高抗干扰能力
- 精确的采样时机控制
- 完整的错误检测机制
7. 顶层 UART 模块集成
将发送器和接收器组合成完整的 UART 模块,并添加缓冲机制:
SCALA
2
class UART(params: UARTParams) extends Module {
3
val io = IO(new Bundle {
5
val txd = Output(Bool())
6
val rxd = Input(Bool())
9
val txValid = Input(Bool())
10
val txData = Input(UInt(params.dataBits.W))
11
val txReady = Output(Bool())
13
val rxValid = Output(Bool())
14
val rxData = Output(UInt(params.dataBits.W))
15
val rxError = Output(Bool())
16
val rxReady = Input(Bool())
19
val tx = Module(new UARTTx(params))
20
val rx = Module(new UARTRx(params))
24
tx.io.valid := io.txValid
25
tx.io.bits := io.txData
26
io.txReady := tx.io.ready
30
io.rxValid := rx.io.valid
31
io.rxData := rx.io.bits
32
io.rxError := rx.io.error
33
rx.io.ready := io.rxReady
36
val loopback = RegInit(false.B)
38
rx.io.rxd := tx.io.txd
8. ChiselTest 单元测试实现
Chisel 的强大测试框架让我们能够构建全面的验证环境:
SCALA
2
class UARTUnitTests extends AnyFlatSpec with ChiselScalatestTester {
3
"UART Tx" should "correctly transmit data" in {
4
test(new UARTTx(UARTParams(baudRate = 115200, clockFreq = 100000000))) { dut =>
6
dut.io.valid.poke(false.B)
9
// 发送测试数据 0x55 (01010101)
10
dut.io.bits.poke(0x55.U)
11
dut.io.valid.poke(true.B)
14
while (dut.io.ready.peek().litToBoolean) {
17
dut.io.valid.poke(false.B)
20
dut.clock.step(434) // 半个比特周期
21
dut.io.txd.expect(false.B) // 应该是起始位
24
val expectedBits = "01010101" // LSB first
25
for (i <- 0 until 8) {
26
dut.clock.step(868) // 一个完整的比特周期
27
val expectedBit = expectedBits(i) == '1'
28
dut.io.txd.expect(expectedBit.B)
33
dut.io.txd.expect(true.B)
37
"UART Rx" should "correctly receive data with noise" in {
38
test(new UARTRx(UARTParams(baudRate = 115200, clockFreq = 100000000))) { dut =>
40
def sendBitWithNoise(bit: Boolean, noise: List[Int] = Nil): Unit = {
41
val baseValue = if (bit) 1 else 0
42
for (cycle <- 0 until 868) {
43
val value = if (noise.contains(cycle)) !bit else bit
44
dut.io.rxd.poke(value.B)
49
// 发送起始位 + 数据 0xAA + 停止位
50
sendBitWithNoise(false) // 起始位
51
sendBitWithNoise(false, List(100, 200, 300)) // D0 with noise
52
sendBitWithNoise(true, List(150)) // D1 with noise
54
sendBitWithNoise(true) // 停止位
57
dut.io.valid.expect(true.B)
58
dut.io.bits.expect(0xAA.U)
59
dut.io.error.expect(false.B) // 多数表决应该能纠正噪声
9. FPGA 实测与调试技巧
9.1 引脚约束与时钟管理
在 FPGA 项目中,正确的引脚约束至关重要:
TCL
2
set_property PACKAGE_PIN AB12 [get_ports {txd}]
3
set_property IOSTANDARD LVCMOS33 [get_ports {txd}]
4
set_property PACKAGE_PIN AB13 [get_ports {rxd}]
5
set_property IOSTANDARD LVCMOS33 [get_ports {rxd}]
8
create_clock -name sys_clk -period 10.000 [get_ports clk]
9.2 在线调试技巧
使用 FPGA 的 ILA(集成逻辑分析仪)进行调试:
VERILOG
4
.probe0(tx_state), // 发送器状态
5
.probe1(rx_state), // 接收器状态
8
.probe4(tx_valid), // 发送有效信号
9
.probe5(rx_valid) // 接收有效信号
9.3 常见问题排查指南
| 问题现象 |
可能原因 |
排查方法 |
| 发送数据正确但接收不到 |
波特率不匹配 |
检查时钟频率和分频系数 |
| 接收数据出现帧错误 |
采样点偏移 |
调整采样点位置,检查时钟稳定性 |
| 随机误码 |
信号完整性问题 |
添加施密特触发器,检查PCB布局 |
| 只能单向通信 |
流控信号配置错误 |
检查RTS/CTS连接状态 |
10. 性能优化与扩展功能
10.1 FIFO 缓冲集成
为提升吞吐量,可以添加 FIFO 缓冲:
SCALA
1
class UARTWithFIFO(params: UARTParams, fifoDepth: Int = 16) extends Module {
2
val txFIFO = Module(new Queue(UInt(params.dataBits.W), fifoDepth))
3
val rxFIFO = Module(new Queue(UInt(params.dataBits.W), fifoDepth))
6
txFIFO.io.enq <> io.txInterface
7
uart.io.txValid := txFIFO.io.deq.valid
8
uart.io.txData := txFIFO.io.deq.bits
9
txFIFO.io.deq.ready := uart.io.txReady
11
uart.io.rxReady := rxFIFO.io.enq.ready
12
rxFIFO.io.enq.valid := uart.io.rxValid
13
rxFIFO.io.enq.bits := uart.io.rxData
10.2 自动波特率检测
高级功能:实现自动波特率检测:
SCALA
1
class AutoBaudDetector extends Module {
3
def detectBaudRate(startPulseWidth: UInt): UInt = {
5
val clockCycles = startPulseWidth * 2 // 起始位是半个比特周期
6
val calculatedBaud = io.clockFreq / clockCycles
这个完整的 Chisel UART 实现展示了硬件设计语言在现代数字设计中的优势。通过参数化配置、强大的测试框架和模块化设计,我们构建了一个工程可用的通信核心。相比传统 Verilog,Chisel 让硬件设计更接近软件开发体验,同时保持了生成代码的性能和效率。
实际项目中,你可以基于这个基础框架继续扩展,比如添加 DMA 支持、多通道配置、或者更高级的错误纠正功能。这种设计方法也适用于其他通信协议如 SPI、I2C 的实现。