PDA

Просмотр полной версии : Эмуляция встроенного таймера процессора 1801ВМ1



Patron
14.02.2011, 16:10
После завершения серии тестов (http://bk0010.org/forum/?id=3799) встроенного таймера процессора 1801ВМ1 ( но же ВЕ-таймер ) мною написан алгоритм максимально точной эмуляции его работы.

Предлагаю всем заинтересованным участникам проверить корректность алгоритма:



typedef unsigned long long QWORD;
typedef unsigned long DWORD;

#define WORD(a) (*(unsigned short *)&aMemory[a])

PowerON()
{
WORD(0177706) = wCPU_ID;
wVE_Timer_Counter = 0177777;
WORD(0177710) = wVE_Timer_Counter;
WORD(0177712) = 0177400;
}

Reset()
{
WORD(0177712) = 0;
Last_IO_Mode = IO_WRITE;
wLast_IO_Addr = 0177712;
Make_IO();
}

Make_IO()
{
switch( wLast_IO_Addr )
{
// VE-Timer Counter
case 0177710:
{
if( Last_IO_Mode == IO_WRITE )
{
WORD(0177710) = wVE_Timer_Counter;
}
else
{
if( bVE_Go )
{
QWORD qwCurrentCPU_RunTimeNS = qwCPU_TotalRunTimeNS;
qwCurrentCPU_RunTimeNS += dwCurrentStepsLatencyNS;
QWORD qwCurrentCPU_Clocks = qwCurrentCPU_RunTimeNS/uCPU_CycleNS;
QWORD qwUsedCPU_Clocks = qwCurrentCPU_Clocks - qwVE_TimerStart_CPU_Clocks;
qwUsedCPU_Clocks += qwVE_TimerStart_CPU_Clocks%128;
QWORD qwCurrentVE_Timer_Clocks = qwUsedCPU_Clocks/nVE_CPU_Clock_Divider;

if( !dwInitialVE_CounterVal ) { dwInitialVE_CounterVal = 0x10000; }

DWORD dwTimerClocksHi = qwCurrentVE_Timer_Clocks / dwInitialVE_CounterVal;
DWORD dwTimerClocksLow = qwCurrentVE_Timer_Clocks % dwInitialVE_CounterVal;

if( dwTimerClocksHi )
{
bVE_Expiry = true;

if( bVE_NoReload )
{
dwTimerClocksLow = ( qwCurrentVE_Timer_Clocks - dwInitialVE_CounterVal ) % 0xFFFF;
dwInitialVE_CounterVal = 0xFFFF;
wVE_Timer_Counter = dwInitialVE_CounterVal - dwTimerClocksLow;
}
else if( bVE_NoRestart )
{
wVE_Timer_Counter = 0;
}
else
{
wVE_Timer_Counter = dwInitialVE_CounterVal - dwTimerClocksLow;

if( !dwTimerClocksLow || dwInitialVE_CounterVal == 1 )
{
if( qwCurrentCPU_Clocks%128 < 5 )
{
wVE_Timer_Counter = 0;
}
}
}
}
else
{ // dwTimerClocksHi == 0
wVE_Timer_Counter = dwInitialVE_CounterVal - dwTimerClocksLow;
}

WORD(0177710) = wVE_Timer_Counter;
}
}
}
break;

// VE-Timer CSR
case 0177712:
{
word wVE_Timer_CSR = WORD(0177712);

if( Last_IO_Mode == IO_READ )
{
wLast_IO_Addr = 0177710;
Make_IO();
wLast_IO_Addr = 0177712;

if( bVE_Expiry )
{
if( bVE_Monitor )
{
wVE_Timer_CSR |= BIT_7;
}

if( bVE_NoRestart && !bVE_NoReload )
{
wVE_Timer_CSR &= ~BIT_4;
}
}
WORD(0177712) = wVE_Timer_CSR|0xFF00;
}
else
{ // Last_IO_Mode == IO_WRITE
bVE_Expiry = false;

bVE_Go = wVE_Timer_CSR & BIT_4;

if( wVE_Timer_CSR & BIT_0 )
{
bVE_Go = false;
}

bVE_NoReload = wVE_Timer_CSR & BIT_1;
bVE_Monitor = wVE_Timer_CSR & BIT_2;
bVE_NoRestart = wVE_Timer_CSR & BIT_3;

nVE_CPU_Clock_Divider = 128;

if( wVE_Timer_CSR & BIT_5 )
{
nVE_CPU_Clock_Divider *= 16;
}

if( wVE_Timer_CSR & BIT_6 )
{
nVE_CPU_Clock_Divider *= 4;
}


QWORD qwCurrentCPU_RunTimeNS = qwCPU_TotalRunTimeNS;
qwCurrentCPU_RunTimeNS += dwCurrentStepsLatencyNS;

QWORD qwCurrentCPU_Clocks = qwCurrentCPU_RunTimeNS/uCPU_CycleNS;

if( bVE_Go )
{
qwVE_TimerStart_CPU_Clocks = qwCurrentCPU_Clocks;
}

dwInitialVE_CounterVal = WORD(0177706);
wVE_Timer_Counter = WORD(0177706);

WORD(0177710) = wVE_Timer_Counter;
WORD(0177712) = wVE_Timer_CSR|0xFF00;
}
}
break;
}
}


...

Добавлен файл VM1T8.zip, содержащий "1801VM1 Interrupts Test #8".
Добавлен файл VM1VE8.zip, содержащий "1801VM1 VE-Timer Test #8".
Добавлен файл VM1VE9.zip, содержащий "1801VM1 VE-Timer Test #9".
Добавлен файл VM1E10.zip, содержащий "1801VM1 VE-Timer Test #10".
Добавлен файл VM1E11.zip, содержащий "1801VM1 VE-Timer Test #11".

Patron
27.02.2011, 23:20
Дополнительные тесты позволили выявить несколько ошибок и неточностей в первой версии алгоритма.

Вот новая версия алгоритма, ещё более точно (как я полагаю) эмулирующая работу встроенного таймера процессора 1801ВМ1:



typedef unsigned long long QWORD;
typedef unsigned long DWORD;
typedef unsigned short word;

#define WORD(a) (*(unsigned short *)&aMemory[a])

PowerON()
{
WORD(0177706) = wCPU_ID;
wVE_Timer_Counter = 0177777;
WORD(0177710) = wVE_Timer_Counter;
WORD(0177712) = 0177400;
nVE_MonitoredExpiryCount = 0;
bVE_Expiry = false;
bVE_Go = false;
}

Reset()
{
WORD(0177712) = 0;
Last_IO_Mode = IO_WRITE;
wLast_IO_Addr = 0177712;
Make_IO();
}

Make_IO()
{
switch( wLast_IO_Addr )
{
// VE-Timer Counter Initial Value
case 0177706:
{
if( Last_IO_Mode == IO_WRITE )
{
Last_IO_Mode = IO_READ;
wLast_IO_Addr = 0177710;
Make_IO();
Last_IO_Mode = IO_WRITE;
wLast_IO_Addr = 0177706;

if( bVE_Go && !bVE_NoReload && !bVE_NoRestart )
{
QWORD qwCurrentCPU_RunTimeNS = qwCPU_TotalRunTimeNS;
qwCurrentCPU_RunTimeNS += dwCurrentStepsLatencyNS;
QWORD qwCurrentCPU_Clocks = qwCurrentCPU_RunTimeNS/uCPU_CycleNS;

if( qwCurrentCPU_Clocks >= qwNextVE_TimerStart_CPU_Clocks )
{
qwPrevVE_TimerStart_CPU_Clocks = qwNextVE_TimerStart_CPU_Clocks;
dwPrevInitialVE_CounterVal = dwNextInitialVE_CounterVal;

QWORD qwUsedCPU_Clocks = qwCurrentCPU_Clocks - qwPrevVE_TimerStart_CPU_Clocks;
QWORD qwCurrentVE_Timer_Clocks = qwUsedCPU_Clocks/nVE_CPU_Clock_Divider;

DWORD dwTimerClocksLow = qwCurrentVE_Timer_Clocks % dwPrevInitialVE_CounterVal;

qwNextVE_TimerStart_CPU_Clocks = qwPrevVE_TimerStart_CPU_Clocks;
qwNextVE_TimerStart_CPU_Clocks += ( dwPrevInitialVE_CounterVal - dwTimerClocksLow ) * nVE_CPU_Clock_Divider;
}

dwNextInitialVE_CounterVal = WORD(0177706);
if( !dwNextInitialVE_CounterVal ) { dwNextInitialVE_CounterVal = 0x10000; }
}
}
}
break;

// VE-Timer Counter
case 0177710:
{
if( Last_IO_Mode == IO_WRITE )
{
WORD(0177710) = wVE_Timer_Counter;
}
else
{
if( bVE_Go )
{
QWORD qwCurrentCPU_RunTimeNS = qwCPU_TotalRunTimeNS;
qwCurrentCPU_RunTimeNS += dwCurrentStepsLatencyNS;
QWORD qwCurrentCPU_Clocks = qwCurrentCPU_RunTimeNS/uCPU_CycleNS;

QWORD qwVE_TimerStart_CPU_Clocks = qwPrevVE_TimerStart_CPU_Clocks;
DWORD dwInitialVE_CounterVal = dwPrevInitialVE_CounterVal;

if( qwCurrentCPU_Clocks >= qwNextVE_TimerStart_CPU_Clocks )
{
bVE_Expiry = true;

if( !bVE_NoRestart )
{
qwVE_TimerStart_CPU_Clocks = qwNextVE_TimerStart_CPU_Clocks;
dwInitialVE_CounterVal = dwNextInitialVE_CounterVal;
}
}

QWORD qwUsedCPU_Clocks = qwCurrentCPU_Clocks - qwVE_TimerStart_CPU_Clocks;
QWORD qwCurrentVE_Timer_Clocks = qwUsedCPU_Clocks/nVE_CPU_Clock_Divider;

DWORD dwTimerClocksHi = qwCurrentVE_Timer_Clocks / dwInitialVE_CounterVal;
DWORD dwTimerClocksLow = qwCurrentVE_Timer_Clocks % dwInitialVE_CounterVal;

if( bVE_Expiry )
{
if( bVE_Monitor && (dwTimerClocksHi > nVE_MonitoredExpiryCount) )
{
nVE_MonitoredExpiryCount = dwTimerClocksHi;
}

if( bVE_NoReload )
{
wVE_Timer_Counter = dwInitialVE_CounterVal - dwTimerClocksLow;
}
else if( bVE_NoRestart )
{
bVE_Go = false;

wVE_Timer_Counter = WORD(0177706);

if( dwTimerClocksHi == 1 && !dwTimerClocksLow )
{
if( qwCurrentCPU_Clocks%128 < 5 )
{
wVE_Timer_Counter = 0;
}
}
}
else
{
wVE_Timer_Counter = dwInitialVE_CounterVal - dwTimerClocksLow;

if( !dwTimerClocksLow )
{
if( qwCurrentCPU_Clocks%128 < 5 )
{
wVE_Timer_Counter = 0;
}
}
}
}
else
{ // !bVE_Expiry
wVE_Timer_Counter = dwInitialVE_CounterVal - dwTimerClocksLow;
}

WORD(0177710) = wVE_Timer_Counter;
}
}
}
break;

// VE-Timer CSR
case 0177712:
{
IO_ModeType IO_Mode = Last_IO_Mode;

Last_IO_Mode = IO_READ;
wLast_IO_Addr = 0177710;
Make_IO();
wLast_IO_Addr = 0177712;
Last_IO_Mode = IO_Mode;

word wVE_Timer_CSR = WORD(0177712);

if( Last_IO_Mode == IO_READ )
{
if( bVE_Expiry )
{
if( bVE_Monitor && nVE_MonitoredExpiryCount > 1 )
{
wVE_Timer_CSR |= BIT_7;
}

if( bVE_NoRestart && !bVE_NoReload )
{
wVE_Timer_CSR &= ~BIT_4;
}
}
WORD(0177712) = wVE_Timer_CSR|0xFF00;
}
else
{ // Last_IO_Mode == IO_WRITE

if( nVE_MonitoredExpiryCount )
{
nVE_MonitoredExpiryCount = 2;
}

bVE_Expiry = false;

bVE_Go = wVE_Timer_CSR & BIT_4;

if( wVE_Timer_CSR & BIT_0 )
{
bVE_Go = false;
}

bVE_NoReload = wVE_Timer_CSR & BIT_1;
bVE_Monitor = wVE_Timer_CSR & BIT_2;
bVE_NoRestart = wVE_Timer_CSR & BIT_3;

nVE_CPU_Clock_Divider = 128;

if( wVE_Timer_CSR & BIT_5 )
{
nVE_CPU_Clock_Divider *= 16;
}

if( wVE_Timer_CSR & BIT_6 )
{
nVE_CPU_Clock_Divider *= 4;
}


if( bVE_Go )
{
QWORD qwCurrentCPU_RunTimeNS = qwCPU_TotalRunTimeNS;
qwCurrentCPU_RunTimeNS += dwCurrentStepsLatencyNS;
QWORD qwCurrentCPU_Clocks = qwCurrentCPU_RunTimeNS/uCPU_CycleNS;

QWORD qwVE_TimerStart_CPU_Clocks = qwCurrentCPU_Clocks;
qwVE_TimerStart_CPU_Clocks -= qwVE_TimerStart_CPU_Clocks%128;

QWORD dwInitialVE_CounterVal = WORD(0177706);
if( !dwInitialVE_CounterVal ) { dwInitialVE_CounterVal = 0x10000; }

dwPrevInitialVE_CounterVal = dwInitialVE_CounterVal;
dwNextInitialVE_CounterVal = dwInitialVE_CounterVal;

if( bVE_NoReload )
{
dwNextInitialVE_CounterVal = 0x10000;
}

qwPrevVE_TimerStart_CPU_Clocks = qwVE_TimerStart_CPU_Clocks;
qwNextVE_TimerStart_CPU_Clocks = qwVE_TimerStart_CPU_Clocks;
qwNextVE_TimerStart_CPU_Clocks += dwInitialVE_CounterVal * nVE_CPU_Clock_Divider;
}

wVE_Timer_Counter = WORD(0177706);

WORD(0177710) = wVE_Timer_Counter;
WORD(0177712) = wVE_Timer_CSR|0xFF00;
}
}
break;
}
}

nzeemin
10.07.2011, 13:30
Patron, не могли бы вы сделать ещё текстовое описание этого алгоритма. Код конечно кодом, тесты тестами, но чтобы до конца понять этот алгоритм нужно погрузиться в этот код и тесты до вашего уровня. А после этого нужно переписать алгоритм под своё окружение. В общем, текст с выводами полученными из тестов -- сильно бы помог.

Patron
10.07.2011, 20:26
Patron, не могли бы вы сделать ещё текстовое описание этого алгоритма. Код конечно кодом, тесты тестами, но чтобы до конца понять этот алгоритм нужно погрузиться в этот код и тесты до вашего уровня. А после этого нужно переписать алгоритм под своё окружение. В общем, текст с выводами полученными из тестов -- сильно бы помог.Мои планы следующие - я разрабатываю универсальный API эмуляции и в процессе этой разработки выяснилось, что необходимо разработать базовый универсальный API, который позволял бы экземпляру объекта-клиента подключаться к экземпляру объекта-сервера, задействуя (только в момент начального подключения) прокси-интерфейс, необходимый для согласования различных версий универсального API, возможно использовавшихся при компиляции модулей эмуляции, экспортирующих связываемые объект-клиент и объект-сервер.

Если сказать то же самое более простым языком - добавление (при развитии API ) новых методов в таблицу виртуальных функций делает невозможной прямую динамическую компоновку порождённых в эмуляторе экземпляров объектов, созданных разработчиками с использованием библиотек API разных версий. Использование прокси-интерфейса, осуществляющего динамическую компоновку экземпляров объектов модулей эмуляции не по номеру позиции метода в таблице виртуальных функций API, а по его сигнатуре - cделает универсальный API подлинно универсальным.

Сейчас все работы по этой тематике у меня отложены ( предположительно до осени ). После завершения разработки первой версии универсального API - я выложу различные исходники, включая почти референсную модель процессора 1801ВМ1 (исследование растактовок (http://bk0010.org/forum/?id=3799) которого, начатое Вами - ещё не закончено ).

Пока же - вот предварительный комментарий к выложенному исходнику ( если дополнительно нужен комментарий кода - то насколько подробный ? ).

Эмуляция в данном случае осуществляется покомандно. Движок эмулятора вызывает у модуля эмуляции процессора метод Steps(nSteps), передавая в качестве аргумента количество команд, которые нужно сэмулировать, и получая в глобальной переменной dwCurrentStepsLatencyNS - общее время в "эмуляторных" наносекундах, которое бы затратил реальный процессор на выполнение сэмулированных команд, которое движок затем прибавляет к общему "накопителю" qwCPU_TotalRunTimeNS, а dwCurrentStepsLatencyNS обнуляет.

Когда процессор обращается к адресу на странце ввода-вывода - происходит вызов метода Make_IO(), с предварительным занесением аргументов в переменные wLast_IO_Addr и Last_IO_Mode (выглядит довольно глупо - переделаю на обычную передачу аргументов через стек).

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

Эмулировать довольно сложное поведение таймера только в моменты обращения к регистрам оказалось довольно непросто, но зато накладные расходы на эмуляцию равны нулю.

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

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