DEV Community


Posted on

Mini Micro and the 6502: Adding Keyboard Input

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:

bit KEYCTRL        ; $4101
bpl read
lda KEYGET         ; $4102
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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."


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

ESC = $1B


prompt: .byte "Welcome to echo! Type away!", 13, "Press ESC when done...", 13, 13, 0

.proc reset
    lda #<prompt
    lda #>prompt
    sta STRING_OUT_HIGH     ; Print prompt

    ldx #$00
    stx TEXTDELIM           ; Save old text.delimiter

    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

    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

.segment "VECTORS"
.word reset
.word reset
.word reset
Enter fullscreen mode Exit fullscreen mode

Here's a screenshot of the Echo program in action:

Output of the Echo program in action
Output of the Echo program

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.

GIF Output

Echo output: A and PC are of particular interest

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.

Top comments (0)