[{"element_type":"text","content":"\n        \u003cp\u003eThis guide note is a work in progress\u003c/p\u003e\n      ","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/278/original/20260303_194140.jpg?1772588633","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eI've been wanting to output video from my microcontroller projects for ages. But I didn't want to jump through obscure hoops and I really wanted multiple video outputs on one board.\u003c/p\u003e\n\u003cp\u003eWell, the Adafruit Metro RP2350 has a Raspberry Pi RP2350B microcontroller with lots of pins, memory and programmable PIO state machines.\u003c/p\u003e\n\u003cp\u003eThere isn't a great deal on VGA video for the RP2350, only on the RP2040. And certainly no other code for using CircuitPython + PIO. So hopefully this may help some folks.\u003c/p\u003e\n\u003cp\u003eHere is my work just on VGA. Certain design decisions were made which might be changed by others. Yes I vibe coded it as I don't really know PIO syntax.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n        \u003ch3\u003eDesign Considerations\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eThe clock of the RP2350B remains standard at 150MHz in this implementation\u003c/li\u003e\n\u003cli\u003eGiven the CPU frequency, certain video output compromises are in the code to do the best with the processing and time budgets. This may effect certain monitors differently.\u003c/li\u003e\n\u003cli\u003eUsing CircuitPython introduces PIO SM design rules if HSYNC and VSYNC were both driven in PIO. The alternative was to do HSYNC in PIO, VSYNC in CircuitPython. The algorithm syncs the two but is not ideal.\u003c/li\u003e\n\u003c/ul\u003e\n      \n","metadata":{}},{"element_type":"alert","content":"\u003cp\u003eThis is not presented as a solid design. Rather you should look to wire things up and test on your own to ensure it meets your expectations. It's my design, not Adafruit's, so don't go to their forums saying it doesn't work as you thought. You can contact me, I'll likely say \"tweak it to your satisfaction\". If you come up with a better design, perfect, let me know and if you used some of my material, under the MIT license please say so.\u003c/p\u003e","metadata":{"markdown":"This is not presented as a solid design. Rather you should look to wire things up and test on your own to ensure it meets your expectations. It's my design, not Adafruit's, so don't go to their forums saying it doesn't work as you thought. You can contact me, I'll likely say \"tweak it to your satisfaction\". If you come up with a better design, perfect, let me know and if you used some of my material, under the MIT license please say so.","alert_type":"purple","icon":"thinking"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eMIT License\u0026nbsp; Copyright (c) 2026 Anne Barela\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003ch3\u003eFiles\u003c/h3\u003e\n\u003cp\u003eSee this project's \u003ca href=\"https://github.com/TheKitty/Metro-RP2350-VGA\" target=\"_blank\"\u003eGitHub Repository\u003c/a\u003e for all the files.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eLICENSE\u003c/li\u003e\n\u003cli\u003ePINS files show the pins used, although the Red and Blue pins are switched by Claude for strange reasons. Red has GPIO 2, 3, and 4. Blue has GPIO 8, and 9.\u003c/li\u003e\n\u003cli\u003eSCHEMATIC, in several file formats\u003c/li\u003e\n\u003cli\u003evga_color_bars.py, the code\u003c/li\u003e\n\u003cli\u003eRP2350 BASIC VGA.zip, all the files prezipped\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n        \u003ch3\u003eCode\u003c/h3\u003e\n      \n\n\n","metadata":{}},{"element_type":"code","content":"# SPDX-FileCopyrightText: 2026 Anne Barela\n# SPDX-License-Identifier: MIT\n\"\"\"\nMetro RP2350 VGA RGB332 — SMPTE colour bars.\n\nDisplays SMPTE standard colour bars on a VGA monitor using an Adafruit\nMetro RP2350 and a resistor DAC.  See PINS.md for wiring and SCHEMATIC.md\nfor the resistor DAC circuit.\n\nHardware:\n  GPIO2–9:  RGB332 resistor DAC (8-bit colour, MSB first)\n  GPIO10:   HSYNC  (PIO sideset)\n  GPIO11:   VSYNC  (CPU digitalio, timed spin loop)\n\nOutput: 640×480 @ ~59.9 Hz using a 320×240 framebuffer, pixel- and\nline-doubled in DMA.\n\nAfter connecting the monitor, press Auto-Adjust on the monitor OSD.\nIf the right edge is clipped, increase the monitor Clock setting 2–3 steps.\n\nColour encoding (hardware-specific DAC wiring):\n  bits[7:5]  →  GPIO2–4  →  VGA Blue  (3-bit, 0–7)\n  bits[4:2]  →  GPIO5–7  →  VGA Green (3-bit, 0–7)\n  bits[1:0]  →  GPIO8–9  →  VGA Red   (2-bit, 0–3)\n  byte = (blue3 \u003c\u003c 5) | (green3 \u003c\u003c 2) | red2\n\"\"\"\n\nimport board\nimport rp2pio\nimport adafruit_pioasm\nimport digitalio\nimport gc\nimport time\n\ngc.collect()\n\n# ── Pin definitions ──────────────────────────────────────────────────────────\n\nVGA_RGB_BASE = board.D2    # GPIO2  — first of 8-bit RGB332 bus (D2–D9)\nVGA_HSYNC    = board.D10   # GPIO10 — HSYNC (PIO sideset)\nVGA_VSYNC    = board.D11   # GPIO11 — VSYNC (CPU digitalio)\n\n# ── VGA timing ───────────────────────────────────────────────────────────────\n\nVGA_PIXEL_CLOCK = 25_175_000   # Hz\n\nVGA_V_VISIBLE = 480\nVGA_V_FRONT   =  10\nVGA_V_SYNC    =   2\nVGA_V_BACK    =  33\nVGA_V_TOTAL   = 525            # 480 + 10 + 2 + 33\n\nVGA_BUF_W = 320   # logical framebuffer width  (pixel-doubled to 640)\nVGA_BUF_H = 240   # logical framebuffer height (line-doubled  to 480)\n\n# ── PIO program: pixel output + HSYNC ────────────────────────────────────────\n#\n# Timing per line at 25.175 MHz (800 cycles total):\n#   Visible + overhead:  657 cycles  (16 iter × 20 px × 2 clk + 17 loop overhead)\n#   Front porch:           8 cycles\n#   HSYNC pulse:          96 cycles  (spec 96)\n#   Back porch:           39 cycles\n#\n# .side_set 1 uses one bit from the 5-bit delay field; max delay = [15].\n# Instruction count: 29 of 32 maximum.\n\nvga_program = adafruit_pioasm.assemble(\"\"\"\n.program vga_rgb_hsync\n.side_set 1\n\n.wrap_target\n    ; Visible: 16 iterations × 20 pixels × 2 cycles + 17 overhead = 657 cycles\n    set y, 15           side 1      ; 16 iterations (y = 15 down to 0)\npixel_loop:\n    out pins, 8         side 1 [1]  ; pixels 1–20, 2 cycles each\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    out pins, 8         side 1 [1]\n    jmp y-- pixel_loop  side 1      ; loop (16 cycles overhead total)\n\n    ; Front porch: 8 cycles, HSYNC high\n    nop                 side 1 [7]\n\n    ; HSYNC pulse: 96 cycles, HSYNC low (16 + 5×16 - 5×1 = 16 + 75 + 5 = 96)\n    set x, 4            side 0 [15] ; 16 cycles\nhsync_loop:\n    nop                 side 0 [14] ; 15 cycles × 5 iterations = 75\n    jmp x-- hsync_loop  side 0      ;  1 cycle  × 5 iterations =  5\n\n    ; Back porch: 39 cycles, HSYNC high\n    nop                 side 1 [15]\n    nop                 side 1 [15]\n    nop                 side 1 [6]\n.wrap\n\"\"\")\n\n# ── Colour definitions ───────────────────────────────────────────────────────\n\n# SMPTE standard colour bar order (left to right).\n# Byte encoding: bits[7:5]=Blue, bits[4:2]=Green, bits[1:0]=Red\n_COLOR_BARS = [\n    0xFF,  # White    B=7 G=7 R=3\n    0x1F,  # Yellow   B=0 G=7 R=3\n    0xFC,  # Cyan     B=7 G=7 R=0\n    0x1C,  # Green    B=0 G=7 R=0\n    0xE3,  # Magenta  B=7 G=0 R=3\n    0x03,  # Red      B=0 G=0 R=3\n    0xE0,  # Blue     B=7 G=0 R=0\n    0x00,  # Black    B=0 G=0 R=0\n]\n\n# ── VGA output ───────────────────────────────────────────────────────────────\n\nclass VGAOutput:\n    \"\"\"320×240 RGB332 framebuffer with DMA-ready full_frame buffer.\n\n    Call draw_color_bars() then sync_frame() to populate full_frame.\n    Pass full_frame to sm.background_write(loop=...) for continuous DMA output.\n    \"\"\"\n\n    def __init__(self):\n        self.width       = VGA_BUF_W\n        self.height      = VGA_BUF_H\n        self.framebuffer = bytearray(self.width * self.height)\n        # DMA source: VGA_V_TOTAL lines × width bytes.\n        # Visible lines 0–479 are filled by sync_frame().\n        # Blanking lines 480–524 remain zero (bytearray default).\n        self.full_frame  = bytearray(self.width * VGA_V_TOTAL)\n\n    def sync_frame(self):\n        \"\"\"Copy framebuffer into full_frame with pixel- and line-doubling.\"\"\"\n        w  = self.width\n        fb = memoryview(self.framebuffer)\n        ff = self.full_frame\n        for y in range(self.height):\n            row = fb[y * w:(y + 1) * w]\n            ff[(2 * y)     * w:(2 * y + 1) * w] = row\n            ff[(2 * y + 1) * w:(2 * y + 2) * w] = row\n\n    def draw_color_bars(self):\n        \"\"\"Fill framebuffer with SMPTE colour bars.\"\"\"\n        fb  = self.framebuffer\n        w   = self.width\n        h   = self.height\n        n   = len(_COLOR_BARS)\n        bw  = w // n\n        for i, color in enumerate(_COLOR_BARS):\n            x0    = i * bw\n            seg_w = bw if i \u003c n - 1 else w - x0\n            seg   = bytes([color]) * seg_w\n            for row in range(h):\n                start = row * w + x0\n                fb[start:start + seg_w] = seg\n\n# ── Main ─────────────────────────────────────────────────────────────────────\n\ndef main():\n    print(\"Metro RP2350 VGA — SMPTE colour bars   A. B. 2026\")\n\n    vga = VGAOutput()\n    vga.draw_color_bars()\n    vga.sync_frame()\n    gc.collect()\n    print(f\"Free memory: {gc.mem_free()} bytes\")\n\n    # VSYNC output — idle high (VGA negative polarity)\n    vsync = digitalio.DigitalInOut(VGA_VSYNC)\n    vsync.direction = digitalio.Direction.OUTPUT\n    vsync.value = True\n\n    # PIO state machine: RGB332 pixel data + HSYNC sideset\n    sm = rp2pio.StateMachine(\n        vga_program,\n        frequency=VGA_PIXEL_CLOCK,\n        first_out_pin=VGA_RGB_BASE,\n        out_pin_count=8,\n        first_sideset_pin=VGA_HSYNC,\n        sideset_pin_count=1,\n        auto_pull=True,\n        pull_threshold=8,\n        out_shift_right=False,   # MSB first: bits[7:5]=Blue → GPIO2–4\n    )\n\n    # Frame timing in integer nanoseconds (avoids float drift)\n    actual        = sm.frequency\n    _NS           = 1_000_000_000\n    frame_ns      = VGA_V_TOTAL * 800 * _NS // actual\n    vsync_low_ns  = (VGA_V_VISIBLE + VGA_V_FRONT) * 800 * _NS // actual\n    vsync_high_ns = (VGA_V_VISIBLE + VGA_V_FRONT + VGA_V_SYNC) * 800 * _NS // actual\n\n    # Start continuous DMA. Record t0 immediately before background_write()\n    # so VSYNC can be aligned to the DMA frame boundary on the first frame.\n    # Without alignment, the VSYNC anchor lands at a random phase each boot,\n    # causing vertical image shift or bottom clipping.\n    t0 = time.monotonic_ns()\n    sm.background_write(loop=vga.full_frame)\n    time.sleep(0.1)\n    print(f\"DMA looping: {sm.writing}\")\n\n    now   = time.monotonic_ns()\n    phase = (now - t0) % frame_ns          # position within current DMA frame\n    next_low  = now + (frame_ns - phase) + vsync_low_ns\n    next_high = now + (frame_ns - phase) + vsync_high_ns\n    _LEAD_NS  = 1_000_000                  # 1 ms fine-spin window before pulse\n\n    print(\"Running. Press Auto-Adjust on monitor OSD if image needs centering.\")\n\n    while True:\n        # Sleep for bulk of frame (no heap allocations), then fine-spin for\n        # precise VSYNC timing. GC cannot fire during the ~64 µs pulse window.\n        sleep_ns = next_low - time.monotonic_ns() - _LEAD_NS\n        if sleep_ns \u003e 1_000_000:\n            time.sleep(sleep_ns / _NS)\n        while time.monotonic_ns() \u003c next_low:\n            pass\n        vsync.value = False\n        while time.monotonic_ns() \u003c next_high:\n            pass\n        vsync.value = True\n        next_low  += frame_ns\n        next_high += frame_ns\n\n\nmain()","metadata":{"language":"python","linenums":false,"filename":""}}]