4

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

3 Answers3

4

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.

2501
  • 25,460
  • 4
  • 47
  • 87
  • Does this mean I would have to use: AF.A, AF.F, AF.AF whenever I need to access registers individually? And does modifying one part modify the whole? –  Jul 21 '16 at 06:17
  • @JoshThieme It does, but be careful. I would suggest you use the second option. See the update. – 2501 Jul 21 '16 at 06:21
  • Thank you for the in depth answer. However, I believe that due to how some of the cpu instructions work, a and b be need to be pointers to integers rather than integers. Can this be adapted to that case? –  Jul 21 '16 at 06:37
  • @JoshThieme Pointing raw pointers is problematic in this case. Most types do not alias. I suggest you ask a new question with a specific example. – 2501 Jul 21 '16 at 06:50
0

You could solve it like this:

volatile uint16_t afvalue = 0x55AA;   // Needs long lifetime.

volatile uint16_t *afptr = &afvalue;
volatile uint8_t  *aptr  = (uint8_t *)afptr;
volatile uint8_t  *fptr  = aptr + 1;

if (*aptr == 0xAA) {         // Take endianness into account.
  aptr++;                    // Swap the pointers.
  fptr--;
}

afvalue = 0x0000;

Now aptr points to the high bits and fptr points to the low bits, no matter if the emulator runs on a little endian or big endian machine.

Note: since these pointer types are different, a compiler might not be aware that they point to the same memory, thereby optimizing the code so that a write to *afptr is not seen by a later read from *aptr. Therefore the volatile.

Roland Illig
  • 40,703
  • 10
  • 88
  • 121
  • 2
    Volatile has nothing to do with strict aliasing. If the types are not compatible it is undefined behavior. – 2501 Jul 21 '16 at 07:21
-1

You could of course use pointers to point anywhere you want.

#include<stdio.h>
#include <stdint.h>

int main() {
    uint16_t a=1048;
    uint8_t *ah=(uint8_t*)&a,*al=((uint8_t*)&a)+1;
    printf("%u,%u,%u\n",a,*ah,*al);
}

You would need to take care of endianness as mentioned in the comment ( I believe the gameboy is little endian, while the x86 is big endian ).

And of course as most poeple recommend you should use a union which would make your code less error prone due to pointer address calculations

dvhh
  • 4,724
  • 27
  • 33