Новые инструкции ZX Spectrum Next
ZX Spectrum Next реализован на FPGA включая процессор, поэтому в нём Z80 не только смогли повторить, но и улучшить внедрив несколько новых инструкций.
Полностью весь набор инструкций можно посмотреть тут: wiki.specnext.dev/Extended_Z80_instruction_set
Новые там отмечены литерой E в колонке «Status».
Все они реализованы на неиспользованных слотах расширенных команд Z80 с префиксом $ED.
Было любопытно взглянуть что это за инструкции и я бы даже сказал, что в них частично отразилась профессиональная боль программистов под ZX Spectrum.
Полностью весь набор инструкций можно посмотреть тут: wiki.specnext.dev/Extended_Z80_instruction_set
Новые там отмечены литерой E в колонке «Status».
Все они реализованы на неиспользованных слотах расширенных команд Z80 с префиксом $ED.
Было любопытно взглянуть что это за инструкции и я бы даже сказал, что в них частично отразилась профессиональная боль программистов под ZX Spectrum.
- PUSH imm16 — затолкать в стек непосредственное данное зашитое в инструкцию. Раньше можно было только регистровую пару.
- LDWS — интересный вариант блочной инструкции, перебрасывает байт из адреса (HL) в (DE) при этом инкрементирует регистры L и… D. Т.е. только нижний байт HL и верхний байт DE. Написано что это полезно для заполнения графикой Layer 2.
- LDIX, LDIRX, LDDX, LDDRX — аналоги старых блочных инструкций без префикса X. Все они перебрасывают байт из (HL) в (DE) и декрементируют BC. Старые инструкции без префикса X при этом — если третья буква I — то инкрементируют HL и DE, а если D — декрементируют их. Если есть префикс R, то инструкция автоматически повторяется пока BC не станет равен 0.
Новые варианты с X главным отличием имеют то, что если перебрасываемый байт равен аккумулятору A, то запись в (DE) не производится. Ну, очевидно, это у нас прозрачность при проброске битмапов можно так реализовать. Еще важное отличие, что новые команды не меняют флаги и декрементирующие с третьей литерой D увеличивают DE, а не уменьшают. - LDPIRX — еще одна новая блочная инструкция больше всего по поведению похожая на LDIRX, но не изменяет значения HL, а адрес откуда байт читается берет не просто HL, а подменяет ему нижние 3 бита нижними тремя битами DE. Т.е. HL указывает на выровненный по 8 байтам 8-байтный блок-паттерн, а DE идёт по памяти линейно и переливает этот паттерн циклически в неё. Как и написано — полезно для заливок трафаретом.
- ADD HL/DE/BC, A — интереснейшая вещь — сложение 16-битных регистровых пар с 8-битным аккумулятором (беззнаково). Очень на самом деле полезно, т.к. в Z80 приёмником 16-битных сложений мог быть только HL и IX/IY в расширенных командах, а увеличить регистровую пару больше чем на один инкремент хочется нередко без длинных пробросок.
- ADD HL/DE/BC, imm16 — опять таки сложение 16-битных регистровых пар с 16-битной константой из инструкции — по вышеописанным же причинам очень полезная штука.
- MIRROR A — переставляет биты аккумулятора полностью задом-наперёд (0-7 в 7-0).
- TEST imm8 — «тестирует» аккумулятор с непосредственным данным — выполняет AND imm16 изменяя флаги, но не меняя аккумулятор.
- BSLA/BSRA/BSRL/BSRF/BRLC DE,B — сдвиги и прокрутки 16-битной регистровой пары DE на B бит. Тоже очень полезные инструкции, т.к. все сдвиги и прокрутки в Z80 сдвигали и прокручивали только на 1 бит за инструкцию, здесь же можно сразу на любое осмысленное и программируемое в рантайме число бит. Забавно что прокрутка (rotate) выражена только командой «прокрутка влево»: BRLC, а чтобы вращать вправо надо выполнить её же с B=16-параметр. Еще необычным тут показался сдвиг вправо BSRF — он в отличие от сдвига со знаком всегда вдвигает в верхние разряды 1 (т.е. принудительно делает число отрицательным даже если оно было положительным). Не припомню такого где-то еще.
- SETAE — очищает аккумулятор A и выставляет в нём единичный бит с номером 7-E (на деле 7-(E&7), т.к. в E учитываются только нижние 3 бита). Написано, что это полезно как маска для пиксельных режимов ULA когда в E находится координата X.
- SWAPNIB — обменивает местами нижнюю и верхнюю квадры бит в аккумуляторе.
- JP (C) — выполняет чтение из порта ввода с номером в регистре C и обновляет нижние 14 бит счётчика инструкций PC следующим образом: PC[13:0] = (IN (C) << 6). Вот тут я растерялся зачем оно может быть полезно и пояснение «can be used to execute code block read from a disk stream» ничем не помогло.
- OUTINB — действует как OUTI, но НЕ декрементирует B, т.е. out(BC,(HL)); HL++
- NEXTREG nn1, nn2 — часть новых портов ввода-вывода ZX Spectrum Next действует опосредованно через порт $243B. Сперва в него пишется номер суб-порта с которым мы работаем, а потом записывается данное которое мы в него пишем. Эта инструкция делает запись в этот порт непосредственных данных минуя эту сложную развязку. А по смыслу (но не размеру и скорости) это эквивалентно out($243B,nn1); out($253B,nn2).
- NEXTREG nn, A — то же самое что выше в варианте out($243B,nn); out($253B,a).
- PIXELDN — берёт адрес в HL как адрес в видеопамяти и обновляет его так чтобы он указывал на одну строку пикселей ниже. Тоже полезная штука, т.к. нелинейность видеопамяти спектрума эту задачу делает не совсем тривиальной (см. тут или более академично тут).
- PIXELAD — помещает в HL адрес байта в видеопамяти ULA в котором содержится точка с координатами (E,D). Ох! Это штука конечно берёт приз моих зрительских симпатий. xD
33 комментария
Чем больше размышляю над этой идеей тем больше нравится она мне. «Выпрямить» адресацию реально же можно просто анализом и перестановкой сигналов на шине адреса. Но с другой стороны у PIXELAD еще выполняются сдвиги, т.е. вычисляется уже готовый адрес по осмысленным готовым координатам в DE.
Я конечно согласен, что выглядит это FPGA-роскошество как изврат над самой идеей general processor, а сам слой ULA при наличии нормально скроллируемых тайлового слоя и 8bpp вообще непонятно зачем нужен кроме совместимости, но я всё-таки даже восхищаюсь этим непотребством, уже больно оно так сказать по мозолям топчется. :)
LD_ACC32_DEHL, EXXACC32, LD_DEHL_ACC32, INC_DEHL, DEC_DEHL, ADD_DEHL_A, SUB_DEHL_A, ADD_DEHL_BC, SUB_DEHL_BC, MIRROR_DE, PIXELTOATTR, ATTRTOPIXEL и т.д.
За статью — спасибо. Полезно.
Тут явно надо родить анекдот класса «стадии смирения»…
Наподобие такого:
Есть три стадии развития программиста на спектруме:
1. ты не понимаешь как раскладку видеопамяти ULA
2. ты понимаешь раскладку видеопамяти ULA
3. ты не понимаешь раскладки видеопамяти не как в ULA
Бегло взглянул на zxnDMA сейчас — оно как я понял является даже усечением Z80 DMA и насколько я понял из упрощений только то что раньше количество итераций вбивалось как X+1 (и в режиме совместимости это сохранено), а сейчас можно как X передавать в новом режиме. А в остальном вроде всё такое же, но там уже куча битовых флагов и адресов и перевод уже дело нудненькое.
Так то я все демки для z80DMA написанные пересмотрел, работают в большинстве своем наверное правильно, как авторы задумывали. Но большинство работает и в режиме zxnDMA, иногда даже лучше, чем в нативном.
В эмулях работает DMA пока плоховато :( Хотелось бы поточнее эмууляцию.
Ну и щас скажу ужасное (instrospec, закрой глаза): в последнее время подумываю, как расширить зетник для работы с регистровым файлом по типу 6502.
В связи с этим вопрос — какие то пути преодоления проблемы просматриваются? :)
То есть я о чём. Просто набор команд недостаточен, чтобы сказать, это хорошо и плохо. Нужно знать растактовки команд и смотреть как будут выглядеть решения стандартных задач. И только там станет понятно, где у нас улучшено, а где просто изменено.
Я крайне с этим согласен!
И скорее всего сейчас начну говорить жуткую банальщину, но как раз эта вот растактовка меня давно уже не удовлетворяет в том же Z80.
Взять ту же самую LDIR. Блочная инструкция, двухбайтовая, пересылающая сразу блоки байт, но как?
А так что на каждый пересылаемый полезный по «ПН (полезной нагрузке)» байт сопровождается двумя считываемыми байтами инструкции и скорее всего еще больше тактов по общему смыслу.
Не сомневаюсь что это всё поднималось ранее тысячи раз.
Потому что ну реально концепция general processor на каждый байт обрабатываемых полезно данных нагрузки реально чаще всего даёт +10 байт инструкций, опкодов и адресов. Который нужно считывать.
В итоге LDIR работает полукалечно: просто выполняет LDI и делает JR -2, чтобы снова считывать 2 байта инструкции чтобы переслать 1 байт полезных данных.
И это дико бесит перфекциониста типа меня и рождает DMA-контроллеры. Просто потому что general processors сосут. Откровенно.
Понятно что там еще с прерываниями надо разобраться и тому подобное. Не всегда уместно блочить шины DMA-контроллером надолго. Может и звук защёлкать и так далее.
Но я хотел сказать лишь то что наверняка уже тысячи раз говорилось: хотелось бы чтобы в general processor были по настоящему эффективные инструкции могущие не пересчитывать байты инструкции x2-3 байта из памяти каждый раз пересылая лишь 1 байт полезных байт нагрузки.
А LDIR настолько такой не является, что её реально попают пушами.
Аааа…
например, ex sp,hl вместо (или вместе с) ld sp,hl
Ваш перелопаченный Z80 потребует пересмотра вообще всех таких привычных соображений, во всяком случае с точки зрения хорошо оптимизированного кода. Поэтому невозможно вообще что-либо сказать, ни про твои доработки с сегментом регистров а-ля геймбой, ни про доработки некста. Без точной информации о скорости команд, разговор получится ни о чём.
Либо префиксировать каждую команду, либо включать отдельный блок расширенной ISA. Префикс можно выбрать либо ED, либо из команд типа LD R,R — где реги одинаковые. Второй метод позволяет сильно экономить память — по объему и машциклам, а также дает неограниченную свободу воли в выборе опкодов. Для крайней совместимости предусмотреть защелку, блокирующую включение доп. опкодов.
По набору команд.
Основная цель — С компиляторы. Полагаю, 256 байт для регфайла достаточно. Должна быть общая арифметика, хотя бы 2-операндная, между любыми ячейками регистрового файла. Объединение соседних ячей в пары/четверки. Адресация ячеек как непосредственно, так и регистрами. Постинкремент и предекремент адресующих регов.
Над конкретным набором команд надо садиться и думать.
Ну и конечно, сначала опробовать все это в эмуляторе, что несложно.
Пытаясь максимально сохранять синтаксис Си компилятор просто возвращается к корням — ЭВМ из 60-х и не поддерживает рекурсию. И всё начинает работать гладко и шелковисто и компилироваться в бодрый и довольно рабочий код.
Тот же CC65 на следующем коде (очень для него неудачном):
Рожает следующий тихий ужас:
А вот KickC в своём асмосинтаксисе (как можно догадаться KickAssembler) делает почти оптимально:
Весьма впечатляющий результат, хотя видно что еще есть куда работать.
Отсутствие локальных переменных еще позволяет производить оптимизацию с размещением переменных и параметров в одном и том же месте если они не пересекаются в callstack. Тоже недоступная для Си вещь даже если шлёпать static.
Вот такой эффект от того что как в древнем Fortran когда не было там еще ключевого слова RESURSIVE.
тут можно поспорить. call nnnn вполне себе «затолкать в стек непосредственное данное зашитое в инструкцию»