Before jumping into Lunar Lander I wanted to write a program that was a lot less ambitious but still useful moving forward. Obsolescence Guaranteed's PROGRAMMING THE DEC PDP-1 - A QUICK WAY TO GET STARTED guide has a simple draw a circle example.
CIRCLE 100/ go, lac x lup, cma sar 4s add y dac y lio y sar 4s add x dac x dpy jmp lup x, 200000 y, 0 start go
Without going into a lot of detail:
- CIRCLE is the program name.
- 100/ is the memory address to start assembling at.
- The rest is the assembly language program itself. Labels are up to 3 characters followed by a comma.
- start go is a directive that kicks off the execution of the program at the address provided.
In order to get this snippet of code to run here is what a PDP-1 programmer would have had to do:
- Mount the paper tape with an editor onto the reader and load it into the machine. This would most likely have been ET (short for Expensive Typewriter or more formally 'the PDP-1 Symbolic Tape Editor'). ET is a modal editor much like VIM today.
- Once ET was loaded you could key in or edit your program using a hardcopy system console, often a Teletype Model 33 ASR.
- When done editing you would instruct ET via a "p" command to punch your code onto a paper tape remembering to add a STOP code at the end ("s" command).
- Mount the assembler (MACRO) paper tape and load it by pressing the READ IN switch on the console.
- Mount your program tape and load it by pressing the CONTINUE switch on the console. The console will type "Pass 1" (assuming there were no errors).
- Mount your program tape again and reload it by pressing CONTINUE again. "Pass 2" will be typed to the console and the paper tape punch will start to output the binary tape.
- Press CONTINUE one more time to punch a start block to the end of the tape.
- At this point you can remove the paper tape from the punch, load in into the tape reader, and run the code by pressing READ IN.
Easy right. To be fair when I was in high school in the late 60s I had to "keypunch" my programs onto cards and send them off to be run with 2 day turn around. So I would have welcomed the system described above. I sure hope that paper tape was cheap back then because they must have burned through a ton of it.
For my starter program I decided to make a line drawing routine based on Bresenham's algorithm since it is well suited to simple integer based systems with no floating point hardware like the PDP-1. This felt like it was:
- enough of a challenge to be interesting,
- definitely useful,
- and a nice self contained problem.
Here is the pseudo code that I used as my template:
plotLine(x0, y0, x1, y1) dx = abs(x1 - x0) sx = x0 < x1 ? 1 : -1 dy = -abs(y1 - y0) sy = y0 < y1 ? 1 : -1 error = dx + dy while true plot(x0, y0) e2 = 2 * error if e2 >= dy if x0 == x1 break error = error + dy x0 = x0 + sx end if if e2 <= dx if y0 == y1 break error = error + dx y0 = y0 + sy end if end while
The development progressed pretty well. Fortunately I did not have jump through as many hoops as the original PDP-1 programmers did to edit and run my code. On a Raspberry Pi with the PiDP-1 tools installed the steps I took were:
- Write the assembly code using a text editor. I just used the built in mousepad editor. It was fine.
- From a terminal window assemble the code using macro1_1. This will produce a .rim “paper tape” file and a .lst file with assembly details.
- From the desktop run the load tape application and select your .rim file. A virtual graphic of the tape will appear in the virtual tape reader.
- Click the READ IN switch on either the virtual or physical PiDP-1 console. You will see an animation of the tape loading and when it stops if all went well your program will start running.
As someone who has been using some kind of IDE to write programs for the past 35 years or so, writing code in this environment is a bit of a shock. You can't just view your program on a screen, set breakpoints by clicking on lines of code, or even easily add a “print” statement. There was a debugger available at the time, DDT (Dynamic Debugging Technique), that looks pretty useful. It was command driven and accessed through the teletype console. I had a few problems with my code but never had to make the effort to learn DDT (so far).
Bresenham's Line Drawing Algorithm
Here is the code:
/ Draw the line defined by the points X1,Y1 and X2,Y2. Called as subroutine.
drt, jmp .
dln, dap drt / Save the return address.
lac X1 / Get the starting coordinate.
dac x
lac Y1
dac y
lac X2 / Calculate dx.
sub X1 / X2 - X1
spa / Skip compliment if AC positive.
cma / Compliment AC to get positive value.
dac dx / Save absolute value in dx.
lac Y2 / Calculate -dy.
sub Y1 / Y2 - Y1
spa i / Skip compliment if AC negative.
cma / Compliment AC to get negative value.
dac dy / Save negative value in dy.
lio PSLOPE / Positive slope to start.
lac X2 / Calculate sx.
sub X1 / X2 - X1
spa / Skip switch if AC positive.
lio NSLOPE / Negative slope.
dio sx / Save slope in sx.
lio PSLOPE / Positive slope to start.
lac Y2 / Calculate sy.
sub Y1 / Y2 - Y1
spa / Skip switch if AC positive.
lio NSLOPE / Negative slope.
dio sy / Save slope in sy.
lac dx / Intitialize the error term.
add dy
dac err
/ Plot the points of the line.
lup, plot x,y / Draw the next point.
/ Figure out the next point to plot.
ctn, lac err / Calculate the e2 term.
sal 1s / e2 = err * 2
dac e2
/ Check for y change.
lac e2 / Is (e2 > dy)?
sub dy / e2 - dy
sma / Skip if negative.
jmp doy / Process y change.
jmp dox / Check for x change.
doy, lac X2 / Is x done?
sub x / x2 - x
sza i / Skip if not zero.
jmp dne / Line done.
lac err / err = err + dy;
add dy
dac err / err + dy
lac x / x = x + sx
add sx
dac x / x + sx
/ Check for x change.
dox, lac dx / Is (e2 < dx)?
sub e2 / dx - e2
sma / Skip if minus.
jmp prx / Process y.
jmp lup / Keep drawing line.
prx, lac Y2 / Is y done?
sub y / y2 - y
sza i / Skip if not zero.
jmp dne / Line done.
lac err / err = err + dx
add dx
dac err / err + dx
lac y / y = y + sy
add sy
dac y / y + sy
jmp lup / Keep drawing the line.
dne, jmp drt
/ Temporary variables used by drawline.
x, 0 / Used to "walk" the line.
y, 0
dx, 0 / Amount to change x and y on each step.
dy, 0
sx, 0 / Direction of x and y movement.
sy, 0
err, 0 / Error terms used in algorithm.
e2, 0
/ Line coordinates.
X1, 333 / Starting coordinate.
Y1, 333
X2, 1333 / Ending coordinate.
Y2, 1333
/ Constants.
PSLOPE, 1 / Line has positive slope.
NSLOPE, -1 / Line has negative slope.
This is pretty much a direct implementation of the pseudo code listed above. The two biggest problems that I had getting this code to run had nothing to do with the code itself but are I think worth mentioning.
Problem No. 1 Screen Coordinates
There is an actual opcode, coded as dpy, used to plot a point on the Type 30 Display. When dpy is executed it uses the value of the AC (accumulator) register as the X coordinate and the value of the IO (input-output) register as the Y coordinate to draw a dot on the CRT. Note that the hardware does this directly, there is no screen buffer so after a few seconds the dot will fade and be gone. This means that to draw a persistent image on the screen the program itself must continually refresh it. Problem No. 1A for me was that I had failed to read the fine print that only the upper 10-bits of each register was used to plot. Sigh.
Problem No. 1B was equally as stupid on my part. Another point I missed was that the origin of the screen coordinates is in the center of the screen. Like this:
+511
-511 0/0 +511
-511
I had assumed with my 21st century programming bias that the screen origin was in the lower left corner with increasing values going up and to the right. The annoying thing about both these problems, other than the fact that they were completely avoidable had I RTFM more carefully, was that I was getting dots on the screen, just not where I expected them. So I kept looking for bugs in my line code.
Both of these were corrected in my plot(x ,y) routine.
/ Swap values between AC and IO.
define swap
rcl 9s
rcl 9s
term
/ Draw a point at X,Y on the display. Note X and Y are positive integers
/ between 0 and 1023 that are converted to screen coordinates by this macro.
define plot X,Y
lac Y / Get the y coordibate.
add SOFFSET / Convert to screen coordinates.
sal 8s / Move y to high bits.
swap / Move value from AC to IO register.
lac X / Get the x coordinate.
add SOFFSET / Convert to screen coordinates.
sal 8s / Move x to high AC bits.
dpy-i / Draw pixel. Don't wait.
term
/ Constants.
SOFFSET, -777 / Convert 0-1023 to screen coordinates.
This a macro definition. As a todo list item I would like to get the line drawing algorithm to work directly with screen coordinates avoiding all of this extra overhead, making the routine much faster.
Testing and Problem No. 2
I wrote a couple of programs to test the line drawing code. The first just continuously generates random end point coordinates and draws a line between them.
... line and plot routines ...
/ Generate a random number.
define random
lac ran
rar 1s
xor (355670
add (355670
dac ran
term
/ Random number goes here.
ran, 0
/ Install sequence break routine.
3/ jmp sbf / Ignore sequence breaks.
jmp go / Start address of program.
/ Routine to flush sequence breaks if they occur.
sbf, tyi / Reset console typewriter.
lio 2 / Restore IO from address 2.
lac 0 / Restore AC from address 0.
lsm / Leave sequence break mode.
jmp i 1 / Jump indirect through address 1.
/ Mainline.
go, jsp dln / Draw the line.
random / Set new starting coordinate.
and LOWBITS
dac X1
random
and LOWBITS
dac Y1
random / Set new ending coordinate.
and LOWBITS
dac X2
random
and LOWBITS
dac Y2
jmp go / Do the next line.
/ Constants.
LOWBITS, 1777 / Mask off the upper 8 bits leaving the lower 10 bits.
/ Insert constants and variables, end of program.
constants
variables
start 4
At the top there is a random number macro that when called leaves an 18-bit random number in the AC register. This code is lifted directly from Norbert Landsteiner's ICSS document and can be found verbatim in the Spacewar code. Random will produce 262,144 unique 18-bit random numbers before repeating.
Just below random there is code to "flush sequence breaks if they occur". Again from ICSS this is part of the Computer Space program and Spacewar. Sequence breaks are interrupts. If someone presses a key on the console teletype a sequence break is initiated. This code basically just ignores such interrupts while the game is being played. I am following the conventions used in ICSS and Spacewar closely because I hope one day to run my code on a real PDP-1 and thus want to follow "best practices".
Problem No. 2 occurred when I tried running this program. Instead of seeing ever changing lines being drawn, the sequence of lines would start repeating after a small number of iterations, overwriting lines that could still be seen on the screen due to the (virtual) persistence. It's highly unlikely that random was broken. I posted this issue to the PiDP-1 Google Group. Finally I took a closer look at the Computer Space code and realized that I had not included the "constants" and "variables" directives as seen above. The simple CIRCLE program did not have them so I assumed they were optional (another bad assumption on my part). When I posted this finding Norbert Landsteiner replied:
Whenever the CONSTANTS keyword is encountered, any previously assembled
code is dumped onto a data block on tape and memory is allocated
in-place for any newly gathered constant expressions.
So the working theory is that in the absence of the CONSTANTS keyword the ran variable that random uses was not being initialized to 0 properly, and that the value being randomly ;-) picked up resulted in a short cycle. Tricky.
Here is the demo program running. I call it cloud chamber.
I like the way new lines get piled on top of the old lines as they fade to black. The long persistence of the Type 30 CRT phosphors (expertly rendered by the virtual PiDP-1 screen) can be used to great effect when creating demos.
Michael Gardi
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.