CW RTTY SSTV PSK31 ACARS FAX SITOR App For the Mac     
I’ve started work on an FPGA based Z80 computer. I wanted to generate the video in the FPGA as well, turns out VGA is fairly easy to generate. There’s numerous websites that describe the timing and parameters of the various VGA modes, and how VGA works.
This project is also my Verilog learning experience, so pardon any horrible code.
In a nutshell, for basic VGA you need a total of 5 digital output lines. One for horizontal sync, one for vertical, and one each for the red, green, and blue video signals. The RGB signals are actually analog, but for my purposes implemented as digital, either on or off. That allows a total of eight colors, including white and black. For now the text is only color. To produce additional colors, the RGB lines can be driven with a D/A converter, which could be as simple as a resistor ladder circuit.
These analog voltages need to be limited to under 1 volt. The FPGA I/O outputs are 3.3 volts so a simple resistor and diode circuit was used for each line. Here’s the circuit, as you can see it is quite simple. Virtually any reasonably fast diode can be used:
I decided to use the standard 640 x 480 60 Hz mode, as it would be more than adequate for my goal of displaying 25 lines of 80 characters.
Each character is displayed as a 8 by 16 pixel matrix, resulting in 640 x 400 pixels, the rest of the screen is not used.
The design was implemented on a RioRand EP2C5T144 Altera Cyclone II FPGA Mini Development Board available at Amazon for $19.99. What a deal.
I used the free version of Altera’s Quartus II software package.
Video memory is a 2K byte dual ported RAM, organized in a linear fashion, using 2000 bytes.
The character generator ROM is a 1K ROM, storing 8 rows of 8 pixels for each of the 128 ASCII characters. Each row is displayed twice, for a total of 16 rows. As of now the high bit of the stored ASCII value in video memory is not used, I plan on eventually using it to implement a low-res color graphics mode, a la the Apple ][.
The RioRand-EP2C5T144 FPGA dev board has a 50 MHz oscillator signal as a clock input. I implemented a 4x PLL to produce an internal 200 MHz clock. While the 50 MHz clock is adequate for 640×480 video, the faster clock will allow higher resolution video modes down the road.
A state machine is used to generate the video. A frame of video actually has 525 scan lines, it starts with 10 “front porch” lines (essentially all black video lines), then 2 lines of vertical sync, followed by 33 more “back porch” lines.
Front porch and back porch are old terms from the original NTSC analog TV system developed in the 1940s. You’ll see that totals 45 lines, leaving 480 lines of actual video.
Each video line has 800 pixels (as far as timing is concerned). There’s a front porch of 16 pixels, 96 pixels of horizontal sync, 48 pixels of a back porch, then 640 pixels of actual video content. At a 200 MHz clock, each pixel is 8 clock cycles.
Prior to the display of a pixel, the video RAM is read. The lower 7 bits of the byte, which is the ASCII value of the character to be displayed, is used along with 3 bits of the video line, to form a 10 bit address into the character generator.
Since each line of a character is displayed twice, bits [3:1[ and not [2:0] of the video line are used. If I was willing to dedicate 2K to the character ROM, I could display 16 rows of pixels for each character.
The resulting VGA video, the video RAM was pre-loaded with data using the Altera MegaWizard option to do so from an Intel Hex file.
Verilog Code:
module vga1 ( clock_in, red, green, blue, hsync, vsync ); // these are placeholders for when we hook up a CPU later reg [15:0] addressWriteVideoRam=16'b0; reg [7:0] dataInWriteVideoRam; wire [7:0] dataOutReadVideoRam; reg writeEnableWriteVideoRam=1'b0; reg writeVideoRamClock; // VGA video generator, undocumented for now. Good luck. input clock_in; // 50 MHz input clock // RGB outputs output reg red; output reg green; output reg blue; // horizontal and vertical sync outputs output reg hsync; output reg vsync; wire pllClock; pll my_pll (clock_in, pllClock); // 200 MHz clock from the 50 MHz board clock reg [9:0] char_rom_address; wire [7:0] char_rom_output; reg char_rom_clock; reg [7:0] char_rom_data_byte; reg pixel; reg [15:0] clockCounter; // 200 MHz clock reg [15:0] pixelCounter; // 25 MHz pixel clock reg video_inclock; // video RAM clock reg video_wren; // not used yet reg [7:0] video_data; // not used yet // 1K x 8 bit video character ROM, 8 lines of 8 pixels for 128 characters, high ASCII bit not used char_rom my_char_rom(char_rom_address, char_rom_clock, char_rom_output); // 2K video dual port RAM for 25 lines of 80 characters video_ram_dp the_video_ram(.address_a (video_display_address[10:0]), .data_a(video_data), .clock_a(video_inclock), .wren_a(video_wren), .q_a(video_data_byte) , .address_b(addressWriteVideoRam[10:0]), .data_b(dataInWriteVideoRam), .clock_b(writeVideoRamClock), .wren_b(writeEnableWriteVideoRam), .q_b(dataOutReadVideoRam) ); reg [15:0] video_display_address; wire [7:0] video_data_byte; reg [15:0] horzCounter=16'd0; reg [15:0] lineCounter=16'd0; // VGA640x480x60_200mhz clock parameter pixel_rate=8; parameter horz_front_porch=16*pixel_rate; parameter horz_sync=96*pixel_rate; parameter horz_back_porch=48*pixel_rate; parameter horz_line=800*pixel_rate; parameter vert_front_porch=10; parameter vert_sync=2; parameter vert_back_porch=33; parameter vert_frame=525; parameter horz_sync_polarity = 1'b0; parameter vert_sync_polarity = 1'b0; parameter first_video_line=49; // first line of video parameter number_of_video_lines=400; // 25 ASCII lines of 16 pixels each is 400 video lines always @(negedge pllClock) begin char_rom_clock<=horzCounter[0]; end always @(posedge pllClock) begin clockCounter<=clockCounter+1'b1; if (clockCounter[2:0]==7) pixelCounter<=pixelCounter+1'b1; if (pixelCounter[2:0]==7) begin case (clockCounter[2:0]) // 0 : not used // 1 : not used 3 : char_rom_address<={video_data_byte[6:0],lineCounter[3:1]}; // high 7 bits of charactor ROM address is the ASCII character, low 3 bits video line counter, we display each line twice 4 : char_rom_data_byte<=char_rom_output; // latch the character ROM data // 5 : not used 6 : video_inclock<=0; // clock video for next ASCII character 7 : video_inclock<=1; endcase end if (horzCounter==0) begin // set video to black red<=1'b0; green<=1'b0; blue<=1'b0; clockCounter<=0; end if (horzCounter==horz_front_porch) hsync<=horz_sync_polarity; if (horzCounter==(horz_front_porch+horz_sync)) hsync<=!horz_sync_polarity; if ( (lineCounter>=(vert_front_porch+vert_sync+vert_back_porch+first_video_line)) && (lineCounter<=(vert_front_porch+vert_sync+vert_back_porch+first_video_line+number_of_video_lines)) ) begin // video frame time if ((pixelCounter[2:0]==6) && (clockCounter[2:0]==7)) video_display_address<=video_display_address+1'b1; if (horzCounter==(horz_front_porch+horz_sync+horz_back_porch)) begin pixelCounter<=0; // start of video line end if (horzCounter>=(horz_front_porch+horz_sync+horz_back_porch)) begin // video line case (pixelCounter[2:0]) 0 : pixel=char_rom_data_byte[0]; 1 : pixel=char_rom_data_byte[1]; 2 : pixel=char_rom_data_byte[2]; 3 : pixel=char_rom_data_byte[3]; 4 : pixel=char_rom_data_byte[4]; 5 : pixel=char_rom_data_byte[5]; 6 : pixel=char_rom_data_byte[6]; 7 : pixel=char_rom_data_byte[7]; endcase // set RGB outputs red<=pixel; green<=pixel; blue<=pixel; end // video line time end // video frame time horzCounter <= horzCounter + 16'd1; if (horzCounter==horz_line) begin // end of scan line, set video to black red<=1'b0; green<=1'b0; blue<=1'b0; horzCounter<=0; lineCounter<=lineCounter+1'b1; if (lineCounter>=(vert_front_porch+vert_sync+vert_back_porch)) begin // video frame time if (lineCounter[3:0]!=4'd15) video_display_address<=video_display_address-8'd100; else video_display_address<=video_display_address-8'd20; // 80 chars back to beginning of line end if (lineCounter==vert_front_porch) vsync<=vert_sync_polarity; if (lineCounter==(vert_front_porch+vert_sync)) vsync<=!vert_sync_polarity; if (lineCounter==vert_frame) begin // end of the video frame, start over video_display_address<=466; // offset so we start reading video data at the correct address lineCounter<=0; end end end endmodule