Внезапно я осознал, что держать все в голове невозможно. Поэтому буду делать небольшие заметки об устройстве NES, которые я смог выяснить, изучая различные версии документаций и играясь с Visual 2A03. Некоторая информация, которую я здесь привожу, нигде не упоминается, но учитывается в современных эмуляторах.
Здесь я постарался описать работу счетчика кадров, который отвечает за генерацию сигналов для остальных частей APU в определенное время на кадре.
Работа счетчика и такты APU
Как и CPU, APU обрабатывает такты в две фазы, однако это происходит только по окончанию фазы \phi_2 у CPU. Это иллюстрирует диаграмма:
1 2 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1] APU ....][ phi1 ][ phi2 ][.... |
По началу \phi_1 обновляется счетчик. Он представляет из себя 15-разрядный сдвиговый регистр с обратной связью (странноватый подход). Биты 13 и 14 складываются и кладутся в начало при каждом сдвиге
1 |
FrameLFSR = ((FrameLFSR << 1) | ((FrameLFSR >> 14) ^ ((FrameLFSR >> 13) & 1)) & 0x7fff; |
Начальное значение счетчика устанавливается в 0x7fff
.
Текущая фаза определяется по значению счетчика: 0x1061
для первой фазы, 0x3603
для второй, 0x2cd3
для третьей, 0x0a1f
для четвертой (флаг устанавливается только в 4-фазном режиме), 0x7165
для пятой фазы. Установленный флаг определяет, какие сигналы будут генерироваться по \phi_2. Можно составить следующую таблицу:
Значение счетчика | Номер такта | Номер фазы | Генерируемые сигналы |
---|---|---|---|
0x1061 |
3728 | A | QuarterFrame |
0x3603 |
7456 | B | QuarterFrame, HalfFrame |
0x2cd3 |
11185 | C | QuarterFrame |
0x0a1f |
14914 | D | QuarterFrame, HalfFrame |
0x7185 |
18640 | E | QuarterFrame, HalfFrame |
Флаг фазы D напрямую связан с флагом FrameIRQ: и по \phi_1, и по \phi_2 будет генерироваться прерывание, пока флаг установлен (при условии, что прерывания не запрещены). Также флаги фаз D и E устанавливают флаг сброса счетчика.
По сбросу счетчика также генерируется прерывание (если установлен 4-фазный режим и прерывания не запрещены).
В итоге диаграмма выглядит следующим образом:
1 2 3 4 5 6 7 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi1 ][ phi2 ][ phi1 ][ FrameLFSR 450f][ 0a1f ][ 7fff PhaseD 0 ][ 1 ][ 0 ~HalfFrame 1 ][ 0 ][ 1 ~QuarterFrame 1 ][ 0 ][ 1 FrameIRQ ][ * ][ * ][ * ][ |
Импульсы HalfFrame и QuarterFrame длятся только половину такта, поскольку все остальные части APU работают с той же частотой, что и CPU, в отличие от счетчика кадров.
Регистры
Режимы счетчика определяются регистром 0x4017
:
1 2 3 4 5 |
0x4017 MI-- ---- || |+-------- Флаг запрета прерывания (I = 0 -> прерывания работают в обычном режиме, I = 1 -> флаг не будет устанавливаться в 1) +--------- Определяет режим (M = 0 -> 4-фазный режим, M = 1 -> 5-фазный режим) |
Флаг запрета прерываний нельзя путать с флагом прерывания. Установка флага запрета прерывания приведет к немедленному сбросу флага прерывания. Чтение из 0x4015
также приводит к сбросу флага прерывания, но не затрагивает флаг запрета прерывания.
Помимо всего прочего, запись в 0x4017
приводит к генерации QuarterFrame и HalfFrame в 5-фазном режиме. В 4-фазном этого не происходит.
После записи в 0x4017
по следующему \phi_1 будет установлен флаг сброса счетчика, что повлечет за собой сброс регистра и вызов прерывания (если оно не запрещено) по следующему далее \phi_1.
Диаграмма выглядит следующим образом:
1 2 3 4 5 6 7 8 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 0936 ][ 126c ][ 7fff ~HalfFrame 1 ][ 0 ][ 1 ~QuarterFrame 1 ][ 0 ][ 1 FrameIRQ ][ * ][ |
Однако может произойти следующее:
1 2 3 4 5 6 7 8 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi1 ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 0936][ 126c ][ 24d8 ][ 7ffff ~HalfFrame 1 ][ 0 ][ 1 ~QuarterFrame 1 ][ 0 ][ 1 FrameIRQ ][ * ][ |
Из-за того, что запись пришлась на середину такта APU произошла задержка при сбросе счетчика. Такие же задержки происходят с генерацией сигналов четверти и половины кадра.
Люди, изучавшие документацию по 2A03/2A07, сразу же заметят ошибку: какое еще прерывание по записи в 0x4017
?! И действительно, обычно при записи в этот регистр никакого прерывания не вызывается. Дело в том, что по сбросу счетчика FrameIRQ устанавливается в 1, только если установлен внутренний флаг. Этот флаг устанавливается по фазе D и сбрасывается при сбросе счетчика. Однако если при каких-то условиях он не будет сброшен (например по RESET), то мы увидим прерывание по 0x4017
. Но как это протестировать — пока не понятно.
Двойная запись
Было бы очень интересно посмотреть, что произойдет, если записать в 0x4017
два такта подряд, либо через один такт. Второй вариант реализовать практически невозможно, но первый — легко. Для этого достаточно написать:
1 |
INC $4017 |
Что произойдет? Вот как будет это обрабатывать наш CPU:
1 2 3 4 5 6 |
1. Прочитать опкод, PC++ 2. Прочитать нижние 8 бит адреса, PC++ 3. Прочитать верхние 8 бит адреса, PC++ 4. Прочитать байт по указанному адресу 5. Записать его же по этому же адресу 6. Записать значение + 1 туда же |
В итоге произойдут две последовательных записи по адресу 0x4017
. Что мы будем туда записывать? При чтении $4017 возвращает статус кнопок второго джойстика, либо открытую шину (0x40
). В любом случае биты M и I будут сброшены.
Получаем диаграмму:
1 2 3 4 5 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi1 ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 0008][ 0010 ][ 0020 ][ 7ffff |
Либо, если добавить сдвиг:
1 2 3 4 5 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ phi2 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 0040 ][ 0080 ][ 7fff ][7ffe |
Результат вполне логичный и подтверждает предположения, изложенные выше: по началу \phi_1 устанавливается флаг для сброса.
Эмуляторы
Насколько же хорошо эмуляторы учитывают все описанное выше? Большинство разработчиков стараются сделать их программы как можно более точными, поскольку многие игры NES очень зависят от этого и полагаются на всякие аппаратные гличи. Я написал простой тест, проверяющий генерацию QuarterFrame и HalfFrame по записи в 0x4017
значения 0x80
. Все эмуляторы легко прошли тест.
Но что, если запись происходит именно тогда, когда сам счетчик кадров должен генерировать эти сигналы? Сигнал должен генерироваться лишь один раз. И тут все эмуляторы посыпались. И сложно их винить — в документации про это не сказано абсолютно ничего.
Диаграмма для теста 1:
1 2 3 4 5 6 7 8 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi1 ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 2287][ 450f ][ 0a1f ][ 7fff ~HalfFrame 1 ][ 0 ][ 1 ~QuarterFrame 1 ][ 0 ][ 1 Pulse0Len 01 ][ 00 |
Диаграмма для теста 2:
1 2 3 4 5 6 7 8 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 450f ][ 0a1f ][ 7fff ~HalfFrame 1 ][ 0 ][ 1 ~QuarterFrame 1 ][ 0 ][ 1 Pulse0Len 01 ][ 00 |
Диаграмма для теста 3:
1 2 3 4 5 6 7 8 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi1 ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 450f][ 0a1f ][ 7fff ][ 7fff ~HalfFrame 1 ][ 0 ][ 1 ][ 0 ][ 1 ~QuarterFrame 1 ][ 0 ][ 1 ][ 0 ][ 1 Pulse0Len 01 ][ 00 ][ ff |
Диаграмма для теста 4:
1 2 3 4 5 6 7 8 |
CPU [phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1][phi2][phi1] APU ][ phi2 ][ phi1 ][ phi2 ][ phi1 ][ R/~W 1 ][ 0 ][ 1 phi2 [ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ][ 1 ][ 0 ] FrameLFSR 0a1f ][ 7fff ][ 7fff ~HalfFrame 1 ][ 0 ][ 1 ][ 0 ][ 1 ~QuarterFrame 1 ][ 0 ][ 1 ][ 0 ][ 1 Pulse0Len 01 ][ 00 ][ ff |
Тесты 5-8 повторяют 1-4 в 5-фазном режиме.
Таблица с результатами:
Эмулятор | test_1 | test_2 | test_3 | test_4 | test_5 | test_6 | test_7 | test_8 |
---|---|---|---|---|---|---|---|---|
puNES 0.82 03.04.2014 | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED |
Nestopia — Undead Edition 1.45 30.07.2013 | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED |
vpnes 0.4 Build 117 (r242_p1) 09.04.2013 | TEST PASSED | TEST PASSED | Программа уходит в бесконечный цикл 0_o | Программа уходит в бесконечный цикл 0_o | TEST FAILED | TEST FAILED | TEST PASSED | TEST PASSED |
FCEUX 2.2.2 24.09.2013 | TEST PASSED | TEST FAILED | TEST PASSED | TEST PASSED | TEST PASSED | TEST PASSED | TEST FAILED | TEST FAILED |
Заключение
Все особенности учесть невероятно сложно. Сейчас я понятия не имею, как вписать все сказанное выше в мой эмулятор. И ведь это лишь небольшой кусочек всего того, что необходимо правильно обрабатывать. Позднее я копну еще глубже и во всех направлениях, а результаты напишу в этом блоге.