Veronica – Keyboard

Some Input to go with all that Output.

There are moments in a big project like this that really stand out. Those moments where it all seems to come together, and…. magic happens. This is one of those moments. After some rough times on Veronica as of late, it was great to have a win. This is going to be a long piece, so grab a nice local microbrew and get comfortable.

As you’ve already surmised from the title, it was time for Veronica to get some input, in the form of a keyboard. My goal throughout has been for Veronica to get along in the real modern world. That’s why I put all that effort into VGA, even though composite video would have been easier. Composite displays are just getting too hard to find. As I’ve said before, Veronica is basically being surrounded in a “1980s bubble” created by fancy interfaces to convert modern tech down to her level. The keyboard interface is another great example of that. In this spirit of getting along, I wanted to support modern USB keyboards. Most homebrew projects go with a PS/2 keyboard, or a serial terminal, or something custom, because those are all easier. The 1980s home computers had it “easy” in a sense, because they all had custom keyboards built in, so the designers could make them produce whatever signals were most convenient. We have no such luxury here.

I also decided I wanted to use the 6522 Versatile Interface Adapter (VIA). This is a general purpose I/O chip that was part of the original family of 65xx chips. It’s designed to work with the 6502, and it’s “period correct”, if you like. I picked up a nice set of W65C22S chips from Western Design Center, who still makes this modernized version. So, the challenge boils down to getting a USB keyboard from 2013 to talk to an I/O chip from the 1970s. Never a dull moment on Veronica.

I started by researching USB hosting, and what it takes to implement that. It turns out, quite a bit. USB is a very complex protocol, and while implementing a peripheral is fairly straightforward (nice single-chip solutions exist), implementing a host is considerably more involved. I found some expensive multi-chip solutions, but it wasn’t going to be easy. Then I stumbled on to a great alternative, as long as you only want a keyboard (as opposed to general USB connectivity).

It turns out that, thanks to globalization, virtually every USB keyboard out there uses the same USB keyboard controller chip. That chip has legacy goo in it for those old USB-to-PS/2 adapters that every PC in the 1990s came with. During that transition period from PS/2 to USB, this backward compatibility was essential, and it turns out the chip makers never bothered to remove it. I’m told some hardcore gamers still use these, because apparently PS/2 keyboards have some superior characteristics with regards to handling multiple keys being down at once, compared to USB.

Anyways, what this means is you can basically implement the much simpler PS/2 keyboard protocol through a female Type A USB connector, and it will just work. Of course, you end up with a “Keyboards Only” USB port, and chaos might ensue if you tried to plug anything else into it. For us homebrewers, though, that’s fine. The main thing is, it achieves my goal of being able to pull a modern USB keyboard off the shelf, plug it in, and have it work. There’s a trivial trick you need to do to force the keyboard in PS/2 compatibility mode. I’ll explain that in a minute.

So, with that USB problem “solved”, it was time to figure out the PS/2 protocol. There are some great resources out there on this. My favorite is the OSDev Wiki. The executive summary is that each keypress sends an 8-bit scan code to the host when it goes down. When the key comes back up, it sends a special prefix byte (usually $F0), followed by the scan code again. There are also some more exotic packets for special keys, but we can safely ignore them. The nice thing about the PS/2 protocol is that it degrades gracefully. Many special keys, for example, are two byte packets prefixed by $E0. If you simply ignore that prefix byte, the scan code will be interpreted as the second byte of the packet, which will map harmlessly to some other key. That won’t mess up any internal state in your driver (if you code it carefully), it just means the Page Up key might return a ‘9’.

One minor catch with all this- PS/2 devices are serial devices. That means they send 11-bit RS-232 style packets that need to be stripped of all their extraneous bits. We also need to be aware of what happens when the keyboard is first connected. The USB chip in the keyboard sends out a bunch of negotiation bytes. The handy part is, it if doesn’t get the proper replies to those bytes (as per the USB protocol), it falls back to PS/2 compatibility mode. So, that “trick” I mentioned earlier is simply “inaction”. That’s good, because inaction is something I’m really great at. If doing nothing was a professional sport, I’d be internationally ranked.

The point of all that is, we have a few housekeeping tasks we need to perform on the data coming in from the USB connector. When I see a job like this, I think microcontroller. I’m a big fan of the ATTiny13, and I tend to throw handfuls of them at the breadboard until the problem goes away.

To test this approach, I set up a breadboard with the USB connector, an ATTiny to read the data from it, and a shift register to push the bytes out to HexOut. That would allow me to see exactly what the keyboard spits out, and how I can work with that data. Connecting to USB is easy- it’s just power, ground, D-, and D+. In PS/2 compatibility mode, the keyboard uses D- for data, and D+ for clock. New Egg sells a really nice USB socket that ends in a 0.1″ header. It’s perfect for breadboarding USB experiments.

The keyboard, being an input device, might start clocking data out at any time. That means we need to respond with an interrupt handler on the ATtiny, so we don’t miss anything. Here’s the code that I came up with to do that:

/* Name: main.c * Author: Quinn Dunki * Copyright: ©2012 Quinn Dunki * License: All Rights Reserved * * ATtiny13A code to read a USB keyboard, wired * in PS/2 compatability mode * For details, see http://www.quinndunki.com/blondihacks */ #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> // Some generally useful macros #define QDDRB(n) (1<<(DDB##n)) #define QPINBIT(n) PB##n #define READPIN(pin) ((PINB & (1<<(QPINBIT(pin)))) > 0) #define SET_HI(pin) PORTB |= (1<<(QPINBIT(pin))) #define SET_LO(pin) PORTB &= ~(1<<(QPINBIT(pin))) #define PULSE(pin) SET_HI(pin); SET_LO(pin); // Pin assignments #define USB_DATA 0 // Data line from keyboard #define USB_CLK 1 // Clock signal from keyboard #define SHIFT_DS 2 // Data line for shift register #define SHIFT_CLK 3 // Clock line for shift register #define VIA_INT 4 // Port A interrupt line for 6522 VIA int gBitIndex = 0; int gFallingEdge = 1; // Main loop int main(void) { // Configure pins 2,3 and 4 as outputs. DDRB = QDDRB(2) | QDDRB(3) | QDDRB(4); SET_HI(USB_DATA); // Enable pull-up on USB data line SET_HI(USB_CLK); // Enable pull-up on USB clock line SET_LO(VIA_INT); // Configure INT0 (interrupt 0, on line PB0) to be a falling // edge interrupt that we'll get from the keyboard // See ATmega13A datasheet p45 MCUCR |= (1<<ISC01); MCUCR &= ~(1<<ISC00); GIMSK |= (1<<INT0); SET_LO(SHIFT_CLK); sei(); while (1) { } return 0; } ISR(INT0_vect) { static uint8_t buffer = 0; if (gFallingEdge) { if ( gBitIndex > 0 && gBitIndex <= 8 ) // Ignore start, parity, and stop bits { buffer >>= 1; // Make room for the next bit uint8_t bit = READPIN(USB_DATA); if (bit) { buffer |= 0x80; // Set the new bit } } // Now watch for rising edge of clock MCUCR |= (1<<ISC00); gFallingEdge = 0; } else { gBitIndex++; if (gBitIndex==11) { // We've received the whole packet, so push the data byte // out to the shift register int i; for (i=7; i>=0; i--) { SET_LO(SHIFT_DS); if (buffer & (1<<i)) { SET_HI(SHIFT_DS); } PULSE(SHIFT_CLK); } PULSE(SHIFT_CLK); // Extra pulse because shift register clocks are tied together // Let the VIA know we have a byte for it PULSE(VIA_INT); // Clean up and prepare for next byte buffer = 0; gBitIndex=0; // Debounce _delay_ms(1); } // Now watch for falling edge of clock MCUCR &= ~(1<<ISC00); gFallingEdge = 1; } }

That code sets up an interrupt handler that listens for clocks from the USB connector, then buffers up 11 bits, strips off the start and stop bits, and holds the remaining 8 data bits. The code then uses a couple of other pins on the ATtiny to manage a 75HC595 shift register, which is used to convert this serial data in an 8-bit byte usable by the 6522 VIA. The ATtiny is acting as the glue and signal converter between the USB and the 6522.

So, now that I can get data to the VIA, the next piece of the puzzle is interfacing the VIA to the 6502. This could not be easier, since they are designed to work together. If you’ve worked with microcontrollers, it will seem very familiar. The VIA provides “Ports”, timers, and other facilities, which are all memory mapped into the 6502’s address space wherever you like. The interface uses Data Direction Registers and such, just like a microcontroller. I just need to connect up my shift register to provide the keyboard data, add a little address decoding, and throw the whole thing on Veronica’s main bus. Here’s the circuit I came up with to do that.

Note that I could not find an Eagle symbol for the 6522, so I made one. Here it is, free to use, if you like. The schematic above is a work-in-progress, hence a few missing details like decoupling caps and power/ground connections. The VIA has a lot more room for other I/O on it, so I’ll be adding more stuff to this before I finish it up and etch a PCB.

The address decoding is handled by a single 74HC688 8-bit comparator. All addresses in the $E0 page are reserved for I/O in Veronica’s memory map, so this is a nice one-chip way to do decoding. The low address bits are routed to the 6522’s internal address bus, which is how you map its registers into the 6502’s address space. A very elegant design by those wily WDC engineers.

With that design figured out, it was a simple matter of prototyping it on the breadboard and working the kinks out.

When the ATtiny has received a byte from the keyboard, it presents it to the 6522 (using the ‘595), then signals the data is ready using the 6522’s CA1 line. The 6522 datasheet explains very well how to do all this. When the 6522 receives the keyboard byte, it signals the 6502 with an interrupt. So, the next thing we need is an interrupt handler in Veronica’s ROM.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Veronica CPU interrupt handler ; ; CPU Interrupt entry point: $F000 ; ; cpuInterrupt: pha ; Save registers cpuInterruptKeyboard: lda $e001 ; Fetch keyboard scan code cmp #$e0 ; This code prefixes extended stuff we can ignore beq cpuInterruptDone sta KBDINTBUF cpuInterruptDone: lda #$7f ; Clear VIA IFR sta $e00d pla ; Restore registers rti

The secret to a good interrupt handler is speed- get in, do your business, and get out. In this case, all I do is grab the byte and buffer it for processing elsewhere.

To debug this, I wrote a routine to print out the hex values that were landing in the buffer (put there by the above interrupt handler). The final result is shown below. Protip- change the quality setting to 720 on these videos. Otherwise you won’t be able to read the screen.

This is a good point to mention what a joy it is to have full character video output on Veronica. It’s a huge milestone when you can debug things using “print” statements instead of having to bust out the logic analyzer, or do a lot of trial and error. There were moments while debugging this keyboard driver that I started to feel like a software engineer again.

In the above video, I’m pushing my favorite key, ‘Q’. You can see it display the three-byte sequence I described earlier- the scan code ($15 for ‘Q’), followed by the “key going up” marker ($F0), followed by the scan code again. Success!

At this point, we can figure out what kind of keyboard we have. It turns out there are three different kinds of PS/2 keyboards, with three slightly different sets of scan codes. Likely, it seems that 95% of the worlds’ keyboards are the same- Type 2. Here’s the scan code list for a Type 2 PS/2 keyboard. I have no idea how all this applies to non-North-Amercan keyboards, mind you, so your mileage may vary.

Once I can read the scan code sequences correctly, the next step is to track the state of all the keys. The keyboard tells you about events (things going up or down). It’s up to the software to keep track of what state everything is in. For that, I wrote some code to track all the keys in a big bit array. It sets a bit for each key when a scan code comes in, to indicate it is down. When an $F0 code comes in, it sets a flag to note that the next scan code will be an “up” event. When the next code comes in, it clears the appropriate bit in the array.

;;;;;;;;;;;;;;;;;;;;;;; ; processKeyboard ; Processes keyboard data that was fetched via IRQ ; Important- additional IRQ may occur during this routine! ; Args: none ; processKeyboard: pha ; Save registers txa pha tya pha lda $00 ; Save zero page pha lda $01 pha lda $04 pha ldx KBDINTBUF cpx #0 beq processKeyboardDone lda #0 ; Clear the buffer if we pulled from it sta KBDINTBUF ; Determine which byte in the keyboard state array we need txa lsr lsr lsr tay ; Leave the byte index in Y until later ; Determine which bit within that byte we need txa pha ; We'll need the original keycode again later, so stash it sta $00 lda #$8 sta $01 jsr mathMod8 lda #$1 ldx $04 clc processKeyboardMaskLoop: beq processKeyboardMaskDone rol A dex jmp processKeyboardMaskLoop processKeyboardMaskDone: tax ; Determine if we need to set or clear the key state lda #$01 ; If bit $f0 is set, then clear the key state bit KBDSTATE+$1e bne processKeyboardUp ; Set the key's bit in the state byte using the mask we calculated txa ora KBDSTATE,y ; Y still holds the byte index sta KBDSTATE,y ; Buffer the keystroke for input routines, if needed pla ; We stashed the original keycode earlier cmp #$58 ; Check for Caps Lock beq processKeyboardCaps cmp KEYCODE_MAX_ASCII bcs processKeyboardDone sta KBDBUF jmp processKeyboardDone processKeyboardCaps: lda KBDFLAGS eor #KBDFLAG_CAPS sta KBDFLAGS jmp processKeyboardDone processKeyboardUp: pla ; Won't need that stashed value after all txa eor KBDSTATE,y sta KBDSTATE,y lda #$01 ; Clear the $f0 flag eor KBDSTATE+$1e sta KBDSTATE+$1e processKeyboardDone: pla ; Restore zero page sta $04 pla sta $01 pla sta $00 pla ; Restore registers tay pla tax pla rts

That code runs any time the system is waiting for user input. The interrupt handler is running all the time, pulling bytes off the 6522 VIA and jamming them into a holding area. If that processKeyboard routine isn’t called regularly, keys will be missed (which doesn’t actually matter if the software isn’t expecting input at the time). You might think all that processing could be done in the interrupt handler, but it’s too slow (I know because I tried). The interrupt handler has to be fast enough that it can respond to repeated interrupts coming from the VIA at the maximum data rate of the keyboard (which is pretty brisk). If the interrupt handler is too slow, it starts dropping the ends off the multi-byte commands. That causes the key state array to get of sync with the keyboard, because “key up” events were missed. This makes a real mess, as you might imagine, and made for some debugging challenges.

Speaking of debugging, I wrote a display routine to render the keyboard state array for that purpose. Here I am pushing that ‘Q’ again.

You can see the bit for ‘Q’ go high when the first scan code comes in. Then, the $F0 code comes in as the key is released. This sets a special flag bit at the end of the array. Then, when the next ‘Q’ scan code comes in, the ‘Q’ bit is cleared again. Huzzah!

The final piece of this puzzle is a bunch of grunt work. You see, keyboard scan codes aren’t particularly useful. What software really wants is ASCII characters, and some state bits for shift keys and such. To get to that point, we need to track states for modifier keys, manage the caps lock key, and use some lookup tables to convert scan codes to ASCII codes. Here’s a sample of one of the tables I wrote to do that:

scanCodeMatrix: .byte $00,$00,$00,$00,$00,$00,$00,$00 ; 07 v .byte $00,$00,$00,$00,$00,$09,'`',$00 ; 0f v .byte $00,$00,$00,$00,$00,'q','1',$00 ; 17 v .byte $00,$00,'z','s','a','w','2',$00 ; 1f v .byte $00,'c','x','d','e','4','3',$00 ; 27 v .byte $00,' ','v','f','t','r','5',$00 ; 2f v .byte $00,'n','b','h','g','y','6',$00 ; 37 v .byte $00,$00,'m','j','u','7','8',$00 ; 3f v .byte $00,',','k','i','o','0','9',$00 ; 47 v .byte $00,'.','/','l',';','p','-',$00 ; 4f v .byte $00,$00,$27,$00,'[','=',$00,$00 ; 57 v .byte $00,$00,$0d,']',$00,$5c,$00,$00 ; 5f v .byte $00,$00,$00,$00,$00,$00,$08,$00 ; 67 v .byte $00,'1',$00,'4','7',$00,$00,$00 ; 6f v keypad .byte '0','.','2','5','6','8',$1B,$00 ; 77 v keypad .byte $00,'+','3','-','*','9',$00,$00 ; 7f v keypad .byte $00,$00,$00,$00,$00,$00,$00,$00 ; 87 v

Then there’s a bunch of fiddly state and flag management code that combines those tables with the keyboard state array we set up earlier, to generate actual characters:

;;;;;;;;;;;;;;;;;;;;;;; ; scanCodeToASCII ; Return ASCII value of a keyboard scancode, if any ; Args: $00: Scan code byte ; Out: $04: Corresponding ASCII code, or 0 if none scanCodeToASCII: pha ; Save registers tya pha ; Check for modifier keys lda #$04 ; Left shift bit KBDSTATE+$02 bne scanCodeToASCIIShift lda #$02 ; Right shift bit KBDSTATE+$0b bne scanCodeToASCIIShift lda #$10 bit KBDSTATE+$02 ; Control bne scanCodeToASCIIControl lda #$01 bit KBDSTATE+$02 ; Alt bne scanCodeToASCIINull ldy $00 lda scanCodeMatrix,Y sta $04 jmp scanCodeToASCIIProcess scanCodeToASCIIShift: ldy $00 lda scanCodeMatrixShift,Y sta $04 jmp scanCodeToASCIIProcess scanCodeToASCIIControl: ldy $00 lda scanCodeMatrixControl,Y sta $04 jmp scanCodeToASCIIProcess scanCodeToASCIINull: lda #0 sta $04 scanCodeToASCIIProcess: ; Apply Caps Lock, if needed tax lda #KBDFLAG_CAPS bit KBDFLAGS beq scanCodeToASCIIDone cpx #'a' bcc scanCodeToASCIIDone cpx #'{' bcs scanCodeToASCIIDone txa clc sbc #31 ; Depromote lower case sta $04 scanCodeToASCIIDone: pla ; Restore registers tay pla rts

Then putting alllll that stuff together, we can write an actual input routine, that polls the keyboard buffer and displays the keystrokes:

waitForInput: jsr processKeyboard jsr getCh ldx $04 beq waitForInput lda #PLOTSTR sta GPUREG txa sta GPUREG jmp waitForInput ;;;;;;;;;;;;;;;;;;;;;;; ; getKey ; Read a scan code from the keyboard ; Args: None ; Out: $04: Pending keystroke, or 0 if none getKey: pha ; Save registers lda KBDBUF sta $04 beq getKeyDone lda #0 sta KBDBUF ; Clear keyboard buffer for next time getKeyDone: pla ; Restore registers rts ;;;;;;;;;;;;;;;;;;;;;;; ; getCh ; Read an ASCII character from the keyboard ; Args: None ; Out: $04: Pending character, or 0 if none getCh: pha ; Save registers jsr getKey lda $04 cmp KEYCODE_MAX_ASCII bcs getChNull sta $00 jsr scanCodeToASCII jmp getChDone getChNull: lda #0 sta $04 getChDone: pla ; Restore registers rts

Now, the money shot:

My text generator still needs some work- especially the lowercase. But hey, it’s moderately legible.

As I said at the top of the piece, there are a few magic moments in a big project like this. Moments where all the dozens of layers of electronics and protocols and interfaces and software all come together and make something that looks and feels like…. a computer. These are the moments that keep me going through the really hard parts.

Magic, indeed.