Colour and Extended PCG Graphics
Microbee Colour Support
The Microbee supports colour through a block of RAM that stores one byte for each character. A colour is defined by 4 bits where the intensity bit increases the value of all three colour channels.
- Bit 0 - Foreground Red component
- Bit 1 - Foreground Green component
- Bit 2 - Foreground Blue component
- Bit 3 - Foreground Intensity mode
- Bit 4 - Background Red component
- Bit 5 - Background Green component
- Bit 6 - Background Blue component
- Bit 7 - Background Intensity mode
Colour RAM is accessed at 0xF800 - the same location as the PCG RAM, but is bank switched via port 8. When bit 6 of of port 8 is 1, 0xF800 to 0xFFFF maps to the colour RAM. When it's zero, that area of RAM accesses the PCG RAM.
Added Colour RAM
In the FPGABee version 1, I was using the Xilinx Core Generator to generate block RAMs. Since then I've learned that the tool-chain can "infer" block RAMs from VHDL code the conforms to a certain layout. As such, I've put together a couple of simple generic entities that make instantiating different block RAMs easy.
Here's my implementation of a parameterized dual port block RAM where one port is read/write and one is read only:
-- Entity RamDualPort library ieee; use ieee.std_logic_1164.ALL; use ieee.numeric_std.ALL; use std.textio.all; use ieee.std_logic_textio.all; entity RamDualPort is generic ( ADDR_WIDTH : integer; DATA_WIDTH : integer := 8 ); port ( -- Port A clock_a : in std_logic; addr_a : in std_logic_vector(ADDR_WIDTH-1 downto 0); din_a : in std_logic_vector(DATA_WIDTH-1 downto 0); dout_a : out std_logic_vector(DATA_WIDTH-1 downto 0); wr_a : in std_logic; -- Port B clock_b : in std_logic; addr_b : in std_logic_vector(ADDR_WIDTH-1 downto 0); dout_b : out std_logic_vector(DATA_WIDTH-1 downto 0) ); end RamDualPort; architecture behavior of RamDualPort is constant MEM_DEPTH : integer := 2**ADDR_WIDTH; type mem_type is array(0 to MEM_DEPTH) of std_logic_vector(DATA_WIDTH-1 downto 0); shared variable ram : mem_type; begin process (clock_a) begin if rising_edge(clock_a) then if wr_a = '1' then ram(to_integer(unsigned(addr_a))) := din_a; end if; dout_a <= ram(to_integer(unsigned(addr_a))); end if; end process; process (clock_b) begin if rising_edge(clock_b) then dout_b <= ram(to_integer(unsigned(addr_b))); end if; end process; end;
Now instead of having to run the slow Core Generator, I can create block RAMs directly in code like this:
-- 2k Color RAM at 0xF800 - 0xFFFF color_ram : entity work.RamDualPort GENERIC MAP ( ADDR_WIDTH => 11 ) PORT MAP ( -- port A for CPU read/write clock_a => z80_clock, wr_a => color_ram_wea, addr_a => translated_address(10 downto 0), din_a => z80_dout, dout_a => color_ram_dout, -- port B for 6545, read-only clock_b => pixel_clock, addr_b => char_ram_addr_b, dout_b => color_ram_dout_b );
Other than that, setting up the colour RAM was identical to how the other memory components are configured.
Supporting Colour in the Video Controller
With the colour RAM in place, the video controller just needed to be updated to use it. Firstly, the colour RAM is retrieved at the same time as the character, and uses the same address. Unlike the retrieved character that is used in the next clock cycle, the colour RAM isn't required until the cycle after next, so we need to delay it by one clock - similarly to how the PCG flag is delayed by 1 clock:
process (pixel_clock) begin if (rising_edge(pixel_clock)) then pcg_select <= char_ram_dout(7); color_delayed <= color_ram_dout; -- delay colour info by one clock cycle end if; end process;
Next, it needed to pick out the appropriate fore/back colour nibble depending on whether the pixel should be "lit" or not:
-- Select the appropriate half of the color byte color_nibble <= color_delayed(3 downto 0) when (pixel='1') else color_delayed(7 downto 4);
And, finally generate the appropriate colour:
-- Translate color vgaRed <= color_nibble(0) & color_nibble(3) & "0"; vgaGreen <= color_nibble(1) & color_nibble(3) & "0"; vgaBlue <= color_nibble(2) & color_nibble(3);
(Note that I'm only using the two most significant colour bits of each colour component - the Nexys only supports 3-3-2 colour depth).
Extended PCG RAM and Attribute RAM
The Premium model Microbee's also supported extended PCG RAM with most being fitted with 16K (as opposed to 2K) and some up to 32K.
The PCG RAM was accessed via a set of 2K banks, and the bank to be used for a particular character is selected through another block of attribute RAM where the bottom 4 bits specify the bank. The attribute RAM is located at 0xF000 (just like the character RAM) and is bank switched via bit 4 of port 0x1C.
Furthermore, bit 7 of port 0x1C controls whether the extended PCG graphics are enabled at all - so that older software would continue to run without modification.
From the CPU side this all boiled down to more logic to decide which block RAMs should be used for a particular memory access.
From the video controller side of things it meant an extra memory look-up into the attribute RAM (in the same memory cycle as the character and colour RAM), followed by selecting the appropriate addressing for the PCG RAM based on whether the extended graphics are enabled and which bank is selected by the attribute RAM.
After implementing all of the above I was ready to start testing it, but not ready to boot up the OS, so I wrote a simple "boot ROM" program to put the video controller through it's paces. It took a little debugging, but soon had it working. Here are some screen shots of a few of the different modes.
The numbers at the bottom show the screen width in characters, screen height in rows and character height in scan lines.
The little numbers 1 through 7 at the top of the second photo are each from a different PCG bank confirming the attribute and PCG RAM is working. The tests also flipped extended graphics on/off to confirm they reverted to bank 0 (as in the first photo).
Video Controller Complete
So that's it for the video controller geometry and colour support improvements. Next I'll be looking at the Premium's main memory bank switching.