Новые инструкции ZX Spectrum Next

ZX Spectrum Next реализован на FPGA включая процессор, поэтому в нём Z80 не только смогли повторить, но и улучшить внедрив несколько новых инструкций.
Полностью весь набор инструкций можно посмотреть тут: 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 комментария

avatar
avatar
Ахаха:D А всё началось с того, что я попросил Виктора впилить ldix и lddx как хоть какую-то альтернативу блиттеру, который они наотрез отказались делать. А дальше всё вышло из под контроля:D
avatar
Каждый раз ор и рукалицо с этих костылей. А вы думали, вот что надо сделать, чтоб исправить неудобство адресации оригинальной юлы? Ага, точно — надо «исправлять» другой и самый сложный компонент — процессор, вместо простого. С результатом всё равно неудобнее, чем у старых самопальных компов советских. Вот казалось бы, всё равно со спеком несовместимо, ну запилите вы прозрачную трансляцию адреса. Но нет, эти люди лёгких путей не ищут.
avatar
«ну запилите вы прозрачную трансляцию адреса»

Чем больше размышляю над этой идеей тем больше нравится она мне. «Выпрямить» адресацию реально же можно просто анализом и перестановкой сигналов на шине адреса. Но с другой стороны у PIXELAD еще выполняются сдвиги, т.е. вычисляется уже готовый адрес по осмысленным готовым координатам в DE.
Я конечно согласен, что выглядит это FPGA-роскошество как изврат над самой идеей general processor, а сам слой ULA при наличии нормально скроллируемых тайлового слоя и 8bpp вообще непонятно зачем нужен кроме совместимости, но я всё-таки даже восхищаюсь этим непотребством, уже больно оно так сказать по мозолям топчется. :)
avatar
В Z80N на самом деле в разное время присутствовали еще некоторые команды, о назначении (и полезности?) которых можно лишь догадываЦЦа.
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 и т.д.
За статью — спасибо. Полезно.
avatar
Было бы точнее сказать, что в этих инструкциях отражены профессиональная боль непрограммистов на спектруме.
avatar
xD
Тут явно надо родить анекдот класса «стадии смирения»…
Наподобие такого:
Есть три стадии развития программиста на спектруме:
1. ты не понимаешь как раскладку видеопамяти ULA
2. ты понимаешь раскладку видеопамяти ULA
3. ты не понимаешь раскладки видеопамяти не как в ULA
avatar
aa-dav, а планируется ли такая же обзорно-поучительная статья по преимуществам zxnDMA в отличии от Z80DMA? Но на великом и могучем, а не как у них в dev.
avatar
Нет. У меня сейчас на самом деле денди с маппером MMC3 голова забита если про хобби говорить. Про Next получилось просто что одна предыдущая статья была написана уже давно, а система команд ну очень уж немного и интересно, перевести так и просилось.
Бегло взглянул на zxnDMA сейчас — оно как я понял является даже усечением Z80 DMA и насколько я понял из упрощений только то что раньше количество итераций вбивалось как X+1 (и в режиме совместимости это сохранено), а сейчас можно как X передавать в новом режиме. А в остальном вроде всё такое же, но там уже куча битовых флагов и адресов и перевод уже дело нудненькое.
avatar
Главное отличие, что в burst mode можно теперь с прерыванием im2 работать. Я очень долго за это бился (:
avatar
Это если развернуть фразу — значит в оригинальном z80DMA — этого делать нельзя?? хмм… И в чем же проявляется отличие, если на пальцах?
Так то я все демки для z80DMA написанные пересмотрел, работают в большинстве своем наверное правильно, как авторы задумывали. Но большинство работает и в режиме zxnDMA, иногда даже лучше, чем в нативном.
В эмулях работает DMA пока плоховато :( Хотелось бы поточнее эмууляцию.
avatar
Ну насколько я понимаю, пока DMA перебрасывает данные процессор остановлен и прерывание тоже не приходит. Отличие в том, что во время переброски данных z80DMA всегда DI на Z80, а это очень неудобно. И я думаю в zxnDMA более упощенный, хотя не проверял, но вряд ли там всё возможности реализованы.
avatar
Это очень хороший анекдот. Непонимание раскладки ULA — натурально, бич начинающих кодеров. Она действительно не всегда удобна, но думаю многие со мной согласятся, что с опытом приходит понимание, что она намного эффективнее линейной раскладки.
avatar
ну, может, для демок и эффективнее НЕнамного, но мой опыт ковыряния игрушек говорит об обратном
avatar
Да, я согласен что очень многие игры на спектруме написаны людьми, на спектруме программировать не умеющими!
avatar
В подавляющем, абсолютном большинстве игрушек линейный буфер, так что и умеющие так делали. Для игродела плюсов в спековской раскладке весьма немного и они только в очень частных случаях проявляются. А вот неудобства намного чаще.
avatar
Подавляющее большинство игрушек не умеют делать vsync. Подавляющее большинство игрушек обновляют экран раз в 3 фрейма или даже медленнее (offscreen buffer — одна из важных причин этого). Подавляющее большинство игрушек shipped с артефактами изображения. Мне продолжать?
avatar
Нет, не стоит. Потому что это всё, во-1, необязательно признак неумелости, а во-2, не настолько большинство, как линейный буфер. Контрпримеры легче найти гораздо, чем примеры с буфером в юлашной раскладке.
avatar
В контроллерах, для которых разрабатывают свои ISA, специализированные инструкции для железа вполне норма. Раз некст «улучшенный», то почему не улучшить и проц? Говорить о том, что в з80 не хватает элементарных вещей, наверное излише. Но я не столько сами их инструкции не понимаю, сколько scope (забываю русский) ихнего дизайна. Что они хотят выносить на аксели, а что на проц? Выглядит как-то сумбурно.
Ну и щас скажу ужасное (instrospec, закрой глаза): в последнее время подумываю, как расширить зетник для работы с регистровым файлом по типу 6502.
  • tsl
  • +1
avatar
посмотри в сторону 6309
avatar
Я посмотрел, но там ВСЁ другое.
avatar
в последнее время подумываю, как расширить зетник для работы с регистровым файлом по типу 6502
В Sharp LR35902 в последние 256 байт памяти замапили четыре иструкции LDH (imm8), a / LDH a, (imm8) / LDH (c), a / LDH a, (c), но блин увы, там обратная совместимость серьёзно была подпорчена и потому эти инструкции заняли место других из i8080 и будучи однобайтовыми опкодом действительно были судя по всему штукой интересной. А чтобы не ломать совместимость с Z80 тут надо тоже вклиниваться в ED-коды и 2 байта на опкод уже сильно ущемляют полезность потому что ничем не лучше однобайтного LD A,(imm6).
В связи с этим вопрос — какие то пути преодоления проблемы просматриваются? :)
avatar
Мне кажется, что думать в терминах конкретных инструкций бесперспективно. Потому что как выглядят большинство хотелок в обсуждениях этого типа? «Я тут пишу код и мне хотелось бы для этого иметь EX HL,HL', было бы удобнее, быстрее и компактнее». Это подразумевает, что мы берём быстрые циклы для Z80 и ищем как их ускорить. Но качественные реформы, вроде расширенного блока регистров предложенного выше, на самом деле радикально меняют весь подход к оптимизации кода. Например, мы привыкли что самая быстрая заливка на Z80 — серия команд PUSH. А на Sharp LR35902 команда PUSH занимает уже не 11, а 16 тактов, и это при том что добавлена новая 8-тактовая команда LD (HL+),A, которая по сути может залить байтами с той же скоростью что и PUSH.

То есть я о чём. Просто набор команд недостаточен, чтобы сказать, это хорошо и плохо. Нужно знать растактовки команд и смотреть как будут выглядеть решения стандартных задач. И только там станет понятно, где у нас улучшено, а где просто изменено.
avatar
То есть я о чём. Просто набор команд недостаточен, чтобы сказать, это хорошо и плохо. Нужно знать растактовки команд и смотреть как будут выглядеть решения стандартных задач. И только там станет понятно, где у нас улучшено, а где просто изменено.

Я крайне с этим согласен!
И скорее всего сейчас начну говорить жуткую банальщину, но как раз эта вот растактовка меня давно уже не удовлетворяет в том же Z80.
Взять ту же самую LDIR. Блочная инструкция, двухбайтовая, пересылающая сразу блоки байт, но как?
А так что на каждый пересылаемый полезный по «ПН (полезной нагрузке)» байт сопровождается двумя считываемыми байтами инструкции и скорее всего еще больше тактов по общему смыслу.
Не сомневаюсь что это всё поднималось ранее тысячи раз.
Потому что ну реально концепция general processor на каждый байт обрабатываемых полезно данных нагрузки реально чаще всего даёт +10 байт инструкций, опкодов и адресов. Который нужно считывать.
В итоге LDIR работает полукалечно: просто выполняет LDI и делает JR -2, чтобы снова считывать 2 байта инструкции чтобы переслать 1 байт полезных данных.
И это дико бесит перфекциониста типа меня и рождает DMA-контроллеры. Просто потому что general processors сосут. Откровенно.
Понятно что там еще с прерываниями надо разобраться и тому подобное. Не всегда уместно блочить шины DMA-контроллером надолго. Может и звук защёлкать и так далее.
Но я хотел сказать лишь то что наверняка уже тысячи раз говорилось: хотелось бы чтобы в general processor были по настоящему эффективные инструкции могущие не пересчитывать байты инструкции x2-3 байта из памяти каждый раз пересылая лишь 1 байт полезных байт нагрузки.
А LDIR настолько такой не является, что её реально попают пушами.
Аааа…
avatar
«Я тут пишу код и мне хотелось бы для этого иметь EX HL,HL', было бы удобнее, быстрее и компактнее». Это подразумевает, что мы берём быстрые циклы для Z80 и ищем как их ускорить. Но качественные реформы, вроде расширенного блока регистров предложенного выше, на самом деле радикально меняют весь подход к оптимизации кода.
радикально изменить подход может и одна-единственная команда
например, ex sp,hl вместо (или вместе с) ld sp,hl
avatar
Такты в ФПГА реализации не всегда боттлнек, кое-что можно значительно оптимизировать. В основном же существующие корки имеют растактовки для совместимости.
avatar
Я это всё понимаю. Речь о другом. Даже если вы просто возьмёте и сделаете «Z80 на стероидах», просто с кучей однотактовых команд, ничего не меняя, это уже будет подразумевать что паттерны хорошего кода поменяются. Вот там выше aa-dav пишет про неэффективность LDIR, что зачастую правда, но только если речь идёт о скорости копирования. С точки зрения компактности кода LDIR очень эффективна, а по скорости — не более чем посредственна, так что вы не найдёте, например, декомпрессора на спектруме без LDIR. Т.е. даже медленная LDIR — очень даже пригождается.

Ваш перелопаченный Z80 потребует пересмотра вообще всех таких привычных соображений, во всяком случае с точки зрения хорошо оптимизированного кода. Поэтому невозможно вообще что-либо сказать, ни про твои доработки с сегментом регистров а-ля геймбой, ни про доработки некста. Без точной информации о скорости команд, разговор получится ни о чём.
avatar
По опкодам.
Либо префиксировать каждую команду, либо включать отдельный блок расширенной ISA. Префикс можно выбрать либо ED, либо из команд типа LD R,R — где реги одинаковые. Второй метод позволяет сильно экономить память — по объему и машциклам, а также дает неограниченную свободу воли в выборе опкодов. Для крайней совместимости предусмотреть защелку, блокирующую включение доп. опкодов.

По набору команд.
Основная цель — С компиляторы. Полагаю, 256 байт для регфайла достаточно. Должна быть общая арифметика, хотя бы 2-операндная, между любыми ячейками регистрового файла. Объединение соседних ячей в пары/четверки. Адресация ячеек как непосредственно, так и регистрами. Постинкремент и предекремент адресующих регов.

Над конкретным набором команд надо садиться и думать.
Ну и конечно, сначала опробовать все это в эмуляторе, что несложно.
avatar
Если тебе реально нужны компиляторы Си, тебе нужно развивать другие вещи. Я бы начал с команд типа ld r,(rr+n), ld r,(rr+r), push nn (единственная стоящая команда выше, мне кажется), и всяких add rr,nn. Потому что я ещё никого не видел, кто бы сказал, что Си проще компилировать в 6502, чем в Z80. Но вообще заниматься компилятором Си сложнее, чем очень много чем другим.
avatar
К слову о Си и 8-битных процессорах буквально на днях наткнулся на KickC: forums.nesdev.com/viewtopic.php?f=2&t=20187
Пытаясь максимально сохранять синтаксис Си компилятор просто возвращается к корням — ЭВМ из 60-х и не поддерживает рекурсию. И всё начинает работать гладко и шелковисто и компилироваться в бодрый и довольно рабочий код.
Тот же CC65 на следующем коде (очень для него неудачном):

void str_cpy( char *dst, char const *src ) {
   while ( *dst++ = *src++ ) {}
}

Рожает следующий тихий ужас:

.proc   _str_cpy: near
.segment        "CODE"
        jsr     pushax
L0006:  ldy     #$03
        jsr     ldaxysp
        sta     regsave
        stx     regsave+1
        jsr     incax1
        ldy     #$02
        jsr     staxysp
        lda     regsave
        ldx     regsave+1
        jsr     pushax
        ldy     #$03
        jsr     ldaxysp
        sta     regsave
        stx     regsave+1
        jsr     incax1
        ldy     #$02
        jsr     staxysp
        ldy     #$00
        lda     (regsave),y
        jsr     staspidx
        tax
        bne     L0006
        jmp     incsp4
.endproc

А вот KickC в своём асмосинтаксисе (как можно догадаться KickAssembler) делает почти оптимально:

str_cpy: {
    .label dst = 4
    .label src = 2
  __b1:
    // *dst++ = *src++
    ldy #0
    lda (src),y
    sta (dst),y
    // while ( *dst++ = *src++ )
    lda (dst),y
    inc.z dst
    bne !+
    inc.z dst+1
  !:
    inc.z src
    bne !+
    inc.z src+1
  !:
    cmp #0
    bne __b1
    // }
    rts
}

Весьма впечатляющий результат, хотя видно что еще есть куда работать.
Отсутствие локальных переменных еще позволяет производить оптимизацию с размещением переменных и параметров в одном и том же месте если они не пересекаются в callstack. Тоже недоступная для Си вещь даже если шлёпать static.
Вот такой эффект от того что как в древнем Fortran когда не было там еще ключевого слова RESURSIVE.
avatar
c push моя идея… уж больно нравилась она мне в x86. test тоже полезная)
avatar
— затолкать в стек непосредственное данное зашитое в инструкцию. Раньше можно было только регистровую пару

тут можно поспорить. call nnnn вполне себе «затолкать в стек непосредственное данное зашитое в инструкцию»
avatar
а сорри, не то
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.