Voltmace Delta 14B/1 KEYPAD loader and resident driver

Updated 2 Sep 2026

← All Voltmace Delta 14B Driver versions

Install the resident keypad driver

Enable the 50 Hz vertical-sync event and route it to the matrix scanner. Sets User VIA port B to strobe the keypad (DDRB = &F0: top nibble out, bottom nibble in) and hooks EVNTV to point at the event handler, saving the previous vector at saved_evntv. Called from the BASIC front-end via CALL &A00.

On ExitAcorrupted
Xcorrupted
Ycorrupted
LoadExec
1900 0A00 .install_driver
PHP ; Preserve the caller flags
1901 0A01 PHA ; Preserve A
1902 0A02 TYA ; Preserve Y...
1903 0A03 PHA ; ...on the stack
1904 0A04 TXA ; Preserve X...
1905 0A05 PHA ; ...on the stack
1906 0A06 LDA #&0e ; OSBYTE 14: enable an event...
1908 0A08 LDX #4 ; ...event 4, the 50 Hz vertical sync
190A 0A0A JSR osbyte ; call OSBYTE
190D 0A0D LDA #&f0 ; DDRB = &F0: bits 4-7 drive the column strobes + 74LS157 select...
190F 0A0F STA user_via_ddrb ; ...bits 0-3 sense the four row lines
1912 0A12 SEI ; Block IRQs while re-pointing the vector
1913 0A13 LDA evntv ; Save the current event vector low byte...
1916 0A16 STA saved_evntv ; ...at saved_evntv, to chain to on exit
1919 0A19 LDA evntv_hi ; Save its high byte...
191C 0A1C STA saved_evntv_hi ; ...too
191F 0A1F LDA #&31 ; Point EVNTV at vsync_event_handler: low byte &31...
1921 0A21 STA evntv ; ...store it
1924 0A24 LDA #&0a ; ...high byte &0A...
1926 0A26 STA evntv_hi ; ...store it
1929 0A29 CLI ; Re-enable IRQs
192A 0A2A PLA ; Restore X...
192B 0A2B TAX ; ...from the stack
192C 0A2C PLA ; Restore Y...
192D 0A2D TAY ; ...from the stack
192E 0A2E PLA ; Restore A
192F 0A2F PLP ; Restore the flags
1930 0A30 RTS ; Return to the BASIC caller

Vertical-sync event handler: scan the keypad

Entered from the MOS every 50 Hz vsync event. Strobes the 3x4 matrix through User VIA port B (bit 7 selects handset 0/1 via the 74LS157), debounces and auto-repeats via debounce_counter, and on a pressed key sounds a short key-click (OSWORD 7) and inserts the mapped character into the keyboard buffer (OSBYTE &99). Chains to the previous event vector on exit.

LoadExec
1931 0A31 .vsync_event_handler
PHP ; Preserve the interrupted flags
1932 0A32 PHA ; Preserve A
1933 0A33 TYA ; Preserve Y...
1934 0A34 PHA ; ...on the stack
1935 0A35 TXA ; Preserve X...
1936 0A36 PHA ; ...on the stack
1937 0A37 LDA #0 ; Quick probe: drive every column low on handset 0...
1939 0A39 STA user_via_orb ; ...write it
193C 0A3C LDA user_via_orb ; ...and read the rows back
193F 0A3F CMP #&0f ; All four rows high (&0F) => nothing down on handset 0
1941 0A41 BNE check_held_key ; Something is down -> go locate it
1943 0A43 LDA #&80 ; Probe handset 1 (bit 7 selects it)...
1945 0A45 STA user_via_orb ; ...write it
1948 0A48 LDA user_via_orb ; ...read the rows
194B 0A4B CMP #&8f ; &8F = handset-1 select set, all rows high = no key
194D 0A4D BEQ no_key_down ; Nothing on either handset -> idle
194F 0A4F .check_held_key←1← 0A41 BNE
LDY current_row ; Is a key already being held? (current_row 5 = none)
1952 0A52 CPY #5 ; against the "none" marker (5)
1954 0A54 BEQ scan_matrix ; No held key -> full matrix scan
1956 0A56 LDX current_col ; Re-test the held key: fetch its column strobe...
1959 0A59 LDA col_strobes,x ; from the strobe table
195C 0A5C STA user_via_orb ; ...drive it
195F 0A5F LDA user_via_orb ; ...read the rows...
1962 0A62 AND row_masks,y ; ...and mask its row bit
1965 0A65 BNE scan_matrix ; Released (bit high now) -> rescan for a new key
1967 0A67 DEC debounce_counter ; Still held: count down to the next auto-repeat (patched to LDA by the editor to disable auto-repeat)
196A 0A6A BNE handler_exit ; Not time to repeat yet -> exit
196C 0A6C LDA #4 ; Reload the repeat interval (4 frames)...
196E 0A6E STA debounce_counter ; store it
1971 0A71 CLC ; clear carry for the branch
1972 0A72 BCC emit_key ; Re-emit the held key
1974 0A74 .scan_matrix←2← 0A54 BEQ← 0A65 BNE
LDX #0 ; Full scan: start at column 0
1976 0A76 .scan_next_column←1← 0A92 BNE
LDY #0 ; Start at row 0 of this column
1978 0A78 .scan_next_row←1← 0A8D BNE
CPY #0 ; Only (re)strobe the column when starting at row 0...
197A 0A7A BNE test_row ; ...otherwise the strobe already stands
197C 0A7C LDA col_strobes,x ; Drive column X (its handset bit included)...
197F 0A7F STA user_via_orb ; write it
1982 0A82 .test_row←1← 0A7A BNE
LDA user_via_orb ; Read the rows...
1985 0A85 AND row_masks,y ; ...test this row bit
1988 0A88 BEQ key_pressed ; Bit low -> key at (column X, row Y) is pressed
198A 0A8A INY ; Next row...
198B 0A8B CPY #4 ; all four rows done?
198D 0A8D BNE scan_next_row ; ...until all 4 rows tested
198F 0A8F INX ; Next column...
1990 0A90 CPX #6 ; ...6 columns = 3 per handset
1992 0A92 BNE scan_next_column ; loop back for the next column
1994 0A94 CLC ; clear carry for the branch
1995 0A95 BCC handler_exit ; Nothing pressed -> exit
1997 0A97 .key_pressed←1← 0A88 BEQ
LDA #&18 ; New press: set the initial auto-repeat delay (24 frames)...
1999 0A99 STA debounce_counter ; store it
199C 0A9C STX current_col ; Remember which key is now held: column...
199F 0A9F STY current_row ; ...and row
19A2 0AA2 .emit_key←1← 0A72 BCC
LDA #7 ; OSWORD 7: play the key-click sound...
19A4 0AA4 LDX #&f6 ; ...parameter block at sound_block (&0AF6)...
19A6 0AA6 LDY #&0a ; block high byte &0A
19A8 0AA8 JSR osword ; call OSWORD
19AB 0AAB CLC ; Key-table index = col*4 + row:
19AC 0AAC LDA current_col ; take the column...
19AF 0AAF ASL ; ...times 4...
19B0 0AB0 ASL ; shifted twice = x4
19B1 0AB1 ADC current_row ; ...plus the row
19B4 0AB4 TAX ; ...as an index
19B5 0AB5 LDY key_codes,x ; Fetch that cell character into Y
19B8 0AB8 LDA #&99 ; OSBYTE &99: insert Y into buffer 0 (keyboard)...
19BA 0ABA LDX #0 ; X=0 selects the keyboard buffer
19BC 0ABC JSR osbyte ; ...as if the key were typed
19BF 0ABF CLC ; clear carry for the branch
19C0 0AC0 BCC handler_exit ; Done
19C2 0AC2 .no_key_down←1← 0A4D BEQ
LDA #5 ; Record 'no key held' (row 5)...
19C4 0AC4 STA current_row ; store it
19C7 0AC7 .handler_exit←3← 0A6A BNE← 0A95 BCC← 0AC0 BCC
PLA ; Restore X...
19C8 0AC8 TAX ; into X
19C9 0AC9 PLA ; Restore Y...
19CA 0ACA TAY ; into Y
19CB 0ACB PLA ; Restore A
19CC 0ACC PLP ; Restore the flags
19CD 0ACD JMP (saved_evntv) ; Chain to the event handler we displaced
19D0 0AD0 .debounce_counter←3← 0A67 DEC← 0A6E STA← 0A99 STA
EQUB &10 ; Frames until the next auto-repeat (0 = repeat this frame)
19D1 0AD1 .current_row←4← 0A4F LDY← 0A9F STY← 0AB1 ADC← 0AC4 STA
EQUB &04 ; Row of the held key (5 = none)
19D2 0AD2 .current_col←3← 0A56 LDX← 0A9C STX← 0AAC LDA
EQUB &00 ; Column of the held key
19D3 0AD3 .row_masks←2Used as index base by← 0A62 AND← 0A85 AND
EQUB &08, &04, &02, ; Port-B input bit for matrix &01 ; rows 0-3
19D7 0AD7 .col_strobes←2Used as index base by← 0A59 LDA← 0A7C LDA
EQUB &60, &50, ; Port-B strobe per column: cols 0-2 = &30, &E0, ; handset 0 (&60,&50,&30), cols 3-5 = &D0, &B0 ; handset 1 (bit 7 set for the ; 74LS157)
19DD 0ADD .key_codes←1Used as index base by← 0AB5 LDY
EQUB &7F ; Default character per cell, indexed col*4+row: cells 0-11 handset 0 (digits/DELETE/RETURN), cells 12-23 handset 1 (letters). The editor overwrites this table.
19DE 0ADE EQUS "1470258"
19E5 0AE5 EQUB &0D
19E6 0AE6 EQUS "369ADGJBEHKCFIL"
19F5 0AF5 EQUB &00 ; Spare byte
19F6 0AF6 .sound_block
EQUW &0000, ; OSWORD 7 block: channel &0000, &FFF8, ; amplitude &FFF8 (patched by the editor &80, ; beep option), pitch &0080, duration &0001 ; &0001
19FE 0AFE .saved_evntv←2← 0A16 STA← 0ACD JMP
EQUW &0000 ; Previous EVNTV, chained to on exit
; Image of page &0B (the MOS soft-key/function-key ; buffer). The relocator skips page &0B so the user keys ; survive, making these bytes inert filler.
1A00 EQUB &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10, &10
; ROL-encoded tokenised BASIC (the keypad-definition ; editor); relocates to PAGE=&0C00 and is decrypted in ; place. Source: ; basic/voltmace-delta-14b-driver-keypad.bas.
1B00
; Zero padding between the encrypted BASIC and the *RUN ; loader
3869 EQUB &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00, &00
; Filler ahead of the loader entry; reads as a stub BASIC ; line (&0D, line 13, then RTS bytes)
3900 .loader_preamble
EQUB &0D, &00, &0D, &60, &60, &60

*RUN entry: relocate, decode, and auto-run

The DFS execution address. Relocates the whole image down to &0A00 (installing the resident driver code and moving the encrypted BASIC to PAGE &0C00), decrypts the BASIC in place, then queues 'PAGE=&C00 / OLD / RUN' so the now-plain BASIC front-end starts.

3906 .main
PHA ; Preserve A
3907 TXA ; Preserve X...
3908 PHA ; ...on the stack
3909 TYA ; Preserve Y...
390A PHA ; ...on the stack
390B JSR relocate_image ; Copy the image down (resident driver to &0A00, BASIC to PAGE &0C00)
390E JSR decode_basic ; Decrypt the relocated BASIC in place
3911 JSR patch_basic_header ; Repair the BASIC's first-line length byte
3914 JSR os_dependent_setup ; Queue the auto-run commands (OS-dependent)
3917 PLA ; Restore Y...
3918 TAY ; into Y
3919 PLA ; Restore X...
391A TAX ; into X
391B PLA ; Restore A
391C RTS ; Return to the MOS; the queued commands then run the BASIC

Decrypt the relocated BASIC

Rotate every byte of the relocated BASIC left one bit (the inverse of the ROL-1 storage protection), across pages &0C00-&2AFF, in place.

391D .decode_basic←1← 390E JSR
LDA decode_ptr ; Save the caller decode_ptr low byte...
391F STA decode_ptr_save ; save it
3922 LDA decode_ptr_hi ; ...and high byte...
3924 STA decode_ptr_save_hi ; ...at decode_ptr_save
3927 LDX #0 ; Point decode_ptr at PAGE &0C00: low byte 0...
3929 STX decode_ptr ; set it
392B LDX #&0c ; ...high byte &0C...
392D LDY #0 ; byte index 0
392F .decode_next_page←1← 3941 BNE
STX decode_ptr_hi ; Set the page being decrypted
3931 .decode_next_byte←1← 393A BNE
LDA (decode_ptr),y ; Read an encrypted byte...
3933 CLC ; clear carry for the rotate
3934 ASL ; ...rotate it left one bit (undo the ROL-1 protection)...
3935 ADC #0 ; ...carrying bit 7 into bit 0...
3937 STA (decode_ptr),y ; ...and store it back
3939 INY ; Next byte...
393A BNE decode_next_byte ; ...to the end of the page
393C LDX decode_ptr_hi ; Next page...
393E INX ; increment the page
393F CPX #&2b ; ...until page &2B (end of the BASIC region)
3941 BNE decode_next_page ; keep decrypting pages
3943 LDA decode_ptr_save ; Restore the caller decode_ptr...
3946 STA decode_ptr ; low byte
3948 LDA decode_ptr_save_hi ; high byte
394B STA decode_ptr_hi ; store it
394D RTS ; Done

OS-version-dependent setup

Reads os_signature to pick how the auto-run commands are queued.

394E .os_dependent_setup←1← 3914 JSR
LDY #0 ; clear Y
3950 LDA os_signature ; Read the OS ROM signature byte...
3953 CMP #&4f ; ...'O' identifies the supported OS
3955 BEQ use_direct_poke ; Supported OS -> poke the buffer directly
3957 JSR setup_keys ; Other OS -> insert via OSBYTE
395A RTS ; Done
395B .use_direct_poke←1← 3955 BEQ
JSR queue_autorun ; Queue by poking the keyboard buffer
395E RTS ; Done
395F .decode_ptr_save←2← 391F STA← 3943 LDA
EQUW &EAEA ; Scratch: caller's decode_ptr saved across decode_basic (&EAEA at rest)

Repair the BASIC program's first line

Write &16 (22) into basic_line10_len, the length byte of the line-10 REM at PAGE=&0C00. That byte is stored as 0 (part of the protection), so without this repair the relocated program cannot be LISTed or RUN.

3961 .patch_basic_header←1← 3911 JSR
LDA #&16 ; The first BASIC line is 22 bytes long...
3963 STA basic_line10_len ; ...restore its length byte (stored as 0 by the protection)
3966 RTS ; Done

Queue the auto-run commands (direct poke)

Copy autorun_commands into the MOS keyboard buffer so the OS reads them as typed and runs the decoded BASIC. autorun_index wraps into the 32-byte buffer at &03E0-&03FF.

3967 .queue_autorun←1← 395B JSR
LDY #0 ; Start at the first command byte
3969 .queue_next_byte←1← 3982 JMP
LDX autorun_index ; Current buffer fill position...
396C LDA autorun_commands,y ; read a command byte...
396F BEQ queue_done ; ...&00 terminates
3971 STA kbd_buffer,x ; Poke it into the keyboard buffer
3974 INC autorun_index ; Advance the fill position...
3977 LDA autorun_index ; reload it
397A BNE queue_advance ; unless it wrapped to 0
397C LDA #&e0 ; ...wrapping back to &E0 (buffer at &03E0)...
397E STA autorun_index ; store it
3981 .queue_advance←1← 397A BNE
INY ; Next command byte
3982 JMP queue_next_byte ; loop
3985 .queue_done←1← 396F BEQ
RTS ; Done

Queue the auto-run commands (OSBYTE path)

The non-&4F-OS variant of queue_autorun: set the BREAK/ESCAPE behaviour, then insert each autorun_commands byte with OSBYTE &8A.

3986 .setup_keys←1← 3957 JSR
LDA #&c8 ; OSBYTE 200: set the BREAK/ESCAPE behaviour...
3988 LDX #3 ; value 3
398A JSR osbyte ; call OSBYTE
398D LDX #0 ; Start at the first command byte
398F .insert_next_key←1← 39A5 JMP
LDA autorun_commands,x ; Read a command byte...
3992 BEQ setup_keys_done ; ...&00 terminates
3994 TAY ; The character to insert
3995 TXA ; Save the loop index...
3996 STA setup_key_index ; save it
3999 LDX #0 ; OSBYTE &8A: insert Y into buffer 0 (keyboard)...
399B LDA #&8a ; A = &8A
399D JSR osbyte ; call OSBYTE
39A0 LDA setup_key_index ; Restore the loop index...
39A3 TAX ; into X
39A4 INX ; Next command byte
39A5 JMP insert_next_key ; loop
39A8 .setup_keys_done←1← 3992 BEQ
RTS ; Done
39A9 .setup_key_index←2← 3996 STA← 39A0 LDA
EQUB &EA ; Scratch: saved loop index (&EA at rest)

Relocate the program image to &0A00

OSCLI the startup command, then block-copy the image from &1900 down to &0A00 (copy_pages pages via copy_src -> copy_dst), skipping destination page &0B so the soft-key buffer survives.

39AA .relocate_image←1← 390B JSR
LDX #&f1 ; OSCLI the startup command at oscli_command (&39F1): low byte...
39AC LDY #&39 ; ...high byte...
39AE JSR oscli ; ...call OSCLI
39B1 SEC ; (carry set; not used by the copy below)
39B2 LDA #0 ; Leftover byte count = 0 (whole pages only)...
39B4 STA copy_rem ; store it
39B6 LDA #&20 ; Copy &20 = 32 pages...
39B8 STA copy_pages ; store it
39BA LDA #0 ; Destination = &0A00: low byte...
39BC STA copy_dst ; store it
39BE LDA #&0a ; ...high byte...
39C0 STA copy_dst_hi ; store it
39C2 LDA #0 ; Source = &1900 (the loaded image): low byte...
39C4 STA copy_src ; store it
39C6 LDA #&19 ; ...high byte...
39C8 STA copy_src_hi ; store it
39CA LDY #0 ; Byte index within the page
39CC LDX copy_pages ; Page count...
39CE BEQ copy_remainder ; ...none -> just the remainder
39D0 .copy_page←2← 39DB BNE← 39E2 BNE
LDA copy_dst_hi ; Which destination page are we about to write?
39D2 CMP #&0b ; Leave page &0B untouched so the user soft-key definitions survive the move
39D4 BEQ copy_next_byte ; ...skip the store for that page
39D6 LDA (copy_src),y ; Copy one byte...
39D8 STA (copy_dst),y ; to the destination
39DA .copy_next_byte←1← 39D4 BEQ
INY ; Next byte...
39DB BNE copy_page ; ...to the end of the page
39DD INC copy_src_hi ; Next source page...
39DF INC copy_dst_hi ; ...and destination page
39E1 DEX ; one fewer page
39E2 BNE copy_page ; ...until all pages copied
39E4 .copy_remainder←1← 39CE BEQ
LDX copy_rem ; Any leftover bytes? (none here)
39E6 BEQ copy_done ; none -> done
39E8 .copy_remainder_byte←1← 39EE BNE
LDA (copy_src),y ; Copy one byte...
39EA STA (copy_dst),y ; to the destination
39EC INY ; next byte
39ED DEX ; one fewer
39EE BNE copy_remainder_byte ; loop
39F0 .copy_done←1← 39E6 BEQ
RTS ; Done
39F1 .oscli_command
EQUS "T." ; Startup *-command "T." (*TAPE): select the cassette filing system, CR-terminated
39F3 EQUB &0D
39F4 .autorun_commands←2Used as index base by← 396C LDA← 398F LDA
EQUB &15, &0D ; CTRL-U + CR: clear the input line
39F6 EQUS "PA.=&C00" ; set PAGE to &0C00, where the BASIC now lives
39FE EQUB &0D ; submit
39FF EQUS "OLD" ; reinstate the decoded program (OLD)
3A02 EQUB &0D ; submit
3A03 EQUS "RUN" ; run it (RUN)
3A06 EQUB &0D, &00 ; submit; the &00 then stops the queue copy
; Uninitialised tail of the saved image. The file is a memory ; dump &1900-&3A80 (the BASIC clears exactly TO &3A80); the ; loader's code and tables end ~120 bytes short, so this holds ; whatever occupied the memory at save time. It is referenced by ; nothing and decodes as neither 6502, tokenised BASIC (raw or ; bit-rotated), nor text; its byte statistics are those of ; noise. Inert: the BASIC memory-clear overwrites it at startup.
3A08 .trailing_data
EQUB &0D, &83, &FC, &A6, &9A, &0E, &EF, &7C, &D1, &53, &48, &0A, &DF, &19, &6B, &A3, &C5, &A4, &06, &E7, &F8, &19, &48, &DA, &AC, &0D, &D7, &7F, &BB, &95, &91, &6B, &05, &C7, &E8, &5F, &61, &3B, &08, &08, &84, &CA, &16, &93, &A6, &D3, &CF, &15, &1A, &51, &34, &6F, &E4, &40, &F0, &19, &BA, &96, &05, &DF, &FD, &73, &81, &03, &CF, &D8, &09, &FC, &48, &31, &0A, &67, &EB, &BC, &3A, &0D, &29, &34, &D7, &11, &E9, &6B, &D0, &32, &A0, &34, &38, &0D, &5E, &8B, &9F, &63, &35, &41, &4B, &8C, &2B, &FC, &01, &19, &16, &2D, &E0, &39, &C7, &E0, &8B, &36, &53, &A2, &AC, &B4, &25, &A8, &21, &5D, &1C, &5D, &35, &2F