Files
learn-fpga/FemtoRV/RTL/DEVICES/FGA.v
2021-01-04 16:45:49 +01:00

215 lines
7.9 KiB
Verilog

// femtorv32, a minimalistic RISC-V RV32I core
// (minus SYSTEM and FENCE that are not implemented)
//
// Bruno Levy, 2020-2021
//
// This file: FGA: Femto Graphics Adapter
// Note: VRAM is write-only ! (the read port is used by HDMI)
// Mode 1: 320x200x16bpp.
`include "HDMI_clock.v"
`include "TMDS_encoder.v"
module FGA(
input wire clk, // system clock
input wire sel, // if zero, writes are ignored
input wire [3:0] mem_wmask, // mem write mask and strobe /write Legal values are 000,0001,0010,0100,1000,0011,1100,1111
input wire [16:0] mem_address, // address in graphic memory (128K), word-aligned
input wire [31:0] mem_wdata, // data to be written
input wire pixel_clk, // 25 MHz
output wire [3:0] gpdi_dp, // HDMI signals, blue, green, red, clock
// dgpi_dn generated by pins (see ulx3s.lpf)
input wire io_wstrb,
input wire io_rstrb,
input wire sel_cntl, // select control register (R/W)
input wire sel_dat, // select data in (W)
output wire [31:0] rdata // data read from register
);
wire [14:0] vram_word_address = mem_address[16:2];
reg [31:0] VRAM[32767:0];
/************************* HDMI signal generation ***************************/
// This part is just like a VGA generator.
reg [9:0] X, Y; // current pixel coordinates
reg hSync, vSync; // horizontal and vertical synchronization
reg DrawArea; // asserted if current pixel is in drawing area
reg mem_busy; // asserted if memory transfer is running.
// read control register
assign rdata = (io_rstrb && sel_cntl) ?
{vSync,hSync,DrawArea,mem_busy,4'b0,2'b0,X,2'b0,Y} :
32'b0;
always @(posedge pixel_clk) begin
DrawArea <= (X<640) && (Y<480);
X <= (X==799) ? 0 : X+1;
if(X==799) Y <= (Y==524) ? 0 : Y+1;
hSync <= (X>=656) && (X<752);
vSync <= (Y>=490) && (Y<492);
end
// Fetch pixel data
reg [16:0] pix_address;
reg [16:0] row_start;
reg [15:0] pix_data;
always @(posedge pixel_clk) begin
if(X == 0) begin
if(Y == 0) begin
row_start <= 0;
pix_address <= 0;
end else begin
// Increment row address every 2 Y (2 because 320x200->640x400)
if(Y[0]) begin
row_start <= row_start + 640;
pix_address <= row_start + 640;
end else begin
pix_address <= row_start;
end
end
end
// Increment pix_address every 4 X (2 because 320x200->640x400 and 2 because 16 bpp)
if(X[1:0] == 2'b11) pix_address <= pix_address + 4;
// Draw 640x400 zone, and hide first pixel row (fetch delay)
// Select word's 16msb or 16lsbs based on X[1] (again, every 4 X)
pix_data <= (X == 0 || Y > 400) ? 0 :
X[1] ? VRAM[pix_address[16:2]][31:16] : VRAM[pix_address[16:2]][15:0];
end
// Decode pixel data: RRRRR GGGGG 0 BBBBB
wire [7:0] R = {pix_data[15:11],3'b000};
wire [7:0] G = {pix_data[10:5], 2'b00 };
wire [7:0] B = {pix_data[4:0], 3'b000};
// RGB TMDS encoding
// Generate 10-bits TMDS red,green,blue signals. Blue embeds HSync/VSync in its
// control part.
wire [9:0] TMDS_R, TMDS_G, TMDS_B;
TMDS_encoder encode_R(.clk(pixel_clk), .VD(R), .CD(2'b00) , .VDE(DrawArea), .TMDS(TMDS_R));
TMDS_encoder encode_G(.clk(pixel_clk), .VD(G), .CD(2'b00) , .VDE(DrawArea), .TMDS(TMDS_G));
TMDS_encoder encode_B(.clk(pixel_clk), .VD(B), .CD({vSync,hSync}), .VDE(DrawArea), .TMDS(TMDS_B));
// 250 MHz clock
// This one needs some FPGA-specific specialized blocks (a PLL).
wire clk_TMDS; // The 250 MHz clock used by the serializers.
HDMI_clock hdmi_clock(.clk(pixel_clk), .hdmi_clk(clk_TMDS));
// Modulo-10 clock divider (my version, using a 1-hot in a 10 bits ring)
reg [9:0] TMDS_mod10=1;
wire TMDS_shift_load = TMDS_mod10[9];
always @(posedge clk_TMDS) TMDS_mod10 <= {TMDS_mod10[8:0],TMDS_mod10[9]};
// Shifters
// Every 10 clocks, we get a fresh R,G,B triplet from the TMDS encoders,
// else we shift.
reg [9:0] TMDS_shift_R=0, TMDS_shift_G=0, TMDS_shift_B=0;
always @(posedge clk_TMDS) begin
TMDS_shift_R <= TMDS_shift_load ? TMDS_R : {1'b0,TMDS_shift_R[9:1]};
TMDS_shift_G <= TMDS_shift_load ? TMDS_G : {1'b0,TMDS_shift_G[9:1]};
TMDS_shift_B <= TMDS_shift_load ? TMDS_B : {1'b0,TMDS_shift_B[9:1]};
end
// HDMI signal, positive part of the differential pairs
// (negative part generated by the pins, see ulx3s.lpf)
assign gpdi_dp[2] = TMDS_shift_R[0];
assign gpdi_dp[1] = TMDS_shift_G[0];
assign gpdi_dp[0] = TMDS_shift_B[0];
assign gpdi_dp[3] = pixel_clk;
/*************************************************************************/
// control register - commands
localparam SET_MODE = 7'd0; // TODO
localparam SET_PALETTE_R = 7'd1; // TODO
localparam SET_PALETTE_G = 7'd2; // TODO
localparam SET_PALETTE_B = 7'd3; // TODO
localparam SET_WWINDOW_X = 7'd4;
localparam SET_WWINDOW_Y = 7'd5;
// Emulation of SSD1351 OLED display.
// - write window command, two commands:
// (send 32 bits to IO_FGA_CNTL hardware register)
// CMD=4: SET_WWINDOW_X: X2[11:0] X1[11:0] CMD[7:0]
// CMD=5: SET_WWINDOW_Y: Y2[11:0] Y1[11:0] CMD[7:0]
//
// - write data: send 8 bits to IO_FGA_DAT hardware register
// MSB first, encoding follows SSD1351: RRRRR GGGGG 0 BBBBB
reg[9:0] window_x1, window_x2, window_y1, window_y2, window_x, window_y;
reg[16:0] window_row_start;
reg[16:0] window_byte_address;
always @(posedge clk) begin
if(io_wstrb) begin
if(sel_cntl) begin
case(mem_wdata[7:0])
SET_WWINDOW_X: begin // SET_WWINDOW_X command
window_x1 <= mem_wdata[19:8];
window_x2 <= mem_wdata[31:20];
window_x <= mem_wdata[19:8];
mem_busy <= 1'b1;
end
SET_WWINDOW_Y: begin // SET_WWINDOW_Y command
window_y1 <= mem_wdata[19:8];
window_y2 <= mem_wdata[31:20];
window_y <= mem_wdata[19:8];
mem_busy <= 1'b1;
window_row_start <= (mem_wdata[19:8] * 320 + window_x1) << 1;
window_byte_address <= (mem_wdata[19:8] * 320 + window_x1) << 1;
end
endcase
end else if(sel_dat) begin // one byte of data sent to IO_FGA_DAT
// increment pixel address.
window_byte_address <= window_byte_address + 1;
if(window_byte_address[0]) begin
if(window_x == window_x2) begin
if(window_y == window_y2) begin
mem_busy <= 1'b0;
end else begin
window_y <= window_y+1;
window_x <= window_x1;
window_byte_address <= window_row_start + 640;
window_row_start <= window_row_start + 640;
end
end else begin
window_x <= window_x + 1;
end
end
end // if (sel_dat)
end // if (wstrb)
end // always @ (posedge clk)
// write VRAM (interface with processor)
always @(posedge clk) begin
if(io_wstrb) begin
if(sel_dat && mem_busy) begin // one byte of data sent to IO_FGA_DAT
// write data to VRAM.
case(window_byte_address[1:0])
// Note: bytes are swapped (following SSD1351 norm)
2'b00: VRAM[window_byte_address[16:2]][15:8 ] <= mem_wdata[7:0];
2'b01: VRAM[window_byte_address[16:2]][ 0:7 ] <= mem_wdata[7:0];
2'b10: VRAM[window_byte_address[16:2]][31:24] <= mem_wdata[7:0];
2'b11: VRAM[window_byte_address[16:2]][23:16] <= mem_wdata[7:0];
endcase
end
end else if(sel && !mem_busy) begin
if(mem_wmask[0]) VRAM[vram_word_address][ 7:0 ] <= mem_wdata[ 7:0 ];
if(mem_wmask[1]) VRAM[vram_word_address][15:8 ] <= mem_wdata[15:8 ];
if(mem_wmask[2]) VRAM[vram_word_address][23:16] <= mem_wdata[23:16];
if(mem_wmask[3]) VRAM[vram_word_address][31:24] <= mem_wdata[31:24];
end
end
endmodule