diff --git a/flake.lock b/flake.lock index 6294073..5e857ba 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1712791164, - "narHash": "sha256-3sbWO1mbpWsLepZGbWaMovSO7ndZeFqDSdX0hZ9nVyw=", + "lastModified": 1723637854, + "narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1042fd8b148a9105f3c0aca3a6177fd1d9360ba5", + "rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e0f63ca..0bbce4c 100644 --- a/flake.nix +++ b/flake.nix @@ -23,7 +23,6 @@ inherit system; config.allowUnfree = true; overlays = [ - litex-overlay ]; # patches, version pins, new pkgs here. } )); @@ -36,16 +35,6 @@ packages = with pkgs; [ (python3.withPackages (ps: with ps; [ cocotb - cocotb-bus - litex - litedram - liteeth - litescope - litespi - liteiclink - pythondata-cpu-vexriscv - pythondata-software-compiler_rt - pythondata-software-picolibc amaranth ])) yosys diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..4f047bc --- /dev/null +++ b/pdm.lock @@ -0,0 +1,98 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:711e8d9f3db580c3fd719dc08025858852c340c17bd0ec68be953ed3a4a18d6d" + +[[metadata.targets]] +requires_python = "==3.12.*" + +[[package]] +name = "amaranth" +version = "0.5.1" +requires_python = "~=3.8" +summary = "Amaranth hardware definition language" +groups = ["default"] +dependencies = [ + "Jinja2~=3.0", + "importlib-resources; python_version < \"3.9\"", + "jschon~=0.11.1", + "pyvcd<0.5,>=0.2.2", +] +files = [ + {file = "amaranth-0.5.1-py3-none-any.whl", hash = "sha256:2d370cc5b97e2472aab0a4eca515ab7f5116274550bd454132520eaec6068fb6"}, + {file = "amaranth-0.5.1.tar.gz", hash = "sha256:58b01efcec24c3696a7465e97a6e62f46466afab1f956a6eb00bda4b791c8345"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["default"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[[package]] +name = "jschon" +version = "0.11.1" +requires_python = "~=3.8" +summary = "A JSON toolkit for Python developers." +groups = ["default"] +dependencies = [ + "rfc3986", +] +files = [ + {file = "jschon-0.11.1-py3-none-any.whl", hash = "sha256:2350e8b6747b17358022960f91208bea70de448b914827af3184d30e20500f0f"}, + {file = "jschon-0.11.1.tar.gz", hash = "sha256:c0ca0beab1f1694a03d726b91ed75ec604a7787af3ae91ead765f78215bf149f"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["default"] +files = [ + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "pyvcd" +version = "0.4.0" +requires_python = ">=3.7" +summary = "Python VCD file support" +groups = ["default"] +files = [ + {file = "pyvcd-0.4.0-py2.py3-none-any.whl", hash = "sha256:a21b10e5018b7940c8f2c20ef83d97496e86f15e215afed134ee115166035e17"}, + {file = "pyvcd-0.4.0.tar.gz", hash = "sha256:31be3f501441a9b8c5dc72660ff7b9cfef9b43b2121a23d96f586d2863270290"}, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +requires_python = ">=3.7" +summary = "Validating URI References per RFC 3986" +groups = ["default"] +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..341feeb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "groovylight" +version = "0.1.0" +description = "Default template for PDM package" +authors = [ + {name = "saji", email = "saji@saji.dev"}, +] +dependencies = [ + "amaranth>=0.5.1", +] +requires-python = "==3.12.*" +readme = "README.md" +license = {text = "MIT"} + + +[tool.pdm] +distribution = false + +[tool.pdm.dev-dependencies] # or the poetry equivalent +dev = [ + "basedpyright", # you can pin the version here if you want, or just rely on the lockfile + "ruff" +] + + +[tool.ruff.lint] + + +[tool.basedpyright] +reportInvalidTypeForm = false +typeCheckingMode = "off" diff --git a/src/groovylight/__init__.py b/src/groovylight/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/groovylight/bitslicer.py b/src/groovylight/bitslicer.py new file mode 100644 index 0000000..23b56eb --- /dev/null +++ b/src/groovylight/bitslicer.py @@ -0,0 +1,210 @@ +from amaranth import Module, Cat, Signal, Assert, unsigned +from amaranth.build import Platform +from amaranth.lib import wiring, data +from amaranth.lib.wiring import In, Out + + +class RGBLayout(data.StructLayout): + def __init__(self, r_bits, g_bits, b_bits): + super().__init__( + { + "red": unsigned(r_bits), + "green": unsigned(g_bits), + "blue": unsigned(b_bits), + } + ) + + +rgb888_layout = RGBLayout(8, 8, 8) + +rgb111_hub75 = RGBLayout(1, 1, 1) + + +class Hub75Stream(wiring.Signature): + """A Hub75E Driver for a single string of panels.""" + + def __init__(self): + super().__init__( + { + "latch": Out(1), + "oe": Out(1), + "addr": Out(5), + "rgb0": Out(3), + "rgb1": Out(3), + "display_clk": Out(1), + } + ) + + +class Hub75Ctrl(wiring.Signature): + """Hub75E control/addressing signals. + This is separated because certain designs keep these common + between all panel outputs (e.g Colorlight 5A-75b boards). + + """ + + def __init__(self): + super().__init__( + { + "latch": Out(1), + "oe": Out(1), + "addr": Out(5), + "display_clk": Out(1), + } + ) + + +class Hub75Data(wiring.Signature): + """ Data lines for HUB75 displays. When combined with Hub75Ctrl, this forms a complete HUB75 interface. + These are kept separate as some devices have a single set of control signals. + """ + def __init__(self): + super().__init__( + { + "rgb0": Out(rgb111_hub75), + "rgb1": Out(rgb111_hub75), + } + ) +class Hub75StringDriver(wiring.Component): + """ A data driver for Hub75 panels. This accesses the line memory and feeds out the data + """ + bcm_select: In(3) + + +class Hub75Coordinator(wiring.Component): + """ A shared-control hub75 driver""" + pass + +class Bitslicer(wiring.Component): + start_write: In(1) + done: Out(1) + bitplane_addr: Out(11) + bitplane_wren: Out(1) + bitplane_data: Out(6) + + def elaborate(self, platform: Platform) -> Module: + m = Module() + + bitplane_bit = Signal(3) + with m.FSM(): + with m.State("init"): + m.d.sync += bitplane_bit.eq(0) + m.d.sync += self.done.eq(0) + + return m + + +class Hub75EDriver(wiring.Component): + """An optimized driver for hub75 strings. + This version is faster than most implementations by merging the exposure + period and the data-write period to happen simultaneously. As a result, + the display can be brighter due to higher duty cycle. + """ + + start: In(1) + done: Out(1) + out: Out(Hub75Stream()) + buf_addr: Out(9) + buf_data: In(36) + + row_depth = 128 + bit_depth = 8 + bcm_len = 32 + + def elaborate(self, platform: Platform) -> Module: + m = Module() + + counter = Signal(32) + bcm_shift: Signal = Signal(4, init=7) + m.d.sync += counter.eq(counter + 1) + # construct helper signals. + ram_r = self.buf_data[16:23] + ram_b = self.buf_data[8:15] + ram_g = self.buf_data[0:7] + + ram_rgb_slice = Cat( + ram_r.bit_select(bcm_shift, 1), + ram_g.bit_select(bcm_shift, 1), + ram_b.bit_select(bcm_shift, 1), + ) + pixnum = Signal(8, reset=127) + pixrow = Signal(1, reset=0) + m.d.comb += self.buf_addr.eq(Cat(pixrow, pixnum)) + + should_clock = counter < (128 * 2 + 1) + should_expose = (counter < (32 << (bcm_shift + 1) + 1)).bool() & ( + bcm_shift != 7 + ).bool() + + with m.FSM(): + with m.State("init"): + m.d.sync += [ + bcm_shift.eq(7), + counter.eq(0), + self.done.eq(0), + pixnum.eq(127), + pixrow.eq(0), + ] + with m.If(self.start == 1): + m.next = "prefetch" + with m.State("prefetch"): + with m.If(counter == 0): + m.d.sync += pixrow.eq(0) + with m.Elif(counter == 1): + m.d.sync += pixrow.eq(1) + with m.Elif(counter == 2): + m.d.sync += self.out.rgb0.eq(ram_rgb_slice) + with m.Elif(counter == 3): + m.d.sync += [self.out.rgb1.eq(ram_rgb_slice), counter.eq(0)] + m.next = "writerow" + with m.State("writerow"): + # expose if we haven't done it for long enough yet. + m.d.sync += self.out.oe.eq(~(should_expose)) + with m.If(should_clock): + # clock is high on entry + m.d.sync += self.out.display_clk.eq(counter[1] == 0) + + with m.If(counter[0:1] == 0): + # rising edge of the clock + m.d.sync += pixrow.eq(0) + + with m.If(counter[0:1] == 1): + m.d.sync += [ + pixrow.eq(1), + self.out.rgb0.eq(ram_rgb_slice), + ] + with m.If(counter[0:1] == 2): + m.d.sync += [ + pixnum.eq(pixnum - 1), + pixrow.eq(0), + self.out.rgb1.eq(ram_rgb_slice), + ] + + with m.Elif(~(should_expose)): + # we are done both feeding in the new data and exposing the previous. + m.d.sync += [counter.eq(0), self.out.display_clk.eq(0)] + m.next = "latchout" + with m.State("latchout"): + m.d.sync += [ + pixnum.eq(127), + self.out.latch.eq(1), + ] + with m.If(counter > 3): + m.d.sync += self.out.latch.eq(0) + m.d.sync += counter.eq(0) + with m.If(bcm_shift == 0): + m.next = "finish" + with m.Else(): + m.d.sync += bcm_shift.eq(bcm_shift - 1) + m.next = "prefetch" + + with m.State("finish"): + m.d.sync += Assert(bcm_shift == 0, "finish without bcm shift 0") + + with m.If(counter < (32)): + m.d.sync += self.out.oe.eq(0) + with m.Else(): + m.d.sync += [self.out.oe.eq(1), self.done.eq(1)] + m.next = "init" + + return m diff --git a/src/groovylight/lineram.py b/src/groovylight/lineram.py new file mode 100644 index 0000000..9a3e363 --- /dev/null +++ b/src/groovylight/lineram.py @@ -0,0 +1,12 @@ +from amaranth import Module, Cat, Signal, Assert, unsigned +from amaranth.build import Platform + +from amaranth.lib import wiring, data +from amaranth.lib.wiring import In, Out +from amaranth.lib.memory import Memory +# file to wrap line ram using PDPw16KD +# this is tricky. + + +def lineram(): + return Memory(shape=unsigned(24), depth=512) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29