Part 4 - Keyboard
This article continues on from Part 3 - Implementing the 6545 CRTC Chip and Cursor.
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:
- 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.
- 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
-- 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
-- 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';
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
-- 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!