The TMS9901 in the TI-99/4A
Operating the TMS9901
In I/O mode
In timer mode
+----+--+----+ RST1* |1 o 40| Vcc CRUOUT|2 39| S0 CRUCLK|3 T 38| P0 CRUIN |4 M 37| P1 CE* |5 S 36| S1 INT6* |6 35| S2 INT5* |7 9 34| INT7*/P15 INT4* |8 9 33| INT8*/P14 INT3* |9 0 32| INT9*/P13 PHI* |10 1 31| INT10*/P12 INTREQ*|11 30| INT11*/P11 IC3 |12 29| INT12*/P10 IC2 |13 28| INT13*/P9 IC1 |14 27| INT14*/P8 IC0 |15 26| P2 Vss |16 25| S3 INT1* |17 24| S4 INT2* |18 23| INT15*/P7 P6 |19 22| P3 P5 |20 21| P4 +------------+CRU interface
CE* Chip enable. This input pin decodes the remainder of the CRU address and becomes active (low) when the TMS9901 is the addressed device. Remember that the TI-99/4A only decodes part of the address bus, lines A3-A5!
CRUIN This output pin carries the current CRU bit to the CPU each time CE* is active.
CRUCLK This input pin receives the CRU clock pulse emmited by the TMS9900 for CRU ouput operations.
CRUOUT This input bit imports the current CRU bit from the CPU provided CE* is active (low) and a pulse is received on CRUCLK
INTREQ* This output pin sends an interrupt request signal to the CPU by becoming active (low).
IC0-IC3 These output pins carry the interrupt number (1 through 15) to the CPU. Note that this feature is not used on the TI-99/4A which considers all interrupts as level 1. These pins are thus not connected in the TI-99/4A console.
PHI* This pin receives a clock signal that will be used to synchronize the emission of interrupts and to decrement the counter in the timer. In the TI-99/4A console, this pin receives the PHI3* clock signal.
The TMS9901 features 21 pins that can be programmed as input, output or interrupt pin. An interrupt pin is a pin that generates an interrupt when it is low. Not every pin have the three functions though. In fact, I/O pins can be divided into three groups:
INT1*-INT6* Pure input pins. These pins cannot be programmed as output pins. They can be used as input pins (by setting their interrupt mask as 0 with CRU bits 1-15, which is the default) or as interrupt pins (by setting their mask as 1). Pins are numbered according to the interrupt code they place on pins IC0-IC3. Note that there is no INT0* pin.
P0-P6 Pure I/O pins. These pin cannot generate interrupts. When the TMS9901 is reset they are programmed an input pins by default. They can be turned into output pins by writting data to them via CRU bits 16 and higher. Once they are programmed as output they won't revert to input pins until the TMS9901 is reset. Caution: trying to force current into a pin programmed as output may damage the TMS9901!
INT7*/P15-INT15*/P7 Versatile pins. These pins have the 3 capabilities: by default they are input pins. They can be turned into interrupt pins by setting their mask as 1, or as output pins by writing to them.
RST1* This input pin resets the TMS9901, clears any interrupts, disables the timer and turns all pins into input pins.
Bit 0 is used to select the timer mode. When it equals 1, the TMS9901 is in timer mode and bits 1-15 have a special meaning (see below), when 0 it starts the timer (if needed) and bits 1-15 are used to control I/O pins, just as bits 16-31.
Bits 1-15 are used to read the status of the 15 interrupt pins (whether they are used as interrupt pin or as input pin). Writing to one of these bits does not output any data, but sets the interrupt mask for the corresponding pin: writing a 1 results in issuing interrupts when the pin is held low. The interrupt trigger is synchronized by the PHI* pin.
Bits 16-31 are used to read the status of the 15 programmable I/O pins,
provided they are used as input (or interrupt) pins. Note that the 8 versatile
pins INT7*/P15 through INT15*/P7 can be read either with bits 7-15 or with
bits 23-31. Writing to CRU bits 16-31 turns the corresponding pins into
output pins and places the bit values on the pins.
|Bit||R12 address||Meaning when read||Effect when written|
|0||>0000||Mode, should be 0||1: switch to timer mode|
|1||>0002||Value of the INT1* pin (#17)||Set interrupt mask for pin INT1* (1:int)|
|2||>0004||Value of the INT2* pin (#18)||Set interrupt mask for pin INT2*|
|3||>0006||Value of the INT3* pin (#9)||Set interrupt mask for pin INT3*|
|4||>0008||Value of the INT4* pin (#8)||Set interrupt mask for pin INT4*|
|5||>000A||Value of the INT5* pin (#7)||Set interrupt mask for pin INT5*|
|6||>000C||Value of the INT6* pin (#6)||Set interrupt mask for pin INT6*|
|7||>000E||Value of the INT7*/P15 pin (#34)||Set interrupt mask for pin INT7*/P15|
|8||>0010||Value of the INT8*/P14 pin (#33)||Set interrupt mask for pin INT8*/P14|
|9||>0012||Value of the INT9*/P13 pin (#32)||Set interrupt mask for pin INT9*/P13|
|10||>0014||Value of the INT10*/P12 pin (#31)||Set interrupt mask for pin INT10*/P12|
|11||>0016||Value of the INT11*/P11 pin (#30)||Set interrupt mask for pin INT11*/P11|
|12||>0018||Value of the INT12*/P10 pin (#29)||Set interrupt mask for pin INT12*/P10|
|13||>001A||Value of the INT13*/P9 pin (#28)||Set interrupt mask for pin INT13*/P9|
|14||>001C||Value of the INT14*/P8 pin (#27)||Set interrupt mask for pin INT14*/P8|
|15||>001E||Value of the INT15*/P7 pin (#23)||Set interrupt mask for pin INT15*/P7|
|16||>0020||Value of the P0 pin (#38)||Set output value of pin P0|
|17||>0022||Value of the P1 pin (#37)||Set output value of pin P1|
|18||>0024||Value of the P2 pin (#26)||Set output value of pin P2|
|19||>0026||Value of the P3 pin (#22)||Set output value of pin P3|
|20||>0028||Value of the P4 pin (#21)||Set output value of pin P4|
|21||>002A||Value of the P5 pin (#20)||Set output value of pin P5|
|22||>002C||Value of the P6 pin (#19)||Set output value of pin P6|
|23||>002E||Value of the INT15*/P7 pin (#23)||Set output value of pin P7|
|24||>0030||Value of the INT14*/P8 pin (#27)||Set output value of pin INT14*/P8|
|25||>0032||Value of the INT13*/P9 pin (#28)||Set output value of pin INT13*/P9|
|26||>0034||Value of the INT12*/P10 pin (#29)||Set output value of pin INT12*/P10|
|27||>0036||Value of the INT11*/P11 pin (#30)||Set output value of pin INT11*/P11|
|28||>0038||Value of the INT10*/P12 pin (#31)||Set output value of pin INT10*/P12|
|29||>003A||Value of the INT9*/P13 pin (#32)||Set output value of pin INT9*/P13|
|30||>003C||Value of the INT8*/P14 pin (#33)||Set output value of pin INT8*/P14|
|31||>003E||Value of the INT7*/P15 pin (#34)||Set output value of pin INT7*/P15|
|Bit||R12 address||Meaning when read||Effect when written|
|0||>0000||Mode, should be 1||1: switch to timer mode|
|1||>0002||Content of Read register (LSBit)||Data to write to Clock register (LSBit)|
|14||>001C||Ditto (Most Significant Bit)||Ditto (Most Significant Bit)|
|15||>001E||Value of the INTREQ* pin (#11)||0: Software reset (aka RST2*)|
For each pin, the latched bit is combined with a mask bit that determines whether the interrupt is active or ignored. These mask bits are set by the CPU via the CRU interface: a "1" enables the interrupt, a "0" masks it out.
On the rising edge of the same clock pulse, the duly masked bits are processed by the priority encoding logic. At the falling edge of the next clock pulse, the INTREQ* becomes active, and the code of the lowest active interrupt is placed on IC0-IC3. Provided there is an unmasked interrupt, of course.
The CPU can sense the status of the interrupt lines by reading the corresponding CRU bits. It is therefore possible to determine which line caused the interrupt. This is especially usefull in the TI-99/4A where the IC0-IC3 pins are not connected, and all interrupt are considered as level 1. Note that the CRU bit is not influenced by the mask bits: it accesses the INTx* pins directly. The CPU can also sense the status of the INTREQ* pin, by reading CRU bit 15 while in timer mode (i.e. after writing a "1" to CRU bit 0). This way, one can implement a polling strategy: interrupts are disabled at the CPU level (with LIMI 0), and the CPU periodically checks CRU bit 15 to determine whether an "interrupt" is pending.
In addition, an internal timer can also generate interrupts. These are assigned priority level 3, and the INT3* pin becomes a pure input pin (i.e. a low level on it will not generate interrupts). The mask bit for the timer is the same as for the INT3* pin. To clear the timer interrupt condition you must write to CRU bit 3. It does not matter whether you are writing a 0 or a 1, which allows you to leave interrupt 3 active or to disable it, while still clearing the pending condition.
+----------------+ CRU ====>| Clock register | +----------------+ || \/ +----------------+ =0 PHI* --->| Decrementer |-----> Interrupt 3 64 +----------------+ || \/ +----------------+ CRU <====| Read register | +----------------+A 14-bit long data word (i.e. >0000->3FFF, or 16384) can be loaded in the clock register via CRU bits 1-14, when the chip is in timer mode. CRU bit 1 is the least significant bit, CRU bit 14 is the most significant bit.
The TMS9901 can be returned to I/O mode by writing a 0 to CRU bit 0, or by accessing a bit higher than 15. If at that point the Clock register contains a non-zero value, it will be copied in the decrementer and decremented at every 64th clock pulse on the PHI* pin. The new value will constantly be copied to the Read register.
To access the current value of the decrementer, the TMS9901 should be placed in timer mode again (CRU bit 0 set to 1). This will freeze the update of the Read register, but will not stop the decrementer. The contents of the Read register can be read from CRU bits 1-14 with a STCR instruction.
Placing the TMS9901 in I/O mode again will resume updating of the Read register. However, if any bit between 1 and 14 is written to while in timer mode, the decrementer will be reinitialized with the current value of the Clock register. This is nice because it means that it is not necessary to reload all 14 bits in the Clock register: since they haven't changed, writting one of them (such as the least significant one) is enough to reload the whole data word.
Once the decrementer reaches zero, it reloads itself with the value stored in the Clock register and continues its decrementing job. At this point, it also issues a level 3 interrupt. If the corresponding mask was set to 1 (with CRU bit 3, in I/O mode), the INTREQ* line will become active to signal the interrupt to the CPU. Note that while the decrementer is working, pin INT3 cannot generate interrupts: it can still be read, but even a low level will not trigger interrupts. The decrementer will not generate any more interrupts after that one, unless re-enabled by entering and exiting timer mode.
The decrementer can be stopped by simply writing a zero to the leaving register, and leaving timer mode.
Note that it is NOT possible to leave the chip in timer mode in order to store a data word in the clock register and access it later from the Read register without having it decremented. That's because memory operations are likely to place an address in the range 16-31 on lines A10-A15. Although this is not a CRU operation, it is seen by the TMS9901 and results in exiting timer mode and decrementing will begin.
In timer mode, CRU bit 15 has different meanings when read or written to: reading bit 15 returns the current status of the INTREQ* pin. This could be used to check whether an interrupt request is currently sent to the CRU. Writing a 0 to CRU bit 15 resets the TMS9901. This is not the same as activating the RST1* line though, as this type of software reset (aka RST2*) only resets all I/O pins as pure input pins, but does not affect the timer.
|0||>0000||I/O||0: I/O mode 1: timer mode|
|1||>0002||I+||Peripheral interrupt incoming line|
|2||>0004||I+||VDP interrupts incoming line|
= . , M N / fire1 fire2
space L K J H ; left1 left2
enter O I U Y P right1 right2
(none) 9 8 7 6 0 down1 down2
fctn 2 3 4 5 1 up1 up2
shift S D F G A
ctrl W E R T Q
(none) X C V B Z
|11||>0016||-||(see bit 27)|
|12||>0018||I/I+||Pull up 10K to +5V|
|13||>001A||-||(see bit 25)|
|14||>001C||-||(see bit 24)|
|15||>001E||-||(see bit 23)|
|18||>0024||O||Select keyboard column (or joystick)|
|21||>002A||O||Select alpha-lock key|
|22||>002C||O||1: turn CS1 motor on|
|23||>002E||O||1: turn CS2 motor on|
|25||>0032||O||Output to cassette mike jack|
|26||>0034||-||(see bit 18)|
|27||>0036||I||Input from cassette headphone jack|
|28||>0038||-||(see bit 10: keyboard mirror)|
|29||>003A||-||(see bit 9)|
|30||>003C||-||(see bit 8)|
|31||>003E||-||(see bit 7)|
CLR R12 CRU base address of the TMS9901 TB 29 Read pin INT9*/P13 JNE TEST Do something LI R12,>000A CRU address for pin INT5* STCR R1,8 Read pins INT5* to INT12*/P10
To use a pin as an interrupt pin, set its mask to one. A low level on
this pin will now trigger an interrupt on the next PHI* pulse. You will
still be able to read the pin, which can be used by the ISR to determine
where the interrupt came from. To clear the interrupt status, write 1 again
to its CRU bit. To disable interrupts from this pin, write 0 to the bit.
CLR R12 CRU base address of the TMS9901 SBO 4 Enable interrupt (level 4) for pin INT4* ... TB 4 Read that pin (if = 0 an interrupt has occured) JEQ TEST Pin is high: no interrupt SBO 4 Clear interrupt condition, but leave mask enabled ... SBZ 4 Disable interrupt
To use a pin as an output pin, just write a value to it, it will appear
on the pin (0=low, 1=high). A pin that has been used once for output cannot
be used for input any more, until the TMS9901 is reset.
CLR R12 CRU base address of the TMS9901 SBZ 23 Set pin INT15*/P7 low LI R12,>0026 CRU address for pin P3 LI R1,>1300 Value to write LDCR R1,5 Write to pins P3 to INT15*/P7
EVENT1 CLR R12 CRU base of the TMS9901 SBO 0 Enter timer mode LI R1,>3FFF Maximum value INCT R12 Address of bit 1 LDCR R1,14 Load value DECT R12 There is a faster way (see below) SBZ 0 Exit clock mode, start decrementer ... EVENT2 CLR R12 SBO 0 Enter timer mode STCR R2,15 Read current value (plus mode bit) SRL R2,1 Get rid of mode bit LDCR R12,15 Clear Clock register, and exit timer mode S R2,R1 How many cycles were done?
R1 now contains the number of times the decrementer was decremented. Since this is synchronized by PHI3* on the TI-99/4A and we know PHI3* has a period of 333 ns (for a 3 MHz clock), we can easily calculate the elapsed time. One unit on the decrementer represents 64 clock periods, thus 64*333 = 21.3 microseconds. This is the resolution of the timer, the smallest time that it can measure.
The other limitation is that the maximal possible time is 16383*64*333 ns = 349.2 milliseconds. To time longer intervals we have three possibilities:
Otherwise, the only solution we have is to set the flag bit that will
cause the ISR to enter cassette management routines, no matter where the
interupt came from. Unfortunately, these routines do not preserve the return
address! The only way we can get around this problem is by enabling interrupts
at only one location in our program, and have our ISR return to it.
* This routine hooks the timer interrupts * It expects a delay value in R0 * and a branch vector in R1 (or >0000 to use a forever loop) TIMEON SOCB @H20,@>83FD Set timer interrupt flag bit MOV R12,@OLDR12 Preserve caller's R12 CLR R12 CRU base address >0000 SBZ 1 Disable peripheral interrupts SBZ 2 Disable VDP interrupts SBO 3 Enable timer interrupts MOV R1,@>83E2 Zero if we want to wait in a forever loop JEQ EVERLP SETO @>83E2 Flad: we intend to branch elsewhere MOV R1,@>83EC Set address where to go EVERLP SLA R0,1 Make room for clock bit INC R0 Set the clock bit to put TMS9901 in clock mode LDCR R1,15 Load the clock bit + the delay SBZ 0 Back to normal mode: start timer MOV @OLDR12,R12 Restore caller's R12 B *R11
* This routines "unhooks" the timer interrupt TIMOFF SZCB @H20,@>83FD Clear timer interrupt flag bit MOV R12,@OLDR12 Preserve caller's R12 CLR R12 CRU base address >0000 SBO 1 Enables peripheral interrupts SBO 2 Enables VDP interrupts SBZ 3 Disables timer interrupts MOV @OLDR12,R12 Restore caller's R12 B *R11
OLDR12 DATA 0 Temporary buffer for caller's R12 H20 BYTE >20 EVEN
* This is our ISR. All it does is to count the number of times it is called. OURISR INC @COUNT Count the number of times the timer fired LWPI >83C0 Back to interrupt workspace (R13, R15 unchanged) MOV @RETPT,R14 Get the return point (as R14 contains OURISR) RTWP Return to IRET
COUNT DATA 0 The event counter RETPT DATA 0 The return point
* This is the main program. It starts the timer with a delay of 1/3 second * and uses the value in COUNT to time a process. MAIN LI R0,>3D09 That's 333 usec LI R1,OURISR That's our hook CLR @COUNT Reset the counter BL @TIMEON Start the timer ... LI R11,IRET Desired ISR return point MOV R11,@RETPT From now on, timer interrupts will return at IRET LOOP LIMI 2 Enable interrupts IRET LIMI 0 Disable interrupts (Our ISR returns here) ... Perform the action to be timed JNE LOOP Until done BL @TIMOFF Stops the timer MOV @COUNT,R1 Divide by 3 to get the number of seconds ...
Because we have set the cassette flag bit, the console ISR will enter the cassette management routine after any interrupt, without checking where it came from. That routine will branch at the address provided in >83EC. The way it branches, is by copying >83EC in R14 of the interrupt workspace and performing a RTWP. This will of course overwrite the return address (that was in R14), and enter OURISR with the user workspace and status.
OURISR just increments a counter and returns to the caller. Unfortunately, it has no way to know where to return. So it returns at IRET, i.e. the LIMI 0 instruction. This has to be to correct return point, since the interrupt can only occur after a LIMI 2. The drawback is that there must be a frequently executed loop in the timed process, into which we can stuff these two instructions. Note that they could be more than one loop: all we have to do is to set the correct return point before each LIMI 2.
I tried that and it did not work. I had a hard time to figure out why, but finally I got it: when the cassette ISR branches to OURISR with a RTWP it restores the status register from R15 in the >83C0 workspace. This will automatically restore the interrupt mask of 2 that was set by the LIMI 2 instruction. But the cassette ISR did not necessarily clear the interrupt condition: it clears timer interrupts, but not VDP or peripheral interrupts. Therefore, another interrupt will occur immediately, before the first instruction of OURISR can be executed. And we are trapped in a forever loop, whithin the console ISR!
Of course, there are unwanted side-effects to this techniques:
What for? Well, to control any piece of hardware we could want to install directly inside the console, a switch to change clock speed for instance. Or a logic gate that would disable basic GROMs: this way, if you have a german GRAM-Karte, you can replace the console GROMs with your own operating system.
LI R12,>0006 CRU address of keyboard rows LI 1,>FF00 Interrupt mask=1 for all of them LDCR R1,8 Set mask LI R12,>0024 Select column 0 LDCR R12,3
Now we must hook the cassette ISR as above, since those interrupts would equally be mistaken for peripheral interrupts. Once this is done, your main program can simply scan the keyboard with: LIMI 2 LIMI 0. Our ISR will call the keyboard scanning routine (BL @>000E) and react to the key pressed by returning at various places in our program.
I agree, we could do the same with:
LI R12,>0006 CRU address of keyboard rows SETO R1 STCR R1,8 Read 8 pins INV R1 A pin reads as 0 if a key is pressed JNE KPRESS A key was pressed, find which, react.
But the "interrupt" way is more fun.
I don't think so, although I didn't dare to check. The input pins for
the keyboard row are pulled up via 10K resistors, thus the maximal current
they will sink if we set them as low output is 5V/10K= 0.5 mAmps. If we
set them as high, their current will be drawn by the 74LS138 decoder via
470 Ohm resistors, thus here again the current will be limited (assuming
4 Volts for a high pin, which is a lot): 4V/470=8.5 mAmps. And that's assuming
the decoder has no current limitation of itself. The TMS9901 data manual
does not state what's the minimal current that would cause damage to the
chip, but we ought to be safe with those values. Anyone willing to test?
Just get yourself an extra console and do:
LI R12,>0024 CRU address for column selection LI R1,>0100 LDCR R1,3 Select column 1
LI R12,>0038 Address of keyboard rows as output pins CLR 1 Try to force them low (against pullup) LDCR R1,4 As only 4 pins can be set as output BL @WAIT Allow time for damage ... SETO R1 Now try to force them high LDCR R1,4 Set output values to 1 ... Now press a key (2,S,W or X) and hold it down
Next, reset the TI-99/4A and test the keyboard. Is it still ok?? (Please, let me know).
______ _>225__ _______| 300-2000_| ______ PHI r\__/f \__/ \__/ \__/ __ 45-300 ________________________ INTn* \>60|_________________/>60| _____________________ ___ INTREQ* |>110\__________________|<110/
r: 5-40 ns f: 10-40 ns
CRU write cycle CRU read cycle ____ _________________ \______________/ \_________________ CE* |>100|______ | >300 | _________/10-185\______________________________________ CRUCLK |>100| |>60| XXXX/ valid address \XXXXXXXXXXXXXXXX/ valid address S0-S4 | >320 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ valid data INTx/Px | >200 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ valid CRUIN |>100| |>60| XXX/ valid data \XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX CRUOUT
Supply voltage: -0.3 to 10 V All inputs and output voltage: -0.3 to 10 V Continuous power dissipation: 0.85 W Free-air temperature: 0 to 70 `C Storage temperature: -65 to 150 `C
|Supply voltage, Vcc||4.75||5||5.25||V|
|Supply voltage, Vss||-||0||-||V|
|High-level input voltage||2||-||Vcc||V|
|Low-level input voltage||Vss-3||-||0.8||V|
|High-level output voltage||I = -100 uA
I = -200 uA
|Low-level output voltage||I = 3.2 mA||Vss||-||0.4||V|
|Input current (any pin)||V = 0 to Vcc||-||-||100||uA|
|Averrage supply current||Clock period = 330ns, T = 70 `C||-||-||150||mA|
|Small signal input capacitance||Freq = 1 MHz, any pin||-||-||15||pF|
Revision 1. 2/19/99. OK for release Revision 2. 3/30/99. Polishing Revision 3. 5/30/99. Tested and debugged examples Revision 4. 9/1/99 In CRU map, VDP+peripheral ints were inverted!
Back to the TI-99/4A Tech Pages