Mini Micro and the 6502: Adding Keyboard Input
BibleClinger
Posted on March 16, 2024
As I wrote last time, I had decided to embark on the journey of writing a 6502 microprocessor emulator for the Mini Micro with the intent to try to make a Pong clone written in 6502 assembly. I got as far as being able to write a working "Hello, World!" program in 6502 assembly -- a task that took me 25 lines just in 6502 assembly code alone!
I improved my Hello World example to print the string directly. That is useful, but we already have text output. What we really need right now is input. Let's add some keyboard support.
Mini Micro API
Before we can do that, let's examine the Mini Micro keyboard API, which is written entirely in the fantastic language called MiniScript. We can see from the MiniScript wiki that we have the key
class, which lets us obtain keyboard input. Fantastic.
Here are the two functions we'll want to look at first:
key.available
key.get
key.available
returns 1 if we have input in the buffer, and 0 otherwise.
key.get
returns the next character in the input buffer, but it blocks if no input is immediately available.
In my last post, we discussed memory mapping, which is how the 6502 talks to hardware. We can map an address to each of the above functions. Let's map $4101 to key.available
and $4102 to key.get
. This means if we read from $4101 in assembly, we'll get the result of key.available
, and if we read from $4102 in assembly, we'll get the result of key.get
.
The BIT Op Code
Loading the value of $4101 via lda $4101
takes 4 CPU cycles, and it overwrites what is already inside A. Overwriting A is not always ideal. Thankfully, the 6502 has a way of being slightly more efficient.
The BIT opcode allows us to examine bits 7 and 6 of any value stored in memory without actually loading it first into the accumulator. This means we should map key.available
specifically to bit 7 (or 6, if we preferred) of $4101 to take advantage of this.
Reading Input
Let's assign names to the addresses $4101 and $4102. Let's call them KEYCTRL and KEYGET respectively. Our assembly code for polling for the next character can look like this:
read:
bit KEYCTRL ; $4101
bpl read
lda KEYGET ; $4102
The MiniScript code for this project is already rather lengthy, so we'll just look at some snippets.
Here's the code for reading KEYCTRL ($4101):
KeyboardIO.getKeyCtrl= function(self, memoryAddress, value)
self.cpu.writeMemory(memoryAddress, key.available * 127, true) // Note: the result goes to bit 7
end function
What about our code for reading KEYGET ($4102)? I was advised on the MiniScript Discord that it would not be in the spirit of the 6502 to block for input, as key.get
does when it doesn't have any input available. As a result of conforming to this design philosophy, here's the MiniScript code for reading KEYGET ($4102):
KeyboardIO.getKey = function(self, memoryAddress, value)
if key.available then self.cpu.writeMemory(memoryAddress, key.get.code % 256, true)
end function
When KEYGET ($4102) is read by the assembly environment, then if and only if key.available
returns 1 does the value of key.get
become converted into an integer and written to memory for the assembly program to read. Otherwise, the function returns immediately without changing anything, and the assembly program will read whatever was left in KEYGET ($4102) prior to this moment -- which could be the last key entered. If I were to document this behavior in a manual, I would probably write something like, "If the 6502 assembly program reads from KEYGET ($4102) before KEYCTRL ($4101) indicates that there is valid input available, the value read from KEYGET ($4102) is undefined."
Echo
Now I can write a program that takes input from the user and echoes it back to the screen until the user presses the Escape key. Here is the relevant assembly:
KEYCTRL = $4101
KEYGET = $4102
PRINT_BYTE = $4001
PRINT_CHAR = $4002
TEXTDELIM = $4003
STRING_OUT_LOW = $4004
STRING_OUT_HIGH = $4005
ESC = $1B
.CODE
prompt: .byte "Welcome to echo! Type away!", 13, "Press ESC when done...", 13, 13, 0
.proc reset
lda #<prompt
sta STRING_OUT_LOW
lda #>prompt
sta STRING_OUT_HIGH ; Print prompt
ldy TEXTDELIM
ldx #$00
stx TEXTDELIM ; Save old text.delimiter
read:
bit KEYCTRL ; Is key.available true?
bpl read
lda KEYGET ; Read key
cmp #ESC ; Compare it to ESC press
beq done
sta PRINT_CHAR ; Print character to screen
jmp read
done:
sty TEXTDELIM ; Restore old text.delimiter
sty PRINT_CHAR ; Adds an extra char. This should be char(13) normally.
brk ; Note, we don't RTS out of reset
.endproc
.segment "VECTORS"
.word reset
.word reset
.word reset
Here's a screenshot of the Echo program in action:
Character Encoding
Without getting into too much detail on character encoding, suffice it to say that characters are encoded here using plain numbers.
Notice how A = 2E in the upper right hand corner of the screenshot. This debug view into the emulation of the 6502 microprocessor shows us the value of the registers in hexadecimal representation.
Since we're performing lda KEYGET
every time we have an available key, we're effectively loading the numerical representation of the last character typed into A. Let's look at an ASCII chart, and see if this makes sense. We can see in the chart that hexadecimal 2E, is the numerical representation of the period character. This is, in fact, the last character I had typed in the above screenshot.
A Gif is worth a Million Words
It's more fun to watch this happen live. Here's a gif image of the program execution. Watch how A is always changing to reflect the character typed last. Watch how PC, which is the program counter, is regularly trapped in the loop to bit KEYCTRL
until input is available.
At the end of the program when BRK is executed, A is shown to be 1B in hexadecimal. This is because the last character read from KEYGET was in fact the Escape key which has a hexadecimal value of 1B (which is 27 in decimal). Nothing else overwrote the A register before the BRK caused the program to terminate, so it stays the same.
Final Thoughts
I have a model to connect Mini Micro functionality to the assembly world. My two enemies now are complexity and performance, as I try to map the graphics in an efficient manner. If I can fend off these opponents, I'll have made major progress toward my goal of making a Pong clone in 6502 assembly for the Mini Micro.
Posted on March 16, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.