Jump to content
IGNORED

Additional GROM technical details?


brain

Recommended Posts

I've pored over http://www.unige.ch/medecine/nouspikel/ti99/groms.htm, but am wondering if there are more technical details around the GROM IC?

 

I am trying my hand at implementing in Verilog, mainly because I want to know how it works, and I am having only partial luck at present

 

  • I've placed a GROM3 game (astroblast, I think), in a FLASH ROM, and wired it to the CPLD
  • I've then connected the relevant GROM lines to the CPLD
  • I have implemented the 3 bit ID register and the 13 bit counter
  • I have implemented the counter write
  • I have my code checking to see if ID is 3
  • I dump data on the databus and increment the counter when it matches

But, I am getting conflicts on the GROM bus. When my cart is installed, it seems to be fighting with the internal GROMs. Lots of artifacts on the screen.

 

I am trying to look at the signals, but thought a more technical document would be helpful, to answer questions like:

 

  • I assume more than 1 clock period elapses during a read or write (at least during a read, since the ROM is so slow). With that in mind, when do you latch the address or set/reset the flip flop telling which side of the 16 bit address to store?

 

Jim

 

Link to comment
Share on other sites

Yes, blasto is the GROM I am using. It's a small one, and seemed perfect to test with.

 

I have spent the evening performing logic analysis on the GROMs in the TI, with the following observations:

 

  • It looks like MO,M can change before *GROMSEL goes high, so one needs to save them off before
  • It appears data is valid on the falling edge of GROMCLK after *GROMSEL goes low
  • They might be valid on falling edge of GROMSEL, but it does not appear to be the case.
Link to comment
Share on other sites

I didn't need to do any careful timing against GROMSEL. But you need to participate in every single GROM transaction -- whether the address matches your ID bits or not. The only thing the ID bits gate is whether you present data on your data pins or leave them tri-stated. Everything else, you do - every read, every write, every address read and every address write.

 

You can safely ignore GROMCLK as well - the rest of the TI system does not match it, and it's out of sync with the TI clock. It's only there to drive the GROMs themselves. I experimented a lot with and without it and eventually left it ignored.

 

Be careful with GROM READY - it's open collector - you can pull it down but never pull it up. It seems to be okay to drive the data bus when you are allowed to, but tri-state it otherwise.

 

This is the sequence that the UberGROM uses (it does not offer address read-back, and I don't know MO, M off the top of my head, so you'll need to interpret signal names):

 

An idle GROM is /not/ ready - that is, the GROM READY line is pulled low. The TI ignores the GROM Ready line except during an actual GROM operation. So you should come up in that state. READY actually means that the operation is complete, rather than ready for control. The prefetch register is undefined at startup.

 

Wait for GROM Select to go LOW, indicating a GROM operation. The bus should have the data you need already.

 

Check the MODE and DIRECTION (R/W) pins to determine the operation:

 

-Write Data - If you're a ROM, then you can ignore the data. But you should still increment your address and prefetch

-Read Data - if you are the current GROM, set the prefetched data on your output pins. Either way then increment your address and prefetch.

-Write Address - Shift your address register up 8 bits, and then merge the data bus into the lower 8 (You should prefetch on the second write here, but UG doesn't)

-Read Address - don't output data unless you're doing the prefetch operations, else you'll fight the real GROMs. However, you still must handle the address munging it causes. The LSB is /copied/ to the MSB (and normally the MSB would be output).

 

After the correct operation is complete and output data (if any) is presented, /release/ GROM READY. Note that the console will wait for /all/ GROMs to signal high, since any GROM can pull that line low. If you force it high you'll cause bus contention - a pull up in the console will raise it when everyone releases it.

 

Watch GROM Select. You must /immediately/ release the data bus when it goes high, as the TI is no longer looking at GROM Ready and won't wait for you. You should also set GROM READY low again as soon as you can, but you have a lot more time for that. ;)

 

Of course, I used a microcontroller, so my response time to signals is slower than your FPGA will be, but I never experienced any racing with the select line up to 20MHz. You might want to oversample the select signal or something to give yourself a bit of buffer as well as to ignore bounce. ;)

Link to comment
Share on other sites

This is my current theory about GROM operations, from observing the behavior, and used for the implementation in MAME. Of course, this is not guaranteed to be the real story. Maybe someone can attach a GROM to an oscilloscope or drive it by an Arduino and check the outputs.

post-35000-0-70456400-1485346786_thumb.png

Link to comment
Share on other sites

I have done exactly that, attach a real GROM to a logic analyzer, and I have the luxury of creating additional signals with the CPLD, since I have fed the lines through the CPLD to the Logic Analyzer, so I can see the original data and any results of testing on the same screen.

 

(I pulled my TI apart and tried to attach the LA directly to the GROM pins, but the MO line is evidently very heavily loaded and adding another load with my LA causes the TI to fail to boot, so I am doing the next best thing). Here are things that I have found thus far:

 

(for the discussions, MO = address (H)/data (L) and M = Read(H)/Write(L) and GROMSEL is ACTIVE(L)/INACTIVE(H)

  • I agree that GROMCLK is not synchronized, but I disagree that it is unimportant.
  • I have found that simply latching data on the falling edge of GROMSEL will not work
    • I implemented a sequence that latches the address lines when MO is high (address) and M is low (write) and GROMSEL goes low (falling edge).
    • I then added the required flip flop to denote low part of the address and high
    • I verified that the flip flop was working (looking at it as an output on the LA)
    • I then created a signal called ME that equals (top 3 bits of address == 011)
    • I started the TI, and looked at ME. It was always 0
    • I then changed my latching code to latch DATA on the falling edge of GROMCLK when GROMSEL is low (just the first clk falling edge)
    • ME suddenly became active during boot, going to high for 1 byte, then low, then high, etc.
    • That shows me that I am a) getting the address correctly, and b) the falling edge of GROMSEL is not good enough to latch the data (it probably works on a uC, since you'd put GROMSEL on an interrupt, and it takes 10 cycles or so to service an INT, which would be at least .5uS later at 20MHz.
  • I determined that the rising edge of GROMSEL is also no good for latching data
    • On the LA, I noticed that the first address write was not working in my CPLD, and I traced it to code that says (if GROMSEL is rising and we're writing and address, flip the flip/flop, but if we're doing anything with data, set flop to 0 (as per the GROM tech notes). I realized that the address line (A14) was switching before GROMSEL was rising (which makes sense, as GROMSEL is the address lines passing through a '138 or similar, which has latency.

 

I will do more testing tonight, but I believe my current issue is the use of GROMCLK. I need to use it (or some clock signal) to synchronize the inputs to the GROM code, but since GROMCLK has no relationship to the other signals, I think there are times that GROMCLK goes high (or low, you pick the state) where the GROMSEL line is also going low but the other signals have not settled into their correct state. Thus, what I need to do is:

 

  • wait for GROMSEL to go low
  • wait for a time period
  • latch data, M, MO, and perform operations on the resulting state

 

The wait for a time period I think is where I need to use GROM clock:

 

if(GROMSEL falls)

reset counter to 0

clock counter with GROMCLK

if counter == 2 // can't do counter =1, since GROMCLK could go high immediately after GROMSEL going low, and then the issue exists. We need to wait at least 1 cycle, and up to 2.

latch M, MO, DATA and do work

 

I'll try to capture screen shots tonight. It is interesting to see how the GPL interpreter reads the GROMs.

 

Jim

Link to comment
Share on other sites

A couple more notes, from LA work this evening:

 

  • GREADY is always released on the rising edge of GROMCLK, and has a small latency/skew from the CLK line
  • a GROM cycle varies from 2 GROMCLK period to 4.
    • GREADY is released on the 1st rise of GROMCLK after 2 falls of GROMCLK when writing the low byte of the address
    • GREADY is release on the 1st rise of GROMCLOCK after 3 falls of GROMCLK when writing the high byte of the address
    • GREADY is released on the 1st rising edge after 3 falling GROMCLK edges on data reads

 

I would assume that TI synchronizes as I am considering.

  1. once you see GROMSEL go low, start 2 bit counter clocked on falling edge of GROMCLK
  2. when counter == 2
    1. IF M:MO:HIBYTE = 010,
      1. clock data into low 8 bits of counter
      2. negate HIBYTE
      3. release GREADY on next clock rise
    2. IF M:MO:HIBYTE = 011,
      1. clock data into high 8 bits
      2. negate HIBYTE
      3. set prefetch flag
      4. wait for counter ==3 (not sure why this would be needed, maybe it must shift the low order bits somewhere else to make room for these bits.
      5. release GREADY on next clock rise
    3. IF M:MO = 10
      1. place data on databus
      2. reset HIBYTE
      3. wait for clock fall
      4. set prefetch flag
      5. release GREADY on next clock rise
    4. IF M:MO:HIBYTE = 110 // reading low byte of address counter
      1. place low 8 bits on databus
      2. negate HIBYTE
      3. ?
      4. release GREADY on next clock rise
    5. IF M:MO:HIBYTE = 111 // reading hi byte of address counter
      1. place high 8 bits on databus
      2. negate HIBYTE
      3. ?
      4. releae GREADY on next close rise
  3. on GROMSEL rise
    1. drag GREADY low
    2. if prefetch flag
      1. fetch byte and store
      2. increment counter
      3. reset fetch flag
Link to comment
Share on other sites

Interesting findings!

 

GREADY is always released on the rising edge of GROMCLK, and has a small latency/skew from the CLK line

 

Mind that GROMCLK and CLK are asynchronous; they are driven by different timer sources (GROMCLK comes from the VDP with its 10.7 Mhz quartz, divided by 24). So there is no relationship between GREADY and CLK.

Link to comment
Share on other sites

 

A couple more notes, from LA work this evening:

  • GREADY is always released on the rising edge of GROMCLK, and has a small latency/skew from the CLK line

 

Just one more question so that I understand properly: What does "released" mean here? GR is normally L, so does it mean it goes H? That is, in my figure above, this is happening just at the left edge of the yellow zone, right? As I said, that this figure is just based on "educated guesses".

 

How far does the shaded area go? Does it quickly return to L? The point is, it does not need to be H very long, just for one cycle of CLK (not GROMCLK).

Link to comment
Share on other sites

When a signal is implemented as "open collector", typically "released" means "I quit driving the signal to ground". At the beginning on the GROM cycle (GROMSEL goes low), GREADY is driven hard to 0. Then, according to the rules above (2-3 GROMCLK falls and then a rise), GREADY is "released" to float high.

 

The following pic shows the absolute first GROM requests on boot:

 

first_grom.png

 

You'll see GROMSEL go low, and the first GROM action is to read data (not sure why)

Notice that the GROMSEL fall happens after the GROMCLK line is already high.

Notice that the GREADY line tracks the GROMCLK signal, delayed by a small amount.

Notice that GREADY stays high unitl GROMSEL goes high, at which time (subject to some latency), it goes to 0 again.

 

The next GROM select is a low byte address write (the blip on test1 is an indicator of this, but so is the fact that M is low and MO is high)

Notice it takes far less time to complete (2 falls and then the rise)

Notice that the high byte write takes the same time as a data read (I theorize this is because the address write needs to do the same things as the data read (prefetch)

The last select in the trace is a data read.

 

I agree that GREADY need only be as long as one CLK cycle, but that's not what I see. I see GREADY always be at least 1 CLK cycle long. I *think* you have added another piece of information, because I see GREADY always being at least 1 GROMCLK length in duration, never any shorter. I would expect that GREADY would vary in length, according to when it goes high relative to the state of the CLK signal, but I don't.

 

I'll see if I can solder a wire onto the CLK* line (I think a negated clock is on the side port), and add that to the trace.

 

Jim

Link to comment
Share on other sites

OK, did not think about the OC. So theoretically it need only be one CLK cycle, but the GROM lives in a world of 477 kHz, and it knows nothing about CLK cycles. Its behavior should be independent of the CPU CLK, I suppose.

True. but GREADY is not equal to one GROMCLK cycle. I checked, and GREADY varies from 1.04uS to 1.333uS, so it is obvious that GROM releases GREADY but then waits until GROMSEL goes high to bring GREADY low again. Thus, the CPU is controlling the action after GREADY goes high.

 

With that in mind, you would assume GROMSEL would go low in less than 1uS after GREADY goes high, as the cycle could have started right after GREADY went high, and then the CPU would have moved to the next instruction, but it does not.

 

Very peculiar.

 

Jim

Link to comment
Share on other sites

I realize Tursi (UberGROM) and ralphbb (FinalGROM) have already solved the problem, but I wanted to prove to myself that I could implement a GROM in Verilog. I fought with it all week, and I *JUST* found the error.

 

In short, the docs are wrong :-(

 

http://www.unige.ch/medecine/nouspikel/ti99/groms.htm

 

I probably should have studied Tursi's AVR C code and found the problem earlier, but I had not yet found the Project Archive.

 

Specifically, I found the error by hand converting all of the GROM data accesses into data from the traces I made. Laborious, but I found the error. My code was killing the TI when I would dump data from the GROM emulator onto the bus, and I did not know why. Found it:

 

The docs say:

 

 

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.

 

Quite simply, the order of bytes in the documentation is wrong. Addresses are written and read high byte first, low byte last. Once I changed that one thing, my GROM code started working. Before, my code would trigger to start response for any addresses in the XX6X-XX7X range, since it would interpret the low byte of the address as the high byte, which trashed the initialization code for the TI.

 

Exhausted, but pleased.

 

For the record, here are the first few accesses:

ADDR (HEX)	DATA (HEX)	
		
0	90	Read Before Address Known
0020		Address Write
0020	40	
0021	52	
0022	00	Partial Address Read
0052		Address Write
0052	87	
0053	80	
0054	CE	
0055	BE	
0056	8F	
0057	11	
0058	00	
0059	70	
005A	BE	
005B	81	
005C	00	
005D	9F	
005E	BE	
005F	81	
0060	00	
0061	BF	
0062	BE	
0063	81	
0064	00	
0065	DF	
0066	BE	
0067	81	
0068	00	
0069	FF	
006A	BF	
006B	72	
006C	FF	
006D	7E	
006E	39	
006F	00	
0070	08	
0071	00	
0072	04	
0073	51	
0074	00	address read high byte
0075	75	address read low byte
0451		Address Write
0451	00	
0452		Address Write
0452	20	
0453		Address Write
0453	F0	
0454		Address Write
0454	0E	
0455		Address Write
0455	F9	
0456		Address Write
0456	86	
0457		Address Write
0457	F8	
0458		Address Write
0458	F7	
0074		Address Write
0074	86	
0075	00	
0076	35	
0077	00	
0078	71	
0079	01	
007A	00	address read high byte
007B	7C	address read low byte
007B		Address Write
007B	35	

Link to comment
Share on other sites

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.

 

Rare enough, but indeed an error in Thierry's documentation.

 

From other sources (Michael Bunyard's "Hardware Manual for the TI-99/4A Home Computer" - must read!) there is reason to believe that the address counter is a shift register where the two bytes are shifted in from the right. The fact that the address returned is plus one is pretty simple: This is the effect of the prefetch. When the address counter is loaded, the byte at the loaded address is immediately loaded and put in an output register, and the address counter is increased by one. Also, when the address is read, it is shifted out of the address counter, so after reading, the address is gone, and the counter must be set again. Again, Thierry seems to have mixed up the VDP and GROM behavior, because you always get MSB, then LSB from GROMs.

 

Link to comment
Share on other sites

 

Rare enough, but indeed an error in Thierry's documentation.

 

From other sources (Michael Bunyard's "Hardware Manual for the TI-99/4A Home Computer" - must read!) there is reason to believe that the address counter is a shift register where the two bytes are shifted in from the right. The fact that the address returned is plus one is pretty simple: This is the effect of the prefetch. When the address counter is loaded, the byte at the loaded address is immediately loaded and put in an output register, and the address counter is increased by one. Also, when the address is read, it is shifted out of the address counter, so after reading, the address is gone, and the counter must be set again. Again, Thierry seems to have mixed up the VDP and GROM behavior, because you always get MSB, then LSB from GROMs.

 

It's ultimately my fault, as I should have consulted a few sources, not just 1. Is this other reference available online? Or, is there an offline PDF?

 

I'm not sure I would call it a shift register, maybe a FIFO, or stacked register

 

The address register is implemented as a 2 x 8 RAM cell. An address write clocks both registers, and the second register is connected to the first register's inputs. When the the first (high) byte is clocked in, the first register's contents are clock into the second register at the same time. On a read, the same process is performed, with the second register's outputs hooked to DATA.

 

This should be easy to test, as a partial address write followed by a full address read would show what is going on. Or, a partial address read, followed by an partial address write, and then a full address read.

 

Jim

Link to comment
Share on other sites

It's not just the flipflop, GROMs have an internal prefetch like the VDP. The prefetch register is undefined at powerup, that first read gets data into it. That's why the readback address is always one greater than expected. (Thierry knows this, I guess he just never updated that comment ;) ).

 

I did the same kind of GROM access tracing when I did the AVR version, heh. ;)

Link to comment
Share on other sites

It's not just the flipflop, GROMs have an internal prefetch like the VDP. The prefetch register is undefined at powerup, that first read gets data into it. That's why the readback address is always one greater than expected. (Thierry knows this, I guess he just never updated that comment ;) ).

 

I did the same kind of GROM access tracing when I did the AVR version, heh. ;)

I disagree, there is no need to put data into the prefetch register, as the address write will fill it, and a read does the same thing, but returns invalid data (as there is nothing in the prefetch register to send).

 

The reason for the initial read is solely to ensure the address write flip flop is in a known (correct) state prior to writing an address.

 

Jim

Link to comment
Share on other sites

More information for for posterity:

 

I can confirm that the M and MO lines are not stable as GROMSEL* falls. I rewrote that portion to latch the lines on GROMCLK fall when GROMSEL = 0, and the state machine corrupted very quickly. It takes 2 clock falls (or 1 least 1 GROMCLK period) to stabilize. If I can figure out how to trigger on both edges of GROMCLK, I think things stabilize in 1/2 GROMCLOCK period.

 

There are not 1 but 2 address counter flip flops, one for write and one for read. I cannot be certain, but TI might have placed code into the GPL startup to corrupt simple GROM implementations. If one looks at the trace, there is a data read, 2 address writes, and then 2 data reads, followed by 1 address read (a partial address read). If the address read code is implemented using the same flip flop, a partial address read followed by a complete address write will put the address in the wrong order. The address read will set the flop so that the next read will pull the low byte, but the following write will store what is supposed to be the high byte of the address into the low address byte in the GROM, because the partial address read will look like the address write first step to the GROM if one assumes a single flip flop. I can confirm that implementing a read flop and a write flop allowed games to work, where as using a single flop caused games to fail almost immediately.

 

My GROM implementation runs slightly faster than the TI GROM (all accesses take 3 GROMCLK periods, not up to 4), but the TI GROMs lag and so no speedup is seen. I have successfully implemented the address read, write, and data read capabilities. Obviously, since my code uses very fast RAM (55ns), I can run the entire 3 GROMCLK cycle in the same time as 1 CPU data access, assuming the original GROMs were removed and I put a clock on the CPLD.

 

Jim

Link to comment
Share on other sites

I disagree, there is no need to put data into the prefetch register, as the address write will fill it, and a read does the same thing, but returns invalid data (as there is nothing in the prefetch register to send).

 

The reason for the initial read is solely to ensure the address write flip flop is in a known (correct) state prior to writing an address

 

You love to disagree, don't you? ;)

 

I didn't say you have to put the data there, I said that's what the GROMs do.

 

But I'm happy to stop speaking up and wasting your time.

Link to comment
Share on other sites

You love to disagree, don't you? ;)

My apologies, I have not intended to. After seeing how folks went after you in 2012/2013 for GROM behavior naming conventions, I have no intention of getting into such an argument.

 

I didn't say you have to put the data there, I said that's what the GROMs do.

 

I think I mis communicated. (I tend to updated posts on technical items at the end of the night, after I am done researching. And, I am usually tired and so am brief. Maybe I am overly so.)

 

When I stated "there is no need to put data in the prefetch...", I meant "there is no need for the GROM to put data in the prefetch..."

 

To restate:

 

I don't feel there is a need for the software developer to issue a GROM read in order to force the GROM to fill the prefetch register. The developer would know the first read would deliver invalid data to the CPU, as the address counter is unknown or invalid. The developer would write the address counter as soon as possible, and the act of writing the address register would force the prefetch action, filling the prefetch register with valid data. Thus, I don't believe the initial read action is performed to place correct data in the prefetch register, but rather is only performed to reset the address write flip flop to a known state prior to the address write action.

 

But I'm happy to stop speaking up and wasting your time.

I hope not. WIthout your response, I'd still be scratching my head on why the Utilities ROM is not working.

 

Again, my apologies for this breach of forum etiquette.

 

Jim

Edited by brain
Link to comment
Share on other sites

I understand the original GROM most likely implemented the 2 byte address counter as a stacked set of 8 bit registers, which makes sense given the destructive read of the GROM address. The inputs of the lsb are connected to data lines, and the outputs of the msb register are also connected. In that configuration, the second flip flop would not be needed, as no state is needed.

 

I wonder, though, if a partial address read would trigger a prefetch (probably). I suppose it could be easily checked by loading a bank 0 grom with >0001 and doing a partial address read, followed by a data read, and seeing if the data for location 00 was returned.

 

Jim

Link to comment
Share on other sites

If you put your analyzer on it and watch the response times, you'll see that the second address write causes a much longer cycle than the first, so I don't THINK it prefetches every time. I've not tried that particular test though.

 

(edit: ah, wait, you said address READ, not write. That's interesting.... I've no idea. My money's on no. ;) )

Edited by Tursi
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...