Graphic Read-Only Memories (GROMs) are a very peculiar type of ROM manufactured by Texas Instruments. They have a multiplexed address and data port, which means you must telll the GROM which address you want to access and then reach data in there. The GROM has an internal counter that is automatically incremented each time you access data, so that you don't need to re-enter the address each time.
The TI-99/4A GROMs
GROM port
GROM headers
Console GROMs
+----+--+----+
AD7 |1 o G 16| Vss
AD6 |2 R 15| GR
AD5 |3 O 14| Vdd
AD4 |4 M 13| GRC
AD3 |5 12| M
AD2 |6 11| MO
AD1 |7 10| GS
AD0 |8 9| Vcc
+------------+
Power supply
Vcc +5V
Vdd -5V
Vss Ground. Actually -0.72V in the TI-99/4A console due to the
following
circuit (whose purpose is totally mysterious to me):
-5V--WWW-----+---+---|Vss |
270 | | +-------+
PG1992 ^ = 0.1uF
| |
Gnd Gnd
CPU interface
AD0-AD7 Multiplexed address and data bus.
M Direction: Read (high) or Write (low). Connected to DBIN in
the
console.
MO Access mode: Address counter (high) or GROM data (low).
Connected
to A14 in the console (via a TTL buffer).
GS* GROM select. Active low, chip select signal.
GREADY Active high: signals that the GROM is ready to operate.
GRCLK Clock signal, 3.5 MHz generated by the VDP (pin 38) or
447.4
kHz generated by the VDP (pin. 37). The actual signal is selected via a
jumper in the console. As far as I know, it's the 447.4 kHz that's used.
The GREADY line is necessary because GROMs are slow memory devices: this line puts the TMS9900 microprocessor on hold until the GROM is ready to answer.
The M line determines the direction of access: read or write. Obviously writing data to a GROM is meaningless, but this line is required to write an address.
The MO pin determines whether the AD0-AD7 port is accessing the address counter or the data. When MO is low, data can be accessed on a byte-wise manner. The counter will be incremented after each operation, whether it's a read or a write. When the top of the GROM is reached, the counter loops back to zero.
If MO is high, writing operations load an address into the counter. As an single-byte address would limit GROM sizes to 256 bytes, the address must be passed as two bytes. The GROM has an internal flip-flop that keeps track of the bytes entered and "knows" whether it is the first, least significant byte, or the second. Any data access resets this flip-flop to "first byte expected".
Reading from the GROM with MO high returns the current value of the counter *PLUS ONE* (don't ask me why). Here also, the lower byte is passed first, then the high-order byte.
The standard TI-GROM size is >1800 bytes, wich means any address can be expressed as a 13-bit number. The remaining 3-bits are used as GROM identifier: only the GROM with a matchin internal ID will respond to such an address.
Exemple (address >54A6):
0101 0100 1010 0110
iiiA AAAA AAAA AAAA Address = >1456, ID = 2
This allows to install upto 8 GROMs on the same data bus: they all respond to every address operation, latching the current address or returning the counter value (plus one). But only the GROM with an ID matching that in the high-order address byte will accept data operations. Typically, you would have :
GROM 0: >0000-17FF
GROM 1: >2000-37FF
GROM 2: >4000-57FF
GROM 3: >6000-77FF
GROM 4: >8000-97FF
GROM 5: >A000-B7FF
GROM 6: >C000-D7FF
GROM 7: >E000-F7FF
That's kind of a waste of space since >0800 bytes in each GROM are lost in addressing gaps. I know there were a few cartridges around that had full-size GROMs of >2000 bytes, but I'm not sure whether these were real GROMs or ROMs with an emulation circuitery.
There are three places GROMs can be found in the TI-99/4A: in the console, in a cartridge or in a peripheral card.
The console contains three >1800-byte GROMs, with ID numbers 0, 1 and 2. Which means they map at addresses >0000-17FF, >2000-37FF, and >4000-57FF. These GROMs contain most of the Basic Interpreter, routines for mathematical operations with real numbers, and DSRs for cassette tape operations.
In addition, GROMs 3 to 7 (addresses >6000-FFFF) can be implemented in cartidges to be plugged in the front panel of the console. For more information, see my pages (including commented listings) for the following cartridges: Editor/Assembler, Mini-Memory and TI-Writer.
Apart for GRAM cards, the only peripheral card with GROMs I'm aware of is the p-code card.
No matter where they are installed, GROMs are always accessed through a set of 4 addresses in CPU memory. The first address is known as the "GROM base":
>9800: read data (GROM base)
>9802: read address+1
>9C00: write data
>9C02: write address
As you may have guessed, addess line A5 (weight >0400) is connected to the MO pins of the GROMs. Different addresses are used to read and write because the TMS9900 always preceedes a write with a read operation. This would have the annoying result of bumping the GROM counter by two on a data write operation! The solution is straightforward: a simple TTL circuitery filters out reading operations at address >9Cxx. Only the write goes through so the counter increments only once.
Texas Instruments probably sensed that 8 times >1800 bytes wouldn't be enough memory, so they implemented an additional level of subtility in the TI-99/4A. There are upto 16 sets of 4 addresses that can be used for GROM access. The GROM base for each set is >0004 apart: >9800, >9804, >9808, >980C ... >983C. In fact, the address space is free upto >9C00, so there could technically be 256 such sets. However the console ROM routines that search GROMs for subprograms only consider the first 16 bases. But still, this provides an address space of 16 * 64K, which is 1 megabyte. Even with gaps, it's not bad for a 1980s computer!
Mind you, the console does not contain any circuitery to decode the GROM base, which means that the console GROMs can be accessed from any GROM base. Ditto for the classical TI cartridges like Extended Basic or Editor/Assembler. But the possibility is here: one could design an extension device that would let you plug in several cartridges and have one at base >9800, another at base >9804, etc. To my knowledge, such a gadget was never released, but several GRAM-cards make use of this trick. For instance the german 128K GramKarte uses bases >9800 and >9820.
Optionally, a GROM can start with a standard header that contains lists of programs, subprograms, DSRs, etc. The first byte in the GROM must be >AA to indicate that this GROM contains a header. The structure of such a header is the following:
Bytes | Content | |
---|---|---|
>x000 | >AA indicates a standard header | |
>x001 | Version number | |
>0xx2 | Number of programs (optional) | |
>x003 | Not used | |
>x004 | Pointer to power-up list (>0000 if none) | |
>x006 | Pointer to program list (>0000 if none) | |
>x008 | Pointer the DSR list (>0000 if none) | |
>x00A | Pointer to subprogram list (>0000 if none) |
Structure of a list:
Link to next item --+
Address |
Name length |
Name |
+-------------------+
|
V
Link to next item: >0000
Address
Name length
Name
N.B. Name lengh and name are not necessary for power-up routines
If you would like more details on standard headers and how to write subprograms and/or DSRs, I have a whole page on the subject.
A commented listing of the console GROMs can be found in Heiner Martin's book "TI-99/4A intern" (pdf file 560k). Here I'm just providing a brief description of the contents of these GROMs.
Only GROMs 0 and 1 have a standard header, GROM 2 is the continuation of GROM 1 and has no header.
At the beginning of GROM 0 is a table of subprograms, that contains only branching instructions (mostly BR, and a few B). The location of these subprograms may vary among GROM versions, but the table is always at the same place. The way to use these routine is described in my page on GPL language, clicking on one of the addresses in the table below will take you there.
N.B. FAC (floating point accumulator) corresponds to >834A-8351, ARG (argument) to >835C-8363
Mnemonic | Address | Use |
---|---|---|
LINK | >0010 | Subroutine/DSR call: FETCh >0A/>08, name ptr in >8356 |
RETURN | >0012 | Return from a subroutine/DSR |
CNS | >0014 | Convert number to string (from FAC to FAC, infos in >8355-57) |
STCASE | >0016 | Load title screen character patterns at VDP address in >834A |
UPCASE | >0018 | Load upper case character patterns at VDP address in >834A |
BWARN | >001A | Issue warning message |
BERR | >001C | Issue error message |
BEXEC | >001E | Begins Basic excution: FETCh 4 bytes: address of first and last lines |
PWRUP | >0020 | System reset |
INT | >0022 | Convert real to integer (from FAC to FAC) |
PWR | >0024 | Power-of-ten routine FAC=FAC * 10^ARG |
SQR | >0026 | Square root routine FAC=SQR(FAC) |
EXP | >0028 | Exponentiation routine FAC=e^FAC |
LOG | >002A | Logarithm calculation FAC=ln(FAC) |
COS | >002C | Cosine calculation FAC=cos(FAC) |
SIN | >002E | Sine calculation FAC=sin(FAC) |
TAN | >0030 | Tangent calculation FAC=tgn(FAC) |
ATN | >0032 | Arctangent calculation FAC=atn(FAC) |
BEEP | >0034 | Issue acceptation sound |
HONK | >0036 | Issue error sound |
GETSPA | >0038 | Get VDP memory space for a string |
BITREV | >003B | Bit reversal routine >834A: VDP address, >834C: number of bytes |
NAMLNK | >003D | Part of LINK: searches in GROM only |
PABSPA | >003F | Check memory space for PAB |
TOKEN | >0042 | Get next token, set Basic pointers |
LOCASE | >004A | Load lower case character patterns |
The address of the following routines may vary, since they are not
included
in a vector (branch) table.
Address | Use |
---|---|
>1387 | OPEN cassette. |
>13CF | READ cassette. |
>13DA | WRITE cassette. |
>13F2 | OLD cassette. |
>140E | CLOSE cassette. |
>1444 | Verify cassette. |
>1489 | SAVE cassette. |
>216F | Start of Basic interpreter (Entry point for NEW). |
>2214 | Address table for RUN, NEW, CONTINUE, LIST, BYE, NUMBER, OLD, RES,SAVE and EXIT. |
>27E3 | Clears screen, resets cursor and continues as below: |
>27F1 | Loads char patterns, resets colors and VDP registers 2,3 and 4. |
>2A42 | Start line editor with default position and length. |
>2A49 | Ditto with max length in >835E. |
>2A4F | Ditto with starting screen position in >8361. |
>3450 | Checks if a char is valid for a variable name (A-Z, a-z, 0-9..). |
>351C | CALL CLEAR. |
>3538 | CALL SOUND. |
>360E | CALL HCHAR. |
>362A | CALL VCHAR. |
>3643 | CALL CHAR. |
>3708 | CALL KEY. |
>3748 | CALL JOYST. |
>37D6 | CALL SCREEN. |
>401E | OPEN a file. |
>4160 | DELETE a file. |
>4174 | CLOSE a file |
>41CF | Closes all files. |
>41D7 | RESTORE a file. |
>4227 | PRINT in a file or on screen. |
>426C | DISPLAY on screen. |
>4344 | INPUT from files or keyboard. |
>45E3 | READ the DATA inserted in a program. |
>4641 | OLD loads a program. |
>46FC | SAVE a program. |
>474C | LIST a program. |
>482B | EOF tests for end of file. |
>4D7C | Prints "Bad Value". |
>4D81 | Prints "String-number mismatch". |
>566C | Prints "Can't do that". |
>56CD | Scrolls up. |
>56EF | CALL GCHAR. |
>5713 | CALL COLOR. |
Finally, here are some usefull data in GROM. The exact addresses may
also
vary according to GROM versions:
Address | Contents |
---|---|
>0451 | Default values of the 8 VDP registers. |
>0459 | Content of the color table, for title screen. |
>04B4 | Characters 32 to 95 patterns, for title screen. |
>06B4 | Regular upper case character patterns. |
>0874 | Lower case character patterns. |
>16E0 | Joysticks codes returned by SCAN. |
>1700 | Key codes returned by SCAN. |
>1730 | Ditto with SHIFT. |
>1760 | Ditto with FCTN. |
>1790 | Ditto with CTRL. |
>17C0 | Key codes in keyboard modes 1 et 2 (half-keyboards). |
>2022 | Error messages (with Basic bias of >60, and lenght byte). |
>285C | Reserved words in Basic, and corresponding tokens. |