Using the raw keyboard mode under Linux

by Karsten Scheibler

If you read something from STDIN, the read bytes are normally in ASCII format. In some cases it is useful to get the keyboard input as raw as possible. Imagine: Your program should react on the Alt or Ctrl Key with menu highlightning, or you use the fb device (or run Mode X with direct VGA register access) and you want to surpress terminal switching and Shift+PageUp/Shift+PageDown. Locking the terminal may be also done with the following code (already known from the fb article):


	%assign VT_GETMODE	05601h
	%assign VT_SETMODE	05602h
	%assign VT_PROCESS	1

	section .text
				struc tvtmode
				alignb 4
	tvtmode_mode:		resb	1
	tvtmode_waitv:		resb	1
	tvtmode_relsig:		resb	1
	tvtmode_acqsig:		resb	1
	tvtmode_frsig:		resb	1
				endstruc

				mov	dword eax, SYS_IOCTL
				mov	dword ebx, STDIN
				mov	dword ecx, VT_GETMODE
				mov	dword edx, vtmode_data
				int	byte  080h
				mov	byte  [vtmode_data + tvtmode_mode], VT_PROCESS
				mov	dword eax, SYS_IOCTL
				mov	dword ebx, STDIN
				mov	dword ecx, VT_SETMODE
				mov	dword edx, vtmode_data
				int	byte  080h

	section .data
	vtmode_data:		istruc	tvtmode
				iend

	
 

But this doesn't solves the problem how to catch a single Alt (Ctrl, Shift or whatever) keypress and to surpress Shift+PageUp/Shift+PageDown. In raw keyboard mode such things are possible. In this mode you get directly the untranslated keycodes coming from your keyboard. It doesn't generate ASCII like codes it is far away from that. If you press a key on your keyboard two codes are send to the controller in your PC a so called make code if you press a key and a break code if you release a key. The most generated codes are 1 byte codes. It is easy to recognize which sort of code you have received: bit 7 clear = make, bit 7 set = break.

Examples:

But there are also codes longer than 1 byte, such codes are escaped with a leading 0e0h. Example: RIGHT CTRL: 0e0h 01dh (make), 0e0h 09eh (break). Three byte codes are escaped with 0e1h (there should be only one such key on your keyboard: PAUSE. This key is a little bit strange because it generates make & break codes without any delay if you press it).

The example code below will simply dump the hex code of every key to the terminal. Use ESC to exit the program.

The translation to ASCII Codes in the kernel is done via tables (the same tables are changed if you run the command loadkeys, which loads the correct keytable for your country). You can reach this tables with the ioctl's KDGKBENT and KDSKBENT. They expect a pointer to a structure of the type kbentry (see /usr/src/linux/include/linux/kd.h). If you want to get an entry you have to set the values kb_table and kb_index, if you want to set an entry kb_value must also be set. kb_index is the raw keycode, kb_table contains bits for the keys Shift, Alt, AltGr and Ctrl (it is some kind of table selector, for making things like Shift+A easier). kb_value contains the ASCII Code. For simple keyhandling this should be enough, the real key translation in the kernel seems to be a little bit more complex (think of escape sequences if you press a cursor key), in such cases kb_value contains some magic values.

The raw keyboard mode only works on local console. With the ioctl TIOCLINUX i check if the program was started on local console. Maybe this is not very clean, but it does what it should. Using the VT_GETMODE ioctl does also the trick (see the source above for ioctl number and vtmode struct):


			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, VT_GETMODE
			mov	dword edx, vtmode_data
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error	;not on local console

	
 

Like all low level accesses you should try as hard as you can to restore the original state, because if you fail the keyboard stays in raw mode and this means the local console is unusable (Ctrl+Alt+Del doesn't work, no terminal switching etc.). Therefore you should catch all signals, which can terminate your program (including SIGSEGV) with an exit handler.

Note:
Opening /dev/tty is not really needed, it is more or less cosmetic.
Hint:
The ioctl KDGKBMODE expects like the most ioctl's an address as third argument, but KDSKBMODE expects directly the mode (K_RAW for the raw keyboard mode K_XLATE for the normal mode). There is also a K_MEDIUMRAW available but i let it up to you to figure out the differences between K_RAW and K_MEDIUMRAW ;-).

short procedure for switching to raw keyboard mode:

  1. use the TIOCLINUX or VT_GETMODE ioctl's to determine if you your program was called from local console.
  2. save the current terminal state (ioctl's TCGETS and KDGKBMODE and friends)
  3. set signal handlers
  4. set the terminal values:
    1. switch off input flags: ISTRIP, INLCR, ICRNL, IGNCR, IXON, IXOFF.
    2. switch off local flags: ECHO, ICANON, ISIG.
    3. Disable kernel keyboard translation.

If you find mistakes or have suggestions, mail me: karsten.scheibler@bigfoot.de


-[sample Makefile for this code]---------------------------------------------
NASM=nasm -f elf -w+orphan-labels
STRIP=strip -R .note -R .comment
LD=ld -s
RM=rm -f

.PHONY: all  clean

all: rawkb

rawkb: rawkb.nasm
	${NASM} rawkb.nasm
	${LD} -e rawkb_start -o rawkb rawkb.o
	${STRIP} rawkb

clean:
	${RM} *.bak *~ rawkb rawkb.o core
-----------------------------------------------------------------------------


;****************************************************************************
;****************************************************************************
;*
;* USING LINUX RAW KEYBOARD MODE
;*
;* written by Karsten Scheibler, 2000-SEP-15
;*
;****************************************************************************
;****************************************************************************





;you have to specify this as entry point to the linker, as done above in the
;Makefile

global rawkb_start


;****************************************************************************
;* some assign's ************************************************************
;****************************************************************************
%assign SYS_EXIT		1
%assign SYS_READ		3
%assign SYS_WRITE		4
%assign SYS_OPEN		5
%assign SYS_CLOSE		6
%assign SYS_IOCTL		54

%assign TCGETS			05401h
%assign TCSETSW			05403h
%assign TIOCLINUX		0541ch
%assign KDGKBMODE		04b44h
%assign KDSKBMODE		04b45h
%assign K_RAW			000h

%assign ISTRIP			000400q
%assign INLCR			000100q
%assign IGNCR			000200q
%assign ICRNL			000400q
%assign IXON			002000q
%assign IXOFF			010000q

%assign ISIG			1
%assign ICANON			2
%assign ECHO			8

%assign STDIN			0
%assign STDOUT			1

%assign O_RDWR			000002q

%assign NOT32			0ffffffffh

%assign STATE_OPEN		001h
%assign STATE_TERMIOS_SAVED	002h
%assign STATE_KBMODE_SAVED	004h

				struc	ttermios
				alignb	4
termios_input_flags:		resd	1
termios_output_flags:		resd	1
termios_control_flags:		resd	1
termios_local_flags:		resd	1
termios_line_discipline:	resb	1
termios_control_characters:	resb	64
				endstruc

				struc	tkbentry
				alignb	4
kbentry_table:			resb	1
kbentry_index:			resb	1
kbentry_value:			resw	1
				endstruc



;****************************************************************************
;* rawkb_start **************************************************************
;****************************************************************************
section .text
rawkb_start:

			;try to open /dev/tty, if this fails use STDIN for
			;all the following operations

			mov	dword eax, SYS_OPEN
			mov	dword ebx, .tty_path
			mov	dword ecx, O_RDWR
			int	byte  080h
			test	dword eax, eax
			js	.no_handle
			mov	dword [tty_handle], eax
			or	dword [tty_state], STATE_OPEN
.no_handle:

			;check if we are on local console

			push	dword 12

			mov	dword eax, SYS_IOCTL
			mov	dword ebx, STDIN
			mov	dword ecx, TIOCLINUX
			mov	dword edx, esp
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error
			mov	dword eax, SYS_IOCTL
			mov	dword ebx, STDOUT
			mov	dword ecx, TIOCLINUX
			mov	dword edx, esp
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error
			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, TIOCLINUX
			mov	dword edx, esp
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error

			pop	dword eax

			;save terminal state

			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, TCGETS
			mov	dword edx, tty_termios_saved
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error
			or	dword [tty_state], STATE_TERMIOS_SAVED

			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, KDGKBMODE
			mov	dword edx, tty_kbmode_saved
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error
			or	dword [tty_state], STATE_KBMODE_SAVED

			;set terminal values

			cld
			mov	dword ecx, ttermios_size
			mov	dword esi, tty_termios_saved
			mov	dword edi, tty_termios
			rep movsb

			and	dword [tty_termios + termios_input_flags], (NOT32 - (ISTRIP | INLCR | ICRNL | IGNCR | IXON | IXOFF))
			and	dword [tty_termios + termios_local_flags], (NOT32 - (ECHO | ICANON | ISIG))

			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, TCSETSW
			mov	dword edx, tty_termios
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error

			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, KDSKBMODE
			mov	dword edx, K_RAW
			int	byte  080h
			test	dword eax, eax
			js	near  rawkb_error

			;display the hex codes of all pressed keys, ESC exits

.display:		mov	dword eax, SYS_READ
			mov	dword ebx, STDIN
			mov	dword ecx, character
			mov	dword edx, 1
			int	byte  080h
			xor	dword ebx, ebx
			xor	dword ecx, ecx
			mov	byte  bl, [character]
			mov	byte  cl, bl
			cmp	byte  bl, 001h
			je	near  rawkb_end
			shr	dword ecx, 4
			and	byte  bl, 00fh
			mov	dword eax, 00a680000h
			mov	byte  ah, [.hex_table + ebx]
			mov	byte  al, [.hex_table + ecx]
			mov	dword [hex_number], eax
			mov	dword eax, SYS_WRITE
			mov	dword ebx, STDOUT
			mov	dword ecx, hex_number
			mov	dword edx, 4
			int	byte  080h
			jmp	.display

.tty_path:		db	"/dev/tty", 0
.hex_table:		db	"0123456789abcdef"

section .data
			align	4
tty_handle:		dd	STDIN
tty_state:		dd	0

section .bss
			align	4
tty_termios_saved:	resb	ttermios_size
tty_kbmode_saved:	resd	1
tty_termios:		resb	ttermios_size
hex_number:		resd	1
character		resb	1



;****************************************************************************
;* rawkb_restore ************************************************************
;****************************************************************************
section .text
rawkb_restore:
			test	dword [tty_state], STATE_TERMIOS_SAVED
			jz	.no_termios
			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, TCSETSW
			mov	dword edx, tty_termios_saved
			int	byte  080h
.no_termios:
			test	dword [tty_state], STATE_KBMODE_SAVED
			jz	.no_kbmode
			mov	dword eax, SYS_IOCTL
			mov	dword ebx, [tty_handle]
			mov	dword ecx, KDSKBMODE
			mov	dword edx, [tty_kbmode_saved]
			int	byte  080h
.no_kbmode:
			test	dword [tty_state], STATE_OPEN
			jz	.not_open
			mov	dword eax, SYS_CLOSE
			mov	dword ebx, [tty_handle]
			int	byte  080h
.not_open:		ret



;****************************************************************************
;* rawkb_exit ***************************************************************
;****************************************************************************
section .text
rawkb_end:
			call	rawkb_restore
			xor	dword eax, eax
			mov	dword ebx, eax
			inc	dword eax
			int	byte  080h



;****************************************************************************
;* rawkb_error **************************************************************
;****************************************************************************
section .text
rawkb_error:
			call	rawkb_restore
			xor	dword eax, eax
			inc	dword eax
			mov	dword ebx, eax
			int	byte  080h
;********************************************* karsten.scheibler@bigfoot.de *