Saturday, October 28, 2023

What the KIM-1 really needs is an LCD screen

Giving the 1976 1K RAM, 1MHz 6502-based KIM-1 single-board computer bubble memory storage was all well and good, but the basic unit is still just six numeric LEDs for its display. Let's solve that problem.
Here's a sidecar screen for my KIM-1 that's large enough to be useful, small enough to be portable, and efficient enough to be powered by my unit's built-in power supply. Plus, it can be driven by the KIM-1's unallocated I/O lines so that its 20mA current loop terminal interface remains available, with bonus points for being self-lit so you can see it like the LEDs. And the output driver takes up just 64 bytes of RAM.

Sure, you could hook up a serial terminal like the Silent 700 we used in that entry, but that means lugging around two units — which seems like a lost opportunity because this particular KIM-1 is completely self-contained with a power supply and I/O card in a hard plastic briefcase I can simply pick up and take with me. Plus, I can work just fine with a hex keypad and we've had predictive text on smaller phone keyboards for years, so even the Silent 700's keyboard isn't obligatory.

Some of you will have already exclaimed, "Build a TV Typewriter!" I do have Don Lancaster (rip)'s book for building the TVT 6 5/8 video system on anything with a 6800 bus, which the 6502 is infamously compatible with (and a big part of why Motorola sued MOS). However, I'd have to actually put it together and I'm lazy, and more to the point, to get a modest 32x16 screen requires giving up half (i.e., 32 times 16 = 512 bytes) of the 1K of RAM I have and tying up the CPU generating the screen with the memory that's left. Plus, once built and running, all the TVT gives me is a video interface; I still need a display to connect it to. I might do this as a future project but it doesn't really work within the constraints we've set here.

There are of course LCDs you can drive directly and some that can be controlled with I2C, but what makes the LCD device I've chosen nearly ideal is it maintains its own state so we don't have to constantly refresh it or dedicate system memory to it, and it can be programmed with a minimum of GPIO. While these sorts of devices are a bit more expensive than a bare LCD, they're a lot simpler to work with. In fact, this device can be controlled solely over a good old fashioned RS-232 serial link with just one of the KIM's I/O pins serving as a bitbanged serial output at 19.2kbps. That keeps our current loop interface available for uploading programs to the KIM monitor and requires only writing a driver for the serial output, sacrificing no other additional memory.

The particular one we're going to use is a 20x4 character backlit LCD panel sold as the Matrix Orbital LK204-25. These units are fairly common, not inordinately expensive (I got mine as NOS for about US$40), and still sold and supported along with a whole stable of RS-232 based displays they sell like this 7.0" 800x480 bitmapped TFT panel (these displays can also be controlled via USB, TTL and I2C). The serial protocol can be as simple as sending it characters to display or full special control sequences, just like any other terminal, and most of their devices have at least basic sequences in common. It's so well understood you can even emulate one with a Palm Pilot.
I'm chagrined to admit, as with so many other present-day retrocomputing peripherals, that the LK204-25's onboard microcontroller is substantially more powerful than the KIM-1's 6502. Matrix Orbital has made the interface sufficiently agnostic that they can change the architecture around as long as it still obeys the same control sequences, though anything they'd be running would still outclass the 6502 controlling it. Current examples appear to be using the ATmega8535, but this older NOS unit sports a PIC16F877A, a 20MHz part with eight kilowords of flash program memory, 368 bytes of data RAM and 256 bytes of EEPROM. It has five I/O ports, a USART and a master serial port driver configured to service I2C requests, sending its output to a Hitachi HD44780-style controller which actually manages the display. The LK204-25 can be rigged to run off a variety of voltages; we'll just stick to 5V, which we can get directly from our KIM-1's PSU.

The unit I got was an RS-232-based unit that you can just connect a straight-thru DE-9 serial cable to, but the KIM-1's GPIO pins are naturally TTL-level. While the LK204-25 presently comes in a TTL version, it was cheaper just to buy this older NOS one and get one of the cheapo MAX232/MAX3232 level shifters which can also be powered off 5V.

Note, however, that these older units may also have older firmware and might not support everything in later devices. If you're following along at home, you'll need at least a revision 1.23 device for these examples which is upwardly compatible with the 1.3 devices currently sold (it may work with older ones but I haven't tried).

The LK204-25 is additionally able to read a connected keypad (to the horizontal row of pins at the bottom left) and send values back to the host, as well as accept sequences to set the unit's General Purpose Output — not GPIO — pins in pairs on the middle left. We won't be using these features here; the KIM-1 can read its own keypad just fine, and it's faster to use the KIM's own GPIO pins if we need to. The jumper block at the top has the default jumper block settings for 19.2kbps (jumpers on J2 and J1), this device's fastest speed and how it comes from the factory.

To ensure the LK LCD was working, I fashioned up a tap from two jumper wires fed into the power supply's 5V line, and two more fed into the PSU's common ground. One jumper of each goes to the LK and to the level shifter.
With the power supply switched on, the LK204-25 displays its internal startup screen. Excellent!

The next step is the driver.

The KIM's two I/O chips are 6530 RRIOTs, standing for (mask) ROM, RAM, I/O and Timers. Each has two ports of eight I/O ports, each of which can be configured as input or output through the chip's data direction registers. These ports are exposed as pins on the KIM's card-edge application connector, which is the bottommost of the two. One RRIOT (6530-002) is allocated by default to the KIM's LEDs, keypad, tape connector and terminal interface, while the other one (6530-003) is available for user applications.

What we're essentially going to do with this driver is create a second serial port in software. I have one of Bob Applegate (rip)'s I/O boards plugged in, which makes this a bit easier as it exposes most of the application connector's edge lines as pin headers you can connect jumpers to. For this application we will use the free RIOT's PA0 pin as an output, which is located on pin A-14, and the common ground on A-1. We don't have to connect any other lines since we won't need any of the LK204-25's keypad reading features, so we don't need to accept input from it. As the KIM-1 is a 5V device, we'll jumper 5V and Vcc on this multivoltage USB-TTL dongle to signal we're using 5V TTL, and then connect the dongle's ground pin to A-1 and its RXD (receive) pin to A-14. While I ordinarily keep a jumper wire on the dongle's TXD (transmit) for convenience, it's not connected to anything here.

Due to various inefficiencies in the KIM ROM its internal serial routines are not reliable much above 300 baud, so we can't just use it directly. Instead, for the serial transmit driver we're going to dust off our 9600bps 8N1 (eight data bits, no parity, one stop bit) bitbanger we originally wrote for the serial-based DECtalk Express with two obvious necessary changes. The first is we need to double our send rate by reducing the delay between bits (at exactly 1 million clock cycles per second, we have 1_000_000/19_200 = 52.1 CPU cycles to send each bit, which is of course exactly half of the 9600bps rate's cycles per bit). The second is to change the code to use the memory-mapped I/O locations for this port, which is at $1700 (-003 port A) instead of $1742 (-002 port B), using the data direction register on -003. Fortunately, because the 9600bps sender also uses the lowest bit of the port register, all we need to do is change the memory location since the same bit-level logic will suffice.

The resulting routine to send a character is below, assembled into the small section of free RAM provided by the RRIOTs at $1780 so we can keep the KIM's nominal 1K of program memory unoccupied (except for a couple bytes in zero page, but the monitor also uses these, so there is no net effect). Note well that compared to the original 9600bps sender, the delay counts in this 19200bps version were not simply halved, because we still have to take into effect the fixed cycle counts of calling the delay loop. As before, this assembly code relies both on the self-synchronizing property of RS-232 by letting the stop bit run a little slow as well the forgiveness of most UARTs by clocking the start and data bits a little fast.

        * = $1780

        ; max 102 bytes
        ;
        ; send a character using PA0 as a bitbanged serial port at 19200bps
        ; (pin A-14)
        ;
        ; (c)2023 cameron kaiser. all rights reserved. bsd license.

zp      = $f9
yp      = zp+2

ch      = $fe

        ; 19200 bps on 1.000000MHz 6502 is 52.1 cycles/bit
        ; (i.e., 1_000_000/19_200)

send    sei             ; paranoia
        sta ch

        lda $1701
        ora #1
        sta $1701       ; pa0 to output

        ldx #8

        ; start bit
        lda $1700       ; (4)
        and #$fe        ; (2)
        sta $1700       ; (4) start bit live

        ldy #5
        jsr delayy      ; 38
        
send8   ; cribbed from kim-1 outch routine $1ea0
        and #$fe        ; 2
        lsr ch          ; 5
        adc #0          ; 2
        sta $1700       ; 4 - data bit live

        ldy #4
        jsr delayy      ; 33

        dex             ; 2 
        bne send8       ; if taken 3 if not taken 2
        ; 4 or 5

        ; stop bit
sends   lda $1700       ; 4
        ora #$01        ; 2
        nop             ; 2
        nop             ; 2
        sta $1700       ; 4 - stop bit live

        ldy #7
        jsr delayy      ; 48 (safest)
        cli
        rts

        ; plus 12 jsr and rts are 12 cycles combined
        ; so cycles = 2 + y*2 + (y-1)*3 + 2
delayy  dey
        bne delayy      ; each loop taken = 3, final branch = 2
        rts

It's probably possible to squeeze a bit more throughput out of it, but this seemed very reliable with this demonstration routine (assemble both with xa65):

        * = $0000

lup     lda `chr
        cmp #97
        bcc gosend
        lda #65
        sta `chr
gosend  jsr $1780
        inc `chr
        jmp lup

chr     .byt 65
Uploading the serial and test routines to the KIM-1 with KIMup and automatically starting execution at $0000 (kimup -g 0 0 test.o 0x1780 serial.o), the receive LED dutifully flickers on the dongle and in minicom at 19200bps we get

EFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRST
UVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCD
EFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRST
UVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCD
EFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRST
UVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCD
EFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRST
UVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCD
EFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRST
UVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCD
EFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRST

in a nice stable loop, over and over, without any garbled or dropped characters.

That should do for our serial driver pending a test on the actual device. Let's now get the screen situated a little more conveniently in the briefcase. Since we've got a little space between the power supply and the upper portion of the KIM board, we can put a little shelf in there for it to rest on. The LK's rear pins prop it up on the acrylic sheet I have mounted over the KIM's circuit board, so I got out a 90-degree corner brace and marked its holes at the right height to match what it sits at.

Drilling holes for the bolts.
Putting on the bolts and nuts. The top of the briefcase closes easily over the entire shebang, LCD included.
I put a 3M Command strip onto the top of the corner brace as a non-conductive pad to avoid shorting anything out (the back of the LK204-25 is just a naked circuit board). While I may want to do something with the expansion connector it would be blocking, if I decide I want to more permanently affix the LCD anyway, the needed side of the Command strip is already there. In that case I'd probably put a strip of electrical tape on the back of the LK and have the Command strip adhesive stick to that rather than the bare PCB. I also reworked the wires to go under the spade connectors on the other side of the power supply so that they're not having to loop around.
Finally, we can dispense with the A-1 ground connector because that line is the same common ground already going to both the LK and the level shifter from the power supply. The only connection we need now between the KIM and the LK is a single jumper from A-14 to TX (transmit) on the level shifter. With that wired up, we plunk the LK on its new shelf and fire up the test program again.
The LK autoscrolls and shows everything minicom did, just smaller, and I couldn't see any obviously mangled or dropped characters either. We therefore have a working driver for the LCD and a nice little homebrew shelf for it to sit on, it's powered by the existing power supply, it only needs one line to hook it up to the application connector, it can be easily disconnected if needed, and it all fits neatly in the KIM's carrying case.

Let's look at a couple example applications. All assembly code assembles with xa65 and is available in the Github project for this entry.

The title screen I showed you in the first image was generated using one of the HD44780's notable features: a partially programmable character set, in glyphs of 5x8. By creating a filled block character and various filled right triangles, we're basically drawing a picture using graphic characters as you would on a Commodore PET or C64. Later versions of the LK firmware have multiple banks for custom characters and can even store their definitions to non-volatile memory, but for this 1.23 unit we'll use the lowest common denominator, which supports temporarily redefining the first eight (0-7) until the unit is restarted. The terminal sequences look like this:

        ; install character sequences
ccharsq
        ; block as character 0
        .byt $fe, $4e, $00
        .byt %00011111
        .byt %00011111
        .byt %00011111
        .byt %00011111
        .byt %00011111
        .byt %00011111
        .byt %00011111
        .byt %00011111
        ; slash as character 1
        .byt $fe, $4e, $01
        .byt %00000001
        .byt %00000011
        .byt %00000011
        .byt %00000111
        .byt %00000111
        .byt %00001111
        .byt %00001111
        .byt %00011111

All LK control sequences start with byte $fe (254). The character definition sequences left-pad the 5-bit patterns with 000 to yield a full byte. We define all four variations of the filled right triangle and then emit this sequence to draw the screen:

        .byt $fe, $58, $fe, $48, $fe, $52
.byt $20, $20, $00, $01, $02, $20, $00, $20, $00, $03
.byt $01, $00, $20, $20, $20, $01, $00, $20, $20, $20

.byt $20, $20, $00, $02, $20, $20, $00, $20, $00, $04
.byt $02, $00, $20, $2d, $20, $02, $00, $20, $20, $20

.byt $20, $20, $00, $03, $20, $20, $00, $20, $00, $20
.byt $20, $00, $20, $20, $20, $20, $00, $20, $20, $20

.byt $20, $20, $00, $04, $03, $20, $00, $20, $00, $20
.byt $20, $00, $20, $20, $20, $00, $00, $00, $20

.byt $ff

The first line of bytes clears the screen (and homes the cursor just for paranoia) and turns automatic scrolling off, though for added paranoia since the final character is a space we transmit everything but the last byte (i.e., 79 characters). The graphics characters are bytes 0-7, which we'll redefine to have a full block glyph in 0 and the triangles in 1-4. The final $ff is a delimiter for this routine, which blasts the characters to the display using our serial sender at $1780 and then sits in an infinite loop:

        * = $0000

        ldy #0
        sty `zpy
strlup  ldy `zpy
        lda (zp),y
        cmp #$ff
        beq strdun
        jsr $1780       ; trashes all registers
        inc `zpy
        bne strlup
strdun  jmp strdun

zp      .word ccharsq
zpy     .byt $00

Although this sort of thing is largely the extent of what you can do with firmware 1.23, units with firmware 1.3 or later can store not only the redefined characters to non-volatile memory but the screen as well — allowing you to create a custom startup display. There are also built-in sequences in both firmware versions to swap in graph-drawing characters and digits of various sizes.

For games we could figure out some quick twitch ones and the screen is definitely fast enough for it, but I think the LCD adds particular value to the KIM's handful of word and text games, neither of which is the basic unit's strong suit (see also KIMdle). One I particularly enjoy is Reverse, from The First Book of KIM. On startup it displays a scrambled string of six letters ("ABCDEF") on the LEDs that you unscramble using digits 2-6 to flip that many letters (e.g., if you have CBADEF onscreen and you press 3, you get ABCDEF, and you win). This was a Jim Butterfield adaptation of the Bob Albrecht People's Computer Co. original BASIC game (the Creative Computing reproduction credits it to Peter Sessions, but I'll go with Jim's comment in The First Book).

Here we've added a simple title screen and a scrolling display so you can see your most recent moves. Reverse is known to have several algorithmic solutions and the move history aids you in this task, especially since the program won't let you make back to back flips of the same count (no doing 2 and then another 2 to flip it back, for example).
When you win, it displays a line of dashes to underscore your victory (get it?), just as Butterfield's version did. Hold down GO to repeatedly generate a new sequence until you release it.

As we don't need custom characters here, we'll adjust our string sender to use "regular" null delimited strings.

        ; take a null terminated string pointed to by x/y and
        ; emit it to the LCD
strout  lda #0
        sta zpy
        stx zp
        sty zp+1
strlup  ldy zpy
        lda (zp),y
        beq strdun
        jsr chrout      ; trashes all registers
        inc zpy
        bne strlup
strdun  rts

The title screen is another miracle of paranoia in which we also send full CRLFs. We also turn the cursor off since we don't need it for this game and it's just in the way.

        ; set screen to auto scroll, auto wrap, no cursor and clear
hello   .asc $fe, $58, $fe, $43, $fe, $4b, $fe, $54, $fe, $48, $fe, $51
        .asc $0d, $0a
        ;     --------------------
        .asc "      RE-VERSE",      $0d, $0a
        .asc " press key to start", $0d, $0a, $00

Scrolling the display requires a little bit of cheating. If you send a CRLF on the bottom line, it ... goes back up to the top without scrolling. Instead, we send this sequence before emitting the current letters:

        ; force screen to scroll, then
scroll  .asc $fe, 71, 20, 4, $20
        ; padding to center on screen
        .asc $20, $20, $20, $20, $20, $20, $20, $00

What this does is move the invisible cursor to the bottom right corner, manually emit a space (thus forcing the scroll), and then emit seven more spaces to put the six-character current state in the centre of the LCD (printing that sequence then follows). This scroll glitch may have been fixed in later firmware versions but this approach should work on any such unit.

I did have to do some other refactoring of the game in addition to reworking its display. Butterfield ran a tight loop flashing the LEDs and scanning the keypad since the LEDs have to be constantly refreshed, and he also worked some of the other game logic into that same main loop like checking for the win condition. We do not — indeed, should not — keep sending the current game state repeatedly to the LCD while waiting for keys, so those program segments had to be teased apart and the loop simplified to just waiting for a key.

I might do some other game ports to the LCD; Hunt the Wumpus does a lot of text display on the LEDs and would be a fairly well-suited, if large, undertaking. However, I'm already thinking of upgrading the panel to one of the new LK204-25s. This would be more expensive than what I have here, but I'd get the features of the 1.3 firmware, and with a TTL-specific unit I can get rid of the level shifter and have a free 5V and ground line for some other interesting peripheral — and a free LK204-25 with a built-in serial port I can give to something else. Meanwhile, the KIM's new LCD opens up a lot of possibilities. I might have another interesting KIM-1 project very soon.

The assembly language source code, suitable for assembly with xa65, is on Github. I've also provided binaries you can directly upload with KIMup.

No comments:

Post a Comment

Comments are subject to moderation. Be nice.