A closer look at the OSD/Control Module

Part 4 – Keyboard control

So far we have the control module printing a message to the screen and autonomously sending signals to the underlying core. Now it’s time to make it somewhat interactive!

To do this we need to add Keyboard support. Many FPGA platforms still have support for PS/2 keyboards simply because they’re electrically simple and don’t require much in the way of high speed transceivers or carefully-routed circuits to drive them. Thus the PS/2 keyboard is a de-facto standard for FPGA projects, even to the point that on the MIST platform (which has no PS/2 ports, but does have USB ports) there is a bridge component available which makes the keyboard masquerade as PS/2. This is taken care of in the MIST-specific toplevel file, which allows the main project files to be identical between platforms.

To add keyboard support we need three things: a hardware interface to the PS/2 socket (whether it’s real or emulated!), software to drive that hardware interface, and support for interrupts, so that the CPU doesn’t have to poll the hardware waiting for keystrokes.

The interrupt controller is very simple and looks like this:

entity interrupt_controller is
generic (
	max_int : integer :=15  -- Specify here how many interrupts should be handled.
);
port (
	clk : in std_logic;
	reset_n : in std_logic; -- active low
	enable : in std_logic :='1'; -- Interrupt enable
	trigger : in std_logic_vector(max_int downto 0) := (others => '0'); -- Unused inputs will be optimised awayby the synthesis tools
	ack : in std_logic;
	int : buffer std_logic; -- 1 if an interrupt is pending
	status : out std_logic_vector(max_int downto 0) -- Bitfield with a set bit for each pending interrupt
);
end entity;

architecture rtl of interrupt_controller is

signal pending : std_logic_vector(max_int+1 downto 0) := (others => '0'); -- highest bit is set if any other bit is set.
begin

process(clk)
begin
	if rising_edge(clk) then

		-- Clear the int bit if the interrupt is acknowledged.
		-- While int is 1, the status is frozen, and new interrupts
		-- are held pending, which prevents them being lost.
		if ack='1' then
			int<='0';
		end if;

		-- If no interrupts are currently signalled
		-- copy any pending interrupts to status.
		-- We clear the pending signal at the same time.
		if int='0' and enable='1' then
			status<=pending(status'high downto 0);
			int<=pending(pending'high);
			pending<=(others => '0');
		end if;

		-- Latch any incoming interrupt pulses in the pending signal
		-- If no interrupts are already pending this will be propagated
		-- on the next clock edge; otherwise it will be stored until
		-- the current interrupt is acknowledged.
		for I in trigger'low to trigger'high loop
			if trigger(I)='1' then
				pending(I)<='1';
				pending(pending'high)<='1';
			end if;
		end loop;
	end if;
end process;

end architecture;

The trigger inputs will detect a momentary high pulse and raise the int signal in response.
The status signal will contain a '1' bit corresponding to the trigger input that caused the interrupt.
The int signal remains high until acknowledged by way of a momentary high pulse on the ack signal. While the int signal is high, new incoming interrupt triggers are held pending and not actioned until the interrupt currently in operation has been acknowledged. It's possible for more than one interrupt to come in while waiting for the ack signal, so be prepared for there to be multiple '1' bits in the status signal.
The ZPU has no concept of interrupt acknowledgement, so we'll have to build something into a hardware register to allow us to acknowledge them.

We instantiate the interrupt controller like so:

intcontroller: entity work.interrupt_controller
generic map (
	max_int => int_max
)
port map (
	clk => clk,
	reset_n => reset_n,
	enable => int_enabled,
	trigger => int_triggers,
	ack => int_ack,
	int => int_req,
	status => int_status
);

int_triggers<=(0=>kbdrecv,
		1=>vblank,
		others => '0');

We need int_status to be readable from the ZPU, and need int_enabled to be writable, so it's time to add new hardware registers.
For no particular reason other than it's what I used in another project, we'll give the interrupt controller location 0xffffffb0, and decode writes to that address like so:

	when X"B0" => -- Interrupts
		int_enabled<=mem_write(0);
		mem_busy<='0';

The low bit of a write determines whether interrupts are enabled or disabled.
Reads from the same register are implemented like this:

	when X"B0" => -- Read from Interrupt status register
		mem_read<=(others=>'X');
		mem_read(int_max downto 0)<=int_status;
		int_ack<='1';
		mem_busy<='0';

Reads from 0xffffffb0 now return which interrupts are currently being acted upon, and reads have the side effect of acknowledging the interrupt. To ensure that int_ack only goes high for one pulse, we assign '0' to it immediately before the case block.

All that remains hardware-wise is to connect the int_req signal to the ZPU's interrupt input.

Then it's a question of handling the software side of things.
When the ZPU receives an interrupt it jumps to an interrupt service routine at location 0x20. The startup code provides this routine, and the default routine in the ZPUFlex's supplied startup code exports a variable called _inthandler_fptr to which the user application can supply a function pointer to be called when an interrupt occurs. The Control Module's firmware contains helper functions in interrupts.[c|h] with the following signatures:

void SetIntHandler(void(*handler)());
void EnableInterrupts();
void DisableInterrupts();
int GetInterrupts();

We now have basic interrupt capabilities in the project, so it's time to add the keyboard controller.

PS/2 is a bidirectional interface, but if all we need to do is monitor keystrokes (as opposed to set repeat delays and toggle Caps Lock lights, etc), we can get away with ignoring the outputs making the keyboard a single-directional interface. (This is not the case for PS/2 mice, the vast majority of which require a wake-up byte to be sent before they'll perform automatic reporting of movement.)

Rather than create a PS/2 interface from scratch, I'm going to use Peter Wendrich's one from the Chameleon64's hardware test project.

We need the following new signals in the CtrlModule:

-- PS/2 related signals
signal kbdrecv : std_logic;
signal kbdrecvreg : std_logic;
signal kbdrecvbyte : std_logic_vector(10 downto 0);

We instantiate the io_ps2_com component like this: (As previously mentioned, we're only receiving here, not bothering with sending, so some of the signals are left unconnected.)

-- PS2 keyboard
mykeyboard : entity work.io_ps2_com
generic map (
	clockFilter => 15,
	ticksPerUsec => sysclk_frequency/10
)
port map (
	clk => clk,
	reset => not reset_n, -- active high!
	ps2_clk_in => ps2k_clk_in,
	ps2_dat_in => ps2k_dat_in,
--	ps2_clk_out => ps2k_clk_out, -- Receive only
--	ps2_dat_out => ps2k_dat_out,
	
	inIdle => open,
	sendTrigger => '0',
	sendByte => (others=>'X'),
	sendBusy => open,
	sendDone => open,
	recvTrigger => kbdrecv,
	recvByte => kbdrecvbyte
);

The PS2 component brings kdbrecv momentarily high when a byte is received from the keyboard. We could feed this directly into the interrupt controller and leave it at that, but instead we'll latch the signal into kbdrecvreg, where it will remain high until the CPU clears it. This aids flexibility by allowing software to poll for PS/2 data if for some reason we don't want to use interrupts.
recvByte is actually 11 bits, not 8, since there is a start bit, a stop bit and a parity bit. The bits of interest are bits 8 downto 1.
We handle reads from the PS/2 register (0xffffffe0) like so:

	when X"E0" =>	-- Read from PS/2 regs
		mem_read<=(others =>'X');
		mem_read(11 downto 0)<=kbdrecvreg & '1' & kbdrecvbyte(10 downto 1);
		kbdrecvreg<='0';
		mem_busy<='0';

(The '1' at bit 10 is a placeholder for a CTS signal, which would be needed if we were implementing writes as well as reads.)

Finally, we perform latching of the kbdrecv signal, after the address decoding block:

	if kbdrecv='1' then
		kbdrecvreg <= '1'; -- remains high until cleared by a read
	end if;

It has to be done after the register handling code to ensure that pulses aren't missed if the CPU happens to clear the register at the same time as a new byte arrives.

Having added the interrupt and keyboard controllers, all that remains is software support. CtrlModule/Firmware contains some support routines to make this easy, including an interrupt handler which copies received PS/2 bytes to a ringbuffer, and a raw keycode handling routine which reads from the ringbuffer and maintains a key state table.
The project as it stands toggles the On-screen display with the F12 key, and switches between test patterns with F1-F4. The inner-loop code to achieve this is as simple as this:

	while(1)
	{
		HandlePS2RawCodes();
		if(TestKey(KEY_F12)&2) // Has the key been pressed since the las
t test (as opposed to being held down)
			osd_enabled^=1;
		OSD_Show(osd_enabled);		

		if(TestKey(KEY_F1))
			dipsw=0;
		if(TestKey(KEY_F2))
			dipsw=1;
		if(TestKey(KEY_F3))
			dipsw=2;
		if(TestKey(KEY_F4))
			dipsw=3;
		HW_HOST(REG_HOST_SW)=dipsw;
	}

Full source is, as always, on github, tagged as Step3.

In addition to the DE1 and MIST project files, this time I've added project files for Emanuel's experimental EMS11-BB3.7 board, which is Spartan-based, so hopefully it will serve as a useful example for both Altera and Xilinx platforms.

Leave a Reply

Your email address will not be published. Required fields are marked *