Open stefanberndtsson opened 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);
}
Sounds sensible. Should there be an implicit or explicit "end_of_instruction" step too, where the ucode can hook on exceptions/interrupts?
Maybe a terminating NULL
, or a nicely named end_sequence
.
Yes, I think I'd prefer an explicit end_sequence
there.
Agreed.
I edited the example. Possibly, ea_begin_modify
should only handle memory destination operands, and let the instruction handle register destination operands.
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);
}
}
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?
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.
For that matter, do you have a handle on externally versus internally generated bus errors?
How can a bus error be internally generated? Without a bus access, there shouldn't be a possibility for a bus error.
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
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).
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.
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.
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.
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. Eachuop
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):
uop
:s that's needed before any EAThe 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 theTRAP
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.