VGA
简介
VGA接口正在成为一种“濒危”的技术,但实现VGA控制器仍然是一个很好的练习。
有关 VGA 协议的说明可以在 此处 找到。
本VGA控制器教程基于 此文件 实现。
数据结构
在实现控制器本体之前,我们需要定义一些数据结构。
RGB颜色
首先,我们需要一个三通道的颜色结构(红、绿、蓝)。该数据结构将用于向控制器提供像素,也将由 VGA 总线使用。
case class RgbConfig(rWidth : Int, gWidth : Int, bWidth : Int) {
def getWidth = rWidth + gWidth + bWidth
}
case class Rgb(c: RgbConfig) extends Bundle {
val r = UInt(c.rWidth bits)
val g = UInt(c.gWidth bits)
val b = UInt(c.bWidth bits)
}
VGA总线
io名称 |
驱动 |
描述 |
---|---|---|
vSync |
master |
垂直同步,表示新帧的开始 |
hSync |
master |
水平同步,表示新行的开始 |
colorEn |
master |
当位于界面可见部分时为高 |
color |
master |
携带颜色信息,当colorEn为低时无效 |
case class Vga(rgbConfig: RgbConfig) extends Bundle with IMasterSlave {
val vSync = Bool()
val hSync = Bool()
val colorEn = Bool()
val color = Rgb(rgbConfig)
override def asMaster() : Unit = this.asOutput()
}
此Vga Bundle
使用 IMasterSlave
特质,它允许您使用以下命令创建主/从 VGA 接口:
master(Vga(...))
slave(Vga(...))
VGA时序
VGA接口使用8种不同的时序进行驱动。以下是一个能够携带它们的 Bundle
的简单示例。
case class VgaTimings(timingsWidth: Int) extends Bundle {
val hSyncStart = UInt(timingsWidth bits)
val hSyncEnd = UInt(timingsWidth bits)
val hColorStart = UInt(timingsWidth bits)
val hColorEnd = UInt(timingsWidth bits)
val vSyncStart = UInt(timingsWidth bits)
val vSyncEnd = UInt(timingsWidth bits)
val vColorStart = UInt(timingsWidth bits)
val vColorEnd = UInt(timingsWidth bits)
}
但这并不是一种很好的指定方式,因为在垂直和水平时序方面存在冗余。
让我们写得更清晰一些:
case class VgaTimingsHV(timingsWidth: Int) extends Bundle {
val colorStart = UInt(timingsWidth bits)
val colorEnd = UInt(timingsWidth bits)
val syncStart = UInt(timingsWidth bits)
val syncEnd = UInt(timingsWidth bits)
}
case class VgaTimings(timingsWidth: Int) extends Bundle {
val h = VgaTimingsHV(timingsWidth)
val v = VgaTimingsHV(timingsWidth)
}
然后,我们可以添加一些函数来为特定分辨率和帧率设置这些时序参数:
case class VgaTimingsHV(timingsWidth: Int) extends Bundle {
val colorStart = UInt(timingsWidth bits)
val colorEnd = UInt(timingsWidth bits)
val syncStart = UInt(timingsWidth bits)
val syncEnd = UInt(timingsWidth bits)
}
case class VgaTimings(timingsWidth: Int) extends Bundle {
val h = VgaTimingsHV(timingsWidth)
val v = VgaTimingsHV(timingsWidth)
def setAs_h640_v480_r60(): Unit = {
h.syncStart := 96 - 1
h.syncEnd := 800 - 1
h.colorStart := 96 + 16 - 1
h.colorEnd := 800 - 48 - 1
v.syncStart := 2 - 1
v.syncEnd := 525 - 1
v.colorStart := 2 + 10 - 1
v.colorEnd := 525 - 33 - 1
}
def setAs_h64_v64_r60(): Unit = {
h.syncStart := 96 - 1
h.syncEnd := 800 - 1
h.colorStart := 96 + 16 - 1 + 288
h.colorEnd := 800 - 48 - 1 - 288
v.syncStart := 2 - 1
v.syncEnd := 525 - 1
v.colorStart := 2 + 10 - 1 + 208
v.colorEnd := 525 - 33 - 1 - 208
}
}
VGA控制器
规范
io名称 |
方向 |
描述 |
---|---|---|
softReset |
in |
复位内部计数器并保持 VGA 接口不激活 |
timings |
in |
指定 VGA 水平和垂直时序 |
pixels |
slave |
为VGA控制器提供RGB颜色流输入 |
error |
out |
当像素流太慢时为高 |
frameStart |
out |
新帧开始时为高电平 |
vga |
master |
VGA接口 |
该控制器不集成任何像素缓冲。它直接从 pixels
Stream
中获取像素,并在正确的时间将它们放在 vga.color
上。如果 pixels
无效,则 error
在一个周期内变高。
组件及io定义
让我们定义一个新的 VgaCtrl Component
,它将 RgbConfig
和 timingsWidth
作为参数。我们将位宽设置为默认值 12。
case class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component {
val io = new Bundle {
val softReset = in Bool()
val timings = in(VgaTimings(timingsWidth))
val pixels = slave Stream Rgb(rgbConfig)
val error = out Bool()
val frameStart = out Bool()
val vga = master(Vga(rgbConfig))
}
...
水平和垂直逻辑
The logic that generates horizontal and vertical synchronization signals is quite the same. It kind of resembles ~PWM~. The horizontal one counts up each cycle, while the vertical one use the horizontal synchronization signal as to increment.
Let’s define HVArea
, which represents one ~PWM~ and then instantiate it two times: one for both horizontal and vertical synchronization.
case class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component {
...
case class HVArea(timingsHV: VgaTimingsHV, enable: Bool) extends Area {
val counter = Reg(UInt(timingsWidth bits)) init 0
val syncStart = counter === timingsHV.syncStart
val syncEnd = counter === timingsHV.syncEnd
val colorStart = counter === timingsHV.colorStart
val colorEnd = counter === timingsHV.colorEnd
when(enable) {
counter := counter + 1
when(syncEnd) {
counter := 0
}
}
val sync = RegInit(False) setWhen syncStart clearWhen syncEnd
val colorEn = RegInit(False) setWhen colorStart clearWhen colorEnd
when(io.softReset) {
counter := 0
sync := False
colorEn := False
}
}
val h = HVArea(io.timings.h, True)
val v = HVArea(io.timings.v, h.syncEnd)
...
正如你所看到的,它是通过使用 Area
来完成的。这是为了避免创建一个新的 Component
,否则会变得冗长得多。
互连
现在我们有了水平和垂直同步的时序生成器,我们需要驱动输出。
case class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component {
...
val colorEn = h.colorEn && v.colorEn
io.pixels.ready := colorEn
io.error := colorEn && ! io.pixels.valid
io.frameStart := v.syncEnd
io.vga.hSync := h.sync
io.vga.vSync := v.sync
io.vga.colorEn := colorEn
io.vga.color := io.pixels.payload
...
额外奖励
上面定义的VgaCtrl是通用的(不特定于某个应用)。我们可以假设这样一种情况,系统提供RGB的 Fragment
流 Stream
,这意味着系统在图片开始/结束指示之间传输像素。
在这种情况下,我们可以通过在发生 error
时激活 softReset
输入以实现自动管理 softReset
,然后等待当前 pixels
图片结束以取消激活 error
。
让我们向 VgaCtrl
添加一个函数,可以从父组件调用该函数,以通过使用RGB的 Fragment
流 Stream
来提供数据给 VgaCtrl
。
case class VgaCtrl(rgbConfig: RgbConfig, timingsWidth: Int = 12) extends Component {
...
def feedWith(that : Stream[Fragment[Rgb]]): Unit = {
io.pixels << that.toStreamOfFragment
val error = RegInit(False)
when(io.error) {
error := True
}
when(that.isLast) {
error := False
}
io.softReset := error
when(error) {
that.ready := True
}
}
}