Re: A89: Re: porting Z80 to 68k.....possible?? (yes, but not easy)
[Prev][Next][Index][Thread]
Re: A89: Re: porting Z80 to 68k.....possible?? (yes, but not easy)
> Why is it crazy?? There are similar instructions on both processors,
> the 68k has more registers so you don't have to worry about running
> out, and the languages are pretty much the same (I've programmed in
> both and they are very similar). I don't see any difficulties except
> for the memory locations which would be easily switched between calcs.
> I dunno, maybe I'm just stupid and I don't know anything about this,
> but from my POV, it looks like a good idea.
Actually, I have been working on this very problem myself; I am making
a computer program to automatically convert TI-85 programs to run on
the 68K calculators. I do think I have (more or less) solved most of
the problems involved, but it's much more difficult than you seem to
suggest. My program will only work from Z80 source code, and should do
both ZShell and Usgard programs. Since it's just a simple translator,
the output won't be as efficient as code written for the 68K, but since
the 68K is a faster processor it should allow programs to run somewhere
near the correct speed. Anyway, here's a list of the things the program
needs to do:
1) Parse Z80 source code. Recognize instructions, directives,
registers, etc. This part is straightforward.
2) Basic code translation. I'm only referring to the basic instructions
here, and not memory access or control stuff which is addressed later.
This means to take a Z80 instruction and write out a 68K equivalent.
This is not as easy as you make it seem, as the registers are not that
similar. The difficulty has to do with the fact that registers can be
split and combined (e.g. B, C, and BC). Anyway, this is my plan for
dealing with this:
The register equivalencies will be this (there is a reason for these
particular choices: D0 and D1 are scratch since library calls may
change them, and the address registers are in D2 to D6 so they can be
conveniently mirrored in A2 to A6, as is explained later).
D7 = A
D2 = BC
D3 = DE
D4 = HL
D5 = IX
D6 = IY
In these registers, I do plan to keep them in big-endian order as is
normal in the 68K, and will just reverse the order (with ror.w #8,dn)
when storing them to memory.
The 68K can deal with the low word and low byte of a register as
seperate values, but not so with the second byte.
For example, the one instruction
ld c,a
becomes the one instruction
move.b d7,d2
while the one instruction
ld b,a
must become multiple instructions, such as
move.b d7,d0
lsl.w #8,d0
and.w #$ff,d2
or.w d0,d2
However, some special cases such as the instruction
dec b
can still work in one instruction
sub.w #$100,d2
I have considered some systems in which each component of a
register would have its own 68K equivalent, in which case only
word operations would need special tricks, but I decided not to do
this in the first version of the translator (though I might make it
an option later).
Of course, alignment is also a problem as the 68K needs instructions
on even addresses. This is not, however, a huge problem. As a
temporary solution, I'll just write "EVEN" whenever there is a change
from data to instructions. As far as loading words from memory, I'll
simply load each byte a time to avoid trouble.
3) Memory access - Unfortunately, this is not simply a matter of
finding equivalent addresses. Most addresses on the TI-85 have no
equivalent on the 68K calcs (and the situation is much worse on the
TI-86). The solution I have come up with is as follows:
Z80 memory addresses Where they go
$0000-$7FFF nowhere
$8000-$8FFF Temporary 4K buffer
$9000-$EFFF program's code
$F000-$FFFF Temporary 4K buffer
This memory arrangement allows all low memory variables to work,
and also allows tricks with the stack. In addition, programs that
allocate memory from the top of the VAT (e.g. grayscale buffers)
could still work. Mapping another area to the program's code
allows the program to reference data in itself (which is very
common) and allows a possibilty to get self-modifying code to work.
This does have the downside that any program which is larger than
24K when converted will fail.
(If you're wondering, I did consider other memory systems before
coming to conclude that this one is best. At first I thought I
would just try to convert addresses, but realized that this couldn't
work. The fact that addresses are different sizes, can be part of
data structures, and can be "built" byte by byte (e.g. calculating
H and L seperately, then combining to get a pixel address) and the
fact that a value like $FFFF could easily used in calculations as
-1 or the lower right corner of the LCD, or both, and other things
led me to think against simple translations).
I'd also need to do something with labels to get the fake addresses.
I'd have to labels corresponding to each original; one that's its
real 68K address (for program branches) and another that's the fake
virtual address for other references.
This virtual addressing isn't 100% perfect, so converted programs
need to be extensively tested befor they are considered stable, even
when the original is reliable. I'm of course trusting that programs
won't try to write to the ROM.
This also is a major performance drain, but I think that memory
references are not used so much that this will be unacceptable. I
have come up with several different methods of determining addresses
that have somewhat different performance issues:
a) Calculate-on-demand. Whenever you reference a memory address, the
real address will be determined. This is more efficient if the
program uses the address registers as data a lot, but will waste time
when the same address is accessed repeatedly.
b) Calculate-on-change. Whenever the address register is changed,
you re-do the address conversion. The real addresses would be
shadowed in address registers. If the register is simply incremented
or decrementing, you only need to increment or decrement it instead
of the whole recalculation. This is faster if the same address is
used a lot, or the program steps through addresses.
c) Validity flags. In this method, I'd have some sort of flag
indicating which registers had valid shadows. When a register is
changed, it's set to invalid. Then any memory access would check
the flag, and if invalid, recalculate and set the flag.
d) Some combination of the above, such as (b) for IX and IY which are
almost always addresses, with (a) for the others which are often data.
In the first release, I've decided to use method (a) though I may
make the others options later, depending on how well it turns out to
work.
4) "Emulation" library - ROM calls, display, ports
These features would go together in an emulation library that the
converted program uses.
The display handling would be fairly simple: just an interrupt that
copies data from the simulated memory area to the actual screen
periodically.
The port handler would most importantly handle keyboard reads.
Other ports could mostly be ignored for most programs, though at
some point the display address port could be activated to allow
grayscale.
The ROM call emulation is possibly the hardest. However, it is
survivable. Since the converter has the program's source code it
can easily identify ROM calls. The library would need to simulate
their action. Some, like UNPACK_HL would be easy (and actually
faster than on the original TI-85). The display routines would be
more difficult; in addition to reading the coordinates from the
virtual address space, you'd have to somehow put the letters back
over the virtual screen area (I haven't thought about this in
detail yet, but it shouldn't be too bad).
The most important feature of the library, of course, would be to
display a message warning the user that a converted program may be
unstable before the program begins, allowing a safe exit for people
who unknowingly run such a program.
5) Program flow control - For the most part, this one is actually
easier than it seems. Conditional branches and stuff within the
code are easily converted to 68K equivalents in most cases. A jump
to a computed address could be dealt with by the normal memory
handling system. For example, this jump table would work fine:
ld a,(hl)
add a,a
ld e,a
ld d,0
ld hl,&address_table
add hl,de
call LD_HL_MHL
ld de,(PROGRAM_ADDR)
add hl,de
jp (hl)
address_table:
.dw addr0
.dw addr1
.dw addr2
Unfortunately, this one would fail miserably:
ld a,(hl)
ld l,a
add a,a
add a,l
ld e,a
ld d,0
ld hl,&address_table
add hl,de
jp (hl)
address_table:
jp &addr0
jp &addr1
jp &addr2
This is very bad, since something is multiplied by 3 to get an
address, while instructions on the 68K can't be at odd addresses.
Even if it could, it would still fail as the addresses aren't lined
up properly. This would require the following *hideous kluge*
- Jump table detector in the converter which notices any string of
three or more jumps
- Jump table list at the end of the code, which holds the addresses
each jump table covers
- A much uglier memory address handler for computed jumps: It would
need to check the address you jump to against these tables, and do
the appropriate conversion if its present. That is, if it's in
the jump table's range, subtract the start address, divide by three,
and then compute the real address with the offset value.
- Even the "kinder, gentler" jump tables with JR instructions would
still need this kluge, since the instructions might expand enough
that the branches wouldn't fit in two bytes.
As you may have guessed by now, I'm not going to put this capability
in the first release of the converter, as it's a mess and not that
useful.
6) Self-modifying code.
Self-modifying code is common in Z80 programs, so it needs to be
dealt with. This is actually, easier than it seems, as most of it
is in the form of stuff like "ld (&label+1),a" which is simply used
to change the constant data of an instruction. This can be detected
by looking for writes to 1 beyond a code label. Then the converter
simply needs to determine the real offset into the new code, and
replace it.
However, this is not the only self-modifying code that's ever been
used, even though it's the most common. For example, in Z-Kart 3D,
I change the an opcode between a RET and a NEG. Since this sort of
thing is probably rare, I'll deal with it by implementing specific
handling for each of the few cases of it I see. Of course, the
first release will only do the standard stuff above.
7) The hard part. Once everything above has been done and tested,
then I can release a "workable" version of the program. However,
the above is only the easy part. Difficult stuff includes:
- Interrupts/grayscale (actually, this one isn't too bad)
- Variables
- Dynamically constructed jump tables (Orzunoid uses this to help
with external levels; the first converted release of Orzunoid
will be from an older version of the game which doesn't do this)
- Programs that throw code around (e.g. Z-Kart 3D which copies its
code to the graph screen)
- Dealing with multiple calculators: the TI-82 and TI-83 probably
wouldn't be that hard to implement, but TI-86 is a real mess due to
its memory swapping and such
- And there's probably more stuff that's so horrible I can't even
think of it...
Follow-Ups:
References: