Skip to content

vga_timing

Source: Verilog/Terminals/rtl/vga_timing.v

Where it sits (Nexys): nd120_nexys4ddr_top > terminal_top > text_screen > vga_timing - instance path: TERMINAL.SCREEN.TIMING

Used in: text_screen (Nexys, MiSTer, MEGA65 R6, MEGA65 R3)

Contains: no other modules.

Module hierarchy - All modules

vga_timing symbol

Schematic

Drawn from the Verilog: the yosys netlist of the Nexys 4 DDR build, instance TERMINAL.SCREEN.TIMING. Sub-modules are boxes (click the picture to open it full size; there every sub-module box links to its page, and every wire shows its Verilog name).

vga_timing schematic

Parameters

Parameter Default
H_VISIBLE 800
H_FRONT_PORCH 40
H_SYNC 128
H_BACK_PORCH 88
V_VISIBLE 600
V_FRONT_PORCH 1
V_SYNC 4
V_BACK_PORCH 23
H_SYNC_POSITIVE 1'b1
V_SYNC_POSITIVE 1'b1
H2_VISIBLE 1920
H2_FRONT_PORCH 48
H2_SYNC 32
H2_BACK_PORCH 80
V2_VISIBLE 1080
V2_FRONT_PORCH 3
V2_SYNC 5
V2_BACK_PORCH 23
H2_SYNC_POSITIVE 1'b1
V2_SYNC_POSITIVE 1'b0

Ports

Direction Width Name Description
input 1 clk pixel clock - MUST match the selected mode
input 1 rst_n (active low) async reset, active low
input 1 mode
output [11:0] x visible pixel column, 0..H_VISIBLE-1 (only valid while de)
output [11:0] y visible pixel row, 0..V_VISIBLE-1 (only valid while de)
output 1 hsync horizontal sync, polarity per H_SYNC_POSITIVE
output 1 vsync vertical sync, polarity per V_SYNC_POSITIVE
output 1 de display enable = in the visible area (~(hblank|vblank))
output 1 hblank outside the visible area horizontally
output 1 vblank outside the visible area vertically
output 1 line_end one pulse at the last pixel clock of every line
output 1 frame_end one pulse at the last pixel clock of every frame

Verilog source

Verilog/Terminals/rtl/vga_timing.v on GitHub.

Show the Verilog of vga_timing (159 lines)
//============================================================================
//! VGA timing generator - pixel/line counters, sync and blanking
//!
//! Part of the board-independent terminal core (Verilog/Terminals/).
//! Plan: Verilog/Terminals/docs/PLAN-vt100-terminal-core.md
//!
//! Defaults are 800x600 @ 60 Hz, "VESA discrete monitor timing":
//!     pixel clock 40.000 MHz
//!     horizontal  800 visible, 40 front porch, 128 sync, 88 back porch = 1056
//!     vertical    600 visible,  1 front porch,   4 sync,  23 back porch =  628
//!     sync polarity POSITIVE on both H and V
//!     40e6 / (1056 * 628) = 60.32 Hz
//!
//! Why 800x600 and not 640x480: on the Nexys 4 DDR the existing MMCM runs a
//! 1000 MHz VCO, so an integer divide of 25 gives EXACTLY 40.000 MHz. The
//! 640x480 pixel clock (25.175 MHz) is not reachable from that VCO - the
//! closest is 25.000 MHz, 0.7% low. See fpga/nexys4ddr/PLAN-vga-console.md.
//!
//! Everything is a parameter, so a board that wants a different mode changes
//! numbers rather than code.
//!
//! Written 27-AUG-2026.
//============================================================================

`default_nettype none

module vga_timing #(
    // Horizontal, in pixel clocks
    parameter integer H_VISIBLE     = 800,
    parameter integer H_FRONT_PORCH = 40,
    parameter integer H_SYNC        = 128,
    parameter integer H_BACK_PORCH  = 88,

    // Vertical, in lines
    parameter integer V_VISIBLE     = 600,
    parameter integer V_FRONT_PORCH = 1,
    parameter integer V_SYNC        = 4,
    parameter integer V_BACK_PORCH  = 23,

    //! 1 = sync pulse is high while active (800x600 wants positive on both).
    //! 640x480 would want 0 on both - the one thing that changes with the mode
    //! besides the counts.
    parameter         H_SYNC_POSITIVE = 1'b1,
    parameter         V_SYNC_POSITIVE = 1'b1,

    // ------------------------------------------------------------------
    // MODE 1 - a SECOND, runtime-selectable video mode.
    //
    // Defaults are 1920x1080@60 (CEA-861: 148.5 MHz pixel clock, both syncs
    // POSITIVE). The board supplies the matching pixel clock; this module only
    // counts, so it does not know or care what the clock actually is - which is
    // exactly why the mode and the clock must be switched by the same bit.
    // ------------------------------------------------------------------
    parameter integer H2_VISIBLE     = 1920,
    parameter integer H2_FRONT_PORCH = 48,
    parameter integer H2_SYNC        = 32,
    parameter integer H2_BACK_PORCH  = 80,
    parameter integer V2_VISIBLE     = 1080,
    parameter integer V2_FRONT_PORCH = 3,
    parameter integer V2_SYNC        = 5,
    parameter integer V2_BACK_PORCH  = 23,
    //! CVT-RB polarity: hsync POSITIVE, vsync NEGATIVE. Getting this wrong does
    //! not blank the screen, it makes a monitor guess the wrong mode - which
    //! looks like the design being broken.
    parameter         H2_SYNC_POSITIVE = 1'b1,
    parameter         V2_SYNC_POSITIVE = 1'b0
) (
    input wire clk,      //! pixel clock - MUST match the selected mode
    input wire rst_n,    //! async reset, active low

    //! 0 = mode 0 (the H_*/V_* parameters), 1 = mode 1 (H2_*/V2_*).
    //! Change this only together with the pixel clock.
    input wire mode,

    output wire [11:0] x,        //! visible pixel column, 0..H_VISIBLE-1 (only valid while de)
    output wire [11:0] y,        //! visible pixel row,    0..V_VISIBLE-1 (only valid while de)
    output wire        hsync,    //! horizontal sync, polarity per H_SYNC_POSITIVE
    output wire        vsync,    //! vertical sync,   polarity per V_SYNC_POSITIVE
    output wire        de,       //! display enable = in the visible area (~(hblank|vblank))
    output wire        hblank,   //! outside the visible area horizontally
    output wire        vblank,   //! outside the visible area vertically
    output wire        line_end, //! one pulse at the last pixel clock of every line
    output wire        frame_end //! one pulse at the last pixel clock of every frame
);

  localparam integer H_TOTAL = H_VISIBLE + H_FRONT_PORCH + H_SYNC + H_BACK_PORCH;
  localparam integer V_TOTAL = V_VISIBLE + V_FRONT_PORCH + V_SYNC + V_BACK_PORCH;

  // Sync starts after the front porch and lasts H_SYNC / V_SYNC ticks.
  localparam integer H_SYNC_START = H_VISIBLE + H_FRONT_PORCH;
  localparam integer H_SYNC_END   = H_SYNC_START + H_SYNC;
  localparam integer V_SYNC_START = V_VISIBLE + V_FRONT_PORCH;
  localparam integer V_SYNC_END   = V_SYNC_START + V_SYNC;

  localparam integer H2_TOTAL = H2_VISIBLE + H2_FRONT_PORCH + H2_SYNC + H2_BACK_PORCH;
  localparam integer V2_TOTAL = V2_VISIBLE + V2_FRONT_PORCH + V2_SYNC + V2_BACK_PORCH;
  localparam integer H2_SYNC_START = H2_VISIBLE + H2_FRONT_PORCH;
  localparam integer H2_SYNC_END   = H2_SYNC_START + H2_SYNC;
  localparam integer V2_SYNC_START = V2_VISIBLE + V2_FRONT_PORCH;
  localparam integer V2_SYNC_END   = V2_SYNC_START + V2_SYNC;

  //! The counts actually in force. Muxed rather than parameterised, because the
  //! mode is chosen by a switch on the board at run time - the whole point.
  wire [11:0] h_total     = mode ? H2_TOTAL[11:0]       : H_TOTAL[11:0];
  wire [11:0] v_total     = mode ? V2_TOTAL[11:0]       : V_TOTAL[11:0];
  wire [11:0] h_visible   = mode ? H2_VISIBLE[11:0]     : H_VISIBLE[11:0];
  wire [11:0] v_visible   = mode ? V2_VISIBLE[11:0]     : V_VISIBLE[11:0];
  wire [11:0] h_sync_beg  = mode ? H2_SYNC_START[11:0]  : H_SYNC_START[11:0];
  wire [11:0] h_sync_fin  = mode ? H2_SYNC_END[11:0]    : H_SYNC_END[11:0];
  wire [11:0] v_sync_beg  = mode ? V2_SYNC_START[11:0]  : V_SYNC_START[11:0];
  wire [11:0] v_sync_fin  = mode ? V2_SYNC_END[11:0]    : V_SYNC_END[11:0];
  wire        h_sync_pos  = mode ? H2_SYNC_POSITIVE     : H_SYNC_POSITIVE;
  wire        v_sync_pos  = mode ? V2_SYNC_POSITIVE     : V_SYNC_POSITIVE;

  reg [11:0] s_hcount;
  reg [11:0] s_vcount;

  //! last pixel clock of a line / of a frame - used to advance the counters and
  //! exported so the character generator can do its per-line and per-frame work
  //! (cursor blink, row address reload) without re-deriving them.
  assign line_end  = (s_hcount == h_total - 12'd1);
  assign frame_end = line_end && (s_vcount == v_total - 12'd1);

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      s_hcount <= 12'd0;
      s_vcount <= 12'd0;
    end else begin
      if (line_end) begin
        s_hcount <= 12'd0;
        s_vcount <= frame_end ? 12'd0 : (s_vcount + 12'd1);
      end else begin
        s_hcount <= s_hcount + 12'd1;
      end
    end
  end

  assign hblank = (s_hcount >= h_visible);
  assign vblank = (s_vcount >= v_visible);
  assign de     = !hblank && !vblank;

  // x and y are only meaningful inside the visible area; outside it they keep
  // counting, which costs nothing and makes the counters easy to watch in a
  // waveform. Consumers must gate on `de`.
  assign x = s_hcount;
  assign y = s_vcount;

  // The raw sync window, then the polarity applied. Kept in two steps because
  // getting the polarity backwards is the classic "monitor says no signal" bug
  // and this way the window itself is readable in a waveform.
  wire s_hsync_window = (s_hcount >= h_sync_beg) && (s_hcount < h_sync_fin);
  wire s_vsync_window = (s_vcount >= v_sync_beg) && (s_vcount < v_sync_fin);

  assign hsync = h_sync_pos ? s_hsync_window : ~s_hsync_window;
  assign vsync = v_sync_pos ? s_vsync_window : ~s_vsync_window;

endmodule

`default_nettype wire