nocrew / ostis

Atari ST Emulator
GNU Lesser General Public License v2.1
20 stars 4 forks source link

Proposal for a generic state machine #143

Open stefanberndtsson opened 8 years ago

stefanberndtsson commented 8 years ago

I propose that ucode takes on the job of running everything around the instructions, and exceptions. The state machine has one state chain for each instruction being executed. A state chain is a sequence of microcode instructions (uop) being executed one after the other. Each uop taking 2 8MHz cycles, and being run on even 8MHz cycles.

EA is just one or more sections of such a chain, and so are exceptions.

All of this assumes the full prefetch with IRC/IR/IRD is in place.

So the main role for an instruction is to setup the chain based on the instruction opcode (steps below may be optional):

  1. Insert any uop:s that's needed before any EA
  2. Append any EA chain that's relevant for reading
  3. The instructions actual operation (ADD, SUB, Shift, a.s.o.)
  4. Any EA chain for writing back to memory or such
  5. Parts for setting flags
  6. Remaining bits and prefetches before the instruction ends

The instruction is now only called to setup, after that, ucode takes over and steps through the chain. If anything would happen that would change the normal execution, like for example a bus error occurs within the EA steps, a bus error chain replaces the rest of the instruction chain, and ucode keeps on executing just like before.

For TRAP this would essentially be the same thing, except the instruction would just insert the exception chain for the TRAP exception into the chain straight away.

Other exceptions could be caught when ucode parses the end step, and if there is an exception/interrupt pending, append the exception chain, and keep on running a bit longer.

Every uop is a callback into a function dealing with it. For steps 2 and 4 above, those are callbacks pretty much the way they are in the EA code now. For 1, 3, 5 and 6, they would be provided by the instruction code.

larsbrinkhoff commented 8 years ago

I had a similar idea today. The overall concept is the same, I think. It depends on how you propose a chain being set up.

My idea was to implement an instruction as a sequence of functions. Each function is responsible for initiating a microprogram. When a microprogram ends, the next function in the sequence is called.

As an example, perhaps ADD would be:

// Computes the addition, runs no microcode (zero length microprogram).
static void add_compute(struct cpu *cpu, WORD op)
{
  int reg = ((op >> 9) & 7);
  // ea_begin_read leaves a result in u_data
  u_data += cpu->d[reg];
  // Set flags here?
  ujump(0, 0);
}

static void write_register(sruct cpu *cpu, WORD op)
{
  if(<destination is register>) {
    if(<byte or word>) {
      ujump(0, 0);
    } else {
      if(<one microcycle>) {
        ujump(w_reg, 1);
      } else {
        ujump(w_reg, 2);
      }
    }
  }
}

static struct u_sequence add_sequence[] = {
  add_start,  // Initialisation
  ea_begin_read, // Read operand
  add_compute, // Do addition
  ea_begin_modify, // Write memory operand, if any
  write_register, // Write register operand, if any
  add_finish // Clean up
};

void add(struct cpu *cpu, WORD op)
{
  u_start_sequence(&add_sequence);
}
stefanberndtsson commented 8 years ago

Sounds sensible. Should there be an implicit or explicit "end_of_instruction" step too, where the ucode can hook on exceptions/interrupts?

larsbrinkhoff commented 8 years ago

Maybe a terminating NULL, or a nicely named end_sequence.

stefanberndtsson commented 8 years ago

Yes, I think I'd prefer an explicit end_sequence there.

larsbrinkhoff commented 8 years ago

Agreed.

I edited the example. Possibly, ea_begin_modify should only handle memory destination operands, and let the instruction handle register destination operands.

stefanberndtsson commented 8 years ago

I think I'd rather see something like separate sequences.

static struct u_sequence add_sequence_reg_to_mem[] = {
  add_start,  // Initialisation
  read_register, // Read register operand
  add_compute, // Do addition
  ea_begin_modify, // Write memory operand
  add_finish // Clean up
};

static struct u_sequence add_sequence_mem_to_reg[] = {
  add_start,  // Initialisation
  ea_begin_read, // Read operand
  add_compute, // Do addition
  write_register, // Write register operand
  add_finish // Clean up
};

void add(struct cpu *cpu, WORD op)
{
  if(op == REG_TO_MEM) {
   u_start_sequence(&add_sequence_reg_to_mem);
  } else {
   u_start_sequence(&add_sequence_mem_to_reg);
  }
}
stefanberndtsson commented 8 years ago

Things to consider regarding this and Bus/Address error.

ucyc is a micro cycle, which is a 2c period of the 8MHz clock.

With the EA sequence nR nr, this is 4 ucyc long, with 2 words being read from the bus. This starts at ucyc0 with the first n.

In case the R (ucyc1) causes a Bus error, everything scheduled to be done after this should immediately be aborted and discarded. ucyc2 should be the first state of the exception.

In case of an Address error, where would things be aborted? Is the address detected before ucyc1, in which case the exception starts at ucyc1? Is the error detected once R has started, and the exception then starts at ucyc2 just like the bus error?

larsbrinkhoff commented 8 years ago

I have something that's almost working now.

I don't think there will be any problem aborting an instruction midway. Just ujump somewhere else, or call u_start_sequence.

I don't know exactly how address error works. I would guess that the CPU detects the bad address at the start of the bus cycle, or ucyc0 in your examples. It may be documented somewhere, or else it's a job for a logic analyser.

larsbrinkhoff commented 8 years ago

For that matter, do you have a handle on externally versus internally generated bus errors?

stefanberndtsson commented 8 years ago

How can a bus error be internally generated? Without a bus access, there shouldn't be a possibility for a bus error.

larsbrinkhoff commented 8 years ago

I read something in the 68000 User's Manual, but now that I read it again, I see it can be interpreted another way:

An address error is similar to an internally generated bus error

stefanberndtsson commented 8 years ago

The next sentence there hints at it being identical in timing as well.

The bus cycle is aborted, and the processor ceases current processing and begins exception processing.

I.e. the bus cycle has started (just like with BERR) and aborts (just like with BERR).

larsbrinkhoff commented 8 years ago

Well, it says "similar" to a bus error. It doesn't say it's done identically. But of course, it's slightly easier to implement them identically.

stefanberndtsson commented 8 years ago

I was also thinking of the other exceptions here.

A TRAP instruction should simply have the TRAP exception chain as part of its normal sequence (TRAP takes priority over trace and interrupts).

If (at the start of an instruction), the trace bit is set, it should also append the trace exception after its normal execution. This will then work well with TRAP as well, since its exception will occur, then immediately followed by the trace exception.

The trace state cannot reliably be checked after the instruction has finished, because according to the User's Manual, the state before the instruction is what determines the trace execution.

If the T bit is set (on) at the beginning of the execution of an instruction, a trace exception is generated after the instruction is completed.

stefanberndtsson commented 8 years ago

My use of "identical" was because of the phrase "bus cycle is aborted". They're of course not really identical, since a bus error is triggered externally, and there could be undeterministic delays from the bus request to the BERR, which will always be deterministic in an address error.