Search (using Google):  Web Karig

 

23 June 2004

Scan codes, part 2

I tried again to get my primitive keyboard-reading code to work. I've written code that gets the scan code from the keyboard controller and displays both the key's name and whether the key was pressed or released. This code works — most of the time. It prints out "H    MAKE" when you press the H key and "H    BRK" when you let the key up. However, it doesn't work 100% perfectly with keys that generate multiple-byte scan codes, such as PAGE UP.

Set 1 scan codes

Here is some information that I probably should have included in my last entry.

There are two kinds of scan code. When you press a key on the keyboard, the keyboard controller spits out a make code. When you remove your finger from the key, the keyboard controller spits out a break code.

Each key on the keyboard has its own make code and its own break code.

The keyboard can generate scan codes from any of up to three sets. The keyboard controller, by default, generates scan codes from Set 1. I haven't written code to make the keyboard controller generate either Set 2 or Set 3 scan codes, so I'm assuming that the keyboard controller is giving me Set 1 scan codes.

Set 1 provides most keys with a one-byte scan code. Certain other keys — the cursor keys, INSERT, DELETE, PAGE UP, and others — have a two-byte scan code; the first byte is 0xE0. Two keys have odd scan codes: The PRINT SCREEN key generates a four-byte scan code beginning with 0xE0, and the PAUSE key generates a six-byte scan code beginning with 0xE1.

The make code and the break code for the same key differ by one bit — bit 7 in a one-byte scan code, or bit 7 in the byte (or bytes) following 0xE0 in a multiple-byte scan code. The make code leaves the bit clear (putting the byte in the range 0x00..0x7F); the break code sets the bit (putting the byte in the range 0x80..0xFF). Thus if the make code for the J key is 0x24, the break code for the J key is 0xA4. The make code for the END key is 0xE0 0x4F, so the break code for the END key is 0xE0 0xCF. Thus you can tell when a key is being held down and when it has been released.

(Theoretically I could switch to using Set 3 scan codes, because they are easier to handle — each key without exception has a one-byte make code. However, I've read in places that not all keyboards can generate Set 3 scan codes, but they can be counted on to generate Set 1 codes.)

Code

My latest project simply waits for a key event, then grabs the name of the key from a table and prints it on the screen, then displays "MAKE" if the key is being pressed, or "BRK" if the key has been released. It also dumps the last byte of the scan code to the screen.

The relevant code is in BOOT.ASM, in the section marked SYSTEM LOADER.

First, I print a message for the user: Hit a key to see its name below.

		call	cls      ; Clear the screen.
		call	tclear   ; Empty the T register.
		_lit	msg
		_lit	len
		call	tapps    ; Copy the message into the T register.
		_lit	1
		call	tprint   ; Print the message to row 1 on the screen.
		call	refresh  ; Copy the screen buffer into video RAM.

Then I enter the get_key loop. First, I reserve a space on the top of the stack, so that I can use the top-of-stack register (EAX, of which AL is a part) to fish bytes out of the keyboard controller ports.

get_key:
		_dup

Then I set EAX to zero, so that I can change AL, and then use the whole of EAX as an offset into the key-name table later.

		xor	eax, eax

Now I wait for the user to press a key.

		call	wait_for_key_byte

I have the first byte of the key's scan code in AL. If this byte is 0xE1, then the user pressed the PAUSE key, which generates a six-byte make code (but no break code). In this case, I just fish out the other five bytes, discard them, and substitute a make code of 0x00.

		cmp	al, 0xE1
		jne	.2

		mov	bx, 5
	.1:	call	wait_for_key_byte
		dec	bx
		jnz	.1

		xor	eax, eax
		jmp	access_key_table

If the byte is 0xE0, then the scan code contains a second byte, which I'll have to retrieve.

	.2:	cmp	al, 0xE0
		jne	.3

		call	wait_for_key_byte

I save this byte into make_or_break (so I can test it later to determine if the key was pressed or released).

		mov	[make_or_break], al

Then I force bit 7 on, to produce a fake one-byte scan code in the range 0x80..0xFF.

		or	al, 0x80
		jmp	access_key_table

If the byte is neither 0xE0 nor 0xE1, then I have the complete scan code in AL. I save it into make_or_break (again, to test it later), and I force bit 7 off, to ensure that I have a scan code in the range 0x00..0x7F.

	.3:	mov	[make_or_break], al
		and	al, 0x7F

I save the altered scan code in offset, so that it will show up when I dump the memory to the screen later.

access_key_table:
		mov	[offset], al

I clear out the T register. Then I grab the four-character string corresponding to the key code in AL, and I append it to the T register.

		call	tclear
		mov	eax, [key_table+eax*4]
		call	tapp4

I add a space to the T register, to separate the key name from the following string.

		_lit	' '
		call	tapp1

Now I can test the scan code I saved to make_or_break. If bit 7 is clear, then the scan code was a make code, so I put the word MAKE in EAX; otherwise the code was a break code, so I load BRK into EAX instead.

		_lit	'MAKE'
		mov	bl, [make_or_break]
		test	bl, 0x80
		jz	.1
		mov	eax, 'BRK '

I append the contents of EAX (MAKE or BRK) to the T register.

	.1:	call	tapp4

I print the T register to row 3.

		_lit	3
		call	tprint

I dump sixteen bytes, starting at make_or_break, to row 5. (Make_or_break is the first byte dumped; offset is the second. The rest of the row shows the first few bytes of the key-name table and can be ignored.)

		_lit	5
		_lit	make_or_break
		call	dump16

I refresh the screen and repeat the whole process.

		call	refresh
		jmp	get_key

Now here is what the wait_for_key_byte routine does. It tests bit zero of port 0x64 repeatedly. If the bit is zero, then there is no data yet, but if the bit is one, I can grab the byte of data from port 0x60 and return.

wait_for_key_byte:
	.1:	in	al, 0x64
		test	al, 1
		jz	.1
		in	al, 0x60
		ret

Key table

The key table is an array of 256 four-character key names, padded with spaces. Obviously I had to abbreviate the real names of many keys to get them to fit into four characters — "Paus" (offset 0x00) is the Pause key, and "BKSP" (offset 0x0E) is the Backspace key.

key_table:
; (Uses Set 1 scan codes;
;  two-byte code E0 xx --becomes-- one-byte code xx+0x80)

		dd	'Paus','ESC ','1   ','2   ' ; 0x00
		dd	'3   ','4   ','5   ','6   '
		dd	'7   ','8   ','9   ','0   '
		dd	'-   ','=   ','BKSP','TAB '

"ENTR" (offset 0x1C) is the Enter key (the one above the right Shift key, not the "tall" one on the numeric keypad. "LCtl" (offset 0x1D) is the left Ctrl key.

		dd	'Q   ','W   ','E   ','R   ' ; 0x10
		dd	'T   ','Y   ','U   ','I   '
		dd	'O   ','P   ','[   ',']   '
		dd	'ENTR','LCtl','A   ','S   '

I wasn't sure how well the semicolon, the double-quote mark, and the "tick" (the grave-accent mark made with the key that's usually above the Tab key and to the left of the "1" key) would show up on the screen, so I just designated these keys with "semi", "quot", and "tick" respectively. "LShf" is the left Shift key.

		dd	'D   ','F   ','G   ','H   ' ; 0x20
		dd	'J   ','K   ','L   ','semi'
		dd	'quot','tick','LShf','\   '
		dd	'Z   ','X   ','C   ','V   '

"RShf" is the right Shift key. "KP" indicates a key in the numeric keypad on the right side of most fullsized keyboards (laptops might not have these keys). "KP *" thus indicates the star or multiplication key on the keypad. "LAlt" is the left Alt key, "Spac" is the space bar, and "CAPS" is the Caps Lock key.

		dd	'B   ','N   ','M   ',',   ' ; 0x30
		dd	'.   ','/   ','RShf','KP *'
		dd	'LAlt','Spac','CAPS','F1  '
		dd	'F2  ','F3  ','F4  ','F5  '

"NumL" and "ScrL" are the Num Lock and Scroll Lock keys respectively.

		dd	'F6  ','F7  ','F8  ','F9  ' ; 0x40
		dd	'F10 ','NumL','ScrL','KP 7'
		dd	'KP 8','KP 9','KP -','KP 4'
		dd	'KP 5','KP 6','KP +','KP 1'

I used "null" in any offset that didn't correspond with any of the keys listed on the Set 1 page. Some keyboards come with extra keys for getting your mail or surfing the Web; such keys might produce strange scan codes that result in the word "null" being printed to the screen.

		dd	'KP 2','KP 3','KP 0','KP .' ; 0x50
		dd	'null','null','null','F11 '
		dd	'F12 ','null','null','null'
		dd	'null','null','null','null'

		dd	'null','null','null','null' ; 0x60
		dd	'null','null','null','null'
		dd	'null','null','null','null'
		dd	'null','null','null','null'

		dd	'null','null','null','null' ; 0x70
		dd	'null','null','null','null'
		dd	'null','null','null','null'
		dd	'null','null','null','null'

Two-byte scan codes that begin with 0xE0 are converted into one-byte scan codes in the range 0x80..0xFF. Entries for such scan codes begin here.


		dd	'null','null','null','null' ; E0 00
		dd	'null','null','null','null'
		dd	'null','null','null','null'
		dd	'null','null','null','null'

A number of keys given here are multimedia keys that aren't present on many keyboards, such as "PREV" (previous track) and "NEXT" (next track). Others should be present on almost every keyboard, such as "KPEn" (the Enter key on the numeric keypad) and "RCtl" (the right Ctrl key).

		dd	'PREV','null','null','null' ; E0 10
		dd	'null','null','null','null'
		dd	'null','NEXT','null','null'
		dd	'KPEn','RCtl','null','null'

"PrSc" is the Print-Screen key of course. (This key actually produces two scan codes, one after the other. The first one is 0xE0 0x2A, and the second is 0xE0 0x37. Thus "PrSc" appears twice in this table.) "V_DN" is the Volume-Down key.

		dd	'MUTE','CALC','PLAY','null' ; E0 20
		dd	'STOP','null','null','null'
		dd	'null','null','PrSc','null'
		dd	'null','null','V_DN','null'

"V_UP" is "Volume Up". "RAlt" is the right Alt key. A key that begins with a "W" is a "Web" key that Windows interprets as a browser command: "WHom" brings up the Home page in the browser.

		dd	'V_UP','null','WHom','null' ; E0 30
		dd	'null','KP /','null','PrSc'
		dd	'RAlt','null','null','null'
		dd	'null','null','null','null'

The four arrow keys are "->UP", "-> L", "-> R", and "->DN" respectively.

		dd	'null','null','null','null' ; E0 40
		dd	'null','null','null','HOME'
		dd	'->UP','PgUP','null','-> L'
		dd	'null','-> R','null','END '

"LGUI" and "RGUI" are the two keys (on newer keyboards) that have the Microsoft "flying windows" or "Windows flag" logo on them. "APPS" is the key (next to the right GUI key) with an icon suggesting a popup menu. "POWR" and "SLEE" are the Power and Sleep keys (on still newer keyboards).

		dd	'->DN','PgDN','INS ','DEL ' ; E0 50
		dd	'null','null','null','null'
		dd	'null','null','null','LGUI'
		dd	'RGUI','APPS','POWR','SLEE'

The keys here: Wake (stop hibernation), WWW Search, WWW Favorites, WWW Refresh, WWW Stop, WWW Forward, WWW Back, My Computer, E-Mail, and Media Select respectively.

		dd	'null','null','null','WAKE' ; E0 60
		dd	'null','WSea','WFav','WRef'
		dd	'WStp','WFor','WBac','MyCo'
		dd	'MAIL','MEDI','null','null'

		dd	'null','null','null','null' ; E0 70
		dd	'null','null','null','null'
		dd	'null','null','null','null'
		dd	'null','null','null','null'

Results

I've tested this code on the computer I have at work and got strange results: Most of the keys that are supposed to produce multibyte scan codes beginning with 0xE0 worked OK when I pressed them — for example, pressing the up-arrow key displays "->UP MAKE" as I intended. However, when I released the key, I'd get "PrSc BRK" — and the dump line beneath showed that the last byte of the break code was 0xAA. This happened only on my computer at work; my laptop at home produced the "->UP BRK" I expected.

From this, I am guessing that some keyboards produce a four-byte scan code not only for the PRINT SCREEN key but also for keys like INSERT and HOME and PAGE UP. I am guessing that, for such keys, the make code begins with 0xE0 0x2A, and the break code ends with 0xE0 0xAA.

I've found some evidence for this, in a text file claiming that 0xE0 0x2A is the "make code prefix for keyboard internal numlock," and that 0xE0 0xAA is the corresponding "break code suffix for keyboard internal numlock." (I suppose this means that whenever the keyboard thinks NumLock is on, it will generate these extra scan codes when certain keys are pressed — see also here.) I've also found a page claiming that 0xE0 0x2A is the make code for a "fake" left-shift key, inserted by some keyboards "in order to fool old programs."

So I'll add a few lines of code later to treat the scan codes 0xE0 0x2A and 0xE0 0xAA as meaningless. This would leave the "real" make code for PRINT SCREEN as 0xE0 0x37.

Relevance for the future

I'm doing this for two reasons: (1) I'll want to experiment with macros by assigning them to keys (so that pressing a key runs a macro). (2) I'll want to set aside 1KB for a table that can handle up to 256 keys. Not only that, but I'll also set aside another 1KB for each "control" key I'll want to use — Shift, Ctrl, and so on. This seems terribly wasteful, and certainly it's very far from the way Chuck Moore handles the keyboard (he wrote code to handle no more than 27 out of the hundred or so keys on a keyboard). However, I will eventually want to experiment with the extra keys on multimedia keyboards.

Final note: Finding video RAM

I forgot to add last time: I added a few lines of code to the boot sector to get the true address of the video RAM. I had to do this in order to test my code on my computer at work. The lines are:

; ------ Find the physical address of the video RAM.
;        (Uses 256 bytes at end of segment zero)
		mov	cx, 0x111      ; mode number
		mov	di, 0xFF00     ; pointer to table
		mov	ax, 0x4F01
		int	0x10
		mov	eax, [0xFF28]  ; PhysBasePtr field in table
		mov	[scr$.vr], eax

Check the index for other entries.