Jump to content
  • entries
  • comments
  • views

Image Capture on the TI 99/4A Computer



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[0]) >> 5) & 7				else:					if color_count == 2:						G = (int(byte_value[0]) >> 5) & 7					else:						B = (int(byte_value[0]) >> 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    

Disk image with all the source and compiled files (E/A option 3): TIVISION.dsk

Here's a video of the entire project. Another fun one :)



  • Like 4

1 Comment

Recommended Comments

Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
  • Create New...