I used to use Milo Sedlacek's MULTIKEY assembly code to replace the keyboard interrupt service routine because up until recently, it never failed to deliver. I understood the basics of what the code was doing, but never looked into HOW it did it. To get it to work, you have to reserve an array in QuickBASIC, get its pointer (both segment and offset) and then use that to reprogram four bytes into the assembly code, so the code knows where it should store the status of each key (the rest is standard stuff like reading port 0x60, checking if the status is press or release, acknowledge interrupt, etc.)
I recently came across an annoying bug that rendered MULTIKEY unusable. I assumed it was due to the fact I was forcing QuickBASIC to consolidate the memory reserved for variables (and thus moving variables -including the array where key status would be stored- in memory) by reserving memory via a DOS interrupt (QuickBASIC always reserves all the available space for your code and variables; if you want a static memory address, you have to put it away from its control by first telling it to free some memory and then reserving it via DOS.) The obvious side-effect was that the array meant to contain the key status would move and MULTIKEY would write to the old address, where something else is stored. At that point, I decided to also reserve memory for the key status buffer via the DOS interrupt to make sure it wouldn't move. But that didn't exactly fix the issue as it seemed that my own PEEK and POKE calls would not land where I expected them to.
So I read Milo's code going instruction by instruction and realized he was modifying the code segment address to access the key status buffer. Since the code is designed to be an interrupt, it means that it can be executed at any time during the main program's execution - for instance, in the middle of a loop that reads/writes stuff to memory via PEEK and POKE - which means the interrupt could impact DEF SEG silently, and there would be no way of knowing. Here's Milo's interrupt code:
Code: Select all
; pushing registers to stack so they can be restored to their initial value
9C PUSHF
50 PUSH AX
53 PUSH BX
51 PUSH CX
52 PUSH DX
1E PUSH DS
56 PUSH SI
06 PUSH ES
57 PUSH DI
; port reading
EA 60 IN AL, 60 ; Read port 0x60, store in AL
B4 01 MOV AH, 01 ; Assume the key is pressed
A8 80 TEST AL, 80 ; Test bit-7 in AL, modifies Sign, Zero and Parity flag register.
74 04 JZ 4 ; If Zero flag is SET, skip 4 bytes (2 instructions)
B4 00 MOV AH, 00 ; The key was in fact released
24 7F AND AL, 7F ; Strip bit-7 from AL (only keep scancode)
; getting offset from begining of the array
D0 E0 SHL AL, 1 ; Multiply AL by 2 (target is an INTEGER array of 129 elements)
88 C3 MOV BL, AL ; Set BX (lower byte) to AL
B7 00 MOV BH, 00 ; Set BX (high byte) to 0
B0 00 MOV AL, 00 ; Set AL to 0
; going to the array memory address and write key status
2E CS: ; Change code segment, set BX (offset)
03 1E 12 00 ADD BX, [0012] ; Add BX to the value stored at 0x12 (array memory offset)
2E CS: ; Change code segment, set DS (segment)
8E 1E 10 00 MOV DS, [0010] ; Set DS to the value stored at 0x10 (array memory segment)
86 E0 XCHG AH, AL ; Swap AH and AL (AH contains the key status)
89 07 MOV [BX], AX ; Write AX (2 bytes) to [BX] (array memory offset)
; the rest is the standard:
; acknowledge interrupt
; restore registers with POP and POPF
; terminate interrupt execution with IRET
Code: Select all
; pushing registers like above
EA 60 IN AL, 60 ; Read port 0x60, store in AL
B4 01 MOV AH, 01 ; Assume the key is pressed
A8 80 TEST AL, 80 ; Test bit-7 in AL, modifies Sign, Zero and Parity flag register.
74 04 JZ 4 ; If Zero flag is SET, skip 4 bytes (2 instructions)
B4 00 MOV AH, 00 ; Our bad, key is actually released.
24 7F AND AL, 7F ; Only preserve bits 6-0 in AL, discard bit 7.
88 C3 MOV BL, AL ; Set BX to scancode: BL = AL
B7 00 MOV BH, 0 ; Set BX to scancode: BH = 0
2E 88 27 MOV CS:[BX], AH ; Copy key status to specified address
; the rest is the standard:
; acknowledge interrupt
; restore registers with POP and POPF
; terminate interrupt execution with IRET
Code: Select all
'$INCLUDE: 'QB.BI'
DECLARE SUB memFree (segAdr AS INTEGER)
DECLARE FUNCTION memAlloc% (numBytes AS LONG)
DECLARE FUNCTION keyInit% ()
DIM keySegm AS INTEGER, tmr AS DOUBLE
CLS
tmr = TIMER + 5
keySegm = keyInit%
DO
DEF SEG = keySegm
LOCATE 1, 1
FOR i% = 0 TO 128
PRINT PEEK(i%);
NEXT i%
LOOP UNTIL (tmr < TIMER)
keySegm = keyInit%
FUNCTION keyInit%
STATIC oldISRSeg AS INTEGER, oldISROfs AS INTEGER, newISRSeg AS INTEGER
DIM regs AS RegTypeX
IF (newISRSeg = 0) THEN
' Reserve memory for buffer & code
newISRSeg = memAlloc%(182) ' key status buffer (129) + code (53)
' Clear key strokes (starting at offset 0 of segment [newISRSeg])
DEF SEG = newISRSeg
FOR i% = 0 TO 128
POKE i%, 0
NEXT i%
' Write code (starting at offset 129 of segment [newISRSeg])
FOR i% = 0 TO 52
POKE i% + 129, VAL("&H" + MID$("FB9C505351521E560657E460B401A8807404B400247F88C3B7002E8827E4610C80E661247FE661B020E6205F075E1F5A595B589DCF", 1 + i% * 2, 2))
NEXT i%
' Preserve vector interrupt 9 (BIOS keyboard ISR)
regs.ax = &H3509
CALL INTERRUPTX(&H21, regs, regs)
oldISRSeg = regs.es
oldISROfs = regs.bx
' Clear keyboard buffer
DEF SEG = 0
POKE (&H41A), PEEK(&H41C)
DEF SEG
' Hook custom keyboard handler
regs.ax = &H2509
regs.ds = newISRSeg ' interrupt code (and buffer) memory segment
regs.dx = 129 ' interrupt code offset
CALL INTERRUPTX(&H21, regs, regs)
ELSE
' Restore BIOS keyboard ISR
regs.ax = &H2509
regs.ds = oldISRSeg
regs.dx = oldISROfs
CALL INTERRUPTX(&H21, regs, regs)
' Deallocate memory reserved for buffer & code
memFree newISRSeg
END IF
keyInit% = newISRSeg ' offset to key status buffer
END FUNCTION
''
'' QuickBASIC always reserves the largest block of memory available for
'' the far heap. If we need to allocate memory for our purpose, we must
'' first tell QuickBASIC to free part of that memory.
''
FUNCTION memAlloc% (numBytes AS LONG)
DIM memReq AS INTEGER, junk AS LONG, regs AS RegTypeX
' Paragraphs are groups of 16 bytes
memReq = (numBytes \ 16) - ((numBytes AND 15) > 0)
' Tell QuickBASIC to free some memory (not sure why a margin is needed)
junk = SETMEM(-CLNG(memReq + 1) * 16)
' Use DOS Interrupt 0x48 to request <memReq> paragraphs of memory
regs.ax = &H4800
regs.bx = memReq
CALL INTERRUPTX(&H21, regs, regs)
' If CF is not clear, something went wrong
IF (regs.flags AND &H1) THEN
junk = SETMEM(650000)
ELSE
memAlloc% = regs.ax
END IF
END FUNCTION
''
'' Free memory reserved via DOS Interrupt 0x21, function 0x48
''
SUB memFree (segAdr AS INTEGER)
DIM junk AS LONG, regs AS RegTypeX
' No segment specified, abort
IF (segAdr = 0) THEN EXIT SUB
' Free allocated memory
regs.ax = &HA900
regs.es = segAdr
CALL INTERRUPTX(&H21, regs, regs)
' Clear segment and offset
segAdr = 0
' Give back memory to QuickBASIC
junk = SETMEM(650000)
END SUB