Thursday 19 April 2018

Playing with VGA output

Foreword

In the previous post we managed to write the output of our VIC-II module to SDRAM. The resulting frame as retrieved from the SDRAM of the Zybo looked pretty distorted, although we could seem some resemblance of the C64 Welcome screen.

After I posted the last post, I did some investigation into why the frames get distorted. It turned out that an old bitstream file got stuck in the Xilinx SDK Workspace I used, and exporting new bitstream files from Vivado to this Workspace simply didn't override this old bitstream.

Eventually, after some frustration, I deleted this Workspace, created a new a one and exported the bitstream again from my Vivado project to this Workspace. It was only then I had the aha moment: My frame rendered perfectly without any distortions!

Well, one less thing to worry about. In this post then I will focus on something totally different.

In this post we will get VGA output to work. We will, however, not be developing a full solution for displaying the contents of a framebuffer on a monitor, but something simple, which is displaying a screen filled with the character 'A'.

Introduction to VGA timings

When you want to display something on a VGA enabled screen you will spend most of your effort getting to know the meaning of VGA timing parameters. So let us start covering them.

The base of all is the frequency of the pixel clock. The pixel clock basically give the pace at which you dish out pixels of your frame to your monitor. The name says it all: One pixel for every clock cycle of the pixel clock.

It is important to note that at every clock cycle of the pixel clock will you not only be outputting displayable information. You will also have pixel cycles where there is blanking happening and synchronisation. These terms will become clear in a moment.

Three terms you will hear quite often when talking about VGA parameters are front porch, back porch and synchronisation pulse. The following diagram will clarify these terms:


This graph illustrates a typical video signal. In the centre, Picture information, represent the visible parts of the signal.

The two small pedestals in the picture represents synchronisation pulses. A synchronisation pulse basically instructs the monitor to reset the place to draw the next pixel on the screen to the beginning of the next line. These sync pulses ensure that monitor draws the pixels of the video signal to the correct places on the screen.

You will notice that this sync pulse is not directly following the picture information, but rather have some padding surrounding it.

This padding was added to the cater for the limitations of Cathode Ray Tubes, which was used in the first VGA monitors.

Actually calling CRT's "The first VGA monitors" sounds a bit misleading, as if it CRT's was only used briefly as VGA monitors. This is anything but!

CRT's have been used for VGA monitors for quite a number of decades. Even in the early 2000's your standard monitor was a CRT. LCD monitors only really started killing CRT's towards the end of 2010.

But, let us get back to the point of the discussion: what limitations does a CRT have? Let us start by reviewing how a CRT works.

A CRT projects an electron beam on a surface that is coated with phosphor. Where the beam hits the surface, a tiny spot on the screen will illuminate. Thus, to have a picture displayed on the screen this beam needs continuously scan across the whole screen.

This is performed from left to right and from top to bottom. The beam is moved around with the aid of magnetic deflection coils.

When the electron beam reaches the end of a line, the horizontal deflection coils moves the beam rapidly back to the left to start a new line.

During the period when the beam moves rapidly back to the left and resuming scanning from left to right, the beam is not moving at a uniform speed, It is during this period we don't want the beam to draw anything on the screen at all.

It is for this reason we need to add some padding surrounding the horizontal sync pulse, so that drawing on the screen can only resume once the beam has reached a steady speed.

These padding surrounding the sync pulse are also parameters that needs to be specified for a VGA signal. There is two parameters for this purpose: Back Porch and Front Porch.

Front Porch is the period of padding in front of the Sync Pulse and the Back Porch is the period of padding after the sync pulse. In the VGA world, these two parameters is specified in terms of pixels.

So in summary, we have covered the following parameters so far:

  • Pixel Clock Frequency
  • Horizontal resolution, measured in pixels
  • Horizontal Front porch in pixels
  • Horizontal Back Porch in pixels
  • Horizontal Sync pulse width, also measured in pixels.
Most of the parameters above is applicable to the horizontal direction. There is, however, a similar set of parameters for the vertical direction. These parameters are not specified in terms of pixel, but rather in lines:

  • Vertical resolution (lines)
  • Vertical Front Porch (lines)
  • Vertical Back Porch (lines)
  • Vertical Sync pulse (lines)
This is about all there is to VGA timings.

Figuring out the VGA Timings

Before we can start to develop a FPGA implementation for outputting a VGA signal, we need to first figure the VGA timing parameters to use as discussed in the previous section.

For this exercise I am going to use LCD monitor for displaying the signal that has a resolution of 1360X768 @ 60HZ. This is not really a standard VESA resolution that you will find timings for on the VESA website, so I had a bit of a hard time doing Internet searches for finding the parameters.

Eventually I found something useful from the following link:

http://forums.entechtaiwan.com/index.php?topic=2578.25;wap2

Scrolling down the web page I found the timing parameters I was looking for, but package as a Linux modeline:

"1360x768" 85.875 1360 1408 1520 1768 768 769 772 810 +hsync +vsync
I have seen Linux modelines a couple of times in the past and is used to configure the video card when running XWindows. However, I never really paid attention to what these numbers really mean. The numbers in quotaion obviously looks like the target resolution, but the other numbers is a bit Greek to me :-)

So, let us do a bit of further Internet Searching on what these numbers mean...

The following web page comes to the rescue:

http://howto-pages.org/ModeLines/

The key on the page is where they describe how to write down the numbers:

...you write down the frequency of the pixel clock in MHz: 108
Next, you simply list out in this order: HDisplay HSyncStart HSyncEnd HTotal. In my case:
1280 1346 1458 1688.
Fourthly, you list out the corresponding vertical data: VDisplay VSyncStart VSyncEnd VTotal:
1024 1025 1028 1066
We can apply the same reasoning to our modeline. So, the number 85.875 is the frequency of pixel clock.

To understand the rest of the numbers, we should visualise one long line that starts at the beginning of visible data and extends all the way to the end of the Horizontal Back Porch. All the crucial timing elements is then marked as a specific pixel on the line.

So, if we start with the first number after the pixel clock. This number, 1360, indicates that pixel number 1360 is the last visible pixel on the line. Pixels after this pixel is part of the Front Porch.

The Front Porch pixels carries on till we reach pixel number 1408 (e.g. the number in the list of parameters). At this pixel we enable the Horizontal sync pulse which lasts till we reach pixel 1520 after which the sync pulse is switched off.

After the Horizontal Sync pulse is switched off, we are in the Back Porch period which lasts till pixel number 1768.

After pixel 1768 we wrap back to pixel 0, and we are at the beginning of the visible area of the next line.

The set are numbers following 1768 are related to Vertical Syncing, which follows the same convention as the Horizontal parameters. The only difference is that we specify the Vertical parameters in terms of lines.

With Linux modelines somehow demastified, we can now calculate the parameters for use within our FPGA design.

Starting width the Front Porch, we know it starts at pixel 1360 and carries on till pixel 1408, so the Horizontal Front Porch width in terms of pixels is:

1408 - 1360 = 48

Similarly, the Horizontal sync pulse width can be calculated as:

1520 - 1408 = 112

Finally, we can calculate the Back Porch width as:

1768 - 1520 = 248

We can know move on to the Vertical parameters. Front Vertical Porch width:

769 - 768 = 1 line

Vertical sync pulse width:

772 - 769 = 3 lines

Back Vertical Porch:

810 - 772 = 38 lines

The last two parameters in the list, +hsync and +vsync, indicates the polarity of the horizontal and vertical sync pulse. In this case both our sync pulses will trigger a sync action when they are at a logic level '1'.

Designing the FPGA module

We finally have enough information to start the design of our FPGA module.

We start with a very basic skeleton:

module vga(
  input clk,
    );

endmodule


We currently only have the clock as an input port. Obviously this clock will need to clock at the desired pixel frequency which is 85.875MHz.

Next, we should add the various VGA parameters to our module:

module vga(
  input clk,
    );

parameter HORIZ_RES = 1360;
parameter VERT_RES = 768;
parameter HORIZ_BACK_PORCH = 248;
parameter HORIZ_FRONT_PORCH = 48;
parameter HORIZ_SYNC = 112;
parameter VERT_BACK_PORCH = 38;
parameter VERT_FRONT_PORCH = 1;
parameter VERT_SYNC = 3;

endmodule


From these parameters, we add further deducted parameters for triggering the various events during scanlines:

...
parameter TOTAL_HORIZ_RES = HORIZ_RES + HORIZ_BACK_PORCH + HORIZ_SYNC + HORIZ_FRONT_PORCH;
parameter TOTAL_VERT_RES = VERT_RES + VERT_BACK_PORCH + VERT_SYNC + VERT_FRONT_PORCH;
parameter HORIZ_SYNC_START = HORIZ_RES + HORIZ_FRONT_PORCH;
parameter HORIZ_SYNC_END = HORIZ_SYNC_START + HORIZ_SYNC;          
parameter VERT_SYNC_START = VERT_RES + VERT_FRONT_PORCH;
parameter VERT_SYNC_END = VERT_SYNC_START + VERT_SYNC;          
...

Admitted, these parameters looks exactly as the modeline parameters we started with. You would probably only go this approach if you got the VGA parameters in another way and not via a modeline...

Next, which should implement two counters for both the vertical and horizontal directions:

...
reg [10:0] horiz_pos = 0;
reg [10:0] vert_pos = 0;
...
always @(posedge clk)
if (horiz_pos < TOTAL_HORIZ_RES - 1)
  horiz_pos <= horiz_pos + 1;
else begin
  horiz_pos <= 0;
  if (vert_pos < TOTAL_VERT_RES - 1)
  begin
    vert_pos <= vert_pos + 1;
  end else
  begin
    vert_pos <= 0;  
  end
end
...

These counters will synchronise all the functionality within our VGA module.

Next up, let us generate the vertical and horizontal sync pulses:

module vga(
  input clk,
  output vert_sync,
  output horiz_sync,
    );
...
assign vert_sync = vert_pos >= VERT_SYNC_START & vert_pos < VERT_SYNC_END;  
assign horiz_sync = horiz_pos >= HORIZ_SYNC_START & horiz_pos < HORIZ_SYNC_END;
...


Next, we should generate the actual displayable pixel data. As mentioned earlier, we want to display a screen filled with 'A's. We use the A image contained in the C64 Character ROM, which is an 8x8 pixel image.

We will generate the image data in almost the same way as we did with our VIC-II module, which is loading a byte of image data into a shift register and then shifting it out bit by bit for display.

Here is the implementation for the shift register:

wire [2:0] pixel_in_char;
reg [7:0] pixel_shift_reg;
...
assign pixel_in_char = horiz_pos[2:0];
...
always @(posedge clk)
  if (pixel_in_char == 0)
  begin
    case (vert_pos[2:0])
      3'h0 : pixel_shift_reg <= 8'h18;
      3'h1 : pixel_shift_reg <= 8'h3C;
      3'h2 : pixel_shift_reg <= 8'h66;
      3'h3 : pixel_shift_reg <= 8'h7E;
      3'h4 : pixel_shift_reg <= 8'h66;
      3'h5 : pixel_shift_reg <= 8'h66;
      3'h6 : pixel_shift_reg <= 8'h66;
      3'h7 : pixel_shift_reg <= 8'h00;
    endcase
  end    
  else
    pixel_shift_reg <= {pixel_shift_reg[6:0], 1'b0};   
...

We basically break up the visible area in 8x8 cells. When we are at the first pixel of a cell (e.g. bits 2-0 of horiz_pos == 0) we load pixel_shift_reg with the byte value for te applicable row. For the remaining pixels, we just keep shifting out till we get to a new 8x8 cell.

So, if we are within the visible area of the screen pixel_shift_reg[7] will tell us if the current pixel at hand should be on or off.

Next thing we should do is to map an on/off pixel to a color. Before we can this, we should first find out how color signals work in VGA.

To convey color information, a VGA connector provides three analogue pins. There is a separate pin for Red, Green and Blue.

An FPGA can only output zeros and ones on its output pins, an ADC (Analogue to Digital Converter) is required to interface with the color pins on the VGA connector.

Luckily the designers of the Zybo board have taken care of this for us by, apart of the onboard VGA connector, also providing a simple ADC between the FPGA and the VGA connector. One can see a diagram of the setup in the Technical Reference Manual Of the Zybo:

You provide color sample values in 16-bit binary numbers having the RGB-565 format. On the Zybo Board itself there is 3 resister ladder networks, for converting each color channel to an anlague representation. If you want to read a bit more on ADC using resister ladders, you can read the following on Wikipedia:

https://en.wikipedia.org/wiki/Resistor_ladder#R–2R_resistor_ladder_network_(digital_to_analog_conversion)

It is quite an interesting subject!

Back to our FPGA design. For now, we will just output Black if the pixel is off and White if it is on. This translates to the following:

module vga(
  input clk,
  output vert_sync,
  output horiz_sync,
  output [4:0] red,
  output [5:0] green,
  output [4:0] blue
 
    );
...
wire [15:0] out_pixel;
...
assign red = out_pixel[15:11];
assign green = out_pixel[10:5];
assign blue = out_pixel[4:0];
...
assign out_pixel = (vert_pos < VERT_RES) & (horiz_pos < HORIZ_RES) ? (pixel_shift_reg[7] ? 16'hffff : 0) : 0;
...

We only output a value for out_pixel from our shift register if we are within the visible region, otherwise we just output a black pixel.

This concludes our VGA output module.

Wiring everything up

With the VGA output module we need to create an instance of this module and wire up all the ports.

We do this by first wrapping this module into an IP Block, which we covered in a previous post.

We then create a new block design. In this Block Design we will start by droping an instance of our VGA block.

We will also need to invoke the Clock Wizard to create a Block for generating a 85.875MHz clock signal, which will be our pixel clock. We will link up this singal to the clk port of our VGA block.

As usual for our Zybo designs, we also need to add a ZYNQ processing block with relevant supporting blocks to our block design.

Up to this point the block design will look something like the following:


What still needs to be done is to connect the output ports of our VGA block to the pins of the FPGA that leads to the VGA connector.

We need to create a constraint file for doing the pin assignments. There is a constraints file available on GITHUB for the pin assignments of the Zybo Board. From this file just copy out the pin definitions for the VGA related pins which will yield more or less the following:

#VGA Connector
#IO_L7P_T1_AD2P_35
set_property PACKAGE_PIN M19 [get_ports {vga_r[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_r[0]}]

#IO_L9N_T1_DQS_AD3N_35
set_property PACKAGE_PIN L20 [get_ports {vga_r[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_r[1]}]

#IO_L17P_T2_AD5P_35
set_property PACKAGE_PIN J20 [get_ports {vga_r[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_r[2]}]

#IO_L18N_T2_AD13N_35
set_property PACKAGE_PIN G20 [get_ports {vga_r[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_r[3]}]

#IO_L15P_T2_DQS_AD12P_35
set_property PACKAGE_PIN F19 [get_ports {vga_r[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_r[4]}]

#IO_L14N_T2_AD4N_SRCC_35
set_property PACKAGE_PIN H18 [get_ports {vga_g[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_g[0]}]

#IO_L14P_T2_SRCC_34
set_property PACKAGE_PIN N20 [get_ports {vga_g[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_g[1]}]

#IO_L9P_T1_DQS_AD3P_35
set_property PACKAGE_PIN L19 [get_ports {vga_g[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_g[2]}]

#IO_L10N_T1_AD11N_35
set_property PACKAGE_PIN J19 [get_ports {vga_g[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_g[3]}]

#IO_L17N_T2_AD5N_35
set_property PACKAGE_PIN H20 [get_ports {vga_g[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_g[4]}]

#IO_L15N_T2_DQS_AD12N_35
set_property PACKAGE_PIN F20 [get_ports {vga_g[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_g[5]}]

#IO_L14N_T2_SRCC_34
set_property PACKAGE_PIN P20 [get_ports {vga_b[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_b[0]}]

#IO_L7N_T1_AD2N_35
set_property PACKAGE_PIN M20 [get_ports {vga_b[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_b[1]}]

#IO_L10P_T1_AD11P_35
set_property PACKAGE_PIN K19 [get_ports {vga_b[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_b[2]}]

#IO_L14P_T2_AD4P_SRCC_35
set_property PACKAGE_PIN J18 [get_ports {vga_b[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_b[3]}]

#IO_L18P_T2_AD13P_35
set_property PACKAGE_PIN G19 [get_ports {vga_b[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vga_b[4]}]

#IO_L13N_T2_MRCC_34
set_property PACKAGE_PIN P19 [get_ports vga_hs]
set_property IOSTANDARD LVCMOS33 [get_ports vga_hs]

#IO_0_34
set_property PACKAGE_PIN R19 [get_ports vga_vs]
set_property IOSTANDARD LVCMOS33 [get_ports vga_vs]

You will notice that each pin of a vector like vga_r, vga_g, vga_b is specified separately.

We still need to add the actual pins to out block design. So, right your block design and select Create Port. Complete the popup box as follows:


For the Port name, you should specify the same name specified  following get_ports in the constraints file. You will need to create a port for each color channel, called vga_r, vga_g, vga_b.

Remember to specify the correct vector range for each one (e.g. 4..0 for vga_r/vga_b and 5..0 for vga_g).

Luckily you need add only one port per color channel, and not one per pin as performed in the constraints file.

After the color channel ports, you need to create two more ports, vga_hs and vga_vs, which are both single ports.

With all the ports crated, you just need to wire them up to your vga block, yielding the following:


We are are done drawing our block design. We can now continue to Synthesise the design and generating the BitStream file.

Once this finished you can export the Bitstream to a Xilinx SDK Workspace and start design on the FPGA as we did in previous posts.

The End Results

I took a close up of the screen with the FPGA running our VGA module:

The 'A''s are pretty crisp. As mentioned this screen is 1360 pixels wide, so one can fit 170 characters on a line on this screen.

A small disappointment is a small un-utilised part at the top of the screen.  The following photo will give you an idea of the unused part of the screen. (The over-use of the flash light was on purpose. The glare on the Gloss border of the monitor helps to identity the real margins of the screen):


There is also a very small margin on the right hand side.

For now, however, I not too fussed with the margins.

In Summary

In this post we played around with VGA output using the Zybo Board.

In the end we managed to get a screen filled with A's.

In the next post and in coming posts I will start working on functionality for reading back the the frames from SDRAM to our FPGA and then displaying it on the VGA screen.

Till next time!

No comments:

Post a Comment