# SPDX-FileCopyrightText: 2026 Anne Barela
# SPDX-License-Identifier: MIT
"""
Metro RP2350 VGA RGB332 — SMPTE colour bars.

Displays SMPTE standard colour bars on a VGA monitor using an Adafruit
Metro RP2350 and a resistor DAC.  See PINS.md for wiring and SCHEMATIC.md
for the resistor DAC circuit.

Hardware:
  GPIO2–9:  RGB332 resistor DAC (8-bit colour, MSB first)
  GPIO10:   HSYNC  (PIO sideset)
  GPIO11:   VSYNC  (CPU digitalio, timed spin loop)

Output: 640×480 @ ~59.9 Hz using a 320×240 framebuffer, pixel- and
line-doubled in DMA.

After connecting the monitor, press Auto-Adjust on the monitor OSD.
If the right edge is clipped, increase the monitor Clock setting 2–3 steps.

Colour encoding (hardware-specific DAC wiring):
  bits[7:5]  →  GPIO2–4  →  VGA Blue  (3-bit, 0–7)
  bits[4:2]  →  GPIO5–7  →  VGA Green (3-bit, 0–7)
  bits[1:0]  →  GPIO8–9  →  VGA Red   (2-bit, 0–3)
  byte = (blue3 << 5) | (green3 << 2) | red2
"""

import board
import rp2pio
import adafruit_pioasm
import digitalio
import gc
import time

gc.collect()

# ── Pin definitions ──────────────────────────────────────────────────────────

VGA_RGB_BASE = board.D2    # GPIO2  — first of 8-bit RGB332 bus (D2–D9)
VGA_HSYNC    = board.D10   # GPIO10 — HSYNC (PIO sideset)
VGA_VSYNC    = board.D11   # GPIO11 — VSYNC (CPU digitalio)

# ── VGA timing ───────────────────────────────────────────────────────────────

VGA_PIXEL_CLOCK = 25_175_000   # Hz

VGA_V_VISIBLE = 480
VGA_V_FRONT   =  10
VGA_V_SYNC    =   2
VGA_V_BACK    =  33
VGA_V_TOTAL   = 525            # 480 + 10 + 2 + 33

VGA_BUF_W = 320   # logical framebuffer width  (pixel-doubled to 640)
VGA_BUF_H = 240   # logical framebuffer height (line-doubled  to 480)

# ── PIO program: pixel output + HSYNC ────────────────────────────────────────
#
# Timing per line at 25.175 MHz (800 cycles total):
#   Visible + overhead:  657 cycles  (16 iter × 20 px × 2 clk + 17 loop overhead)
#   Front porch:           8 cycles
#   HSYNC pulse:          96 cycles  (spec 96)
#   Back porch:           39 cycles
#
# .side_set 1 uses one bit from the 5-bit delay field; max delay = [15].
# Instruction count: 29 of 32 maximum.

vga_program = adafruit_pioasm.assemble("""
.program vga_rgb_hsync
.side_set 1

.wrap_target
    ; Visible: 16 iterations × 20 pixels × 2 cycles + 17 overhead = 657 cycles
    set y, 15           side 1      ; 16 iterations (y = 15 down to 0)
pixel_loop:
    out pins, 8         side 1 [1]  ; pixels 1–20, 2 cycles each
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    out pins, 8         side 1 [1]
    jmp y-- pixel_loop  side 1      ; loop (16 cycles overhead total)

    ; Front porch: 8 cycles, HSYNC high
    nop                 side 1 [7]

    ; HSYNC pulse: 96 cycles, HSYNC low (16 + 5×16 - 5×1 = 16 + 75 + 5 = 96)
    set x, 4            side 0 [15] ; 16 cycles
hsync_loop:
    nop                 side 0 [14] ; 15 cycles × 5 iterations = 75
    jmp x-- hsync_loop  side 0      ;  1 cycle  × 5 iterations =  5

    ; Back porch: 39 cycles, HSYNC high
    nop                 side 1 [15]
    nop                 side 1 [15]
    nop                 side 1 [6]
.wrap
""")

# ── Colour definitions ───────────────────────────────────────────────────────

# SMPTE standard colour bar order (left to right).
# Byte encoding: bits[7:5]=Blue, bits[4:2]=Green, bits[1:0]=Red
_COLOR_BARS = [
    0xFF,  # White    B=7 G=7 R=3
    0x1F,  # Yellow   B=0 G=7 R=3
    0xFC,  # Cyan     B=7 G=7 R=0
    0x1C,  # Green    B=0 G=7 R=0
    0xE3,  # Magenta  B=7 G=0 R=3
    0x03,  # Red      B=0 G=0 R=3
    0xE0,  # Blue     B=7 G=0 R=0
    0x00,  # Black    B=0 G=0 R=0
]

# ── VGA output ───────────────────────────────────────────────────────────────

class VGAOutput:
    """320×240 RGB332 framebuffer with DMA-ready full_frame buffer.

    Call draw_color_bars() then sync_frame() to populate full_frame.
    Pass full_frame to sm.background_write(loop=...) for continuous DMA output.
    """

    def __init__(self):
        self.width       = VGA_BUF_W
        self.height      = VGA_BUF_H
        self.framebuffer = bytearray(self.width * self.height)
        # DMA source: VGA_V_TOTAL lines × width bytes.
        # Visible lines 0–479 are filled by sync_frame().
        # Blanking lines 480–524 remain zero (bytearray default).
        self.full_frame  = bytearray(self.width * VGA_V_TOTAL)

    def sync_frame(self):
        """Copy framebuffer into full_frame with pixel- and line-doubling."""
        w  = self.width
        fb = memoryview(self.framebuffer)
        ff = self.full_frame
        for y in range(self.height):
            row = fb[y * w:(y + 1) * w]
            ff[(2 * y)     * w:(2 * y + 1) * w] = row
            ff[(2 * y + 1) * w:(2 * y + 2) * w] = row

    def draw_color_bars(self):
        """Fill framebuffer with SMPTE colour bars."""
        fb  = self.framebuffer
        w   = self.width
        h   = self.height
        n   = len(_COLOR_BARS)
        bw  = w // n
        for i, color in enumerate(_COLOR_BARS):
            x0    = i * bw
            seg_w = bw if i < n - 1 else w - x0
            seg   = bytes([color]) * seg_w
            for row in range(h):
                start = row * w + x0
                fb[start:start + seg_w] = seg

# ── Main ─────────────────────────────────────────────────────────────────────

def main():
    print("Metro RP2350 VGA — SMPTE colour bars   A. B. 2026")

    vga = VGAOutput()
    vga.draw_color_bars()
    vga.sync_frame()
    gc.collect()
    print(f"Free memory: {gc.mem_free()} bytes")

    # VSYNC output — idle high (VGA negative polarity)
    vsync = digitalio.DigitalInOut(VGA_VSYNC)
    vsync.direction = digitalio.Direction.OUTPUT
    vsync.value = True

    # PIO state machine: RGB332 pixel data + HSYNC sideset
    sm = rp2pio.StateMachine(
        vga_program,
        frequency=VGA_PIXEL_CLOCK,
        first_out_pin=VGA_RGB_BASE,
        out_pin_count=8,
        first_sideset_pin=VGA_HSYNC,
        sideset_pin_count=1,
        auto_pull=True,
        pull_threshold=8,
        out_shift_right=False,   # MSB first: bits[7:5]=Blue → GPIO2–4
    )

    # Frame timing in integer nanoseconds (avoids float drift)
    actual        = sm.frequency
    _NS           = 1_000_000_000
    frame_ns      = VGA_V_TOTAL * 800 * _NS // actual
    vsync_low_ns  = (VGA_V_VISIBLE + VGA_V_FRONT) * 800 * _NS // actual
    vsync_high_ns = (VGA_V_VISIBLE + VGA_V_FRONT + VGA_V_SYNC) * 800 * _NS // actual

    # Start continuous DMA. Record t0 immediately before background_write()
    # so VSYNC can be aligned to the DMA frame boundary on the first frame.
    # Without alignment, the VSYNC anchor lands at a random phase each boot,
    # causing vertical image shift or bottom clipping.
    t0 = time.monotonic_ns()
    sm.background_write(loop=vga.full_frame)
    time.sleep(0.1)
    print(f"DMA looping: {sm.writing}")

    now   = time.monotonic_ns()
    phase = (now - t0) % frame_ns          # position within current DMA frame
    next_low  = now + (frame_ns - phase) + vsync_low_ns
    next_high = now + (frame_ns - phase) + vsync_high_ns
    _LEAD_NS  = 1_000_000                  # 1 ms fine-spin window before pulse

    print("Running. Press Auto-Adjust on monitor OSD if image needs centering.")

    while True:
        # Sleep for bulk of frame (no heap allocations), then fine-spin for
        # precise VSYNC timing. GC cannot fire during the ~64 µs pulse window.
        sleep_ns = next_low - time.monotonic_ns() - _LEAD_NS
        if sleep_ns > 1_000_000:
            time.sleep(sleep_ns / _NS)
        while time.monotonic_ns() < next_low:
            pass
        vsync.value = False
        while time.monotonic_ns() < next_high:
            pass
        vsync.value = True
        next_low  += frame_ns
        next_high += frame_ns


main()