Part 2 - Video Controller
This article continues on from Part 1 - CPU Environment.
In the previous article on building FPGABee I talked about getting the Z80 CPU up and running. This article focuses on the next step which was getting FPGABee connected to a monitor.
The default Microbee screen resolution is 64 characters by 16 lines with each character 8 pixels wide and 16 tall. Character patterns for the characters 0 to 127 are stored in a character ROM while characters 128 to 255 are read from a block of ram - the PCG RAM - allowing for programming of custom character sets and graphics.
The format for character ROM and PCG RAM is simply one byte for each scan line in the character where one bit represents one pixel - a total of 16 bytes for each character.
The video memory map looks like this:
|0xF000 - 0xF7fff||2K of video ram.|
|0xF800 - 0xFFFF||2K of PCG RAM.|
|0xF000 - 0xFFFF||4K of character ROM.|
You'll notice the character ROM occupies the same address space as the video RAM and bank switching is used to swap between them. By writing a
1 to I/O port
11 the contents of the Character ROM are available for reading (writing still writes to video/PCG RAM). Microbee BASIC uses this to program inverse and underline character sets into the PCG RAM.
FPGABee's implementation was trivial. First listen for writes to port 11 (0x0B):
-- Port 0b (11) is the VDU latch rom -- when 1, mem 0xF000 -> FFFFF comes from charrom -- when 0, mem 0xF000 -> FFFFF is the video/pcg ram if z80_addr(7 downto 0)=x"0b" and port_wr = '1' then latch_rom <= z80_dout(0); end if;
and multiplex the CPU data-in appropriately:
z80_din <= x"00" when (boot_scan = '1') else ram_dout when (ram_rd = '1') else vram_dout when (video_rd = '1' and z80_addr(11)='0' and latch_rom = '0') else pcgram_dout when (video_rd = '1' and z80_addr(11)='1' and latch_rom = '0') else charrom_dout when (video_rd = '1' and latch_rom = '1') else -- etc...
Dual Port Memory
One of the nice features of using the Core Generator for declaring block memory is that you can create "Dual Port" memory blocks which have two sets of address, data and control lines allowing two circuits to access the memory simultaneously (and on different clocks).
By declaring the video RAM, PCG RAM and character ROM as dual port both the CPU and the video circuitry can access them at the same time. This is different from the original Microbee where the CPU and video had to share the same lines and caused black "glitches" or flickering in the display when both needed to access the memory at the same time. It was particularly noticeable when displaying a mostly white screen.
By using dual port memory FPGABee avoids the complexity of sharing a single memory bus and fixed the video glitching at the same time.
Note that the 2K of video RAM and 2K of PCG RAM are implemented as two separate memory cores instead of one consecutive 4K block. This is because the video circuitry needs to access these at the same time and having separate cores simplifies this considerably.
The video controller consists of five main parts:
- Video Memory - as discussed above
- Pixel Clock - a clock that ticks once for each pixel displayed.
- VGA Controller - A component that generates the VGA timing signals and the coordinates of the current pixel.
- A set of registers that store things like cursor position and size. In the Microbee this was handled by a 6545 CRTC chip.
- Video Generation - the circuitry that uses the pixel coordinate from the VGA controller and works out whether that pixel should be lit up or not.
The pixel clock for a 640x480 VGA at 60Hz is 25Mhz. To get this I ran the Clock Core Generator again and added a second output of 25Mhz. This clock is connected to the VGA controller and the B ports of the video RAM, PCG RAM and character ROMs.
For the VGA Controller I used a reference component from the Digilent website. There's nothing too complicated in there - it just needs to maintain counters for the X and Y coordinates and allow time for and generate signals for horizontal and vertical retraces. I wont go into this in any further detail - there are plenty of other articles, explanations and examples on the basics of connecting an a VGA monitor to an FPGA.
The 6545 registers I didn't need to deal with just yet and this will be discussed in the next article.
As for the video generation, all we need to do it figure out if the pixel at the current location should be illuminated or not:
- Convert the current VGA pixel coordinate into Microbee coordinate
- Work out what character the current pixel is in
- Set up the port B video RAM address lines with the address of that character
- Use the returned character and the current scan line number to work out the address in character ROM or PCG ram that holds the correct pixel pattern.
- Put that address on the port B address lines of both the PCG RAM and Character ROM.
- Depending on whether the character is between 0 -> 127 or 128 -> 255, select the scanline pixels returned from either the PCG RAM or Character ROM.
- Use the x-pixel index to pick out the correct pixel from the selected scanline byte.
All of this comes down to something like this:
-- Convert VGA pixel coordinates to Microbee pixel coordinates -- Microbee screen resolution = 512x256 -- VGA screen resolution = 640x480 -- So we center the microbee display in the VGA screen. pixel_x <= std_logic_vector(unsigned(vga_pixel_x) - 64); pixel_y <= std_logic_vector(unsigned(vga_pixel_y) - 112); -- Convert bee pixel coordinates to character position char_x <= pixel_x(9 downto 3); -- /8 pixels per character char_y <= pixel_y(9 downto 4); -- /16 pixels rows per character -- Work out the current character pixel index char_pixel_x <= pixel_x(2 downto 0); char_pixel_y <= pixel_y(3 downto 0); -- Setup the video ram address vram_addr <= char_y(4 downto 0) & char_x(5 downto 0); -- Setup PCG/CharRom look up address pcgram_addr <= vram_din(6 downto 0) & char_pixel_y; charrom_addr <= "0" & vram_din(6 downto 0) & char_pixel_y; -- Select the appropriate pixel data from either charrom or pcg ram by checking the top bit of the character scanline_pixels <= pcgram_din when (vram_din(7)='1') else charrom_din; -- Work out the current pixel value current_pixel <= scanline_pixels(to_integer(unsigned(not char_pixel_x))); -- In the pixel in range? pixel_in_range <= '1' when ( (unsigned(vga_pixel_x) >= 64) and (unsigned(pixel_x) < 512) and (unsigned(vga_pixel_y) >= 112) and (unsigned(pixel_y) < 256)) else '0'; -- Generate the output pixel pixel <= '0' when (video_blank = '1' or pixel_in_range = '0') else current_pixel;
If you're not familiar with VHDL, note that the
<= symbol represents a "continuous assignment" and the code doesn't execute sequentially like program code. These assignments all happen simultaneously and continuously and therefore the order of them is irrelevant except for human readability.
A good way to think of these assignments is like a formula in a spreadsheet cell - they update automatically when a dependent input changes. So as the VGA controller changes the value of
vga_y_pixel, all of the above circuitry "executes".
You'll notice that because all the metrics are a power of 2 (screen width, character width, character height etc...), all the multiply and divide operations reduce to simple bit shifting operations.
Note: there are timing issues with the code above and in fact it doesn't even work as well as the screen shots depict, but there's no point discussing the hack I used to get it working. The topic will be revisited in a later article where I fix it properly.
Finally, I needed to generate the actual outputs to the monitor. The Nexys3 has a 8 bit colour support, 3 bits of green, 3 bits of red an 2 bits of blue. FPGABee is monochrome, but that doesn't mean black and white - it means black and amber - because amber screens are cool. :)
-- Generate pixel colour based on pixel output from VDU -- RGB(111,110,000) = Amber! vgaRed <= "111" when (pixel='1') else "000"; vgaGreen <= "110" when (pixel='1') else "000"; vgaBlue <= "00";
These colour values, along with the horizontal and vertical sync signals are all that's required to control a VGA monitor.
(Normally when talking about colour and computers I use the US spelling "color", but the Microbee was Australian so for these articles I'm using "colour" :)
Basic Testing and Microworld Basic Boot Screen.
To test this, I wrote another little assembly language program to write a text string to video RAM. It took a few goes and some trial and error but I eventually got the message to appear.
Of course at this point I was wondering what would happen if I loaded the Microbee Basic ROM instead of my test program. A quick re-programming of the flash memory and shockingly the Microworld Basic boot screen appeared.
Getting an image to appear was a major success moment, but there was at least one key piece still missing from the video controller - the hardware cursor, which is the topic of the next article.