
This project is a multi-application VGA graphics system built entirely in Verilog and synthesized into a single Tiny Tapeout 1×1 logic tile. It generates a 640×480 @ 60Hz VGA signal using a 25.175 MHz pixel clock, with all graphics computed combinatorially per-pixel — no external RAM, ROM, or framebuffer is required. A top-level mode multiplexer reads ui_in to select one of three applications.
The VGA timing is generated by a pair of 10-bit counters (h_cnt, v_cnt) that count through all 800×525 pixels per frame, producing hsync, vsync, and display_on as combinatorial outputs. A 12-bit frame_counter increments once per frame and is used throughout the design to drive animations.
Shown when both ui_in[5] and ui_in[0] are LOW.
The scene is rendered in two layers:
Foreground — DLSU Shield: The four letters D, L, S, U are each defined by bounding box inequalities directly in h_cnt/v_cnt screen space, with inner cutouts to create hollow letterforms. A 6-pixel-offset copy of each letter is drawn in dark green first as a drop shadow. A diamond-shaped outer border (abs_sum > 160 && abs_sum < 164) and an animated inner diamond (inner_pulse) are drawn using Manhattan distance from the screen center at (320, 240), where abs_sum = |h_cnt − 320| + |v_cnt − 240|. The inner diamond's radius is modulated by a triangle-wave derived from frame_cnt[7:0], producing a smooth breathing pulse.
Background — Galvantronix Circuit Motif: Scrolling horizontal and vertical traces are drawn by aliasing the lower 5 bits of h_cnt and v_cnt against a slowly incrementing pan offset derived from frame_cnt[6:2]. Small via pads appear at trace intersections. XOR operations on the upper bits of h_cnt and v_cnt create a checkerboard-like alternating pattern to avoid a uniform grid.
Text overlay: Four rows of small pixel-font text are rendered using a shared 31-glyph font ROM (indexed by a txt_idx register) and sampled at half the tile resolution (h_cnt[3:1], v_cnt[3:1]).
ui_in[0] HIGH, ui_in[5] LOW)A combinatorial fractal engine rendering Order-1 through Order-5 Hilbert space-filling curves on a 16×16 cell grid (256×256 pixels, centered on screen).
The core is the function hilbert_d(hx, hy) — a fully unrolled, multiplierless 4-level cascade that computes an 8-bit Hilbert sequence number from a 4-bit (x, y) cell coordinate. At each level, the quadrant bits rx, ry are extracted, the 2-bit sub-index d is computed as {rx, rx^ry}, and the coordinates are conditionally reflected/swapped for the next level — all using XOR and bit-select operations with no multiplier.
An anim_timer register increments each frame (at a speed scaled to the current order) and is compared against the sequence number d_curr of each cell. Cells where d_curr <= active_d are drawn as a 10×10 pixel block within the 16×16 cell, leaving a 3-pixel border. This creates a draw-in animation that traces the curve from start to end. After completing each order, a 60-frame pause precedes the transition to the next order. Colors are derived from the upper bits of d_curr summed with frame_cnt bits, producing a slow rainbow cycle.
ui_in[5] HIGH)A playable 2D maze on a 20×15 tile grid, where each tile is 32×32 pixels.
The wall map is encoded as one 20-bit literal per row inside the combinatorial function is_wall_at(grid_x, grid_y), implemented as a case statement — the synthesizer maps this to a logic ROM with no flip-flops. The function is shared (time-multiplexed) between display rendering (called every pixel clock) and move validation (called once per frame at frame_tick).
Player input on ui_in[4:1] is sampled at frame_tick into btn_state, with the previous sample held in btn_prev. Rising-edge detection (btn_state[i] & ~btn_prev[i]) produces single-cycle move pulses. The candidate new position (try_x, try_y) is computed combinatorially and validated against is_wall_at before being committed to (px, py).
When the player reaches grid position (9, 7), maze_state transitions to the win state and a win_timer counts 180 frames. During this period, win_timer[4] drives the maze_flash signal which toggles the wall and player colors between green and yellow at ~1 Hz. After 180 frames the maze resets.
Connect a VGA PMOD to uo_out and a DIP switch array to ui_in. Power on and observe the DLSU Shield as the default display.
ui_in state |
Active application |
|---|---|
| All LOW | DLSU Animo Shield + Galvantronix Background |
[0] HIGH, [5] LOW |
Hilbert Curve (Order 1→5, cycling) |
[5] HIGH |
Maze Game |
Note: ui_in[5] takes priority. Setting both [5] and [0] HIGH will show the Maze, not the Hilbert Curve.
With ui_in[5] HIGH, use the D-Pad pins to navigate the cyan square to the white target:
| Pin | Action |
|---|---|
ui_in[1] |
Move Up |
ui_in[2] |
Move Down |
ui_in[3] |
Move Left |
ui_in[4] |
Move Right |
Inputs are rising-edge detected and sampled once per frame (~60 times per second). Holding a button does not repeat — toggle it to move. Reaching the target triggers a ~3-second flashing animation before the maze resets to the start.
uo_out dedicated output pinsui_in dedicated input pins| # | Input | Output | Bidirectional |
|---|---|---|---|
| 0 | R1 | ||
| 1 | G1 | ||
| 2 | B1 | ||
| 3 | VSync | ||
| 4 | R0 | ||
| 5 | G0 | ||
| 6 | B0 | ||
| 7 | HSync |