FPGABee v3.0

PCU Supporting Hardware

Now that I'm confident in the idea of using the NMI to hijack the Microbee's Z80 to perform PCU duties, the next step is to build the supporting hardware that the PCU will need. This includes ability to display an on-screen menu, to take control of the keyboard, a way to configure the disk controller and to read/write the SD card.

PCU Video Controller

In order for the PCU to display an on-screen user-interface it needs a video controller. I decided on a tiny 32x16 character display. It supports the same colour set as a Microbee and is overlaid in-front of the regular Microbee display. Using the video is pretty simple:

  • Character RAM is accessed at 0xF000 to 0xF1ff (512 characters = 32x16 display mode)
  • Colour RAM is access at 0xF200 to 0xF3ff
  • Bit 0 on Port 0x81 turns the display on when set to 1, off when set to 0

One interesting aspect of the PCU's video controller is that setting a character's foreground and background colour to the same value causes the background to appear transparent - allowing the Microbee screen to show through. This can be used to display popups that don't obscure as much of the underlying screen.

The PCU has a custom 128 character font ROM. Each character cell is 8 pixels wide and 12 pixels tall and the character set is based on the standard Microbee small height font, but with some special box draw characters placed in the 0-31 character positions:

PcuFont.png

The font was composed in GIMP and then converted to VHDL with a custom a python script using png.py to read the image data.

I won't go into the details of implementing the PCU video controller as it's basically a cut-down/simplified version of the main Microbee video controller. The only point of interest is how the PCU display is overlaid on the main display. The PCU video controller exposes and extra signal pcu_pixel_visible that indicates if the current pixel being generated is opaque. If so the PCU colours are sent to the monitor, otherwise the Microbee's:

-- Output video
vgaRed <= vgaRed_mbee when pcu_pixel_visible='0' or pcu_display_mode(0)='0' else vgaRed_pcu;
vgaGreen <= vgaGreen_mbee when pcu_pixel_visible='0' or pcu_display_mode(0)='0' else vgaGreen_pcu;
vgaBlue <= vgaBlue_mbee when pcu_pixel_visible='0' or pcu_display_mode(0)='0'else vgaBlue_pcu;

PCU Keyboard Support

Normally keystrokes are sent directly to the Microbee. When the PCU menu is displayed however I need the ability to intercept all keystrokes and have them sent to the PCU instead. This can be controlled by bit 1 on port 0x81:

  • When 0, all keystrokes except F12 are sent to the Microbee.
  • When 1, all keystrokes are sent to the PCU, and the 6545 returns "unpressed" for all keys

To read available keystrokes, the PCU uses port 0x82 and 0x83:

  • Port 0x82 provides the PS2 scan code of the next pressed key
  • Port 0x83 is a status bitmask
    • Bit 0 - 1 if the keystroke is an extended scan code
    • Bit 1 - always 0
    • Bit 2 - 1 if the shift key is pressed
    • Bit 3 - 1 if the ctrl key is pressed
    • Bit 4-6 - always 0
    • Bit 7 = 1 when keystroke is available, 0 when not

Reading port 0x83 also removes the keystroke from the input buffer. So to read the keyboard port, port 0x82 should be read and stored followed by a read of port 0x83 to check if the data at port 0x82 was valid. Note that only key press events are sent to the PCU (ie: no key-up events).

Since the PCU code primarily runs inside the context of a Z80 non-maskable interrupt, I can't raise a second interrupt to indicate another keystroke has arrived. Although I don't expect the PCU to often take a long time to handle the processing of one keystroke, I thought it prudent to put in a small buffer to store any pending pressed keys.

The keyboard input buffer is a simple 7 keystroke FIFO (First-In, First-Out) buffer. When not in PCU mode and when keyboard FIFO indicates data is available the NMI is triggered to switch to PCU mode. The FIFO is implemented as a generic component:

-- Entity Fifo

library ieee;
use ieee.std_logic_1164.ALL;
use ieee.numeric_std.ALL;
use std.textio.all;
use ieee.std_logic_textio.all;

entity Fifo is
    generic
    (
        ADDR_WIDTH : integer;
        DATA_WIDTH : integer := 8
    );
    port
    (
        reset : in std_logic;
        clock_rd : in std_logic;
        clock_wr : in std_logic;
        full : out std_logic;
        available : out std_logic;
        din : in std_logic_vector(DATA_WIDTH-1 downto 0);
        dout : out std_logic_vector(DATA_WIDTH-1 downto 0);
        wr : in std_logic;
        rd : in std_logic
    );
end Fifo;

architecture behavior of Fifo 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);
    signal ram : mem_type;
    signal wrat : std_logic_vector(ADDR_WIDTH-1 downto 0);
    signal rdat : std_logic_vector(ADDR_WIDTH-1 downto 0);
    signal wrat_plus1 : std_logic_vector(ADDR_WIDTH-1 downto 0);
    signal rdat_plus1 : std_logic_vector(ADDR_WIDTH-1 downto 0);
    signal full_internal : std_logic;
    signal empty_internal : std_logic;
begin

    wrat_plus1 <= std_logic_vector(unsigned(wrat)+1);
    rdat_plus1 <= std_logic_vector(unsigned(rdat)+1);
    empty_internal <= '1' when wrat=rdat else '0';
    full_internal <= '1' when wrat_plus1=rdat else '0';
    available <= NOT empty_internal;
    full <= full_internal;

    dout <= ram(to_integer(unsigned(rdat)));

    process (clock_wr, reset)
    begin
        if reset = '1' then

            wrat <= (others=>'0');

        elsif rising_edge(clock_wr) then

            if wr='1' and full_internal='0' then
                ram(to_integer(unsigned(wrat))) <= din;
                wrat <= wrat_plus1;
            end if;

        end if;
    end process;


    process (clock_rd, reset)
    begin
        if reset = '1' then

            rdat <= (others=>'0');

        elsif rising_edge(clock_rd) then

            if rd='1' and empty_internal='0' then
                rdat <= rdat_plus1;
            end if;

        end if;
    end process;
end;

PCU Disk Controller Support

The PCU needs to be able to read/write raw blocks on the SD card so that it can access the FBDS config and directory blocks.

This functionality is implemented as part of the same component that implements the Microbee disk controller (primarily to simplify sharing access to the SD card). While the Microbee maps ports 0x40-0x47 to the disk controller, when in PCU mode, ports 0xC0 and 0xC7 are also mapped to the disk controller (ie: the same port range but with the high bit set). The interface for reading and writing the SD card is similar, but works with raw block numbers instead of track/head/sector numbers:

  • Port: 0xC0 - data in/out - drains/fills a 512 buffer in the disk controller
  • Port: 0xC1 - write 32-bit block number. 4 writes to this port sets the block number. LSB sent first.
  • Port: 0xC7 - write 0 to start a read, write 1 to initiate a write (actual write occurs after sending 512 bytes to port 0xC0)
  • Port: 0xC7 - read status: where bit 7 - disk controller busy, bit 6 - sd card initializing, bit 0 - error

Finally, the PCU needs a way to tell the disk controller, which disk image to use for which Microbee disk drive - so that the PCU configure the HDD image on startup and can "change disk" on a floppy drive. This is provided by sending a command to port 0xC7 with the highest bit set, in which case:

  • Bits 0-2 indicate the drive number to be configured
  • Bits 3-6 indicate the type of disk (for geometry calculations)
  • The base block number of the disk image is provided through port 0xc1

So for example, set to disk 1, to drive type hd0 (ie: 6) with a base image at block 0x0001234:

; Send the base block number
ld		C,0xC1
out		(C),0x34
out		(C),0x12
out		(C),0x00
out		(C),0x00

; Set the drive image info
ld		A,10110001b		; 1 (set drive cmd) 0110 (hd) 001 (disk 1)
out		(0xC7),A

Again, I'm not going into the details of how the disk controller changes were implemented since it's all very similar to the existing functionality. The only thing I will add is that it's important that the PCU disk operations don't interfere with the Microbee's disk operations since the NMI could be invoked while the Microbee is in the middle of a disk operation.

To support this, a second 512 buffer is used for the PCU's read/write operations and a second set of signals to invoke and respond to the disk controller's command execution are used. In other words, the disk controller can accept two commands at the same time - one from the Microbee and one from the PCU and will simply report busy if currently executing another command.

PCU Hardware Complete

That was a really quick overview of the changes made to FPGABee to support the PCU. I think hardware wise everything is now in-place for the first version of the PCU, and I now need to go and actually write the software.