This is not a new project which was initially started in 2015, but it was never well documented and I was not very happy with the end result at that time. I picked it up again last month and made significant improvements with some guidance from Tursi, so here it is.
Image capture has not been attempted previously on the TI 99/4A computer even though most other contemporary computers did have such a facility developed for them. While I realize that nowadays it's not really necessary since one could snap any picture with a digital camera, process it using Tursi's Convert9918 program (http://www.harmlesslion.com/cgi-bin/onesoft.cgi?2) and transfer it to the computer, there is still something special about doing this directly on the TI.
The first challenge was figuring out which capture camera to use, with the major requirement being ease of interfacing and good documentation as well as low cost, and I ended up settling on the Raspberry Pi camera. It has a large amount of support available online and is very easy to work with. Of course this entailed using a Raspberry Pi SBC as well, but I already had a couple of spare ones, namely model B, lying around, and they are dirt cheap anyway.
A standard bitmap image on the TI has a resolution of 256x192 pixels, or a total of just over 49,000 pixels. With that many data points to transmit to the TI, I opted to use the TI's parallel port (PIO) for ease of access and processing. One of the issues encountered at this point was the fact that the Raspberry Pi does not have a parallel port, so the solution was to simulate one by commissioning 8 GPIO pins on the board to act as a bidirectional 8-bit parallel port via software. Another issue related to the fact that the Raspberry Pi operates at 3.3V whereas the TI operates at 5V, therefore requiring the use of an interface between the two in order to convert the voltages back and forth and avoid frying the Raspberry Pi. The interface also allowed the gating of information to and from the TI.
Here's the schematic:
The idea here is that the Rpi camera will capture a raw RGB image at a resolution of 256x192 pixels, which produces 3 bytes of information per pixel, one for each red, green and blue colors. Obviously that's a massive amount of information, and so I had the Rpi compress the three bytes into one, taking advantage of the fact that the blue color is poorly perceived by the human eye. In the final scheme, one byte per pixel was produced, with 3 bits for red, 3 bits for green and 2 bits for blue, which was then transmitted to the TI one byte at a time.
Here's the initial breadboarded prototype:
I experienced a significant amount of noise with that prototype, likely related to the mass of wires required, but I was confident that the design was sound, so I bit the bullet and decided to create a double sided PCB for the project. This was the first time I had attempted to create a double sided PCB, and the end result was barely satisfactory, although it did require a lot of nursing to correct for missed vias and broken traces.
The general protocol for data transmission from the Raspberry Pi to the TI was as follows:
-Both handshake output lines start LOW
-TI polls the RPI handshake line, waiting for HIGH
-RPI sets the data port pins with valid data
-RPI then sets its handshake output HIGH to indicate data is available on the port
-RPI then polls the TI's handshake output, waiting for HIGH
-When TI see the RPI handshake high, it reads the data byte from the data pins
-TI then sets its output HIGH as an acknowledge
-TI then polls the RPI handshake line, waiting for LOW
-RPI sees the TI pin go HIGH, and releases its handshake (sets it LOW). RPI is now free to go process the next byte
-TI see the RPI line go LOW, and sets it's handshake LOW. TI is now free to process the byte.
-This process resumes at the top.
import RPi.GPIO as GPIOimport timeimport picameraimport structGPIO.setwarnings(False)GPIO.setmode(GPIO.BCM)GPIO.setup(24, GPIO.IN, pull_up_down = GPIO.PUD_DOWN)GPIO.setup(25, GPIO.OUT)GPIO.setup(23, GPIO.IN)GPIO.setup(8, GPIO.OUT)GPIO.setup(7, GPIO.OUT)GPIO.setup(11, GPIO.OUT)# Define the GPIO ports to be used for data transfersdata_ports = (9,10,22,27,17,4,3,2)# Set the initial state of the HANDSHAKEIN and SPAREIN lines to lowGPIO.output(25, GPIO.LOW)GPIO.output(11, GPIO.LOW)# Set the control lines logic level shifter direction from TI to RPidef control_in(): GPIO.output(8, GPIO.HIGH) # Set the control lines logic level shifter direction from RPi to TI def control_out(): GPIO.output(8, GPIO.LOW) # Set the data lines logic level shifter direction from TI to RPidef data_in(): GPIO.output(7, GPIO.HIGH) # Set the data lines logic level shifter direction from RPi to TIdef data_out(): GPIO.output(7, GPIO.LOW) # Pulse the HANDSHAKEIN line high and lowdef HSK_pulse(): GPIO.output(25, GPIO.HIGH) time.sleep(0.025) GPIO.output(25, GPIO.LOW) # Pulse the SPAREIN line high and lowdef SPR_pulse(): GPIO.output(11, GPIO.HIGH) time.sleep(0.025) GPIO.output(11, GPIO.LOW)# Set HANDSHAKEIN line to highdef HSK_high(): GPIO.output(25, GPIO.HIGH)# Set HANDSHAKEIN line to lowdef HSK_low(): GPIO.output(25, GPIO.LOW)# Set SPAREIN line to highdef SPR_high(): GPIO.output(11, GPIO.HIGH)# Set SPAREIN line to lowdef SPR_low(): GPIO.output(11, GPIO.LOW)# Place the byte to send to the TI on the data GPIO linesdef send_byte(b): for i in range(: if b[i] == '0': GPIO.output(data_ports[i], GPIO.LOW) else: GPIO.output(data_ports[i], GPIO.HIGH)# Read the command code sent from the TI to the GPIO data linesdef read_command(): for i in range(: GPIO.setup(data_ports[i], GPIO.IN) byte_value = '' global command for i in range(: if GPIO.input(data_ports[i]) == True: byte_value = byte_value + '1' else: byte_value = byte_value + '0' command = int(byte_value,2) for i in range(: GPIO.setup(data_ports[i], GPIO.OUT)# Main program looptry: while True: control_out() # Wait for SPAREOUT line to go high # Command code present on the data lines GPIO.wait_for_edge(24, GPIO.RISING) data_in() read_command() # Disable interrupt GPIO.remove_event_detect(24) GPIO.setup(24, GPIO.IN) if command == 1: # Command code 1: capture still image # Capture one image frame with picamera.PiCamera() as camera: camera.resolution = (256, 192) camera.start_preview() camera.iso = 100 time.sleep(2) camera.capture('image.data', 'rgb') picture = open('image.data', 'rb') # Acknowledge command receipt HSK_pulse() # Send image byte by byte over the data lines picture.seek(0) byte_counter = 0 data_out() print('Sending image data') while True: # Set up data lines with next byte # Prepare byte from file color_count = 1 # pack RGB values while color_count < 4: raw_byte = picture.read(1) byte_value = struct.unpack('<B',raw_byte) if color_count == 1: R = (int(byte_value) >> 5) & 7 else: if color_count == 2: G = (int(byte_value) >> 5) & 7 else: B = (int(byte_value) >> 6) & 3 color_count += 1 R = R << 5 G = G << 2 comp_byte = 0 comp_byte = R | G | B byte = bin(comp_byte)[2:].zfill( send_byte(byte) byte_counter += 1 # Signal the TI that a byte is available on the data line and wait for acknowledgement HSK_high() while GPIO.input(23) == False: pass HSK_low() if byte_counter == 49152: print('Image transfer done!') break # Wait for the TI to finish processing the byte while GPIO.input(23) == True: pass picture.close() GPIO.cleanup() GPIO.setup(24, GPIO.IN, pull_up_down = GPIO.PUD_DOWN) GPIO.setup(25, GPIO.OUT) GPIO.setup(23, GPIO.IN) GPIO.setup(8, GPIO.OUT) GPIO.setup(7, GPIO.OUT) GPIO.setup(11, GPIO.OUT)except KeyboardInterrupt: picture.close() GPIO.cleanup()
On the TI side, I initially used an extremely primitive method of processing the image where I averaged the values of the RGB colors for each row of 8 pixels after decompressing the received bytes and chose the closest color from the TI's palette to assign to that entire row. If you recall, the TI can only assign a single foreground and a single background color to each row of 8 pixels due to the limitations of the 9918 video display processor. For black and white, I used the same method of RGB averaging, but then selected a threshold between white and black that gave the best image. Needless to say that the results were less than stellar...
And this was where things stood for the next 3 years, until I decided recently to revisit the project. I contacted Tursi regarding the algorithms he used for his Convert9918 PC program, and he was kind enough to give me all the information needed for me to revamp the image processing program on the TI. I decided to skip color processing at this time given that it would have been very intensive from a processing and memory standpoint and would make for a very long image display time, although I might re-visit this at some point in the future.
Black and white
Obviously a major improvement! The whole image transfer process takes about 45 seconds, which is pretty reasonable. Source file for the half-tone program below
** TI VISION CONTROL PROGRAM **** HALF-TONE VERSION **** OCTOBER 2018 **** BY WALID MAALOULI ** DEF START REF VSBW,VSBR,VMBW,VMBR,VWTR,KSCANKEY EQU >8375 ADDRESS OF KEY PRESSED VALUEGPLSTS EQU >837C GPL STATUS BYTEPIO EQU >5000 PARALLEL PORT DATA BYTE ADDRESSTEMP BSS 6 TEMPORARY STORAGE AREASTEMP1 BSS 6TEMP2 BSS 30CHRTBL BSS 256*8 STORAGE SPACE FOR CHARACTER TABLECOUNTR DATA 6144 SCREEN PIXEL COUNTERCOL DATA 0 SCREEN COLUMN COUNTERROW DATA 0 SCREEN PIXEL ROW COUNTERLINE DATA 0 SCREEN LINE COUNTEREIGHT DATA 8ONE DATA 1PDT DATA 0 PATTERN DESCRIPTOR TABLE POINTERCT DATA 0 COLOR TABLE POINTERX DATA 0 X COORDINATE OF CURRENT PIXELY DATA 0 Y COORDINATE OF CURRENT PIXELPIXBYT BYTE >FF PIXEL PATTERN FOR A ROW OF 8 PIXELSPIXCTR BYTE 0 PIXEL COUNTERPIXTAB BYTE >80,>40,>20,>10,>8,>4,>2,>1ANYKEY BYTE >20TITLE TEXT 'TI VISION COMMAND'CMTXT1 TEXT 'PRESS ANY KEY TO INITIATE'CMTXT2 TEXT 'TARGET ACQUISITION' EVEN ** INITIALIZE TEXT MODE **START LWPI >8300 LI R0,>0731 GREEN LETTERS ON BLACK BACKGROUND BLWP @VWTR LI R0,>F000 VALUE TO BE LOADED IN VR1 MOVB R0,@>83D4 SAVE VALUE LI R0,>01F0 START TEXT MODE BLWP @VWTR BL @CLRTXT ** INITIALIZE RS232 CARD ** BL @INIT ** DISPLAY TITLE ** LI R0,11 LI R1,TITLE LI R2,17 BLWP @VMBW LI R0,407 LI R1,CMTXT1 LI R2,25 BLWP @VMBW LI R0,491 LI R1,CMTXT2 LI R2,18 BLWP @VMBW ** WAIT FOR USER COMMAND ** BL @KINPTX ** SWITCH TO BITMAP MODE ** LI R0,>0800 POINT TO PDT LI R1,CHRTBL LI R2,256*8 LENGTH OF CHARACTER TABLE IN R2 BLWP @VMBR SAVE PDT INTO CHARACTER TABLE LI R0,>A000 VALUE TO BE PLACED IN VR1 MOVB R0,@>83D4 SAVE VALUE IN INTERPRETER WORKSPACE LI R0,>01A0 BLWP @VWTR BLANK THE SCREEN LI R0,>0207 BLWP @VWTR SIT ADDRESS AT >1C00 LI R0,>0403 BLWP @VWTR PDT ADDRESS AT >0 LI R0,>03FF BLWP @VWTR CT ADDRESS AT >2000 LI R0,>053E BLWP @VWTR SPRITE ATTRIBUTE LIST AT >1F00 LI R0,>0603 BLWP @VWTR SPRITE DEFINITION TABLE AT >1800 LI R0,>1F00 LI R1,>D000 BLWP @VSBW MARK THE SAL AS INVALID (NO SPRITES DEFINED) LI R0,>1C00 INITIALIZE SIT CLR R1 LI R2,3SITINI BLWP @VSBW INC R0 AI R1,>100 JNE SITINI DEC R2 JNE SITINI LI R0,>2000 INITIALIZE COLOR TABLE LI R1,>F100 FOREGROUND = WHITE, BACKGROUND = BLACK LI R2,>1800CTSET BLWP @VSBW INC R0 DEC R2 JNE CTSET CLR R0 INITIALIZE PDT CLR R1 LI R2,>1800PDTSET BLWP @VSBW INC R0 DEC R2 JNE PDTSET LI R0,2 START BITMAP MODE BLWP @VWTR LI R0,>E000 TURN SCREEN ON MOVB R0,@>83D4 LI R0,>01E0 BLWP @VWTR ** START IMAGE CAPTURE **STRCPT SBZ 2 SET HANDSHAKEOUT LINE TO LOW LI R0,>0100 PLACE IMAGE CAPTURE CODE INTO R0 MOVB R0,@PIO TRANSFER BYTE TO DATA LINES LI R5,8 CLR @TEMP CLEAR TEMPORARY STORAGE AREA CLR @TEMP+2 CLR @TEMP+4 LI R8,TEMP SBO 3 SET SPAREOUT LINE TO HIGH SBZ 3 RESET TO LOWRPT TB 2 WAIT FOR HANDSHAKEIN LINE TO GO HIGH JNE RPTRPT10 TB 2 WAIT FOR HANDSHAKEIN LINE TO GO LOW JEQ RPT10 SBO 1 SET PIO PORT TO INPUT CLR R1 CLR @X CLR @YNXTBYT SBZ 2 SET HANDSHAKEOUT LINE TO LOWRPT0 TB 2 WAIT FOR HANDSHAKEIN LINE TO GO HIGH JNE RPT0 SBO 2 SET HANDSHAKEOUT LINE TO HIGH CLR R0 MOVB @PIO,R0 RECEIVE BYTE FROM RPIRPT1 TB 2 WAIT FOR HANDSHAKEIN LINE TO GO LOW JEQ RPT1 SWPB R0 ** BYTE CONTAINS RGB INFORMATION IN 3-3-2 BIT PATTERN ** UNPACK AND STORE COLOR COMPONENTS MOV R0,R2 MOV R0,R3 MOV R0,R4 SRL R2,5 UNPACK RED COMPONENT SLA R2,5 MOV R2,*R8+ SAVE RED COMPONENT SRL R3,2 UNPACK GREEN COMPONENT ANDI R3,7 SLA R3,5 MOV R3,*R8+ SAVE GREEN COMPONENT ANDI R4,3 SLA R4,6 UNPACK BLUE COMPONENT MOV R4,*R8 SAVE BLUE COMPONENT ** CALCULATE LUMA VALUE FOR BYTE LI R8,TEMP CLR R2 MOV *R8+,R1 GET RED VALUE A R1,R2 MULTIPLY RED VALUE BY 3 AND ADD TO TOTAL A R1,R2 A R1,R2 MOV *R8+,R1 GET GREEN VALUE SLA R1,2 MULTIPLY GREEN VALUE BY 4 AND ADD TO TOTAL A R1,R2 MOV *R8,R1 GET BLUE VALUE A R1,R2 ADD TO TOTAL SRL R2,3 DIVIDE TOTAL BY 8 - THIS IS THE LUMA VALUE ** PROCESS PIXEL** SCALE LUMA VALUE ACCORDING TO MATRIX** [0 2] --> [-25% 25%]** [3 1] --> [50% 0%] MOV @Y,R1 ANDI R1,1 CHECK IF EVEN OR ODD SLA R1,1 MULTIPLY BY 2 (ODD ROW GETS SECOND MATRIX ROW) MOV @X,R0 ANDI R0,1 CHECK IF EVEN OR ODD A R0,R1 R1 NOW HAS A VALUE FROM 0-3 MOV R2,R4 CI R1,0 JNE NOTZER SRL R4,2 SCALE LUMA VALUE TO 25% S R4,R2 SUBSTRACT FROM ORIGINAL LUMA VALUE JMP UPDXYNOTZER CI R1,1 JNE NOTONE SRL R4,2 SCALE LUMA VALUE TO 25% A R4,R2 ADD TO ORIGINAL LUMA VALUE JMP UPDXYNOTONE CI R1,2 JNE UPDXY LUMA VALUE UNCHANGED HERE MOV R2,R3 SRL R4,1 SCALE LUMA VALUE BY 50% A R4,R2 ADD TO ORIGINAL LUMA VALUE JMP UPDXY ** UPDATE X/Y PIXEL COORDINATESUPDXY INC @X POINT TO NEXT PIXEL IN ROW MOV @X,R0 CI R0,>0100 CHECK IF AT END OF ROW JNE APPTHR CLR @X INC @Y NEXT ROW ** APPLY LUMA THRESHOLDAPPTHR CI R2,255 CHECK IF ADJUSTED LUMA VALUE > 255 JLE LUMAOK LI R2,255 OTHERWISE CLAMP AT 255LUMAOK CI R2,128 JLT PIXOFF JMP CONT3 ** TURN OFF PIXEL - ALL PIXELS ON INITIALLY IN IMAGE BYTEPIXOFF CLR R1 MOVB @PIXCTR,R1 GET PIXEL COUNTER SWPB R1 CLR R0 MOVB @PIXTAB(R1),R0 POINT TO APPROPRIATE BIT LOCATION IN IMAGE BYTE CLR R1 MOVB @PIXBYT,R1 GET IMAGE BYTE SZCB R0,R1 CLEAR SELECTED BIT MOVB R1,@PIXBYT STORE BACK THE IMAGE BYTE ** REPEAT THE PROCESS UNTIL 8 PIXELS ARE PROCESSEDCONT3 DEC R5 8 PIXELS PER IMAGE BYTE JEQ CONT LI R8,TEMP POINT BACK TO BEGINNING OF STORAGE AREA INC @PIXCTR POINT TO NEXT PIXEL IN IMAGE BYTE CLR R1 JMP NXTBYT ** UPDATE THE PATTERN DESCRIPTOR TABLECONT CLR R0 MOVB R0,@PIXCTR RESET THE PIXEL COUNTER MOV @PDT,R0 POINT TO PDT LOCATION MOVB @PIXBYT,R1 GET THE MODIFIED IMAGE BYTE BLWP @VSBW MOVE THE BYTE TO THE PDT SETO R0 MOVB R0,@PIXBYT TURN BACK ON ALL PIXELS IN THE IMAGE BYTE DEC @COUNTR CHECK IF AT END OF IMAGE TRANSFER JEQ RECAP IF IT IS THEN SET UP FOR NEW CAPTURE A @ONE,@COL LI R1,31 C @COL,R1 JH NXTROW A @EIGHT,@PDT LI R8,TEMP CLR R1 LI R5,8 B @NXTBYTNXTROW CLR @COL INC @ROW INC @LINE C @ROW,@EIGHT JEQ CONT5 MOV @LINE,@PDT LI R8,TEMP CLR R1 LI R5,8 B @NXTBYTCONT5 CLR @ROW LI R1,248 A R1,@LINE MOV @LINE,@PDT LI R8,TEMP CLR R1 LI R5,8 B @NXTBYT ** SET UP FOR A NEW IMAGE CAPTURERECAP LI R1,6144 MOV R1,@COUNTR RESET TOTAL NUMBER OF BYTES IN IMAGE CLR @PDT CLR @COL CLR @ROW CLR @LINE SBZ 0 TURN OFF RS232 CARD SBZ 7 TURN OFF CARD LED BL @KINPTX WAIT FOR KEYPRESS BL @INIT RE-INITIALIZE THE RS232 CARD B @STRCPT START A NEW IMAGE CAPTURE PROCESS ********************************************************************************** CLEAR TEXT SCREEN ROUTINE **CLRTXT CLR R0 POINT TO SIT CLR R1 LI R2,960 LENGTH OF SITREDO BLWP @VSBW CLEAR SIT BYTE INC R0 DEC R2 JNE REDO REPEAT UNTIL SIT IS CLEARED RT********************************************************************************** INTIALIZE RS232 CARD ROUTINE **INIT LI R12,>1300 SELECT DSR ADDRESS OF CARD SBO 0 ACTIVATE CARD SBO 7 TURN CARD LED ON CLR @PIO CLEAR PARALLEL DATA LINES SBZ 1 SET PIO PORT TO OUTPUT SBZ 2 HANDSHAKEOUT LINE LOW SBZ 3 SPAREOUT LINE LOW RT********************************************************************************** TEXT MODE KEY INPUT ROUTINE **KINPTX CLR @GPLSTS BLWP @KSCAN READ KEYBOARD CB @ANYKEY,@GPLSTS BIT 2 OF GPLSTS IS SET WHEN DIFFERENT KEY PRESSED JNE KINPTX RESCAN IF SAME KEY PRESENT IN BUFFER RT******************************************************************************** END