Просмотр полной версии : мысли по написанию модуля эмуляции z80 на С
думаю написать отдельный модуль эмуляции 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*/
лень писать ,)
а зачем свою? если 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 все времени получились жутко запутанными
а зачем свою? если 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 сделано?
, в которой понадобиться наследовать от класса 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
если делать совсем универсальное ядро, нужно предусмотреть увеличение текущего времени и в функциях работы с памятью/портами - в некоторых моделях, да хоть в оригинальных 48/128, вырабатывается WAIT при обращении к порту #FE и половине страниц памяти. так же могут тормозить процессор и устройства типа скорпионовского контроллера пц-клавиатуры
насчет страниц памяти я брежу -- 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'ах обращения к памяти/порту.
с потактовым callback'ом будет в 10 раз медленнее. сейчас, конечно, не времена P-133, поэтому всё равно. но если переносить из unreal AY и ULA с отвязкой их от Z80, друг от друга и от других устройств, придётся покилять все супер-извраты и сделать в лоб, как и для Z80, функцию step, которая эмулирует 1 такт работы устройства и вызывать её из такого callback'а
сейчас придумал независимый интерфейс для AY: на входе массив записей в порт (ном.регистра, значение, такт AY), и последний такт эмуляции (на случай, если записи не было, но звук получить надо). на выходе - количество полных семплов, выданных AY-ком до нужного такта и массив собственно семплов. вызывающая функция обязана предоставить буфер достаточного размера. таким образом, минимизируются потери от частых переключений Z80/ULA/AY, также можно писать такой буфер сразу в PSG-файл, или, записав лишь последние значения регистров в кадре и прогнав через LHA, в VTX-файл. так больше нравится, чем потактовая эмуляция?
о!, в следующих версиях unreal так и сделаю - должна появиться прибавка в производительности за счёт минимизации замещения данных к L1-кеше кода и последовательному доступу к массивам. заодно аниальясинговый FIR-фильтр можно будет написать более прозрачно, почти "в лоб"
сейчас придумал независимый интерфейс для AY: на входе массив записей в порт (ном.регистра, значение, такт AY), и последний такт эмуляции (на случай, если записи не было, но звук получить надо). на выходе - количество полных семплов, выданных AY-ком до нужного такта и массив собственно семплов. вызывающая функция обязана предоставить буфер достаточного размера. таким образом, минимизируются потери от частых переключений Z80/ULA/AY, также можно писать такой буфер сразу в PSG-файл, или, записав лишь последние значения регистров в кадре и прогнав через LHA, в VTX-файл. так больше нравится, чем потактовая эмуляция?
то есть аккумулировать обращения к портам AY, а потом скармливать такой ф-ии, получая от нее кусок звука... очень симпатично :)
но тогда ведь будет некоторое отставание AY от Z80 -- что делать, если кодер захочет не записать регистр, а прочитать?
а, это мелочь. хранить массив из 16 регистров, они же не меняются со стороны самой AY
а, это мелочь. хранить массив из 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);
Powered by vBulletin® Version 4.2.5 Copyright © 2024 vBulletin Solutions, Inc. All rights reserved. Перевод: zCarot