As a first step in re-targeting FPGABee towards a Premium 128K I thought I'd start with the video controller. In version 1, the video controller was hard wired to 64x16 characters, with each character cell being 8x16 pixels - the default arrangement for a ROM Basic machine.
Since the disk based machines typically boot up in 80x25 character mode, obviously I was going to need to fix this. I knew too that some games and other programs used different screen geometry too.
Screen Geometry Basics
The Microbee's screen geometry is controlled through a set of registers on the 6545 CRTC chip. In particular the following registers needed to be implemented:
- R1 - the screen width in characters.
- R6 - the screen height in characters.
- R9 - the number of scan lines per character, minus 1
Implementing the CPU side of these registers was straight forward and involved simply storing the values written by the CPU.
In version 1, I'd taken a number of short-cuts to get the video controller up and running, including pre-calculating where within the VGA display area the Microbee screen would appear. Since the Microbee's resolution was no longer fixed, this meant I needed to calculate this offset. ie:
- x-offset = (640 - (screenwidth * 8))/2
- y-offset = (480 - (screenheight * charheight))/2
Since they're powers of two, the multiply by 8 and divide by 2 operations can be implemented in logic by simple bit shifts. The
screenheight*charheight is more difficult.
Although the Spartan FPGA chip has a couple of on-board multipliers, it seemed a bit of a waste to use one just for this. Thinking about it, I realized I could do the multiplications as a series of N adds. The pixel clock for the video controller runs at 25Mhz anyway, which means I can easily recalculate the height in the time it takes to draw a few pixels.
Basically the video controller has a piece of logic that continuously works out the vertical resolution through a series of add operations:
-- Calculate vertical resolution = char_height * reg_screen_height_chars char_height <= std_logic_vector(unsigned(reg_char_height_minus_1) + 1); process (pixel_clock, reset) begin if reset='1' then -- omitted elsif rising_edge(pixel_clock) then if iter_rows_left="0000000" then vert_resolution <= accum_vert_resolution; -- resolution calculated iter_rows_left <= reg_screen_height_chars; accum_vert_resolution <= (others => '0'); else accum_vert_resolution <= std_logic_vector(unsigned(accum_vert_resolution) + unsigned(char_height)); iter_rows_left <= std_logic_vector(unsigned(iter_rows_left) - 1); end if; end if; end process;
Once I had the horizontal and vertical resolution calculating the screen offset was trivial.
Two Pixels Missing
The video controller requires two memory access cycles to bring together all the information it needs to generate a single pixel:
- The first memory cycle setups the address lines to read the character RAM
- The second memory cycle uses the retrieved character and current character scan line number to setup the address lines for the PCG and Character ROM.
- The third memory cycle generates the pixel color.
(Note these operations are "pipelined" - while the second memory cycle for one pixel is happening, the first memory cycle for the next pixel is happening at the same time)
In version 1, I cheated a little and simply generated the whole screen two pixels late, or in other words shifted two pixels to the right. This worked fine for a 64 character wide display. 64 characters * 8 pixels per cell = 512 pixels - well within the 640 pixel width of the VGA display.
Now however I have to support 80 characters * 8 pixels per cell = 640 pixels. In other words, two pixels would be off the right hand side of the display.
The fix for this was to rewrite most of the pixel coordinate calculation logic to make the memory requests ahead of time, instead of generating the pixel signals later in time. I used to work with a "leading" and "trailing" coordinate. Now I use the terms "current" for the pixel currently being generated, and "upcoming" for the coordinate of the upcoming pixel that we need to start the memory requests for now.
The upcoming coordinate is simply the current coordinate plus 2, except when at the very far RHS of the scan line when we're about about to start generating pixels 0 and 1 - in which case we need to do some wrapping.
current_x_coord <= std_logic_vector(unsigned(vga_pixel_x) - unsigned(blank_pixels_at_left)); process (current_x_coord) begin if unsigned(current_x_coord)=798 then -- (RHS-2) upcoming_x_coord <= "00000000001"; elsif unsigned(current_x_coord)=799 then -- (RHS-1) upcoming_x_coord <= "00000000001"; else upcoming_x_coord <= std_logic_vector(unsigned(current_x_coord)+2); end if; end process;
(Note that for 640x480 VGA pixel coordinates go from 0 to 799 by the time you include the front and back porch parts of the signal timing)
Base Memory Address
In FPGABee version 1 I didn't bother implementing registers R12 and R13 which are used to set the base address - the address in video memory of the first character to be displayed. These registers can be used for scrolling or to support flipping between multiple screens of data.
In reality I don't know of any Microbee programs that actually use this (except see below), but decided to implement it anyway. I won't go into the details as it was pretty simple - store the register values when sent from the CPU and add it to calculated video RAM addresses when accessing the character RAM.
Small Character Set
The Microbee character ROM contains two character sets - 8x16 characters in the first 2K and a 8x11 characters in the second 2K. The video controller determines which character set to use from the base address (hence my real motivation for implementing those registers).
Since the Microbee supports a maximum of 8K of character RAM, bits 13, 14 and 15 in any video memory address are ignored. Bit 13 (ie: 0x2000) is re-purposed as a flag to switch between the two character sets. Very clever.
So when setting up the address for the CharRom, FPGABee injects this bit to get the second character set when requested:
charrom_addr <= reg_base_addr(13) & char_ram_dout(6 downto 0) & current_ypos_in_char(3 downto 0);
That covers the changes required for the screen geometry support, next I'll be adding support for colour and testing it.