Last active
December 2, 2023 13:32
-
-
Save JayKickliter/6b18d4f6850948f72c08bf0323badfbb to your computer and use it in GitHub Desktop.
NTSC in Verilog
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module interlaced_ntsc ( | |
input wire clk, | |
input wire [2:0] pixel_data, // 0 ( black )..5 (bright white) | |
output wire h_sync_out, // single clock tick indicating pixel_y will incrememt on next clock ( for debugging ) | |
output wire v_sync_out, // single clock tick indicating pixel_y will reset to 0 or 1 on next clock, depending on the field ( for debugging ) | |
output wire [9:0] pixel_y, // which line | |
output wire [9:0] pixel_x, | |
output wire pixel_is_visible, | |
output reg [2:0] ntsc_out | |
); | |
parameter BASE_PIXEL_X = 10'd184; | |
parameter RESOLUTION_HORIZONTAL = 10'd560; | |
parameter BASE_PIXEL_Y = 10'd89; | |
parameter RESOLUTION_VERTICAL = 10'd400; | |
reg [2:0] luminance; // TODO: Make a better interface for inputting luminance | |
reg [9:0] line_count_reg, | |
line_count_reg_next; // 0..524 | |
reg [1:0] line_type_reg, | |
line_type_reg_next; | |
reg [11:0] horizontal_count_reg, | |
horizontal_count_reg_next; | |
localparam [1:0] LINE_TYPE_EQ = 2'b00, | |
LINE_TYPE_VERTICAL_BLANK = 2'b01, | |
LINE_TYPE_SCANLINE = 2'b10; | |
localparam WIDTH_FRONT_PORCH = 75, // 1.5 uS @ 50 MHz | |
WIDTH_SYNC_TIP = 235, // 4.7 uS @ 50 MHz | |
WIDTH_BACK_PORCH = 235, // 4.7 uS @ 50 MHz | |
WIDTH_VIDEO = 2630, // 52.6 uS @ 50 MHz | |
WIDTH_WHOLE_LINE = 3175, // 63.5 uS @ 50 MHz | |
WIDTH_HALF_LINE = 1588, // 31.75 uS @ 50 MHz | |
WIDTH_EQ_PULSE = 117, // 2.35 uS @ 50 MHz | |
WIDTH_V_SYNC_PULSE = 1353; // 27.05 uS @ 50 MHz | |
localparam [2:0] SIGNAL_LEVEL_SYNC = 3'b000, // 0 V | |
SIGNAL_LEVEL_BLANK = 3'b001, // 0.3 V | |
SIGNAL_LEVEL_BLACK = 3'b010, | |
SIGNAL_LEVEL_DARK_GREY = 3'b011, | |
SIGNAL_LEVEL_GREY = 3'b100, | |
SIGNAL_LEVEL_LIGHT_GREY = 3'b101, | |
SIGNAL_LEVEL_WHITE = 3'b110, | |
SIGNAL_LEVEL_BRIGHT_WHITE = 3'b111; // 1 V | |
localparam HALF_LINE_EVEN_FIELD = 18, | |
HALF_LINE_ODD_FIELD = 527; | |
// ____ _ _ _ _ ____ ____ _ ____ _ _ ____ _ ____ | |
// [__ \_/ |\ | | [__ | | __ |\ | |__| | [__ | |
// ___] | | \| |___ ___] | |__] | \| | | |___ ___] | |
// | |
wire at_half_line_width = ( horizontal_count_reg >= WIDTH_HALF_LINE ); // signals that the current line has | |
// reached a half scanline's 31.75 | |
wire at_full_line_width = ( horizontal_count_reg >= WIDTH_WHOLE_LINE ); // signals that the current line has | |
// reached a normal scanline's 63.5us | |
wire is_a_half_line = ( line_count_reg == HALF_LINE_EVEN_FIELD ) | ( line_count_reg == HALF_LINE_ODD_FIELD ); // signals current line should be treaded as a half | |
wire is_a_whole_line = ~ is_a_half_line; // signals current line should be treaded as a whole | |
wire h_sync = ( is_a_half_line & at_half_line_width ) | ( is_a_whole_line & at_full_line_width ); | |
wire v_sync = h_sync & line_count_reg >= 526; | |
assign h_sync_out = h_sync; | |
assign v_sync_out = v_sync; | |
assign pixel_is_visible = horizontal_count_reg[11:2] >= BASE_PIXEL_X & horizontal_count_reg[11:2] < BASE_PIXEL_X + RESOLUTION_HORIZONTAL & line_count_reg >= BASE_PIXEL_Y & line_count_reg < BASE_PIXEL_Y + RESOLUTION_VERTICAL; | |
assign pixel_x = pixel_is_visible ? horizontal_count_reg[11:2] - BASE_PIXEL_X : 0; | |
assign pixel_y = pixel_is_visible ? line_count_reg - BASE_PIXEL_Y : 0; | |
// _ _ ____ _ _ _ ____ ____ ____ _ ____ ___ ____ ____ | |
// |\/| |__| | |\ | |__/ |___ | __ | [__ | |___ |__/ | |
// | | | | | | \| | \ |___ |__] | ___] | |___ | \ | |
// ___ ____ ____ _ _ ____ ____ ____ ____ | |
// | |__/ |__| |\ | [__ |___ |___ |__/ | |
// | | \ | | | \| ___] | |___ | \ | |
// | |
always @( posedge clk ) | |
begin | |
horizontal_count_reg <= horizontal_count_reg_next; // all registers that are needed for decision | |
line_count_reg <= line_count_reg_next; // keeping are buffered so they hold their | |
line_type_reg <= line_type_reg_next; // current value until the next clock cycle | |
end | |
// _ _ _ _ ____ ____ ___ ____ ___ ____ | |
// | | |\ | |___ [__ | |__| | |___ | |
// |___ | | \| |___ ___] | | | | |___ | |
// | |
always @* // TODO: might be able to move this to a wire signal | |
if ( line_count_reg <= 5 || ( line_count_reg >= 12 && line_count_reg <= 18 ) ) // is this an equalizing pulse line? | |
line_type_reg_next = LINE_TYPE_EQ; | |
else if ( line_count_reg >= 6 && line_count_reg <= 11 ) // is this a vertical blanking line? | |
line_type_reg_next = LINE_TYPE_VERTICAL_BLANK; | |
else | |
line_type_reg_next = LINE_TYPE_SCANLINE; // must be a normal scanline | |
// ____ _ ____ _ _ ____ _ ___ _ _ _ _ _ _ ____ | |
// [__ | | __ |\ | |__| | | | |\/| | |\ | | __ | |
// ___] | |__] | \| | | |___ | | | | | | \| |__] | |
// | |
always @* | |
if ( h_sync ) // reached the end of the current line? | |
horizontal_count_reg_next = 0; // yes, reset counter to 0 | |
else | |
horizontal_count_reg_next = horizontal_count_reg + 1; // nope, advance | |
// this section below used to be signals, but it was hard to read | |
// generates the proper signals depending on line type | |
always @* | |
if ( line_type_reg == LINE_TYPE_EQ ) | |
if ( horizontal_count_reg < WIDTH_EQ_PULSE || (horizontal_count_reg > WIDTH_HALF_LINE && horizontal_count_reg < WIDTH_HALF_LINE + WIDTH_EQ_PULSE )) | |
ntsc_out = SIGNAL_LEVEL_SYNC; | |
else | |
ntsc_out = SIGNAL_LEVEL_BLANK; | |
else if ( line_type_reg == LINE_TYPE_VERTICAL_BLANK ) | |
if ( horizontal_count_reg < WIDTH_V_SYNC_PULSE || (horizontal_count_reg > WIDTH_HALF_LINE && horizontal_count_reg < WIDTH_HALF_LINE + WIDTH_V_SYNC_PULSE )) | |
ntsc_out = SIGNAL_LEVEL_SYNC; | |
else | |
ntsc_out = SIGNAL_LEVEL_BLANK; | |
else if ( line_type_reg == LINE_TYPE_SCANLINE ) | |
begin | |
if ( horizontal_count_reg > WIDTH_FRONT_PORCH && horizontal_count_reg < WIDTH_FRONT_PORCH + WIDTH_SYNC_TIP ) | |
ntsc_out = SIGNAL_LEVEL_SYNC; | |
else if ( horizontal_count_reg > WIDTH_WHOLE_LINE - WIDTH_VIDEO ) | |
ntsc_out = luminance; | |
else | |
ntsc_out = SIGNAL_LEVEL_BLANK; | |
end | |
always @* | |
casex ( {v_sync, h_sync, line_count_reg} ) // a lookup table to determing next line number | |
{1'b1, 1'b1, 10'd526} : line_count_reg_next = 1; // v_sync & line number 526, go to line 1 | |
{1'b1, 1'b1, 10'd527} : line_count_reg_next = 0; // v_sync & line number 527, go to line 0 | |
{1'b0, 1'b1, 10'bx } : line_count_reg_next = line_count_reg + 2; // hsync, but not vsync, jump a line | |
default : line_count_reg_next = line_count_reg; // do nothing | |
endcase | |
// _ _ _ _ _ _ _ _ ____ _ _ ____ ____ | |
// | | | |\/| | |\ | |__| |\ | | |___ | |
// |___ |__| | | | | \| | | | \| |___ |___ | |
// | |
always @* | |
if ( pixel_is_visible ) | |
case ( pixel_data ) | |
0 : luminance = SIGNAL_LEVEL_BLANK; | |
1 : luminance = SIGNAL_LEVEL_DARK_GREY; | |
2 : luminance = SIGNAL_LEVEL_GREY; | |
3 : luminance = SIGNAL_LEVEL_LIGHT_GREY; | |
4 : luminance = SIGNAL_LEVEL_WHITE; | |
5 : luminance = SIGNAL_LEVEL_BRIGHT_WHITE; | |
default : luminance = SIGNAL_LEVEL_BLANK; | |
endcase | |
else | |
luminance = SIGNAL_LEVEL_BLANK; | |
endmodule |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// static text display | |
module top_ntsc( | |
input wire clk, | |
output wire [2:0] ntsc_out | |
); | |
reg [6:0] character_address; | |
reg [2:0] pixel_data; | |
reg [6:0] character; | |
wire [9:0] pixel_y; | |
wire [9:0] pixel_x; | |
reg [9:0] scaled_pixel_x; | |
reg [9:0] scaled_pixel_y; | |
wire pixel_is_visible; | |
wire [4:0] text_row = scaled_pixel_y[9:4]; | |
wire [6:0] text_column = scaled_pixel_x[9:3]; | |
wire [10:0] rom_address; | |
wire [3:0] font_row_address; | |
wire [2:0] bit_address; | |
wire [0:7] font_word; | |
wire font_bit; | |
wire text_bit_on; | |
assign font_row_address = scaled_pixel_y[3:0]; | |
assign rom_address = {character, font_row_address}; | |
assign bit_address = scaled_pixel_x[2:0]; | |
assign font_bit = font_word[bit_address]; | |
always @( posedge clk ) | |
begin | |
scaled_pixel_x = pixel_x[9:2]; | |
scaled_pixel_y = pixel_y[9:2]; | |
end | |
always @* | |
if (font_bit) | |
pixel_data = 3'b010; // green | |
else | |
pixel_data = 3'b000; // black | |
always @* | |
case ( text_column[2:0] ) | |
0 : character = "F"; | |
1 : character = "P"; | |
2 : character = "G"; | |
3 : character = "A"; | |
default : character = 7'h00; | |
endcase | |
interlaced_ntsc ntsc ( | |
.clk ( clk ), | |
.ntsc_out ( ntsc_out ), | |
.pixel_is_visible ( pixel_is_visible ), | |
.pixel_data ( pixel_data ), | |
.pixel_y ( pixel_y ), | |
.pixel_x ( pixel_x ) | |
); | |
font_rom font_unit ( | |
.clk ( clk ), | |
.addr ( rom_address ), | |
.data ( font_word ) | |
); | |
endmodule |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment