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:
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:
On to the next thing