diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index f4799269..c4f7b30e 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -10,11 +10,64 @@ permissions: contents: read jobs: + rtl-tests: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - name: Install RTL simulator + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y iverilog + + - name: Test timing-report gate + run: python3 -B -m unittest discover -s tb -p 'test_*.py' + + - name: Run RTL regressions + run: sh tb/run.sh + + scanout-module: + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + + - name: Install kernel build tools + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + build-essential bc bison flex libssl-dev libelf-dev \ + libgmp-dev libmpc-dev libmpfr-dev kmod xz-utils git curl ca-certificates + rustup toolchain install 1.95.0 --profile minimal --no-self-update + + - name: Cache verified compiler archive + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: kernel/.build/ci/gcc-arm-10.2-2020.11-x86_64-arm-none-linux-gnueabihf.tar.xz + key: scanout-gcc-102825ae56c9e00142d06f35d2bdd3299edb6060e84a275a25b095e66fd3fc2a + + - name: Build qualified kernel and module + run: bash kernel/build-scanout.sh + + - name: Upload scanout module + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: zaparoo-scanout-6.18.38-MiSTer + path: kernel/scanout-slots/zaparoo_scanout.ko + if-no-files-found: error + build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - name: Build with Quartus Lite run: | @@ -25,11 +78,13 @@ jobs: theypsilon/quartus-lite-c5:17.0.2.docker0 \ /opt/intelFPGA_lite/quartus/bin/quartus_sh --flow compile menu - - name: Check build output - run: test -f output_files/menu.rbf + - name: Check build output and timing + run: | + test -f output_files/menu.rbf + python3 -B tb/check_timing.py output_files/menu.sta.rpt - name: Upload RBF - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: menu-rbf path: output_files/menu.rbf diff --git a/.gitignore b/.gitignore index a80016f1..1f2d08c9 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,15 @@ c5_pin_model_dump.txt *.xml *_netlist *.cdf +/kernel/.build/ +/kernel/**/*.o +/kernel/**/*.ko +/kernel/**/*.mod +/kernel/**/*.mod.c +/kernel/**/.*.cmd +/kernel/**/Module.symvers +/kernel/**/modules.order +/kernel/**/.tmp_versions/ +/test-output/ +.pi/ +.claude/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..808defb5 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Startup core for MiSTer + +## Native CRT video (this fork) + +This fork drives the analog output with a native 15 kHz signal generated by +the core itself: 352x240p60 (NTSC) by default, with 720x480i60 and 352x288p50 +(PAL) selectable by the ARM-side launcher through a DDR control block (see +[DDR contract](rtl/native_video_reader.sv)). There are no video options in the OSD — the +mode and the H/V centering trims are owned by the launcher. Idle video stays +black during frontend handoff; upstream-style snow appears behind the stock +OSD. Native frontend frames take priority over both backgrounds. + +Native writers must use the v2 magic and publish a live frame counter. The +legacy 320-pixel, no-magic DDR contract is no longer accepted. + +Note: `forced_scandoubler` (the "Forced scandoubler" MiSTer.ini setting) is +ignored by this core — the analog output is always 15 kHz. If your VGA output +feeds a 31 kHz-only monitor, set `vga_scaler=1` in MiSTer.ini instead. + +* **ESC** - Back/Options +* **Enter** - OK +* **F1** - Cycle Background/Wallpaper +* **F9** - Go to Linux terminal (F12 - back) +* **F11** - Bluetooth Pairing Script +* **F12** - Recent Cores + +## RTL tests + +Install Icarus Verilog 12 and GNU coreutils, then run `sh tb/run.sh` from the +repository root. CI runs the same native timing, DDR reader, scanout latch, +snow cadence and bootstrap-black suites on Ubuntu 24.04. Assertions, compilation failures, +and a 600-second timeout per simulation fail the run. Generated simulators +stay in ignored `test-output/rtl/`. + +Quartus builds must also pass `python3 tb/check_timing.py output_files/menu.sta.rpt`. +CI rejects missing or negative setup, hold, recovery, removal and pulse-width +results, even when Quartus reports successful compilation. + +## Notes: +* Core supports sub-folders started with _ character. +* Regardless the place of RBF file, boot rom/vhd should be placed into either root of SD card or core's dedicated folder (should be created in root of SD card). +* Joystick (including emulation by keyboard) buttons defined in this core is default map for all cores unless defined in particalar core. + +## Wallpaper +* Place menu.png or menu.jpg to the root of SD card to have it as background on HDMI (you can use vga_scaler=1 if you want it on VGA). +* Create "wallpapers" folder and place multiple .jpg or .png to it. Use **F1** to cycle between standard MiSTer backgrounds and wallpapers in folder. diff --git a/Readme.md b/Readme.md deleted file mode 100644 index 224bb29d..00000000 --- a/Readme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Startup core for MiSTer - -* **ESC** - Back/Options -* **Enter** - OK -* **F1** - Cycle Background/Wallpaper -* **F9** - Go to Linux terminal (F12 - back) -* **F11** - Bluetooth Pairing Script -* **F12** - Recent Cores - -## Notes: -* Core supports sub-folders started with _ character. -* Regardless the place of RBF file, boot rom/vhd should be placed into either root of SD card or core's dedicated folder (should be created in root of SD card). -* Joystick (including emulation by keyboard) buttons defined in this core is default map for all cores unless defined in particalar core. - -## Wallpaper -* Place menu.png or menu.jpg to the root of SD card to have it as background on HDMI (you can use vga_scaler=1 if you want it on VGA). -* Create "wallpapers" folder and place multiple .jpg or .png to it. Use **F1** to cycle between standard MiSTer backgrounds and wallpapers in folder. diff --git a/files.qip b/files.qip index 830b5981..fd608e59 100644 --- a/files.qip +++ b/files.qip @@ -1,5 +1,15 @@ +set_global_assignment -name QIP_FILE rtl/pll_video.qip +set_global_assignment -name SDC_FILE menu.sdc set_global_assignment -name SYSTEMVERILOG_FILE rtl/sdram.sv -set_global_assignment -name SYSTEMVERILOG_FILE rtl/ddram.sv set_global_assignment -name VERILOG_FILE rtl/lfsr.v set_global_assignment -name SYSTEMVERILOG_FILE rtl/cos.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_reader.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_timing.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/native_video_top.sv set_global_assignment -name SYSTEMVERILOG_FILE menu.sv +set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_vblank_latch.sv +set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_latch_sys_top_bridge.sv +set_global_assignment -name SYSTEMVERILOG_FILE sys/mister_magik_bootstrap_black.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_bootstrap_video.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_snow_phase.sv +set_global_assignment -name SYSTEMVERILOG_FILE rtl/zaparoo_pixel_enable.sv diff --git a/kernel/build-scanout.sh b/kernel/build-scanout.sh new file mode 100644 index 00000000..5a829901 --- /dev/null +++ b/kernel/build-scanout.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Build the exact kernel/module pair accepted by scanout-slots/Makefile. +set -euo pipefail + +root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +build_root=${BUILD_ROOT:-"$root/kernel/.build/ci"} +kernel_src=${KERNEL_SRC:-"$build_root/linux"} +kernel_build="$build_root/kernel" +revision=aec7dc3aa4846385736f1d54c9155e3b3c726708 +package=gcc-arm-10.2-2020.11-x86_64-arm-none-linux-gnueabihf +mkdir -p "$build_root" + +if [[ -z ${CROSS_COMPILE:-} ]]; then + archive="$build_root/$package.tar.xz" + if [[ ! -f $archive ]]; then + curl --fail --location --retry 3 --connect-timeout 30 --max-time 1200 \ + "https://developer.arm.com/-/media/Files/downloads/gnu-a/10.2-2020.11/binrel/$package.tar.xz" \ + --output "$archive.partial" + mv "$archive.partial" "$archive" + fi + # SHA-256: https://github.com/buildroot/buildroot/blob/2021.02/toolchain/toolchain-external/toolchain-external-arm-arm/toolchain-external-arm-arm.hash + printf '%s %s\n' 102825ae56c9e00142d06f35d2bdd3299edb6060e84a275a25b095e66fd3fc2a "$archive" | sha256sum -c - + tar -xJf "$archive" -C "$build_root" + export CROSS_COMPILE="$build_root/$package/bin/arm-none-linux-gnueabihf-" +fi + +test "$("${CROSS_COMPILE}gcc" -dumpfullversion -dumpversion)" = 10.2.1 +if [[ -z ${KERNEL_SRC:-} ]]; then + if [[ ! -d $kernel_src ]]; then + git init "$kernel_src" + git -C "$kernel_src" remote add origin https://github.com/MiSTer-devel/Linux-Kernel_MiSTer.git + fi + if ! git -C "$kernel_src" cat-file -e "$revision^{commit}" 2>/dev/null; then + git -C "$kernel_src" fetch --depth=1 origin "$revision" + fi + if git -C "$kernel_src" rev-parse --verify HEAD >/dev/null 2>&1; then + git -C "$kernel_src" diff --quiet HEAD -- . + fi + git -C "$kernel_src" checkout --detach "$revision" +fi +# Explicit source overrides are read-only inputs, never reset or checked out. +test "$(git -C "$kernel_src" rev-parse HEAD)" = "$revision" +git -C "$kernel_src" diff --quiet HEAD -- . + +# Pin optional tool detection as well as the target compiler. Rust 1.95.0 is +# needed only to reproduce the qualified Kconfig fingerprint; no Rust is built. +export RUSTUP_TOOLCHAIN=1.95.0 +rustc --version | grep -E '^rustc 1\.95\.0 ' +export PAHOLE=false +export BINDGEN=false +kernel_args=(-C "$kernel_src" "O=$kernel_build" ARCH=arm + "CROSS_COMPILE=$CROSS_COMPILE" LOCALVERSION=-MiSTer) +make "${kernel_args[@]}" MiSTer_defconfig +printf '%s %s\n' 0d010a3d551cbffcd91af7850f3f745ce73f3bb911cfd56ead902fc9b6c69823 "$kernel_build/.config" | sha256sum -c - +# modules_prepare alone cannot supply a genuine Module.symvers. +make "${kernel_args[@]}" -j"${JOBS:-$(nproc)}" vmlinux modules +make -C "$root/kernel/scanout-slots" KERNEL_SRC="$kernel_src" KERNEL_BUILD="$kernel_build" CROSS_COMPILE="$CROSS_COMPILE" +vermagic=$(modinfo -F vermagic "$root/kernel/scanout-slots/zaparoo_scanout.ko") +test "${vermagic% }" = '6.18.38-MiSTer SMP mod_unload ARMv7 p2v8' +sha256sum "$root/kernel/scanout-slots/zaparoo_scanout.ko" diff --git a/kernel/scanout-slots/Makefile b/kernel/scanout-slots/Makefile new file mode 100644 index 00000000..22bcb822 --- /dev/null +++ b/kernel/scanout-slots/Makefile @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2026 Nigel Breslaw +# Zaparoo fork: local builds against the qualified MiSTer kernel. + +KERNEL_SRC ?= ../.build/linux-6.18 +KERNEL_BUILD ?= ../.build/kernel-6.18 +ARCH ?= arm +CROSS_COMPILE ?= arm-none-linux-gnueabihf- +LOCALVERSION ?= -MiSTer + +obj-m += zaparoo_scanout.o + +.PHONY: all qualify clean +all: qualify + $(MAKE) -C $(abspath $(KERNEL_SRC)) O=$(abspath $(KERNEL_BUILD)) M=$(CURDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) LOCALVERSION=$(LOCALVERSION) modules + +# Pin source/config as well as vermagic; never bless a different 6.18 build. +qualify: + test "$(ARCH)" = arm + test "$(LOCALVERSION)" = -MiSTer + test "$$(git -C $(abspath $(KERNEL_SRC)) rev-parse HEAD)" = aec7dc3aa4846385736f1d54c9155e3b3c726708 + git -C $(abspath $(KERNEL_SRC)) diff --quiet HEAD -- . + printf '%s %s\n' 0d010a3d551cbffcd91af7850f3f745ce73f3bb911cfd56ead902fc9b6c69823 $(abspath $(KERNEL_BUILD))/.config | sha256sum -c - + test -s $(abspath $(KERNEL_BUILD))/Module.symvers + test "$$($(CROSS_COMPILE)gcc -dumpfullversion -dumpversion)" = 10.2.1 + +clean: + $(MAKE) -C $(abspath $(KERNEL_SRC)) O=$(abspath $(KERNEL_BUILD)) M=$(CURDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) LOCALVERSION=$(LOCALVERSION) clean diff --git a/kernel/scanout-slots/README.md b/kernel/scanout-slots/README.md new file mode 100644 index 00000000..90ed93c4 --- /dev/null +++ b/kernel/scanout-slots/README.md @@ -0,0 +1,103 @@ +# Zaparoo scanout slots + +This GPL-3.0-or-later component derives from Nigel Breslaw's MagiK scanout-slot +module and the Zaparoo demo's 1080p extension. Keep its source and attribution +with this Menu fork. It is a separate kernel artifact, not linked into the +frontend. Source imports retain their original license; the Linux module +loader's license classification is separate and must not be changed to gain +access to GPL-only kernel exports. + +## Compatibility policy + +Initially support only the exact qualified MiSTer 6.18 kernel build. Do not +force-load a module, update the kernel, or reuse an unverified module. Unknown +and older kernels retain the ordinary fb0 frontend path. Future kernel changes +require rebuilding and requalifying the module and its memory-map contract. + +Use a Zaparoo-specific module/device identity and ABI. Do not install over +`mem_wc.ko` or `mister_magik_scanout_slots.ko`, or unload someone else's module. + +## Shared hardware + +DreamSTer's `mem_wc` provides a generic write-combined physical-memory mapper. +MagiK's module provides bounded scanout slots. These are not interchangeable +interfaces. A loaded module is not necessarily an active renderer. + +Only one Zaparoo client may own the slots at a time. Resource reservations must +last until the last mapping/file reference closes, including after process +termination. Idle module residency must not reserve another application's +memory indefinitely. Main must coordinate the frontend's FPGA bus access and +terminate its child when Main exits. + +Resource reservations prevent conflicting cooperative drivers, but do not +stop arbitrary `/dev/mem` or `/dev/mem_wc` mappings or another FPGA bitstream. +Conflict detection and lifecycle handoff are required; this is not a security +boundary against another privileged process starting an unrestricted mapper. +Do not claim concurrent DreamSTer/MagiK/Zaparoo rendering is supported. + +## Upstream references + +- https://github.com/NigelBreslaw/MiSTer-MagiK/tree/main/mister/platform/kernel/scanout-slots +- https://github.com/skmp/minicast/tree/master/mem_wc +- https://github.com/MiSTer-devel/Linux-Kernel_MiSTer/tree/MiSTer-v6.18 + +## Qualified build + +CI builds the module in a separate job from Quartus and uploads +`zaparoo-scanout-6.18.38-MiSTer` containing `zaparoo_scanout.ko`. It does not +install the module or change the device kernel. Only the checksum-verified +compiler archive is cached; kernel output and `Module.symvers` are built fresh. + +Run the same build from the Menu repository root. Install the build packages +listed in `.github/workflows/ci_build.yml`, then: + +```sh +rustup toolchain install 1.95.0 --profile minimal +bash kernel/build-scanout.sh +``` + +The script pins the kernel revision and verifies the GNU ARM 10.2.1 archive's +SHA-256 before extracting it. Rust 1.95.0 reproduces Kconfig's tool-detection +fields; the kernel/module build does not compile Rust. All downloaded inputs +and kernel output stay under ignored `kernel/.build/ci/`. + +For local reuse, `CROSS_COMPILE` may point to Main's qualified GNU toolchain +(not the frontend's musl compiler), and `KERNEL_SRC` may point to an existing +clean checkout of the pinned revision. `BUILD_ROOT` accepts an absolute build +path, and `JOBS` controls parallelism. The full kernel build creates real +`Module.symvers`; do not suppress modpost errors or substitute +`modules_prepare` alone. + +The module Makefile rejects a different source revision, tracked source edits, +config fingerprint, compiler version or missing symbol table. Do not loosen +these checks to make an unknown build pass. Expected vermagic: +`6.18.38-MiSTer SMP mod_unload ARMv7 p2v8`. + +Qualification evidence: + +| Input | SHA-256 | +|---|---| +| Generated `.config` | `0d010a3d551cbffcd91af7850f3f745ce73f3bb911cfd56ead902fc9b6c69823` | +| `drivers/video/fbdev/MiSTer_fb.c` | `f4044889e96a843a54bde091737825043b71b6bb8994fe3f92387cccd6ee3924` | +| `arch/arm/boot/dts/intel/socfpga/socfpga_cyclone5_de10_nano.dts` | `5c03d8ffb9e1477523d6434c5255db46433158f771fb6288320c42f8d3484938` | + +ABI v1 uses `/dev/zaparoo-scanout`, ioctl `_IOR('Z', 1, layout)` and a 64-byte +layout. Slots start at `0x23000000` and `0x23400000`, outside the complete +`MiSTer_fb` DT aperture (`0x22000000`, 8 MiB). Each has 4,147,200 usable bytes +and a 4,149,248-byte mapping. Slot-one mmap selector is 8,294,400, **not its +physical address**. Only exact shared read/write mappings are accepted; +executable mappings and fork inheritance are disabled. + +6.18 compatibility decisions: + +- `registered_fb` is no longer exported. Validate the pinned root-level DT + aperture instead of linking to that private symbol. +- `no_llseek` is gone; use a NULL file-operation entry. +- Published-VMA setters require GPL-only locking helpers. The pinned kernel + invokes `.mmap` on a newly allocated VMA before insertion into the tree, so + initialize its flags with `vm_flags_init`. Recheck this ordering for a new + kernel; do not change the loader license marker to bypass modpost. + +Successful compilation is **not hardware qualification**. Before installing +or distributing the artifact, verify the matched Main/frontend/Menu stack on +the target device, including ownership handoff, crash recovery and fb0 fallback. diff --git a/kernel/scanout-slots/zaparoo_scanout.c b/kernel/scanout-slots/zaparoo_scanout.c new file mode 100644 index 00000000..eac3033f --- /dev/null +++ b/kernel/scanout-slots/zaparoo_scanout.c @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Nigel Breslaw +// Zaparoo fork: namespaced device, 6.18 platform validation, lifetime ownership. + +/* Bounded write-combined RGB565 slots. No FPGA commands, DMA, or IRQ ownership. + * Reservations belong to the open file and all its VMAs, not module residency. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "zaparoo_scanout_platform.h" +#include "zaparoo_scanout_uapi.h" + +#define DEVICE_NAME "zaparoo-scanout" + +static DEFINE_MUTEX(owner_lock); +static bool owned; +static struct resource *slot_resources[ZAPAROO_SCANOUT_SLOT_COUNT]; +static const unsigned long slot_addresses[] = { + ZAPAROO_SCANOUT_SLOT0_PHYS, ZAPAROO_SCANOUT_SLOT1_PHYS +}; +static const struct zaparoo_scanout_layout layout = { + .abi_version = ZAPAROO_SCANOUT_ABI_VERSION, + .slot_count = ZAPAROO_SCANOUT_SLOT_COUNT, + .max_width = ZAPAROO_SCANOUT_MAX_WIDTH, + .max_height = ZAPAROO_SCANOUT_MAX_HEIGHT, + .max_stride_bytes = ZAPAROO_SCANOUT_MAX_STRIDE, + .slot_capacity_bytes = ZAPAROO_SCANOUT_CAPACITY, + .map_bytes = ZAPAROO_SCANOUT_MAP_BYTES, + .flags = ZAPAROO_SCANOUT_WRITE_COMBINE | ZAPAROO_SCANOUT_EXCLUSIVE_OWNER, + .slots = { + { ZAPAROO_SCANOUT_SLOT0_PHYS, 0 }, + { ZAPAROO_SCANOUT_SLOT1_PHYS, ZAPAROO_SCANOUT_SLOT1_SELECTOR }, + }, +}; + +/* 6.18 no longer exports registered_fb. Check the exact root-level DT window + * instead: slots are above its complete aperture, independent of live fb mode. + * Raw property reads deliberately use non-GPL-only exports. Do not mislabel + * the GPL-3.0 source's loader classification to access GPL-only helpers. + */ +static int validate_platform(void) +{ + struct device_node *node; + const __be32 *cells; + int len, ret = -ENODEV; + + if (strcmp(utsname()->release, ZAPAROO_SCANOUT_KERNEL_RELEASE) || + !of_machine_is_compatible(ZAPAROO_SCANOUT_MACHINE)) + return -ENODEV; + cells = of_get_property(of_root, "#address-cells", &len); + if (!cells || len != 4 || be32_to_cpup(cells) != 1) + return -ENODEV; + cells = of_get_property(of_root, "#size-cells", &len); + if (!cells || len != 4 || be32_to_cpup(cells) != 1) + return -ENODEV; + node = of_find_compatible_node(NULL, NULL, "MiSTer_fb"); + if (!node) + return -ENODEV; + cells = of_get_property(node, "reg", &len); + if (node->parent == of_root && cells && len == 8 && + be32_to_cpup(cells) == ZAPAROO_SCANOUT_FB_DT_BASE && + be32_to_cpup(cells + 1) == ZAPAROO_SCANOUT_FB_DT_BYTES) + ret = 0; + of_node_put(node); + return ret; +} + +static void release_slots(void) +{ + unsigned int i; + for (i = 0; i < ARRAY_SIZE(slot_resources); i++) { + if (slot_resources[i]) { + release_mem_region(slot_addresses[i], ZAPAROO_SCANOUT_MAP_BYTES); + slot_resources[i] = NULL; + } + } +} + +static int scanout_open(struct inode *inode, struct file *file) +{ + unsigned int i; + int ret = 0; + if (!capable(CAP_SYS_RAWIO) || !(file->f_mode & FMODE_READ) || + !(file->f_mode & FMODE_WRITE)) + return -EPERM; + mutex_lock(&owner_lock); + if (owned) { + ret = -EBUSY; + goto out; + } + /* Busy System RAM or another cooperating driver's region also rejects + * these requests. Never map Linux-managed RAM on a different boot setup. + */ + for (i = 0; i < ARRAY_SIZE(slot_addresses); i++) { + slot_resources[i] = request_mem_region_exclusive(slot_addresses[i], + ZAPAROO_SCANOUT_MAP_BYTES, DEVICE_NAME); + if (!slot_resources[i]) { + release_slots(); + ret = -EBUSY; + goto out; + } + } + owned = true; +out: + mutex_unlock(&owner_lock); + return ret; +} + +static int scanout_release(struct inode *inode, struct file *file) +{ + /* VMA file references postpone this until every mapping is gone, even + * after close(fd). The final release may run in a different task. + */ + mutex_lock(&owner_lock); + release_slots(); + owned = false; + mutex_unlock(&owner_lock); + return 0; +} + +static int scanout_mmap(struct file *file, struct vm_area_struct *vma) +{ + unsigned long phys; + if (vma->vm_end - vma->vm_start != ZAPAROO_SCANOUT_MAP_BYTES || + !(vma->vm_flags & VM_SHARED) || !(vma->vm_flags & VM_READ) || + !(vma->vm_flags & VM_WRITE) || (vma->vm_flags & VM_EXEC)) + return -EINVAL; + if (!vma->vm_pgoff) + phys = ZAPAROO_SCANOUT_SLOT0_PHYS; + else if (vma->vm_pgoff == ZAPAROO_SCANOUT_SLOT1_SELECTOR / PAGE_SIZE) + phys = ZAPAROO_SCANOUT_SLOT1_PHYS; + else + return -EINVAL; + + vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot); + /* The pinned kernel's __mmap_new_vma invokes this callback before + * inserting the new VMA into its tree. Initialize flags here; changing + * a published VMA would instead require the per-VMA locking helpers. + */ + vm_flags_init(vma, (vma->vm_flags & ~(VM_EXEC | VM_MAYEXEC)) | + VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP | VM_DONTCOPY); + if (remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT, + ZAPAROO_SCANOUT_MAP_BYTES, vma->vm_page_prot)) + return -EAGAIN; + return 0; +} + +static long scanout_ioctl(struct file *file, unsigned int command, unsigned long argument) +{ + if (command != ZAPAROO_SCANOUT_GET_LAYOUT) + return -ENOTTY; + return copy_to_user((void __user *)argument, &layout, sizeof(layout)) ? -EFAULT : 0; +} + +static const struct file_operations scanout_fops = { + .owner = THIS_MODULE, + .open = scanout_open, + .release = scanout_release, + .unlocked_ioctl = scanout_ioctl, + .mmap = scanout_mmap, + .llseek = NULL, +}; +static struct miscdevice scanout_device = { + .minor = MISC_DYNAMIC_MINOR, + .name = DEVICE_NAME, + .fops = &scanout_fops, + .mode = 0600, +}; + +static int __init scanout_init(void) +{ + BUILD_BUG_ON(sizeof(layout) != 64); + BUILD_BUG_ON(ZAPAROO_SCANOUT_MAP_BYTES & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_SLOT0_PHYS & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_SLOT1_PHYS & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_SLOT1_SELECTOR & ~PAGE_MASK); + BUILD_BUG_ON(ZAPAROO_SCANOUT_CAPACITY != ZAPAROO_SCANOUT_MAX_STRIDE * ZAPAROO_SCANOUT_MAX_HEIGHT); + BUILD_BUG_ON(ZAPAROO_SCANOUT_CAPACITY > ZAPAROO_SCANOUT_MAP_BYTES); + BUILD_BUG_ON(ZAPAROO_SCANOUT_FB_DT_BASE + ZAPAROO_SCANOUT_FB_DT_BYTES > ZAPAROO_SCANOUT_SLOT0_PHYS); + BUILD_BUG_ON(ZAPAROO_SCANOUT_SLOT0_PHYS + ZAPAROO_SCANOUT_MAP_BYTES > ZAPAROO_SCANOUT_SLOT1_PHYS); + if (validate_platform()) { + pr_err("zaparoo_scanout: unsupported kernel/device-tree platform\n"); + return -ENODEV; + } + return misc_register(&scanout_device); +} + +static void __exit scanout_exit(void) +{ + misc_deregister(&scanout_device); +} +module_init(scanout_init); +module_exit(scanout_exit); +MODULE_DESCRIPTION("Zaparoo exclusive write-combined scanout slots"); +MODULE_AUTHOR("Nigel Breslaw; Zaparoo Project contributors"); +/* Linux's loader classification is not the source license. */ +MODULE_LICENSE("Proprietary"); +MODULE_INFO(source_license, "GPL-3.0-or-later"); +MODULE_INFO(kernel_revision, ZAPAROO_SCANOUT_KERNEL_REVISION); diff --git a/kernel/scanout-slots/zaparoo_scanout_platform.h b/kernel/scanout-slots/zaparoo_scanout_platform.h new file mode 100644 index 00000000..b53ddb42 --- /dev/null +++ b/kernel/scanout-slots/zaparoo_scanout_platform.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright (C) 2026 Nigel Breslaw + * Zaparoo fork: 1080p slots outside fbdev, pinned MiSTer 6.18 platform. + */ +#ifndef ZAPAROO_SCANOUT_PLATFORM_H +#define ZAPAROO_SCANOUT_PLATFORM_H + +#define ZAPAROO_SCANOUT_KERNEL_RELEASE "6.18.38-MiSTer" +#define ZAPAROO_SCANOUT_KERNEL_REVISION "aec7dc3aa4846385736f1d54c9155e3b3c726708" +#define ZAPAROO_SCANOUT_MACHINE "altr,socfpga-cyclone5" +#define ZAPAROO_SCANOUT_FB_DT_BASE 0x22000000UL +#define ZAPAROO_SCANOUT_FB_DT_BYTES 0x00800000UL +#define ZAPAROO_SCANOUT_SLOT0_PHYS 0x23000000UL +#define ZAPAROO_SCANOUT_SLOT1_PHYS 0x23400000UL +#define ZAPAROO_SCANOUT_MAX_WIDTH 1920UL +#define ZAPAROO_SCANOUT_MAX_HEIGHT 1080UL +#define ZAPAROO_SCANOUT_MAX_STRIDE 3840UL +#define ZAPAROO_SCANOUT_CAPACITY 4147200UL +#define ZAPAROO_SCANOUT_MAP_BYTES 4149248UL +#define ZAPAROO_SCANOUT_SLOT1_SELECTOR 8294400UL + +#endif diff --git a/kernel/scanout-slots/zaparoo_scanout_uapi.h b/kernel/scanout-slots/zaparoo_scanout_uapi.h new file mode 100644 index 00000000..9f004c0b --- /dev/null +++ b/kernel/scanout-slots/zaparoo_scanout_uapi.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright (C) 2026 Nigel Breslaw + * Zaparoo fork: separate device identity and exclusive mapping ownership. + */ +#ifndef ZAPAROO_SCANOUT_UAPI_H +#define ZAPAROO_SCANOUT_UAPI_H + +#include +#include + +#define ZAPAROO_SCANOUT_ABI_VERSION 1U +#define ZAPAROO_SCANOUT_SLOT_COUNT 2U +#define ZAPAROO_SCANOUT_WRITE_COMBINE 0x00000001U +#define ZAPAROO_SCANOUT_EXCLUSIVE_OWNER 0x00000002U + +struct zaparoo_scanout_slot { + __u32 physical_address; + __u32 mmap_offset_bytes; +}; + +struct zaparoo_scanout_layout { + __u32 abi_version; + __u32 slot_count; + __u32 max_width; + __u32 max_height; + __u32 max_stride_bytes; + __u32 slot_capacity_bytes; + __u32 map_bytes; + __u32 flags; + struct zaparoo_scanout_slot slots[ZAPAROO_SCANOUT_SLOT_COUNT]; + __u32 reserved[4]; +}; + +/* Distinct ioctl namespace: a MagiK device must not pass this handshake. */ +#define ZAPAROO_SCANOUT_GET_LAYOUT _IOR('Z', 0x01, struct zaparoo_scanout_layout) +#endif diff --git a/menu.qsf b/menu.qsf index 577fcc3e..ac77c577 100644 --- a/menu.qsf +++ b/menu.qsf @@ -54,7 +54,7 @@ set_global_assignment -name ALM_REGISTER_PACKING_EFFORT LOW set_global_assignment -name OPTIMIZE_POWER_DURING_SYNTHESIS OFF set_global_assignment -name ROUTER_REGISTER_DUPLICATION ON set_global_assignment -name FITTER_AGGRESSIVE_ROUTABILITY_OPTIMIZATION ALWAYS -set_global_assignment -name SEED 1 +set_global_assignment -name SEED 3 source sys/sys.tcl source sys/sys_analog.tcl diff --git a/menu.sdc b/menu.sdc new file mode 100644 index 00000000..0a01b314 --- /dev/null +++ b/menu.sdc @@ -0,0 +1,12 @@ +# Core-level timing constraints (processed after sys/sys_top.sdc). + +# CLK_VIDEO comes from a dedicated PLL (rtl/pll_video.v) and is asynchronous +# to every other clock in the design: all crossings into and out of the video +# domain go through two-flop synchronizers or the line FIFO's dual-clock +# logic (rtl/native_video_reader.sv, rtl/native_video_top.sv). Without this +# group, derive_pll_clocks leaves the 27 MHz output related to the other +# clocks (shared 50 MHz reference, and absent from sys_top.sdc's exclusive +# groups), and the fitter tries to close those CDC paths against a ~1 ns +# edge relationship. +set_clock_groups -asynchronous \ + -group [get_clocks { *|pll_video|pll_video_inst|altera_pll_i|*[*].*|divclk}] diff --git a/menu.sv b/menu.sv index 7ac4c3bf..5cbcfdba 100644 --- a/menu.sv +++ b/menu.sv @@ -33,7 +33,9 @@ assign DDRAM_CLK = clk_sys; assign CE_PIXEL = ce_pix; assign VGA_SL = 0; -assign VGA_F1 = 0; +// Field number for 480i: ascal (HDMI) keys deinterlacing off this, and the +// analog csync path passes it through. 0 in the progressive modes. +assign VGA_F1 = native_field; assign VIDEO_ARX = 0; assign VIDEO_ARY = 0; assign VGA_SCALER= 0; @@ -56,11 +58,13 @@ wire [26:0] act_cnt2 = {~act_cnt[26],act_cnt[25:0]}; assign LED_POWER[0]= FB ? led[2] : act_cnt2[26] ? act_cnt2[25:18] > act_cnt2[7:0] : act_cnt2[25:18] <= act_cnt2[7:0]; -`include "build_id.v" +`include "build_id.v" +// No video options here: native video mode and centering trims arrive via +// the DDR control block written by the launcher (see rtl/native_video_reader.sv). localparam CONF_STR = { "MENU;UART31250,MIDI;", "- ;", - "V,v",`BUILD_DATE + "V,v",`BUILD_DATE }; wire forced_scandoubler; @@ -82,10 +86,21 @@ pll pll .refclk(CLK_50M), .rst(0), .outclk_0(clk_sys), - .outclk_1(CLK_VIDEO), + .outclk_1(), // stock 27.027 MHz output, unused (see pll_video) .locked(locked) ); +// Exact 27.000000 MHz video clock from its own PLL: 27 MHz can't share a +// VCO with the 100 MHz clk_sys (lcm = 2700 MHz, above the VCO ceiling). +wire vid_locked; +pll_video pll_video +( + .refclk(CLK_50M), + .rst(0), + .outclk_0(CLK_VIDEO), + .locked(vid_locked) +); + ///////////////////// SDRAM /////////////////// // @@ -185,36 +200,16 @@ always @(posedge clk_sys) begin state <= state+1'd1; end 16: begin - sdram_addr <= addr[24:0]; - sdram_din <= 0; - sdram_we <= we; + sdram_we <= 0; end endcase end end -ddram ddr -( - .*, - .reset(RESET), - .dout(), - .din(0), - .rd(0), - .ready() -); - -reg we; -reg [28:0] addr = 0; - -always @(posedge clk_sys) begin - reg [4:0] cnt = 9; - - if(~RESET & cfg[15]) begin - cnt <= cnt + 1'b1; - we <= &cnt; - if(cnt == 8) addr <= addr + 1'd1; - end -end +// DDR clear loop removed: native_video_reader owns DDRAM_* signals. +// The reader polls the launcher's control block once per vblank; until the +// launcher publishes frames it issues a single 64-bit read per frame and the +// core shows the noise pattern. //////////////////////////// MT32pi ////////////////////////////////// @@ -323,87 +318,114 @@ end ///////////////////// VIDEO /////////////////// -localparam lfsr_n = 63; - -wire PAL = status[4]; wire FB = status[5]; wire [2:0] led = status[8:6]; -reg [9:0] hc; -reg [9:0] vc; -reg [9:0] vvc; - -reg [lfsr_n:0] rnd_reg; -wire [lfsr_n:0] rnd; - -wire [5:0] rnd_c = {rnd_reg[0],rnd_reg[1],rnd_reg[2],rnd_reg[2],rnd_reg[2],rnd_reg[2]}; - -lfsr #(lfsr_n) random(rnd); - -always @(posedge CLK_VIDEO) begin - if(forced_scandoubler) ce_pix <= 1; - else ce_pix <= ~ce_pix; - - if(ce_pix) begin - if(hc == 637) begin - hc <= 0; - if(vc == (PAL ? (forced_scandoubler ? 623 : 311) : (forced_scandoubler ? 523 : 261))) begin - vc <= 0; - vvc <= vvc + 9'd6; - end else begin - vc <= vc + 1'd1; - end - end else begin - hc <= hc + 1'd1; - end +// Pixel clock: CLK_VIDEO = 27.000 MHz (the universal SD video clock). +// ce_pix /4 = 6.75 MHz gives exactly 15734.27 Hz (NTSC, 429-px line) and +// 15625.00 Hz (PAL, 432-px line); the 480i mode runs /2 = 13.5 MHz with an +// 858-px line for the same 15734.27 Hz. Both the cosine fallback and the FB +// reader use this ce_pix. +wire [1:0] native_mode; +wire ce_pix; +zaparoo_pixel_enable pixel_enable ( + .clk(CLK_VIDEO), + .reset(RESET | ~vid_locked), + .mode(native_mode), + .ce_pix(ce_pix) +); - rnd_reg <= rnd; - end -end +// Native video timing + DDR reader. Timing outputs (sync, DE, vcount, frame +// edge) are the SINGLE source of truth for VGA scanout in both modes — that's +// what guarantees the CRT sees a clean 15 kHz line rate whether we're +// painting cosine noise or reading a Linux-rendered framebuffer. Mode and +// centering trims come from the launcher's DDR control block, not the OSD. +wire [7:0] native_r; +wire [7:0] native_g; +wire [7:0] native_b; +wire native_hs; +wire native_vs; +wire native_de; +wire [8:0] native_vcount; +wire native_new_frame; +wire native_field; +wire native_active; + +native_video_top native_video +( + .clk_sys (clk_sys), + .clk_vid (CLK_VIDEO), + .ce_pix (ce_pix), + .reset (RESET | ~vid_locked), + + .ddr_busy (DDRAM_BUSY), + .ddr_burstcnt (DDRAM_BURSTCNT), + .ddr_addr (DDRAM_ADDR), + .ddr_dout (DDRAM_DOUT), + .ddr_dout_ready (DDRAM_DOUT_READY), + .ddr_rd (DDRAM_RD), + .ddr_din (DDRAM_DIN), + .ddr_be (DDRAM_BE), + .ddr_we (DDRAM_WE), + + .vga_r (native_r), + .vga_g (native_g), + .vga_b (native_b), + .vga_hs (native_hs), + .vga_vs (native_vs), + .vga_de (native_de), + .vga_hblank (), + .vga_vblank (), + .vga_vcount (native_vcount), + .vga_new_frame (native_new_frame), + .vga_mode (native_mode), + .vga_field (native_field), + .active (native_active) +); -reg HBlank; -reg HSync; -reg VBlank; -reg VSync; +// Keep upstream's asynchronous noise source and grayscale weighting. Only +// the frame-phase enable differs: native timing stretches new_frame over +// several video clocks, so it must be sampled on ce_pix, not every clock. +wire [62:0] snow_random; +reg [2:0] snow_sample = 0; +wire [9:0] snow_phase; +wire [7:0] snow_cos; +wire [5:0] snow_level = {1'b0, snow_cos[7:3]} + 6'd32; +wire [5:0] snow_noise = {snow_sample[0], snow_sample[1], {4{snow_sample[2]}}}; +wire [7:0] snow_pixel = (snow_level >= snow_noise) ? + {snow_level - snow_noise, 2'b00} : 8'd0; + +lfsr #(.N(63)) snow_source(snow_random); +cos snow_wave(snow_phase, snow_cos); +zaparoo_snow_phase snow_motion ( + .clk(CLK_VIDEO), .reset(RESET | ~vid_locked), .ce_pix(ce_pix), + .new_frame(native_new_frame), .vcount(native_vcount), .phase(snow_phase) +); -reg ce_pix; +// OSD status comes from the HDMI domain. Main disables OSD before handing +// video to the frontend, so startup stays black without a new bus command. +(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) +reg [1:0] snow_osd_sync = 0; always @(posedge CLK_VIDEO) begin - if (hc == 529) HBlank <= 1; - else if (hc == 0) HBlank <= 0; - - if (hc == 544) begin - HSync <= 1; - - if(PAL) begin - if(vc == (forced_scandoubler ? 609 : 304)) VSync <= 1; - else if (vc == (forced_scandoubler ? 617 : 308)) VSync <= 0; - - if(vc == (forced_scandoubler ? 601 : 300)) VBlank <= 1; - else if (vc == 0) VBlank <= 0; - end - else begin - if(vc == (forced_scandoubler ? 490 : 245)) VSync <= 1; - else if (vc == (forced_scandoubler ? 496 : 248)) VSync <= 0; - - if(vc == (forced_scandoubler ? 480 : 240)) VBlank <= 1; - else if (vc == 0) VBlank <= 0; - end + if (RESET | ~vid_locked) begin + snow_sample <= 0; + snow_osd_sync <= 0; + end else begin + if (ce_pix) snow_sample <= snow_random[2:0]; + snow_osd_sync <= {snow_osd_sync[0], OSD_STATUS}; end - - if (hc == 590) HSync <= 0; end -reg [7:0] cos_out; -wire [5:0] cos_g = cos_out[7:3]+6'd32; -cos cos(vvc + {vc>>forced_scandoubler, 2'b00}, cos_out); - -wire [7:0] comp_v = (cos_g >= rnd_c) ? {cos_g - rnd_c, 2'b00} : 8'd0; - -assign VGA_DE = ~(HBlank | VBlank); -assign VGA_HS = HSync; -assign VGA_VS = VSync; -assign VGA_G = comp_v; -assign VGA_R = comp_v; -assign VGA_B = comp_v; +// Black handoff, snow behind stock OSD, or native frontend RGB. All three +// share the same native sync/DE; no video-mode switch is needed. +zaparoo_bootstrap_video bootstrap_video ( + .native_active(native_active), + .native_rgb({native_r, native_g, native_b}), + .show_snow(snow_osd_sync[1]), + .snow_rgb({snow_pixel, snow_pixel, snow_pixel}), + .de_in(native_de), .hs_in(native_hs), .vs_in(native_vs), + .rgb_out({VGA_R, VGA_G, VGA_B}), + .de_out(VGA_DE), .hs_out(VGA_HS), .vs_out(VGA_VS) +); endmodule diff --git a/rtl/native_video_reader.sv b/rtl/native_video_reader.sv new file mode 100644 index 00000000..7160a6fe --- /dev/null +++ b/rtl/native_video_reader.sv @@ -0,0 +1,454 @@ +// Zaparoo native video DDR reader. +// +// DDR contract v2 (one 64-bit beat at 0x3A000000, read each vblank): +// word0 [31:0]: (frame_counter << 2) | active_buffer; 0 = writer stopped +// word1 [63:32]: [31:16] magic 0x5A50 ("ZP") +// [15:8] h_offset, signed pixels (+ = right) +// [7:4] v_offset, signed lines (+ = down) +// [3:0] mode: 0 = 352x240p60, 1 = 720x480i60, 2 = 352x288p50 +// 0x3A001000: buffer 0 0x3A180000: buffer 1 (tight stride, width*4 B) +// +// The magic is mandatory, and a block is only painted once the writer has +// been shown to be live. Nothing clears DDR on this core's startup, so a +// previous core's leftovers sit at CTRL_ADDR looking like a control block; +// without both checks the reader latches onto that and scans out garbage +// instead of idle video, permanently. A writer proves itself either by +// starting after the reader has seen no writer (word0 == 0 or no magic), or by +// advancing the counter under a block that was already there at reset. Stale +// DDR does neither. Note this is not a heartbeat: once trusted, a writer may +// idle indefinitely without republishing. +// +// In 480i the app publishes one progressive 720x480 frame; this reader +// fetches source line vcount*2 + field, so no field-splitting on the ARM +// side. 720 px = 360 words exceeds the 8-bit burst counter, so 480i lines +// are fetched as two 180-beat bursts. + +module native_video_reader +( + input wire ddr_clk, + input wire ddr_busy, + output reg [7:0] ddr_burstcnt, + output reg [28:0] ddr_addr, + input wire [63:0] ddr_dout, + input wire ddr_dout_ready, + output reg ddr_rd, + output wire [63:0] ddr_din, + output wire [7:0] ddr_be, + output wire ddr_we, + + input wire clk_vid, + input wire ce_pix, + input wire reset, + input wire de, + input wire vblank, + input wire new_frame, + input wire new_line, + input wire field, + + // Quasi-static, ddr_clk domain: caller synchronizes into the video + // domain; the timing module latches them at the field wrap. + output reg [1:0] mode_out, + output reg signed [7:0] h_offset_out, + output reg signed [3:0] v_offset_out, + + output reg [7:0] r_out, + output reg [7:0] g_out, + output reg [7:0] b_out, + output wire frame_ready +); + +assign ddr_din = 64'd0; +assign ddr_be = 8'hFF; +assign ddr_we = 1'b0; + +localparam [28:0] CTRL_ADDR = 29'h07400000; +localparam [28:0] BUF0_V2 = 29'h07400200; +localparam [28:0] BUF1_V2 = 29'h07430000; +localparam [15:0] MAGIC_V2 = 16'h5A50; +localparam [19:0] TIMEOUT_MAX = 20'hF_FFFF; + +reg [1:0] new_frame_sync; +always @(posedge ddr_clk) begin + if(reset) new_frame_sync <= 2'b0; + else new_frame_sync <= {new_frame_sync[0], new_frame}; +end +wire new_frame_ddr = ~new_frame_sync[1] & new_frame_sync[0]; + +reg [1:0] new_line_sync; +always @(posedge ddr_clk) begin + if(reset) new_line_sync <= 2'b0; + else new_line_sync <= {new_line_sync[0], new_line}; +end +wire new_line_ddr = ~new_line_sync[1] & new_line_sync[0]; + +reg [1:0] vblank_sync; +always @(posedge ddr_clk) begin + if(reset) vblank_sync <= 2'b0; + else vblank_sync <= {vblank_sync[0], vblank}; +end +wire vblank_ddr = vblank_sync[1]; + +// Field is stable for a whole field; the reader samples it only while +// scanning, long after the edge. +reg [1:0] field_sync; +always @(posedge ddr_clk) begin + if(reset) field_sync <= 2'b0; + else field_sync <= {field_sync[0], field}; +end +wire field_ddr = field_sync[1]; + +reg [1:0] reset_vid_sync; +always @(posedge clk_vid or posedge reset) begin + if(reset) reset_vid_sync <= 2'b11; + else reset_vid_sync <= {reset_vid_sync[0], 1'b0}; +end +wire reset_vid = reset_vid_sync[1]; + +reg frame_ready_reg; +reg [1:0] frame_ready_sync; +always @(posedge clk_vid) begin + if(reset_vid) frame_ready_sync <= 2'b0; + else frame_ready_sync <= {frame_ready_sync[0], frame_ready_reg}; +end +wire frame_ready_vid = frame_ready_sync[1]; +assign frame_ready = frame_ready_vid; + +localparam [3:0] ST_IDLE = 4'd0; +localparam [3:0] ST_POLL_CTRL = 4'd1; +localparam [3:0] ST_WAIT_CTRL = 4'd2; +localparam [3:0] ST_CHECK_CTRL = 4'd3; +localparam [3:0] ST_READ_LINE = 4'd4; +localparam [3:0] ST_WAIT_LINE = 4'd5; +localparam [3:0] ST_LINE_DONE = 4'd6; +localparam [3:0] ST_WAIT_DISPLAY = 4'd7; + +reg [3:0] state; +reg [31:0] ctrl_word; +reg [31:0] ctrl_word1; +reg [29:0] prev_frame_counter; +reg [28:0] buf_base_addr; +reg [8:0] cur_line; +reg [7:0] beat_count; +reg burst_idx; +reg first_frame_loaded; +// Gates frame_ready. Set when the reader observes no writer (so whatever +// publishes next started while it was watching), and when the counter changes +// under a block that was already present at reset. Never cleared afterwards: +// an idle writer that stops republishing keeps its last frame on screen. +reg writer_trusted; +reg preloading; +reg [19:0] timeout_cnt; +reg fifo_wr; +reg [63:0] fifo_wr_data; +wire fifo_full; + +// Per-frame fetch geometry, registered in ST_CHECK_CTRL from the parsed +// control block: line length in 64-bit words, line count, interlace flag. +reg [8:0] line_words; +reg [8:0] scan_lines; +reg scan_interlaced; +reg two_bursts; + +wire magic_ok = (ctrl_word1[31:16] == MAGIC_V2); +wire [1:0] ctrl_mode = (ctrl_word1[3:0] > 4'd2) ? 2'd0 : ctrl_word1[1:0]; + +// 480i: source line = displayed line * 2 + field, from one progressive frame. +wire [8:0] src_line = scan_interlaced ? ({cur_line[7:0], 1'b0} + {8'd0, field_ddr}) : cur_line; +wire [28:0] line_base = buf_base_addr + src_line * line_words; +wire [7:0] burst_len = two_bursts ? 8'd180 : line_words[7:0]; + +reg [3:0] fifo_aclr_cnt; +wire fifo_aclr_ddr_active = (fifo_aclr_cnt != 4'd0); +wire fifo_aclr = reset | fifo_aclr_ddr_active; + +always @(posedge ddr_clk) begin + if(reset) begin + state <= ST_IDLE; + ddr_rd <= 1'b0; + ddr_burstcnt <= 8'd1; + ddr_addr <= 29'd0; + ctrl_word <= 32'd0; + ctrl_word1 <= 32'd0; + prev_frame_counter <= 30'd0; + buf_base_addr <= BUF0_V2; + cur_line <= 9'd0; + beat_count <= 8'd0; + burst_idx <= 1'b0; + first_frame_loaded <= 1'b0; + writer_trusted <= 1'b0; + frame_ready_reg <= 1'b0; + preloading <= 1'b0; + timeout_cnt <= 20'd0; + fifo_wr <= 1'b0; + fifo_wr_data <= 64'd0; + fifo_aclr_cnt <= 4'd0; + mode_out <= 2'd0; + h_offset_out <= 8'sd0; + v_offset_out <= 4'sd0; + line_words <= 9'd160; + scan_lines <= 9'd240; + scan_interlaced <= 1'b0; + two_bursts <= 1'b0; + end + else begin + fifo_wr <= 1'b0; + if(fifo_aclr_cnt != 4'd0) fifo_aclr_cnt <= fifo_aclr_cnt - 4'd1; + if(!ddr_busy) ddr_rd <= 1'b0; + + if(state == ST_WAIT_LINE && ddr_dout_ready) begin + fifo_wr <= 1'b1; + fifo_wr_data <= ddr_dout; + beat_count <= beat_count + 8'd1; + timeout_cnt <= 20'd0; + end + + case(state) + ST_IDLE: begin + if(new_frame_ddr) state <= ST_POLL_CTRL; + end + + ST_POLL_CTRL: begin + if(!ddr_busy) begin + ddr_addr <= CTRL_ADDR; + ddr_burstcnt <= 8'd1; + ddr_rd <= 1'b1; + timeout_cnt <= 20'd0; + state <= ST_WAIT_CTRL; + end + end + + ST_WAIT_CTRL: begin + if(ddr_dout_ready) begin + ctrl_word <= ddr_dout[31:0]; + ctrl_word1 <= ddr_dout[63:32]; + timeout_cnt <= 20'd0; + state <= ST_CHECK_CTRL; + end + else if(timeout_cnt == TIMEOUT_MAX) begin + // Stale frame_ready would scan a dead buffer forever; + // drop back to the noise pattern instead. + frame_ready_reg <= 1'b0; + first_frame_loaded <= 1'b0; + state <= ST_IDLE; + end + else timeout_cnt <= timeout_cnt + 20'd1; + end + + ST_CHECK_CTRL: begin + if(ctrl_word == 32'd0 || !magic_ok) begin + // Writer stopped, never started, or this is not a control + // block at all: revert to idle video and forget the + // previous session. + frame_ready_reg <= 1'b0; + first_frame_loaded <= 1'b0; + // Nothing is publishing right now, so the next block to + // appear started under observation and can be trusted. + writer_trusted <= 1'b1; + prev_frame_counter <= 30'd0; + mode_out <= 2'd0; + h_offset_out <= 8'sd0; + v_offset_out <= 4'sd0; + state <= ST_IDLE; + end + else begin + mode_out <= ctrl_mode; + h_offset_out <= $signed(ctrl_word1[15:8]); + v_offset_out <= $signed(ctrl_word1[7:4]); + line_words <= (ctrl_mode == 2'd1) ? 9'd360 : 9'd176; + scan_lines <= (ctrl_mode == 2'd2) ? 9'd288 : 9'd240; + scan_interlaced <= (ctrl_mode == 2'd1); + two_bursts <= (ctrl_mode == 2'd1); + + if(ctrl_word[31:2] != prev_frame_counter) begin + prev_frame_counter <= ctrl_word[31:2]; + buf_base_addr <= ctrl_word[0] ? BUF1_V2 : BUF0_V2; + cur_line <= 9'd0; + burst_idx <= 1'b0; + preloading <= 1'b1; + fifo_aclr_cnt <= 4'd8; + // A counter moving under a block we already fetched is + // a live writer even if it was there at reset. + if(first_frame_loaded) begin + writer_trusted <= 1'b1; + frame_ready_reg <= 1'b1; + end + state <= ST_READ_LINE; + end + else if(first_frame_loaded) begin + cur_line <= 9'd0; + burst_idx <= 1'b0; + preloading <= 1'b1; + fifo_aclr_cnt <= 4'd8; + state <= ST_READ_LINE; + end + else begin + state <= ST_IDLE; + end + end + end + + ST_READ_LINE: begin + if(!ddr_busy && !fifo_aclr_ddr_active) begin + ddr_addr <= line_base + (burst_idx ? 29'd180 : 29'd0); + ddr_burstcnt <= burst_len; + ddr_rd <= 1'b1; + beat_count <= 8'd0; + timeout_cnt <= 20'd0; + state <= ST_WAIT_LINE; + end + end + + ST_WAIT_LINE: begin + if(beat_count == burst_len) begin + if(two_bursts && !burst_idx) begin + burst_idx <= 1'b1; + state <= ST_READ_LINE; + end + else begin + burst_idx <= 1'b0; + state <= ST_LINE_DONE; + end + end + else if(timeout_cnt == TIMEOUT_MAX) begin + frame_ready_reg <= 1'b0; + first_frame_loaded <= 1'b0; + state <= ST_IDLE; + end + else if(!ddr_dout_ready) timeout_cnt <= timeout_cnt + 20'd1; + end + + ST_LINE_DONE: begin + cur_line <= cur_line + 9'd1; + if(cur_line == scan_lines - 9'd1) begin + first_frame_loaded <= 1'b1; + frame_ready_reg <= writer_trusted; + preloading <= 1'b0; + state <= ST_IDLE; + end + else if(preloading && cur_line < 9'd1) begin + state <= ST_READ_LINE; + end + else begin + preloading <= 1'b0; + state <= ST_WAIT_DISPLAY; + end + end + + ST_WAIT_DISPLAY: begin + if(cur_line < scan_lines && new_line_ddr && !vblank_ddr) state <= ST_READ_LINE; + end + + default: state <= ST_IDLE; + endcase + end +end + +wire [63:0] fifo_rd_data; +wire fifo_empty; +reg fifo_rd; + +// 1024 words: 480i preloads 2 x 360-word lines (720 words peak); the +// progressive modes peak around 368 words with their 176-word lines. +dcfifo #( + .intended_device_family ("Cyclone V"), + .lpm_numwords (1024), + .lpm_showahead ("ON"), + .lpm_type ("dcfifo"), + .lpm_width (64), + .lpm_widthu (10), + .overflow_checking ("ON"), + .rdsync_delaypipe (4), + .underflow_checking ("ON"), + .use_eab ("ON"), + .wrsync_delaypipe (4) +) line_fifo ( + .aclr (fifo_aclr), + .data (fifo_wr_data), + .rdclk (clk_vid), + .rdreq (fifo_rd), + .wrclk (ddr_clk), + .wrreq (fifo_wr), + .q (fifo_rd_data), + .rdempty (fifo_empty), + .wrfull (fifo_full), + .eccstatus(), + .rdfull (), + .rdusedw (), + .wrempty (), + .wrusedw () +); + +reg [63:0] pixel_word; +reg pixel_high; +reg pixel_word_valid; + +wire [31:0] pixel_low = pixel_word[31:0]; +wire [31:0] pixel_high_word = pixel_word[63:32]; + +task automatic output_pixel; + input [31:0] pixel; + begin + // linuxfb write path lands as B,G,R,X in DDR on MiSTer; swap here + // so launcher can keep doing row memcpy with no CPU-side repack. + r_out <= pixel[23:16]; + g_out <= pixel[15:8]; + b_out <= pixel[7:0]; + end +endtask + +always @(posedge clk_vid) begin + if(reset_vid) begin + fifo_rd <= 1'b0; + r_out <= 8'd0; + g_out <= 8'd0; + b_out <= 8'd0; + pixel_word <= 64'd0; + pixel_high <= 1'b0; + pixel_word_valid <= 1'b0; + end + else begin + fifo_rd <= 1'b0; + + if(ce_pix) begin + if(de && frame_ready_vid) begin + if(pixel_word_valid) begin + if(pixel_high) begin + output_pixel(pixel_high_word); + pixel_word_valid <= 1'b0; + pixel_high <= 1'b0; + end + else begin + output_pixel(pixel_low); + pixel_high <= 1'b1; + end + end + else if(!fifo_empty) begin + pixel_word <= fifo_rd_data; + pixel_word_valid <= 1'b1; + pixel_high <= 1'b1; + fifo_rd <= 1'b1; + output_pixel(fifo_rd_data[31:0]); + end + else begin + r_out <= 8'd0; + g_out <= 8'd0; + b_out <= 8'd0; + end + end + else if(de) begin + r_out <= 8'd0; + g_out <= 8'd0; + b_out <= 8'd0; + end + else begin + r_out <= 8'd0; + g_out <= 8'd0; + b_out <= 8'd0; + pixel_high <= 1'b0; + pixel_word_valid <= 1'b0; + end + end + end +end + +endmodule diff --git a/rtl/native_video_timing.sv b/rtl/native_video_timing.sv new file mode 100644 index 00000000..0d442cde --- /dev/null +++ b/rtl/native_video_timing.sv @@ -0,0 +1,187 @@ +// Zaparoo native video timing: standard-definition CRT modes from 27 MHz. +// +// mode 0: 352x240p60 (NTSC) ce_pix = 27/4 = 6.75 MHz, 429x262, 15734.27 Hz +// mode 1: 720x480i60 (CEA-861) ce_pix = 27/2 = 13.5 MHz, 858x525, 15734.27 Hz +// mode 2: 352x288p50 (PAL) ce_pix = 27/4 = 6.75 MHz, 432x312, 15625.00 Hz +// +// mode_in/h_offset_in/v_offset_in are quasi-static (two-flop synchronized by +// the caller) and are latched here at the field wrap so a mid-frame update +// can't corrupt sync. Offsets shift the image by repartitioning front/back +// porch; totals are invariant, so line/frame rates never move. Out-of-range +// offsets are clamped to the supported -8..+8 px / -8..+2 line window, which +// keeps every mode's effective porches at or above 2 px / 1 line. + +module native_video_timing +( + input wire clk, + input wire ce_pix, + input wire reset, + + input wire [1:0] mode_in, + input wire signed [7:0] h_offset_in, // + = right, honored -8..+8 px + input wire signed [3:0] v_offset_in, // + = down, honored -8..+2 lines + + output reg [1:0] mode, // latched active mode; selects the ce_pix divider + output reg field, // 480i field number, 0 in progressive modes + output reg hsync, + output reg vsync, + output reg hblank, + output reg vblank, + output reg de, + output reg [9:0] hcount, + output reg [8:0] vcount, // line within the current field + output reg new_frame, + output reg new_line +); + +localparam [1:0] MODE_NTSC = 2'd0; +localparam [1:0] MODE_480I = 2'd1; +localparam [1:0] MODE_PAL = 2'd2; + +// Per-mode parameter sets (Switchres ntsc/pal presets, CEA-861 for 480i). +// 480i: 525-line frame as two fields of 262 (field 0) and 263 (field 1) +// lines; field 1 additionally asserts vsync half a line late (see below). +reg [9:0] H_ACTIVE, H_FP, H_BP, H_TOTAL; +reg [6:0] H_SYNC; +reg [8:0] V_ACTIVE, V_FP, V_BP, V_TOTAL; +reg [4:0] V_SYNC; + +always @* begin + case(mode) + MODE_480I: begin + H_ACTIVE = 10'd720; H_FP = 10'd19; H_SYNC = 7'd62; H_BP = 10'd57; H_TOTAL = 10'd858; + V_ACTIVE = 9'd240; V_FP = 9'd4; V_SYNC = 5'd3; + V_BP = field ? 9'd16 : 9'd15; + V_TOTAL = field ? 9'd263 : 9'd262; + end + MODE_PAL: begin + H_ACTIVE = 10'd352; H_FP = 10'd11; H_SYNC = 7'd32; H_BP = 10'd37; H_TOTAL = 10'd432; + V_ACTIVE = 9'd288; V_FP = 9'd3; V_SYNC = 5'd3; V_BP = 9'd18; V_TOTAL = 9'd312; + end + default: begin // MODE_NTSC + H_ACTIVE = 10'd352; H_FP = 10'd12; H_SYNC = 7'd32; H_BP = 10'd33; H_TOTAL = 10'd429; + V_ACTIVE = 9'd240; V_FP = 9'd3; V_SYNC = 5'd3; V_BP = 9'd16; V_TOTAL = 9'd262; + end + endcase +end + +wire [1:0] next_mode = (mode_in == 2'd3) ? MODE_NTSC : mode_in; + +function automatic signed [4:0] clamp_h(input signed [7:0] v); + if (v > 8'sd8) clamp_h = 5'sd8; + else if (v < -8'sd8) clamp_h = -5'sd8; + else clamp_h = v[4:0]; +endfunction + +function automatic signed [3:0] clamp_v(input signed [3:0] v); + if (v > 4'sd2) clamp_v = 4'sd2; + else clamp_v = v; +endfunction + +reg signed [4:0] h_offset; +reg signed [3:0] v_offset; + +// Sync starts shift with the offset; two's-complement subtraction in +// unsigned arithmetic yields the correct result at both ends of the range. +wire [9:0] H_SYNC_START = H_ACTIVE + (H_FP - {{5{h_offset[4]}}, h_offset}); +wire [9:0] H_SYNC_END = H_SYNC_START + H_SYNC; +wire [8:0] V_SYNC_START = V_ACTIVE + (V_FP - {{5{v_offset[3]}}, v_offset}); +wire [8:0] V_SYNC_END = V_SYNC_START + V_SYNC; + +// In 480i the odd field's vsync transitions half a scanline (H_TOTAL/2 +// ce_pix clocks) after the line boundary, interleaving its scanlines +// between the even field's. Without this both fields land on the same +// scanlines (line pairing). vs_step fires once per line at the point a +// vsync edge may occur; vs_line is the line whose start (field 0) or +// midpoint (field 1) that edge aligns to. +wire vs_step = field ? (hcount == (H_TOTAL >> 1) - 10'd1) + : (hcount == H_TOTAL - 10'd1); +wire [8:0] vs_line = field ? vcount : (vcount + 9'd1); + +wire line_wrap = (hcount == H_TOTAL - 10'd1); +wire field_wrap = line_wrap && (vcount == V_TOTAL - 9'd1); + +always @(posedge clk) begin + if(reset) begin + mode <= MODE_NTSC; + field <= 1'b0; + h_offset <= 5'sd0; + v_offset <= 4'sd0; + hcount <= 10'd0; + vcount <= 9'd0; + hsync <= 1'b0; + vsync <= 1'b0; + hblank <= 1'b0; + vblank <= 1'b0; + de <= 1'b1; + new_frame <= 1'b0; + new_line <= 1'b0; + end + else if(ce_pix) begin + reg next_hblank; + reg next_vblank; + + new_frame <= 1'b0; + new_line <= 1'b0; + + if(line_wrap) begin + hcount <= 10'd0; + if(vcount == V_TOTAL - 9'd1) vcount <= 9'd0; + else vcount <= vcount + 9'd1; + end + else begin + hcount <= hcount + 10'd1; + end + + // Mode and trims apply only at the field wrap, with counters at + // zero, so every line of a field is cut from one parameter set. + if(field_wrap) begin + mode <= next_mode; + if(next_mode != MODE_480I) field <= 1'b0; + h_offset <= clamp_h(h_offset_in); + v_offset <= clamp_v(v_offset_in); + end + + if(hcount == H_ACTIVE - 10'd1) hblank <= 1'b1; + else if(line_wrap) hblank <= 1'b0; + + if(hcount == H_SYNC_START - 10'd1) hsync <= 1'b1; + else if(hcount == H_SYNC_END - 10'd1) hsync <= 1'b0; + + if(vs_step) begin + if(vs_line == V_SYNC_START) vsync <= 1'b1; + else if(vs_line == V_SYNC_END) vsync <= 1'b0; + end + + if(line_wrap) begin + if(vcount == V_ACTIVE - 9'd1) vblank <= 1'b1; + else if(vcount == V_TOTAL - 9'd1) vblank <= 1'b0; + end + + if(hcount == H_ACTIVE - 10'd1) new_line <= 1'b1; + // Field flips at the START of vblank, not the field wrap: the + // reader preloads the next field's first lines right after + // new_frame, so the field it reads must already be the one about + // to be displayed. The vsync inside this blanking interval then + // uses the new field's phase, which keeps the half-line + // alternation intact (intervals stay exactly 262.5 lines). + if(line_wrap && vcount == V_ACTIVE - 9'd1) begin + new_frame <= 1'b1; + field <= (mode == MODE_480I) ? ~field : 1'b0; + end + + next_hblank = hblank; + if(hcount == H_ACTIVE - 10'd1) next_hblank = 1'b1; + else if(line_wrap) next_hblank = 1'b0; + + next_vblank = vblank; + if(line_wrap) begin + if(vcount == V_ACTIVE - 9'd1) next_vblank = 1'b1; + else if(vcount == V_TOTAL - 9'd1) next_vblank = 1'b0; + end + + de <= ~next_hblank & ~next_vblank; + end +end + +endmodule diff --git a/rtl/native_video_top.sv b/rtl/native_video_top.sv new file mode 100644 index 00000000..0b55f623 --- /dev/null +++ b/rtl/native_video_top.sv @@ -0,0 +1,130 @@ +// Zaparoo native video wrapper: timing + RGBX8888 DDR reader. +// +// Mode and centering trims arrive from the launcher through the DDR control +// block (parsed by the reader in the ddr_clk domain). They are quasi-static: +// two-flop synchronized here into the video clock, then latched by the +// timing module at the field wrap. + +module native_video_top +( + input wire clk_sys, + input wire clk_vid, + input wire ce_pix, + input wire reset, + + input wire ddr_busy, + output wire [7:0] ddr_burstcnt, + output wire [28:0] ddr_addr, + input wire [63:0] ddr_dout, + input wire ddr_dout_ready, + output wire ddr_rd, + output wire [63:0] ddr_din, + output wire [7:0] ddr_be, + output wire ddr_we, + + output wire [7:0] vga_r, + output wire [7:0] vga_g, + output wire [7:0] vga_b, + output wire vga_hs, + output wire vga_vs, + output wire vga_de, + output wire vga_hblank, + output wire vga_vblank, + output wire [8:0] vga_vcount, + output wire vga_new_frame, + + output wire [1:0] vga_mode, // active timing mode; selects ce_pix divider + output wire vga_field, // 480i field number (VGA_F1) + output wire active +); + +wire tim_hs; +wire tim_vs; +wire tim_hblank; +wire tim_vblank; +wire tim_de; +wire [9:0] tim_hcount; +wire [8:0] tim_vcount; +wire tim_new_frame; +wire tim_new_line; +wire tim_field; + +wire [1:0] rd_mode; +wire signed [7:0] rd_h_offset; +wire signed [3:0] rd_v_offset; + +reg [1:0] mode_sync [1:0]; +reg [7:0] h_offset_sync [1:0]; +reg [3:0] v_offset_sync [1:0]; +always @(posedge clk_vid) begin + mode_sync[0] <= rd_mode; mode_sync[1] <= mode_sync[0]; + h_offset_sync[0] <= rd_h_offset; h_offset_sync[1] <= h_offset_sync[0]; + v_offset_sync[0] <= rd_v_offset; v_offset_sync[1] <= v_offset_sync[0]; +end + +native_video_timing timing +( + .clk (clk_vid), + .ce_pix (ce_pix), + .reset (reset), + .mode_in (mode_sync[1]), + .h_offset_in ($signed(h_offset_sync[1])), + .v_offset_in ($signed(v_offset_sync[1])), + .mode (vga_mode), + .field (tim_field), + .hsync (tim_hs), + .vsync (tim_vs), + .hblank (tim_hblank), + .vblank (tim_vblank), + .de (tim_de), + .hcount (tim_hcount), + .vcount (tim_vcount), + .new_frame (tim_new_frame), + .new_line (tim_new_line) +); + +wire frame_ready; + +native_video_reader reader +( + .ddr_clk (clk_sys), + .ddr_busy (ddr_busy), + .ddr_burstcnt (ddr_burstcnt), + .ddr_addr (ddr_addr), + .ddr_dout (ddr_dout), + .ddr_dout_ready (ddr_dout_ready), + .ddr_rd (ddr_rd), + .ddr_din (ddr_din), + .ddr_be (ddr_be), + .ddr_we (ddr_we), + + .clk_vid (clk_vid), + .ce_pix (ce_pix), + .reset (reset), + .de (tim_de), + .vblank (tim_vblank), + .new_frame (tim_new_frame), + .new_line (tim_new_line), + .field (tim_field), + + .mode_out (rd_mode), + .h_offset_out (rd_h_offset), + .v_offset_out (rd_v_offset), + + .r_out (vga_r), + .g_out (vga_g), + .b_out (vga_b), + .frame_ready (frame_ready) +); + +assign vga_hs = tim_hs; +assign vga_vs = tim_vs; +assign vga_de = tim_de; +assign vga_hblank = tim_hblank; +assign vga_vblank = tim_vblank; +assign vga_vcount = tim_vcount; +assign vga_new_frame = tim_new_frame; +assign vga_field = tim_field; +assign active = frame_ready; + +endmodule diff --git a/rtl/pll/pll_0002.v b/rtl/pll/pll_0002.v index 7320a578..4c7ed140 100644 --- a/rtl/pll/pll_0002.v +++ b/rtl/pll/pll_0002.v @@ -25,7 +25,7 @@ module pll_0002( .output_clock_frequency0("100.000000 MHz"), .phase_shift0("0 ps"), .duty_cycle0(50), - .output_clock_frequency1("20.000000 MHz"), + .output_clock_frequency1("27027027 Hz"), .phase_shift1("0 ps"), .duty_cycle1(50), .output_clock_frequency2("0 MHz"), diff --git a/rtl/pll_video.qip b/rtl/pll_video.qip new file mode 100644 index 00000000..e8d4b6d1 --- /dev/null +++ b/rtl/pll_video.qip @@ -0,0 +1,6 @@ +set_global_assignment -name VERILOG_FILE [file join $::quartus(qip_path) "pll_video.v"] +set_global_assignment -name VERILOG_FILE [file join $::quartus(qip_path) "pll_video/pll_video_0002.v"] + +set_instance_assignment -name PLL_COMPENSATION_MODE DIRECT -to "*pll_video_0002*|altera_pll:altera_pll_i*|*" +set_instance_assignment -name PLL_AUTO_RESET ON -to "*pll_video_0002*|altera_pll:altera_pll_i*|*" +set_instance_assignment -name PLL_BANDWIDTH_PRESET AUTO -to "*pll_video_0002*|altera_pll:altera_pll_i*|*" diff --git a/rtl/pll_video.v b/rtl/pll_video.v new file mode 100644 index 00000000..1fbb347d --- /dev/null +++ b/rtl/pll_video.v @@ -0,0 +1,25 @@ +// Dedicated video PLL: exact 27.000000 MHz for SD CRT timing. +// +// This cannot come from the main PLL: every output of a PLL divides the +// same VCO, and the smallest common multiple of 100 MHz (clk_sys) and +// 27 MHz is 2700 MHz — outside the Cyclone V's 600-1600 MHz VCO range. +// (That constraint is why stock MiSTer uses 27.027027 MHz: 1000 MHz / 37 +// is the closest to 27 MHz a VCO shared with 100 MHz can reach.) +// Standalone, 27 MHz is exact: VCO = 50 MHz x 27 = 1350 MHz, C = 50. + +`timescale 1 ps / 1 ps +module pll_video ( + input wire refclk, // refclk.clk + input wire rst, // reset.reset + output wire outclk_0, // outclk0.clk + output wire locked // locked.export + ); + + pll_video_0002 pll_video_inst ( + .refclk (refclk), // refclk.clk + .rst (rst), // reset.reset + .outclk_0 (outclk_0), // outclk0.clk + .locked (locked) // locked.export + ); + +endmodule diff --git a/rtl/pll_video/pll_video_0002.v b/rtl/pll_video/pll_video_0002.v new file mode 100644 index 00000000..0980b88b --- /dev/null +++ b/rtl/pll_video/pll_video_0002.v @@ -0,0 +1,86 @@ +`timescale 1ns/10ps +module pll_video_0002( + + // interface 'refclk' + input wire refclk, + + // interface 'reset' + input wire rst, + + // interface 'outclk0' + output wire outclk_0, + + // interface 'locked' + output wire locked +); + + altera_pll #( + .fractional_vco_multiplier("false"), + .reference_clock_frequency("50.0 MHz"), + .operation_mode("direct"), + .number_of_clocks(1), + .output_clock_frequency0("27.000000 MHz"), + .phase_shift0("0 ps"), + .duty_cycle0(50), + .output_clock_frequency1("0 MHz"), + .phase_shift1("0 ps"), + .duty_cycle1(50), + .output_clock_frequency2("0 MHz"), + .phase_shift2("0 ps"), + .duty_cycle2(50), + .output_clock_frequency3("0 MHz"), + .phase_shift3("0 ps"), + .duty_cycle3(50), + .output_clock_frequency4("0 MHz"), + .phase_shift4("0 ps"), + .duty_cycle4(50), + .output_clock_frequency5("0 MHz"), + .phase_shift5("0 ps"), + .duty_cycle5(50), + .output_clock_frequency6("0 MHz"), + .phase_shift6("0 ps"), + .duty_cycle6(50), + .output_clock_frequency7("0 MHz"), + .phase_shift7("0 ps"), + .duty_cycle7(50), + .output_clock_frequency8("0 MHz"), + .phase_shift8("0 ps"), + .duty_cycle8(50), + .output_clock_frequency9("0 MHz"), + .phase_shift9("0 ps"), + .duty_cycle9(50), + .output_clock_frequency10("0 MHz"), + .phase_shift10("0 ps"), + .duty_cycle10(50), + .output_clock_frequency11("0 MHz"), + .phase_shift11("0 ps"), + .duty_cycle11(50), + .output_clock_frequency12("0 MHz"), + .phase_shift12("0 ps"), + .duty_cycle12(50), + .output_clock_frequency13("0 MHz"), + .phase_shift13("0 ps"), + .duty_cycle13(50), + .output_clock_frequency14("0 MHz"), + .phase_shift14("0 ps"), + .duty_cycle14(50), + .output_clock_frequency15("0 MHz"), + .phase_shift15("0 ps"), + .duty_cycle15(50), + .output_clock_frequency16("0 MHz"), + .phase_shift16("0 ps"), + .duty_cycle16(50), + .output_clock_frequency17("0 MHz"), + .phase_shift17("0 ps"), + .duty_cycle17(50), + .pll_type("General"), + .pll_subtype("General") + ) altera_pll_i ( + .rst (rst), + .outclk (outclk_0), + .locked (locked), + .fboutclk ( ), + .fbclk (1'b0), + .refclk (refclk) + ); +endmodule diff --git a/rtl/zaparoo_bootstrap_video.sv b/rtl/zaparoo_bootstrap_video.sv new file mode 100644 index 00000000..117137fb --- /dev/null +++ b/rtl/zaparoo_bootstrap_video.sv @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Native CRT frames take priority; idle stays black except behind stock OSD. +`timescale 1ns/1ps +module zaparoo_bootstrap_video ( + input wire native_active, + input wire [23:0] native_rgb, + input wire show_snow, + input wire [23:0] snow_rgb, + input wire de_in, hs_in, vs_in, + output wire [23:0] rgb_out, + output wire de_out, hs_out, vs_out +); + wire [23:0] black_rgb; + mister_magik_bootstrap_black black ( + .rgb_in(native_rgb), .de_in(de_in), .hs_in(hs_in), .vs_in(vs_in), + .rgb_out(black_rgb), .de_out(de_out), .hs_out(hs_out), .vs_out(vs_out) + ); + assign rgb_out = native_active ? native_rgb : + (show_snow && de_in) ? snow_rgb : black_rgb; +endmodule diff --git a/rtl/zaparoo_pixel_enable.sv b/rtl/zaparoo_pixel_enable.sv new file mode 100644 index 00000000..45418eb9 --- /dev/null +++ b/rtl/zaparoo_pixel_enable.sv @@ -0,0 +1,21 @@ +`timescale 1ns/1ps +// Keep the exported pixel enable idle while reset is held or the video PLL +// is unlocked. Restart the divider phase when video becomes available. +module zaparoo_pixel_enable ( + input wire clk, + input wire reset, + input wire [1:0] mode, + output reg ce_pix +); + reg [1:0] ce_div; + always @(posedge clk) begin + if (reset) begin + ce_div <= 2'd0; + ce_pix <= 1'b0; + end + else begin + ce_div <= ce_div + 2'd1; + ce_pix <= (mode == 2'd1) ? ce_div[0] : (ce_div == 2'd0); + end + end +endmodule diff --git a/rtl/zaparoo_snow_phase.sv b/rtl/zaparoo_snow_phase.sv new file mode 100644 index 00000000..725d2836 --- /dev/null +++ b/rtl/zaparoo_snow_phase.sv @@ -0,0 +1,18 @@ +`timescale 1ns/1ps +// Upstream Menu advances its cosine phase by six once per displayed frame. +// native_video_timing holds new_frame until the next pixel-enable edge. +module zaparoo_snow_phase ( + input wire clk, + input wire reset, + input wire ce_pix, + input wire new_frame, + input wire [8:0] vcount, + output wire [9:0] phase +); + reg [9:0] frame_phase = 0; + always @(posedge clk) begin + if (reset) frame_phase <= 0; + else if (ce_pix && new_frame) frame_phase <= frame_phase + 10'd6; + end + assign phase = frame_phase + {vcount[7:0], 2'b00}; +endmodule diff --git a/sys/mister_magik_bootstrap_black.sv b/sys/mister_magik_bootstrap_black.sv new file mode 100644 index 00000000..6536b5fb --- /dev/null +++ b/sys/mister_magik_bootstrap_black.sv @@ -0,0 +1,23 @@ +// Copyright (C) 2026 Nigel Breslaw +// SPDX-License-Identifier: GPL-3.0-or-later + +`timescale 1ns/1ps + +module mister_magik_bootstrap_black +( + input wire [23:0] rgb_in, + input wire de_in, + input wire hs_in, + input wire vs_in, + output wire [23:0] rgb_out, + output wire de_out, + output wire hs_out, + output wire vs_out +); + + assign rgb_out = 24'd0; + assign de_out = de_in; + assign hs_out = hs_in; + assign vs_out = vs_in; + +endmodule diff --git a/sys/mister_magik_latch_protocol.svh b/sys/mister_magik_latch_protocol.svh new file mode 100644 index 00000000..b83f71eb --- /dev/null +++ b/sys/mister_magik_latch_protocol.svh @@ -0,0 +1,134 @@ +/* Upstream: https://github.com/NigelBreslaw/MiSTer-MagiK + * Revision d70c141fd25996459867dce341070a0a81a9683d, + * mister/platform/fpga/menu-vblank-latch/mister_magik_latch_protocol.svh. + * Includes 1080p limits/CRC from local Zaparoo demo Menu_MiSTer snapshot + * 8ab57fdf153dd2fad733aa9691942c88e23c02ef. + * No local generator; keep constants aligned with the paired frontend. */ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* Copyright (C) 2026 Nigel Breslaw */ + +localparam [7:0] MAGIK_UIO_SET_FBUF_LATCH = 8'h57; +localparam [7:0] MAGIK_UIO_GET_FBUF_LATCH = 8'h58; +localparam [7:0] MAGIK_UIO_GET_FBUF_LATCH_CAPS = 8'h59; +localparam [7:0] MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS = 8'h5A; +localparam [7:0] MAGIK_UIO_GET_FBUF_LATCH_RECEIPT = 8'h5B; +localparam [15:0] MAGIK_FBUF_LATCH_MAGIC = 16'h4D47; +localparam [15:0] MAGIK_FBUF_STATUS_MAGIC = 16'h4D48; +localparam [15:0] MAGIK_FBUF_CAPS_MAGIC = 16'h4D49; +localparam [15:0] MAGIK_FBUF_DIAGNOSTICS_MAGIC = 16'h4D4A; +localparam [15:0] MAGIK_FBUF_RECEIPT_MAGIC = 16'h4D4B; +localparam [15:0] MAGIK_FBUF_PROTOCOL_VERSION = 16'd4; +localparam [15:0] MAGIK_FBUF_PROTOCOL_V4 = 16'd4; +localparam [15:0] MAGIK_FBUF_CAPS_FLAGS = 16'h01FF; +// Zaparoo fork extension: native 1080p RGB565 limits (paired with the +// enlarged scanout-slots module in kernel/scanout-slots/). +localparam [15:0] MAGIK_FBUF_MAX_WIDTH = 16'd1920; +localparam [15:0] MAGIK_FBUF_MAX_HEIGHT = 16'd1080; +localparam [15:0] MAGIK_FBUF_MAX_STRIDE = 16'd3840; +localparam [4:0] MAGIK_FBUF_V4_CAPS_WORDS = 5'd6; +localparam [4:0] MAGIK_FBUF_V4_SET_PAYLOAD_WORDS = 5'd11; +localparam [4:0] MAGIK_FBUF_V4_SET_WORDS = 5'd12; +localparam [4:0] MAGIK_FBUF_V4_STATUS_WORDS = 5'd16; +localparam [4:0] MAGIK_FBUF_V4_DIAGNOSTICS_WORDS = 5'd7; +localparam [4:0] MAGIK_FBUF_V4_RECEIPT_WORDS = 5'd11; + +localparam [15:0] MAGIK_RECEIPT_NONE = 16'd0; +localparam [15:0] MAGIK_RECEIPT_ACCEPTED = 16'd1; +localparam [15:0] MAGIK_RECEIPT_REJECTED = 16'd2; + +localparam [15:0] MAGIK_CAP_RGB565 = 16'h0001; +localparam [15:0] MAGIK_CAP_DOUBLE_BUFFER = 16'h0002; +localparam [15:0] MAGIK_CAP_VARIABLE_GEOMETRY = 16'h0004; +localparam [15:0] MAGIK_CAP_TRANSACTIONAL_POST = 16'h0008; +localparam [15:0] MAGIK_CAP_COHERENT_STATUS = 16'h0010; +localparam [15:0] MAGIK_CAP_STATUS_CRC = 16'h0020; +localparam [15:0] MAGIK_CAP_POST_CRC = 16'h0040; +localparam [15:0] MAGIK_CAP_REJECTION_CONTEXT = 16'h0080; +localparam [15:0] MAGIK_CAP_AUTHORITATIVE_RECEIPT = 16'h0100; + +localparam integer MAGIK_STATUS_ACTIVE_ENABLED = 0; +localparam integer MAGIK_STATUS_PENDING_ENABLED = 1; +localparam integer MAGIK_STATUS_PENDING = 2; +localparam integer MAGIK_STATUS_MAGIK_OWNERSHIP = 3; +localparam integer MAGIK_STATUS_REJECT_REASON_SHIFT = 4; +localparam integer MAGIK_STATUS_REJECT_REASON_WIDTH = 4; + +localparam [3:0] MAGIK_REJECT_NONE = 4'd0; +localparam [3:0] MAGIK_REJECT_MISSING_WORD = 4'd1; +localparam [3:0] MAGIK_REJECT_DUPLICATE_WORD = 4'd2; +localparam [3:0] MAGIK_REJECT_OUT_OF_ORDER = 4'd3; +localparam [3:0] MAGIK_REJECT_POST_CLOSE = 4'd4; +localparam [3:0] MAGIK_REJECT_BAD_CRC = 4'd5; +localparam [3:0] MAGIK_REJECT_INVALID_MODE = 4'd6; +localparam [3:0] MAGIK_REJECT_INVALID_BASE = 4'd7; +localparam [3:0] MAGIK_REJECT_INVALID_GEOMETRY = 4'd8; +localparam [3:0] MAGIK_REJECT_INVALID_STRIDE = 4'd9; +localparam [3:0] MAGIK_REJECT_INVALID_BOUNDS = 4'd10; +localparam [3:0] MAGIK_REJECT_ADDRESS_WRAP = 4'd11; +localparam [3:0] MAGIK_REJECT_RESTARTED = 4'd12; +localparam [3:0] MAGIK_REJECT_SHIFTED_WORD = 4'd13; +localparam [3:0] MAGIK_REJECT_PENDING_BUSY = 4'd14; +localparam [3:0] MAGIK_REJECT_RESERVED = 4'd15; + +localparam [15:0] MAGIK_CRC_POLYNOMIAL = 16'h1021; +localparam [15:0] MAGIK_CRC_INITIAL = 16'hFFFF; +localparam [15:0] MAGIK_CRC_FINAL_XOR = 16'h0000; + +// Caps golden updated for the Zaparoo 1080p limit extension: +// payload [4, 0x1FF, 1920, 1080, 3840], CRC-16/CCITT-FALSE 0x2984. +localparam [15:0] MAGIK_GOLDEN_CAPS_V4_0 = 16'h0004; +localparam [15:0] MAGIK_GOLDEN_CAPS_V4_1 = 16'h01FF; +localparam [15:0] MAGIK_GOLDEN_CAPS_V4_2 = 16'h0780; +localparam [15:0] MAGIK_GOLDEN_CAPS_V4_3 = 16'h0438; +localparam [15:0] MAGIK_GOLDEN_CAPS_V4_4 = 16'h0F00; +localparam [15:0] MAGIK_GOLDEN_CAPS_V4_CRC = 16'h2984; + +localparam [15:0] MAGIK_GOLDEN_SET_V4_0 = 16'h8014; +localparam [15:0] MAGIK_GOLDEN_SET_V4_1 = 16'h9000; +localparam [15:0] MAGIK_GOLDEN_SET_V4_2 = 16'h227E; +localparam [15:0] MAGIK_GOLDEN_SET_V4_3 = 16'h03C0; +localparam [15:0] MAGIK_GOLDEN_SET_V4_4 = 16'h021C; +localparam [15:0] MAGIK_GOLDEN_SET_V4_5 = 16'h0000; +localparam [15:0] MAGIK_GOLDEN_SET_V4_6 = 16'h03BF; +localparam [15:0] MAGIK_GOLDEN_SET_V4_7 = 16'h0000; +localparam [15:0] MAGIK_GOLDEN_SET_V4_8 = 16'h021B; +localparam [15:0] MAGIK_GOLDEN_SET_V4_9 = 16'h0780; +localparam [15:0] MAGIK_GOLDEN_SET_V4_10 = 16'h002B; +localparam [15:0] MAGIK_GOLDEN_SET_V4_CRC = 16'h56F5; + +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_0 = 16'h002A; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_1 = 16'h002B; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_2 = 16'h000F; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_3 = 16'h0003; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_4 = 16'h0004; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_5 = 16'h9000; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_6 = 16'h227E; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_7 = 16'h03C0; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_8 = 16'h021C; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_9 = 16'h0780; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_10 = 16'h0007; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_11 = 16'h0009; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_12 = 16'h0064; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_13 = 16'h0065; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_14 = 16'h0065; +localparam [15:0] MAGIK_GOLDEN_STATUS_V4_CRC = 16'h917A; + +localparam [15:0] MAGIK_GOLDEN_DIAGNOSTICS_V4_0 = 16'h0007; +localparam [15:0] MAGIK_GOLDEN_DIAGNOSTICS_V4_1 = 16'h0001; +localparam [15:0] MAGIK_GOLDEN_DIAGNOSTICS_V4_2 = 16'h000B; +localparam [15:0] MAGIK_GOLDEN_DIAGNOSTICS_V4_3 = 16'h0000; +localparam [15:0] MAGIK_GOLDEN_DIAGNOSTICS_V4_4 = 16'h0058; +localparam [15:0] MAGIK_GOLDEN_DIAGNOSTICS_V4_5 = 16'h0000; +localparam [15:0] MAGIK_GOLDEN_DIAGNOSTICS_V4_CRC = 16'hEF7D; + +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_0 = 16'h0065; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_1 = 16'h002B; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_2 = 16'h0001; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_3 = 16'h0065; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_4 = 16'h002B; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_5 = 16'h0065; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_6 = 16'h002B; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_7 = 16'h0064; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_8 = 16'h002A; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_9 = 16'h0000; +localparam [15:0] MAGIK_GOLDEN_RECEIPT_V4_CRC = 16'h5881; diff --git a/sys/mister_magik_latch_sys_top_bridge.sv b/sys/mister_magik_latch_sys_top_bridge.sv new file mode 100644 index 00000000..201fa814 --- /dev/null +++ b/sys/mister_magik_latch_sys_top_bridge.sv @@ -0,0 +1,123 @@ +// Copyright (C) 2026 Nigel Breslaw +// SPDX-License-Identifier: GPL-3.0-or-later + +`timescale 1ns/1ps +`default_nettype none + +// Production adapter between Main's UIO word stream and the latch protocol. +// Its independent command counter follows the same io_uio/io_strobe framing as +// sys_top, so the exact adapter driven in simulation is the one built into RBFs. +module mister_magik_latch_sys_top_bridge ( + input wire clk_sys, + input wire hdmi_vbl, + input wire io_uio, + input wire io_strobe, + input wire [15:0] io_din, + + input wire active_lfb_en, + input wire [31:0] active_lfb_base, + input wire [11:0] active_lfb_width, + input wire [11:0] active_lfb_height, + input wire [13:0] active_lfb_stride, + + output wire response_valid, + output wire [15:0] response_data, + output wire apply, + output wire apply_accepted, + output wire legacy_write, + output wire [3:0] active_word_index, + + output wire route_en, + output wire route_flt, + output wire [5:0] route_fmt, + output wire [11:0] route_width, + output wire [11:0] route_height, + output wire [11:0] route_hmin, + output wire [11:0] route_hmax, + output wire [11:0] route_vmin, + output wire [11:0] route_vmax, + output wire [31:0] route_base, + output wire [13:0] route_stride, + + output wire pending, + output wire [15:0] pending_seq, + output wire [15:0] active_seq, + output wire [15:0] post_count, + output wire [15:0] flip_count, + output wire [15:0] drop_count, + output wire [15:0] reject_count, + output wire [15:0] active_route_epoch +); + + reg [7:0] command = 8'd0; + reg has_command = 1'b0; + reg [7:0] word_count = 8'd0; + + wire command_start = io_uio && io_strobe && !has_command; + wire command_data = io_uio && io_strobe && has_command; + wire [7:0] command_id = has_command ? command : io_din[7:0]; + assign active_word_index = word_count[3:0]; + assign legacy_write = + command_data && (command == 8'h2f) && (word_count < 8'd10); + assign apply_accepted = apply && !legacy_write; + + always @(posedge clk_sys) begin + if(!io_uio) begin + command <= 8'd0; + has_command <= 1'b0; + word_count <= 8'd0; + end + else if(io_strobe) begin + if(!has_command) begin + command <= io_din[7:0]; + has_command <= 1'b1; + word_count <= 8'd0; + end + else begin + word_count <= word_count + 1'd1; + end + end + end + + mister_magik_vblank_latch latch ( + .clk_sys(clk_sys), + .hdmi_vbl(hdmi_vbl), + .cmd_start(command_start), + .cmd_data(command_data), + .cmd_id(command_id), + .word_index(active_word_index), + .data_in(io_din), + .active_lfb_en(active_lfb_en), + .active_lfb_base(active_lfb_base), + .active_lfb_width(active_lfb_width), + .active_lfb_height(active_lfb_height), + .active_lfb_stride(active_lfb_stride), + .apply_accepted(apply_accepted), + .legacy_write(legacy_write), + .response_valid(response_valid), + .response_data(response_data), + .apply(apply), + .route_en(route_en), + .route_flt(route_flt), + .route_fmt(route_fmt), + .route_width(route_width), + .route_height(route_height), + .route_hmin(route_hmin), + .route_hmax(route_hmax), + .route_vmin(route_vmin), + .route_vmax(route_vmax), + .route_base(route_base), + .route_stride(route_stride), + .pending(pending), + .pending_seq(pending_seq), + .active_seq(active_seq), + .post_count(post_count), + .flip_count(flip_count), + .drop_count(drop_count), + .reject_count(reject_count), + .active_route_epoch(active_route_epoch) + ); + +endmodule + +`default_nettype wire diff --git a/sys/mister_magik_vblank_latch.sv b/sys/mister_magik_vblank_latch.sv new file mode 100644 index 00000000..03a405e8 --- /dev/null +++ b/sys/mister_magik_vblank_latch.sv @@ -0,0 +1,581 @@ +// Copyright (C) 2026 Nigel Breslaw +// SPDX-License-Identifier: GPL-3.0-or-later + +`timescale 1ns/1ps +`default_nettype none + +module mister_magik_vblank_latch ( + input wire clk_sys, + input wire hdmi_vbl, + input wire cmd_start, + input wire cmd_data, + input wire [7:0] cmd_id, + input wire [3:0] word_index, + input wire [15:0] data_in, + + input wire active_lfb_en, + input wire [31:0] active_lfb_base, + input wire [11:0] active_lfb_width, + input wire [11:0] active_lfb_height, + input wire [13:0] active_lfb_stride, + input wire apply_accepted, + input wire legacy_write, + + output wire response_valid, + output reg [15:0] response_data, + output wire apply, + + output reg route_en = 1'b0, + output reg route_flt = 1'b0, + output reg [5:0] route_fmt = 6'd0, + output reg [11:0] route_width = 12'd0, + output reg [11:0] route_height = 12'd0, + output reg [11:0] route_hmin = 12'd0, + output reg [11:0] route_hmax = 12'd0, + output reg [11:0] route_vmin = 12'd0, + output reg [11:0] route_vmax = 12'd0, + output reg [31:0] route_base = 32'd0, + output reg [13:0] route_stride = 14'd0, + + output reg pending = 1'b0, + output reg [15:0] pending_seq = 16'd0, + output reg [15:0] active_seq = 16'd0, + output reg [15:0] post_count = 16'd0, + output reg [15:0] flip_count = 16'd0, + output reg [15:0] drop_count = 16'd0, + output reg [15:0] reject_count = 16'd0, + output reg [15:0] active_route_epoch = 16'd0 +); + + `include "mister_magik_latch_protocol.svh" + + function automatic [15:0] crc_byte; + input [15:0] current; + input [7:0] value; + integer bit_index; + reg [15:0] next; + begin + next = current ^ {value, 8'h00}; + for(bit_index = 0; bit_index < 8; bit_index = bit_index + 1) begin + if(next[15]) next = (next << 1) ^ MAGIK_CRC_POLYNOMIAL; + else next = next << 1; + end + crc_byte = next; + end + endfunction + + function automatic [15:0] crc_word; + input [15:0] current; + input [15:0] value; + begin + crc_word = crc_byte(crc_byte(current, value[15:8]), value[7:0]); + end + endfunction + + function automatic [15:0] crc_header; + input [7:0] command; + input [15:0] non_crc_words; + reg [15:0] next; + begin + next = crc_word(MAGIK_CRC_INITIAL, {8'd0, command}); + next = crc_word(next, MAGIK_FBUF_PROTOCOL_VERSION); + crc_header = crc_word(next, non_crc_words); + end + endfunction + + reg rx_open = 1'b0; + reg rx_faulted = 1'b0; + reg [3:0] rx_expected = 4'd0; + reg [10:0] rx_mask = 11'd0; + reg [15:0] rx_crc = 16'd0; + reg [15:0] rx_mode = 16'd0; + reg [31:0] rx_base = 32'd0; + reg [15:0] rx_width_word = 16'd0; + reg [15:0] rx_height_word = 16'd0; + reg [15:0] rx_hmin_word = 16'd0; + reg [15:0] rx_hmax_word = 16'd0; + reg [15:0] rx_vmin_word = 16'd0; + reg [15:0] rx_vmax_word = 16'd0; + reg [15:0] rx_stride_word = 16'd0; + reg [15:0] rx_seq = 16'd0; + reg [25:0] rx_row_span = 26'd0; + reg rx_address_wrap = 1'b0; + + reg [3:0] last_reject_reason = MAGIK_REJECT_NONE; + reg [15:0] last_reject_expected_index = 16'hffff; + reg [15:0] last_reject_observed_index = 16'hffff; + reg [15:0] last_reject_command = 16'd0; + reg [15:0] last_reject_receiver_flags = 16'd0; + reg magik_ownership = 1'b0; + reg [15:0] attempted_transaction = 16'd0; + reg [15:0] accepted_transaction = 16'd0; + reg [15:0] pending_transaction = 16'd0; + reg [15:0] active_transaction = 16'd0; + reg [15:0] accepted_seq = 16'd0; + reg [15:0] receipt_attempted_transaction = 16'd0; + reg [15:0] receipt_attempted_sequence = 16'd0; + reg [15:0] receipt_disposition = MAGIK_RECEIPT_NONE; + reg [3:0] receipt_reject_reason = MAGIK_REJECT_NONE; + + // Read commands are serialized, so one bank preserves each command-start + // snapshot without carrying three mutually exclusive register arrays. + reg [15:0] response_snapshot [0:14]; + reg [15:0] tx_crc = 16'd0; + reg [3:0] tx_expected = 4'd0; + reg [7:0] tx_command = 8'd0; + + (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) + reg vbl_meta = 1'b0; + (* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *) + reg vbl_sys = 1'b0; + reg vbl_old = 1'b0; + wire vbl_rise = ~vbl_old & vbl_sys; + assign apply = pending && vbl_rise; + + wire [15:0] live_status_flags = + ({12'd0, last_reject_reason} << MAGIK_STATUS_REJECT_REASON_SHIFT) | + (active_lfb_en ? (16'd1 << MAGIK_STATUS_ACTIVE_ENABLED) : 16'd0) | + ((pending && route_en) ? (16'd1 << MAGIK_STATUS_PENDING_ENABLED) : 16'd0) | + (pending ? (16'd1 << MAGIK_STATUS_PENDING) : 16'd0) | + (magik_ownership ? (16'd1 << MAGIK_STATUS_MAGIK_OWNERSHIP) : 16'd0); + + wire rx_reserved_fields = + (|rx_width_word[15:12]) || (|rx_height_word[15:12]) || + (|rx_hmin_word[15:12]) || (|rx_hmax_word[15:12]) || + (|rx_vmin_word[15:12]) || (|rx_vmax_word[15:12]) || + (|rx_stride_word[15:14]); + wire [11:0] rx_width = rx_width_word[11:0]; + wire [11:0] rx_height = rx_height_word[11:0]; + wire [11:0] rx_hmin = rx_hmin_word[11:0]; + wire [11:0] rx_hmax = rx_hmax_word[11:0]; + wire [11:0] rx_vmin = rx_vmin_word[11:0]; + wire [11:0] rx_vmax = rx_vmax_word[11:0]; + wire [13:0] rx_stride = rx_stride_word[13:0]; + wire [11:0] rx_height_minus_one = rx_height - 12'd1; + wire [25:0] rx_next_row_span = + rx_height_minus_one * data_in[13:0]; + wire [32:0] rx_pipelined_end_address = + {1'b0, rx_base} + + {{7{1'b0}}, rx_row_span} + + {{20{1'b0}}, rx_width, 1'b0}; + + reg [3:0] semantic_reject; + always @(*) begin + semantic_reject = MAGIK_REJECT_NONE; + if(!rx_mode[15]) begin + if((rx_mode != 16'd0) || (rx_base != 32'd0) || + (rx_width_word != 16'd0) || (rx_height_word != 16'd0) || + (rx_hmin_word != 16'd0) || (rx_hmax_word != 16'd0) || + (rx_vmin_word != 16'd0) || (rx_vmax_word != 16'd0) || + (rx_stride_word != 16'd0)) + semantic_reject = MAGIK_REJECT_INVALID_MODE; + end + else if((|rx_mode[13:6]) || (rx_mode[5:0] != 6'h14)) + semantic_reject = MAGIK_REJECT_INVALID_MODE; + else if(rx_reserved_fields) + semantic_reject = MAGIK_REJECT_RESERVED; + else if((rx_base == 32'd0) || rx_base[0]) + semantic_reject = MAGIK_REJECT_INVALID_BASE; + else if((rx_width == 0) || + ({4'd0, rx_width} > MAGIK_FBUF_MAX_WIDTH) || + (rx_height == 0) || + ({4'd0, rx_height} > MAGIK_FBUF_MAX_HEIGHT)) + semantic_reject = MAGIK_REJECT_INVALID_GEOMETRY; + else if(rx_stride[0] || (rx_stride < ({2'd0, rx_width} << 1)) || + ({2'd0, rx_stride} > MAGIK_FBUF_MAX_STRIDE)) + semantic_reject = MAGIK_REJECT_INVALID_STRIDE; + else if((rx_hmin > rx_hmax) || (rx_vmin > rx_vmax)) + semantic_reject = MAGIK_REJECT_INVALID_BOUNDS; + else if(rx_address_wrap) + semantic_reject = MAGIK_REJECT_ADDRESS_WRAP; + end + + assign response_valid = + (cmd_start && ((cmd_id == MAGIK_UIO_SET_FBUF_LATCH) || + (cmd_id == MAGIK_UIO_GET_FBUF_LATCH) || + (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_CAPS) || + (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS) || + (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_RECEIPT))) || + (cmd_data && ((cmd_id == MAGIK_UIO_GET_FBUF_LATCH) || + (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_CAPS) || + (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS) || + (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_RECEIPT))); + + always @(*) begin + response_data = 16'd0; + if(cmd_start) begin + case(cmd_id) + MAGIK_UIO_SET_FBUF_LATCH: response_data = MAGIK_FBUF_LATCH_MAGIC; + MAGIK_UIO_GET_FBUF_LATCH: response_data = MAGIK_FBUF_STATUS_MAGIC; + MAGIK_UIO_GET_FBUF_LATCH_CAPS: response_data = MAGIK_FBUF_CAPS_MAGIC; + MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS: + response_data = MAGIK_FBUF_DIAGNOSTICS_MAGIC; + MAGIK_UIO_GET_FBUF_LATCH_RECEIPT: + response_data = MAGIK_FBUF_RECEIPT_MAGIC; + default: response_data = 16'd0; + endcase + end + else if(cmd_data && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH)) begin + if(word_index < 4'd15) response_data = response_snapshot[word_index]; + else if(word_index == 4'd15) + response_data = tx_crc ^ MAGIK_CRC_FINAL_XOR; + end + else if(cmd_data && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_CAPS)) begin + case(word_index) + 4'd0: response_data = MAGIK_FBUF_PROTOCOL_VERSION; + 4'd1: response_data = MAGIK_FBUF_CAPS_FLAGS; + 4'd2: response_data = MAGIK_FBUF_MAX_WIDTH; + 4'd3: response_data = MAGIK_FBUF_MAX_HEIGHT; + 4'd4: response_data = MAGIK_FBUF_MAX_STRIDE; + 4'd5: response_data = tx_crc ^ MAGIK_CRC_FINAL_XOR; + default: response_data = 16'd0; + endcase + end + else if(cmd_data && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS)) begin + if(word_index < 4'd6) response_data = response_snapshot[word_index]; + else if(word_index == 4'd6) + response_data = tx_crc ^ MAGIK_CRC_FINAL_XOR; + end + else if(cmd_data && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_RECEIPT)) begin + if(word_index < 4'd10) response_data = response_snapshot[word_index]; + else if(word_index == 4'd10) + response_data = tx_crc ^ MAGIK_CRC_FINAL_XOR; + end + end + + always @(posedge clk_sys) begin + vbl_meta <= hdmi_vbl; + vbl_sys <= vbl_meta; + vbl_old <= vbl_sys; + + if(cmd_start && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH)) begin + response_snapshot[0] <= active_seq; + response_snapshot[1] <= pending_seq; + response_snapshot[2] <= live_status_flags; + response_snapshot[3] <= flip_count; + response_snapshot[4] <= post_count; + response_snapshot[5] <= active_lfb_base[15:0]; + response_snapshot[6] <= active_lfb_base[31:16]; + response_snapshot[7] <= {4'd0, active_lfb_width}; + response_snapshot[8] <= {4'd0, active_lfb_height}; + response_snapshot[9] <= {2'd0, active_lfb_stride}; + response_snapshot[10] <= reject_count; + response_snapshot[11] <= active_route_epoch; + response_snapshot[12] <= active_transaction; + response_snapshot[13] <= pending_transaction; + response_snapshot[14] <= accepted_transaction; + tx_crc <= crc_header(MAGIK_UIO_GET_FBUF_LATCH, 16'd15); + tx_expected <= 4'd0; + tx_command <= MAGIK_UIO_GET_FBUF_LATCH; + end + else if(cmd_start && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_CAPS)) begin + tx_crc <= crc_header(MAGIK_UIO_GET_FBUF_LATCH_CAPS, 16'd5); + tx_expected <= 4'd0; + tx_command <= MAGIK_UIO_GET_FBUF_LATCH_CAPS; + end + else if(cmd_start && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS)) begin + response_snapshot[0] <= reject_count; + response_snapshot[1] <= {12'd0, last_reject_reason}; + response_snapshot[2] <= last_reject_expected_index; + response_snapshot[3] <= last_reject_observed_index; + response_snapshot[4] <= last_reject_command; + response_snapshot[5] <= last_reject_receiver_flags; + tx_crc <= crc_header(MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS, 16'd6); + tx_expected <= 4'd0; + tx_command <= MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS; + end + else if(cmd_start && (cmd_id == MAGIK_UIO_GET_FBUF_LATCH_RECEIPT)) begin + response_snapshot[0] <= rx_open ? attempted_transaction : + receipt_attempted_transaction; + response_snapshot[1] <= rx_open ? rx_seq : receipt_attempted_sequence; + response_snapshot[2] <= rx_open ? MAGIK_RECEIPT_REJECTED : + receipt_disposition; + response_snapshot[3] <= accepted_transaction; + response_snapshot[4] <= accepted_seq; + response_snapshot[5] <= pending_transaction; + response_snapshot[6] <= pending_seq; + response_snapshot[7] <= active_transaction; + response_snapshot[8] <= active_seq; + response_snapshot[9] <= rx_open ? {12'd0, MAGIK_REJECT_MISSING_WORD} : + {12'd0, receipt_reject_reason}; + tx_crc <= crc_header(MAGIK_UIO_GET_FBUF_LATCH_RECEIPT, 16'd10); + tx_expected <= 4'd0; + tx_command <= MAGIK_UIO_GET_FBUF_LATCH_RECEIPT; + end + else if(cmd_data && (cmd_id == tx_command) && + (word_index == tx_expected)) begin + if((tx_command == MAGIK_UIO_GET_FBUF_LATCH) && (word_index < 4'd15)) begin + tx_crc <= crc_word(tx_crc, response_snapshot[word_index]); + tx_expected <= tx_expected + 1'd1; + end + else if((tx_command == MAGIK_UIO_GET_FBUF_LATCH_CAPS) && + (word_index < 4'd5)) begin + case(word_index) + 4'd0: tx_crc <= crc_word(tx_crc, MAGIK_FBUF_PROTOCOL_VERSION); + 4'd1: tx_crc <= crc_word(tx_crc, MAGIK_FBUF_CAPS_FLAGS); + 4'd2: tx_crc <= crc_word(tx_crc, MAGIK_FBUF_MAX_WIDTH); + 4'd3: tx_crc <= crc_word(tx_crc, MAGIK_FBUF_MAX_HEIGHT); + 4'd4: tx_crc <= crc_word(tx_crc, MAGIK_FBUF_MAX_STRIDE); + // The enclosing range check makes this defensive arm unreachable. + /* verilator coverage_off */ + default: tx_crc <= tx_crc; + /* verilator coverage_on */ + endcase + tx_expected <= tx_expected + 1'd1; + end + else if((tx_command == MAGIK_UIO_GET_FBUF_LATCH_DIAGNOSTICS) && + (word_index < 4'd6)) begin + tx_crc <= crc_word(tx_crc, response_snapshot[word_index]); + tx_expected <= tx_expected + 1'd1; + end + else if((tx_command == MAGIK_UIO_GET_FBUF_LATCH_RECEIPT) && + (word_index < 4'd10)) begin + tx_crc <= crc_word(tx_crc, response_snapshot[word_index]); + tx_expected <= tx_expected + 1'd1; + end + end + + if(legacy_write) begin + magik_ownership <= 1'b0; + active_seq <= 16'd0; + active_transaction <= 16'd0; + accepted_seq <= 16'd0; + accepted_transaction <= 16'd0; + pending_seq <= 16'd0; + pending_transaction <= 16'd0; + pending <= 1'b0; + active_route_epoch <= active_route_epoch + 1'd1; + end + else if(apply_accepted) begin + magik_ownership <= 1'b1; + active_seq <= pending_seq; + active_transaction <= pending_transaction; + pending_seq <= 16'd0; + pending_transaction <= 16'd0; + active_route_epoch <= active_route_epoch + 1'd1; + flip_count <= flip_count + 1'd1; + pending <= 1'b0; + end + + if(cmd_start) begin + if(rx_open) begin + reject_count <= reject_count + 1'd1; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= 16'd0; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + if(cmd_id == MAGIK_UIO_SET_FBUF_LATCH) + last_reject_reason <= MAGIK_REJECT_RESTARTED; + else + last_reject_reason <= MAGIK_REJECT_MISSING_WORD; + receipt_attempted_transaction <= attempted_transaction; + receipt_attempted_sequence <= rx_seq; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= (cmd_id == MAGIK_UIO_SET_FBUF_LATCH) ? + MAGIK_REJECT_RESTARTED : MAGIK_REJECT_MISSING_WORD; + end + if(cmd_id == MAGIK_UIO_SET_FBUF_LATCH) begin + attempted_transaction <= attempted_transaction + 1'd1; + receipt_attempted_transaction <= attempted_transaction + 1'd1; + receipt_attempted_sequence <= 16'd0; + receipt_disposition <= MAGIK_RECEIPT_NONE; + receipt_reject_reason <= MAGIK_REJECT_NONE; + rx_open <= 1'b1; + rx_faulted <= 1'b0; + rx_seq <= 16'd0; + rx_expected <= 4'd0; + rx_mask <= 11'd0; + rx_crc <= crc_header(MAGIK_UIO_SET_FBUF_LATCH, 16'd11); + rx_row_span <= 26'd0; + rx_address_wrap <= 1'b0; + end + else begin + rx_open <= 1'b0; + rx_faulted <= 1'b1; + end + end + else if(cmd_data && (cmd_id == MAGIK_UIO_SET_FBUF_LATCH)) begin + if(!rx_open) begin + if(!rx_faulted) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_POST_CLOSE; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_POST_CLOSE; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_faulted <= 1'b1; + end + end + else if((word_index < rx_expected) && + (word_index + 1'd1 == rx_expected)) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_DUPLICATE_WORD; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_DUPLICATE_WORD; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_open <= 1'b0; + rx_faulted <= 1'b1; + end + else if(word_index < rx_expected) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_OUT_OF_ORDER; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_OUT_OF_ORDER; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_open <= 1'b0; + rx_faulted <= 1'b1; + end + else if((word_index == 4'd11) && (rx_expected != 4'd11)) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_MISSING_WORD; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_MISSING_WORD; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_open <= 1'b0; + rx_faulted <= 1'b1; + end + else if(word_index > rx_expected) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_SHIFTED_WORD; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_SHIFTED_WORD; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_open <= 1'b0; + rx_faulted <= 1'b1; + end + else if(word_index < 4'd11) begin + rx_mask[word_index] <= 1'b1; + rx_crc <= crc_word(rx_crc, data_in); + rx_expected <= rx_expected + 1'd1; + case(word_index) + 4'd0: rx_mode <= data_in; + 4'd1: rx_base[15:0] <= data_in; + 4'd2: rx_base[31:16] <= data_in; + 4'd3: rx_width_word <= data_in; + 4'd4: rx_height_word <= data_in; + 4'd5: rx_hmin_word <= data_in; + 4'd6: rx_hmax_word <= data_in; + 4'd7: rx_vmin_word <= data_in; + 4'd8: rx_vmax_word <= data_in; + 4'd9: begin + rx_stride_word <= data_in; + rx_row_span <= rx_next_row_span; + end + 4'd10: begin + rx_seq <= data_in; + receipt_attempted_sequence <= data_in; + rx_address_wrap <= + rx_pipelined_end_address > 33'h100000000; + end + // word_index < 11 and exact ordering restrict this case to 0..10. + /* verilator coverage_off */ + default: begin end + /* verilator coverage_on */ + endcase + end + else begin + rx_open <= 1'b0; + rx_faulted <= 1'b0; + // Exact in-order framing makes a CRC commit reachable only after all + // eleven payload bits are set. Keep the mask check as defense in depth. + /* verilator coverage_off */ + if(rx_mask != 11'h7ff) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_MISSING_WORD; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_MISSING_WORD; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_faulted <= 1'b1; + end + /* verilator coverage_on */ + else if(data_in != (rx_crc ^ MAGIK_CRC_FINAL_XOR)) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_BAD_CRC; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_BAD_CRC; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_faulted <= 1'b1; + end + else if(pending) begin + reject_count <= reject_count + 1'd1; + drop_count <= drop_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_PENDING_BUSY; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= MAGIK_REJECT_PENDING_BUSY; + rx_faulted <= 1'b1; + end + else if(semantic_reject != MAGIK_REJECT_NONE) begin + reject_count <= reject_count + 1'd1; + last_reject_reason <= semantic_reject; + receipt_disposition <= MAGIK_RECEIPT_REJECTED; + receipt_reject_reason <= semantic_reject; + last_reject_expected_index <= {12'd0, rx_expected}; + last_reject_observed_index <= {12'd0, word_index}; + last_reject_command <= {8'd0, cmd_id}; + last_reject_receiver_flags <= {14'd0, rx_faulted, rx_open}; + rx_faulted <= 1'b1; + end + else begin + route_en <= rx_mode[15]; + route_flt <= rx_mode[14]; + route_fmt <= rx_mode[5:0]; + route_base <= rx_base; + route_width <= rx_width; + route_height <= rx_height; + route_hmin <= rx_hmin; + route_hmax <= rx_hmax; + route_vmin <= rx_vmin; + route_vmax <= rx_vmax; + route_stride <= rx_stride; + pending_seq <= rx_seq; + pending_transaction <= attempted_transaction; + accepted_transaction <= attempted_transaction; + accepted_seq <= rx_seq; + pending <= 1'b1; + post_count <= post_count + 1'd1; + last_reject_reason <= MAGIK_REJECT_NONE; + receipt_disposition <= MAGIK_RECEIPT_ACCEPTED; + receipt_reject_reason <= MAGIK_REJECT_NONE; + end + end + end + end + + `ifndef SYNTHESIS + always @(posedge clk_sys) begin + if(!pending) begin + assert(accepted_seq == active_seq) + else $fatal(1, "accepted N / active N-1 / no pending is forbidden"); + assert(accepted_transaction == active_transaction) + else $fatal(1, "active transaction must originate from the accepted receipt"); + end + end + `endif + +endmodule + +`default_nettype wire diff --git a/sys/sys_top.v b/sys/sys_top.v index a4168795..ca94777a 100644 --- a/sys/sys_top.v +++ b/sys/sys_top.v @@ -231,7 +231,7 @@ end // gp_in[31] = 0 - quick flag that FPGA is initialized (HPS reads 1 when FPGA is not in user mode) // used to avoid lockups while JTAG loading -wire [31:0] gp_in = {1'b0, btn_user | btn[1], btn_osd | btn[0], io_dig, 7'd0, ~HDMI_TX_INT, io_ver, io_ack, io_wide, io_dout | io_dout_sys}; +wire [31:0] gp_in = {1'b0, btn_user | btn[1], btn_osd | btn[0], io_dig, 7'd0, ~HDMI_TX_INT, io_ver, io_ack, io_wide, io_dout | io_dout_sys | magik_io_dout}; wire [31:0] gp_out; wire [1:0] io_ver = 1; // 0 - obsolete. 1 - optimized HPS I/O. 2,3 - reserved for future. @@ -351,6 +351,73 @@ reg [12:0] arc2x = 0; reg [12:0] arc2y = 0; reg [15:0] io_dout_sys; +// GPL-3.0-or-later latch adapter by Nigel Breslaw; see mister_magik_latch_sys_top_bridge.sv. +wire magik_response_valid; +wire [15:0] magik_response_data; +wire magik_lfb_apply_accepted; +wire magik_lfb_en; +wire magik_lfb_flt; +wire [5:0] magik_lfb_fmt; +wire [11:0] magik_lfb_width; +wire [11:0] magik_lfb_height; +wire [11:0] magik_lfb_hmin; +wire [11:0] magik_lfb_hmax; +wire [11:0] magik_lfb_vmin; +wire [11:0] magik_lfb_vmax; +wire [31:0] magik_lfb_base; +wire [13:0] magik_lfb_stride; + +mister_magik_latch_sys_top_bridge magik_latch_bridge +( + .clk_sys(clk_sys), + .hdmi_vbl(hdmi_vbl), + .io_uio(io_uio), + .io_strobe(io_strobe), + .io_din(io_din), + .active_lfb_en(LFB_EN), + .active_lfb_base(LFB_BASE), + .active_lfb_width(LFB_WIDTH), + .active_lfb_height(LFB_HEIGHT), + .active_lfb_stride(LFB_STRIDE), + .response_valid(magik_response_valid), + .response_data(magik_response_data), + .apply(), + .apply_accepted(magik_lfb_apply_accepted), + .legacy_write(), + .active_word_index(), + .route_en(magik_lfb_en), + .route_flt(magik_lfb_flt), + .route_fmt(magik_lfb_fmt), + .route_width(magik_lfb_width), + .route_height(magik_lfb_height), + .route_hmin(magik_lfb_hmin), + .route_hmax(magik_lfb_hmax), + .route_vmin(magik_lfb_vmin), + .route_vmax(magik_lfb_vmax), + .route_base(magik_lfb_base), + .route_stride(magik_lfb_stride), + .pending(), + .pending_seq(), + .active_seq(), + .post_count(), + .flip_count(), + .drop_count(), + .reject_count(), + .active_route_epoch() +); + +// Independent response register avoids decoder assignments overwriting replies. +// Diagnostic command 0x45 counts response strobes, including payload words. +reg [15:0] magik_io_dout = 16'd0; +reg [7:0] magik_response_strobes = 8'd0; +always @(posedge clk_sys) begin + if(~io_uio) magik_io_dout <= 16'd0; + else if(io_strobe) begin + magik_io_dout <= magik_response_valid ? magik_response_data : 16'd0; + if(magik_response_valid) magik_response_strobes <= magik_response_strobes + 1'd1; + end +end + always@(posedge clk_sys) begin reg [7:0] cmd; reg has_cmd; @@ -360,6 +427,20 @@ always@(posedge clk_sys) begin reg [7:0] fb_crc; reg [1:0] sl_r; + if(magik_lfb_apply_accepted) begin + LFB_EN <= magik_lfb_en; + LFB_FLT <= magik_lfb_flt; + LFB_FMT <= magik_lfb_fmt; + LFB_WIDTH <= magik_lfb_width; + LFB_HEIGHT <= magik_lfb_height; + LFB_HMIN <= magik_lfb_hmin; + LFB_HMAX <= magik_lfb_hmax; + LFB_VMIN <= magik_lfb_vmin; + LFB_VMAX <= magik_lfb_vmax; + LFB_BASE <= magik_lfb_base; + LFB_STRIDE <= magik_lfb_stride; + end + coef_wr <= 0; sl_r <= FB_EN ? 2'b00 : scanlines; @@ -408,6 +489,7 @@ always@(posedge clk_sys) begin `endif if(io_din[7:0] == 'h42) io_dout_sys <= {1'b1, frame_cnt}; if(io_din[7:0] == 'h44) io_dout_sys <= 1; + if(io_din[7:0] == 'h45) io_dout_sys <= {8'h5A, magik_response_strobes}; end else begin cnt <= cnt + 1'd1; diff --git a/tb/bootstrap_video_tb.sv b/tb/bootstrap_video_tb.sv new file mode 100644 index 00000000..3486fad8 --- /dev/null +++ b/tb/bootstrap_video_tb.sv @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +`timescale 1ns/1ps +module bootstrap_video_tb; + reg native_active = 0; + reg [23:0] native_rgb = 0; + reg show_snow = 0; + reg [23:0] snow_rgb = 24'habcdef; + reg de_in = 0, hs_in = 0, vs_in = 0; + wire [23:0] rgb_out; + wire de_out, hs_out, vs_out; + zaparoo_bootstrap_video dut (.*); + initial begin + // A valid native CRT writer must never be covered by bootstrap black. + // Writer stop returns to black without changing any timing signals. + for (integer active = 0; active < 2; active = active + 1) begin + native_active = active[0]; + for (integer timing = 0; timing < 8; timing = timing + 1) begin + {de_in, hs_in, vs_in} = timing[2:0]; + for (integer color = 0; color < 256; color = color + 1) begin + native_rgb = {color[7:0], ~color[7:0], color[7:0]}; + #1; + assert(rgb_out == (native_active ? native_rgb : 24'd0)) + else $fatal(1, "idle black/native CRT mux mismatch"); + assert({de_out, hs_out, vs_out} == {de_in, hs_in, vs_in}) + else $fatal(1, "bootstrap changed timing"); + end + end + end + native_active = 0; #1; + assert(rgb_out == 0) else $fatal(1, "writer stop did not restore black"); + show_snow = 1; de_in = 1; #1; + assert(rgb_out == snow_rgb) else $fatal(1, "OSD did not reveal snow"); + de_in = 0; #1; + assert(rgb_out == 0) else $fatal(1, "snow painted outside DE"); + native_active = 1; #1; + assert(rgb_out == native_rgb) else $fatal(1, "snow covered native frontend"); + native_active = 0; show_snow = 0; de_in = 1; #1; + assert(rgb_out == 0) else $fatal(1, "OSD close did not restore black"); + $display("PASS: black handoff, OSD snow, native CRT priority and timing"); + $finish; + end +endmodule diff --git a/tb/check_timing.py b/tb/check_timing.py new file mode 100644 index 00000000..3cd783f9 --- /dev/null +++ b/tb/check_timing.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Reject failed or incomplete Quartus timing reports, even after exit status 0.""" + +import re +import sys +from decimal import Decimal +from pathlib import Path + +METRICS = {"setup", "hold", "recovery", "removal", "minimum pulse width"} +SLACK = re.compile( + r"^Info \(332146\): Worst-case ([a-z ]+) slack is (-?\d+(?:\.\d+)?)\s*$", + re.MULTILINE, +) + + +def check_timing(report: str) -> None: + if "Timing requirements not met" in report: + raise ValueError("Quartus reports unmet timing requirements") + seen = set() + for metric, value in SLACK.findall(report): + if metric not in METRICS: + continue + seen.add(metric) + if Decimal(value) < 0: + raise ValueError(f"negative {metric} slack: {value} ns") + if seen != METRICS: + raise ValueError(f"missing timing results: {', '.join(sorted(METRICS - seen))}") + + +if __name__ == "__main__": + try: + check_timing(Path(sys.argv[1]).read_text()) + except (IndexError, OSError, ValueError) as error: + sys.exit(f"Timing check failed: {error}") + print("PASS: setup, hold, recovery, removal and pulse-width timing") diff --git a/tb/dcfifo_sim.sv b/tb/dcfifo_sim.sv new file mode 100644 index 00000000..47b29b95 --- /dev/null +++ b/tb/dcfifo_sim.sv @@ -0,0 +1,70 @@ +// Behavioral stand-in for the Altera dcfifo megafunction (showahead mode), +// simulation only. No real CDC modeling — pointers are plain integers. + +module dcfifo #( + parameter intended_device_family = "", + parameter lpm_numwords = 1024, + parameter lpm_showahead = "ON", + parameter lpm_type = "dcfifo", + parameter lpm_width = 64, + parameter lpm_widthu = 10, + parameter overflow_checking = "ON", + parameter rdsync_delaypipe = 4, + parameter underflow_checking = "ON", + parameter use_eab = "ON", + parameter wrsync_delaypipe = 4 +)( + input wire aclr, + input wire [lpm_width-1:0] data, + input wire rdclk, + input wire rdreq, + input wire wrclk, + input wire wrreq, + output wire [lpm_width-1:0] q, + output wire rdempty, + output wire wrfull, + output wire [1:0] eccstatus, + output wire rdfull, + output wire [lpm_widthu-1:0] rdusedw, + output wire wrempty, + output wire [lpm_widthu-1:0] wrusedw +); + +reg [lpm_width-1:0] mem [0:lpm_numwords-1]; +integer wptr = 0, rptr = 0; +integer peak_used = 0; // TB-visible; may be reset hierarchically +integer overflow_count = 0; // writes dropped (overflow_checking semantics) +integer underflow_count = 0; + +wire [31:0] used = wptr - rptr; + +assign q = mem[rptr % lpm_numwords]; +assign rdempty = (used == 0); +assign wrfull = (used >= lpm_numwords); +assign eccstatus = 2'b00; +assign rdfull = wrfull; +assign wrempty = rdempty; +assign rdusedw = used[lpm_widthu-1:0]; +assign wrusedw = used[lpm_widthu-1:0]; + +always @(posedge wrclk or posedge aclr) begin + if (aclr) wptr <= 0; + else if (wrreq) begin + if (wrfull) overflow_count = overflow_count + 1; + else begin + mem[wptr % lpm_numwords] <= data; + wptr <= wptr + 1; + if (wptr + 1 - rptr > peak_used) peak_used = wptr + 1 - rptr; + end + end +end + +always @(posedge rdclk or posedge aclr) begin + if (aclr) rptr <= 0; + else if (rdreq) begin + if (rdempty) underflow_count = underflow_count + 1; + else rptr <= rptr + 1; + end +end + +endmodule diff --git a/tb/native_video_reader_tb.sv b/tb/native_video_reader_tb.sv new file mode 100644 index 00000000..fa82dab1 --- /dev/null +++ b/tb/native_video_reader_tb.sv @@ -0,0 +1,302 @@ +// System-level testbench for native_video_top (timing + reader) against a +// behavioral DDR model and tb/dcfifo_sim.sv. Verifies the v2 DDR contract: +// +// - no writer (word0 == 0): core stays inactive, only control polls issued +// - v2 frames: correct buffer base, 176-word line bursts, mode + h/v +// offsets parsed from word1 and latched by the timing module +// - double buffering: counter change switches buffer base +// - writer stop: word0 -> 0 drops active (frame_ready) again +// - stale DDR: a magic-carrying block already present at reset with a +// frozen counter never activates (this is what a previous core leaves +// behind); a counter advance under it does activate; no magic never does +// - PAL (mode 2): 288 line fetches +// - 480i (mode 1): two 180-beat bursts per line, source line = 2*line+field, +// alternating per field; FIFO never overflows +// - DDR timeout: unresponsive bus drops active instead of latching stale +// +// Run: tb/run.sh + +`timescale 1ns/1ps + +module native_video_reader_tb; + +reg clk_sys = 0; always #5 clk_sys = ~clk_sys; // 100 MHz DDR-side +reg clk_vid = 0; always #18.5185 clk_vid = ~clk_vid; // 27 MHz +reg reset = 1; + +wire [1:0] vmode; +wire vfield; + +// ce_pix divider mirrors menu.sv. +reg [1:0] ce_div = 0; +reg ce_pix = 0; +always @(posedge clk_vid) begin + if (reset) ce_div <= 2'd0; + else ce_div <= ce_div + 2'd1; + ce_pix <= (vmode == 2'd1) ? ce_div[0] : (ce_div == 2'd0); +end + +// ---- DDR model ------------------------------------------------------------- +localparam [28:0] CTRL_ADDR = 29'h07400000; +localparam [28:0] BUF0_V2 = 29'h07400200; +localparam [28:0] BUF1_V2 = 29'h07430000; +localparam [63:0] PIX_DATA = 64'h00FFAA55_00FFAA55; // B,G,R,X = 55,AA,FF,00 + +wire ddr_rd; +wire [28:0] ddr_addr; +wire [7:0] ddr_burstcnt; +reg [63:0] ddr_dout = 0; +reg ddr_dout_ready = 0; + +reg [63:0] ctrl_q = 64'd0; // {word1, word0} as published by the "writer" +reg respond_en = 1; + +integer req_n = 0; +reg [28:0] req_addr [0:199999]; +reg [7:0] req_cnt [0:199999]; + +reg [28:0] cur_addr = 0; +integer cur_left = 0; +integer lat = 0; + +always @(posedge clk_sys) begin + ddr_dout_ready <= 0; + if (ddr_rd) begin + req_addr[req_n] = ddr_addr; + req_cnt[req_n] = ddr_burstcnt; + req_n = req_n + 1; + if (respond_en) begin + cur_addr <= ddr_addr; + cur_left <= ddr_burstcnt; + lat <= 3; + end + end + else if (cur_left != 0) begin + if (lat != 0) lat <= lat - 1; + else begin + ddr_dout <= (cur_addr == CTRL_ADDR) ? ctrl_q : PIX_DATA; + ddr_dout_ready <= 1; + cur_addr <= cur_addr + 29'd1; + cur_left <= cur_left - 1; + end + end +end + +// ---- DUT -------------------------------------------------------------------- +wire [7:0] vga_r, vga_g, vga_b; +wire new_frame, active; + +native_video_top dut +( + .clk_sys (clk_sys), + .clk_vid (clk_vid), + .ce_pix (ce_pix), + .reset (reset), + + .ddr_busy (1'b0), + .ddr_burstcnt (ddr_burstcnt), + .ddr_addr (ddr_addr), + .ddr_dout (ddr_dout), + .ddr_dout_ready (ddr_dout_ready), + .ddr_rd (ddr_rd), + .ddr_din (), + .ddr_be (), + .ddr_we (), + + .vga_r (vga_r), + .vga_g (vga_g), + .vga_b (vga_b), + .vga_hs (), + .vga_vs (), + .vga_de (), + .vga_hblank (), + .vga_vblank (), + .vga_vcount (), + .vga_new_frame (new_frame), + .vga_mode (vmode), + .vga_field (vfield), + .active (active) +); + +// ---- helpers ----------------------------------------------------------------- +integer errors = 0; + +task check(input string name, input integer got, input integer exp); + begin + if (got !== exp) begin + errors = errors + 1; + $display("FAIL %-36s got %0d (0x%0h), expected %0d (0x%0h)", name, got, got, exp, exp); + end + else $display("pass %-36s %0d", name, got); + end +endtask + +task wait_frames(input integer n); + repeat (n) @(posedge new_frame); +endtask + +// Publish a control block: word0 = (counter << 2) | buffer. +task publish(input bit magic, input [3:0] pmode, input signed [7:0] hoff, + input signed [3:0] voff, input integer counter, input bit buffer); + begin + ctrl_q = {magic ? 16'h5A50 : 16'h0000, hoff, voff, pmode, + counter[29:0], 1'b0, buffer}; + end +endtask + +// Verify one whole frame's DDR request sequence: a control poll followed by +// nlines line fetches of bpl bursts of blen beats each, line l fetched from +// base + src*stride + b*blen, where src = l (progressive) or 2*l + field +// (intl set). Also asserts the FIFO never overflowed during the frame. +task check_frame_fetch(input string tag, input [28:0] base, input integer stride, + input integer nlines, input integer blen, input integer bpl, + input bit intl, output reg fld); + integer s, l, b, idx, src; + begin + @(posedge new_frame); + @(negedge clk_vid); + fld = vfield; + s = req_n; + dut.reader.line_fifo.peak_used = 0; + dut.reader.line_fifo.overflow_count = 0; + @(posedge new_frame); + check({tag, " fifo overflow-free"}, dut.reader.line_fifo.overflow_count, 0); + $display("info %s fifo peak occupancy: %0d / 1024 words", tag, dut.reader.line_fifo.peak_used); + check({tag, " ctrl poll addr"}, req_addr[s], CTRL_ADDR); + check({tag, " ctrl poll burst"}, req_cnt[s], 1); + check({tag, " requests/frame"}, req_n >= s + 1 + nlines*bpl, 1); + for (l = 0; l < nlines; l = l + 1) begin + src = intl ? (2*l + fld) : l; + for (b = 0; b < bpl; b = b + 1) begin + idx = s + 1 + l*bpl + b; + if (req_addr[idx] !== base + src*stride + b*blen || req_cnt[idx] !== blen[7:0]) begin + errors = errors + 1; + $display("FAIL %s line %0d burst %0d: got addr 0x%0h cnt %0d, expected addr 0x%0h cnt %0d", + tag, l, b, req_addr[idx], req_cnt[idx], base + src*stride + b*blen, blen); + l = nlines; b = bpl; // bail after first mismatch + end + end + end + $display("pass %s frame fetch sequence (%0d lines x %0d bursts, field %0d)", tag, nlines, bpl, fld); + end +endtask + +// Sample vga_r at a given (hcount, vcount) pixel tick. +task sample_r(input integer hx, input integer vx, output [7:0] r); + begin + @(posedge clk_vid); + while (!(ce_pix && dut.timing.hcount == hx[9:0] && dut.timing.vcount == vx[8:0] && dut.timing.de)) + @(posedge clk_vid); + r = vga_r; + end +endtask + +reg [7:0] rs; +reg fld_a, fld_b; + +// ---- test sequence ------------------------------------------------------------ +initial begin + repeat (20) @(posedge clk_sys); + reset = 0; + + // Phase 0: no writer. + $display("--- phase 0: no writer ---"); + wait_frames(3); + check("idle: active low", {31'd0, active}, 0); + check("idle: only ctrl polls", req_cnt[req_n-1], 1); + check("idle: poll addr", req_addr[req_n-1], CTRL_ADDR); + + // Phase 1: v2 writer, mode 0, offsets +5/-3, buffer 0. + $display("--- phase 1: v2 mode 0 ---"); + publish(1, 4'd0, 8'sd5, -4'sd3, 1, 0); + wait (active === 1'b1); + wait_frames(2); + check("v2: mode latched", vmode, 0); + check("v2: h_offset latched", dut.timing.h_offset, 5); + check("v2: v_offset latched", dut.timing.v_offset, -3); + check_frame_fetch("v2-buf0", BUF0_V2, 176, 240, 176, 1, 0, fld_a); + sample_r(100, 100, rs); check("v2: interior pixel R", rs, 8'hFF); + sample_r(6, 100, rs); check("v2: paints to the edge", rs, 8'hFF); + + // Phase 2: counter advances with buffer 1. + $display("--- phase 2: double buffer ---"); + publish(1, 4'd0, 8'sd5, -4'sd3, 2, 1); + wait_frames(2); + check_frame_fetch("v2-buf1", BUF1_V2, 176, 240, 176, 1, 0, fld_a); + + // Phase 3: writer stops. + $display("--- phase 3: writer stop ---"); + ctrl_q = 64'd0; + wait_frames(3); + check("stop: active drops", {31'd0, active}, 0); + check("stop: mode reverts", vmode, 0); + + // Phase 4: a block already present at reset is a previous core's DDR + // leftovers, not a writer, and must never be painted. + $display("--- phase 4: stale control block rejected ---"); + publish(1, 4'd0, 8'sd0, 4'sd0, 9, 0); + reset = 1; repeat (20) @(posedge clk_sys); reset = 0; + wait_frames(8); + check("stale at reset: inactive", {31'd0, active}, 0); + sample_r(100, 100, rs); check("stale at reset: idle not DDR", rs !== 8'hFF, 1); + + // ...but a writer that then starts advancing the counter is real. + publish(1, 4'd0, 8'sd0, 4'sd0, 10, 1); + wait (active === 1'b1); + $display("pass stale block: counter advance activates"); + + // No magic is rejected outright, however trusted the writer was. + publish(0, 4'd0, 8'sd0, 4'sd0, 11, 0); + wait_frames(4); + check("no magic: active drops", {31'd0, active}, 0); + sample_r(100, 100, rs); check("no magic: idle not DDR", rs !== 8'hFF, 1); + ctrl_q = 64'd0; + wait_frames(3); + + // Phase 5: PAL. + $display("--- phase 5: v2 mode 2 (PAL) ---"); + publish(1, 4'd2, 8'sd0, 4'sd0, 12, 0); + wait (vmode === 2'd2); + wait (active === 1'b1); + wait_frames(2); + check_frame_fetch("pal", BUF0_V2, 176, 288, 176, 1, 0, fld_a); + + // Phase 6: 480i. + $display("--- phase 6: v2 mode 1 (480i) ---"); + publish(1, 4'd1, 8'sd0, 4'sd0, 13, 0); + wait (vmode === 2'd1); + wait_frames(2); + check_frame_fetch("480i-a", BUF0_V2, 360, 240, 180, 2, 1, fld_a); + wait_frames(1); // realign so the next measured field has opposite parity + check_frame_fetch("480i-b", BUF0_V2, 360, 240, 180, 2, 1, fld_b); + check("480i: both field parities seen", {31'd0, fld_a ^ fld_b}, 1); + + // Phase 7: DDR stops responding mid-session. + $display("--- phase 7: DDR timeout ---"); + publish(1, 4'd0, 8'sd0, 4'sd0, 14, 0); + wait (vmode === 2'd0); + wait_frames(3); + check("pre-timeout: active", {31'd0, active}, 1); + respond_en = 0; + wait (active === 1'b0); + $display("pass timeout: active dropped"); + respond_en = 1; + publish(1, 4'd0, 8'sd0, 4'sd0, 15, 0); + wait (active === 1'b1); + $display("pass timeout: recovered after writer republish"); + + if (errors == 0) $display("ALL CHECKS PASSED"); + else begin + $display("%0d CHECK(S) FAILED", errors); + $fatal(1); + end + $finish; +end + +initial begin + #3_000_000_000; + $display("TIMEOUT"); + $fatal(1); +end + +endmodule diff --git a/tb/native_video_timing_tb.sv b/tb/native_video_timing_tb.sv new file mode 100644 index 00000000..e1ec8ecc --- /dev/null +++ b/tb/native_video_timing_tb.sv @@ -0,0 +1,229 @@ +// Self-checking testbench for rtl/native_video_timing.sv. +// +// Measures, in exact ce_pix ticks, per mode: line period, hsync width, +// sync→active delay, active width, field period, vsync width, active lines +// per field — and for 480i: field alternation and the half-line vsync offset +// (both vsync intervals must be exactly 262.5 lines = 225225 ticks, which is +// impossible without the offset). Also exercises the h/v offset trims and +// their clamping. +// +// Run: tb/run.sh + +`timescale 1ns/1ps + +module native_video_timing_tb; + +reg clk = 0; +always #18.5185 clk = ~clk; // 27 MHz + +reg reset = 1; + +reg [1:0] mode_in = 2'd0; +reg signed [7:0] h_offset_in = 8'sd0; +reg signed [3:0] v_offset_in = 4'sd0; + +wire [1:0] mode; +wire field, hsync, vsync, hblank, vblank, de, new_frame, new_line; +wire [9:0] hcount; +wire [8:0] vcount; + +// ce_pix divider mirrors menu.sv: /4 (6.75 MHz) progressive, /2 (13.5 MHz) 480i. +reg [1:0] ce_div = 0; +reg ce_pix = 0; +always @(posedge clk) begin + if (reset) ce_div <= 2'd0; + else ce_div <= ce_div + 2'd1; + ce_pix <= (mode == 2'd1) ? ce_div[0] : (ce_div == 2'd0); +end + +native_video_timing dut +( + .clk (clk), + .ce_pix (ce_pix), + .reset (reset), + .mode_in (mode_in), + .h_offset_in (h_offset_in), + .v_offset_in (v_offset_in), + .mode (mode), + .field (field), + .hsync (hsync), + .vsync (vsync), + .hblank (hblank), + .vblank (vblank), + .de (de), + .hcount (hcount), + .vcount (vcount), + .new_frame (new_frame), + .new_line (new_line) +); + +// ---- monitor: everything in ce_pix ticks --------------------------------- +// DUT outputs are sampled one tick late, uniformly, so intervals are exact. +integer tick = 0; +reg hs_d = 0, vs_d = 0, de_d = 0; +reg await_de = 0; +reg vs_field = 0, vs_field_d = 0; + +integer hs_rise_tick = 0, vs_rise_tick = 0, de_rise_tick = 0; +integer hs_period = 0, hs_width = 0; +integer de_width = 0, hs_to_de = 0, vs_to_de = 0; +integer vs_period = 0, vs_period_d = 0, vs_width = 0; +integer de_lines = 0, de_lines_last = 0; +integer vs_count = 0; + +always @(posedge clk) begin + if (ce_pix) begin + if (hsync & ~hs_d) begin + hs_period <= tick - hs_rise_tick; + hs_rise_tick <= tick; + end + if (~hsync & hs_d) hs_width <= tick - hs_rise_tick; + + if (de & ~de_d) begin + de_rise_tick <= tick; + hs_to_de <= tick - hs_rise_tick; + de_lines <= de_lines + 1; + if (await_de) begin + vs_to_de <= tick - vs_rise_tick; + await_de <= 0; + end + end + if (~de & de_d) de_width <= tick - de_rise_tick; + + if (vsync & ~vs_d) begin + vs_period_d <= vs_period; + vs_period <= tick - vs_rise_tick; + vs_rise_tick <= tick; + de_lines_last <= de_lines; + de_lines <= 0; + vs_field_d <= vs_field; + vs_field <= field; + await_de <= 1; + vs_count <= vs_count + 1; + end + if (~vsync & vs_d) vs_width <= tick - vs_rise_tick; + + hs_d <= hsync; + vs_d <= vsync; + de_d <= de; + tick <= tick + 1; + end +end + +// ---- helpers --------------------------------------------------------------- +integer errors = 0; + +task check(input string name, input integer got, input integer exp); + begin + if (got !== exp) begin + errors = errors + 1; + $display("FAIL %-32s got %0d, expected %0d", name, got, exp); + end + else $display("pass %-32s %0d", name, got); + end +endtask + +// Wait n vsync rising edges (plenty for the field-wrap latch + a full +// measurable frame after any control change). +task settle(input integer n); + integer target; + begin + target = vs_count + n; + wait (vs_count >= target); + @(posedge clk); + end +endtask + +// ---- test sequence --------------------------------------------------------- +initial begin + repeat (8) @(posedge clk); + reset = 0; + + // ---- mode 0: 352x240p60 NTSC -------------------------------------- + settle(3); + $display("--- mode 0: 352x240p60 (line 63.556us, 15734.27 Hz, 60.05 Hz) ---"); + check("ntsc line period (px)", hs_period, 429); + check("ntsc hsync width (px)", hs_width, 32); + check("ntsc sync->active (px)", hs_to_de, 65); // 32 sync + 33 BP + check("ntsc active width (px)", de_width, 352); + check("ntsc field period (px)", vs_period, 429*262); + check("ntsc vsync width (px)", vs_width, 429*3); + check("ntsc active lines", de_lines_last, 240); + check("ntsc vsync->active (px)", vs_to_de, 429*19); // 3 sync + 16 BP + check("ntsc field flat", {31'd0, vs_field}, 0); + + // ---- mode 2: 352x288p50 PAL ---------------------------------------- + mode_in = 2'd2; + settle(4); + $display("--- mode 2: 352x288p50 (line 64.000us, 15625.00 Hz, 50.08 Hz) ---"); + check("pal line period (px)", hs_period, 432); + check("pal hsync width (px)", hs_width, 32); + check("pal sync->active (px)", hs_to_de, 69); // 32 sync + 37 BP + check("pal active width (px)", de_width, 352); + check("pal field period (px)", vs_period, 432*312); + check("pal vsync width (px)", vs_width, 432*3); + check("pal active lines", de_lines_last, 288); + + // ---- mode 1: 720x480i60 --------------------------------------------- + mode_in = 2'd1; + settle(5); + $display("--- mode 1: 720x480i60 (line 63.556us, 15734.27 Hz, 59.94 Hz) ---"); + check("480i line period (px)", hs_period, 858); + check("480i hsync width (px)", hs_width, 62); + check("480i sync->active (px)", hs_to_de, 119); // 62 sync + 57 BP + check("480i active width (px)", de_width, 720); + // Both vsync intervals = 262.5 lines exactly: proves the half-line + // offset (integer field lengths would give 262*858 / 263*858). + check("480i field period A (px)", vs_period, 225225); + check("480i field period B (px)", vs_period_d, 225225); + check("480i vsync width (px)", vs_width, 858*3); + check("480i active lines/field", de_lines_last, 240); + check("480i fields alternate", {31'd0, vs_field ^ vs_field_d}, 1); + + // ---- offset trims + clamping (mode 0) ------------------------------- + mode_in = 2'd0; + settle(4); + $display("--- offset trims (mode 0) ---"); + + h_offset_in = 8'sd8; settle(4); + check("h=+8 sync->active", hs_to_de, 73); + check("h=+8 active width", de_width, 352); + check("h=+8 line period", hs_period, 429); + h_offset_in = -8'sd8; settle(4); + check("h=-8 sync->active", hs_to_de, 57); + h_offset_in = 8'sd100; settle(4); + check("h=+100 clamps to +8", hs_to_de, 73); + h_offset_in = -8'sd100; settle(4); + check("h=-100 clamps to -8", hs_to_de, 57); + h_offset_in = 8'sd0; + + v_offset_in = 4'sd2; settle(4); + check("v=+2 vsync->active", vs_to_de, 429*21); + check("v=+2 field period", vs_period, 429*262); + v_offset_in = -4'sd8; settle(4); + check("v=-8 vsync->active", vs_to_de, 429*11); + v_offset_in = 4'sd7; settle(4); + check("v=+7 clamps to +2", vs_to_de, 429*21); + v_offset_in = 4'sd0; + + // ---- mode change sanity: back to NTSC after everything -------------- + settle(4); + check("ntsc restore line period", hs_period, 429); + check("ntsc restore sync->active", hs_to_de, 65); + check("ntsc restore field flat", {31'd0, vs_field}, 0); + + if (errors == 0) $display("ALL CHECKS PASSED"); + else begin + $display("%0d CHECK(S) FAILED", errors); + $fatal(1); + end + $finish; +end + +initial begin + #2_000_000_000; // 2 s simulated-time guard + $display("TIMEOUT"); + $fatal(1); +end + +endmodule diff --git a/tb/pixel_enable_tb.sv b/tb/pixel_enable_tb.sv new file mode 100644 index 00000000..892540a3 --- /dev/null +++ b/tb/pixel_enable_tb.sv @@ -0,0 +1,37 @@ +`timescale 1ns/1ps +module pixel_enable_tb; + reg clk = 0; + always #5 clk = ~clk; + reg reset = 1; + reg [1:0] mode = 0; + wire ce_pix; + zaparoo_pixel_enable dut (.*); + + initial begin + // Both progressive modes, interlace, and the reserved-mode fallback. + // Assert reset at every divider phase, as PLL lock may disappear mid-frame. + for (integer m = 0; m < 4; m = m + 1) begin + for (integer phase = 0; phase < 4; phase = phase + 1) begin + @(negedge clk); reset = 1; mode = m; + repeat (5) begin + @(posedge clk); #1; + assert(ce_pix === 1'b0) else $fatal(1, "pixel enable active during reset"); + end + @(negedge clk); reset = 0; + for (integer i = 0; i < 8 + phase; i = i + 1) begin + @(posedge clk); + if (i == 0) + assert(ce_pix === 1'b0) else $fatal(1, "consumer saw stale enable at reset release"); + #1; + assert(ce_pix === ((m == 1) ? (i % 2 == 1) : (i % 4 == 0))) + else $fatal(1, "divider cadence/phase changed: mode=%0d cycle=%0d", m, i); + end + end + end + $display("PASS: pixel enable reset, restart phase, progressive and interlaced cadence"); + $finish; + end + initial begin + #20000; $fatal(1, "pixel enable test timeout"); + end +endmodule diff --git a/tb/run.sh b/tb/run.sh new file mode 100755 index 00000000..337a1b39 --- /dev/null +++ b/tb/run.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Simulate native video, scanout and bootstrap with Icarus Verilog. +set -e +cd "$(dirname "$0")" + +mkdir -p ../test-output/rtl + +run_test() { + top=$1 + shift + echo "=== $top ===" + iverilog -g2012 -I ../sys -s "$top" -o "../test-output/rtl/$top.vvp" "$@" + timeout 600 vvp "../test-output/rtl/$top.vvp" +} + +run_test bootstrap_video_tb \ + ../sys/mister_magik_bootstrap_black.sv ../rtl/zaparoo_bootstrap_video.sv \ + bootstrap_video_tb.sv + +run_test snow_phase_tb ../rtl/zaparoo_snow_phase.sv snow_phase_tb.sv + +run_test pixel_enable_tb ../rtl/zaparoo_pixel_enable.sv pixel_enable_tb.sv + +run_test scanout_tb \ + ../sys/mister_magik_vblank_latch.sv ../sys/mister_magik_latch_sys_top_bridge.sv \ + scanout_tb.sv + +run_test native_video_timing_tb \ + ../rtl/native_video_timing.sv native_video_timing_tb.sv + +run_test native_video_reader_tb \ + ../rtl/native_video_timing.sv ../rtl/native_video_reader.sv \ + ../rtl/native_video_top.sv dcfifo_sim.sv native_video_reader_tb.sv diff --git a/tb/scanout_tb.sv b/tb/scanout_tb.sv new file mode 100644 index 00000000..57030d6a --- /dev/null +++ b/tb/scanout_tb.sv @@ -0,0 +1,176 @@ +// Copyright (C) 2026 Nigel Breslaw +// SPDX-License-Identifier: GPL-3.0-or-later +// Derived from tb_mister_magik_sys_top_integration.sv; Zaparoo bus/540p checks. +`timescale 1ns/1ps +module scanout_tb; + `include "mister_magik_latch_protocol.svh" + reg clk = 0; + always #5 clk = ~clk; + reg vblank = 0, selected = 0, strobe = 0; + reg [15:0] data_in = 0; + wire response_valid, apply, accepted, legacy, enabled, filtered, pending; + wire [15:0] response_data, active_seq, rejects; + wire [3:0] word_index; + wire [5:0] format; + wire [11:0] width, height, hmin, hmax, vmin, vmax; + wire [31:0] base; + wire [13:0] stride; + reg active_enabled = 0; + reg [31:0] active_base = 0; + reg [11:0] active_width = 0, active_height = 0; + reg [13:0] active_stride = 0; + reg [15:0] reply = 0; + mister_magik_latch_sys_top_bridge dut ( + .clk_sys(clk), .hdmi_vbl(vblank), .io_uio(selected), + .io_strobe(strobe), .io_din(data_in), + .active_lfb_en(active_enabled), .active_lfb_base(active_base), + .active_lfb_width(active_width), .active_lfb_height(active_height), + .active_lfb_stride(active_stride), .response_valid(response_valid), + .response_data(response_data), .apply(apply), .apply_accepted(accepted), + .legacy_write(legacy), .active_word_index(word_index), + .route_en(enabled), .route_flt(filtered), .route_fmt(format), + .route_width(width), .route_height(height), .route_hmin(hmin), + .route_hmax(hmax), .route_vmin(vmin), .route_vmax(vmax), + .route_base(base), .route_stride(stride), .pending(pending), + .pending_seq(), .active_seq(active_seq), .post_count(), + .flip_count(), .drop_count(), .reject_count(rejects), .active_route_epoch() + ); + // Mirror sys_top's separate response register and legacy-write priority. + always @(posedge clk) begin + if (!selected) reply <= 0; + else if (strobe) reply <= response_valid ? response_data : 16'd0; + if (accepted) begin + active_enabled <= enabled; + active_base <= base; + active_width <= width; + active_height <= height; + active_stride <= stride; + end + if (legacy) begin + case (word_index) + 0: active_enabled <= data_in[15]; + 1: active_base[15:0] <= data_in; + 2: active_base[31:16] <= data_in; + default: begin end + endcase + end + end + function automatic [15:0] crc_word(input [15:0] crc, input [15:0] word); + reg [15:0] next; + begin + next = crc; + for (integer bit_index = 15; bit_index >= 0; bit_index = bit_index - 1) + next = (next << 1) ^ ((next[15] ^ word[bit_index]) ? 16'h1021 : 16'd0); + return next; + end + endfunction + task automatic transfer(input [15:0] value, output [15:0] response); + @(negedge clk); data_in = value; strobe = 1; + @(posedge clk); #1; response = reply; strobe = 0; + endtask + task automatic begin_command(input [7:0] command, input [15:0] magic); + reg [15:0] response; + @(negedge clk); selected = 1; + transfer({8'd0, command}, response); + assert (response == magic) else $fatal(1, "command %h: %h != %h", command, response, magic); + endtask + task automatic end_command; + @(negedge clk); selected = 0; strobe = 0; + @(posedge clk); #1; + endtask + task automatic post(input bit corrupt, input [15:0] sequence_id); + reg [15:0] words [0:10]; + reg [15:0] crc, response; + // 960x540 RGB565 from qualified slot zero into full 1920x1080 HDMI. + words[0] = 16'h8014; + words[1] = 16'h0000; + words[2] = 16'h2300; + words[3] = 16'd960; + words[4] = 16'd540; + words[5] = 16'd0; + words[6] = 16'd1919; + words[7] = 16'd0; + words[8] = 16'd1079; + words[9] = 16'd1920; + words[10] = sequence_id; + crc = crc_word(crc_word(crc_word(16'hffff, 16'h57), 16'd4), 16'd11); + begin_command(8'h57, MAGIK_FBUF_LATCH_MAGIC); + for (integer i = 0; i < 11; i = i + 1) begin + crc = crc_word(crc, words[i]); + transfer(words[i], response); + end + transfer(crc ^ (corrupt ? 16'd1 : 16'd0), response); + end_command(); + endtask + initial begin + reg [15:0] response, receipt [0:10]; + repeat (3) @(posedge clk); + end_command(); + begin_command(8'h59, MAGIK_FBUF_CAPS_MAGIC); + transfer(0, response); assert(response == 4) else $fatal; + transfer(0, response); assert(response == 16'h1ff) else $fatal; + transfer(0, response); assert(response == 1920) else $fatal; + transfer(0, response); assert(response == 1080) else $fatal; + transfer(0, response); assert(response == 3840) else $fatal; + transfer(0, response); assert(response == 16'h2984) else $fatal; + end_command(); + post(1, 1); + assert(!pending && !active_enabled && rejects == 1) else $fatal(1, "bad CRC changed route"); + post(0, 2); + assert(pending && !active_enabled) else $fatal(1, "route changed before vblank"); + post(0, 3); + assert(pending && rejects == 2) else $fatal(1, "pending route overwritten"); + begin_command(8'h5b, MAGIK_FBUF_RECEIPT_MAGIC); + for (integer i = 0; i < 11; i = i + 1) transfer(0, receipt[i]); + assert(receipt[1] == 3 && receipt[2] == MAGIK_RECEIPT_REJECTED && + receipt[4] == 2 && receipt[6] == 2 && receipt[9] == {12'd0, MAGIK_REJECT_PENDING_BUSY}) + else $fatal(1, "receipt lost rejected/accepted distinction"); + end_command(); + @(negedge clk); vblank = 1; + repeat (5) @(posedge clk); #1; + assert(active_enabled && active_base == 32'h23000000 && active_width == 960 && + active_height == 540 && active_stride == 1920 && active_seq == 2 && !pending && + hmax == 1919 && vmax == 1079) + else $fatal(1, "540p/full HDMI route not applied atomically"); + @(negedge clk); vblank = 0; + repeat (4) @(posedge clk); + post(0, 4); + begin_command(8'h2f, 0); + @(negedge clk); vblank = 1; + while (!apply) @(negedge clk); + data_in = 0; strobe = 1; + @(posedge clk); #1; strobe = 0; + assert(!pending && active_seq == 0 && !active_enabled) + else $fatal(1, "legacy collision failed to reclaim ownership"); + transfer(16'h4444, response); transfer(16'h2222, response); + end_command(); + assert(active_base == 32'h22224444) else $fatal(1, "legacy framebuffer restore lost"); + for (integer gap = 0; gap < 9; gap = gap + 1) begin + @(negedge clk); vblank = 0; + repeat (4) @(posedge clk); + post(0, 16'd10 + gap); + assert(pending) else $fatal(1, "gap probe must start with pending route"); + begin_command(8'h2f, 0); + for (integer word = 0; word < 10; word = word + 1) begin + transfer(16'ha000 + word, response); + if (word == gap) begin + @(negedge clk); vblank = 1; + repeat (8) begin + @(posedge clk); #1; + assert(!pending && !accepted) + else $fatal(1, "route applied in legacy gap %0d", gap); + end + end + end + end_command(); + assert(active_base == 32'ha002a001) + else $fatal(1, "legacy descriptor corrupted across gap %0d", gap); + end + $display("PASS: all nine inter-word legacy vblank gaps preserve takeover"); + $display("PASS: scanout CAPS, CRC, pending/receipts, 540p HDMI, vblank, legacy takeover"); + $finish; + end + initial begin + #20000; $fatal(1, "scanout simulation timeout"); + end +endmodule diff --git a/tb/snow_phase_tb.sv b/tb/snow_phase_tb.sv new file mode 100644 index 00000000..3763b056 --- /dev/null +++ b/tb/snow_phase_tb.sv @@ -0,0 +1,44 @@ +`timescale 1ns/1ps +module snow_phase_tb; + reg clk = 0; + always #5 clk = ~clk; + reg reset = 1, ce_pix = 0, new_frame = 0; + reg [8:0] vcount = 0; + wire [9:0] phase; + zaparoo_snow_phase dut (.*); + + task automatic field_pulse(input integer clocks_per_pixel); + for (integer i = 0; i < clocks_per_pixel; i = i + 1) begin + @(negedge clk); + new_frame = 1; + ce_pix = (i == clocks_per_pixel - 1); + @(posedge clk); #1; + end + @(negedge clk); new_frame = 0; ce_pix = 0; + endtask + + initial begin + @(posedge clk); #1; + assert(phase == 0) else $fatal(1, "phase not reset"); + @(negedge clk); reset = 0; + field_pulse(4); + assert(phase == 6) else $fatal(1, "240p frame advanced more than once"); + field_pulse(2); + assert(phase == 12) else $fatal(1, "480i field advanced more than once"); + repeat (4) begin @(negedge clk); ce_pix = 1; end + @(posedge clk); #1; + assert(phase == 12) else $fatal(1, "phase advanced without frame edge"); + vcount = 9'd1; #1; + assert(phase == 16) else $fatal(1, "wrong upstream line phase"); + vcount = 9'd256; #1; + assert(phase == 12) else $fatal(1, "line phase failed to wrap"); + vcount = 0; + for (integer i = 0; i < 170; i = i + 1) field_pulse(4); + assert(phase == 8) else $fatal(1, "frame phase failed to wrap"); + $display("PASS: upstream snow cadence, 240p/480i enable and phase wrap"); + $finish; + end + initial begin + #20000; $fatal(1, "snow phase simulation timeout"); + end +endmodule diff --git a/tb/test_check_timing.py b/tb/test_check_timing.py new file mode 100644 index 00000000..3c05294c --- /dev/null +++ b/tb/test_check_timing.py @@ -0,0 +1,41 @@ +import unittest + +from check_timing import METRICS, check_timing + + +def report(value="0.042"): + return "\n".join( + f"Info (332146): Worst-case {metric} slack is {value}" + for metric in sorted(METRICS) + ) + + +class TimingCheckTests(unittest.TestCase): + def test_positive_and_zero_pass(self): + check_timing(report()) + check_timing(report("0.000")) + + def test_each_negative_metric_fails(self): + for metric in METRICS: + with self.subTest(metric=metric), self.assertRaises(ValueError): + check_timing(report().replace(f"{metric} slack is 0.042", f"{metric} slack is -0.016")) + + def test_incomplete_or_unrecognized_report_fails(self): + for text in ("", "Quartus compilation successful", report().split("\n", 1)[1], report("NaN")): + with self.subTest(text=text), self.assertRaises(ValueError): + check_timing(text) + + def test_critical_warning_fails_even_with_positive_summary(self): + with self.assertRaises(ValueError): + check_timing(report() + "\nCritical Warning (332148): Timing requirements not met") + + def test_negative_duplicate_is_not_hidden(self): + with self.assertRaises(ValueError): + check_timing(report("-0.016") + "\n" + report()) + + def test_crlf_report_passes(self): + check_timing(report().replace("\n", "\r\n")) + + +if __name__ == "__main__": + unittest.main()