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 ,它将 RgbConfigtimingsWidth 作为参数。我们将位宽设置为默认值 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的 FragmentStream ,这意味着系统在图片开始/结束指示之间传输像素。

在这种情况下,我们可以通过在发生 error 时激活 softReset 输入以实现自动管理 softReset ,然后等待当前 pixels 图片结束以取消激活 error

让我们向 VgaCtrl 添加一个函数,可以从父组件调用该函数,以通过使用RGB的 FragmentStream 来提供数据给 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
    }
  }
}