 • entries
11
44
• views
13,289

# Oscilloscope Graphics with the TI 99/4A Computer

I recently came across videos on YouTube showing how to use an arduino to create graphics on an oscilloscope. The basic idea was to use a digital to analog converter (DAC) to convert digital signals from the arduino into analog voltages which could be then fed into an oscilloscope set up in XY mode to create what essentially amounts to cartesian drawings. It certainly looked simple enough conceptually, and I figured I should be able to use the TI 99/4A computer's parallel port to re-create the same thing.

First off, I needed to make sure that I had the proper oscilloscope. We are all familiar with the standard oscilloscope mode, which is voltage against time. However, most 2 channel oscilloscopes have a special mode called XY mode, where one channel would be X (horizontal) and the other channel would be Y (vertical) in cartesian geometry, with the oscilloscope displaying a point of light at the intersection of the X and Y voltages. So in essence, by varying the X and Y voltages appropriately and rapidly enough, one could draw an image on the screen.
While one could use a modern digital oscilloscope for this, the latter would require quite a high rate of data transmission in order to give a decent image. Far better is to use an old school CRT based oscilloscope where the the phosphorous on the screen will decay relatively slowly and thus give a usable image at much lower data transfer rates. As luck would have it, I had picked up a used but perfectly functional Tektronics 2213A 2-channel oscilloscope a couple of years ago for under \$100, and it was the perfect device for this project.  Next I focused on constructing a simple DAC based on the R2R ladder design. Here's an excellent tutorial video which goes into all the gory details about that particular type of DAC:

https://youtu.be/AulX1OM7RwE

The TI's parallel port is an 8-bit port, and therefore if I want to output 2 channels (one for X and another for Y), I would be limited to a 4-bit resolution for each channel. In other words, I can only output digital values from 0-15 for each. That's a pretty low resolution, but it is what I have to work with. I suppose I could have created some form of multiplexer to expand my channel resolution, but I wanted to keep the external hardware as simple as possible.

Here's the circuit I came up with: Technically the resistors should have been at an exact ration of 1:2, but it's really not that critical as long as they're close enough to that ratio. I used 1K and 2.2K resistors because that's what I had on hand, but you can use any other values you want as long as the 1:2 ratio is maintained. The capacitors' value is very important in order to bridge the gap between the points on the oscilloscope screen. If they are skipped then all you get is points with no lines connecting them. Too low a value and the lines are very faint, too high and you get distortions. I came up with the 4.7nF value through trial and error, although there has got to be a more scientific way to go about it The DAC is divided into 2 separate sections, one for each channel. Since the voltage increases from left to right, it is important to pay particular attention to the wiring of the parallel port data lines which go from D0 (MSB) to D7 (LSB) as noted in the schematic. Initially I had wired it straight from D0 to D7, and it took me a while to figure out why the darn thing was not working since the higher order bits were producing less, not more voltage than the lower order ones!

And here's the finished product. I just used a proto board and did point to point connections for simplicity.  I use a parallel port breakout board to access the various lines on the parallel port. The breakout board is connected via a ribbon cable to the PIO port on the RS232 card in the TI Peripheral Expansion box. The oscilloscope probes attach to the board as seen above. Yup, it's a rat's nest of wires, but it works!

With the hardware in place, it was time to start working on the software side of things.
Drawings are encoded using a simple cartesian system. Since we have only a 4-bit resolution per channel, each X and Y axis will go from 0 to 15, and for each point we want to send out all we have to do is figure out its coordinates on the 16x16 grid and convert that into a single binary number we can put out to the parallel port. The X axis uses the upper 4 bits of the port, the Y channel uses the lower 4 bits. Therefore, the formula for converting the cartesian coordinates to a binary number is X*16+Y.
For example, if X=5 and Y=1, then we would output 5*16+1=81 (01010001 in binary). What the DAC will see is 0101 (5 decimal) on X and 0001 (1 decimal) on Y, and therefore it is obvious that the voltage on X will be higher than on Y, so the point displayed on the oscilloscope screen will be 5 units to the right and one unit upwards, with the 0,0 origin being on the lower left corner.

With that in mind, I wanted to create a program with an integrated drawing editor which I could use to create the drawings on a grid, then have the program do the appropriate coordinate conversions and send out the result to the parallel port. I was initially hoping to use Rich Extended Basic exclusively taking advantage of its CALL IO feature to access the parallel port, however it turned out that it was not fast enough to beat the phosphorous excitation decay time on the oscilloscope screen and all I was getting was points. So I ended up using Extended Basic for the editing and conversion functions, along with a support assembly subprogram to actually send the data to the parallel port with a maximal rate measured at about 27.3 kHz.
That output frequency was barely enough however to give a good image on the oscilloscope without flicker and nice lines between points, but adding even a simple keyboard scanning routine to allow the assembly subroutine to return to Basic would slow things down causing the lines between the points to start fading as the phosphorous excitation decayed. Therefore, once the port output was initiated, there was no way to return to editing and the computer would have to be rebooted...
I also wanted to have the option of rotating the drawing in the X axis, and this worked, but delays needed to be introduced after each rotation frame during data output to the port in order to not have all the frames blurred together, and this led to very faint lines between the points since the output frequency went down accordingly...

Here's the Extended Basic program

`100 CALL CLEAR110 CALL INIT :: CALL LOAD("DSK5.PIOOUT")120 DIM VERTEX(256),POINT(16,16),XVERTEX(30),YVERTEX(30),SINE(17)130 DEF MAP(X)=INT((X-XMIN)*(OUT_MAX-OUT_MIN)/(XMAX-XMIN)+OUT_MIN)140 CALL CHAR(126,"FF818181818181FFFFFFFFFFFFFFFFFF0000183C3C18000")150 DISPLAY AT(2,19)BEEP:"WAIT..."160 X=2 :: Y=2 :: VPOINTER=0 :: CALL SPRITE(#1,126,5,(Y-1)*8+1,(X-1)*8+1)170 FOR I=2 TO 17 :: CALL HCHAR(I,2,126,16) :: NEXT I180 N=0190 FOR PIRANGE=0 TO 588.75 STEP 39.25 :: SINE(N)=SIN(PIRANGE/100) :: N=N+1 :: NEXT PIRANGE200 FOR I=0 TO 15 :: FOR J=0 TO 15 :: POINT(I,J)=0 :: NEXT J :: NEXT I210 DISPLAY AT(2,19)BEEP:"       "220 REM   DRAW IMAGE230 DISPLAY AT(2,18):"E/S/D/X:" :: DISPLAY AT(3,19):"CURSOR"240 DISPLAY AT(4,18):"SPACE BAR:" :: DISPLAY AT(5,19):"ON/OFF"250 DISPLAY AT(6,18):"V:" :: DISPLAY AT(7,19):"SET VERTEX"260 DISPLAY AT(8,18):"R:" :: DISPLAY AT(9,19):"DEL VERTEX"270 DISPLAY AT(10,18):"N:" :: DISPLAY AT(11,19):"NEW IMAGE"280 DISPLAY AT(12,18):"P:" :: DISPLAY AT(13,19):"SEND > PIO"290 CALL KEY(0,K,S) :: IF S=0 THEN 290300 IF K=ASC("S")THEN 380 ELSE IF K=ASC("D")THEN 400310 IF K=ASC("E")THEN 420 ELSE IF K=ASC("X")THEN 440320 IF K=32 AND POINT(X-2,Y-2)=0 THEN POINT(X-2,Y-2)=1 :: CALL HCHAR(Y,X,127) :: GOTO 290330 CALL GCHAR(Y,X,C)340 IF K=32 AND POINT(X-2,Y-2)=1 AND C<>128 THEN POINT(X-2,Y-2)=0 :: CALL HCHAR(Y,X,126) :: GOTO 290350 IF K=ASC("V")THEN 470 ELSE IF K=ASC("R")THEN 520360 IF K=ASC("N")THEN CALL CLEAR :: GOTO 150370 IF K=ASC("P")THEN 560 ELSE 290380 IF X=2 THEN X=18390 X=X-1 :: CALL LOCATE(#1,(Y-1)*8+1,(X-1)*8+1) :: GOTO 290400 IF X=17 THEN X=1410 X=X+1 :: CALL LOCATE(#1,(Y-1)*8+1,(X-1)*8+1) :: GOTO 290420 IF Y=2 THEN Y=18430 Y=Y-1 :: CALL LOCATE(#1,(Y-1)*8+1,(X-1)*8+1) :: GOTO 290440 IF Y=17 THEN Y=1450 Y=Y+1 :: CALL LOCATE(#1,(Y-1)*8+1,(X-1)*8+1) :: GOTO 290460 REM   ADD VERTEX POINT470 CALL GCHAR(Y,X,C) :: IF C=128 THEN 290480 IF POINT(X-2,Y-2)=0 THEN POINT(X-2,Y-2)=1490 XVERTEX(VPOINTER)=X-2 :: YVERTEX(VPOINTER)=Y-2500 VERTEX(VPOINTER)=(X-2)*16+Y-2 :: VPOINTER=VPOINTER+1 :: CALL HCHAR(Y,X,128) :: GOTO 290510 REM   REMOVE VERTEX POINT520 CALL GCHAR(Y,X,C) :: IF VPOINTER=0 OR POINT(X-2,Y-2)=0 OR C<>128 THEN 290530 IF VERTEX(VPOINTER-1)<>((X-2)*16+Y-2)THEN 290540 VERTEX(VPOINTER-1)=-1 :: VPOINTER=VPOINTER-1 :: CALL HCHAR(Y,X,127) :: GOTO 290550 REM   SEND IMAGE TO PIO PORT560 IF VPOINTER<=30 THEN DISPLAY AT(15,18)BEEP:"ROTATE?" :: ACCEPT AT(15,26)VALIDATE("YN"):ANS\$570 CALL LINK("ARYLOC",MEMADR)580 CALL LOAD(MEMADR,VPOINTER) :: MEMADR=MEMADR+1590 IF ANS\$="Y" THEN 640600 FOR I=0 TO VPOINTER-1 :: CALL LOAD(MEMADR+I,VERTEX(I)) :: NEXT I610 DISPLAY AT(15,18):" "620 CALL LINK("DATOUT")630 GOTO 290640 DISPLAY AT(15,18):" " :: DISPLAY AT(24,2):"PROCESSING. PLEASE WAIT..."650 COUNT=0 :: MEMADR=MEMADR+1660 XMAX=0 :: XMIN=500670 FOR I=0 TO VPOINTER-1680 XMAX=MAX(XMAX,XVERTEX(I)) :: XMIN=MIN(XMIN,XVERTEX(I))690 NEXT I700 XMID=(XMAX+XMIN)/2 :: XHALF=(XMAX-XMIN)/2710 FOR N=0 TO 16720 DX=SINE(N)*XHALF :: OUT_MAX=XMID+DX :: OUT_MIN=XMID-DX730 FOR I=0 TO VPOINTER-1 :: RVERTEX=MAP(XVERTEX(I))*16+YVERTEX(I) :: CALL LOAD(MEMADR+COUNT,RVERTEX) :: COUNT=COUNT+1 :: NEXT I740 NEXT N750 DISPLAY AT(24,1):" "760 CALL LOAD(MEMADR-2,COUNT) :: CALL LOAD(MEMADR-1,VPOINTER) :: CALL LINK("ROTATE") `

All the calculations are being done in XB, and then the final data is sent to a reserved memory area in lower memory where the assembly subprogram accesses it and sends it out to the parallel port. That way the throughput speed of the port is maximized. Rotation of the drawing is actually done by using a sine function to remap the coordinates of the drawing around its center, thus giving the illusion of rotation around the X axis. Only a total of 16 frames are pre-calculated given the low resolution of the drawing as more frames would not result in a significant improvement in the rotation effect.

And here's the assembly support subroutine:

`** XB PIO LOW LEVEL DATA OUT ROUTINE ****          AUGUST 2017              ****       BY WALID MAALOULI           **        DEF  DATOUT,ARYLOC,ROTATEPIO    EQU  >5000             PIO PORT DATA ADDRESSFAC    EQU  >834A             FLOATING POINT ACCUMULATOR ADDRESSXMLLNK EQU  >2018NUMASG EQU  >2008REGSTR BSS  8                 STORAGE FOR RETURN REGISTERSVERTEX BSS  500               STORAGE FOR VERTICES ** SEND ADDRESS OF VERTICES STORAGE LOCATION IN MEMORYARYLOC MOV  R11,@REGSTR       SAVE RETURN REGISTERS       MOV  R13,@REGSTR+2       MOV  R14,@REGSTR+4       MOV  R15,@REGSTR+6       CLR  @FAC       LI   R0,VERTEX       MOV  R0,@FAC           PLACE STORAGE ADDRESS IN FAC       BLWP @XMLLNK       DATA >0020             CONVERT TO FLOATING POINT       CLR  R0       LI   R1,1              SELECT ARGUMENT 1 OF XB CALL       BLWP @NUMASG       B    @RETURN ** SEND UNROTATED ARRAY TO PIO PORTDATOUT BL   @PIOINIREDO   CLR  R3       MOVB @VERTEX,R3        GET TOTAL NUMBER OF VERTICES IN ARRAY       SWPB R3       LI   R2,VERTEX+1LOOP   MOVB *R2+,@PIO         SEND ARRAY BYTE TO PIO       DEC  R3       JNE  LOOP       JMP  REDO ** SEND ROTATED ARRAY TO PIO PORTROTATE BL   @PIOINIREDO1  CLR  R3       MOVB @VERTEX,R3        GET TOTAL NUMBER OF ELEMENTS IN ARRAY       SWPB R3       CLR  R4       MOVB @VERTEX+1,R4      GET NUMBER OF ELEMENTS IN EACH ROTATION FRAME       SWPB R4       MOV  R4,R6       LI   R2,VERTEX+2       START OF ARRAY DATALOOP1  MOVB *R2+,@PIO       DEC  R6       JNE  CONT1       MOV  R4,R6       BL   @DELAY            INTRODUCE A DELAY BETWEEN EACH ROTATION FRAMECONT1  DEC  R3       JNE  LOOP1       JMP  REDO1 ** INITIALIZE RS232 CARDPIOINI LI   R12,>1300         PLACE RS232 CARD CRU ADDRESS IN R12       SBO  0                 ACTIVATE CARD       SBO  7                 TURN CARD LED ON       SBZ  1                 SET PIO PORT TO OUTPUT       RT ** DELAY LOOPDELAY  LI   R5,10000CONT   DEC  R5       JNE  CONT       RT ** RETURN TO XBRETURN MOV  @REGSTR,R11       MOV  @REGSTR+2,R13       MOV  @REGSTR+4,R14       MOV  @REGSTR+6,R15       LI   R12,>1300         PLACE CRU ADDRESS OF RS232 CARD IN R12       SBZ  7                 TURN CARD LED OFF       SBZ  0                 TURN CARD OFF       RT        END                                                             `

Notice that it does not return to XB once an image is sent out the parallel port, and just loops around.

And finally a short video demonstrating the project:

https://youtu.be/-S5u7uiUhYk

On to the next thing • 3

That's pretty cool!

• 1

What you could do is have one of the bits select between two sample & hold circuits. So while X is passing through the DAC output, Y is holding the previous value & vice-versa. Then you would have 128 levels. Or instead of having the DAC drive the channels directly, you put them through an integrator. Then instead of X, Y you're generating dX/dY and potentially much higher resolution. dX/dY also requires some way of generating negative input to the integrator and it also helps to have a way to zero the integrators.

If you're interested, try and find a document titled something like "the secret life of vector generators", or have a look at the Vectrex for how it generated a display.

• 1

That's raelly cool. Your DAC + capacitor system for drawing reminds me of the Vectrex, I guess that was already mentioned. • 1

What you could do is have one of the bits select between two sample & hold circuits. So while X is passing through the DAC output, Y is holding the previous value & vice-versa. Then you would have 128 levels. Or instead of having the DAC drive the channels directly, you put them through an integrator. Then instead of X, Y you're generating dX/dY and potentially much higher resolution. dX/dY also requires some way of generating negative input to the integrator and it also helps to have a way to zero the integrators.

If you're interested, try and find a document titled something like "the secret life of vector generators", or have a look at the Vectrex for how it generated a display.

Alternating the X and Y outputs is a brilliant idea as it does not require any additional hardware. The only downside would be that it would cut the port output frequency by half since we would be processing 2 values for each point instead of one. I will have to test it out and see how that will affect the image quality.

One thing I have not figured out is how to blank the electron beam when I want to move from one point to another without having a line drawn. As things stand currently, my drawings have to constitute a loop otherwise I get a line from the last vertex to the first as the beam resets to the beginning.

I couldn't find anything tangible on the net but I have a feeling that additional hardware would be required... This would be essential if I want to be able to create complex drawings on the screen.

To blank the beam you set the intensity to zero - which is probably impossible for an O-scope if it doesn't have that as an input.

Although if you move the beam really, really fast it might not show up as much.

To blank the beam you set the intensity to zero - which is probably impossible for an O-scope if it doesn't have that as an input.

Although if you move the beam really, really fast it might not show up as much.

I was afraid of that. My scope is a very basic one and there is no such facility. Unfortunately, the TI is not fast enough to do this either. Oh well... ×   Pasted as rich text.   Paste as plain text instead

Only 75 emoji are allowed.