=== What is GP2IO? === My goal is to create an interface between Apple II and the library of peripherals and devices that exist for the modern hobbyist, a list that has exploded recently due to the popularity of Arduino, Raspberry Pi, and other programmable single board computers. These peripherals - displays, LED arrays, sensors, motor controllers and more – generally communicate via one of a handful of protocols like SPI, I2C or serial. Out of the box, an Apple II can't talk on these channels except serial. Even serial has its limits, though, since the Super Serial Card and later models with built-in serial ports use the older RS232 standard. This requires an adapter to convert the signal voltages to TTL for more modern UART serial, that's built into Arduino, Pi, etc. The "glue" between the Apple II and the newer devices is the GP2IO. This stands for "General Purpose Game Port I/O" GP2IO is based on sending serial data from an Apple II game port annunciator pin to an Atmel AVR microcontroller. An Arduino "sketch" on the AVR interprets the data and then acts on it accordingly – triggering LEDs, sending or receiving data to peripherals via I2C, serial, SPI, etc. Communication is bidirectional, as well. Data is received on the Apple II via reading data shifted into one of the paddle button pins from the AVR, similar to the way data is read through the cassette input. Reference: ========= Pushbutton 3 (GS) / NC 9 | * * | 8 Ground Gm Ctrl 1 (Stick-1 Y) 10 | * * | 7 Gm Ctrl 2 (Stick-2 X) Gm Ctrl 3 (Stick-2 Y) 11 | * * | 6 Gm Ctrl 0 (Stick-1 X) Annunciator 3 12 | * * | 5 +5V (GS ONLY) / $C040 Strobe (II-IIe) Annunciator 2 13 | * * | 4 Pushbutton 2 Annunciator 1 14 | * * | 3 Pushbutton 1 Annunciator 0 15 | * * | 2 Pushbutton 0 No Connection 16 | * * | 1 +5V ===| |=== ^ Notch on socket (faces toward front of computer) For input, I use the joystick/paddle button pins on the game port. Setting these to TTL HIGH (pressed) or LOW (not pressed) and "bit banging" in rapid succession gives them the capacity to carry serial data. Elsewhere, this is referred to as the Track & Field Protocol (http://atomsandelectrons.com/blog/2010/04/apple-t/) The first two buttons addresses are shared with the Apple keys, so it's best not to fiddle with those, if your program uses the Apple keys for keyboard input, macros, or for the PEW PEW! PB0 / OPNAPPLE = $C061 ; open apple (command) key data (read) PB1 / CLSAPPLE = $C062 ; closed apple (option) key data (read) PB2 = $C063 ; game Pushbutton 2 (read) <- Our victim PB3 (GS only) = $C060 ; game Pushbutton 3 (read) $C060 bit 7 = data from cassette on Apple II, II+, IIe Button 3 and the cassette share the same memory address $C060. So, we could toggle pin 9 on the game port (PB3) to load data directly into the memory address for the built-in cassette routines. But that pin is only on the GS. Poo. And the GS, having no cassette input, has no cassette routine in ROM. Double poo. Luckily, there are already examples of reading in data on those pins. I dug into [Michael Mahon's NadaNet](http://michaeljmahon.com/NadaNetPaper.html) implementation for inspiration, and found proof of speedy and reliable communication on the game port. This was encouraging. But, rather than require developers to implement NadaNet network packet reads just to get a few bytes into and out of an AVR, we can continue to take inspiration from the cassette routines. Using those as a starting point, I look for a transition from LOW to HIGH in pushbutton 1, then count the loops until the transition from HIGH to LOW. If it's a short time, the bit is zero, if it's longer, a one. === The game port also has four output pins: Annunciators 0,1,2,3. Each has a softswitch and memory location for "setting" and "clearing", counterintuitively these are 0 and 1 respectively. For AVR comms, I'll refer to HIGH (1) and LOW (0). SETAN0 = $C058 ;Set annunciator-0 output to 0 CLRAN0 = $C059 ;Set annunciator-0 output to 1 SETAN1 = $C05A ;Set annunciator-1 output to 0 CLRAN1 = $C05B ;Set annunciator-1 output to 1 SETAN2 = $C05C ;Set annunciator-2 output to 0 CLRAN2 = $C05D ;Set annunciator-2 output to 1 SETAN3 = $C05E ;Set annunciator-3 output to 0 CLRAN3 = $C05F ;Set annunciator-3 output to 1 NOTE: You won't likely be able to use the GP2IO in this mode with a double hi-res program. Double hi-res is incompatible with the annunciators since it clobbers those memory locations: SETIOUDIS= $C07E ;enable DHIRES & disable $C058-5F (W) CLRIOUDIS= $C07E ;disable DHIRES & enable $C058-5F (W) SETDHIRES= $C05E ;if IOUDIS Set, turn on double-hires CLRDHIRES= $C05F ;if IOUDIS Set, turn off double-hires Sorry. Other peripherals, like the ZIP Chip, use the annunciators' memory locations for setting configuration flags, so, timing matters aside, there are probably other incompatible setups as well. Sorry again. === Sending data from Apple II to GP2IO === Considering it's available in ROM on a bare II or II+, repurposing the cassette output seemed like a good place to start. There's no handshaking (other than the user pressing Record on the tape recorder) and it requires minimal effort on the Apple II to send a stream of bytes. The source for the ROM cassette output, via https://github.com/cmosher01/Apple-II-Source/blob/master/src/system/monitor/common/cassette.m4 TAPEOUT = $C020 HEADR LDY #$4B ;WRITE A*256 'LONG 1' JSR ZERDLY ; HALF CYCLES BNE HEADR ; (650 USEC EACH) ADC #$FE BCS HEADR ;THEN A 'SHORT 0' LDY #$21 ; (400 USEC) WRBIT JSR ZERDLY ;WRITE TWO HALF CYCLES INY ; OF 250 USEC ('0') INY ; OR 500 USEC ('0') ZERDLY DEY BNE ZERDLY BCC WRTAPE ;Y IS COUNT FOR LDY #$32 ; TIMING LOOP ONEDLY DEY BNE ONEDLY WRTAPE LDY TAPEOUT LDY #$2C DEX RTS Sending bytes from the Apple II via GP2IO is similar to writing to the cassette port. Instead of writing to the $C020 casette output for each bit, annunciator 1 is set to HIGH, then back to LOW after a short wait (zero) or longer wait (one). There is also a short "reset" transition from LOW to HIGH between bits, which are ignored by the AVR, to remove the necessity of keeping track of the ANN1 value between bits and bytes. I've also added in rudimentary handshaking with "Ready to Send" signals. Before sending, to tell the AVR to be ready to receive, I set annunciator 0 HIGH ("clear ANN0"), then LOW again when the byte is done. For speed, and since we have two-way communications, we can dispense with the long header write and sync bit of the cassette routine. Here is the source, which should be fairly relocatable (thanks, Mark Pilgrim): SENDBYTE entry point $0303, or $0300 for sending single bit uses $EF for storing outgoing byte 0300- A0 01 LDY #$01 ; load 1, for a 1 bit send - OR - 0302- 2C BIT ; 0303- A0 08 LDY #$08 ; load 8 for a full byte, loop counter 0305- 85 EF STA $EF ; put byte in zero page for safe keeping 0307- 8D 59 C0 STA $C059 ; annunciator 0 high, RTS 030A- 26 EF ROL $EF ; rotate byte left, high/MSB out to carry (sendbit) 030C- 90 03 BCC $0311 ; "branch on carry clear" - if carry/bit = 0, goto #311 (short loop) 030E- A2 14 LDX #$14 ; if carry/bit = 1, load X with 20 (long loop) - OR - 0310- 2C BIT 0311- A2 0A LDX #$0A ; if carry/bit = 0, load X with 10 (short loop) 0313- 8D 5B C0 STA $C05B ; set annunciator 1 HIGH 0316- CA DEX ; decrement, countdown to setting ANN1 low (countdown) 0317- D0 FD BNE $0316 ; if X > 0, keep counting (countdown) 0319- 8D 5A C0 STA $C05A ; if done, set ANN1 LOW, countdown to getting next bit 031C- A2 05 LDX #$05 ; reset counter for short "reset" transition. This lets the GP2IO stage the bit before the next interrupt 031E- CA DEX ; count down again (countdown2) 031F- D0 FD BNE $031E ; if X > 0, keep counting (countdown2) 0321- 88 DEY ; decrement y, countdown bits sent 0322- D0 E6 BNE $030A ; if y > 0, next bit (sendbit) 0324- 8D 58 C0 STA $C058 ; annunciator 0 low, sending RTS OFF 0327- 60 RTS ; return ___---------------------___ RTS line goes high ____--_-_--_--_-_--_-_-____ as Apple sends 10110100 (0xB4, ASCII 4) On the GP2IO side, the AVR has two interrupts set on ANN0 and ANN1: as the RTS pin goes HIGH (rising), and on changes to the data pin. If the data pin changes state in less than 70 microseconds (.07 milliseconds) the bit is read as zero. Longer than 70 microseconds, a one. ~~~ attachInterrupt(0, APPLERTS, RISING); // ANNUNCIATOR 0, Apple sending byte attachInterrupt(1, RECEIVINGBITS, CHANGE); // ANNUNCIATOR 1, Apple sending bits void APPLERTS() { // signal to start receiving bits from Apple II bitCount = 0; changeCount = 0; returnByte = B00000000; } void RECEIVINGBITS() { // ignore short "reset" transitions currentMicros = micros(); if (changeCount % 2 == 1) { if ((currentMicros - lastMicros) > 70) { receivedBit = 1; } else { receivedBit = 0; } byteArray[7 - bitCount] = receivedBit; bitCount++; } changeCount++; if (bitCount == 8) { // got a BYTE receivedByte = arrayToByte(byteArray, 8); PROCESSBYTE( byte(receivedByte) ); TIMEOUTCLOCK = millis(); } lastMicros = currentMicros; } ~~~ === Practical example SENDKEY === Read input from the keyboard, send each byte to GP2IO 20 1B FD JSR $FD1B ; wait for keyboard input, byte now in Accumulator C9 9B CMP #$9B ; check for ESC F0 0B BEQ $0812 ; return on ESC 20 ED FD JSR $FDF0 ; print byte to screen, local echo 09 80 ORA #$80 ; clear bit 7 (for "low" vs "high" ASCII, display purposes) 20 03 03 JSR $0303 ; send the byte to GP2IO (SENDBYTE) 4C 00 08 JMP $0800 ; repeat 60 RTS ; return on ESC === Processing Bytes on the GP2IO === The AVR starts in a null "standby" state, waiting for a "control" byte from the Apple II to set its function. Subsequent bytes after the function is set are the "message". The value of the first byte from the Apple determines the function according to this table: $01 Sets the RGB LED on the GP2IO to the color value of the next three bytes, in order Red, Green and Blue. Requires 3 byte message $02 Sets the RGB LED to white, intensity based on the 1 byte message that follows (0-255). $04 Write the following message to an internal buffer, for retrieval later. First byte is message length (0-255 bytes follow) $08 Write the following message to UART serial (buffered). First byte is message length (0-255 bytes follow) $10 Write the following 8 bytes to I2C bus. For the demo, this is connected to either an 8x8 LED matrix or a 4 character 7-segment display. Requires 8 byte message. $20 SPI (not yet implemented) $40 Query the buffer. Triggers the AVR to respond with one byte, containing the length of the current buffer. $80 Send the buffer. Triggers the AVR to respond with the first N bytes of the buffer, where N is the byte following the $80 trigger. $00 Debug mode. At the moment, simply echoes the bytes received from the Apple side into the serial buffer, and prints them to USB serial out. Also sends one byte from the GP2IO buffer to the Apple whenever the APPLECTS signal (Annunciator 2) goes high. Each function, as outlined, expects a certain number of bytes per message. For example, to set an RGB LED requires three bytes - one each for Red, Green, and Blue values, plus the first $01 byte to set the function, for a total of four. The AVR then shuttles those bytes to the proper output, sends results back to the Apple, or waits for further instruction. After each message is done, the GP2IO returns to standby for the next function, so before setting the LED to a different color, you need to send the $01 control byte again. In case a message gets cut off, or something goes awry, the GP2IO will also go to standby if nothing is received for roughly 5 seconds. The LED on the GP2IO board goes to blue to indicate it has timed out or is in standby mode waiting for a control byte. EXAMPLE: RGB LED Pulse white 0800- A9 FF LDA #$FF ; set accumulator = 255, starts with LED off after one INC 0802- 48 PHA ; push FF to stack 0803- A9 02 LDA #$02 ; set accumulator = 2, one byte LED mode 0805- 20 03 03 JSR $0303 ; SENDBYTE, sets the mode (setmode) 0808- 68 PLA ; pull current intensity from stack 0809- 18 CLC ; clear carry 080A- 69 01 ADC #$01 ; increment accumulator/intensity value, $FF rolls to $00 080C- 48 PHA ; push new intensity to stack 080D- 20 03 03 JSR $0303 ; SENDBYTE, sets the LED intensity 0810- 4C 03 08 JMP $0803 ; jump to (setmode) for next pass SLIGHTLY MORE COMPLEX EXAMPLE: RGB LED pulsing SETRGB: one-shot, sets the LED with red value in $06, green at $07, blue at $08 0800- A9 01 LDA #$01 ; set accumulator to 1, three byte RGB mode 0802- 20 03 03 JSR $0303 ; $0303 = SENDBYTE routine 0805- A5 06 LDA $06 ; read in red value from zero page 0807- 20 03 03 JSR $0303 ; SENDBYTE 080A- A5 07 LDA $07 ; read green value 080C- 20 03 03 JSR $0303 ; SENDBYTE 080F- A5 08 LDA $08 ; read blue value 0811- 20 03 03 JSR $0303 ; SENDBYTE 0814- 60 RTS ; return. CYCLERGB 0816- A9 00 LDA #$00 ; set accumulator to 0 0818- 85 06 STA $06 ; zero to red value, 081A- 85 07 STA $07 ; zero green 081C- 85 08 STA $08 ; zero blue 081E- 20 00 08 JSR $0800 ; SETRGB 0821- A5 06 LDA $06 ; load red value 0823- 69 02 ADC #$02 ; add 2 to current red value 0825- 85 06 STA $06 ; push it to zero page $06 0827- B0 F5 BCS $081E ; if still less than $FF, return and SETRGB 0829- A5 07 LDA $07 ; else, red has rolled over, load green 082B- 69 02 ADC #$02 ; increment green x2 082D- 85 07 STA $07 ; store green 082F- B0 ED BCS $081E ; if less than $FF, return and SETRGB 0831- A5 08 LDA $08 ; else, green has rolled over, load blue 0833- 69 02 ADC #$02 ; increment blue 0835- 85 08 STA $08 ; store blue 0837- 4C 1E 08 JMP $081E ; return and SETRGB 083A- 00 BRK === Receiving Data from the GP2IO to the Apple === The plan, again, is to mimic data coming in on the cassette port, but with the pushbutton signal going high and low instead of tones on the casette. About the cassette input: "Routines in the monitor scan bit 7 at location $CO6O every 12.8 microseconds looking for a transition. By measuring the time between transitions, the routines can distinguish between header, sync, 0, and 1 bits." From Apple II Circuit Description by Winston Gaylor ©1983 by Howard and Sams & Co, Inc. Here's the source for those calls, again via https://github.com/cmosher01/Apple-II-Source/blob/master/src/system/monitor/common/cassette.m4 TAPEOUT = $C020 TAPEIN = $C060 RDBYTE LDX #$08 ;8 BITS TO READ RDBYT2 PHA ;READ TWO TRANSITIONS JSR RD2BIT ; (FIND EDGE) PLA ROL ;NEXT BIT LDY #$3A ;COUNT FOR SAMPLES DEX BNE RDBYT2 RTS RD2BIT JSR RDBIT RDBIT DEY ;DECR Y UNTIL LDA TAPEIN ; TAPE TRANSITION EOR LASTIN BPL RDBIT EOR LASTIN STA LASTIN CPY #$80 ;SET CARRY ON Y RTS Here is my GP2IO READ BYTE routines: $EF = byte as it's read $EE = pushbutton 1 value stored for comparison $C05D = set annunciator 2 = 1 $C05C = set annunciator 2 = 0 Entry point at $34A. Received byte is returned in Accumulator READCOMBINED 034A- A2 09 LDX #$09 ; reading 8 bits requires 9 transitions. 034C- A9 00 LDA #$00 ; clear the accumulator 034E- 85 EF STA $EF ; $EF is staging for received byte, set 0 0350- 85 EE STA $EE ; $EE is staging for each bit, set 0 0352- 8D 5D C0 STA $C05D ; set ANN2 HIGH, indicate to AVR "Clear to Send" 0355- A0 FF LDY #$FF ; start wait loop 0357- C8 INY ; Increment Y - rolls over to 0 on first run (loopcount) 0358- AD 62 C0 LDA $C062 ; check PB1 status hi/low 035B- 29 80 AND #$80 ; clear 0-6 bits (just need bit 7, all others float) 035D- C5 EE CMP $EE ; compare Accumulator 7 bit with $EE, previous PB2 value 035F- D0 02 BNE $0363 ; if PB2 has changed state, store in $EE (bitchange) 0361- F0 F4 BEQ $0357 ; bit hasn't changed yet, return to (loopcount) 0363- 85 EE STA $EE ; (bitchange) 0365- C0 44 CPY #$44 ; if the loop count is more than 68, bit is one. Bit is set in Carry 0367- 26 EF ROL $EF ; rotate the new bit into $EF, our result byte. 0369- CA DEX ; decrement X, our bit count 036A- D0 E9 BNE $0355 ; if bit count is not yet full, loop back to (loopcount) 036C- 8D 5C C0 STA $C05C ; if bit count is full, set ANN2 LOW, CTS off 036F- A5 EF LDA $EF ; puts received byte into Accumulator 0371- 60 RTS ; return with byte in Accumulator And a demo: this reads one byte from the AVR output buffer to the screen. It gets one byte at a time, triggered by a keypress. The AVR will need to be ready to send one byte at a time when it sees ANN2 go HIGH ("SEND BUFFER CONTENT" mode, set by sending $80 as the function switch first). GETBYTEONKEY 0800- 20 1B FD JSR $FD1B ; wait for keypress 0803- 20 4A 03 JSR $034A ; READBYTE 0806- 20 DA FD JSR $FDDA ; print hex to screen 0809- 20 8B FD JSR $FD8B ; return carriage 080C- 4C 00 08 JMP $0800 ; repeat So, how to receive usefully large chunks of data at once? With no interrupts on the game port to work with on incoming data, and better things to do with your program than poll the game port constantly, we need to tell the AVR when to send, and how much data you're ready to store. The Apple sends a "Send Buffer Data" control byte (0x80) followed by another byte indicating how long the message should be (1-255 bytes). The GP2IO then sends the requested number of bytes from its internal output buffer, zero-padded if necessary. Future versions could optionally include a checksum. The checksum could be a simple AND of each byte, like what's done with the original cassette routine, or something more complicated. This is not yet implemented, and I leave that as an exercise for other programmers. Here is a buffered I/O routine for minimally interactive data transmission between the Apple II and a serial device connected to the GP2IO. 0800- 20 4D 08 JSR $084D ; start by querying the buffer length - is there anything waiting? returns with buffer length in $08 0804- A9 80 LDA #$80 ; load control byte $80 "send buffer bytes" 0806- 20 03 03 JSR $0303 ; send control byte 0809- A5 08 LDA $08 ; load buffer length (or however much buffer you want sent) 080B- 20 03 03 JSR $0303 ; send message length byte 080E- A9 28 LDA #$28 ; wait a moment (betweenbytes) 0810- 20 A8 FC JSR $FCA8 ; wait 0813- 20 4A 03 JSR $034A ; CTS - send byte 0816- 09 80 ORA #$80 ; clear byte 7 (for ASCII) 0818- 20 ED FD JSR $FDED ; display byte on screen 081B- C6 08 DEC $08 ; count down bytes (countdownbuffer) 081D- A5 08 LDA $08 ; load Accumulator with new buffer length 0820- D0 EC BNE $080E ; if there's more to come, loop (betweenbytes) 0822- 20 8E FD JSR $FD8E ; last byte. print CF/LF 0825- 60 RTS ; return GETLINE 0826- 20 6A FD JSR $FD6A ; get line of input 0829- 8A TXA ; get input length into Accumulator 082A- 85 07 STA $07 ; put input length into $07 082C- A9 00 LDA #$00 ; load zero into Accumulator 082E- 85 06 STA $06 ; put zero into $06, keyboard buffer pointer 0830- A9 04 LDA #$04 ; load Accumulator with control byte "write to buffer" 0832- 20 03 03 JSR $0303 ; send control byte 0835- A5 07 LDA $07 ; load input length from $07 0837- 20 03 03 JSR $0303 ; send message length 083A- A6 06 LDX $06 ; load X with keyboard buffer pointer (keybuffer) 083C- BD 00 02 LDA $0200,X ; load Accumulator with bytes from keyboard buffer (start at $200) 083F- 29 7F AND #$7F ; clear high bit (for ASCII) 0841- 20 03 03 JSR $0303 ; send byte 0844- E6 06 INC $06 ; increment keyboard buffer pointer 0846- A5 06 LDA $06 ; load buffer pointer into Accumulator 0848- C5 07 CMP $07 ; compare with message length 084A- D0 EE BNE $083A ; if not at end of message, loop to (keybuffer) 084C- 60 RTS ; return QUERY BUFFER LENGTH returns buffer length in $08 084D- A9 40 LDA #$40 ; control byte for query buffer length 084F- 20 03 03 JSR $0303 ; send control byte 0852- 20 28 03 JSR $0328 ; wait routine - gives GP2IO time to receive bytes from serial 0855- 8D 5C C0 STA $C05C ; set ANN2 to LOW (just in case it's floating high) 0858- 20 4A 03 JSR $034A ; CTS - ready for response byte with buffer length 085B- F0 F0 BEQ $084D ; if there's nothing in the buffer, loop until there is 085D- 85 08 STA $08 ; put the byte in $08 085F- 60 RTS ; return Used Zero Page locations: 06 07 08: SetRGB routine, Red Green Blue bytes EE: ReadBit EF: ReadByte, SendByte === So, What Can It Do? === With a general purpose gateway to I2C, SPI and UART devices (among others), GP2IO lets the Apple II access a variety of different inputs and outputs, from sensors and alternate controllers to external data displays. Some use case ideas include: - light indicator for disk activity, RWTS function - trackstar-type indicator for bootloaders - game status indicators - blink on collision, red for damage, etc - get current internet time via NTP for real time clock function === So, What Can't It Do? === The main limitation is the platforms the GP2IO can connect to. The IIc and IIc+ have no 16-pin game port, and the 9-pin game port on those models lacks connections to the annunciator pins. The GS has the 16-pin port, but the CPU speed makes the timing-based loops run much too quickly without modification. Those models also include serial on-board, so why not use serial connection? === So, Why Not Use A Serial Connection? === Seriously, why not? The GP2IO includes RS232 9-pin connector that's adapted on the board to UART levels, so yes, you can connect to it directly with any Apple II with serial. Did I forget to mention that? === Big thanks to === - Mark Pilgrim for checking and optimizing my code, and helping out testing the various use cases. - David Schmidt for sanity checking my serial implementation and proof-of-concept. - KansasFest committee for allowing me to present, public beta test the GP2IO at KansasFest 2016. - Chris Torrence and Roger Wagner for Assembly Lines, both the book and searchable PDF. - Michael Mahon, for inventing such a nice wheel that I had to go and reinvent it. === The Inaugural, First Annual, and Probably Last, RetroConnector Mini-Hackfest === Each participant will be given a beta GP2IO board and available components as requested, on a first-come-first-served basis. Participants may keep the board, so long as he or she submits a completed entry, and agrees to post instructions publicly. Participants are encouraged to build or purchase additional hardware as needed, so long as they do not spend more than $50. The GP2IO board, its software and components can be altered, repurposed and hacked, but not outright destroyed. Okay, fine, you can destroy whatever you want, as long as your entry is cooler because of it. All entries must be documented or demonstrated to a degree they could be reproduced by someone of reasonable intelligence, and submitted or demonstrated to the judges prior to the Vendor Faire. The judges will have to decide amongst themselves what constitutes "reasonable intelligence." Entries will be judged on the following criteria: - Is it cool? - Does it do something the Apple II couldn't do before? - Is it *really* cool? - Originality. - No, seriously. How cool is that? I'm angry I didn't think of it myself. Judges: - Charles Mangin - Mark Pilgrim - Bill Martens