Можно ли создать два указателя 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
что сделает ваш код менее подверженным ошибкам из-за вычисления адреса указателя