Programmable Systems Interface TMS 9901
The Programmable System Interface (PSI) is a circuit that is used on the TI-99/4A main board, on some expansion cards, and on the Geneve.
The designation of the PSI is TMS9901, and this is the name that most users will be more familiar with.
TODO: Add text
Features
Package
Interrupt mode
In interrupt mode, bits 1 to 15 are used to set the interrupt mask (write), or to query the interrupt inputs (read). That is, the meaning of reading and writing the bits is indeed different.
This schema shows how the interrupt inputs are used.
This illustration is slightly simplified; there is a latch between each interrupt input and the mask AND gate. This is not relevant for our programming, though. For more details see the TMS 9901 Programmable Systems Interface Data Manual.
Checking the interrupt inputs
In general, every interrupt input can be read via CRU access:
CLR R12 TB 15 JEQ NOINT15
TB copies the bit at the specified CRU address to the Equal status bit. This means that JEQ jumps to the location when the bit is 1. Since all interrupt inputs are negative logic, reading a 1 means that there is no interrupt at this input (so I called it NOINT15).
If we want to read several interrupt inputs by a single instruction, we have to use STCR.
LI R12,16 STCR R1,8
This reads the states of the /INT8, /INT9, ..., /INT15 inputs and puts them in the most significant byte of R1. (See the specification of STCR and LDCR for transfers of up to 8 bits.)
Interrupt levels
Each interrupt input (/INT1 to /INT15) may be used to trigger an interrupt at another circuit, typically the CPU. For this purpose, the PSI offers an /INTREQ outgoing line. In the TI console, it is routed to the /INTREQ input of the TMS9900 CPU.
In addition there is a priority encoder, which is not shown here. This encoder outputs a 4-bit number which reflects the smallest number of the interrupt inputs where an interrupt has been sensed.
That is, if /INT4 and /INT7 are currently set to 0 (active), the priority encoder outputs (0100), which means 4.
The TMS9900 CPU can differentiate between these interrupt levels, and by using its LIMI command, we can set a threshold where interrupts are ignored. So when we use a LIMI 3, the interrupts of level 4, 5, 6 and higher will be ignored.
The bad news is that Texas Instruments did not use these priority lines in the TI console. In fact, these outputs (IC0 .. IC3) are not connected, and the respective interrupt level inputs at the CPU are hardwired to (0001), which means that every interrupt that occurs in the TI console is automatically set to level 1. So in order to block all interrupts from the 9901, a LIMI 0 must be executed. To allow all, LIMI 1 is sufficient. (For some reason, TI uses LIMI 2 in all occasions.)
Arming and disarming interrupts
As said above, every interrupt input can be read at any time in interrupt mode. In that sense, we don't really treat these inputs as interrupts inputs, but simply as input ports.
If we want a port to trigger an interrupt, we must arm this interrupt input. This is done by setting the respective bit to 1.
CLR R12 SBO 2
This enables interrupt input /INT2 to cause an /INTREQ when its input changes to 0. In the TI console, /INT2 is connected to the interrupt output of the VDP (video processor). If you want to block interrupts on that input, use SBZ.
Counter mode
Besides the I/O capabilities, the PSI offers a "real-time" clock. This should not be understood as a precise timer with seconds, hundreds of seconds or other but as a countdown timer that is decremented every 64 CPU clock cycles.
This schematic is taken from the Osborne book, redrawn by me in LibreOffice (and with added Load Buffer).
Using the timer
There are basically two ways of using the countdown timer:
- Interval timer: Set the counter to a value; wait until the counter reaches zero. The PSI can be set up to raise an interrupt at that instant.
- Event timer: Set the counter to a value; do something and then read the counter value.
The counter has 14 bits; that is, its maximum value is 0x3FFF (16383). The counter is decremented every 64 clock cycles, and for the TI-99/4A and the Geneve with a clock cycle time of 333ns, this means that the counter resolution is 21.333 µs. The whole 16384 steps take 0.3495 seconds (349.5 ms).
When the counter reaches 0, it is reloaded from the load buffer, and counting continues.
It is essential to understand that the counter always counts when the load buffer contains a non-zero value. That is, it even counts when we are not in clock mode.
Setting the timer
In order to set the load buffer and so to define the maximum counter value, we must switch to clock mode. This is achieved by setting bit 0 of the PSI to 1. In the TI/Geneve systems, the PSI is mapped to CRU base address 0, so we can do this
CLR R12 SBO 0
to enter clock mode. Using SBZ 0 returns to interrupt mode.
The effect of switching to clock mode is that the load buffer becomes accessible by the CRU addresses 1 to 14. Supposed that we want to load the buffer with the value 0x00FF, we may use these lines:
CLR R12 SBO 0 LI R1,>00FF INCT R12 LDCR R1,14
TODO: Check number orientation
As we see, we must change the CRU base. The LDCR operation always starts at the address defined in R12; if this is not 0, we have to set it here. Note, however, that the CRU address is always half of the R12 value, since the rightmost bit, A15, is not part of the address. If we want to start at bit 1, we must increase R12 by 2. This is, by the way, done wrong in several places in the literature where R12 is only set to 1.
There is no need for two separate CRU operations. We can combine the SBO and the LDCR to one operation if we define the rightmost bit of R1 as the bit 0 and the following bits as the timer value.
CLR R12 LI R1,>01FF (0000 0001 1111 1111) LDCR R1,15
Do not write all 16 bits. The last bit will change bit 15 of the PSI, which is the /RST2 bit. When we are in clock mode, writing a 0 to the /RST2 bit resets all I/O ports. This means that all pins are set to inputs, and devices that rely on them as outputs may be turned off unintentionally.
Reading the timer
We are interested for the current value of the timer at some occasions. However, since the counter is constantly changing (every 64 clock cycles), reading from the active counter may deliver a wrong result. Think, for instance, about a value 0x2000, which is decremented after we read the first bits, we may end up with 0x2FFF (2 from 0x2000, FFF from 0x1FFF).
For this reason, the timer value is copied to a read buffer. This happens at every 64th clock cycle, when the counter is also decremented, but only when we are not in clock mode.
To read the buffer, we must set the PSI to clock mode first, which freezes the counter value in the read buffer. Then we can read the counter using STCR.
CLR R12 SBO 0 STCR R1,15 SRL R1,1 SBZ 0
As we see, register 1 will contain bit 0 in its rightmost bit, so we shift R1 by one position to move it out.
Do not forget to return to interrupt mode, or the buffer will not receive further updates.
The weird S0 input
There is one peculiarity with the PSI. When the input line S0 is set to 1, the clock mode is left temporarily - until S0 is 0 again, and bit 0 is 1. If bit 0 is 0, this has no effect, as the clock mode is never entered.
What is the reason for this?
TI obviously considered use cases where the PSI remains in clock mode for longer times, while the I/O ports must still be operated. The I/O ports are available on bits 16-31, which means that the S0 input is 1 for these addresses. So when we do a SBO 16, this is not related to the timer value, and thus should control the first I/O port. To allow this, the chip returns to interrupt/port mode for this operation, and when S0 is reset, returns to clock mode.
In the TI console, the S0-S4 inputs are directly connected with the address bus lines A10-A14. This has a strange side effect: Whenever address bus line A10 changes to 1, the clock mode is left temporarily, and this also happens for ordinary memory accesses.
Suppose that we are in clock mode (SBO 0). In this mode, the read buffer should not be updated, so when we read it, we should get the same value every time, although the real counter value is different. But this is difficult to achieve: If our program uses a memory address where A10 is 1 for some instruction, loading this instruction into the CPU will set A10 to 1, and thus leave clock mode, which updates the counter.
Therefore, you may have the impression that the read register continues to be updated even though you set bit 0 to 1. But this is only happening because your program extends over a location where A10 is 1. So if you want to prove that in clock mode the read register remains fixed, your program must avoid all memory locations where A10 is 1.