Ещё пример использования макросов.
Как обычно пишутся процедуры табличных обработчиков ? Например, на нажатие разных клавиш нужно вызвать разные процедуры.
Суперэконом вариант:
T_LO: DFB >route1-1,>route2-1
T_HI: DFB <route1-1,<route2-1
T_KEY: DFB $8D,$9B
main:
jsr get_key
ldy #1
loop:
cmp t_key,y
beq call
dey
bpl loop
bmi main
call:
lda T_HI,y ; возможно в обратном порядке HI/LO, не помню точно
pha
lda T_LO,y
pha
rts
В цикле loop ищем совпадение полученного в get_key кода с кодами обрабатываемых клавиш, если не нашли возвращаемся на начало, иначе переходим на call и там, выбрав значение адреса обработчика из таблиц T_LO, T_HI, засылаем его в стек и вызываем переход по заданному адресу через команду rts. Таким образом на клавишу ВВОД ($8D) будет вызван обработчик route1, а на РЕД ($9B) - route2.
Вычитание единицы из адреса нужно потому, что rts переходит на адрес + 1 от того, что хранится на стеке.
Чем плох этот код ? Только синтаксически !
1) нужно вычитать 1 (лишние символы каждый раз писать).
2) адрес обработчика указывается дважды (надоедает).
3) если список длинный (а зачастую там клавиш 10-20 может быть), попробуйте удалить элемент из его середины. Найдите нужный код и соответствующий ему обработчик :)
4) количество элементов в таблице задано явно в теле цикла. Добавление/удаление элемента таблицы этого требует правки этого значения.
5) переход на обработчики идёт по схеме jmp. Значит возврат нужно делать тоже по jmp. Удобнее сделать по jsr: как правило, возврат будет требоваться в одно и то же место, jsr позволит использовать для возврата rts - это удобно при написании обработчиков.
Я, в начале своей программистской карьеры, именно так и писал. Это даёт самый компактный код, но поддерживать его тяжело.
Попробуем исправить некоторые косяки:
vec = $E0
T_PROC: DW route1,route2
T_KEY: DFB $8D,$9B
T_KEY_F:
main:
jsr get_key
ldy #T_KEY_F-T_KEY-1
loop:
cmp t_key,y
beq find
dey
bpl loop
bmi main
find:
jsr call
jmp main
call:
tya
asl a
tay
lda T_PROC+0,y
sta vec+0
lda T_PROC+1,y
sta vec+1
jmp (vec)
Процедурка заметно подросла: появились новые команды.
Зато:
1) адрес процедуры упоминается один раз, нет вычитания 1;
2) сделали автоматический расчёт размера таблицы (T_KEY_F-T_KEY);
3) возврат из обработчиков по rts за счёт вызова внутренней процедуры call через jsr.
Так я писал уже в конце 90-х.
Но список пока ещё неудобный. Удобнее, если бы код клавиш чередовался со ссылками на соответствующие обработчики.
Усложним:
vec = $E0
t_router:
DFB $8D
DW route1
DFB $9B
DW route2
DFB $00
DW default_route
main:
jsr get_key
ldy #0
tax
loop:
lda t_router,y
beq found
txa
cmp t_router,y
beq found
iny
iny
iny
jmp loop
found:
jsr call
jmp main
call:
iny
lda t_router,y
sta vec+0
iny
lda t_router,y
sta vec+1
jmp (vec)
Теперь править таблицу гораздо удобнее. Кроме того мы отказались от статического расчёта размера таблицы и ввели специальную закрывающую строку с кодом клавиши 0. Она соответствует любой клавише, т.е. будет срабатывать в случае, если в таблице запись для данной клавиши не найдена.
Если ваша программа достаточно сложная и в ней, в разное время, используются разные таблицы (например, у вас есть процедуры: полноэкранного текстового редактора, строчного редактора и управления выпадающим меню), можно ещё дополнить логику этой процедуры. Например, спроектировать селектор в виде отдельной процедуры, причем таблицу при вызове передавать ей через адрес в стеке:
main:
jsr get_key
jsr router
DFB $8d
DW key_enter
DFB $9b
DW key_esc
DFB 0
DW key_default
jmp main
Т.е. процедура router, получив управление, извлекает из стека адрес возврата, по которому будет расположена таблица. Просмотрев таблицу и вызвав нужный обработчик, router доходит таблицу до конца и возвращает управление на следующую за таблицей команду jmp main.
Такой механизм я использовал в последней крупной агатовской разработке, компилируемой ещё пока ассемблером ДОК.
А что насчёт макросов? Переходим к cc65 и теме топика:
.rodata ; таблицы располагаем в сегменте констант
.macro key_descriptor code, proc; описываем простенький макрос, который позволит
.byte code ; создавать что-то вроде "записи" или "структуры"
.word proc
.endmacro
; объединяем таблицу в единый блок, при необходимости можно будет запросить
; его фактический размер: .sizeof(ED_KEYS_TABLE)
.scope ED_KEYS_TABLE
key_descriptor $8D, ED_K_ENTER
key_descriptor $95, ED_K_RIGHT
key_descriptor $88, ED_K_LEFT
key_descriptor $99, ED_K_UP
key_descriptor $9A, ED_K_DOWN
key_descriptor $9B, ED_K_ESC
key_descriptor $9D, ED_K_LEFT_CUTLINE ; ф7
key_descriptor $9E, ED_K_RIGHT_CUTLINE ; ф8
key_descriptor $9F, ED_K_DELLINE ; ф9
key_descriptor $91, ED_K_INS_MODE ; ф2
key_descriptor $90, ED_K_DELETE ; ф1
key_descriptor $81, ED_K_SCR_MODE ; ф0
key_descriptor 0, ED_K_SYMBOL
.endscope
Красивее получилось ? И нагляднее ! Ещё можно заменить коды клавиш на константы (key_f0, key_...), но и так хорошо.
Ассемблер считается сложным языком. Я не согласен с этим утверждением. Сложны, скорее, задачи, ради которых выбирается ассемблер: работа в условиях жесткой экономии рессурсов, суперпростые операционки, не всегда есть возможность пошаговой отладки программ. Это если проц простой и периферия тоже супербюджетная. А если проц мощный и памяти полно ? Тогда ассемблер может использоваться для управление режимами процессоров, для разработки кода инициализации системы, автозагрузчиков...
Но проблема ассемблера в другом: на нём очень легко создать совершенно не читаемый код, в котором легко потеряться. Этого можно добится на любом языке, но ассемблер - чемпион в этой дисциплине. Если начинающий берётся за изучение PHP и пишет пока ещё корявый код, он всё равно вызывает кучу готовых процедур с понятными именами. А в ассемблере можно избежать даже этого и начать сразу с тёмных материй.
2USR: не принимай на свой счёт, это я в целом.