1 Background
Aside from the standard controller, there are plenty of other input methods for the NES. One of them is the Family BASIC Keyboard.
I didn't know about this, got really curious how it works, and decided to write some code to test it out. The code is set up like this
- Reading out keyboard data
- Putting it on a buffer
- Taking it from the buffer and showing it on screen during NMI
Disclaimer: I haven't tested this on real hardware, only on emulators.
Disclaimer 2: The code is not for a full feature text editor. It doesn't even support the backspace. Only typing text, handling newlines and handling the shift keys.
2 Reading the keyboard data
In order to get the pressed keys, we have to write to KEYBOARD_INPUT so we can get the information we need afterwards from KEYBOARD_OUTPUT.
There are three bits that KEYBOARD_INPUT needs:
- Bit 3, which should always be
1. - Bit 2, which specifies which bits of the key byte we want. 0 for bits 8, 7, 6 and 5; 1 or bits 4, 3, 2 and 1. The first write should be 0, the second write should be 1.
- Bit 1, which resets the input
When we read KEYBOARD_OUTPUT it will store the 4 bits in bit 5, 4, 3 and 2.
Why KEYBOARD_OUTPUT doesn't just output all the bits in one time in the correct place, i don't know.
Since the result from KEYBOARD_OUTPUT is always stored on the same bits of the returned byte, we have to shift it three times to the left first, and then the second time we have to shift right one time.
The bits that are 1 mean the key is not pressed, the ones that are 0 mean the key is pressed. We store it in 9 bytes, labeled by keys.
readKeyboardData:
lda #%00000101
sta KEYBOARD_INPUT
ldx #$00
:
lda #%00000100
sta KEYBOARD_INPUT
lda KEYBOARD_OUTPUT
asl A
asl A
asl A
and #$F0
sta keys, x
lda #%00000110
sta KEYBOARD_INPUT
lda KEYBOARD_OUTPUT
lsr A
and #$0F
ora keys, x
sta keys, x
inx
cpx #$09
bne :-
rts
Reading out the keys will result in the following table:
| bit 8 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | |
|---|---|---|---|---|---|---|---|---|
| byte 0 | ] | [ | RETURN | F8 | STOP | ¥ | Right SHIFT | KANA |
| byte 1 | ; | : | @ | F7 | ^ | - | / | _ |
| byte 2 | K | L | O | F6 | 0 | P | , | . |
| byte 3 | J | U | I | F5 | 8 | 9 | N | M |
| byte 4 | H | G | Y | F4 | 6 | 7 | V | B |
| byte 5 | D | R | T | F3 | 4 | 5 | C | F |
| byte 6 | A | S | W | F2 | 3 | E | Z | X |
| byte 7 | CTR | Q | ESC | F1 | 2 | 1 | GRPH | Left SHIFT |
| byte 8 | ◀ | ▶ | ▲ | CLR HOME | INS | DEL | Space | ▼ |
3 Disabling repeat keys
We can store the pressed keys and prevent the keypress from going through if it has been handled before.
In order to do that we keep a second 8 bytes to store the keys, but this time 1 means we shouldn't handle the key again. Since 0 meant a key is pressed, we now eor #$FF that before storing it in the handled bytes.
We can fill it up at the end of handling all the keys.
@setHandledState:
ldx #$00
:
lda keys+Keys::Pressed, x
eor #$FF
sta keys+Keys::Handled, x
inx
cpx #$09
bne :-
Now, whenever we want to read a key, we first have to check if it has been handled before.
checkHandled:
lda keyLookupTable+Key::Row, y
clc
adc #Keys::Handled
sta lookup
ldx #$00
lda (lookup, x)
and keyLookupTable+Key::Column, y
rts
checkKey:
ldy #(lookupKEY-keyLookupTable)
jsr checkHandled
; rest of the logic
4 Structure of tiles
A character like 'a' is syntax sugar for $61. Those numeric values can be used to index all the characters.
| $0 | $1 | $2 | $3 | $4 | $5 | $6 | $7 | $8 | $9 | $A | $B | $C | $D | $E | $F | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| $20 | ! | ¨ | # | $ | % | & | " | ( | ) | . | + | , | - | . | / | |
| $30 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
| $40 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
| $50 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ |
| $60 | " | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
| $70 | p | q | r | s | t | u | v | w | x | y | z | { | | | } | ~ |
5 Mapping the keys to the tiles
We can map the keys to the tiles with a lookup table. This way we can iterate over the correct tile order and read out the buttons as needed.
The format of the lookup table will be as follows:
keyLookupTable:
key: $buttons+ROW, COLUMN
We don't want to iterate over all keys, so for the buttons we don't want to iterate over we have to add another label to say how far it should go.
keyLookupTable:
printableKey1: $buttons+ROW, COLUMN
printableKey2: $buttons+ROW, COLUMN
;...
keyLookupTablePrintableEnd:
nonPrintableKey1: $buttons+ROW, COLUMN
nonPrintableKey2: $buttons+ROW, COLUMN
;...
Since the printable characters start with a #' ' and end with a #'~' we can just list them in the printable part of the keyLookupTable and iterate over them, right? Well not all characters can be typed! Some of them can only be retrieved by pressing shift.
The last character that can be typed directly is #'_', so we can already make a list based on the structure of the tiles that can be typed. But we somehow have to pad the characters that cannot be typed directly.
An easy way to do that is adding a byte to the keys that is never pressed, that always has a value of #%11111111. We can call it neverPressed.
keyLookupTable:
;key byte bit
lookupSpace: .byte keys+$08, 1<<1
lookupExclam: .byte neverPressed, 1
lookupTrema: .byte neverPressed, 1
;...
lookupPlus: .byte neverPressed, 1
lookupComma: .byte keys+$02, 1<<1
;...
lookupClose: .byte keys+$00, 1<<7
lookupTop: .byte keys+$01, 1<<3
lookupUnder: .byte keys+$01, 1<<0
keyLookupTablePrintableEnd:
lookupRightShift:.byte keys+$00, 1<<1
lookupLeftShift: .byte keys+$07, 1<<0
;...
6 Reading a key
Now that we have our lookup table, we can start reading a certain key.
Since keys is in zero page we just load #$00 into X.
ldy #(lookupKEY-keyLookupTable)
readKey:
lda keyLookupTable+Keys::Pressed+Key::Row, y
sta lookup
ldx #$00
lda (lookup, x)
and keyLookupTable+Key::Column, y
handleKey:
bne notPressed
;If pressed
notPressed:
;If not pressed
7 Handling shift
Now that we can detect the keys that have been pressed directly, it would also be nice to detect the ones we could not directly type.
When we see a shift key has been pressed, we have to offset. The offsets are as follows:
- For letters
#'A'to#'Z', if shift is not pressed we need to add#$20to get#'a'#'z' - For numbers
#'1'to#'9', if shift is pressed we need to subtract#$10to get#'!'to#')' - For symbols
#','to#'/', if shift is pressed we need to add#$10to get#'<'to#'?'
Since there is no difference between a right or left shift, we can store the pressing of shift in shiftPressed and read that when we are reading the other keys.
Keep in mind we don't want to check the handled state of shift, since shift should always be active.
readShift:
ldy #(lookupLeftShift-keyLookupTable)
jsr readKey
beq @shiftPressed
ldy #(lookupRightShift-keyLookupTable)
jsr readKey
beq @shiftPressed
ldx #$00
stx shiftPressed
rts
@shiftPressed:
ldx #$01
stx shiftPressed
rts
Now that we know when shift has been pressed, we can check for it when other keys have been pressed. Lets try it for the numbers.
Normally it would look like this:
readKeys:
ldy #$00
sty whatToDraw+WhatToDraw::Count
@readPrintableKey:
jsr checkHandled
bne @nextKey
jsr readKey
bne @nextKey
tya
lsr A
adc #$20
; <-- Where we would like to check for shift
@printKey:
;...
@nextKey:
iny
iny
cpy #(keyLookupTablePrintableEnd - buttonLookupTable)
bne @readPrintableKey
Now we add the shift handling:
@checkDigit:
ldx shiftPressed
beq @printKey
cmp #'1'
bmi @printKey
cmp #'9'+1
bcs @printKey
sec
sbc #$10
The same, but with other offsets, should be done for some of the symbols and for the letters.
8 Handling return
Every line starts with #%XX100000, so by shifting three times to the left and checking if the result is 0, we can see if we are at the start of a line. When pressing return we have to increment the cursor until we reached the start of the line.
@moveCursorRight:
lda cursor
clc
adc #$01
sta cursor
lda cursor+1
adc #$00
sta cursor+1
lda cursor
asl A
asl A
asl A
bne @moveCursorRight
9 Printing the tiles
In order to really type the text, we need tell the PPU where to place the letters. For this we need a cursor and a buffer to store the tiles that the PPU should draw.
The cursor should be able to go from #$2000 to #$23BF, so we need to store it in a word. If you want to have scrolling, you probably want to use the third name table as well, but be careful with the attribute table and the entirety of the second name table, you would have to jump over that.
What needs to be stored for the PPU is the location, the amount of tiles that need to be places and the tiles themselves. The location is the cursor added to the location of the first name table.
.struct WhatToDraw
Count .byte
Location .word
Tiles .byte 10
.endstruct
WhatToDrawI assume at most 10 tiles will be inserted per frame. If that is not enough you will have to increase the storage of WhatToDraw::Tiles
10 Next steps
I was thinking about making this a text editor but that seems to be way to much work for something that no one will ever use. The following things would have to be added, in order of importance:
- Backspace. Emptying the buffer, setting the count to 1 and decreasing the cursor by one would probably be the easiest way, but you would lose key presses. I think a better way would be to change the buffering logic.
- Showing the cursor. This can be done with a sprite.
- Moving the cursor. Looking at the handled state would be annoying for arrow keys, but not having one would make the cursor go to fast. A timer should be kept to slow it down.
- Insert mode. If you move the cursor backwards now and start typing, it would overwrite the text already written. All that text would need to be shifted.
11 Full code
.include "header.asm"
.include "constants.asm"
.include "variables.asm"
.segment "CODE"
.include "reset.asm"
.include "step.asm"
.include "draw.asm"
.include "keyboard.asm"
.include "palettes.asm"
.segment "VECTORS"
.word draw
.word reset
.word $0000
.segment "TILES"
.incbin "tiles.chr"
.segment "HEADER"
.byte "NES", $1A ; identification of the iNES header
.byte $02 ; number of 16KB PRG-ROM pages
.byte $01 ; number of 8KB CHR-ROM pages
.byte $00 ; no mapper
.byte $00 ; horizontal mirroring
PAGE_ZERO = $0000
PAGE_STACK = $0100
PAGE_SHADOW_OAM = $0200
PPU_CONTROL = $2000
PPU_MASK = $2001
PPU_STATUS = $2002
PPU_OAM_ADDRESS = $2003
PPU_OAM_DATA = $2004
PPU_SCROLL = $2005
PPU_ADDRESS = $2006
PPU_DATA = $2007
PPU_OAM_DMA = $4014
PPU_PALETTE = $3F00
KEYBOARD_INPUT = $4016
KEYBOARD_OUTPUT = $4017
NAME_TABLE_TOP_L = $2000
NAME_TABLE_TOP_R = $2400
NAME_TABLE_BOT_L = $2800
NAME_TABLE_BOT_R = $2C00
ATTR_TABLE_OFFSET = $03C0
.struct Key
Row .byte
Column .byte
.endstruct
.struct Keys
Pressed .byte 9
Handled .byte 9
.endstruct
.struct WhatToDraw
Count .byte
Location .word
Tiles .byte 10
.endstruct
.segment "ZEROPAGE"
drawOccured: .res 1
keys: .tag Keys
cursor: .res 2
lookup: .res 2
neverPressed: .res 1
shiftPressed: .res 1
whatToDraw: .tag WhatToDraw
reset:
cld
sei
@initStack:
ldx #$FF
txs
@disablePPU:
ldx #$00
stx PPU_CONTROL
stx PPU_MASK
@initZeroAndStack:
lda #$00
:
sta PAGE_ZERO, x
sta PAGE_STACK, x
inx
bne :-
@initShadowOAM:
lda #$FF
:
sta PAGE_SHADOW_OAM, x
inx
bne :-
@setKeys:
sta neverPressed
ldx #$00
lda #%11111111
:
sta keys, x
inx
cpx #$08
bne :-
@waitVBlank:
bit PPU_STATUS
bpl @waitVBlank
lda PPU_STATUS
jsr loadPalettes
@enableNMI:
lda #%10000000
sta PPU_CONTROL
@setBackgroundRendering:
lda #%00001010
sta PPU_MASK
@executeFirstStep:
jmp step
step:
@maintainFps:
lda drawOccured
beq step
ldx #$00
stx drawOccured
jsr readKeyboardData
jsr readKeys
clc
jmp step
draw:
@saveToStack:
php
pha
txa
pha
@drawKeys:
lda whatToDraw+WhatToDraw::Count
beq @resetScroll
lda #>NAME_TABLE_TOP_L
clc
adc whatToDraw+WhatToDraw::Location+1
sta PPU_ADDRESS
lda whatToDraw+WhatToDraw::Location
sta PPU_ADDRESS
ldx #$00
:
lda whatToDraw+WhatToDraw::Tiles, x
sta PPU_DATA
inx
cpx whatToDraw+WhatToDraw::Count
bne :-
@resetScroll:
lda #$00
sta PPU_SCROLL
sta PPU_SCROLL
@flagDrawOccured:
lda #$01
sta drawOccured
@restoreFromStack:
pla
tax
pla
plp
rti
readKeyboardData:
lda #%00000101
sta KEYBOARD_INPUT
ldx #$00
:
lda #%00000100
sta KEYBOARD_INPUT
lda KEYBOARD_OUTPUT
asl A
asl A
asl A
and #$F0
sta keys, x
lda #%00000110
sta KEYBOARD_INPUT
lda KEYBOARD_OUTPUT
lsr A
and #$0F
ora keys, x
sta keys, x
inx
cpx #$09
bne :-
rts
readShift:
ldy #(lookupLeftShift-keyLookupTable)
jsr readKey
beq @shiftPressed
ldy #(lookupRightShift-keyLookupTable)
jsr readKey
beq @shiftPressed
ldx #$00
stx shiftPressed
rts
@shiftPressed:
ldx #$01
stx shiftPressed
rts
readReturn:
ldy #(lookupReturn-keyLookupTable)
jsr checkHandled
bne @done
jsr readKey
bne @done
@moveCursorRight:
lda cursor
clc
adc #$01
sta cursor
lda cursor+1
adc #$00
sta cursor+1
lda cursor
asl A
asl A
asl A
bne @moveCursorRight
@done:
rts
readKeys:
@setDrawLocation:
lda cursor
sta whatToDraw+WhatToDraw::Location
lda cursor+1
sta whatToDraw+WhatToDraw::Location+1
@resetWhatToDrawData:
ldy #$00
lda #$00
:
sta whatToDraw+WhatToDraw::Tiles, y
iny
cpy whatToDraw+WhatToDraw::Count
bne :-
@readSpecialKeys:
jsr readShift
jsr readReturn
@readPrintableKeys:
ldy #$00
sty whatToDraw+WhatToDraw::Count
@readPrintableKey:
jsr checkHandled
bne @nextKey
jsr readKey
bne @nextKey
tya
lsr A
adc #$20
@checkDigit:
ldx shiftPressed
beq @checkSymbol
cmp #'1'
bmi @checkSymbol
cmp #'9'+1
bcs @checkSymbol
sec
sbc #$10
@checkSymbol:
ldx shiftPressed
beq @checkLetter
cmp #','
bmi @checkLetter
cmp #'/'+1
bcs @checkLetter
clc
adc #$10
@checkLetter:
ldx shiftPressed
bne @printKey
cmp #'A'
bmi @printKey
cmp #'Z'+1
bcs @printKey
clc
adc #$20
@printKey:
ldx whatToDraw+WhatToDraw::Count
sta whatToDraw+WhatToDraw::Tiles, x
inc whatToDraw+WhatToDraw::Count
clc
lda cursor
adc #$01
sta cursor
lda cursor+1
adc #$00
sta cursor+1
@nextKey:
iny
iny
cpy #(keyLookupTablePrintableEnd - buttonLookupTable)
bne @readPrintableKey
@setHandledState:
ldx #$00
:
lda keys+Keys::Pressed, x
eor #$FF
sta keys+Keys::Handled, x
inx
cpx #$09
bne :-
rts
checkHandled:
lda keyLookupTable+Key::Row, y
clc
adc #Keys::Handled
sta lookup
ldx #$00
lda (lookup, x)
and keyLookupTable+Key::Column, y
rts
readKey:
lda keyLookupTable+Keys::Pressed+Key::Row, y
sta lookup
ldx #$00
lda (lookup, x)
and keyLookupTable+Key::Column, y
rts
keyLookupTable:
;https://www.nesdev.org/wiki/Family_BASIC_Keyboard#Matrix
;key byte bit
lookupSpace: .byte keys+$08, 1<<1
lookupExclam: .byte neverPressed, 1
lookupTrema: .byte neverPressed, 1
lookupHash: .byte neverPressed, 1
lookupDollar: .byte neverPressed, 1
lookupPercent: .byte neverPressed, 1
lookupAnd: .byte neverPressed, 1
lookupQuote: .byte neverPressed, 1
lookupOpenRound: .byte neverPressed, 1
lookupCloseRound:.byte neverPressed, 1
lookupBullet: .byte neverPressed, 1
lookupPlus: .byte neverPressed, 1
lookupComma: .byte keys+$02, 1<<1
lookupMinus: .byte keys+$01, 1<<2
lookupDot: .byte keys+$02, 1<<0
lookupSlash: .byte keys+$01, 1<<1
lookup0: .byte keys+$02, 1<<3
lookup1: .byte keys+$07, 1<<2
lookup2: .byte keys+$07, 1<<3
lookup3: .byte keys+$06, 1<<3
lookup4: .byte keys+$05, 1<<3
lookup5: .byte keys+$05, 1<<2
lookup6: .byte keys+$04, 1<<3
lookup7: .byte keys+$04, 1<<2
lookup8: .byte keys+$03, 1<<3
lookup9: .byte keys+$03, 1<<2
lookupColon: .byte keys+$01, 1<<6
lookupSemicolon: .byte keys+$01, 1<<7
lookupLighter: .byte neverPressed, 1
lookupEqual: .byte neverPressed, 1
lookupGreater: .byte neverPressed, 1
lookupQuestion: .byte neverPressed, 1
lookkupAt: .byte keys+$01, 1<<5
lookupA: .byte keys+$06, 1<<7
lookupB: .byte keys+$04, 1<<0
lookupC: .byte keys+$05, 1<<1
lookupD: .byte keys+$05, 1<<7
lookupE: .byte keys+$06, 1<<2
lookupF: .byte keys+$05, 1<<0
lookupG: .byte keys+$04, 1<<6
lookupH: .byte keys+$04, 1<<7
lookupI: .byte keys+$03, 1<<5
lookupJ: .byte keys+$03, 1<<7
lookupK: .byte keys+$02, 1<<7
lookupL: .byte keys+$02, 1<<6
lookupM: .byte keys+$03, 1<<0
lookupN: .byte keys+$03, 1<<1
lookupO: .byte keys+$02, 1<<5
lookupP: .byte keys+$02, 1<<2
lookupQ: .byte keys+$07, 1<<6
lookupR: .byte keys+$05, 1<<6
lookupS: .byte keys+$06, 1<<6
lookupT: .byte keys+$05, 1<<5
lookupU: .byte keys+$03, 1<<6
lookupV: .byte keys+$04, 1<<1
lookupW: .byte keys+$06, 1<<5
lookupX: .byte keys+$06, 1<<0
lookupY: .byte keys+$04, 1<<5
lookupZ: .byte keys+$06, 1<<1
lookupOpen: .byte keys+$00, 1<<6
lookupYen: .byte keys+$00, 1<<2
lookupClose: .byte keys+$00, 1<<7
lookupTop: .byte keys+$01, 1<<3
lookupUnder: .byte keys+$01, 1<<0
keyLookupTablePrintableEnd:
lookupRightShift:.byte keys+$00, 1<<1
lookupLeftShift: .byte keys+$07, 1<<0
lookupDown: .byte keys+$08, 1<<0
lookupUp: .byte keys+$08, 1<<5
lookupRight: .byte keys+$08, 1<<6
lookupLeft: .byte keys+$08, 1<<7
lookupReturn: .byte keys+$00, 1<<5
lookupEscape: .byte keys+$07, 1<<5
lookupGRPH: .byte keys+$07, 1<<1
lookupCRLHome: .byte keys+$07, 1<<7
lookupCtr: .byte keys+$07, 1<<7
lookupStop: .byte keys+$00, 1<<4
lookupDel: .byte keys+$08, 1<<2
lookupIns: .byte keys+$08, 1<<3
lookupF1: .byte keys+$07, 1<<4
lookupF2: .byte keys+$06, 1<<4
lookupF3: .byte keys+$05, 1<<4
lookupF4: .byte keys+$04, 1<<4
lookupF5: .byte keys+$03, 1<<4
lookupF6: .byte keys+$02, 1<<4
lookupF7: .byte keys+$01, 1<<4
lookupF8: .byte keys+$00, 1<<4
loadPalettes:
lda #>PPU_PALETTE
sta PPU_ADDRESS
ldx #<PPU_PALETTE
stx PPU_ADDRESS
:
lda palettes, x
sta PPU_DATA
inx
cpx #(palettesEnd - palettes)
bne :-
rts
palettes:
.byte $30, $1F, $1F, $1F
.byte $1F, $1F, $1F, $1F
.byte $1F, $1F, $1F, $1F
.byte $1F, $1F, $1F, $1F
.byte $30, $1F, $1F, $1F
.byte $1F, $1F, $1F, $1F
.byte $1F, $1F, $1F, $1F
.byte $1F, $1F, $1F, $1F
palettesEnd:
MEMORY {
ZP: start = $00, size = $0100, type = rw, file = "";
HDR: start = $0000, size = $0010, type = ro, file = %O, fill = yes, fillval = $00;
PRG: start = $8000, size = $8000, type = ro, file = %O, fill = yes, fillval = $00;
CHR: start = $0000, size = $2000, type = ro, file = %O, fill = yes, fillval = $00;
}
SEGMENTS {
ZEROPAGE: load = ZP, type = zp;
HEADER: load = HDR, type = ro;
CODE: load = PRG, type = ro, start = $8000;
VECTORS: load = PRG, type = ro, start = $FFFA;
TILES: load = CHR, type = ro;
}
#!/usr/bin/env sh
~/.local/bin/ca65 main.asm -o main.o
~/.local/bin/ld65 -o main.nes -C config.cfg main.o