Search code examples
cpointerscpu-registersgameboy

Is it possible to create two uint8_t pointers to the first and second half of a value pointed to by a uint16_t pointer?


I'm attempting to write a Gameboy emulator in C, and am currently in the process of deciding how to implement the following behavior:

  • Two 8-bit registers can be combined and treated as a single 16-bit register
  • changing the value of one of the 8-bit registers in the pairing should change the value of the combined register

For example, registers A and F, which are 8-bit registers, can be used jointly as the 16-bit register AF. However when the contents of registers A and F change, these changes should be reflected in subsequent referrals to register AF.

If I implement register AF as a uint16_t*, can I store the contents of registers A and F as uint8_t*'s pointing to the first and second byte of register AF respectively? If not, any other suggestions would be appreciated :)

EDIT: Just to clarify, this is a very similar architecture to the Z80


Solution

  • Use a union.

    union b
    {
        uint8_t a[2];
        uint16_t b;
    };
    

    The members a and b share the bytes. When a value is written in member a and then read using member b the value is reinterpreted in this type. This could be a trap representation, which would cause undefined behavior, but types uint8_t and uint16_t don't have them.

    Another issue is endianness, writing into the first element of member a will always change the first byte of member b, but depending on endianness that byte might represent most or least significant bits of b, so the resulting value will differ over architectures.


    To avoid trap representations and endianness, rather only use the type uint16_t and write into it using bitwise operations. For example, to write into the most significant 8 bits:

    uint16_t a = 0;
    uint8_t b = 200;
    a =  ( uint16_t )( ( ( unsigned int )a & 0xFF ) | ( ( unsigned int )b << 8 ) ) ;
    

    and similarly for the least significant 8 bits:

    a =  ( uint16_t )( ( ( unsigned int )a & 0xFF00 ) | ( unsigned int )b );
    

    These operations should be put into a function.

    The casts to ( unsigned int ) are there to avoid integer promotions. If INT_MAX equals 2^15-1, and there are trap representations for signed integers, then the operation: b << 8 could technically cause undefined behavior.