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
    }
  }
}