The newcomer to ANSI C might wonder how to access I/O registers easily on his platform of choice without having to resort to assembly language. Whether one should avoid using C for coding games or demos on retro platforms is not to be discussed here.
First we have to define memory mapped I/O. In essence this means that any hardware register on any custom chip or device can be accessed with the same instructions used to access RAM. Converted to C, this means you can use a simple pointer and indirect addressing.
For those who move from assembly language to C, if your system has no special IN or OUT instructions for hardware, your platform uses memory mapped I/O. The 6502/6510, 68000 and ARM CPUs all qualify for this, the Z80 notably does not but C compilers for the Z80 will provide special means to access their I/O hardware instead.
I will break down the “how” into a few pieces of C code for your pleasure:
volatile uint8_t * const HWREG = (uint8_t *)0xE000;
This declares a basic pointer to some hardware register at memory location $E000. One should declare it volatile because hardware I/O locations may change inside a different context (interrupt and/or hardware event in our case).
The pointer address itself (essentially the hardware register you want to point to) should be const between register name and the type definition so that noone may modify the pointer and make the pointer address itself readonly but not necessarily the contents of the register.
Adding const before the declaration will declare a read/only register which you can’t modify but just read later on in your algorithm.
const volatile uint8_t * const HWREG_RO = (uint8_t *)0xE00E;
To alter the contents of the declared hardware register, you use the pointer. Note that the register declaration may not be readonly.
*HWREG = 0x5a; /* set I/O for demo purpose */ /* access to alter the pointer is forbidden! Uncomment to try! */ /* HWREG = (uint8_t *) 0xaaaa; */
You can read the register simply by dereferencing the pointer:
uint8_t HWREG_Read() { return(*HWREG); /* read I/O */ }It is also possible to map a whole struct to a pointer and fix the pointer to some I/O location. In effect you can reference whole register sets with one properly named struct. Envision something like this on Amiga:
blitter-> BLTCPTH = &mydata;
The concept can also be extended to access several I/O chips which basically use the same I/O register layout. Simply have pointers to more than one structure. Something like this:
Chip[0].REG_A = 0xB0;On naming the pointer, you should try to use the names as defined by the hardware documentation so the potential reader can directly see which register will be accessed. Adding a _PTR to the name might be helpful to remind that the register has to be accessed with pointer commands * and &.
As a final advice, you should check the compiler output to verify correct code generation. Sometimes there are ways of optimizing.
E.q. on a Motorola 68000 the read access should be translated to
move.b $E000,D0instead of
lea $E000,A5 move.b (A5),D0Activating proper compiler flags will help a lot here. Don’t be shy and experiment. It is possible to generate well working code using this approach. It is actually in heavy worldwide use in various sorts of Embedded Systems like car subsystems and networking equipment.
Simon Sunnyboy /Paradize for LowRes Magazine, January 2011