wiz-lang / wiz

A high-level assembly language for writing homebrew software and games on retro console platforms.
http://wiz-lang.org/
Other
407 stars 40 forks source link

Add variables/pointers to external I/O ports (non-memory mapped) #54

Open Bananattack opened 5 years ago

Bananattack commented 5 years ago

Reading and writing to I/O ports on the Z80, is a bit cumbersome compared to writing to memory-mapped I/O registers right now, because you need to use instrinsics to access them:

io_read
io_write
io_read_increment
io_read_decrement
io_write_increment
io_write_decrement
io_read_increment_repeat
io_read_decrement_repeat
io_write_increment_repeat
io_write_decrement_repeat

For simple read/write, it would be nice if it were as convenient as writing to a memory-mapped register, where you can just use assignment style syntax. In other cases, where there are several operations going on, it might make sense to keep them as intrinsics.

The other issue is that when calling these I/O functions, they currently just take a u8, which is easy to mix up if you're not careful. For the time being, it has meant all I/O port related definitions for SMS/GG and MSX are declared in their own namespace to help prevent passing the wrong address. This sort of separation is kinda helpful because, as an example, it prevents mixing up VDP-side register addresses with the I/O ports used to access the VDP.

If the type system could enforce that only I/O addresses (or integers explicitly casted to them) are passed to the io functions, that would save a lot of small debugging headaches. Allowing a type would also allow a more convenient assignment syntax to be possible. And it would allow taking the address of I/O ports, and treating them as a distinct kind of pointers that is separate from regular pointers.

SDCC (a C compiler for Z80 systems) seems to have support for I/O port declarations. Some platforms that GCC supports have a way to mark something as an I/O port as well, via attributes. It could be pretty handy. Especially, as a part of type system, not just an attribute.

Another benefit of I/O ports being part of the type system, is that it would also interact with writeonly/const qualifiers, so you could have separate port declarations for a read-only and write-only view of the same port address. (This can be useful if you have a different set of flags that are read than can be written. eg. a port that is a command port on write-side, but status port on read-side)

I think we should add a new keyword ioport (or io, or maybe something else, TBD).

Then stuff like this would be possible:

namespace msx {
    namespace vdp {
        extern ioport var data @ 0x99 : u8;
        extern ioport writeonly control @ 0x99 : u8;
        extern ioport const status @ 0x99 : u8;   
        // ...
    }
    // ...
}

and code like this would work (no calls to i/o functions needed here)

// assign to an I/O port.
msx.vdp.control = a = <:0x0000;
msx.vdp.control = a = >:0x0000 | msx.vdp.CONTROL_ACCESS_VRAM_WRITE;
c = &msx.vdp.data as u8; // take the address of an I/O port
hl = 0x2000;
do {
    *(c as *ioport u8) = a = 0; // indirectly assign to an I/O port
    hl--;
    a = h | l;
} while !zero;

For the other I/O calls, they would remain the same, but would take I/O port arguments instead. This would also prevent passing a raw u8 to something that expects an I/O port, without casting to acknowledge you know what you're doing.

on Z80 sizeof(*ioport T) would be 1, because I/O port indices are 8-bit. So I/O port pointers could be a different size than memory pointers potentially.

The trickiest bit would probably be adjusting the platform system to handle other kinds of pointers. It supports far pointers, right now, but it probably needs a way to support yet another pointer type, with its own platform-specific size. And unlike far pointers (which are almost always useful, even if the CPU can't handle them, due to memory mappers / bankswitching being common on almost all 8-bit systems), I/O port pointers shouldn't need to exist on systems that lack a separate I/O bus since they'd actually have no use on a platform lacking that feature.

The other tricky part would be propagating the ioport qualifiers of variables and pointers.

Maybe there will be other discovered work along the way. But this is a rough sketch of the idea that should give a general outline on how this feature could work.

Bananattack commented 5 years ago

Keyword bike-shedding: Might go with ioreg (short for "I/O register") rather than ioport for the keyword. import is very close in spelling (for readability), and might be easy to mix up with ioport during reading if not looking carefully (writing it that way is another story, since they're syntactically different enough). Also ioreg is shorter.

io also looks nice as an even shorter qualifier, but making it a reserved keyword would clash with user-defined identifiers. Probably not possible to make it a contextual keyword without introducing ambiguity.

There could also be iospace just to indicate that the address exists in I/O space without saying it's a "register" per-se, but it's even longer than the other suggestions.