TMS9901 Programmable systems interface

Pinout
CRU interface
Interrupts interface
The internal timer

The TMS9901 in the TI-99/4A
Functions
CRU map

Operating the TMS9901
In I/O mode
In timer mode
Fun stuff

Timing diagrams
Electrical characteristics

Introduction

As indicated by its name, this highly versatile chip is in charge of interfacing a CPU, in our case the TMS9900, with the outside world. It communicates with the CPU via 32 bits in the CRU address space, and with the outside world with 21 pins that can be programmed as input, output, or interrupt pin. It also has an internal timer that can be used to generate interrupts or just to measure elapsed time.


Pinout

       +----+--+----+ 
 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
S0-S4. These input pins tell the TMS9901 which CRU bit the CPU wants to read/write. On the TI-99/4A these pins are controlled by the address bus, lines A10-A14.

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

Interrupt interface
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.

I/O interface
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.

Power supply
Vcc +5V
Vss Ground



CRU interface

The TMS9901 interacts with the CPU via 32 CRU bits. So why do we need 32 bits to control 21 pins? That's because these pins have multiple functions. In addition, the meaning of most these bits is very different when read or written.

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


Timer mode


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)
2 >0004 Ditto Ditto
3 >0006 Ditto Ditto
4 >0008 Ditto Ditto
5 >000A Ditto Ditto
6 >000C Ditto Ditto
7 >000E Ditto Ditto
8 >0010 Ditto Ditto
9 >0012 Ditto Ditto
10 >0014 Ditto Ditto
11 >0016 Ditto Ditto
12 >0018 Ditto Ditto
13 >001A Ditto Ditto
14 >001C Ditto (Most Significant Bit) Ditto (Most Significant Bit)
15 >001E Value of the INTREQ* pin (#11) 0: Software reset (aka RST2*)


Interrupt interface

With the TMS9901, interrupts are level-triggered, i.e. they are triggered by a low voltage on the interrupt pins INT1*-INT15* (as opposed to edge-triggered interrupts, that would react to a voltage change from high to low). The chip stores the status of these pins in internal latches on the falling edge of the clock pulse on the PHI pin, at each and every clock cycle. This means that the interrupt status will be forgotten at the next PHI pulse if the pin does not remain low.

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.



The internal timer

The TMS9901 contains an internal decrementer that can be used as a timer, in conjunction with two registers: the Clock register and the Read register, which together constitute the CRU interface.
         +----------------+ 
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.



The TMS9901 in the TI-99/4A

In the TI-99/4A console, the TMS9901 is used for many purposes:
  • Scanning the keyboard.
  • Signalling VDP and external interrupts to the CPU.
  • Controlling the cassette tape recorders (motors, input and output).
  • Placing sound data directly to the monitor's speaker.
  • Keyboard scanning

    The keyboard is a matrix of six columns of eight keys. Each row of keys is connected to an input pin on TMS9901 (pins INT3* to INT10*/P12). The TMS9901 uses 3 output pins (P2 to P4) to controls a 74LS138 decoder and select a column (column 6 and 7 select the joysticks). If a value of 0 appears on one of the input pins, it means that a key is down at the intersection of this row and the currently selected column. An extra output pin controls the alpha-lock key to avoid the phenomenon of phantom keys (when three keys are pressed, that are placed at three corners of a rectangle in the matrix, the one on the fourth corner appears to be pressed).

    Interrupt handling

    Two pins are programmed as interrupt pins and receive the interrupt request from the VDP (pin INT2*) and peripheral cards in the PE-Box (pin INT1*). If we enable the corresponding mask, the interrupt will be sent to the CPU via the INTREQ* pin.

    Tape recorder control

    The TMS9901 uses two output pins to control the motors on two cassette tape recorders: pin P6 for cassette1 and INT15*/P7 for cassette2. The motors are not connected directly to the chip, mind you: there are isolated via opto-isolators (which means that the polarity of the plug is important!). Pin INT13*/P9 is used to output sound to the cassette, this will be digital sound of course, modulated via an electronic circuit in the console. Another electronic circuit takes its input from the cassette recorder and feeds a digital level of 0 or 1 into input pin INT11*/P11.

    Direct sound

    Pin INT14*/P8 is connected to the AUDIOIN pin of the TMS9919 sound chip. This allows to send digital sound directly to the speaker, without having to rely on the sound generators in the sound chip. I heared there is a program around that makes use of this feature to generate sophisticated sounds, but I can't recall which it is (any help appreciated).



    CRU map of the TMS9901

    Here is a map of the TMS9901 in the CRU address space of the TI-99/4A. Column 3 indicates whether the pin should be programmed as input (I), output (O) or interrupt (I+)
     
    Bit R12 address I/O/I+ Usage
    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 
    3 >0006 I
       =   .   ,   M   N   /  fire1  fire2 
    4 >0008 I
    space  L   K   J   H   ;  left1  left2
    5 >000A I
    enter  O   I   U   Y   P  right1 right2
    6 >000C I
    (none) 9   8   7   6   0  down1  down2
    7 >000E I
    fctn   2   3   4   5   1  up1    up2 
    8 >0010 I
    shift  S   D   F   G   A 
    9 >0012 I
    ctrl   W   E   R   T   Q 
    10 >0014 I
    (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) 
    16 >0020 I/O n.c.
    17 >0022 I/O n.c.
    18 >0024 O Select keyboard column (or joystick) 
    19 >0026 O Ditto
    20 >0028 O Ditto
    21 >002A O Select alpha-lock key 
    22 >002C O 1: turn CS1 motor on
    23 >002E O 1: turn CS2 motor on
    24 >0030 O Audio gate
    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) 



    Operating the TMS9901

    I/O mode

    To read the status of a pin, simply read the corresponding CRU bit. Note that you can read several bits at a time with a STCR instruction.
     
           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


    Timer mode

    Let's first use the decrementer to measure the time elapsed between two events.
     
    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:

    1. Periodically read the timer, save the read value, and reset the timer to its initial value. Add up all the elapsed times calculated this way.
    2. Same as above, but let the timer reload itself. If the value read is larger than last time it was read, the timer has reached >3FFF and looped back to >0000.
    3. Set the timer to generate interrupts every 100 ms (or any suitable interval) and use the ISR to count those interrupts. After reading the final value, just add the number of time the timer has fired times 100 msec.
    The first two method are trivial variants of the above, but method number 3 is slightly trickier. The problem is that the ISR on the TI-99/4A is written in stone, I mean in ROM. When it sees that an interrupt does not come from the VDP, it assumes (and does not check) that it comes from a peripheral card. Thus, when our timer fires, the ISR will call all interrupt routines on peripheral cards, but NOT the ISR hook at >83C4. Now, if you have an Horizon Ramdisk, or any card with RAM at >4000, this is an ideal way to simulate peripheral interrupts and/or to use the timer.

    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.


    An improved ISR ?

    One thought that occured to me was that OURISR could test whether the interrupt came from the VPD, a peripheral card or the timer and branch to the proper address accordingly (>0918 for peripheral cards, >094A for the VDP). Before doing so, it would place the return address in R14 of workspace >83C0.

    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!


    Jeff Brown's solution

    This solution was suggested to me by Jeff Brown, the man behind the interrupt mod. His idea is very elegant: if we fail to acknowledge VDP interrupts any further interrupt will be considered as VDP, so we could use the "interrupt hook" address >83C4 to branch to our own routine after any interrupt type. Only problem: VDP interrupts are acknowledged by the ISR in the console ROMs, and this is done before it calls our routine. Fortunately, there is a way out: reseting the interrupt is done by reading the VDP status at >8802, which is coded as MOVB @>FC00(R15),@>837B in the console ROMs. R15 in the GPL workspace contains the address of the VDP "read address" port >8C02. So all we need to do is to scramble this value, and the console ISR won't be able to clear interrupts any longer.

    Of course, there are unwanted side-effects to this techniques:

    This being said, click here for a complete listing of Jeff's solution.
     
     

    Fun stuff

    A look at the CRU map above may have suggested you some interesting possibilities. For instance:

    Use the extra pins

    First of all, there are three unused pins on the TMS9901 that we could make use of: INT12*/P10 connected to a 10 KOhm pullup resistor would make a nice input (or interrupt) pin. Then P0 and P1 could be used either as input or as output pin.

    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.

    Interrupt-driven keyboard

    And what it we were to enable interrupts from the pins connected to the keyboard? We would end up with an event-driven keyboard, just like on a PC, right? Well, not quite. Remember, the keyboard is scanned one column at a time, thus only the keys on the current column would trigger an interrupt. Still, this is interesting since column 0 contains all modifying keys: Shift, Ctrl and Fctn. Not to mention Enter and the spacebar. If your program follows the "menu bar" standard and is controlled by Fctn-0 to Fctn-9 and Ctrl-0 to Ctrl-9, then keyboard scanning could be performed by an interrupt service routine.
     
           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.

    The Evil Grail?

    Another more frightning thought: what if we were to program the keyboard input pins as output? The TMS9901 data manual says that trying to force data into an ouput pin could damage the chip. Does this mean that we have reached the "Evil Grail" of every hacker: write a virus that effectively damages the computer at the hardware level?

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



    Timing diagrams

    Interrupts

    ______    _>225__    _______| 300-2000_|   ______ PHI
         r\__/f      \__/       \__/       \__/      
    __              45-300   ________________________ INTn*
      \>60|_________________/>60|                    
    _____________________                         ___ INTREQ*
                    |>110\__________________|<110/
    r: 5-40 ns
    f: 10-40 ns


    CRU interface

    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




    Electrical characteristics

    Absolute maximum ratings

    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


    Recommended operating conditions


    Parameter Min Nom Max Unit
    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
    Free-air temperature 0 - 70 `C


    Electrical characteristics under recommended conditions


    Parameter Test conditions Min Typ Max Unit
    High-level output voltage I = -100 uA 
    I = -200 uA
    2.4 
    2.2
    - Vcc 
    Vcc
    V
    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!

    Revision 5. 1/15/06 Added Jeff Brown's hack .
    Revision 6. 1/20/06 Corrected bug: flag bit is at >83FD, not >83FC.
     
     

    Back to the TI-99/4A Tech Pages