Векторы прерывания в формате программирование на ассемблере 6502, можно представить как всем хорошо известный патерн event-observer в высокоуровневых языках программирования. Конечно же можно реализовать данный патерн и на ассемблере но я его привел для большего понимания работы вектора прерывания.

Если помните раннее я уже упоминал про память в 6 байт (3 слова, по 16 бит), которые выделены для секции VECTOR данный сегмент содержит всего 3 адреса процедур

.segment "VECTORS"
	.addr nmi_isr, reset, irq_isr

Когда ассемблер соберет код в бинарный файл заместо меток будет установлен соответствующий адрес данных процедур, которые обработают определенное прерывания. Всего их 3 в Famicom

  1. NMI - Not Masked Interrupt - событие срабатывающие при генерации каждого кадра то есть 60 раз в секунду. Тут будет отрабатывать код для отрисовки кадра игры.

  2. RESET - Событие перезагрузки - тут мы будем инициализировать программу, исходя из названия данное прерывание срабатывает при перезагрузки/загрузки процессора

  3.  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 файлы конфигурации линкера который определяет в какой диапазон памяти положить ту или иную секцию кода, но это уже разговор для следующих статей.

Полезные ссылки:

Комментарии (17)


  1. VelocidadAbsurda
    00.00.0000 00:00
    +2

    Пара мелочей:

    • "T" в RTI/RTS - не из "To", а из "ReTurn". Возвращают они не "в", а "из" того, что в третьей букве - "ReTurn from Interrupt"/"ReTurn from Subroutine".

    • секция векторов - не 3 байта, а 3 слова, указатели - 16-битные


    1. himysay Автор
      00.00.0000 00:00

      Благодарю, поправил!


  1. staticmain
    00.00.0000 00:00
    +1

    Было бы лучше, если бы вы сразу приучали читателя использовать pha/pla. Но с тем подходом, который вы взяли просто так объяснить читателю их суть не получится - сначала надо будет копнуть пласт знаний о регистрах, памяти, стеке.


    1. himysay Автор
      00.00.0000 00:00

      Спасибо! На самом деле я пока сам больше использую переменные в zeropage, но так как zeropage не резиновая надо будет в некоторых участках использовать pha/pla ну или "TXA/PHA/TYA/PHA ..Какая то логика.. PLA/TAY/PLA/TAX ... продолжение логики". Так же сегодня столкнулся с тем что секция RODATA переполнена а значит пришло время использовать мапер пока выбрал MMC3. При этом я так понял он может вызывать прерывание irq по счетчику строк, во многих игр статус бар реализован как часть фона который не прокручивается. Данные карт и коллизий можно положит в переключаемую память PRG.


  1. staticmain
    00.00.0000 00:00
    +1

    конструкции ветвления JMP, BEQ, BNE, BCC, BCS могут перейти только на интервал -128-+128 строк, то есть в пределах адресации лимитированные 1 байтом

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

    Ну и да, про JMP неверно. JMP использует двухбайтовый аргумент как прямой $ba00 или косвенный ($ba00) адрес + рассказать про баг чтения старшего байта на границе страниц.


    1. himysay Автор
      00.00.0000 00:00
      +1

      Я просто разбил код на подпрограммы которые не превышают данный лимит. У меня был к стати момент когда я этот лимит обходил конструкцией с несколькими JMP )) сначала JMP перескакивает метку в середине, и если надо в начало вернуться то делаем Branch на метку где был jmp в середине на начало ))) На самом деле не знаю про такой баг, знаю что сравнение с 0 и при декременте может не особо очевидно работать. Возможно это один и тот же баг.


      1. staticmain
        00.00.0000 00:00
        +2

        Баг в том, что если младший баг в косвенной адресации JMP равен FF, то адресация сломается, потому что наговнякали в камне логику, которая не инкрементит старший байт. Возможно умышленно, чтобы не делать проверки каждый раз.

        JMP ($22FF)

        Возьмет первый байт из $22FF, а второй из $2200.


        1. himysay Автор
          00.00.0000 00:00

          Интересно, ни разу не встречал такого поведения, но зато постоянно с другим поведением кажущимся неправильным но когда углубляешься в флаги становится более понятно что к чему. В переключение nametable для коллизий сейчас проблема. Для первого к примеру можно сделать ADC $20 и это сработает с 20 - 23 младший байт. Но при определении что герой находиться на 2-й таблице вроде сделать бы ADC $24 и вот не могу никак найти как это сделать)) просто есть идея сэкономить и брать просто номер спрайта с таблице имён и по нему определять коллизии на фоновые платформы. Для первой таблице работает, для второй никак, там ещё особенности в адресации PPU могут вызывать как прокрутку так и может переключить nametable. Вот и не могу это победить никак)


        1. CoolCmd
          00.00.0000 00:00
          +1

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


          1. staticmain
            00.00.0000 00:00

            Да я бы с удовольствием, но на мне сейчас висит очень много работы - это раз, во вторых на работе уже давно в todo лекция по потокобезопасности (объема как моя статья про fork)


    1. Kudesnick33
      00.00.0000 00:00

      Полагаю, что это не баг, а умышленно реализованная особенность. Думаю, это сделано с целью увеличения быстродействия. Во всяком случае, в CMOS варианте чипа эту особенность убрали, что привело к увеличению времени выполнения команды на 1 такт. Изменение поведения некоторых команд в 65с02 (CMOS) по сравнению с 6502 (NMOS) можно рассматривать как работу над ошибками. Но нужно учесть, что 6502 работает на частотах в единицы МГц, а 65с02 хорошо себя чувствует на десятках МГц. Это позволило добавить сотню-другую транзисторов и без особого вреда для быстродействия получить более интуитивное поведение, в то время как разработчикам оригинального 6502 приходилось выгрызать каждый такт. Да и транзисторы стоили дороже.


      1. staticmain
        00.00.0000 00:00

        Возможно умышленно, чтобы не делать проверки каждый раз.

        Ну т.е. моё предположение было верным.


        1. Kudesnick33
          00.00.0000 00:00

          Да, весьма похоже на то.


      1. shiru8bit
        00.00.0000 00:00

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


        1. staticmain
          00.00.0000 00:00

          Я полагаю речь о том, что шанс встретить ff максимум 1/256 (на самом деле меньше из-за того, что адресация по два байта и нужно умудриться сдвинуть адрес на 1, например объявив .res 1 где-то перед), а скорее всего на переполнение нужно было проверять при вообще любом косвенном джампе, 255 > 1, и они решили оставить такую "микрооптимизацию". Я бы назвал это преждевременной оптимизацией.

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


        1. staticmain
          00.00.0000 00:00

          Ну и да, я бы не сказал, что это редкая операция. Там где нужен switch-case jmp (x) позволит выйти на О(1), в то время как бинарный поиск будет О(log n), а перебор О(n).


          1. shiru8bit
            00.00.0000 00:00

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