Programmable Systems Interface TMS 9901

From Ninerpedia
Revision as of 21:17, 21 April 2022 by Mizapf (talk | contribs)
Jump to navigation Jump to search

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

This is the pin assignment according to the 9901 specification.

Pack9901.png

You can see pins of related functions marked by the same background color.

  • Orange: Interrupt inputs
  • Red: CRU bit selection (00000 - 11111, i.e. 0-31 decimal)
  • Blue: CRU interface
  • Violet: I/O ports
  • Yellow: Outgoing interrupt and level

Note that some pins have a double function, or in other words, two functions share the same pin. I will discuss this in the following sections.

Input/Output port operation

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.

Int9901.png

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

LIMI 0 

must be executed. To allow all,

LIMI 1 

is sufficient. For some unclear reason, TI used 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.

It is a common misunderstanding that interrupts can be cleared by using a SBZ on an input. This can, for example, be seen in the popular TI Intern book. Instead, SBZ n simply blocks interrupt input /INTn. In the case of the video processor, it is up to the VDP to clear its interrupt, and this is done by reading its status register.

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).

Clock9901.png

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.

Safety precautions

The TMS9901 PSI is an interrupt encoder and an I/O circuit. You may have heard that improper use of I/O circuits may damage the circuit so that it must be replaced. Is this possible with the 9901 as well? Is there a way to kill the 9901?

Short answer: Yes.

Longer answer: Yes, but it depends on the way how the ports are used.

If the port is used as an input, nothing bad will happen, since the ports have high input impedance. You can put 0V or 5V directly to the input; of course, you should not use excessive voltage.

Damaging the 9901 ports

The problem is when the port is configured as an output port. In that case it may output 0V (0) or 5V (1). When you connect the output to the opposite voltage, you will cause a short circuit which quickly exceeds the chip's maximum power dissipation of 750 mW. This means you will fry the chip.

Consider the following setup (but don't try it!).

Badport9901.png

Obviously, port P4 is assumed to be an input. When the switch is open, port P4 will sense a 1 on its input. When the switch is closed, it will sense a 0.

Now what happens when you execute this code?

CLR  R12
SBO  20

This will program port P4 to be an output, and to show a 1 (5V) at the output. SBZ will, in contrast, make this port output a 0. So how do you make it an input port? - This is done by resetting the chip. As long as you don't use a SBZ/SBO (or a LDCR) on the ports, the ports are inputs.

Nothing has happened up to now. But now imagine that you press the switch. This will put the 1 output to ground, causing the short circuit, and your 9901 is toast.

What about the TI console?

So can we damage the 9901 in the TI console by a suitable CRU operation? - Luckily not.

The reason is the way how TI used the ports on the mainboard. Most of the ports are configured as outputs. Nothing happens when you reset them to inputs.

  • The pure interrupt inputs (/INT1 to /INT6) are safe, as they cannot output anything. They have no output driver that could be fried.
  • P0 and P1 are not connected.
  • P2, P3, and P4 are designed as outputs, so they cannot be turned from an input to an output.
  • P5 is the selector of the Alpha Lock key, so output as well.
  • P6 and P7 (/INT15) are the cassette motor control outputs.
  • P8 (/INT14) is the audio gate output.
  • P9 (/INT13) is the cassette output.
  • P10 (/INT12) is pulled up to 5V via a 10 kOhm resistor. If you put a 0 on this port (5V), the maximum current is 500 µA, which results in a power dissipation of 2.5 mW (far below the maximum 750 mW).
  • P11 (/INT11) is the cassette input. However, there is a resistor of 2.2 kOhm (R413) which effectively guards the port of high currents.
  • P12, P13, P14, P15 (/INT10 ... /INT7) are the keyboard input lines. Again, we have pull-ups of 10 kOhm against +5V, but also 470 Ohm on the line, which means a maximum of 8.5 mA or 42.5 mW. This is still well below the maximum value.

So we see that resistors are the best way to protect the ports. And we may calm down; we cannot burn the 9901 in the TI console by a bug in our program.