FPGABee v3.0

Keyboard

In the last post I got a perfect Microbee boot screen working and realized that keyboard support was the only this left to make FPGABee at least partly usable.

The Microbee Keyboard Interface

The Microbee keyboard consists of approximately 64 key switches which are scanned by the 6545 CRTC chip and reported back to the CPU in one of two ways:

  1. The 6545 continuously scans the keyboard and when it finds a pressed key, loads its key switch number into the light-pen register where it is stored until read by the CPU.
  2. Alternatively the CPU can ask the 6545 to check if a key is pressed by loading a key switch number into the 6545's update address register and polling until the the update ready bit is set. The light-pen ready bit indicates if the key is pressed.

(Not implementing the update ready bit was the cause the boot screen infinite loop I discussed in a Part 3).

The PS2 Keyboard Interface

A PC keyboard works by sending scan codes of keys as they're pressed and released. The API is pretty simple and I won't repeat it as there are detailed explanations elsewhere.

The first part of implementing FPGABee's keyboard support was to figure out how to get keyboard data from the PS2 interface. In the end I found a component ps2interface (as part of Digilent's PS2 Mouse Reference Design) that handles reading the PS2 serial interface and decoding it into a series of bytes.

Once I could get data from the keyboard, the next step was to remember the pressed/released state of each key. A simple state machine watches the incoming data and tracks the make/break state of the message, whether the key was an extended key code or not and updates one bit in the keys register.

-- Handle incoming byte from the keyboard
if (rx_data = x"f0") then

    keyb_state_break <= '1';

elsif (rx_data = x"e0") then

    keyb_state_extended <= '1';

else

    if (keyb_state_break='1') then
        keys(to_integer(unsigned(mb_code))) <= '0';
    else
        keys(to_integer(unsigned(mb_code))) <= '1';
    end if;

    keyb_state_break <= '0';
    keyb_state_extended <= '0';

end if;

Scan Code to Key Switch Mapper

In the above code example, notice the reference to mb_code? That's a decoded version of the current scan code that for most keys is the Microbee key-switch number. This mapping is done by a separate module ScanCodeMapper:

-- Scan code mapper maps to "almost" Microbee scan code
ScanCodeMapper: entity work.ScanCodeMapper PORT MAP
(
    scancode => rx_data,
    extended => keyb_state_extended,
    mb_code => mb_code
);

Essentially all this is doing is looking at the current scan code/extended key flag and producing a key-switch number. It's just a big switch statement:

case (scancode) is

    -- A to Z
    when x"1c" => mb_code <= to_unsigned(mbk_A, 8);
    when x"32" => mb_code <= to_unsigned(mbk_B, 8);
    when x"21" => mb_code <= to_unsigned(mbk_C, 8);
    when x"23" => mb_code <= to_unsigned(mbk_D, 8);
    -- etc..

The ScanCode mapper can't do an exact one-to-one mapping because a PC's keys don't map exactly onto Microbee keys. For example, the @ symbol on a PC is Shift+2, where-as on a Microbee it has it's own key... but then Shift+@ on a Microbee gives a backtick character, which on a PC is it's own key without shift.

To fix this I store the pressed state of all the keys I need and then "synthesize" the final state through combinational logic. Most keys are one to one:

-- A-Z
MicrobeeSwitches(mbk_A to mbk_Z) <= keys(mbk_A to mbk_Z);

But others are more complex, here's the @/backtick key decoder:

-- @`
MicrobeeSwitches(mbk_at_backtick) <= 
		'1' when keys(mbk_2) = '1' and shift = '1' else				-- shift+2
		'1' when keys(psk_backtick) = '1' and shift = '0' else   -- `
		'0';

(Note, the mbk_ constants refer to actual MicroBee key codes, whereas the psk_ constants are the PSeudo Keys generated by the ScanCodeMapper that don't map to a Microbee key)

The shift key is the most complex because it needs to be faked on/off depending on other keys that we're simulating:

-- Shift
shift <= keys(psk_shift_l) or keys(psk_shift_r);
MicrobeeSwitches(mbk_shift) <=
		'0' when keys(mbk_2) = '1' and shift = '1' else				-- turn off when shift+2 = @
		'1' when keys(psk_backtick) = '1' and shift = '0' else		-- turn on when '
		'0' when keys(mbk_6) = '1' and shift = '1' else				-- turn off when shift+6 = ^
		'0' when keys(psk_semicolon) = '1' and shift = '1' else		-- turn off when shift+; = :
		'1' when keys(psk_equals) = '1' and shift = '0' else		-- turn on when =
		'1' when keys(psk_quote) = '1' and shift = '0' else    		-- turn on when shift 7 = '
		shift;

All of this key mapping logic lives in it's own module MicrobeeKeyboardDecoder.

-- Receives PS2 keyboard data and decodes it into a set of on/off states for  
-- each Microbee key.
entity MicrobeeKeyboardDecoder is  Port 
( 
    clock : in STD_LOGIC;
    reset : in STD_LOGIC;
    PS2KeyboardData : inout STD_LOGIC;
    PS2KeyboardClk : inout STD_LOGIC;
    MicrobeeSwitches : out STD_LOGIC_VECTOR (0 to 63);
);
end MicrobeeKeyboardDecoder;

MicrobeeKeyboardDecoder was developed and tested in a separate project with a test bench that displayed the key-switch numbers and shift + ctrl key states on the Nexys' 7 segment display.

Updates to the 6545

As mentioned previously, the 6545 is responsible for the keyboard to CPU interface in a Microbee. The first part of this is to simply store the key switch number of any pressed key in the light pen register. I added an instance of MicrobeeKeyboardDecoder to the CRTC module, passed through the appropriate PS2 keyboard signals and then scanned the keyboard, one key on each CPU clock tick:

-- Keyboard scan
-- When a pressed key is found, store in the light pen address register
--    and set the light pen ready flag
key_scan_addr <= key_scan_addr + 1;
if ((reg_status_light_pen_ready = '0') and (latch_rom = '0') and
        (MicrobeeSwitches(to_integer(key_scan_addr))='1') ) then

    reg_status_light_pen_ready <= '1';
    reg_light_pen(9 downto 4) <= std_logic_vector(key_scan_addr);

end if;

Now by reading the 6545 status register and checking the light pen ready bit the CPU can tell if there's a keystroke ready and if so read it from the light pen register. The light-pen register holds that value until it's read at which point it's cleared ready to report another pressed key.

The reference to latch_rom is odd, but is related to the other way the CPU can ask if a specific key is pressed. To do this, the CPU loads the key switch number into the update address registers R18 and R19 and then writes any dummy value to R31 - which triggers the scan. The 6545 will then check for that key to be pressed and once scanned sets the update ready bit in the status register. While it's doing all this the regular keyboard scan needs to be disabled. The Microbee re-purposes the latch_rom bit on port 11 (primarily used to control access to the character ROM), to also disable the regular keyboard scan.

Aside from the above check for latch_rom all of this is handled in the logic that handles writes to register R31:

when "11111" =>
    -- write the R31 - scan keyboard
    if (MicrobeeSwitches(to_integer(unsigned(reg_update_addr(9 downto 4))))='1' and  
            reg_status_light_pen_ready='0') then

        reg_light_pen <= reg_update_addr;
        reg_status_light_pen_ready <= '1';

    end if;

The rest of the changes for the keyboard were simply implementing the light-pen (R16/R17) and update address (R18/R19) registers in the 6545.

A Working Microbee

It took a bit of debugging (I wasn't aware of the latch_rom stuff at first) but I eventually got the keyboard working. I could type some simple BASIC programs and they ran - in other words a somewhat functional Microbee and another major success moment!