Making My Own VGA Driver In SystemVerilog
/This is a continuation of my posts about my final project for EE271, continued from the Project selection process here, here, and here.
The first thing I wanted to get working in this design was the VGA output, as the whole design rests on being able to display the results on a monitor. Not to get too into the weeds of the overall architecture, but I knew I would be using cascaded block rams to create a 1 by 524k frame buffer for the output. 1 bit per pixel since it's only a black and white output, 10 bits for the x coordinate and 9 for the y coordinate, concat them together to create a 19 bit addressing system.
The first step was to figure out how the VGA signal is supposed to behave. I went to google and found an excellent, and very informative website called ePanorama that has a detailed page with timing information, as well as a calculator to figure out timings for different resolutions.
VGA was created a long time ago, during the time of CRT monitors, the time it took for the beam to travel back across the screen to display the next row had to be accounted for. As well as the time it takes for the beam to travel from the bottom of the screen back to the top to start the next frame. These times are referred to as the front porch, back porch, and sync time. What they lead to is an effective resolution that is larger than the 640x480 you will be displaying. The effective resolution then becomes 800x525. During the times outside of the display resolution you need to output a solid black signal, as many monitors and TVs use that time to calibrate the output to the signal being input. There is also a pulse presented on the horizontal and vertical sync lines during the blacked out periods to inform the monitor to go to the next line or back to the top of the screen. I also ended up finding another extremely useful document from a gentlemen named Eduardo Sanchez, called A VGA Display Controller, which i used for reference due to its many pictures and excellent explanations of the timing involved. This image in particular was very helpful to me.
Some things to keep in mind for this design are that because I am using the Basys 3 board, I have 3 4-bit dacs, one for each color, and also that the pixel clock for VGA output at 640x480@60Hz needs to be 25MHz. I was using a faster clock for the rest of the game, so I used a counter inside the VGA module to cut that down to the appropriate 25MHz. This will be covered in more detail in the next post about architecture.
The way I went about building up this design was to use a counter to keep track of x coordinates, that would increment with each pixel clock, and then increment the y coordinate counter each time the x coordinate reached its maximum. These coordinates would then be passed out as the address to the frame buffer block ram, which would return either a 0 or a 1, determining whether the output would be black or white for that pixel. There is also a check to determine whether we are inside the viewable region of the screen, and output black if we are not.
My first attempt at this design did not go particularly well, but it still sort of worked. I loaded the frame buffer with random 0s and 1s just to test the display output. I went and plugged it into a VGA monitor only to see the output shaking and shimmering around on the screen, it was there, but not a solid picture. As an aside, over the course of the term I had sort of gotten lazy about my next state logic and then turning the next state into the present state. I took a lot of shortcuts to save space in my code and this final project is where all that laziness came home to roost.
I don't have a video of shame to show you how this was borne out, nor do I have the original SystemVerilog to show you, but I will just say, it was bad. I decided then, to just start over with a blank slate. I went through and much more methodically determined next states, with separate transitions to those states. This diligence paid off, as the design worked the first time, displaying a solid image on the monitor when I plugged it in.
module v_driver( // Clock input input wire clk, // Data input to be displayed, 1-bit becuase its black and white input wire data, // Outputs to trigger the transfer of data and the calculation of the next frame output reg transfer_start, output reg logic_start, // Outputs for the VGA connector output wire [9:0] x_val, output wire [8:0] y_val, output wire h_sync, v_sync, output wire [3:0] red, green, blue ); // Parameters for the size of the screen parameter X_MAX = 10'd639; parameter Y_MAX = 9'd479; parameter X_SIZE = 10'd799; parameter Y_SIZE = 10'd525; parameter X_ZERO = 10'b0000000000; parameter Y_ZERO = 10'b0000000000; parameter COUNT_MULT = 3'd6; // Registers to track the current and next position of the output reg [9:0] x_reg = X_ZERO, x_next = X_ZERO + 1'b1, y_reg = Y_ZERO, y_next = Y_ZERO; // Registers for the values of the color outputs reg [3:0] red_reg = 4'h0, red_next = 4'h0, green_reg = 4'h0, green_next = 4'h0, blue_reg = 4'h0, blue_next = 4'h0; // Registers to track the sync pusles both horizontally and vertically reg h_sync_reg = 1'b0, h_sync_next = 1'b0, v_sync_reg = 1'b0, v_sync_next = 1'b0; // Register to track whether the output should be enabled or not reg v_en; // Counter to divide the clock by 6 reg [2:0] count = 3'b000, count_next = 3'b000; // Always block to clock through the next state of all the registers in the design always @ (posedge clk) begin count <= count_next; x_reg <= x_next; y_reg <= y_next; v_sync_reg <= v_sync_next; h_sync_reg <= h_sync_next; red_reg <= red_next; green_reg <= green_next; blue_reg <= blue_next; v_en <= ( (x_reg < 10'd640) & (y_reg < 10'd480) ); // Case block to output the read data from the memory or to output black // if we're outside the displayed portion of the screen case(v_en) 1: begin red_next <= {4{data}}; green_next <= {4{data}}; blue_next <= {4{data}}; end 0: begin red_next <= 4'h0; green_next <= 4'h0; blue_next <= 4'h0; end endcase end always_comb begin // Start the transfer at the 432nd line, so that the transfer wont over write anything // that hasn't been displayed yet but will finish as early as possible transfer_start = (x_next == X_ZERO & y_next == 10'd432) ? 1'b1 : 1'b0; // Logic will start calculating the next frame once the new frame starts logic_start = (x_next == X_ZERO + 1'b1 & y_next == Y_ZERO) ? 1'b1 : 1'b0; // sync pulses for ~90 pixels on the horizontal and ~2 pixels on the vertical h_sync_next = (x_reg > 10'd659) & (x_reg < 10'd756); v_sync_next = (y_reg > 10'd493) & (y_reg < 10'd496); // the counter to divide the clock by 6 count_next = (count < COUNT_MULT - 1'b1) ? count + 1'b1 : 4'b0000; // Count x is the counter is at 5, increment y if x is at the end of the row // reset both to 0 if they reach the end of the screen y_next = (x_reg == X_SIZE) ? ( (y_reg < Y_SIZE) ? y_reg + 1'b1 : Y_ZERO) : y_reg; x_next = (x_reg == X_SIZE) ? X_ZERO : ( (count == COUNT_MULT - 1'b1) ? ( x_reg + 1'b1 ) : x_reg ); end // Assign output registers // X and Y registers form the address for the memory to read assign red = red_reg; assign green = green_reg; assign blue = blue_reg; assign h_sync = h_sync_reg; assign v_sync = v_sync_reg; assign x_val = x_reg; assign y_val = y_reg[8:0]; endmodule
Overall I think it was pretty valuable to write my own VGA driver. Sure I could have just used one of the professor's modules to do so, but I don't really like to abstract away things without at least learning how they work first. In this case, the easiest way to learn how it worked was to build it myself. It might not be the cleanest VGA implementation in SystemVerilog out there, but it is mine and it did the job I needed it to. I do wish I had used a version controlling system, so that the horror of my first attempt would be here for you all to see.
Tune in next time, when I will talk about the overall architectural design of the game, and several implementation problems I ran into.