Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/manual/src/applets/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ Applet index
sensor/index
bridge/index
audio/index
video/index
internal/index
11 changes: 11 additions & 0 deletions docs/manual/src/applets/video/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.. _applet.video:

Video capture and output
========================

.. automodule:: glasgow.applet.video

.. toctree::
:maxdepth: 3

ws2812_output
18 changes: 18 additions & 0 deletions docs/manual/src/applets/video/ws2812_output.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
``video-ws2812-output``
=======================

CLI reference
-------------

.. _applet.video.ws2812_output:

.. autoprogram:: glasgow.applet.video.ws2812_output:VideoWS2812OutputApplet._get_argparser_for_sphinx("video-ws2812-output")
:prog: glasgow run video-ws2812-output


API reference
-------------

.. module:: glasgow.applet.video.ws2812_output

.. autoclass:: VideoWS2812OutputInterface
233 changes: 154 additions & 79 deletions software/glasgow/applet/video/ws2812_output/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
from dataclasses import dataclass
import logging
import asyncio
from amaranth import *
from amaranth.lib import io
import typing as t

from ....support.endpoint import *
from ....gateware.pll import *
from ... import *
from amaranth import *
from amaranth.lib import io, wiring, stream
from amaranth.lib.wiring import In

from glasgow.abstract import AbstractAssembly, ClockDivisor, GlasgowPin, PortGroup
from glasgow.applet import GlasgowAppletV2
from glasgow.support.endpoint import *
from glasgow.gateware.pll import *


__all__ = [
"VideoWS2812PixelFormat",
"VIDEO_WS2812_PIXEL_FORMATS",
"VideoWS2812OutputComponent",
"VideoWS2812OutputInterface",
]


@dataclass(frozen=True)
class VideoWS2812PixelFormat:
in_size: int
out_size: int
format_func: t.Callable


VIDEO_WS2812_PIXEL_FORMATS = {
"RGB-BRG": VideoWS2812PixelFormat(
in_size=3, out_size=3, format_func=lambda r, g, b: Cat(b, r, g)
),
"RGB-BGR": VideoWS2812PixelFormat(
in_size=3, out_size=3, format_func=lambda r, g, b: Cat(b, g, r)
),
"RGB-xBRG": VideoWS2812PixelFormat(
in_size=3, out_size=4, format_func=lambda r, g, b: Cat(Const(0, unsigned(8)), b, r, g)
),
"RGBW-WBRG": VideoWS2812PixelFormat(
in_size=4, out_size=4, format_func=lambda r, g, b, w: Cat(w, b, r, g)
),
}


class VideoWS2812Output(Elaboratable):
def __init__(self, ports):
def __init__(self, ports: PortGroup):
self.ports = ports
self.out = Signal(len(ports.out))

Expand All @@ -22,14 +57,16 @@ def elaborate(self, platform):
return m


class VideoWS2812OutputSubtarget(Elaboratable):
def __init__(self, ports, count, pix_in_size, pix_out_size, pix_format_func, out_fifo):
self.ports = ports
self.count = count
self.pix_in_size = pix_in_size
self.pix_out_size = pix_out_size
self.pix_format_func = pix_format_func
self.out_fifo = out_fifo
class VideoWS2812OutputComponent(wiring.Component):
i_stream: In(stream.Signature(8))
framerate_divisor: In(24)

def __init__(self, ports: PortGroup, count: int, pixel_format: VideoWS2812PixelFormat):
self.ports = ports
self.count = count
self.pixel_format = pixel_format

super().__init__()

def elaborate(self, platform):
# Safe timings:
Expand All @@ -50,33 +87,37 @@ def elaborate(self, platform):

m.submodules.output = output = VideoWS2812Output(self.ports)

pix_in_size = self.pix_in_size
pix_out_size = self.pix_out_size
pix_in_size = self.pixel_format.in_size
pix_out_size = self.pixel_format.out_size
pix_out_bpp = pix_out_size * 8

cyc_ctr = Signal(range(t_reset+1))
bit_ctr = Signal(range(pix_out_bpp+1))
byt_ctr = Signal(range((pix_in_size)+1))
pix_ctr = Signal(range(self.count+1))
cyc_ctr = Signal(range(t_reset + 1))
bit_ctr = Signal(range(pix_out_bpp + 1))
byt_ctr = Signal(range((pix_in_size) + 1))
pix_ctr = Signal(range(self.count + 1))
word_ctr = Signal(range(max(2, len(self.ports.out))))
framerate_ctr = Signal(self.framerate_divisor.shape())

pix = Array([ Signal(8) for i in range((pix_in_size) - 1) ])
pix = Array([Signal(8) for i in range((pix_in_size) - 1)])
word = Signal(pix_out_bpp * len(self.ports.out))

with m.If(framerate_ctr + 1 != 0):
m.d.sync += framerate_ctr.eq(framerate_ctr + 1)

with m.FSM():
with m.State("LOAD"):
m.d.comb += [
self.out_fifo.r_en.eq(1),
self.i_stream.ready.eq(1),
output.out.eq(0),
]
with m.If(self.out_fifo.r_rdy):
with m.If(self.i_stream.valid):
with m.If(byt_ctr < ((pix_in_size) - 1)):
m.d.sync += [
pix[byt_ctr].eq(self.out_fifo.r_data),
pix[byt_ctr].eq(self.i_stream.payload),
byt_ctr.eq(byt_ctr + 1),
]
with m.Else():
p = self.pix_format_func(*pix, self.out_fifo.r_data)
p = self.pixel_format.format_func(*pix, self.i_stream.payload)
m.d.sync += word.eq(Cat(word[pix_out_bpp:], p))
with m.If(word_ctr < (len(self.ports.out) - 1)):
m.d.sync += [
Expand All @@ -91,8 +132,10 @@ def elaborate(self, platform):
m.d.comb += output.out.eq((1 << len(self.ports.out)) - 1)
m.d.sync += cyc_ctr.eq(cyc_ctr + 1)
with m.Elif(cyc_ctr < t_one):
m.d.comb += (o.eq(word[(pix_out_bpp - 1) + (pix_out_bpp * i)])
for i,o in enumerate(output.out))
m.d.comb += (
o.eq(word[(pix_out_bpp - 1) + (pix_out_bpp * i)])
for i, o in enumerate(output.out)
)
m.d.sync += cyc_ctr.eq(cyc_ctr + 1)
with m.Elif(cyc_ctr < t_period):
m.d.comb += output.out.eq(0)
Expand Down Expand Up @@ -121,96 +164,128 @@ def elaborate(self, platform):

with m.State("RESET"):
m.d.comb += output.out.eq(0)
m.d.sync += cyc_ctr.eq(cyc_ctr + 1)
with m.If(cyc_ctr == t_reset):
with m.If(cyc_ctr + 1 != 0):
m.d.sync += cyc_ctr.eq(cyc_ctr + 1)
with m.If((cyc_ctr >= t_reset) & (framerate_ctr >= self.framerate_divisor)):
m.d.sync += [
cyc_ctr.eq(0),
pix_ctr.eq(0),
bit_ctr.eq(0),
byt_ctr.eq(0),
word_ctr.eq(0),
framerate_ctr.eq(0),
]
m.next = "LOAD"

return m


class VideoWS2812OutputApplet(GlasgowApplet):
class VideoWS2812OutputInterface:
def __init__(
self,
logger: logging.Logger,
assembly: AbstractAssembly,
*,
out: tuple[GlasgowPin],
count: int,
pixel_format: VideoWS2812PixelFormat,
buffer: int,
):
self._logger = logger
self._frame_size = len(out) * pixel_format.in_size * count
ports = assembly.add_port_group(out=out)
component = assembly.add_submodule(VideoWS2812OutputComponent(ports, count, pixel_format))
self._pipe = assembly.add_out_pipe(
component.i_stream, buffer_size=self._frame_size * buffer
)
self._framerate = assembly.add_clock_divisor(
component.framerate_divisor, ref_period=assembly.sys_clk_period, name="framerate"
)

async def write_frame(self, data):
"""Send one or more frame's worth of pixel data to the LED string."""
assert len(data) % self._frame_size == 0
await self._pipe.send(data)
await self._pipe.flush(_wait=False)

@property
def frame_size(self) -> int:
"""Size of each frame in bytes."""
return self._frame_size

@property
def framerate_limiter(self) -> ClockDivisor:
"""Framerate limiter."""
return self._framerate


class VideoWS2812OutputApplet(GlasgowAppletV2):
logger = logging.getLogger(__name__)
help = "display video via WS2812 LEDs"
description = """
Output RGB(W) frames from a socket to one or more WS2812(B) LED strings.
"""

pixel_formats = {
# in-out in size out size format_func
"RGB-BRG": ( 3, 3, lambda r,g,b: Cat(b,r,g) ),
"RGB-xBRG": ( 3, 4, lambda r,g,b: Cat(Const(0, unsigned(8)),b,r,g) ),
"RGBW-WBRG": ( 4, 4, lambda r,g,b,w: Cat(w,b,r,g) ),
}

@classmethod
def add_build_arguments(cls, parser, access):
super().add_build_arguments(parser, access)

access.add_voltage_argument(parser)
access.add_pins_argument(parser, "out", width=range(1, 17), required=True)
parser.add_argument(
"-c", "--count", metavar="N", type=int, required=True,
help="set the number of LEDs per string")
parser.add_argument(
"-f", "--pix-fmt", metavar="F", choices=cls.pixel_formats.keys(), default="RGB-BRG",
help="set the pixel format (one of: %(choices)s, default: %(default)s)")

def build(self, target, args):
self.pix_in_size, pix_out_size, pix_format_func = self.pixel_formats[args.pix_fmt]

self.mux_interface = iface = target.multiplexer.claim_interface(self, args)
subtarget = iface.add_subtarget(VideoWS2812OutputSubtarget(
ports=iface.get_port_group(out=args.out),
count=args.count,
pix_in_size=self.pix_in_size,
pix_out_size=pix_out_size,
pix_format_func=pix_format_func,
out_fifo=iface.get_out_fifo(),
))
"-f", "--pix-fmt", metavar="F", choices=VIDEO_WS2812_PIXEL_FORMATS.keys(),
default="RGB-BRG", help="set the pixel format (default: %(default)s)")
parser.add_argument(
"-b", "--buffer", metavar="N", type=int, default=16,
help="set the number of frames to buffer internally (buffered twice)")

return subtarget
def build(self, args):
with self.assembly.add_applet(self):
self.assembly.use_voltage(args.voltage)
self.ws2812_iface = VideoWS2812OutputInterface(
self.logger,
self.assembly,
out=args.out,
count=args.count,
pixel_format=VIDEO_WS2812_PIXEL_FORMATS[args.pix_fmt],
buffer=args.buffer,
)

@classmethod
def add_run_arguments(cls, parser, access):
super().add_run_arguments(parser, access)

def add_setup_arguments(cls, parser):
parser.add_argument(
"-b", "--buffer", metavar="N", type=int, default=16,
help="set the number of frames to buffer internally (buffered twice)")
"-r", "--framerate", type=float,
help="configure a framerate limiter in Hz")

async def run(self, device, args):
buffer_size = len(args.out) * args.count * self.pix_in_size * args.buffer
return await device.demultiplexer.claim_interface(self, self.mux_interface, args,
write_buffer_size=buffer_size)
async def setup(self, args):
if args.framerate is not None:
await self.ws2812_iface.framerate_limiter.set_frequency(args.framerate)

@classmethod
def add_interact_arguments(cls, parser):
def add_run_arguments(cls, parser):
ServerEndpoint.add_argument(parser, "endpoint")

async def interact(self, device, args, leds):
frame_size = len(args.out) * args.count * self.pix_in_size
buffer_size = frame_size * args.buffer
endpoint = await ServerEndpoint("socket", self.logger, args.endpoint,
queue_size=buffer_size, deprecated_cancel_on_eof=True)
async def run(self, args):
# This buffer is for the socket only, and is independet from the one
# configured in VideoWS2812OutputInterface
buffer_size = self.ws2812_iface.frame_size * args.buffer
endpoint = await ServerEndpoint(
"socket",
self.logger,
args.endpoint,
queue_size=buffer_size,
)
while True:
try:
data = await asyncio.shield(endpoint.recv(buffer_size))
partial = len(data) % frame_size
while partial:
data += await asyncio.shield(endpoint.recv(frame_size - partial))
partial = len(data) % frame_size
await leds.write(data)
await leds.flush(wait=False)
except asyncio.CancelledError:
await self.ws2812_iface.write_frame(
await endpoint.recv(self.ws2812_iface.frame_size)
)
except EOFError:
pass

@classmethod
def tests(cls):
from . import test

return test.VideoWS2812OutputAppletTestCase
4 changes: 2 additions & 2 deletions software/glasgow/applet/video/ws2812_output/test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from ... import *
from glasgow.applet import GlasgowAppletV2TestCase, synthesis_test
from . import VideoWS2812OutputApplet


class VideoWS2812OutputAppletTestCase(GlasgowAppletTestCase, applet=VideoWS2812OutputApplet):
class VideoWS2812OutputAppletTestCase(GlasgowAppletV2TestCase, applet=VideoWS2812OutputApplet):
@synthesis_test
def test_build(self):
self.assertBuilds(args=["--out", "A0:3", "-c", "1024"])
Loading