Векторы прерывания в формате программирование на ассемблере 6502, можно представить как всем хорошо известный патерн event-observer в высокоуровневых языках программирования. Конечно же можно реализовать данный патерн и на ассемблере но я его привел для большего понимания работы вектора прерывания.
Если помните раннее я уже упоминал про память в 6 байт (3 слова, по 16 бит), которые выделены для секции VECTOR данный сегмент содержит всего 3 адреса процедур
.segment "VECTORS"
.addr nmi_isr, reset, irq_isr
Когда ассемблер соберет код в бинарный файл заместо меток будет установлен соответствующий адрес данных процедур, которые обработают определенное прерывания. Всего их 3 в Famicom
NMI - Not Masked Interrupt - событие срабатывающие при генерации каждого кадра то есть 60 раз в секунду. Тут будет отрабатывать код для отрисовки кадра игры.
RESET - Событие перезагрузки - тут мы будем инициализировать программу, исходя из названия данное прерывание срабатывает при перезагрузки/загрузки процессора
IRQ - Interrupt request - прерывание которое может быть запущенно по запросу какого либо оборудования
Я в своей игре пока не использую IRQ по этому процедура выглядит довольно просто
.proc irq_isr
RTI
.endproc
RTI - return from interrupt возврат в прерывание, это довольно важное замечание забегая вперед скажу что в остальных случаях объявление .proc будет выполнять инструкцию RTS - Return from subroutine что буквально вернуть в подпрограмму.
Мы плавно подошли к понятию .proc (procedure), сам по себе .proc определяет некую область видимость где могут быть использованы и определенны одноименные метки, но в контексте процедуры возможен только переход на метки данной процедуры. К примеру:
.proc foo
LDA var
CMP #$01
BEQ return
LDA var2
STA var3
return:
RTS
.endproc
.proc bar
LDA var
CMP #$02
BEQ return
LDA var2
STA var3
return:
RTS
.endproc
.proc main
JSR foo
JSR bar
.endproc
Но не только саброуты (процедуры) они еще позволяют структурировать наш код в более понятные секции как функции/процедуры/методы в других языках. И еще один огромный плюс, конструкции ветвления JMP, BEQ, BNE, BCC, BCS могут перейти только на интервал -128-+128 строк, то есть в пределах адресации лимитированные 1 байтом (максимальное число 256) а вот процедуры по факту не имеют такого ограничения.
Выше в подпрограмме я привел пример как вызвать процедуру, с помощью инструкции JSR - jump to SubRoutine с помощью нее мы переходим в подпрограмму, выполняем какие то действия и в окончание в подпрограмме которую мы вызвали происходит вызов инструкции RTS, которая возвращает нас обратно в процедуру которая до этого вызвала эту подпрограмму, и родительская подпрограмма продолжает выполняться дальше.
Ассемблер ca65 и многие другие ассемблеры позволяют выполнять инструкции .include "filename.asm" которая включает файлы в главный файл игры. Интересно что в каждом файле мы можем определить нужные нам секции, которые определены в ini файлы конфигурации линкера который определяет в какой диапазон памяти положить ту или иную секцию кода, но это уже разговор для следующих статей.
Полезные ссылки:
https://youtu.be/KGXwoqYJ9Dk - видео/аудио формат статьи
https://github.com/lnroma/newGameNes - репозиторий с кодом игры
https://habr.com/ru/post/715994/ - считывание контроллера
https://habr.com/ru/post/551488/ - вводная статья
https://habr.com/ru/post/715994/ - прокрутка фона
https://habr.com/ru/post/553848/ - создание графики nes/dendy
Комментарии (17)
staticmain
00.00.0000 00:00+1Было бы лучше, если бы вы сразу приучали читателя использовать pha/pla. Но с тем подходом, который вы взяли просто так объяснить читателю их суть не получится - сначала надо будет копнуть пласт знаний о регистрах, памяти, стеке.
himysay Автор
00.00.0000 00:00Спасибо! На самом деле я пока сам больше использую переменные в zeropage, но так как zeropage не резиновая надо будет в некоторых участках использовать pha/pla ну или "TXA/PHA/TYA/PHA ..Какая то логика.. PLA/TAY/PLA/TAX ... продолжение логики". Так же сегодня столкнулся с тем что секция RODATA переполнена а значит пришло время использовать мапер пока выбрал MMC3. При этом я так понял он может вызывать прерывание irq по счетчику строк, во многих игр статус бар реализован как часть фона который не прокручивается. Данные карт и коллизий можно положит в переключаемую память PRG.
staticmain
00.00.0000 00:00+1конструкции ветвления JMP, BEQ, BNE, BCC, BCS могут перейти только на интервал -128-+128 строк, то есть в пределах адресации лимитированные 1 байтом
А вот тут было бы неплохо рассказать, как это ограничение обойти переворотом условия. Поскольку, как бы вы не старались структурировать код, иногда бывает, что нужная точка дальше 128 инструкций.
Ну и да, про JMP неверно. JMP использует двухбайтовый аргумент как прямой $ba00 или косвенный ($ba00) адрес + рассказать про баг чтения старшего байта на границе страниц.
himysay Автор
00.00.0000 00:00+1Я просто разбил код на подпрограммы которые не превышают данный лимит. У меня был к стати момент когда я этот лимит обходил конструкцией с несколькими JMP )) сначала JMP перескакивает метку в середине, и если надо в начало вернуться то делаем Branch на метку где был jmp в середине на начало ))) На самом деле не знаю про такой баг, знаю что сравнение с 0 и при декременте может не особо очевидно работать. Возможно это один и тот же баг.
staticmain
00.00.0000 00:00+2Баг в том, что если младший баг в косвенной адресации JMP равен FF, то адресация сломается, потому что наговнякали в камне логику, которая не инкрементит старший байт. Возможно умышленно, чтобы не делать проверки каждый раз.
JMP ($22FF)
Возьмет первый байт из $22FF, а второй из $2200.
himysay Автор
00.00.0000 00:00Интересно, ни разу не встречал такого поведения, но зато постоянно с другим поведением кажущимся неправильным но когда углубляешься в флаги становится более понятно что к чему. В переключение nametable для коллизий сейчас проблема. Для первого к примеру можно сделать ADC $20 и это сработает с 20 - 23 младший байт. Но при определении что герой находиться на 2-й таблице вроде сделать бы ADC $24 и вот не могу никак найти как это сделать)) просто есть идея сэкономить и брать просто номер спрайта с таблице имён и по нему определять коллизии на фоновые платформы. Для первой таблице работает, для второй никак, там ещё особенности в адресации PPU могут вызывать как прокрутку так и может переключить nametable. Вот и не могу это победить никак)
CoolCmd
00.00.0000 00:00+1нужно было вам статью писать (я серьезно). вы и русский язык в школе не прогуливали, в отличие от некоторых.
staticmain
00.00.0000 00:00Да я бы с удовольствием, но на мне сейчас висит очень много работы - это раз, во вторых на работе уже давно в todo лекция по потокобезопасности (объема как моя статья про fork)
Kudesnick33
00.00.0000 00:00Полагаю, что это не баг, а умышленно реализованная особенность. Думаю, это сделано с целью увеличения быстродействия. Во всяком случае, в CMOS варианте чипа эту особенность убрали, что привело к увеличению времени выполнения команды на 1 такт. Изменение поведения некоторых команд в 65с02 (CMOS) по сравнению с 6502 (NMOS) можно рассматривать как работу над ошибками. Но нужно учесть, что 6502 работает на частотах в единицы МГц, а 65с02 хорошо себя чувствует на десятках МГц. Это позволило добавить сотню-другую транзисторов и без особого вреда для быстродействия получить более интуитивное поведение, в то время как разработчикам оригинального 6502 приходилось выгрызать каждый такт. Да и транзисторы стоили дороже.
staticmain
00.00.0000 00:00Возможно умышленно, чтобы не делать проверки каждый раз.
Ну т.е. моё предположение было верным.
shiru8bit
00.00.0000 00:00Вообще косвенный JMP очень редко встречается в программах, далеко не в каждой найдётся, и даже если встречается, то один-два раза на многие килобайты кода - вряд ли один такт на такой редкой операции играл роль для быстродействия. Так что могли и просмотреть ошибку, или заметить, но (резонно) решить, что это не важно.
staticmain
00.00.0000 00:00Я полагаю речь о том, что шанс встретить ff максимум 1/256 (на самом деле меньше из-за того, что адресация по два байта и нужно умудриться сдвинуть адрес на 1, например объявив .res 1 где-то перед), а скорее всего на переполнение нужно было проверять при вообще любом косвенном джампе, 255 > 1, и они решили оставить такую "микрооптимизацию". Я бы назвал это преждевременной оптимизацией.
Ну или второй вариант - они знать не знали про этот баг и узнали о нем только уже от пользователей, когда уже выпущено тысячи чипов и менять что-то уже поздно - можно сломать совместимость. Ну или это было дорого - предыдущая схематика еще не окупилась или заказано было много партий.
staticmain
00.00.0000 00:00Ну и да, я бы не сказал, что это редкая операция. Там где нужен switch-case jmp (x) позволит выйти на О(1), в то время как бинарный поиск будет О(log n), а перебор О(n).
shiru8bit
00.00.0000 00:00Полезность и редкость - это разные вещи. Я говорю, исходя из личной практики написания эмуляторов и больших количеств кода конкретно для 6502.
VelocidadAbsurda
Пара мелочей:
"T" в RTI/RTS - не из "To", а из "ReTurn". Возвращают они не "в", а "из" того, что в третьей букве - "ReTurn from Interrupt"/"ReTurn from Subroutine".
секция векторов - не 3 байта, а 3 слова, указатели - 16-битные
himysay Автор
Благодарю, поправил!