Можно ли создать два указателя uint8_t на первую и вторую половину значения, на которое указывает указатель uint16_t?

Я пытаюсь написать эмулятор Gameboy на C и в настоящее время решаю, как реализовать следующее поведение:

  • Два 8-битных регистра могут быть объединены и рассматриваться как один 16-битный регистр
  • изменение значения одного из 8-битных регистров в сопряжении должно изменить значение объединенного регистра

Например, регистры A и F, которые являются 8-битными регистрами, могут использоваться совместно как 16-битный регистр AF. Однако, когда содержимое регистров A и F изменяется, эти изменения должны быть отражены в последующих ссылках на регистр AF.

Если я реализую регистр AF как uint16_t*могу ли я хранить содержимое регистров A и F как uint8_t*указывает на первый и второй байт регистра AF соответственно? Если нет, любые другие предложения будут оценены:)

РЕДАКТИРОВАТЬ: просто чтобы уточнить, это очень похоже на архитектуру Z80

3 ответа

Решение

Используйте союз.

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

Члены a и b делят байты. Когда значение записано в элементе a а затем читать с использованием члена b значение повторно интерпретируется в этом типе. Это может быть представление ловушки, которое может вызвать неопределенное поведение, но типы uint8_t и uint16_t не имеют их.

Другая проблема - это порядковый номер, запись в первый элемент члена a всегда будет менять первый байт члена b, но в зависимости от порядка байтов этот байт может представлять наиболее или наименее значимые биты bпоэтому итоговое значение будет отличаться от архитектуры.


Чтобы избежать представления ловушек и порядка байтов, используйте только тип uint16_t и записать в него, используя побитовые операции. Например, чтобы записать в старшие 8 бит:

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

и аналогично для младших 8 битов:

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

Эти операции должны быть помещены в функцию.

Броски к ( unsigned int ) существуют, чтобы избежать целочисленных рекламных акций. Если INT_MAX равно 2^15-1, и есть целые представления для целых чисел со знаком, затем операция: b << 8 технически может вызвать неопределенное поведение.

Вы можете решить это так:

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;

Сейчас aptr указывает на старшие биты и fptr указывает на младшие биты, независимо от того, работает ли эмулятор на машине с прямым или прямым порядком байтов.

Примечание: так как эти типы указателей различны, компилятор может не знать, что они указывают на одну и ту же память, оптимизируя таким образом код для записи в *afptr не видно при последующем прочтении *aptr, Следовательно volatile,

Конечно, вы можете использовать указатели, чтобы указывать куда угодно.

#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);
}

Вам нужно позаботиться о порядке байтов, как упомянуто в комментарии (я полагаю, что у геймбиана есть порядок байтов, а у x86 - порядок байтов).

И, конечно, как большинство людей рекомендуют вам использовать union что сделает ваш код менее подверженным ошибкам из-за вычисления адреса указателя

Другие вопросы по тегам