Using the PDP-11 Line Clock
The line clock allows time-based behaviour to be implemented. Here is a walkthrough of a sample application that uses the line clock.
What is the line clock?
The line clock is a signal that is generated from the alternating current being used by the PDP-11 power supply. This means that the frequency of the line clock signal will vary depending on where in the world you are. In the US, for example, the line clock signal would have a frequency of 60Hz whereas here in Ireland the line clock signal would have a frequency of 50Hz.
The line clock can operate in one of two modes, known as interrupt mode and non-interrupt mode, but only the interrupt mode is considered in this example. In interrupt mode, the line clock will generate interrupts at the frequency of the line signal (60Hz in the US or 50Hz in Ireland, for example).
Interrupt handling on the PDP-11
Briefly, interrupts are received by the CPU and handled in the following way:
When the interrupt is received, the priority of the interrupt is compared to the current priority of the CPU. The priority of the CPU is set in the Processor Status Word (bits 5-7), allowing priority values between 0 and 7. If the priority of the incoming interrupt is lower than the CPU priority, it is ignored.
If the priority of the incoming interrupt is higher than the CPU priority then the CPU completes execution of whatever instruction it is currently executing and then service the interrupt.
When the interrupt is received there will be an interrupt vector address provided by the interrupting device. At this address, the CPU expects to find two word values; the value to load into the Program Counter (PC) and the value to load into the Processor Status Word (PSW).
The CPU will push the current PC and PSW values onto the stack and load the PC and PSW from the interrupt vector.
The CPU will then begin executing instructions at the memory address pointed to by the PC value in the interrupt vector, until the RTI (Return from Interrupt) instruction is encountered.
When the RTI instruction is encountered, the CPU will pop the stored PC and PSW values off the stack and resume execution of the main application.
In the case of the line clock, the interrupt vector location is memory address 100.
The sample program concept
I wanted to implement a program that incremented the value displayed in the data register once every second, up to a maximum of 200 (octal) seconds after which the program loops back to zero and starts counting again.
Here's the approach I used:
Since the line clock interrupts happen at 50Hz (i.e. 50 times per second), use the R0 register to count up to 50, with the line clock interrupt handler incrementing the value held in R0 each time there is an interrupt.
When the value in R0 reaches 50, increment the value in the R1 register.
If the value of R1 equals the threshold value, in my case 200 in octal, then set the R1 register value to zero, otherwise display the value of the R1 register on the panel.
Here's the full code listing:
set cpu 11/70,4M ;set realcons=localhost set realcons panel=11/70 set realcons interval=8 set realcons connected set clk 50HZ ; line clock interrupt vector D 100 001200 D 102 000340 ; main application D 1000 MOV #100, @#177546 D 1006 NOP D 1010 CMP R0, #62 D 1014 BNE 1006 D 1016 MOV #0, R0 D 1022 INC R1 D 1024 CMP R1, #200 D 1030 BEQ 1040 D 1032 MOV R1, #177570 D 1036 BR 1006 D 1040 MOV #0, R1 D 1044 BR 1006 ; line clock interrupt handler D 1200 INC R0 D 1202 RTI RESET ALL SET CPU IDLE ; CPU priority is 5 D PSW 000240 D PC 001000 D SP 002000
The first part of the code relates to setting up the simulator:
set cpu 11/70,4M ;set realcons=localhost set realcons panel=11/70 set realcons interval=8 set realcons connected set clk 50HZ
As I mentioned in a previous post, I think the "realcons" lines are PiDP specific and I am just copying them from project to project until I have time to investigate them properly.
The only line in the section above that has not been any of my previous examples is the "set clk 50HZ" line. The default line clock speed in the PDP-11 emulator is 60Hz, so in the interest of having an authentic Irish experience I set this to 50Hz. This is obviously an optional step.
The next section of the code is the line clock interrupt vector:
; line clock interrupt vector D 100 001200 D 102 000340
Whenever a line clock interrupt is received, the interrupt vector at address 100 is used. As mentioned above, what happens is that the current PC and PSW values are pushed onto the stack, the value at address 100 is loaded into the PC and the value at address 102 is loaded into the PSW.
What this means, in effect, is that whenever there is a line clock interrupt the code at memory address 1200 will be run.
The PSW is loaded with the value of 340 (binary: 011 100 000). Recall that bits 5-7 of the
PSW are the CPU priority. With this PSW value, the CPU priority is set to 7, the highest priority, meaning that no other interrupts will be serviced until the line clock interrupt is complete and the previous PSW value is popped off the stack. By the way, the line clock interrupts have a hard-coded priority of 6.
The next section of the code is the main loop:
; main application D 1000 MOV #100, @#177546 D 1006 NOP D 1010 CMP R0, #62 D 1014 BNE 1006 D 1016 MOV #0, R0 D 1022 INC R1 D 1024 CMP R1, #200 D 1030 BEQ 1040 D 1032 MOV R1, #177570 D 1036 BR 1006 D 1040 MOV #0, R1 D 1044 BR 1006
The first line:
D 1000 MOV #100, @#177546
enables the line clock. The memory location 177546 is the configuration register for the line clock, and setting bit 6 (i.e. moving value 100 in octal into this register) enables interrupt mode for the line clock. Interestingly, I noticed when I was stepping through this code in the emulator that if you query the value of memory address 177546 you always get zero back, even after setting bit 6. I don't know if this is the expected behaviour or a bug in the emulator.
The next line is a no-operation (NOP). This is a stylistic choice that I made because I wanted to have a specific address line to jump to. There is no particular underlying meaning to this instruction and I could have left it out but I prefer the flow of the code as it currently stands.
The next couple of lines examine the value of the R0 register:
D 1010 CMP R0, #62 D 1014 BNE 1006
The first line compares the value of the R0 register to the octal value 62, which is 50 in decimal. Since the line clock operates at 50Hz, and the R0 value is incremented once per interrupt (see below), this value should be reached approximately every second.
If the R0 value does not equal 62 then we jump back up to address 1006, the NOP instruction just above. We loop around and around until the value of R0 equals 62, at which point we move on to the next section of the code:
D 1016 MOV #0, R0 D 1022 INC R1
Here we reset the R0 value back to zero again and increment the R1 value. We now have a situation where the R1 value is being incremented approximately once every second.
The final part of the code decides what to do now that the value of R1 has been changed:
D 1024 CMP R1, #200 D 1030 BEQ 1040 D 1032 MOV R1, #177570 D 1036 BR 1006 D 1040 MOV #0, R1 D 1044 BR 1006
The first line compares the value now contained in R1 with our upper limit value of 200. If R1 contains this value we jump down to address 1040, where we reset the R1 value back to zero again and then jump back to the NOP instruction at address 1006.
If the R1 value has not yet reached the upper limit then we move the value of R1 into the Status Control and Display Register, which will make it appear in the Data row on the panel. After displaying the value we just jump back up to the NOP instruction at address 1006.
The next section of code is the line clock interrupt handler:
; line clock interrupt handler D 1200 INC R0 D 1202 RTI
Note that this code is loaded at address 1200, the value indicated in the line clock interrupt vector. All this code does is increment the value in R0 and then return.
Finally, we have the inital setup values:
RESET ALL SET CPU IDLE ; CPU priority is 5 D PSW 000240 D PC 001000 D SP 002000
The RESET ALL and SET CPU IDLE lines have been discussed in a previous post but the other lines warrant some further explanation.
D PSW 000240
This loads the Processor Status Word with an initial value of 240 (binary: 010 100 000) which sets the initial CPU priority to 5. (because bits 5-7 are binary 101, i.e. 5). The line clock generates interrupts with priority 6 and this initial setting means that the line clock interrupts will be serviced by the CPU.
D PC 001000
This loads the Program Counter with an initial value of 1000. This will cause the main loop of the application to be executed when the program starts.
D SP 002000
This loads the stack pointer with an initial value of 2000. I'm not entirely sure why this line is necessary but when I developed my program above it wouldn't work and I couldn't figure out why. As I stepped through the program the behaviour was that a single line clock interrupt would be received and handled correctly but then no further interrupts were handled and the register values were not interrupting as they should.
Eventually I suspected that the problem was arising because of something to do with the act of servicing the interrupt and subsequently returning to the main code loop. While investigating this possibility I noted that the stack pointer contained a value of zero. I was aware that the stack grows by decrementing the stack pointer and since the stack point had a value of zero, decrementing would wrap around to the highest memory values, which are reserved for registers and this might be having unintended consequences.
Therefore, I selected a random memory location away from my main code and set the stack pointer to this value. When I did this the code worked perfectly.
It was my understanding that the stack resided in an area of memory after the trap and interrupt vectors, approximately in the range of addresses 377 to 777. I will need to investigate this further to understand what's going on here, but for the time being, the program works.
A couple of very helpful references that relate particularly to the operation of the line clock, which I used while I was researching this post: