Using a Family BASIC Keyboard to type text on the Nintendo Entertainment System

Posted on

Table of contents

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
Routing to read data from the keyboard

Reading out the keys will result in the following table:

Key matrix, based on the matrix from nesdev wiki
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 :-
Setting the handled state

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
Checking if it has been handled before

4 Structure of tiles

A character like 'a' is syntax sugar for $61. Those numeric values can be used to index all the characters.

Tiles
$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
Lookup table structure

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
	;...
Full lookup table

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
	;...
The structure of the lookup table

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
Reading a key

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 #$20 to get #'a' #'z'
  • For numbers #'1' to #'9', if shift is pressed we need to subtract #$10 to get #'!' to #')'
  • For symbols #',' to #'/', if shift is pressed we need to add #$10 to 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
Reading shift

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
The printable key loop

Now we add the shift handling:

@checkDigit:
			ldx shiftPressed
			beq @printKey
			cmp #'1'
			bmi @printKey
			cmp #'9'+1
			bcs @printKey

			sec
			sbc #$10
Adding shift

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
Moving to the right until we are at the next line

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
WhatToDraw

I 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"
main.asm
.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
header.asm
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
constants.asm
.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
variables.asm
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
reset.asm
step:
	@maintainFps:
		lda drawOccured
		beq step

		ldx #$00
		stx drawOccured

	jsr readKeyboardData
	jsr readKeys

	clc
	jmp step
step.asm
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
draw.asm
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
keyboard.asm
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:
palettes.asm
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;
}
config.cfg, taken from bbbradsmiths NES-ca65-example
#!/usr/bin/env sh
~/.local/bin/ca65 main.asm -o main.o
~/.local/bin/ld65 -o main.nes -C config.cfg main.o
assemble.sh

12 Sources