diff --git a/build.tcl b/build.tcl
index 8096b62..ab4e095 100644
--- a/build.tcl
+++ b/build.tcl
@@ -17,6 +17,7 @@ if {$argc == 2} {
if {$dev eq "nano20k"} {
set_device GW2AR-LV18QN88C8/I7 -device_version C
add_file src/nano20k/config.v
+ add_file -type verilog "src/snes2hdmi_nano.v"
add_file -type cst "src/nano20k/snestang.cst"
add_file -type verilog "src/nano20k/gowin_pll_hdmi.v"
add_file -type verilog "src/nano20k/gowin_pll_snes.v"
@@ -34,6 +35,7 @@ if {$dev eq "nano20k"} {
} else {
error "Unknown controller $controller"
}
+ add_file -type verilog "src/snes2hdmi.v"
add_file -type verilog "src/primer25k/gowin_pll_27.v"
add_file -type verilog "src/primer25k/gowin_pll_hdmi.v"
add_file -type verilog "src/primer25k/gowin_pll_snes.v"
@@ -50,6 +52,7 @@ if {$dev eq "nano20k"} {
} else {
error "Unknown controller $controller"
}
+ add_file -type verilog "src/snes2hdmi.v"
add_file -type verilog "src/primer25k/gowin_pll_27.v"
add_file -type verilog "src/primer25k/gowin_pll_hdmi.v"
add_file -type verilog "src/primer25k/gowin_pll_snes.v"
@@ -66,6 +69,7 @@ if {$dev eq "nano20k"} {
} else {
error "Unknown controller $controller"
}
+ add_file -type verilog "src/snes2hdmi.v"
add_file -type verilog "src/mega138k/gowin_pll_27.v"
add_file -type verilog "src/mega138k/gowin_pll_hdmi.v"
add_file -type verilog "src/mega138k/gowin_pll_snes.v"
@@ -125,7 +129,6 @@ add_file -type verilog "src/ppuoam.v"
add_file -type verilog "src/smc_parser.v"
add_file -type verilog "src/smp.v"
add_file -type verilog "src/snes.v"
-add_file -type verilog "src/snes2hdmi.v"
add_file -type verilog "src/controller_adapter.sv"
add_file -type verilog "src/controller_ds2.sv"
add_file -type verilog "src/controller_snes.v"
diff --git a/snestang_nano20k.gprj b/snestang_nano20k.gprj
index 36bdb80..1ee89c4 100644
--- a/snestang_nano20k.gprj
+++ b/snestang_nano20k.gprj
@@ -60,7 +60,8 @@
-
+
+
diff --git a/src/iosys/iosys.v b/src/iosys/iosys.v
index 9c42a25..2ee3ae8 100644
--- a/src/iosys/iosys.v
+++ b/src/iosys/iosys.v
@@ -43,8 +43,8 @@ module iosys #(
// OSD display interface
output overlay,
- input [10:0] overlay_x, // 720p
- input [9:0] overlay_y,
+ input [7:0] overlay_x, // 0-255
+ input [7:0] overlay_y, // 0-223
output [14:0] overlay_color, // BGR5
input [11:0] joy1, // joystick 1: (R L X A RT LT DN UP START SELECT Y B)
input [11:0] joy2, // joystick 2
@@ -219,7 +219,7 @@ picorv32 #(
// text display @ 0x0200_0000
textdisp #(.COLOR_LOGO(COLOR_LOGO)) disp (
.clk(clk), .hclk(hclk), .resetn(resetn),
- .overlay_x(overlay_x), .overlay_y(overlay_y), .overlay_color(overlay_color),
+ .x(overlay_x), .y(overlay_y), .color(overlay_color),
.reg_char_we(textdisp_reg_char_sel ? mem_wstrb : 4'b0),
.reg_char_di(mem_wdata)
);
diff --git a/src/iosys/textdisp.v b/src/iosys/textdisp.v
index 7c048cf..562b6a4 100644
--- a/src/iosys/textdisp.v
+++ b/src/iosys/textdisp.v
@@ -2,18 +2,19 @@
// to print characters to the display.
module textdisp(
- input clk, // main logic clock
- input hclk, // hdmi clock
- input resetn,
+ input clk, // main logic clock
+ input hclk, // hdmi clock
+ input resetn,
- input [10:0] overlay_x,
- input [9:0] overlay_y,
- output reg [14:0] overlay_color,
+ input [7:0] x, // 0 - 255
+ input [7:0] y, // 0 - 223
+ output reg [14:0] color, // pixel color, NOTE: 2-cycle latency
+ // we need 1 cycle to fetch character and 1 cycle to fetch font byte
// PicoRV32 I/O interface. Every write updates one character
// [23:16]: x, [15:8]: y, [7-0]: character to print
- input [3:0] reg_char_we,
- input [31:0] reg_char_di
+ input [3:0] reg_char_we,
+ input [31:0] reg_char_di
);
// BGR
@@ -21,7 +22,12 @@ localparam [14:0] COLOR_BACK = 15'b00000_00000_00000;
localparam [14:0] COLOR_TEXT = 15'b10000_11111_11111; // yellow
//localparam [14:0] COLOR_TEXT = 15'b11000_11000_11000; // white
localparam [14:0] COLOR_CURSOR = 15'b10000_11000_11111; // orange
-parameter [14:0] COLOR_LOGO = 15'b00000_10101_00000; // green
+// parameter [14:0] COLOR_LOGO = 15'b00000_10101_00000; // green
+parameter [14:0] COLOR_LOGO = 15'b00000_10011_11111;
+
+// 72x14 pixels 1bpp logo
+localparam LOGO_X = 128-36;
+localparam LOGO_Y = 201;
//
// Pixel output logic for characters and logo:
@@ -31,9 +37,7 @@ parameter [14:0] COLOR_LOGO = 15'b00000_10101_00000; // green
//
reg [10:0] mem_addr_b;
reg [7:0] mem_do_b;
-reg [1:0] mem_cnt;
reg is_cursor;
-reg [2:0] xoff, yoff;
reg [14:0] overlay_color_buf;
wire [1:0] cmd = reg_char_di[31:24];
@@ -56,111 +60,64 @@ gowin_dpb_menu menu_mem (
.dinb(), .doutb(mem_do_b)
);
-localparam overlay_x_start = 256 - 3;
-reg [7:0] px; // = (overlay_x - overlay_x_start) / 3;
-reg [7:0] py; // = (overlay_y - 24) / 3;
-reg [9:0] overlay_y_r;
-reg [1:0] y_cnt;
-
-// 72x14 pixels 1bpp logo
-localparam LOGO_X = 128-36;
-localparam LOGO_Y = 201;
-
reg [6:0] logo_addr;
reg [2:0] logo_xoff;
reg logo_active;
-
-always @* begin
- case (mem_cnt)
- 2'd0: mem_addr_b <= {1'b0, py[7:3], px[7:3]}; // fetch next character
- 2'd1: mem_addr_b <= logo_active ?
- {4'b0111, logo_addr} : // fetch logo byte
- {1'b1, mem_do_b[7] ? 7'h3F : mem_do_b[6:0], yoff};
- // fetch font byte (? for non-ASCII chars)
- default: mem_addr_b <= 0;
+reg [14:0] color_buf;
+reg [7:0] x_r;
+
+// Rendering state machine
+reg [2:0] state;
+localparam MAIN = 0; // default state and fetching character
+localparam FETCH_FONT = 1; // fetch font byte
+localparam FETCH_LOGO = 2; // fetch logo byte
+localparam OUTPUT = 3; // output new pixel and fetch character
+
+always @* begin // address and output logic
+ color = color_buf;
+ case (state)
+ MAIN: mem_addr_b = {1'b0, y[7:3], x[7:3]};
+ FETCH_FONT: mem_addr_b = {1'b1, mem_do_b[7] ? 7'h3F : mem_do_b[6:0], y[2:0]};
+ FETCH_LOGO: mem_addr_b = {4'b0111, logo_addr};
+ OUTPUT: begin
+ mem_addr_b = {1'b0, y[7:3], x[7:3]};
+ if (logo_active)
+ color = mem_do_b[logo_xoff] ? COLOR_LOGO : COLOR_BACK;
+ else
+ color = mem_do_b[x[2:0]] ? (is_cursor ? COLOR_CURSOR : COLOR_TEXT) : COLOR_BACK;
+ end
+ default: mem_addr_b = 0;
endcase
end
-reg py_logo_active;
-
-always @(posedge hclk) begin
- if (py == LOGO_Y)
- py_logo_active <= 1;
- if (py == LOGO_Y + 14)
- py_logo_active <= 0;
-
- if (px >= LOGO_X && px < LOGO_X+71 && py_logo_active)
- logo_active <= 1;
- else
- logo_active <= 0;
-end
-
-always @(posedge hclk) begin
-
- // mem_cnt: cycle counter (0,1,2)
- mem_cnt <= mem_cnt + 2'd1;
- if (mem_cnt == 2'd2) begin
- mem_cnt <= 0;
- px <= px + 8'd1;
- end
- if (overlay_x == overlay_x_start-1) begin
- mem_cnt <= 0;
- px <= 0;
- end
- overlay_y_r <= overlay_y;
- if (overlay_y != overlay_y_r) begin
- if (overlay_y == 10'd24) begin
- py <= 0;
- y_cnt <= 0;
- end else begin
- y_cnt <= y_cnt + 2'd1;
- if (y_cnt == 2'd2) begin
- py <= py + 10'd1;
- y_cnt <= 0;
+always @(posedge hclk) begin // actual state machine
+ reg [7:0] logo_x, logo_y;
+
+ x_r <= x;
+
+ case (state)
+ MAIN, OUTPUT: begin
+ state <= MAIN;
+ if (state == OUTPUT) color_buf <= color;
+ if (x[0] != x_r[0]) begin // moved to new pixel
+ if (x >= LOGO_X && x < LOGO_X+72 && y >= LOGO_Y && y < LOGO_Y+14) begin
+ state <= FETCH_LOGO;
+ logo_active <= 1;
+ end else begin
+ state <= FETCH_FONT;
+ logo_active <= 0;
end
end
- end
-
- // address generation
- case (mem_cnt)
- 2'd0: begin
- reg [6:0] logo_x;
- reg [3:0] logo_y;
-
- // output color from last cycle
- overlay_color <= overlay_color_buf;
-
- // for next character
- is_cursor <= px[7:3] == 0;
- xoff <= px[2:0];
- yoff <= py[2:0];
-
- logo_x = px - LOGO_X;
- logo_y = py - LOGO_Y;
- // logo_addr <= logo_y * 9 + logo_x[6:3];
+ logo_x = x - LOGO_X;
+ logo_y = y - LOGO_Y;
logo_addr <= {logo_y, 3'b0} + logo_y + logo_x[6:3];
logo_xoff <= logo_x[2:0];
-
- // mem_do_b is character after cycle 0
- end
- 2'd1: begin
- // bram fetches font or logo byte to mem_do_b
- end
- 2'd2: begin
- // compute output color
- overlay_color_buf <= COLOR_BACK;
- if (logo_active) begin
- if (mem_do_b[logo_xoff])
- overlay_color_buf <= COLOR_LOGO;
- else
- overlay_color_buf <= COLOR_BACK;
- end else if (mem_do_b[xoff]) begin
- if (is_cursor)
- overlay_color_buf <= COLOR_CURSOR;
- else
- overlay_color_buf <= COLOR_TEXT;
- end
+ is_cursor <= x[7:3] == 0;
end
+
+ FETCH_FONT, FETCH_LOGO: state <= OUTPUT;
+
+ default: ;
endcase
end
diff --git a/src/snes2hdmi.v b/src/snes2hdmi.v
index b871a47..aa78343 100644
--- a/src/snes2hdmi.v
+++ b/src/snes2hdmi.v
@@ -16,8 +16,8 @@ module snes2hdmi (
input [8:0] ys, // {field,y}
input overlay,
- output [10:0] overlay_x,
- output [9:0] overlay_y,
+ output [7:0] overlay_x,
+ output [7:0] overlay_y,
input [14:0] overlay_color,
input [15:0] audio_l,
@@ -72,13 +72,13 @@ module snes2hdmi (
// BRAM line buffer for 16 lines. Each pixel is 5:5:5 RGB
// Each BRAM block holds 4 lines. So 16 lines needs 4 blocks.
//
- localparam BUF_WIDTH = 4; // 2 ^ BUF_WIDTH lines
+ localparam BUF_WIDTH = 5; // 2 ^ BUF_WIDTH lines
localparam BUF_SIZE = 1 << BUF_WIDTH;
reg [14:0] mem [0:BUF_SIZE*256-1];
- reg [11:0] mem_portA_addr;
+ reg [BUF_WIDTH+8-1:0] mem_portA_addr;
reg [14:0] mem_portA_wdata;
reg mem_portA_we;
- wire [11:0] mem_portB_addr;
+ wire [BUF_WIDTH+8-1:0] mem_portB_addr;
reg [14:0] mem_portB_rdata;
reg mem_writeto = 1'b0; // current line we are writing to. we read from the other line
@@ -91,15 +91,18 @@ module snes2hdmi (
reg sync_done = 1'b0;
reg hdmi_first_line;
reg pause_sync; // pause for even number of cycles for PPU sdram access to stay in sync
+ reg hdmi_first_line_r, hdmi_first_line_rr;
always @(posedge clk) begin
+ hdmi_first_line_r <= hdmi_first_line;
+ hdmi_first_line_rr <= hdmi_first_line_r;
if (pause_snes_for_frame_sync) pause_sync <= ~pause_sync;
if (~sync_done) begin
if (~pause_snes_for_frame_sync) begin
- if (ys[7:0] == 8'd2 && snes_refresh) begin // halt SNES during snes dram refresh on line 2
+ if (ys[7:0] == 8'd1 && snes_refresh) begin // halt SNES after first line is buffered
pause_snes_for_frame_sync <= 1'b1;
pause_sync <= 0;
end
- end else if (hdmi_first_line && pause_sync) begin // HDMI frame start, now resume SNES
+ end else if (hdmi_first_line_rr && pause_sync) begin // HDMI frame start, now resume SNES
pause_snes_for_frame_sync <= 1'b0;
sync_done <= 1'b1;
end
@@ -108,7 +111,7 @@ module snes2hdmi (
if (ys[7:0] == 8'd200) sync_done <= 1'b0; // reset sync_done for next frame
end
always @(posedge clk_pixel) begin
- if (cy == 10'd24 && cx >= 11'd256 && cx < 11'd356) // first 100 pixels of first line
+ if (cy == 0 && cx >= 11'd160)
hdmi_first_line <= 1;
else
hdmi_first_line <= 0;
@@ -202,43 +205,77 @@ module snes2hdmi (
//
// Video
//
- assign active = cx >= 11'd256 && cx < 11'd1024 && cy >= 10'd24 && cy < 10'd696;
- wire [10:0] x0 = cx - 11'd256;
- wire [9:0] y0 = cy - 10'd24;
-
- // synthesizer takes care of integer const division with a few ALUs and no DSP usage
- assign x = (cx - 256) / 3;
- assign y = (cy - 24) / 3;
- // another way that works but uses more resources
- // 0.3333 = 0.0101010101 binary
-// assign x = (x0 + (x0 >> 2) + (x0 >> 4) + (x0 >> 6) + (x0 >> 8)) >> 2;
- // assign y = (y0 + (y0 >> 2) + (y0 >> 4) + (y0 >> 6) + (y0 >> 8)) >> 2;
- assign mem_portB_addr = {y[BUF_WIDTH-1:0], x};
- reg [23:0] rgb;
-
- assign overlay_x = cx;
- assign overlay_y = cy;
+ reg [23:0] rgb; // actual RGB output
+ reg active;
+ reg [7:0] xx; // scaled-down pixel position
+ reg [7:0] yy;
+ reg [10:0] xcnt;
+ reg [10:0] ycnt; // fractional scaling counters
+ reg [9:0] cy_r;
+ assign mem_portB_addr = {yy[BUF_WIDTH-1:0], xx};
+ assign overlay_x = xx;
+ assign overlay_y = yy;
+ localparam XSTART = (1280 - 960) / 2; // 960:720 = 4:3
+ localparam XSTOP = (1280 + 960) / 2;
+
+ // address calculation
+ // Assume the video occupies fully on the Y direction, we are upscaling the video by `720/height`.
+ // xcnt and ycnt are fractional scaling counters.
+ always @(posedge clk_pixel) begin
+ reg active_t;
+ reg [10:0] xcnt_next;
+ reg [10:0] ycnt_next;
+ xcnt_next = xcnt + 256;
+ ycnt_next = ycnt + 224;
+
+ active_t = 0;
+ if (cx == XSTART - 1) begin
+ active_t = 1;
+ active <= 1;
+ end else if (cx == XSTOP - 1) begin
+ active_t = 0;
+ active <= 0;
+ end
- // calc rgb value to hdmi
- always @(posedge clk_pixel) r_active <= active;
-
- always @* begin
- if (r_active)
-// if (y < 1) // XXX: for debug purposes, show a gradient on the top
-// rgb <= {x, x, x};
-// else
- if (~overlay)
- rgb <= {mem_portB_rdata[4:0], 3'b0, mem_portB_rdata[9:5], 3'b0, mem_portB_rdata[14:10], 3'b0};
- else begin
-// if (overlay_color == 0)
-// rgb <= {2'b0, mem_portB_rdata[4:0], 3'b0, mem_portB_rdata[9:5], 3'b0, mem_portB_rdata[14:10], 1'b0};
-// else
- rgb <= {overlay_color[4:0], 3'b0, overlay_color[9:5], 3'b0, overlay_color[14:10], 3'b0};
+ if (active_t | active) begin // increment xx
+ xcnt <= xcnt_next;
+ if (xcnt_next >= 960) begin
+ xcnt <= xcnt_next - 960;
+ xx <= xx + 1;
end
- else
- rgb <= 24'h303030; // show a grey background
+ end
+
+ cy_r <= cy;
+ if (cy[0] != cy_r[0]) begin // increment yy at new lines
+ ycnt <= ycnt_next;
+ if (ycnt_next >= 720) begin
+ ycnt <= ycnt_next - 720;
+ yy <= yy + 1;
+ end
+ end
+
+ if (cx == 0) begin
+ xx <= 0;
+ xcnt <= 0;
+ end
+
+ if (cy == 0) begin
+ yy <= 0;
+ ycnt <= 0;
+ end
+
end
+ // calc rgb value to hdmi
+ always @(posedge clk_pixel) begin
+ if (active) begin
+ if (overlay)
+ rgb <= {overlay_color[4:0],3'b0,overlay_color[9:5],3'b0,overlay_color[14:10],3'b0}; // BGR5 to RGB8
+ else
+ rgb <= {mem_portB_rdata[4:0], 3'b0, mem_portB_rdata[9:5], 3'b0, mem_portB_rdata[14:10], 3'b0};
+ end else
+ rgb <= 24'h303030;
+ end
// HDMI output.
logic[2:0] tmds;
diff --git a/src/snes2hdmi_nano.v b/src/snes2hdmi_nano.v
new file mode 100644
index 0000000..2decb45
--- /dev/null
+++ b/src/snes2hdmi_nano.v
@@ -0,0 +1,288 @@
+// NES video and sound to HDMI converter
+// nand2mario, 2022.9
+// This is a special version for tang nano 20k as it uses less BRAM (4 vs the normal 8)
+
+`timescale 1ns / 1ps
+
+module snes2hdmi (
+ input clk, // snes clock
+ input resetn,
+
+ // snes video and audio signals
+ input dotclk,
+ input hblank,
+ input vblank,
+ input [14:0] rgb5,
+ input [8:0] xs, // {x,dotclk}
+ input [8:0] ys, // {field,y}
+
+ input overlay,
+ output reg [7:0] overlay_x,
+ output reg [7:0] overlay_y,
+ input [14:0] overlay_color,
+
+ input [15:0] audio_l,
+ input [15:0] audio_r,
+ input audio_ready,
+ output audio_en,
+
+ // frame-sync pause happens during snes_refresh
+ input snes_refresh,
+
+ // video clocks
+ input clk_pixel,
+ input clk_5x_pixel,
+ input locked,
+
+ output reg pause_snes_for_frame_sync,
+
+ // output [7:0] led,
+
+ // output signals
+ output tmds_clk_n,
+ output tmds_clk_p,
+ output [2:0] tmds_d_n,
+ output [2:0] tmds_d_p
+);
+
+ localparam FRAMEWIDTH = 1280; // 720P
+ localparam FRAMEHEIGHT = 720;
+ localparam TOTALWIDTH = 1650;
+ localparam TOTALHEIGHT = 750;
+ localparam SCALE = 5;
+ localparam VIDEOID = 4;
+ localparam VIDEO_REFRESH = 60.0;
+
+ localparam IDIV_SEL_X5 = 3;
+ localparam FBDIV_SEL_X5 = 54;
+ localparam ODIV_SEL_X5 = 2;
+ localparam DUTYDA_SEL_X5 = "1000";
+ localparam DYN_SDIV_SEL_X5 = 2;
+
+ localparam CLKFRQ = 74250;
+
+ localparam AUDIO_BIT_WIDTH = 16;
+
+ // video stuff
+ wire [9:0] cy, frameHeight;
+ wire [10:0] cx, frameWidth;
+ reg active, r_active;
+ wire [7:0] x, y; // SNES pixel position
+
+ //
+ // BRAM line buffer for 16 lines. Each pixel is 5:5:5 RGB
+ // Each BRAM block holds 4 lines. So 16 lines needs 4 blocks.
+ //
+ localparam BUF_WIDTH = 4; // 2 ^ BUF_WIDTH lines
+ localparam BUF_SIZE = 1 << BUF_WIDTH;
+ reg [14:0] mem [0:BUF_SIZE*256-1];
+ reg [11:0] mem_portA_addr;
+ reg [14:0] mem_portA_wdata;
+ reg mem_portA_we;
+ wire [11:0] mem_portB_addr;
+ reg [14:0] mem_portB_rdata;
+ reg mem_writeto = 1'b0; // current line we are writing to. we read from the other line
+
+ // We need to do a bit of synchronization as the SNES and HDMI run on separate clocks and they
+ // do not align perfectly.
+ // - Let SNES execute a bit faster (0.5%) than the HDMI stack. This way we always have enough pixels.
+ // - For each frame, we let the SNES run until the first line is rendered, and written to line buffer.
+ // Then we pause the SNES and wait (through pause_snes_for_frame_sync) for HDMI to catch up.
+ // - Once HDMI started the frame, start SNES again.
+ reg sync_done = 1'b0;
+ reg hdmi_first_line;
+ reg pause_sync; // pause for even number of cycles for PPU sdram access to stay in sync
+ always @(posedge clk) begin
+ if (pause_snes_for_frame_sync) pause_sync <= ~pause_sync;
+ if (~sync_done) begin
+ if (~pause_snes_for_frame_sync) begin
+ if (ys[7:0] == 8'd2 && snes_refresh) begin // halt SNES during snes dram refresh on line 2
+ pause_snes_for_frame_sync <= 1'b1;
+ pause_sync <= 0;
+ end
+ end else if (hdmi_first_line && pause_sync) begin // HDMI frame start, now resume SNES
+ pause_snes_for_frame_sync <= 1'b0;
+ sync_done <= 1'b1;
+ end
+ end else
+ pause_snes_for_frame_sync <= 1'b0;
+ if (ys[7:0] == 8'd200) sync_done <= 1'b0; // reset sync_done for next frame
+ end
+ always @(posedge clk_pixel) begin
+ if (cy == 10'd24 && cx >= 11'd256 && cx < 11'd356) // first 100 pixels of first line
+ hdmi_first_line <= 1;
+ else
+ hdmi_first_line <= 0;
+ end
+
+ // BRAM port A read/write
+ always_ff @(posedge clk) begin
+ if (mem_portA_we) begin
+ mem[mem_portA_addr] <= mem_portA_wdata;
+ end
+ end
+
+ // BRAM port B read
+ always_ff @(posedge clk_pixel) begin
+ mem_portB_rdata <= mem[mem_portB_addr];
+ end
+
+ integer j;
+ initial begin
+ // On power-up, fill line buffer with a gradient
+ for (j = 0; j < 512; j=j+1) begin
+ mem[j][14:10] = j;
+ mem[j][9:5] = j;
+ mem[j][4:0] = j;
+ end
+ end
+
+ //
+ // Data input
+ //
+ reg r_dotclk, r_vblank, r_hblank;
+ always @(posedge clk) begin
+ r_dotclk <= dotclk;
+ r_vblank <= vblank;
+ mem_portA_we <= 1'b0;
+ if (dotclk && ~r_dotclk && ~hblank && ~vblank && ys[7:0] < 224) begin // on posedge of dotclk, read a pixel
+ mem_portA_addr <= {ys[BUF_WIDTH-1:0], xs[8:1]};
+ mem_portA_wdata <= rgb5;
+ mem_portA_we <= 1'b1;
+ end
+ end
+
+ always @(posedge clk) begin
+ r_hblank <= hblank;
+ if (hblank & ~r_hblank) begin // flip active line on hblank
+ mem_writeto <= ~mem_writeto;
+ end
+ end
+
+
+ // HDMI TX audio - send 32K sampling rate audio
+ // Hoarse sound issue: https://github.com/nand2mario/snestang/issues/16
+ // SNES average framerate is 60.09881. So we need to pause the SNES a bit every frame to ensure 60 fps HDMI.
+ // For sound, that translate to about 8 samples pause time per frame. So we run our DSP a bit faster to
+ // buffer enough samples before the SNES pause.
+ localparam AUDIO_OUT_RATE = 32000;
+ localparam AUDIO_DELAY = CLKFRQ * 1000 / AUDIO_OUT_RATE / 2;
+ reg [15:0] audio_sample_word [1:0];
+ reg [$clog2(AUDIO_DELAY)-1:0] audio_divider;
+ reg clk_audio;
+ reg audio_rinc;
+ wire audio_full, audio_empty;
+ wire [31:0] audio_sample;
+
+ always @(posedge clk_pixel) begin
+ if (resetn) begin
+ audio_rinc <= 0;
+ if (audio_divider != AUDIO_DELAY - 1)
+ audio_divider <= audio_divider + 1;
+ else begin
+ audio_divider <= 0;
+ clk_audio = ~clk_audio; // generate audio clock @ 32Khz
+ // output audio sample on posedge clk_audio
+ if (!clk_audio && !audio_empty) begin
+ {audio_sample_word[0], audio_sample_word[1]} <= audio_sample;
+ audio_rinc <= 1'b1;
+ end
+ end
+ end
+ end
+
+ // Audio sample FIFO for 16 samples
+ dual_clk_fifo #(.DATESIZE(32), .ADDRSIZE(4)) audio_fifo (
+ .clk(clk), .wrst_n(1'b1),
+ .winc(audio_ready), .wdata({audio_l, audio_r}), .wfull(audio_full),
+ .rclk(clk_pixel), .rrst_n(1'b1),
+ .rinc(audio_rinc), .rdata(audio_sample), .rempty(audio_empty),
+ .almost_full(), .almost_empty()
+ );
+
+ //
+ // Video
+ //
+ assign active = cx >= 11'd256 && cx < 11'd1024 && cy >= 10'd24 && cy < 10'd696;
+ wire [10:0] x0 = cx - 11'd256;
+ wire [9:0] y0 = cy - 10'd24;
+
+ // synthesizer takes care of integer const division with a few ALUs and no DSP usage
+ assign x = (cx - 256) / 3;
+ assign y = (cy - 24) / 3;
+ // another way that works but uses more resources
+ // 0.3333 = 0.0101010101 binary
+// assign x = (x0 + (x0 >> 2) + (x0 >> 4) + (x0 >> 6) + (x0 >> 8)) >> 2;
+ // assign y = (y0 + (y0 >> 2) + (y0 >> 4) + (y0 >> 6) + (y0 >> 8)) >> 2;
+ assign mem_portB_addr = {y[BUF_WIDTH-1:0], x};
+ reg [23:0] rgb;
+
+ reg [1:0] overlay_cnt;
+
+ always @(posedge clk_pixel) begin
+ if (cx == 0) begin
+ overlay_x <= 0;
+ overlay_cnt <= 0;
+ end
+ if (cx >= 256) begin
+ overlay_cnt <= overlay_cnt == 2 ? 0 : overlay_cnt + 1;
+ if (overlay_cnt == 1)
+ overlay_x <= overlay_x + 1;
+ end
+ overlay_y <= y;
+ end
+
+ // calc rgb value to hdmi
+ always @(posedge clk_pixel) r_active <= active;
+
+ always @* begin
+ if (r_active)
+// if (y < 1) // XXX: for debug purposes, show a gradient on the top
+// rgb <= {x, x, x};
+// else
+ if (~overlay)
+ rgb <= {mem_portB_rdata[4:0], 3'b0, mem_portB_rdata[9:5], 3'b0, mem_portB_rdata[14:10], 3'b0};
+ else begin
+// if (overlay_color == 0)
+// rgb <= {2'b0, mem_portB_rdata[4:0], 3'b0, mem_portB_rdata[9:5], 3'b0, mem_portB_rdata[14:10], 1'b0};
+// else
+ rgb <= {overlay_color[4:0], 3'b0, overlay_color[9:5], 3'b0, overlay_color[14:10], 3'b0};
+ end
+ else
+ rgb <= 24'h303030; // show a grey background
+ end
+
+
+ // HDMI output.
+ logic[2:0] tmds;
+
+ hdmi #( .VIDEO_ID_CODE(VIDEOID),
+ .DVI_OUTPUT(0),
+ .VIDEO_REFRESH_RATE(VIDEO_REFRESH),
+ .IT_CONTENT(1),
+ .AUDIO_RATE(AUDIO_OUT_RATE),
+ .AUDIO_BIT_WIDTH(AUDIO_BIT_WIDTH),
+ .START_X(0),
+ .START_Y(0) )
+
+ hdmi( .clk_pixel_x5(clk_5x_pixel),
+ .clk_pixel(clk_pixel),
+ .clk_audio(clk_audio),
+ .rgb(rgb),
+ .reset( ~resetn ),
+ .audio_sample_word(audio_sample_word),
+ .tmds(tmds),
+ .tmds_clock(tmdsClk),
+ .cx(cx),
+ .cy(cy),
+ .frame_width( frameWidth ),
+ .frame_height( frameHeight ) );
+
+ // Gowin LVDS output buffer
+ ELVDS_OBUF tmds_bufds [3:0] (
+ .I({clk_pixel, tmds}),
+ .O({tmds_clk_p, tmds_d_p}),
+ .OB({tmds_clk_n, tmds_d_n})
+ );
+
+endmodule
diff --git a/src/snestang_top.v b/src/snestang_top.v
index 9168710..6322b85 100644
--- a/src/snestang_top.v
+++ b/src/snestang_top.v
@@ -629,8 +629,8 @@ assign snes_joy1_di[1] = 0; // P3
assign snes_joy2_di[1] = 0; // P4
wire [14:0] overlay_color;
-wire [10:0] overlay_x;
-wire [9:0] overlay_y;
+wire [7:0] overlay_x;
+wire [7:0] overlay_y;
wire [7:0] dbg_dat_out_loader;