cores/video: new VideoTextGrid component

The VideoTextGrid is similar to the VideoTerminal, but instead of
receiving a stream of characters from an attached UART, it exposes
a csr (YXAC) to allow direct access to the character grid.  Writes
to this register specify the Y coordinate, X coordinate, Attribute,
and Character, encoded in a 32bit value and result in a write to
the character/attribute memory.

The attribute memory currently encodes 3bit RGB foreground and
background colors.  The remaining 2 bits might be useful for
an intensity bit (doubling the colorspace) or some other
feature like h/v flip.

Two additional registers (SCROLLX, SCROLLY) allow the underlying
128x64 character grid to be scrolled within the 80x40 visible
viewport.

Some trickery with the ClockDomainsRenamer was necessary to keep
the register facing side of the text/attr memory.  The "sys" domain
gets renamed to "hdmi" typically, so we put the write side of the
text/attr memory in the "csr" domain and rename that to "sys"
at the same time the "sys" domain is renamed:

vt = ClockDomainsRenamer({ "sys": clock_domain, "csr": "sys" })(vt)
This commit is contained in:
Brian Swetland 2022-04-27 16:18:50 -07:00
parent 665367fe67
commit 2eb4287325
2 changed files with 126 additions and 0 deletions

View file

@ -613,6 +613,106 @@ class VideoTerminal(Module):
) )
] ]
# Video TextGrid --------------------------------------------------------------------------------
class VideoTextGrid(Module, AutoCSR):
def __init__(self, hres=800, vres=600):
self.enable = Signal(reset=1)
self.vtg_sink = vtg_sink = stream.Endpoint(video_timing_layout)
self.source = source = stream.Endpoint(video_data_layout)
# Font Mem.
# ---------
os.system("wget https://github.com/enjoy-digital/litex/files/6076336/ter-u16b.txt") # FIXME: Store Font in LiteX?
os.system("mv ter-u16b.txt ter-u16b.bdf")
font = import_bdf_font("ter-u16b.bdf")
font_width = 8
font_heigth = 16
font_mem = Memory(width=8, depth=4096, init=font)
font_rdport = font_mem.get_port(has_re=True)
self.specials += font_mem, font_rdport
# TextGrid Mem.
# -------------
term_colums_vis = 80
term_lines_vis = 40
term_colums = 128 # 80 rounded to next power of two.
term_lines = 64 # 40 rounded to the next power of two.
attr_width = 8
term_depth = term_colums * term_lines
term_init = [ord(c) for c in [" "]*term_colums*term_lines]
term_mem = Memory(width=font_width + attr_width, depth=term_depth, init=term_init)
term_wrport = term_mem.get_port(write_capable=True, clock_domain="csr")
term_rdport = term_mem.get_port(has_re=True)
self.specials += term_mem, term_wrport, term_rdport
# Registers
# ---------
self._yxac = yxac = CSR(size=32)
self._xscroll = xscroll = CSRStorage(size=7)
self._yscroll = yscroll = CSRStorage(size=6)
# yxac (Y,X,ATTR,CHAR) writes into textgrid memory
self.comb += term_wrport.we.eq(yxac.re)
self.comb += term_wrport.dat_w.eq(yxac.r[0:15])
self.comb += term_wrport.adr[:7].eq(yxac.r[16:23])
self.comb += term_wrport.adr[7:].eq(yxac.r[24:31])
# Video Generation.
# -----------------
ce = (vtg_sink.valid & vtg_sink.ready)
# Timing delay line.
latency = 2
timing_bufs = [stream.Buffer(video_timing_layout) for i in range(latency)]
self.comb += vtg_sink.connect(timing_bufs[0].sink)
for i in range(len(timing_bufs) - 1):
self.comb += timing_bufs[i].source.connect(timing_bufs[i+1].sink)
self.comb += timing_bufs[-1].source.connect(source, keep={"valid", "ready", "last", "de", "hsync", "vsync"})
self.submodules += timing_bufs
# Compute X/Y position.
x = vtg_sink.hcount[int(math.log2(font_width)):]
y = vtg_sink.vcount[int(math.log2(font_heigth)):]
# Get character from Terminal Mem.
term_dat_r = Signal(font_width)
self.comb += term_rdport.re.eq(ce)
self.comb += term_rdport.adr[:7].eq(x + xscroll.storage)
self.comb += term_rdport.adr[7:].eq(y + yscroll.storage)
self.comb += [
term_dat_r.eq(term_rdport.dat_r[:font_width]),
If((x >= term_colums_vis) | (y >= term_lines_vis),
term_dat_r.eq(ord(" ")), # Out of range, generate space.
)
]
# Translate character to video data through Font Mem.
self.comb += font_rdport.re.eq(ce)
self.comb += font_rdport.adr.eq(term_dat_r*font_heigth + timing_bufs[0].source.vcount[:4])
bit = Signal()
cases = {}
for i in range(font_width):
cases[i] = [bit.eq(font_rdport.dat_r[font_width-1-i])]
self.comb += Case(timing_bufs[1].source.hcount[:int(math.log2(font_width))], cases)
# extract fg and bg colors from attribute
br = term_rdport.dat_r[14]
bg = term_rdport.dat_r[13]
bb = term_rdport.dat_r[12]
fr = term_rdport.dat_r[10]
fg = term_rdport.dat_r[9]
fb = term_rdport.dat_r[8]
self.comb += [
If(bit,
Cat(source.r, source.g, source.b).eq(
Cat(Replicate(fr,8),Replicate(fg,8),Replicate(fb,8)))
).Else(
Cat(source.r, source.g, source.b).eq(
Cat(Replicate(br,8),Replicate(bg,8),Replicate(bb,8)))
)
]
# Video FrameBuffer -------------------------------------------------------------------------------- # Video FrameBuffer --------------------------------------------------------------------------------
class VideoFrameBuffer(Module, AutoCSR): class VideoFrameBuffer(Module, AutoCSR):

View file

@ -1940,6 +1940,32 @@ class LiteXSoC(SoC):
# Connect Video Terminal to Video PHY. # Connect Video Terminal to Video PHY.
self.comb += vt.source.connect(phy if isinstance(phy, stream.Endpoint) else phy.sink) self.comb += vt.source.connect(phy if isinstance(phy, stream.Endpoint) else phy.sink)
# Add Video TextGrid ---------------------------------------------------------------------------
def add_video_textgrid(self, name="video_textgrid", phy=None, timings="800x600@60Hz", clock_domain="sys"):
# Imports.
from litex.soc.cores.video import VideoTimingGenerator, VideoTextGrid
# Video Timing Generator.
self.check_if_exists(f"{name}_vtg")
vtg = VideoTimingGenerator(default_video_timings=timings if isinstance(timings, str) else timings[1])
vtg = ClockDomainsRenamer(clock_domain)(vtg)
setattr(self.submodules, f"{name}_vtg", vtg)
# Video Terminal.
timings = timings if isinstance(timings, str) else timings[0]
vt = VideoTextGrid(
hres = int(timings.split("@")[0].split("x")[0]),
vres = int(timings.split("@")[0].split("x")[1]),
)
vt = ClockDomainsRenamer({ "sys": clock_domain, "csr": "sys" })(vt)
setattr(self.submodules, name, vt)
# Connect Video Timing Generator to Video Terminal.
self.comb += vtg.source.connect(vt.vtg_sink)
# Connect Video Terminal to Video PHY.
self.comb += vt.source.connect(phy if isinstance(phy, stream.Endpoint) else phy.sink)
# Add Video Framebuffer ------------------------------------------------------------------------ # Add Video Framebuffer ------------------------------------------------------------------------
def add_video_framebuffer(self, name="video_framebuffer", phy=None, timings="800x600@60Hz", clock_domain="sys", format="rgb888"): def add_video_framebuffer(self, name="video_framebuffer", phy=None, timings="800x600@60Hz", clock_domain="sys", format="rgb888"):
# Imports. # Imports.