Part II – The Software
The software turns out to be a bit easier once all the hardware is built. The two main routines required are the read and write routines. Writing a data byte to the QC involves writing the data to the output port then reading back the echo of the data from the QC to ensure it was received. Reading a data byte from QC involves bringing PCAck (Strobe) high, waiting for the QC to respond with CamRDY high, then reading the first nybble on the input data port. Once the nybble has been read, deassert PCAck (Strobe), wait for CamRDY to go low, then read the second nybble from the input data port. Because of the way the PC parallel port works, Nybble3 from the QC went through an inverter so the QC actually compliments Nybble3 so the PC would read the correct value right from the I/O port. However, the APIC isn’t mapped like the PC parallel port, so this bit will show up inverted on the input data port. The 6502 has to compliment Nybble3 to get the correct value. One other issue mentioned in the hardware post has to be dealt with – the Output Strobe. On the APIC, the Output Strobe connected to PCAck, has an automatic pulse duration that can be set with switches on the card. Because the actual duration of this pulse is dynamic, handshaking with the CamRDY signal, a way to keep re-triggering this signal so it doesn’t prematurely deactivate has to be developed. As luck would have it, the maximum strobe pulse is 15 us, just long enough to check the status of PCAck and either re-trigger the strobe or read the data on the input port before the strobe de-asserts. Finally, reading the echo from a data write is the exact same code as reading a data byte, the two are combined and the data write routine falls right into the data read. Here are the two most important routines with the slot*16 in the X register:
QCWRITE STA $C080,X ; DATA OUT -> CMD QCREAD STA $C082,X ; STROBE -> PCACK LSR $C084,X ; ACK (BIT 0) <- CAMRDY BCC QCREAD LDA $C083,X ; DATA IN <- HI NYBBLE AND #$F0 ; DO ENOUGH WORK SO HANDSHAKE CAN BE SKIPPED EOR #$88 ; COMPLIMENT NYBBLE3 STA TMP LDA #$0F AND $C083,X ; DATA IN <- LO NYBBLE EOR TMP ; COMBINE LO AND HI NYBBLES RTS
This is the core of the code to read and write the QC. The other important routine is the reset/init routine. The original B/W QC would return a parameter to the GetVersion command of 0. The later Color QC returned a non-zero value, plus an additional value. Care must be taken to take this into account if code is to work on both. Also, this code isn’t smart – if a QC isn’t attached, it will wait forever.
RESET LDA #$FF STA $C085,X ; RESET QC JSR $FCA8 ; WAIT LDA #$17 ; GET_VER JSR QCWRITE CMP #$17 ; GOOD ECHO? BNE QCERR JSR QCREAD BEQ QCBW PHA JSR QCREAD ; COLOR QC HAS 2 PARMS PLA QCBW RTS
Once we can initialize the QC and read and write commands, time to read video frame data. The QC can return pixel data in either 4BPP or 6BPP mode. Due to the limitations of the Apple II graphics, 4BPP seems adequate so I don’t bother with 6BPP. I wrote a very simple program to grab small video frames and put them on the Apple’s low resolution graphics screen. By mapping the color blocks into a corresponding grey scale ramp and displaying the result on a monochrome monitor, live video that roughly resembles what the QC is looking at results. I added controls to adjust the exposure value and contrast to that the best image can be obtained given the severe constraints of 4 bit pixels and a 40×48 graphics mode. Finally, I added a high resolution capture command that would take a video frame and display it on the high res screen. Now the high res screen only shows 1 bit pixels, so I had to employ a primitive error diffusion algorithm to get a respectable image. I’ll let you be the judge of the results:
I added a small ordered dither to the pixel calculations to bring out the mid and low grey values. Here is an image of some of my collection:
Finally, here is the code for the lo-res video/hi-res frame capture. It isn’t really well documented, but is fairly short and simple. Meant to be assembled by EDASM.
ORG $2000 DATAOUT EQU $C080 STROBE EQU $C082 DATAIN EQU $C083 ACK EQU $C084 RESET EQU $C085 WAIT EQU $FCA8 ERR EQU $03 TMP EQU $06 HPIX EQU $06 LASTERR EQU $07 * * HARD-CODE SLOT 1 AND RESET * LDX #$10 JSR QCRESET BCC GR RTS * * SET LORES GRAPHICS * GR LDA $C050 LDA $C056 LDA $C052 LDA $C054 * * SET FRAME DIMENSIONS * LDX #$10 LDA #45 JSR QCLEFT LDA #25 JSR QCTOP LDA #20 JSR QCNUMH LDA #48 JSR QCNUMV * * SET FRAME CONTRAST/EXPOSURE * SETCONT LDA CONT JSR QCCONT SETEXP LDA EXP JSR QCEXP * * GRAB FRAME INTO LORES GRAPHICS SCREEN * VIDEO LDA #$08 JSR QCFRAME LDA #$00 STA LINE * * EVEN LINES * ELINE JSR $F847 ; GBASECALC LDY #$00 EPIX STA STROBE,X LSR ACK,X BCC EPIX LDA DATAIN,X AND #$0F TAX LDA GREYLO,X LDX #$10 STA ($26),Y INY LDA DATAIN,X AND #$0F TAX LDA GREYLO,X LDX #$10 STA ($26),Y INY CPY #40 BNE EPIX * * ODD LINES * OLINE LDY #$00 OPIX STA STROBE,X LSR ACK,X BCC OPIX LDA DATAIN,X AND #$0F TAX LDA GREYHI,X LDX #$10 ORA ($26),Y STA ($26),Y INY LDA DATAIN,X AND #$0F TAX LDA GREYHI,X LDX #$10 ORA ($26),Y STA ($26),Y INY CPY #40 BNE OPIX INC LINE LDA LINE CMP #24 BEQ EOFRM JMP ELINE EOFRM JSR QCEOF * * CHEK FOR KEYPRESS * LDA $C000 BMI KEYPRESS JMP VIDEO KEYPRESS STA $C010 CMP #$D1 ; Q BEQ DONE CMP #$AB ; + BNE KP1 INC EXP JMP SETEXP KP1 CMP #$AD ; - BNE KP2 DEC EXP JMP SETEXP KP2 CMP #$BC ; < BNE KP3 DEC CONT JMP SETCONT KP3 CMP #$BE ; > BNE KPEND INC CONT JMP SETCONT KPEND CMP #$A0 ; SPACEBAR BEQ HRCAP JMP VIDEO DONE JMP $FB39 ; TEXT * * HIRES FRAME CAPTURE * HRCAP LDA $C057 LDA $C055 LDA #20 JSR QCLEFT LDA #25 JSR QCTOP LDA #140 JSR QCNUMH LDA #192 JSR QCNUMV LDA #$00 JSR QCFRAME LDA #$40 STA $E6 STA $27 LDY #$00 STY $26 STY LINE STY LASTERR * * FIRST SEVEN BITS OF HIRES WORD - EVEN LINES * EHLINE JSR QCPIXHI CLC ADC #4 JSR PIXTST JSR QCPIXLO SEC SBC #1 JSR PIXTST JSR QCPIXHI CLC ADC #4 JSR PIXTST JSR QCPIXLO SEC SBC #1 JSR PIXTST JSR QCPIXHI CLC ADC #4 JSR PIXTST JSR QCPIXLO SEC SBC #1 JSR PIXTST JSR QCPIXHI CLC ADC #4 JSR PIXTST LDA HPIX LSR STA ($26),Y INY * * SECOND BYTE OF HIRES WORD * JSR QCPIXLO SEC SBC #1 JSR PIXTST JSR QCPIXHI CLC ADC #4 JSR PIXTST JSR QCPIXLO SEC SBC #1 JSR PIXTST JSR QCPIXHI CLC ADC #4 JSR PIXTST JSR QCPIXLO SEC SBC #1 JSR PIXTST JSR QCPIXHI CLC ADC #4 JSR PIXTST JSR QCPIXLO SEC SBC #1 JSR PIXTST LDA HPIX LSR STA ($26),Y INY CPY #40 BEQ NEXTEHLINE JMP EHLINE NEXTEHLINE JSR $F504 ; INC HPOSN INC LINE LDY #$00 * * FIRST SEVEN BITS OF HIRES WORD - ODD LINES * OHLINE JSR QCPIXHI SEC SBC #4 JSR PIXTST JSR QCPIXLO CLC ADC #1 JSR PIXTST JSR QCPIXHI SEC SBC #4 JSR PIXTST JSR QCPIXLO CLC ADC #1 JSR PIXTST JSR QCPIXHI SEC SBC #4 JSR PIXTST JSR QCPIXLO CLC ADC #1 JSR PIXTST JSR QCPIXHI SEC SBC #4 JSR PIXTST LDA HPIX LSR STA ($26),Y INY * * SECOND BYTE OF HIRES WORD * JSR QCPIXLO CLC ADC #1 JSR PIXTST JSR QCPIXHI SEC SBC #4 JSR PIXTST JSR QCPIXLO CLC ADC #1 JSR PIXTST JSR QCPIXHI SEC SBC #4 JSR PIXTST JSR QCPIXLO CLC ADC #1 JSR PIXTST JSR QCPIXHI SEC SBC #4 JSR PIXTST JSR QCPIXLO CLC ADC #1 JSR PIXTST LDA HPIX LSR STA ($26),Y INY CPY #40 BEQ NEXTOHLINE JMP OHLINE NEXTOHLINE JSR $F504 ; INC HPOSN INC LINE LDA LINE CMP #192 BEQ EXIT LDY #$00 JMP EHLINE EXIT JMP QCEOF * * SIMPLE ERROR DIFFUSION * PIXTST CLC ADC LASTERR CMP #$08 BMI PIXNEG ROR HPIX SEC SBC #$0F CMP #$80 ROR STA LASTERR RTS PIXNEG LSR HPIX CMP #$80 ROR STA LASTERR RTS EXP DB 96 CONT DB 104 LINE DB $00 * * QCAM PIXEL DATA -> LORES GREYSCALE * GREYHI DB $30,$60,$A0,$50,$40,$80,$20,$10 DB $00,$F0,$E0,$D0,$70,$B0,$90,$C0 GREYLO DB $03,$06,$0A,$05,$04,$08,$02,$01 DB $00,$0F,$0E,$0D,$07,$0B,$09,$0C *************************** * * QUICKCAM ROUTINES * *************************** * * RESET QUICKCAM * QCRESET STA RESET,X STX TMP LDA #$FF JSR WAIT LDX TMP LDA #$17 ; GET VERSION JSR QCWRITE CMP #$17 BEQ QCOK SEC RTS QCOK JSR QCREAD BEQ QCBW PHA JSR QCREAD PLA QCBW LDA #$19 ; SET CONTRAST JSR QCWRITE LDA #104 JSR QCWRITE LDA #$1B ; AUTO CALIBRATE JSR QCWRITE JSR QCWRITE ; DUMMY PARAMETER QCCAL LDA #$7F JSR WAIT LDA #$21 ; GET OFFSET JSR QCWRITE JSR QCREAD CMP #$FF BEQ QCCAL CLC RTS * * SET CONTRAST IN ACCUM * QCCONT PHA LDA #$19 ; SET CONTRAST JSR QCWRITE PLA JMP QCWRITE * * SET EXPOSURE IN ACCUM * QCEXP PHA LDA #$0B ; SET EXPOSURE JSR QCWRITE PLA JMP QCWRITE * * SET TOP OF FRAME * QCTOP PHA LDA #$0D JSR QCWRITE PLA JMP QCWRITE * * SET FRAME LEFT COORDINATE * QCLEFT PHA LDA #$0F JSR QCWRITE PLA JMP QCWRITE * * SET FRAME LINES TO TRANSFER * QCNUMV PHA LDA #$11 JSR QCWRITE PLA JMP QCWRITE * * SET FRAME PIXELS PER LINE * QCNUMH PHA LDA #$13 JSR QCWRITE PLA JMP QCWRITE * * COMPLETE END OF FRAME * QCEOF STA STROBE,X RTS * * SEND VIDEO FRAME * QCFRAME PHA LDA #$07 JSR QCWRITE PLA * * WRITE BYTE TO CAM, FALL THRU TO READ ECHO * QCWRITE STA DATAOUT,X * * READ BYTE FROM CAM * QCREAD STA STROBE,X LSR ACK,X BCC QCREAD LDA DATAIN,X AND #$F0 EOR #$88 STA TMP LDA #$0F AND DATAIN,X EOR TMP RTS * * READ HI NYBBLE FROM CAM * - NYBBLE IS DUPLICATED IN HI & LO NYBBLE * QCREADHI STA STROBE,X LSR ACK,X BCC QCREADHI * * READ LO NYBBLE FROM CAM * - SAME AS HI NYBBLE W/O WAIT * QCREADLO LDA DATAIN,X EOR #$88 RTS * * READ 4BPP AS B/W RAMP 0->F * QCPIXHI STA STROBE,X LSR ACK,X BCC QCPIXHI QCPIXLO LDA DATAIN,X EOR #$07 CLC ADC #$01 AND #$0F RTS
Part I – The Hardware
I finally got around to looking into connecting an old Connectix Greyscale QuickCam (QC) to an Apple II by way of the Apple Parallel Interface Card (APIC). I had a couple of QuickCams lying around from my webcam astronomy days and bought some APICs off the ePay in anticipation of hooking them together someday. I chose the APIC because it was the only parallel interface card I could find that implemented a full 8-bit input port.
Someday finally came. I pulled out the programming specs on the QC and the APIC Installation and Operation Manual and went about figuring out the details of mapping the PC parallel port pinouts of the QC to the APIC. This is the cable magic required to connect them together:
APIC Signal/Pin QuickCam Signal/Pin Strobe 15 PCAck 17 Ack 16 CamRDY 15 Input D7 18 Nybble3 11 Input D6 21 Nybble2 10 Input D5 19 Nybble1 12 Input D4 14 Nybble0 13 Input D3 25 Nybble3 11 Input D2 3 Nybble2 10 Input D1 17 Nybble1 12 Input D0 1 Nybble0 13 Output D7 13 Cmd7 9 Output D6 12 Cmd6 8 Output D5 11 Cmd5 7 Output D4 23 Cmd4 6 Output D3 22 Cmd3 5 Output D2 8 Cmd2 4 Output D1 6 Cmd1 3 Output D0 5 Cmd0 2 Aux Strobe 7 *Reset 16 GND 24,20,4,2 GND 18-25
There are a few things to take note of. First, the output from the QC, Nybble3-0 is mapped to both to the most significant and the least significant nybble of the APICs 8 bit input port. Since there are only enough I/O pins to implement the 4 bit communications protocol, I decided to map the one QC nybble to a full byte of input to the APIC. The 6502 isn’t very good at shifting, so this alleviates the need to shift the nybble around for certain algorithms. The second thing to note is the output strobe of the APIC connected to the PCAck of the QC. This is one of the major handshake lines that works in conjunction with the CamRDY line for transferring data. The host needs to hold this line high until the QC signals data is ready by raising CamRDY. However, the APICs strobe is pulsed for a short duration, automatically returning to its previous state based upon switch settings on the APIC. The strobe duration can be set using the first three switches on the APIC, from 1 us to 15 us. Luckily, by maxing out the strobe duration to 15 us gives the 6502 just enough time to check the PCAck from the QC and re-triggering the strobe if it isn’t ready, or reading the data input port if it is – all before the strobe falls. The second nybble from the QC quickly follows the first, so there was no need to check the handshaking signals again if the 6502 did enough work in-between nybble reads. The last fortuitous connection was the Aux Strobe to *Reset. The Aux Strobe isn’t really documented in the APIC manual except for the block diagram. It delivers a short pulse whenever a certain address is accessed, and just happens to be active low, matching the requirements for the *Reset on the QC. The APIC allows for the selection of active high or low for the regular strobe signal, thus matching the QCs PCAck signal. The switch settings required on the APIC are thus:
APIC SWITCH BLOCK 1 2 3 4 5 6 7 ON ON ON OFF ON OFF OFF
As this is a fairly delicate connection that I didn’t want to have to re-solder, I wanted to get a DB-25 dual ended hood to create a professional, reliable dongle. However, those parts are no longer made, so I had to buy two DB-25 hoods and take a hack saw to them to create the piece I wanted. Ultimately, I ended up with this:
Finally, the QuickCam takes its power from the PS/2->AT keyboard connector since the parallel port doesn’t provide any. I went through the parts bin, found an old +5V power adapter, an old keyboard cable to hack up, and along with a trip to Radio Shack for the power plug, created this:
The only lines required to connect are the +5V and ground. Here is a link to a nice diagram showing which pins are power for the two connector types: http://www.bbdsoft.com/keyboard.html