PDA

Просмотр полной версии : мысли по написанию модуля эмуляции z80 на С



boo_boo
21.01.2006, 19:01
думаю написать отдельный модуль эмуляции z80, на чистом C, чтоб с полпинка вставлялся куда угодно -- библиотека и h-файл. за основу возьму, наверное, код из FUSE.. или US...
вопрос в том, каким делать API, требования -- возможность создания нескольких процессоров, никаких экспортируемых переменных, только функции, ну и поддержка всех фич, ессно :)
набросал вот чего-то:


/*процессор -- поперто из FUSE*/
typedef struct {
regpair af,bc,de,hl;
regpair af_,bc_,de_,hl_;
regpair ix,iy;
byte i;
word r;
byte r7; /* The high bit of the R register */
regpair sp,pc;
byte iff1, iff2, im;
int halted;
} Z80;

/*создание и инициализация процессора.
page1/2/3 -- указатели на массивы (длиной 16K),
которые будут использоваться как страницы памяти*/
Z80 *z80_create(void *page1, void *page2, void *page3);

/*уничтожение процессора -- тешу свою страсть к разрушению ;) */
void z80_destroy(Z80 *cpu);

/*замена одной из 3х страниц памяти -- для вещей вроде
переключения 128k страниц и тп*/
void z80_set_mempage(Z80 *cpu, int page_num, void *page);

/*выполнение очередной команды, возвращает затраченное
кол-во тактов*/
int z80_step(Z80 *cpu);

/*установка функции-callback'а на чтение из порта*/
void z80_set_pread_cb(Z80 *cpu, z80_pread_cb cb_fn);

/*установка callback'а на запись в порт*/
void z80_set_pwrite_cb(Z80 *cpu, z80_pwrite_cb cb_fn);

/*установка callback'а на чтение из памяти*/
void z80_set_mread_cb(Z80 *cpu, z80_mread_cb cb_fn);

/*установка callback'а на запись в память*/
void z80_set_mwrite_cb(Z80 *cpu, z80_mwrite_cb cb_fn);

/*прерывание*/
void z80_int(Z80 *cpu);

/*немаскируемое прерывание*/
void z80_nmi(Z80 *cpu);

/*сброс*/
void z80_reset(Z80 *cpu)

/*функции для получения значений регистров -- чтоб
не завязываться на поля struct'уры Z80*/
лень писать ,)

SMT
21.01.2006, 20:19
а зачем свою? если TR-DOS был слабоват, то для Z80 легче доисправить то, что есть в глюкалке

а почему 16k массивов только 3, а не 4?

Z80-ядер навалом. могу предложить поискать Z80-ядро как раз под gcc/gpl от (C) Marat Fayzullin (автор эмуля MSX-2), оно ещё и с дизасмом (если не найдёшь, есть RAR архив - 19k)

если не гнаться за скоростью, подойдёт любая реализация. а если гнаться, то нужно встроить функции чтения/записи памяти и портов прямо в код эмуляции инструкций Z80 (без вызова через указатель), и прочие нужности вроде всяких breakpoints на разные события

модульно оформить можно, написав эти функции с пометкой inline и проинклюдив ядро Z80, либо более красиво - как класс-шаблон, тогда в конструктор передаётся класс, читающий память/порты, ест-но inline-функциями

на самом деле, Z80 неразрывно связан с циклом эмуляции. потому что INT обрабатывается в зависимости от того, была ли пред. команда EI

для скорпиона с профПЗУ страницы ПЗУ переключаются при чтении определённых адресов. пентагоновский кеш 2-8K имеет страницы, меньше чем 16K, причём запись в одну область должна сказываться на зеркальных остальных (реализовано в Z80S - там в ядре размер страницы не 16K, а 2K). точная эмуляция бордюрных эффектов требует сделать запись в порт с временной меткой где-то между началом и концом команды, причём сдвиг зависит от типа команды: outi, out (#FE),a или out (c),d

эти 3 примера не учтены предлагаемым API

вообще, отвязать общее время от времени внутри кадра - хорошая идея. жалко, что я её не заметил раньше (наверное, поздновато разглядел __int64). в unreal все времени получились жутко запутанными

boo_boo
21.01.2006, 21:07
а зачем свою? если TR-DOS был слабоват, то для Z80 легче доисправить то, что есть в глюкалке

я как на исходники глюкалки смотрю, так руки опускаются -- все одним сплошным клубком... хочется, чтобы как в жизни -- z80 же не знает, что внутри у ВГ, и наоборот, они просто общаются через некий интерфейс. вообщем, ИМХО, такое разделение и мне время сэкономит (в коде легче ориентироваться и отлаживать, когда все по полочкам), и людям пригодится.


а почему 16k массивов только 3, а не 4?

ага, 4 их, глючу :)


Z80-ядер навалом

я не нашел ни одного практически, которое не требует вмешательства в код, чтобы его использовать... а если так, то заодно можно все малость перелопатить и свой API сделать. Файзуллинский эмуль хорош, но не позволяет больше одного процессора создать, а это может пригодиться (GS и тп)

за скоростью гнаться не хочу, с учетом мощности современных компов затраты на вызов функции через указатель не пугают :)
сделать API классами можно с одной стороны, но с другой чистый C универсальней, а ситуации, в которой понадобиться наследовать от класса Z80, не могу представить :o


на самом деле, Z80 неразрывно связан с циклом эмуляции. потому что INT обрабатывается в зависимости от того, была ли пред. команда EI

можно флажок завести на этот случай...


для скорпиона с профПЗУ страницы ПЗУ переключаются при чтении определённых адресов.

ну так можно в callback'e на чтение памяти отследить это, и поменять страницу...


пентагоновский кеш 2-8K имеет страницы, меньше чем 16K, причём запись в одну область должна сказываться на зеркальных остальных (реализовано в Z80S - там в ядре размер страницы не 16K, а 2K).

а где можно прочитать про этот самый кэш? слабо себе представляю, кто он такой :(


точная эмуляция бордюрных эффектов требует сделать запись в порт с временной меткой где-то между началом и концом команды, причём сдвиг зависит от типа команды: outi, out (#FE),a или out (c),d

а можно пару слов о том, как это в US сделано?

SMT
21.01.2006, 22:09
, в которой понадобиться наследовать от класса Z80, не могу представить
ну можно наследовать, переопределив виртуальные ф-ции чтения портов/памяти, но это не освобождает от вызова через указатель. надо не наследовать от класса Z80, а делать типа этого:


CUniversalZ80WithoutMemPorts<CSpectrumMem,CSpectrumPorts> MainZ80;
CUniversalZ80WithoutMemPorts<CSpectrumMemDbg,CSpectrumPortsDbg> MainZ80Dbg;
CUniversalZ80WithoutMemPorts<CGSMem,CGSPorts> gsZ80;

таких ядер легко можно наделать сколько надо, причём всё, что относится к памяти/портам будет заинлайнено. в принципе, в unreal 3 ядра. не знаю, стоит ли ради повышения скорости в отдельных случаях так сильно увеличивать размер конечного exe-шника. хотя, если теперь эмуляторы даже на жабе пишут, скоростью можно пожертвовать

где можно прочитать про этот самый кэш? слабо себе представляю, кто он такой
журналы чёрн.ворона#3, dejavu#7. это статическое ОЗУ 2-16k, припаиваемое параллельно ПЗУ. имеется небольшая схемка, которая включает его вместо пзу (например, вместо TR-DOS, позволяя программе в этом кеше обращаться сразу и ко всей 48-й памяти, и к портам дисковода). если ОЗУ-шка маленькая (2-8k), то старшие линии адреса никуда не заводятся, поэтому запись в адрес #0111 "запишет" и в #0911,#1111,#1911,#2111,#2911,#3111,#3911...


сделать запись в порт с временной меткой где-то между началом и концом команды

пару слов о том, как это в US сделано?
ядро Z80 увеличивает счётчик тактов cpu.t на нужную величину, вызывает функцию записи в порт (которая читает этот счётчик), и снова увеличивает cpu.t, в сумме эти приращения дадут время выполнения команды. если, как ты хочешь, совсем отказываться от глобальных переменных, придётся эти временные метки таскать как параметры:
например, в void step(Z80 *cpu, int64 &tick) передавать время начала, потом, например, в команде OTIR вызывать void port_out(cpu->bc, read_mem(cpu->hl), tick+16), а потом увеличивать tick на 21

SMT
21.01.2006, 23:12
если делать совсем универсальное ядро, нужно предусмотреть увеличение текущего времени и в функциях работы с памятью/портами - в некоторых моделях, да хоть в оригинальных 48/128, вырабатывается WAIT при обращении к порту #FE и половине страниц памяти. так же могут тормозить процессор и устройства типа скорпионовского контроллера пц-клавиатуры

boo_boo
23.01.2006, 20:10
насчет страниц памяти я брежу -- z80 не знает никаких страниц, с ними имеет дело уже контроллер памяти, а его эмуляция -- отдельный вопрос.
так что, думаю, достаточно задания callback'ов на чтение памяти (возвращает байт по адресу) и на запись (соотв выставляет)


ядро Z80 увеличивает счётчик тактов cpu.t на нужную величину, вызывает функцию записи в порт (которая читает этот счётчик), и снова увеличивает cpu.t, в сумме эти приращения дадут время выполнения команды. если, как ты хочешь, совсем отказываться от глобальных переменных, придётся эти временные метки таскать как параметры:
например, в void step(Z80 *cpu, int64 &tick) передавать время начала, потом, например, в команде OTIR вызывать void port_out(cpu->bc, read_mem(cpu->hl), tick+16), а потом увеличивать tick на 21
угу... хммм... а если так: добавить еще один callback, который будет вызываться на каждом такте. в нем организовывать задержку, запускать кадровый синхроимпульс, INT.... Вообщем, делать там все, что имеет отношение к таймингу. А WAIT от памяти и #FE устраивать тоже снаружи, перехватив эти операции в callback'ах обращения к памяти/порту.

SMT
23.01.2006, 21:00
с потактовым callback'ом будет в 10 раз медленнее. сейчас, конечно, не времена P-133, поэтому всё равно. но если переносить из unreal AY и ULA с отвязкой их от Z80, друг от друга и от других устройств, придётся покилять все супер-извраты и сделать в лоб, как и для Z80, функцию step, которая эмулирует 1 такт работы устройства и вызывать её из такого callback'а

сейчас придумал независимый интерфейс для AY: на входе массив записей в порт (ном.регистра, значение, такт AY), и последний такт эмуляции (на случай, если записи не было, но звук получить надо). на выходе - количество полных семплов, выданных AY-ком до нужного такта и массив собственно семплов. вызывающая функция обязана предоставить буфер достаточного размера. таким образом, минимизируются потери от частых переключений Z80/ULA/AY, также можно писать такой буфер сразу в PSG-файл, или, записав лишь последние значения регистров в кадре и прогнав через LHA, в VTX-файл. так больше нравится, чем потактовая эмуляция?

о!, в следующих версиях unreal так и сделаю - должна появиться прибавка в производительности за счёт минимизации замещения данных к L1-кеше кода и последовательному доступу к массивам. заодно аниальясинговый FIR-фильтр можно будет написать более прозрачно, почти "в лоб"

boo_boo
23.01.2006, 21:26
сейчас придумал независимый интерфейс для AY: на входе массив записей в порт (ном.регистра, значение, такт AY), и последний такт эмуляции (на случай, если записи не было, но звук получить надо). на выходе - количество полных семплов, выданных AY-ком до нужного такта и массив собственно семплов. вызывающая функция обязана предоставить буфер достаточного размера. таким образом, минимизируются потери от частых переключений Z80/ULA/AY, также можно писать такой буфер сразу в PSG-файл, или, записав лишь последние значения регистров в кадре и прогнав через LHA, в VTX-файл. так больше нравится, чем потактовая эмуляция?

то есть аккумулировать обращения к портам AY, а потом скармливать такой ф-ии, получая от нее кусок звука... очень симпатично :)
но тогда ведь будет некоторое отставание AY от Z80 -- что делать, если кодер захочет не записать регистр, а прочитать?

SMT
23.01.2006, 23:20
а, это мелочь. хранить массив из 16 регистров, они же не меняются со стороны самой AY

boo_boo
24.01.2006, 19:28
а, это мелочь. хранить массив из 16 регистров, они же не меняются со стороны самой AYа, круто :)

очередной вариант api для z80, с учетом недоработок предыдущего:


enum Z80_REG_T {regAF,regBC,regDE,regHL,regAF_,regBC_,regDE_,regH L_,regIX,regIY,regPC,regSP,regIR,regIM/*0,1 или 2*/,regIFF1,regIFF2};

typedef void (*z80_tstate_cb)();
/*последующие 4 callback'a первым аргументом принимают номер такта в шаге, на
котором производится ввод/вывод*/
typedef unsigned char (*z80_pread_cb)(unsigned char t_state, unsigned port);
typedef void (*z80_pwrite_cb)(unsigned char t_state, unsigned port, unsigned char value);
typedef unsigned char (*z80_mread_cb)(unsigned char t_state, unsigned addr);
typedef void (*z80_mwrite_cb)(unsigned char t_state, unsigned addr, unsigned char value);

struct _z80_cpu_context;
typedef struct _z80_cpu_context Z80;

/*создание и инициализация процессора.*/
Z80 *z80_create();

/*уничтожение процессора -- тешу свою страсть к разрушению ;) */
void z80_destroy(Z80 *cpu);

/*выполнение очередной команды, возвращает затраченное кол-во тактов*/
int z80_step(Z80 *cpu);

/*установка callback'a, который будет вызываться на каждом такте эмуляции*/
void z80_set_tstate_callback(Z80 *cpu, z80_tstate_cb cb_fn);

/*установка функции-callback'а на чтение из порта*/
void z80_set_pread_cb(Z80 *cpu, z80_pread_cb cb_fn);

/*установка callback'а на запись в порт*/
void z80_set_pwrite_cb(Z80 *cpu, z80_pwrite_cb cb_fn);

/*установка callback'а на чтение из памяти*/
void z80_set_mread_cb(Z80 *cpu, z80_mread_cb cb_fn);

/*установка callback'а на запись в память*/
void z80_set_mwrite_cb(Z80 *cpu, z80_mwrite_cb cb_fn);

/*прерывание, второй аргумент - код операции для IM0 %)*/
void z80_int(Z80 *cpu, unsigned char op);

/*немаскируемое прерывание*/
void z80_nmi(Z80 *cpu);

/*генерация w_states WAIT-циклов. (при этом будет w_states раз вызван
тактовый callback)
для использования в callback'ах обращения к памяти и
портам*/
void z80_w_states(Z80 *cpu, unsigned w_states);

/*сброс*/
void z80_reset(Z80 *cpu)

/*функция для получения значения регистра*/
unsigned z80_get_reg(Z80 *cpu, enum Z80_REG_T reg);

/*функция для установки значения регистра*/
unsigned z80_set_reg(Z80 *cpu, enum Z80_REG_T reg, unsigned value);

/*возвращает 1 если z80 ждет на halt'e*/
int z80_is_halted(Z80 *cpu);