- Регистрация
- 23 Авг 2023
- Сообщения
- 4,154
- Реакции
- 0
- Баллы
- 36
Ofline
Итак, в первой части я смело пообещал вторую статью «из одних картинок», но… мой маленький часовой кукушонок настолько похужал и возмудел за прошедшее время, что пришла пора знакомиться с ним, можно сказать, заново. Оптимизация не пощадила практически ничего, и, хотя я там же в камментах бо́льшую часть перемен отразил — всё равно надо начать с того, что же в сумме у нас получилось.
А получилось у нас что-то, вставшее на скользкий муть… то есть путь обратимого разбухания, то есть в базовом варианте оно несколько толще и умеет гораздо больше, но легко возвращается к изначальному микро-варианту. Урезается адресация, ставшие лишними линии заменяются константами и вжух — оно снова скукожилось примерно в то же самое, что мы видели в первой статье. В конце концов, наш любимый AVR тоже горазд варьироваться от «одни регистры поверх голого скелета» до «на этой дурище пека сделать можно».
Итак, таблица всех возможных опкодов с операндами (несмотря на то, что значения бит у сегмента и смещения в моём случае никогда не «нахлёстываются», как в 8086, то есть параграфов у меня не существует, я всё равно использую нотацию Segment:Offset для простоты):
Извините, что картинкой — бороться с этим «етитьором» я не готов.
SCN — Гермиона за лето почти не изменилась, потому что больше уже некуда. Наш любимый, самый базовый код, работающий по вот этому вот упоротому принципу комбо (интересно, где-то вообще такое исторически встречалось или я первый такой отбитый?) Первые два вызова кладут операнд в старшие три бита регистра P (а бывшие старшие смещаются вправо на три), третий вызов кладётся в регистр сегмента (при этом он «взводится», что и есть наше небольшое новшество), а четвёртый — вкупе со «взведённым» сегментом образует, как и в первой статье, старший байт регистра PC (Program Counter), совершая таким образом безусловный переход по двухбайтному адресу. Но и не только: если в прежнем варианте после третьего вызова был возможен только четвёртый (иначе Seg сбрасывался, делая третий вызов бессмысленным в принципе), то сейчас мне стало как-то жалко эти три бита опкода в четвёртом вызове (которые не делали абсолютно ничего, будучи очевидно-тавтологичными), и я добавил изрядную кучку вариантов использования свеже-«взведённого» регистра сегмента. Вообще, конечно, наличие таких «комбо-команд» говорит в пользу того, что у нас всё-таки микрокод, а «настоящий» код — в пользовательской флэшке. А не нативный код для VM, байт-код для которой — в пользовательской флэшке.
MOV ?, A — начались фокусы и фичи. Поскольку регистр флагов тоже дорос до третьего бита, я решил не бросать на ветер новую возможность и окончательно соединил концептуально флаги и A, а также сегмент и P в без двух минут монолитные девятибитные регистры. Короче, MOV P, A теперь девятибитная операция — флаги при этом сохраняются в регистр сегмента. Ибо нефиг ему пропадать и использоваться только как крошечная «прокладка» между третьим вызовом SCN и следующим опкодом. Да, теперь можно флаги сохранить и восстановить ^___^
Дальше пока всё как и было: MOV B, A, MOV L, A и MOV H, A остались теми же. А вот MOV SRAM[?], A изменилась до неузнаваемости. Во-первых, я плюнул и слил адресное пространство SRAM и регистров управления портами в единое целое. Сколько нужно выделить под порты — столько и выделяем. Где нужно — там и выделяем. Во-вторых — это пространство я увеличил ввосьмеро (512 байт, ну рехнуться как много), то есть мы теперь используем для адресации наш суммарный девятибитный регистр Seg😛; регистру Seg не обязательно быть «взведённым», то есть каждый раз перед обращением к памяти читать мантру из трёх вызовов SCN не нужно. Более того, он теперь регистр-счётчик, да ещё и реверсивный (мне показалось, что выигрыш в транзисторах масочного ПЗУ с микрокодом перевесит потери на реализацию счётчика) — то есть мы можем высвободившиеся опкоды, которые раньше отвечали за доступ к адресным пространствам портов, использовать для операций доступа с пост-инкрементом и пост-декрементом указателя (чем я на тестах сразу же радостно воспользовался для чтения длиннющих 48-битных констант). И, разумеется, если нам нужно реализовать управление каким-нибудь силовым драйвером и весь этот «тюнинг в зоопарке» нам не нужен — всё это легко откатывается обратно, просто в данной версии кристалла вместо Seg туда заводятся старые добрые константы начала адресного пространства оперативки, портов и направлений портов, а регистр-счётчик превращается обратно в обычную «защёлку». И да, даже в полной версии, пока мы работаем в рамках одного сегмента, для задания адреса следующей переменной в оперативке достаточно только двух вызовов SCN, третий нужен только в случае, если она в другом сегменте — не забываем, что Seg должен быть «взведён» только в узком диапазоне применений, а для остальных — один раз задали и гоняем туда-сюда P.
С операцией MOV PC, A😛 (то есть JMP A😛, если человеческими словами) я немного поигрался и вернул всё, как было. Учитывая, что у нас в принципе нет аппаратной поддержки подпрограмм — возможность передать вместе с параметрами адрес возврата перед переходом, а потом его загрузить в A😛 и выполнить эту команду, в общем-то, необходимейшая вещь. Она позволяет реализовать подпрограммы хотя бы «вручную», а без этого написание микрокода для любого вменяемого проца (кода для виртуалки, реализующей минималистичный токенизированный Бейсик, если угодно) быстро превратится в ад ветвлений. В конце концов, нам потребуется сложение и вычитание. И умножение с делением, которые дёргают сложение и вычитание. И куча более сложных операций, которые дёргают по очереди одно и другое…
А вот с командой Jcc я, конечно, порезвился. Ну, что я бит операнда поменял и теперь сначала Jcc B😛 идут, а потом уже Jcc A😛 — это мелочи, это я просто оптимизировал отдельные транзюки в управлении выбором (в камментах кода симулятора это есть). А вот что «взведённый» Seg подменяет собой что A, что B — это уже поинтереснее зверь! Переходы по коротким адресам из начала памяти (первые 1024 байта, потому что три бита задаём через Seg и ещё один — через то, A или B он подменит собой) теперь можно делать тупо через SCN-SCN-SCN-Jcc (а можно и не делать, если мы резвимся в пределах сегмента и как выставили B, так его и не трогаем — все хреновшества срабатывают только при «взведённом» Seg, то есть только если буквально предыдущей командой был третий вызов SCN). Ну а в это самое начало мы, конечно, сложим все наши библиотечные функции, которые постоянно дёргать надо…
Была мысль заменить A😛 в правой половине таблицы на H:L, чтобы не перегружать аккумулятор функционалом (да и обычно когда получены флаги, он занят результатом, а не адресом), но потребное количество «мухов» (мультиплексоров) явно бьёт всю экономию на ручном перекидывании H:L в A😛. Перезапись L→A→P, конечно, Seg флагами перезапишет — но это по понятной причине не считается за его «взведение», по смыслу флаги совсем не адрес и такая операция (в отличие от третьего SCN) этот механизм не активирует, да и на Jcc не повлияет, потому что оригиналы флагов не разрушаются. Плюс, в отличие от «мухов», ручное перекидывание более универсально в плане источника данных. Есть тот момент, когда в вопросах попухания устройства надо остановиться. Да и, в отличие от «продвинутой адресации», это уже не заменяется обратно на проволочки в случае tiny-версии (выражаясь в терминах AVR).
Ну, и дальше снова наша «великолепная пятёрка», которая пишет аккумулятор. MOV A, ? изменилась не очень сильно: теперь MOV A, P заодно восстанавливает флаги, ранее сохранённые в наш сильно разросшийся по функционалу регистр сегмента (интересно, кто-нибудь сможет как-нибудь особо извращённо задавать сегмент, скормив ему комбинацию из флагов?), ну и адресация доступа к SRAM стала такая же, как и в MOV ?, A — она везде теперь такая.
Дальше начались перестановки: XOR A, R и NAND A, R теперь идут подряд. XOR затронули (почти!) только новшества с адресацией, а вот NAND — зверь новый, знакомый нам ранее только по комментариям к первой статье. Что XOR, что NAND доступны в обычном сумматоре типа «nine-NAND» в его левой половинке, до того, как у нас припожалует вальяжный сигнал переноса (и неспешно поползёт дальше, ибо СУПом не кормлен). То есть мы просто выводим их на правах промежуточных результатов, ну и тупо выбираем при помощи «мухов», что именно нас интересует — а сумматор всегда получает на вход всё и работает в более-менее одинаковом режиме (с поправкой на то, что его операнды тоже проходят «мухов»). Ну, и, помимо этого, NAND полезен тем, что (в отличие от AND) он имеет смысл для операндов A, A — он просто инвертирует A при этом!
Как мы это используем? Начнём с недостатков. Если AND A, NOR[H:L] позволял быстро посмотреть при помощи маски в пользовательском коде (который во флэшке лежит), взведён ли какой-то бит в A (при помощи итогового флага ZF), то сейчас мне пришлось заставить обе логические команды модифицировать и CF тоже. Поскольку у побитной логики никакого настоящего переноса в принципе быть не может, я взвожу CF в случае, если результат равен 111111. Сами понимаете, что теперь снова можно точно так же легко проверить любой бит в A 🙂
Возможно, впрочем, что насчёт XOR я погорячился. Проверять его результат на «все единицы» не столь полезно, а вот сохранять CF может быть нужно для того, чтобы быстро «прыгать» между указателями на операнды, делая им «XOR на разницу». В тестовом коде мы ещё вернёмся к этому…
Что же касается достоинств, то тут как раз вопрос о том, почему регистр флагов тоже разросся до трёхбитного: в нём появился флаг NF, сиречь Neg Flag. Он взводится в одном-единственном случае: если NAND был вызван для A, A (CF, кстати, мы при этом не трогаем. Пусть хоть ноль получится, хоть 111111, но он сохраняет старое значение). Во всех остальных случаях (если команда в принципе модифицирует флаги) он сбрасывается. А поскольку для получения отрицательного числа в допкоде нам нужно получить его инверсию плюс единичка — назначение этого флага довольно очевидно, он будет использован в качестве этой самой «плюс единички».
Ну, и в самом конце теперь две обычные арифметические команды: ADD и ADC. Точнее, чистого ADD теперь и вовсе не существует — вместо него теперь ADN, то есть по сути тот же ADC, но вместо CF на сумматор отдельный «мух» закидывает NF. Что в основном не меняет картину, потому что NF у нас в общем случае ноль — но только не тогда, когда мы только что инвертировали вытащенный из SRAM младший байт какой-то переменной! В этом случае NF взведён и к «сумме» будет добавлена та самая необходимая для допкода единичка, образуя разность. Ну, а дальше мы просто используем обычные ADC для остальных байт, не забывая инвертировать вычитаемое (именно поэтому мы не трогаем CF при NAND A, A — он у нас реальный, живой и нужный, с прошлого разряда). Итого фактически у нас есть как многобайтовое сложение, так и многобайтовое вычитание прямо в SRAM, без лишних телодвижений и хранения промежуточных результатов. Вообще, конечно, наличие таких «толстых» команд говорит в пользу того, что у нас всё-таки нативный код для VM, байт-код для которой — в пользовательской флэшке. А не микрокод, а «настоящий» код — в пользовательской флэшке.
Чёрт. Эта загадка будет вечной.
Итак, первая подспойлерная простыня кода: сам эмулятор. Сначала цепляю целиком, чтобы было относительно удобно скопипастить и собрать (думаю, чем угодно собрать можно; но, как разлившийся спирт, его точно можно собрать «ваткой»; надеюсь, однако, что не воткнул в статью ссылку на какой-нибудь новодельный фейковый репозиторий!)
Скрытый текст
Протестировал почти всё, исправил кучу перепутанных битных линий 🙂 и проверил, что вычитание действительно работает. Теперь будем разбирать его по кусочкам, заодно комментируя на нашем родном берёзово-хабровом, а не буржуйском итч-иошном (нет, не надо рассчитывать на то, что оно обязательно там появится. Я просто закладывал такую возможность. Собирайте здесь и сейчас из текста статьи, если хотите — но не ждите ничего и никого).
Начну разбор этого кода я с того факта, что поделие сие написано на ядрёной фене, суржике VHDL и Си. Много лет назад я грозился про него рассказать — и вот, действительно, достаточно весомая оказия для этого (я одновременно могу ввести в суть структуры того, как должно быть описано железо, того, как оно работает, и мне не требуется для этого излагать это на языке, который знают тут не все. Более чем веская причина, я считаю). Здесь я разберу самый простой пример — один clock domain, один главный цикл for. Суть этой фени в особом структурировании кода на Си таким образом, что он становится VHDL-подобен и может быть синтезирован 1:1, не гадая, «чего этот программист имел в виду». Каждое значение устанавливается один раз, зависят они одно от другого однонаправленно, есть определённый чёткий момент, когда значения «защёлкиваются» в регистрах по тому или иному фронту синхронизации — в общем, это чёткая, жёсткая симуляция именно работы «железа», а не просто «в случае команды bar регистры foo становятся Кар и Мяу, а как — ваши половые трудности», как это было бы «просто на Си» (а я как раз хочу про «кухню» рассказать с отягчающими подробностями). Причём в данном случае я ещё и от себя жести добавил — обычно 80% описаний каждого проводочка можно пропустить, ибо они очевидны и для синтезатора тоже. Если мы присваиваем значение регистра равным значению другого регистра — ну ясно же, что между ними есть проводок с этим значением!
Я описал их все.
Ибо.
Этот леденящий душу синтаксический ужас продиктован почти исключительно моей ленью. Его задача — позволить положить морской якорь на написание ассемблера (по крайней мере, пока), и сбацать тестовый код прямо в .h-файле, накидав его дефайнами, отдалённо напоминающими мнемоники этого самого ассемблера. Хотя в паре мест самого эмулятора я таки использую эти дефайны, чтобы не озадачивать при чтении вопросами, что это за «волшебная циферка».
Вот только обычно всё решает один-два бита. Поскольку у нас тут симуляция реального, хорошо оптимизированного железа. И тут не встретится (или почти не встретится) if (command == ADN || command == ADC || command == NAND || command == XOR) ModifyFlags(), а будет жёсткое, суровое, как драка канадского лесоруба с сибирским, безжалостное if (OpcodeMSB). Хотя любой уважающий себя синтезатор (а их пишут не дураки и пишут по своему образу и подобию), конечно, способен разобраться в круговерти тавтологических условий и свести их к одной однобитной линии. Я пишу так не потому, что «мой суржик на другое не способен» — способен так же, как способен VHDL или Verilog. Я пишу так именно потому, что ставлю перед собой задачу показать, как в проектировании железа понятие «группа команд» превращается в одну медную полоску, которую видно не во всякий микроскоп.
Дефайны состоят из масок (две, самые употребимые в коде симулятора), группы опкодов, группы операндов и группы префиксов (одна штука пока). Чтобы набрать в .h-файлах ассемблероподобное кашепойло, нужно набрать операндоподобный дефайн и параметроподобный дефайн, добавить по вкусу префикс (или нет) и закончить запятой. За счёт стратегически расставленных плюсиков при компиляции это всё соберётся в циферку, укладывающуюся в наши 6 бит. Или не укладывающуюся, если использовать префикс: по понятной причине он с пространством команд нашего предмета статьи вообще не пересекается, ибо существовать может только в эмуляторе, где под него физически биты есть. Сейчас он один и предписывает после исполнения команды сдампить SRAM в stdout (там вообще всегда результат дампится в stdout, но SRAM каждый раз кидать как-то жирно). Ну, и если это была команда SCN 0 — остановить эмуляторот удивления, потому что зачем кому-то понадобилось после SCN 0, никак не влияющей на SRAM, её дампить?
Тут мне сначала пришлось посетовать, как же плохо моя феня описывает битности, не кратные 8. А дабы симуляция точной была, лишнее безжалостно обрезать надобно. Это, однако, закрывает возможности преднамеренного эксплойта расхождений между железом и эмуляцией, то есть мои префиксы превратятся в тыкву (а вот логирование как таковое делается элементарно — просто предписываем тем или иным образом при синтезе игнорировать эти непонятные письмена). Тем не менее, если сия затея вам по нраву — проблему надо как-то решать, чтобы можно было один раз описать нестандартную битность и предписать таким образом компилятору каждый раз делать «чик» при каждом присваивании. И не превратить язык в ад перегруза перегруженной перегруженности перегрузок, а то, знаете, одна такая попытка уже была… видел я её… там вроде бы как тоже «всё сделано стандартными средствами языка», но то, что получилось, не просто не «си-подобное» — оно потеряло настолько любое сходство с…, что намного легче любой HDL освоить, чемЪ. Да и, честно говоря, хочется остаться в рамках именно Си: как нетрудно видеть, «плюсы» у меня там больше загрязняющая примесь, чем как-то используются.
Теперь мы имеем описания библиотечных элементов. При наличии реального синтезатора, конечно, это всё будет несколько не так, особенно в отношении SRAM (которую мы не только читаем, но и пишем): у нас есть некий .h-файл, в котором описано её поведение и торчащие из неё линии, а мы в своём коде никаких массивов не описываем, а работаем именно с этими «ногами». Потому, что это обычная, стандартная SRAM, и мы не пытаемся её сложить из отдельных регистров, к каждому из которых подведены свои линии управления и так далее. Мы просто подаём данные на вход, подаём адрес, подаём команду «пиши» и ждём некоторое время, пока транзисторы записи «пересиливают» транзисторы хранения, обычной грубой силой более широких затворов и более жирных микроампер. Потом мы снимаем команду «пиши» и после этого можем, наконец, позволить себе менять что-то на остальных линиях.
Масочные ROM и NOR FLASH ROM, да и сама SRAM на чтение — это намного проще. Подал адрес, подождал, устоялись данные. Можно принимать их во внимание. Потребляющие их линии подключены к ним всегда — но пока данные не устоялись, плюс пока через всю обработку не прошли эти актуальные значения, на выходе у нас мельтешит какая-то дребедень, и в силу «гонки состояний» она до определённого момента являет собой совершенно произвольную кашу. Нельзя рассчитывать на то, что после прибавления единички один битик вежливо подвинется в одну сторону, а другой — в другую, да ещё и одновременно, и это относится и к библиотечным элементам, и к тому, что мы вытворяем. Есть определённые библиотечные элементы, скажем, счётчики-делители, которые за счёт схемотехники гарантируют, что при добавлении единички младший разряд сразу инвертируется, а не будет скакать из нуля в единицу туда-сюда сорок раз, пока не определится со своим новым значением. Но в любой достаточно сложной схеме таких гарантий никто не даст; к счастью, у нас офигенно синхронная схема, в которой на вход кидаются данные, а через некоторое время приходит фронт тактового сигнала и результаты защёлкиваются в регистрах. И нам важно только то, что к моменту этого фронта они точно устаканятся, а сколько раз они решат в процессе превратиться в лису, сову и дракона — это исключительно их половые трудности.
А дальше — они! Благословенные наши регистры, счётчики и регистры-счётчики. Вроде бы библиотечные, а вроде бы — нет. Любой уважающий себя синтезатор их легко соберёт из триггеров, а триггеры — из КМОПов, сообразно их использованию. Были в коде инкременты и декременты — ставим реверсивный счётчик. Были присваивания — ставим защёлку. И так далее. Они идеально синхронные, даже в плане линий управления. Вот подали мы команду «защёлкни новое значение», сняли, опять подали — гонки состояний не унимаются. А ему — до лампочки. Он выполнит то действие, которое будет на входах в момент фронта «синхры». И время ему на это нужно какое-то пренебрежимо малое, потому что он уже всё посчитал и решил — надо только защёлкнуть результат (ну, обычно. Бывают ещё всякие переносы…) Как правило, подкузьмить его внезапным изменением хотелок на его входах в тот момент, когда идёт «процесс защёлкивания» — это должен быть или очень неправильно реализованный регистр, или очень стараться надо специально. Другое дело, что на его выходах изменённый результат когдаааа ещё появится… но хорошо спроектированный девайс такого типа, не в пример нашей записи SRAM, требует времени удержания уровней сигналов во время прохождения фронта синхронизации чуть ли не как длительность нарастания самого фронта. А если там что-то очень долгое — то первой он защёлкнет именно то, какую команду выполнять, и станет глух к её изменениям.
Хотя, конечно, уважающий себя синтезатор учитывает все лаги, и если какие-то гарантированно достаточно велики — что-то где-то упростит, потому что «ну пока эта масочная память свои паразитные ёмкости перезаряжает, ничего ж не изменится один фиг».
А вот счётчик NibbleCounter_2bit, который считает, сколько раз мы SCN вызвали — описывать мне стало лень, что я и откомментировал в немного странной формулировке. Считайте, что это тоже что-то библиотечное. Хотя надо было б дать описание его из пары триггеров-то…
Теперь о дефайнах, которые нужны для синтезатора — пусть он только у нас с вами в голове, это ничего не меняет. Ты и есть синтезатор. И, чтобы можно было понять, как наш нано-камешек работает — я объясню, как понимать сии письмена.
Дело в том, что у нас сишными переменными описываются две базовые вещи: хранилище значения (о благословенные наши регистры-счётчики!) и сигнал, сиречь проволочка, бегущая от логики до логики со всеми гонками состояний. И есть цикл симуляции, когда мы должны определиться со всеми значениями всего на свете, между одним фронтом синхроимпульса и другим. С сигналами всё просто. Особенно у меня тут: я почти полностью детерминированно их описываю, причём сразу задаю описание «или такой, или такой» (небольшое исключение в цепях сигнала Reset). Но это не обязательно в общем случае: проволочка не хранит значений и не может быть не подключена никуда вообще. Поэтому важно за цикл симуляции просто определить её значение так, чтобы оно а) было присвоено один раз, без «я передумал» б) использовалось по коду ниже, чем было присвоено, или показания эмуляции и железа разойдутся на такт. Даже если для разных случаев мы в разных местах значения определяем — синтезатор все их выловит и сделает переключение этой проволочки между разными вариантами мультиплексирования. Если для какой-то ситуации мы вообще не определили значения — значит, это не важно. Вообще. И надо делать так, чтобы избежать лишней логики. А вот изменения значений в хранилищах данных — дело другое. Если не было команды менять содержимое — значит, менять его нельзя. А как же быть с ситуациями, когда сигналы друг другу противоречат? Ведь такая ситуация никогда не возникнет, а синтезатор её будет обрабатывать, тратить кучу логических элементов просто на то, чтобы запретить изменение какого-нибудь регистра (а его надо именно запретить, ведь для этой ситуации, буде она невозможна, мы не дали отмашку что-то делать), всё это внесёт задержки, а сочетание сигналов, требующее такой реакции, в принципе невозможно создать? Синтезатору неведомо, какое сочетание сигналов можно создать, а какое — нельзя. Сам он упразднить эту тавтологию не может — для этого потенциально может потребоваться даже не полный анализ 2^64^64 вариантов состояния логики, а сопроводительная записка от инженерного отдела. Для этого я ввёл эту простую команду Invalid, останавливающую симулятор с ошибкой, а синтезатору предписывающую упрощать логику и не обрабатывать эту ситуацию в принципе. Что получится, то получится. Инкрементируй, декрементируй, экс… обнуляй, перезаписывай, в общем, не важно: эта ситуация заблокирована на более высоких уровнях логики.
Следующие дефайны нужны именно для упорядочивания цикла симуляции. Они попарно задают «ворота», в которые может приходить фронт и спад сигнала синхронизации, соответственно. Ну, то есть задают ТЗ на то, в какие лаги там надо уложиться, чтобы всё успеть. И, что намного менее очевидно — то, что можно переставлять, переносить, менять местами. А заставляет нас всё это делать опять-таки наша SRAM: со всеми остальными всё относительно просто, защёлкнули последствия выполнения очередной команды и автоматически сигналы побежали формировать следующую. А если мы её начнём защёлкивать вместе со всеми как результат выполнения команды на запись в неё-голубушку, то остальные успеют перейти к следующей команде и перекозявят сигналы на её входе, которые должны оставаться постоянными всё время записи. Даже отдельный регистр-защёлка не спасёт: ну, защёлкнули мы там данные для записи, адрес, сам факт режима записи… а кто потом её переключит в режим чтения, чтобы к следующей команде она смогла выдать данные (а всё остальное для этой команды уже давно бежит по цепочкам логики)? И какие задержки это добавит, причём даже там, где и записи-то по факту не было?
К счастью, у нас есть не только фронт «синхры», но и спад (причём в нашем случае — спад «предыдущего» импульса, если о них так можно говорить, потому что отреагировать нам надо заранее, а не с задержкой). И определиться с тем, пишем мы или не пишем, мы можем намного быстрее, чем протащить кучу переносов через АЛУ. Поэтому синхронизация у нас, по сути, двухфазная: через некоторое время после очередного фронта у нас «устаканиваются» новые значения для команды и для данных, после чего мы можем быстро (к спаду) понять, надо ли нам писать в память, при необходимости включить могучие транзисторы, а к очередному фронту, который ознаменует окончание обработки этой команды — выключить, в результате чего память снова будет способна делать что-то другое (например, вместе со всеми выдавать значение данных для уже следующей команды). Поскольку остальные процессы никак с этим не пересекаются — они начинаются тогда же. Но, поскольку они длиннее, чем просто выбор «пишем/не пишем» — у них есть полное время от фронта до фронта.
Синтезатор, конечно, понимает, что если что-то защёлкивается по фронту — значит, надо уложить все «евойные» лаги в отведённую длительность (в нашем случае схема настолько детерминированная, что он может только ответить нам, уложились мы или нет в заданную тактовую, и сколько у нас запаса, если уложились; в сложных схемах, однако ж, у него обычно изрядный простор для манёвров, идти по более быстрому пути или по более экономичному). Но в этом случае со SRAM приходится задавать некие «воротца», указывающие, что надо привязывать к фронтам и спадам, а что — не надо. И тут всё (относительно) просто: если что-то указано до Begin — значит, это надо закончить до соответствующего фронта, будь то передний или задний (доселе именовавшийся спадом). Если указано после — значит, конкретно это событие является синхронным и должно триггериться именно этим фронтом. До — асинхронные переключения всяких «мухов», после — выполнение всяких защёлкиваний и инкрементов счётчиков (наверное, если туда поместить присвоение значения сигналу, синтезатор должен выплюнуть ворнинг и рассматривать его как сигнал, прикрученный таки к какой-то защёлке. Или ошибку. ХЗ). А вот между ними помещается самое интересное — это вещи, индифферентные к данному из фронтов. То есть им вовсе не обязательно закончиться до Begin или триггернуться по End. Но тут водится небольшая мозговая козявка: хотя синтаксически они и описаны выше того, что триггерится End'ом, они-то как раз не обязаны закончиться раньше него (то, что обязано — праааавильно, оно в секции перед Begin). Потому, что в «железе» всё на самом деле происходит одновременно и параллельно. А Begin и End — это просто рамки, в пределах которых может произойти данный фронт (а если он за них вышел — всё, синтез не удался, хотелки не соответствуют быстродействию кремния).
Ну что ж, начинается истинная Боль. Потому, что мы сейчас должны перейти от простой и понятной логики «что делает каждая команда» к логике «чем определяется значение каждого регистра после завершения команды». А оно определяется — прааааавильно, всем и сразу. Всеми командами, которые на него способны хоть как-то повлиять. И логикой выбора того, какое именно значение он в итоге примет.
К счастью, из-за крайней простоты нашего девайса мы можем хотя бы отделить всё, что не пишет в регистр-аккумулятор, и всё, что в него пишет, разбив всё на две группы. В сумме эта простынка описывает исключительно наши сигналы-«проводки», которые сами по себе ничего не хранят, а просто переключаются при помощи «мухов» между источниками. Чтобы это обозначить для синтезатора (даже для того, который сейчас живёт внутри наших голов), я нагло использовал слово volatile. Выбор, конечно, не просто так — я подумываю о том, что в случае развития этого странного языка можно вполне себе даже вести симуляцию разных clock domain в разных тредах. Но пока просто рассматривайте это как словесное украшение. Разумеется, этот выбор говорит о полном моём пренебрежении оптимальностью того кода, который соберёт бедный компилятор (как, впрочем, и всё остальное в этом коде). Я целиком и полностью нацелился на максимальное объяснение цифровой схемотехнической «кухни» моего крошечного «часового проца», вплоть до того, что кто-то может захотеть сделать его на ПЛИС, выделить несколько регистров для управления DRAM, прицепить несколько мегабайт, в качестве микрокода сделать эмуляцию 80386 и запустить на нём не просто Doom, а тот самый, DOS'овский первый Doom, без всякого портирования. И не встретит на этом пути никаких особых препятствий.
Впрочем, расписывать значение каждого слова в этой простынке я всё-таки не стану. Это будет не столько «объяснение», сколько выклёвывание мозга. И если с говорящими названиями типа «шина SRAM, данные» всё более-менее просто, то принципы построения остальных названий я сейчас расскажу. Дело в том, что я использую тут чисто двоичные «мухи», которые имеют ровно два входа — или один выбран, или второй. А для того, чтобы выбирать из достаточно большого количества сигналов — я их каскадирую. И я даже не пытаюсь свести их, скажем, к одному четырёхвходовому. Потому, что критерии дальнейшего выбора для той ветки, у которой старший «мух» получил 0, и той ветки, у которой он получил 1 — они часто бывают, чёрт их дери, разными. И это дорывает мозг, потому что у них в сумме могут быть четыре входа и три управляющих сигнала. А не два. Потому, что при старшем бите 0 работает один вход младшего бита, а при старшем бите 1 — другой. Потому, что нужно так. Потому, что итоговое значение вот так вот выбираться должно. И имена у них получаются соответствующие.
Булевы переменные, понятное дело, соответствуют однобитным сигналам-«проволочкам». Остальные, соответственно, шинам. Имена «Opcode кто-то там» — это сигналы, получающие единицу, если мы сейчас разгребаем именно этот код операции. Я, конечно, опускаю то обстоятельство, что активными могут быть и единица, и ноль, потому, что результат проверки на соответствие значению (как и все остальные результаты) может в силу схемотехники изначально быть инверсным — и нет абсолютно никакого смысла его специально инвертировать для того, чтобы приёмная логика его инвертировала ещё раз, потому что ей, внезапно, вдруг тоже оказался удобнее инверсный вход (а мультиплексор просто примет любой вариант — мы всегда можем просто поменять входы данных местами). Об этом совершенно не надо думать — это, наверное, первое, ради чего в принципе синтезаторы появились. Чтобы хотя бы от этого «синтезатор в голове» разгрузить.
Сигналы «Writing кого-то там» — это окончательное и бесповоротное решение данного товарища в момент фронта синхронизации защёлкнуть. Складываются из кучи причин «Writing кого-то там из кого-то там», например. Или из других. Сигналы «Кто-то там ДатаМух N» — это как раз выбор того, какой из двух вариантов пойдёт нам на выход «муха». Причём те, которые относятся не к старшему биту выбора — они с суффиксами, намекающими, к какому из вариантов старшего бита они относятся. И так далее до. Что же до самих данных — они, во-первых, не булевы (как правило. Вход переноса, который «мухается» или на Carry Flag, или на Neg Flag — исключение), а, во-вторых, не имеют в себе буковки «N». Зато те из них, которые промежуточные (да, я даже их расписал) — имеют не только циферку с номером входа, но и буковки, намекающие на их сущность так же, как и суффиксы у их сигналов-селекторов. И финишный выход не имеет ничего, кроме указания на то, что это данные для какого-то регистра и что они мультиплексированные (или нет, такие переменные тоже легко заметить). А поскольку у нас есть ещё и регистры с опцией реверсивного счётчика, для них есть ещё парочка управляющих команд — нужно ли инкрементировать/декрементировать регистр в момент фронта импульса. Или вообще перезаписать из входов, да. Или перезаписать частично, причём с очень хитро-разными масками. Это собирается автоматически, но из триггеров, поэтому можно управлять каждым битом раздельно.
Как легко видеть, мы перешли к Сути — к циклу симуляции. Это бесконечный for, первый прогон которого — Reset. В принципе, синтезатор (даже на ручной внутричерепной тяге) может и сам до всего догадаться, но, чтобы облегчить ему работу (особенно во втором варианте), кое-что лучше описать в явной форме. В самом начале мы видим такой пример — простынка безусловных присвоений сигналам значений из регистров, счётчиков, регистров-счётчиков, из выходных линий памяти во всех её ипостасях, и так далее. А поскольку сигналы — это просто проволочки, то эта простынка описывает вещи, которые подключены всегда, напрямую (плюс-минус возможная буферизация, если выходного тока одного элемента может не хватить на переключение целой оравы потребителей), а поскольку прошлый цикл закончился присвоением новых значений всем этим регистрам — время «от входа до выхода» открывает счёт суммарного лага (то есть PC, значением которого мы интересуемся в первой же строчке — это не просто PC, это «PC, который уже защёлкнул по фронту новое значение, но до выхода оно ещё не дошло»). Дальше к нему, конечно, добавляется лаг нашего масочного ПЗУ! Вот уж лаг так лаг. Да, оно быстрое, конечно… на фоне «драмы». SRAM я, конечно, описал несколько упрощённо — все трудности я выше рассказывал, но в целом читается она как раз примерно так, основные «недоговорки» будут касаться записи. А вот с этими «смещениями на N бит» туда-сюда — всё вообще просто. Это даже не операция, а просто описание того, из каких проводочков какие значения берутся. Да и вся эта простынка, по сути, только для компилятора является кучей операций. Для синтезатора же это просто netlist, сиречь список соединений. И да, как я уже говорил, можно было бы особо-то не расписывать все эти сигналы, потому что если мы ниже дадим какому-нибудь «муху» в качестве входного значения напрямую регистр — не только компилятор поймёт это правильно, но и синтезатор поймёт, что там подразумевался простейший сигнал «от одного до другого». Но мы сейчас бу́-дем пи-са́ть ка́к в бук-ва-ре́, по слогам и с ударениями.
Что мы тут видим? Мы видим, что к мощным перезаписывающим транзисторам SRAM подведён напрямую регистр A, потому что в системе команд нет других источников значений для SRAM. Как только мы их включим — ячейка, выбранная при помощи шины адреса, перезапишется. Дальше мы видим, что для регистра P существует два источника данных — это A и операнд SCN (плюс частично — старое его значение). Это вполне вяжется с тем, что мы помним про систему команд. Аналогично, Seg может использоваться как сегмент, а может — как временное хранилище флагов. Дальше мы видим, что младший байт PC может задаваться только через регистр P (обычный переход к следующей операции не в счёт, уважающий себя регистр-счётчик это делает через управляющие входы, а не при помощи внешнего, простигосподи, «сумматора с единичкой»). Как мы помним, там мог бы быть ещё вариант L для Jcc H:L, но здравый смысл вовремя очнулся 🙂 А вот старший может задаваться аж четырьмя способами! Через A (Jcc A😛 или MOV PC A😛), через B (Jcc B😛), через третий вызов SCN с последующим Jcc и через четвёртый вызов SCN.
Аналогично не составляет труда понять всё остальное, кроме того, куда девался второй вход «муха» для регистра флагов. А он сам по себе собирается из кучи разных вариантов! А тут мы описываем самую базу, у нас даже секций для фронтов импульса тут нет — то есть тут ничего не происходит. Просто счётчик лагов синтезатора копит лаги для того, чтобы прибавить их потом ко всему, что будет их использовать. И второй вход, естественно, описан уже там. В принципе, там могло быть описано и всё остальное, это больше вопрос структурирования и читаемости.
Дальше мы видим, как некоторые проволочки цепляются к другим проволочкам — по сути, это просто раздача новых имён, с точки зрения синтеза ничего не происходит. «Мы будем использовать линию выхода SRAM как какой-то вход какого-то мультиплексора» (судя по имени. Сам факт того, что это мультиплексор — всплывёт только в момент его описания). По́ сло-га́м, ка́к о-бе-ща́л.
Теперь мы переходим наконец к настоящему поведенческому описанию. Сначала у нас традиционная ветка Reset, то есть что железо должно делать, когда подан соответствующий сигнал. Тут мы не видим описания ровно ничего, кроме того, что надо «обресетить» регистр программного адреса (PC) и зачем-то A (на самом деле — для примера, захотелось мне так. А вот NibbleCounter_2bit, на грамотное описание которого я забил в самом начале, тут должен быть — а его нет). А что с сигналами? А что угодно. Сигнал не описан — сигнал может вести себя произвольно, как мы выше уже разбирали, касаясь Invalid(). Главное, что остальным хранилищам информации что-либо присваивать запрещается. Все управляющие входы к моменту прихода фронта синхронизации выставляются в FALSE, кроме тех, которые отвечают за сброс PC и A. И многообещающее «else» наконец открывает нам мир внутренних принципов работы процессора.
Со всей имеющейся вводной информацией будет легко 😉
Как уже мы знаем, пока длится высокий уровень предыдущего синхроимпульса, мы должны успеть определиться с тем, пишем ли мы в SRAM. А также должны устояться адреса, выходящие со свежеприсвоенных регистров, ну и так далее. И потом их никто не должен трогать пол-цикла (вот это — самое простое. Если мы пишем в SRAM, то никакие другие операции явно не происходят). Фактически первые строки означают задание на расчёт лагов, после которых значение Writing_SRAM является установившимся, правильным и ему можно доверять (насчёт адресов, которые могут в теории ещё «трепыхаться» на выходе с регистра, мы тихо помолчим — вместо нормальной библиотечной SRAM, где это бы тоже прописывалось по отношению к её входам, у нас тут несколько более «игрушечная»).
Дальше у нас начинается окно Begin-End, где располагаются вещи, индифферентные к спаду «синхры». Поскольку по спаду только начинается запись SRAM, тут помещается практически весь процессор с ушками, рожками и ножками, только глазки из бездны моргают робко. Два самых простых начальных условия — мы определяем промежуточные сигналы «у нас опкод SCN» и «у нас опкод JCC» (с записью из A определились раньше — у неё требования к таймингам жёстче). Обычно используется страшный грозный зверь — дешифратор команды, который для каждой команды генерирует свой отдельный управляющий сигнал на отдельном выходе, да ещё и со всякими опциями, вариациями… но у нас система не только упрощённая, но и очень оптимизированная, поэтому такой чести удостоились немногие. Основная масса команд идёт как единая группа. Дальше мы определяемся с тем, какие у нас есть причины записать регистр P, ну и по итогу с тем, пишем ли мы его. Этот сигнал станет по итогу управляющим для регистира P: он придёт на вход «запись», традиционно подёргается из-за гонок состояний туда-сюда, успокоится к моменту прихода фронта синхроимпульса и, если успокоился он в активном уровне, регистр тут же защёлкнет значение на входной шине (и оно медленно поползёт через его ключи к его выходу).
Следующим мы видим нечто более интересное: в качестве критерия того, из какого источника писать P, выступает всего один бит кода операции! Мы не пользуемся даже готовыми линиями, чётко указывающими, что операция у нас SCN (или не SCN). Мы берём один бит, который будет задавать «мусорное» значение для любой команды, кроме двух, нужных нам. Почему? А потому, что у нас нет «готового» значения Opcode_SCN. Всё происходит более-менее одновременно. И этот бит у нас по определению будет раньше, чем вычислится Opcode_SCN — так зачем вносить лишний лаг? «Мух» в любом случае в железе есть, и он никуда не денется. Что-то он должен выбрать в любом случае, или первый вход, или второй. Даже если у нас совсем другая операция, которая его выходом пользоваться не будет. С точки зрения симуляции это выглядит странно и расточительно, но показать разницу между реальным миром, где как раз это — экономия ресурсов, и его жалким софтовым подобием в симуляции, как раз входит в круг моих сегодняшних задач. Примерно то же самое мы видим дальше с регистром сегмента, но они уже настолько срослись в нечто девятибитное, что я даже не всегда удосужился выделить ему отдельные сигналы, хотя вроде обещал, обещал…
Дальше у нас, как легко видеть, PC. Ох уж этот PC! Переходов у нас тьма-тьмущая вариантов, чтобы с нашими 6 битами было можно писать хоть что-нибудь (серьёзно, в какой-то момент написания тестовых микрокодов я ощутил натуральную клаустрофобию, настолько психологически «тесным» показался наш герой дня). Поэтому всё у нас тут то же самое, но толще, выше и мощнее — как минимум, выбор значения уже среди четырёх кандидатов. Определившись с тем, будет ли у нас вообще переход (была ли команда безусловного, была ли условного, сработали ли для неё условия…) — выбираем для неё правильный источник значения. Первое ветвление (с точки зрения как симуляции, так и прохождения сигналов оно, конечно, второе, но с иерархической точки зрения оно именно что первое!) — выбор того, будем мы использовать что-то из тусовки Seg или что-то из тусовки AB. Тут мне пришлось отдельной проверкой бита заблокировать использование Seg в случае, если у нас MOV PC, A😛, потому что иначе, если мы Seg по какой-то причине прогрузили перед этим, произойдёт подмена A на Seg. А такая команда была бы абсолютно тавтологична четвёртому вызову SCN 0 и совершенно не полезна, а только усложнит возврат по вычисленному адресу (что, как я уже говорил, в отсутствие Call/Ret является почти абсолютно необходимым). Уже привычным (надеюсь!) образом я взял один ключевой бит, отличающий одну ситуацию от другой. Любые «третьи, четвёртые и прочие» ситуации не рассматриваются по той же причине — когда результат не будет далее выбран, он может быть любым. Следующим, точно так же — одним битом — мы не менее лихо отличаем B от A. На случай, если нам это вообще понадобится, конечно. Не просто так у нас старший бит операнда равен 1 для перехода по адресу из A как в случае Jcc A😛, так и в случае MOV PC A😛 ! А вот на случай, если нам понадобится правильно задать другой вход в нашем первом ветвлении — мы сейчас и его вычислим. Он зависит от того, SCN у нас или что-то иное. Если SCN — у нас «Seg и значение операнда» (для обработки, естественно, безусловного перехода по четвёртому вызову SCN подряд. Если у нас он не четвёртый — опять же, считай не считай, использоваться это не будет. Пожалуй, мне надо перестать повторяться — это настолько базовый принцип, что он будет встречаться практически в каждой строчке симуляции). Если не SCN — то «Seg и один старший бит операнда», чтобы хотя бы немного расширить диапазон коротких быстрых переходов по Jcc.
Дальше мы можем видеть, как через наши «мухи» идёт уже сам сигнал. Сначала один из них определяется с тем, какой вариант Seg с операндом выбрать — шестибитный или четырёхбитный, то есть требуемый для четвёртого SCN или для Jcc «со взведённым Seg». Затем другой определяется с тем, какой вариант выбрать между A и B (A, которое для Jcc и для MOV PC, A😛, или B, которое для Jcc). И, наконец, последний (иерархически первый! «Капитан, КАПИТАН Джек Воробей!», — пытается этот мультиплексор меня сейчас поправить), выбирает среди их выходов тот, который соответствует задаче.
И нет, мне не обидно за их напрасный труд, потому что всё это, как правило, нигде не будет в итоге «защёлкнуто» — с точки зрения статистики у нас, как правило, ни та и не другая команда :-D Извините, не удержался.
Ну, и напоследок мы формируем управляющие сигналы, указывающие, нужно ли перезаписывать какие-то другие регистры содержимым аккумулятора и нужно ли крутить туда-сюда суммарный девятибитный регистр-счётчик, образованный из Seg и P. Тут вроде всё просто.
Перейдём теперь к АЛУ, включая нерешённый пока что вопрос: иметь Carry для XOR «ради чего-нибудь» или не портить его, чтобы при помощи «XOR на разницу» быстро прыгать между двумя адресами операндов в SRAM.
Думаю, даже сидящие рядом с читателем кошки уже заметили, что логика АЛУ проста, как кирпич — один вход всегда аккумулятор, выход всегда идёт в аккумулятор (и примкнувшие к нему три флага), а второй вход мы выбираем стандартным выбором операнда (с той только разницей, что мы не можем читать PC, зато можем читать флэшку с юзерским кодом и его данными). Поэтому практически всё сводится, опять же, к пачке «мухов»: мы выбираем, откуда брать второй операнд, откуда брать флаг переноса (из Carry Flag или из Neg Flag), ну и откуда брать результат. Причём взять его «из-под носа АЛУ, прямо со второго входа» тоже вариант — им реализуется команда MOV A, R. Но сначала нам, конечно, надо вообще определиться, в ту ли в принципе группу команд мы в этот раз попали? Названия «Opcode_какие-то-биты» говорят сами за себя. И, конечно, если любой из них «выстреливает», то аккумулятор на этом такте перезаписывать надо. А вот флаги, как видно чуть ниже, мы перезаписываем только в случае, если команда реально что-то делает (старшая четвёрка, без MOV), или таки MOV, но особенный, восстанавливающий A из P, а флаги — из Seg. И рядом сразу же задаём для их «муха» особенный источник (тот самый Seg), если команда ничего не вычисляет (то есть MOV).
Дальше идёт выбор второго операнда — наше «любимое» мозголомное ветвление, но в этот раз от младших бит к старшим, то есть плюс-минус в порядке прохождения сигнала. Число веток не является в этот раз степенью двойки, но тем, кто осилил со мной разобраться в предыдущем ветвлении, не составит труда уже и этот клубок сопоставить с тем, как от каждого бита операнда зависит то, какую шину данных мы должны выбрать. А также заметить то, что среди них нет P! Разумеется, это потому, что мы выбираем операнд именно для АЛУ — а АЛУ работает всегда с A. P — привилегия команды MOV A, R, потому что MOV A, A не имеет вообще никакого смысла (да и надо ведь как-то перекладывать обратно в аккумулятор и флаги значения из P и Seg), а вот все остальные команды имеют какой-то смысл, даже если их совершать над аккумулятором и аккумулятором же.
Само АЛУ я описал препохабнейше. Просто как набор отдельных операций на Си. Тут ситуация как со SRAM — надо или здесь и сейчас развивать свою «феню», или описывать каждый логический элемент, сделав это месиво окончательно нечитаемым, или просто на словах объяснить, что там должен быть библиотечный сумматор из шести таких вот разрядов:
Классический «nine-NAND» из Википедии.
Как нетрудно видеть, там есть и NAND (с него всё начинается), и XOR (первая четвёрка), и ADD (всё вместе). Вопрос только в том, откуда это всё с него каким-то дополнительным проводком вытянуть. Эти проводки, включая выходной, носят довольно очевидные имена «ALU_Out-что-то-там» и поступают, как нетрудно догадаться, на вход схемы выбора результата (вместе со вторым операндом и отдельно специально приехавшим туда P, который, как мы видели выше, выбирается по слегка другой логике, нежели второй операнд). Схема эта традиционно многоэтажна, обла, озорна, огромна, стозевна и лаяй, а ещё у неё есть несколько промежуточных выходов: после того, как мы выбрали, скажем, XOR у нас или NAND, мы можем отправить этот результат на дальнейшие туры конкурса, а пока от него тихо в сторонке посчитать наш «липовый Carry для быстрой проверки значения бита»: равны все биты логического результата единице или нет?
Дальше мы из двух вариантов (настоящий арифметический и липовый логический) выбираем значение для будущего Carry Flag, значение для Zero Flag выбирать не из чего — оно всегда определяется вопросами равенства результата нулю, буде у нас вообще регистр флагов в принципе записан по итогам (не считая восстановления его через MOV A, P), Neg Flag бывает в единичке только тогда, когда операция была конкретно NAND A, A, ну а дальше у нас выставляется особенная линия, требующая от нас синтезировать особо головоломный регистр под эти самые флаги — линия «не трожь Carry», которая позволяет сделать для его записи исключение, даже когда пошла команда писать весь регистр флагов (альтернативное решение — сделать отдельные линии для команд записи Carry и всего остального, разумеется). Линия эта активна как минимум в той ситуации, когда мы выполнили этот самый NAND A, A, потому что нам надо в этом случае флаг переноса обязательно сохранить — без этого никакая относительно быстрая арифметика не заработает, как мы увидим сейчас, разбирая микрокод к этой машинке. А ещё можете на свой вкус добавить туда второй вариант активации этой линии — в случае, если команда была XOR, а не NAND. Этой дилеммой мы тоже озадачимся при разборе микрокода поподробнее.
Завершает главную секцию выбор того, надо ли нам записать флаги после операции или флаги, взятые из Seg при их восстановлении. Это тривиально.
Тут у нас находится крошечная секция того, что мы должны сделать по заднему фронту (сиречь спаду) синхроимпульса: включить, если нужно, мощные «пишущие транзисторы» SRAM. Ирония ситуации в том, что положение этой строчки совершенно не означает, что вся орава, которую мы только что разбирали в основной секции, должна завершиться к этому моменту: в конце концов, это только половина интервала тактирования, и там половина этих процессов ещё идёт. Впрочем, я это уже говорил в самом начале. Да и настоящей библиотеки у нас нет, поэтому мы просто присваиваем значение элементу массива. Чик — и записали. Магия.
Ну, и за ней — секция того, что индифферентно к переднему фронту. Она пуста: такого у нас просто нет. Всё или должно закончиться до него (простынки выше), или триггерится непосредственно им (к этому мы перейдём сейчас, потому что описание этого всего как раз здесь-то и начинается…)
Ну, логирование в stdout явно не относится к защёлкам и счётчикам — а вот остальное вполне даже да. В самом начале, будь у нас настоящая библиотека с настоящей SRAM, нам надо было бы подать команду на отключение транзисторов записи (и они ещё должны успеть отключиться раньше, чем какое-нибудь изменение какого-нибудь регистра тут пролетит через все цепи и изменит, скажем, адрес или данные на входе; но, во-первых, обычно библиотеки проектируют с такими параметрами цепей, чтобы оно более-менее без геморроя стыковалось, а во-вторых — никакой регистр, способный на это повлиять, не может измениться на том же такте, на котором память записывалась, потому что это разные операции).
Дальше мы видим, как простейший двухбитный счётчик со сбросом, который я почему-то поленился нормально описать выше, инкрементируется для случаев, когда команда была SCN, и сбрасывается во всех остальных случаях. Счёт у него «гвоздями прибит», а вот Reset — его единственный управляющий сигнал. А вот PC уже немножко посложнее: счёт так же «прибит гвоздями», но по специальному управляющему сигналу он может читать входное значение вместо того, чтобы инкрементировать текущее.
Ещё сложнее у нас парочка P и Seg: мало того, что этот 9-битный счётчик является управляемым и без единички на управляющем входе «не тикает», так он ещё и реверсивен, то есть умеет считать в обратном направлении, если единичка на другом управляющем входе! Ну, и отдельную офигенную круть ему придаёт то, что у него есть раздельные входы для P и Seg, позволяющие читать входное значение для одного и/или другого по отдельности. А чтобы синтезатор не рехнулся понимать, что мы имели в виду — как я уже расписывал в деталях выше, ему надо указать, какие ситуации не нужно вовсе принимать к рассмотрению.
В этом месте я предлагаю подумать и порассуждать в комментариях, а нужен ли нам этот самый декремент? Не лучше ли сделать регистры-дубликаты (что по числу транзисторов может быть где-то порядка этого самого свойства реверсируемости), а третий вариант операнда для работы с памятью (шестой операнд в абсолютных величинах) превратить в «выполни доступ к памяти и свопни регистры друг с другом»? Это может сильно ускорить многобайтовые операции над двумя операндами. Даже больше, лучше и удобнее, чем это вот «убрать запись CF по XOR и использовать XOR для прыжков между двумя значениями P». И как именно, кстати, сделать эту перестановку, чтобы она не мешала автоинкременту (типичному для таких операций, как мы увидим уже практически прямо сейчас)?
Ну, и завершает секцию запись, если оно было нужно, регистров A, B, H, L, ну и флагов — как полностью, так и с исключением для Carry, буде тот требовалось сохранить.
…и пятисекундка логирования. Значения регистров, при необходимости — дамп памяти, а если его заказали для команды SCN 0 — симулятор завершаетсяот удивления. Всё. Выдыхаем, наливаем чашечку чаю и переходим к тестовому микрокоду. Вот он, в файле uCode.h, спрятан под спойлером в полном виде (как был спрятан и полный исходник). А почему? А потому, что там стотыщмильонов строк нулей. Если не собираетесь компилировать — спойлер можно и не открывать. Лучше не открывать.
Скрытый текст
Начинается он с камментов на буржуинском (я ещё надеюсь довести его до достаточного для itch.io уровня приличия, чтобы выложить в качестве «игрушечного», но не советую кому-то надеяться вместе со мной, особенно с учётом того, что мы ещё не определились с теми самыми дилеммами), после чего можно посмотреть сам «код». Приступим же.
Сначала, конечно, проверяем работу нашей самой задорной команды. Делаем безусловный переход аж на 3851-ю строку. Там нас ждёт просто переход обратно:
Это как раз та причина, по которой под спойлер лучше без нужды не заглядывать. Я, конечно, мог бы этого избежать, но при современном весе веб-страниц в целом и любой страницы Хабра в частности не вижу в этом смысла — подумаешь, почти 4 тысячи строк из одних нулей. Килобайты. Зато на случай всякого… всякого всякого без всяких внешних ресурсов всё, необходимое для сборки проекта, лежит неотъемлемо от статьи. Менять же тестовые адреса «на что-то попроще» тоже не хочу — длинный прыжок важен, а мы будем ещё наверняка что-то в процессоре менять и тестировать его заново.
Тут мы развлекаемся с разными командами и напоследок вычисляем адрес в виде A😛 (регистр P, правда, задаём напрямую), причём зачем-то перед этим задали и Seg тоже, хотя для перехода это совсем не нужно…
…а потому, что там, за туманами и простынёй нулей, мы проверяем «восстановление» флагов из Seg. А потом — делаем скок обратно.
Проверяем корректную работу Jcc, не во всех вариантах, конечно, но хотя бы в самых важных. Ставим команду SCN 0, больше для симулятора, чем для проца (состоит из одного только префикса DUMP_SRAM) — она выполняет остановку симуляции, и перепрыгиваем её. А потом нам надо проверить, как работает подмена A/B на Seg…
…что опять потребовало командировку к чёрту на рога и обратно. Теперь давайте о серьёзных вещах.
В пользовательской флэш-памяти лежит аж восьмибайтное число (точнее, 48-битное, как у добротного DSP — байты-то у нас так себе, не уродились, как репа в плохой год). Сейчас мы его будем читать в SRAM по-человечески и задом наперёд, чтобы получить из него два разных числа.
Никакой экономии операций я тут не пытался делать, просто «в лоб» прогрузил H, L, единичку в B и адрес назначения в Seg😛. И просто, спокойно, отматывая декрементом адрес назначения назад, а адрес источника проматывая простым сложением вперёд, вычитал спокойненько все байты. Естественно, адрес назначения грузил соответствующий последнему байту — он же отматывается назад!
Теперь, в том же сегменте, но на небольшом расстоянии, снова выставил адрес назначения — уже на первый байт, после чего снова прочитал то же число, но уже проматывая оба адреса вперёд. Переходим к сладкому: используя Intel Byte Order, вычитаем одно из другого. Со сложением всё просто — бери да складывай. А как вычитать?
…а для вычитания мы используем дополнительный код. Для этого все 8 байт вычитаемого нужно инвертировать побитно, прибавить единичку и побайтно сложить с переносом с уменьшаемым. Причём крайне желательно не пихать куда-то в память ещё и промежуточный (инвертированный) результат — её и так мало!
На помощь, конечно, приходит команда NAND_A A и её отдельный флаг. Для того, чтобы у нас добавилась строго единичка, мы после инвертирования первого байта используем для сложения команду ADN. Флаг «результат был инвертирован», он же флаг Neg («инверсия с целью получения отрицательного числа в дополнительном коде», да-да!) для того и сделан нами. Единичка прибавляется к своему законному результату, и он становится первым байтом разности. Перейдя к следующему, мы снова делаем NAND_A A, но настоящий-то флаг Carry он не трогает! Поэтому ADC прекрасно выполняет обработку следующего байта, уже с совершенно настоящим переносом. И так далее. Один флаг, а уже сразу аппаратная поддержка вычитания!
Ну, и в конце уже не перепрыгиваем остановку симулятора, а честно выполняем.
А вот с адресацией у нас тут полное болото. Ну то есть, в принципе, для микрокода это может быть не так уж и страшно: есть фиксированные места в SRAM, в которых лежат регистры целевого процессора, и можно сделать примерно как в этом примере, путём копирования в нужное место и там уже сложения/вычитания. Да оно ещё и повторяется в каждом сегменте, то есть можно задать Seg до начала и дальше всё тот же код выполнит сложение/вычитание для тех переменных, которые в этом сегменте лежат на предназначенных для сложений и вычитаний местах. Но надо ли говорить, что это уже не минималистичный процессор, а какой-то почти эзотерический? Это, конечно, нормально «в Tiny-исполнении», когда задача в основном время показывать и барабан стиралки крутить, а память скукожилась обратно в 64 байта, включая порты, и никаких инкрементов-декрементов вовсе нет. Но «в Mega-исполнении» же дичь?
Итак, я жду в комментариях оценки следующих вещей:
1) Какое применение можно придумать для флага «результат из одних единиц» (он же — «фальшивый Carry») для команды XOR (с NAND всё понятно — быстрая проверка наличия единицы в некоем бите). Надо ли вообще за него держаться, или просто сделать KeepCarry для XOR тоже?
2) А поможет ли это? Ведь даже если мы положим XOR от обоих адресов в B и будем регулярно брать адрес из P, получать из него то один, то другой при помощи XOR его с B (господи, да оно вообще сработает ли хотя бы математически, или это мне мерещится?), класть его обратно — флаги ведь последуют вместе с ним? Не придётся ли для этого ещё и отказываться от сохранения флагов в Seg?
3) А не придётся ли отказываться от сохранения флагов в Seg в любом случае? Не покажут ли первые полчаса попыток играться с этим процом, что они больше вредят, чем помогают? А если помогают, стоят ли они потраченных на них «мухов»?
4) А не лучше ли сделать, действительно, теневую пару со вторыми Seg😛 и переключать по необходимости после доступа к памяти, вместо этого дурацкого декремента?
5) А как при этом инкрементировать, чтобы перейти к следующему байту операнда?
Попробуйте представить себе, как выглядел бы код, если изменить работу команд тем или иным образом, и написать его, а потом — прокомментировать. На этом я прощаюсь с вами, приятной вам сборки обратно разорванных над этим материалом мозгов, до новых встреч ^_______^
А получилось у нас что-то, вставшее на скользкий муть… то есть путь обратимого разбухания, то есть в базовом варианте оно несколько толще и умеет гораздо больше, но легко возвращается к изначальному микро-варианту. Урезается адресация, ставшие лишними линии заменяются константами и вжух — оно снова скукожилось примерно в то же самое, что мы видели в первой статье. В конце концов, наш любимый AVR тоже горазд варьироваться от «одни регистры поверх голого скелета» до «на этой дурище пека сделать можно».
Итак, таблица всех возможных опкодов с операндами (несмотря на то, что значения бит у сегмента и смещения в моём случае никогда не «нахлёстываются», как в 8086, то есть параграфов у меня не существует, я всё равно использую нотацию Segment:Offset для простоты):
Извините, что картинкой — бороться с этим «етитьором» я не готов.
SCN — Гермиона за лето почти не изменилась, потому что больше уже некуда. Наш любимый, самый базовый код, работающий по вот этому вот упоротому принципу комбо (интересно, где-то вообще такое исторически встречалось или я первый такой отбитый?) Первые два вызова кладут операнд в старшие три бита регистра P (а бывшие старшие смещаются вправо на три), третий вызов кладётся в регистр сегмента (при этом он «взводится», что и есть наше небольшое новшество), а четвёртый — вкупе со «взведённым» сегментом образует, как и в первой статье, старший байт регистра PC (Program Counter), совершая таким образом безусловный переход по двухбайтному адресу. Но и не только: если в прежнем варианте после третьего вызова был возможен только четвёртый (иначе Seg сбрасывался, делая третий вызов бессмысленным в принципе), то сейчас мне стало как-то жалко эти три бита опкода в четвёртом вызове (которые не делали абсолютно ничего, будучи очевидно-тавтологичными), и я добавил изрядную кучку вариантов использования свеже-«взведённого» регистра сегмента. Вообще, конечно, наличие таких «комбо-команд» говорит в пользу того, что у нас всё-таки микрокод, а «настоящий» код — в пользовательской флэшке. А не нативный код для VM, байт-код для которой — в пользовательской флэшке.
MOV ?, A — начались фокусы и фичи. Поскольку регистр флагов тоже дорос до третьего бита, я решил не бросать на ветер новую возможность и окончательно соединил концептуально флаги и A, а также сегмент и P в без двух минут монолитные девятибитные регистры. Короче, MOV P, A теперь девятибитная операция — флаги при этом сохраняются в регистр сегмента. Ибо нефиг ему пропадать и использоваться только как крошечная «прокладка» между третьим вызовом SCN и следующим опкодом. Да, теперь можно флаги сохранить и восстановить ^___^
Дальше пока всё как и было: MOV B, A, MOV L, A и MOV H, A остались теми же. А вот MOV SRAM[?], A изменилась до неузнаваемости. Во-первых, я плюнул и слил адресное пространство SRAM и регистров управления портами в единое целое. Сколько нужно выделить под порты — столько и выделяем. Где нужно — там и выделяем. Во-вторых — это пространство я увеличил ввосьмеро (512 байт, ну рехнуться как много), то есть мы теперь используем для адресации наш суммарный девятибитный регистр Seg😛; регистру Seg не обязательно быть «взведённым», то есть каждый раз перед обращением к памяти читать мантру из трёх вызовов SCN не нужно. Более того, он теперь регистр-счётчик, да ещё и реверсивный (мне показалось, что выигрыш в транзисторах масочного ПЗУ с микрокодом перевесит потери на реализацию счётчика) — то есть мы можем высвободившиеся опкоды, которые раньше отвечали за доступ к адресным пространствам портов, использовать для операций доступа с пост-инкрементом и пост-декрементом указателя (чем я на тестах сразу же радостно воспользовался для чтения длиннющих 48-битных констант). И, разумеется, если нам нужно реализовать управление каким-нибудь силовым драйвером и весь этот «тюнинг в зоопарке» нам не нужен — всё это легко откатывается обратно, просто в данной версии кристалла вместо Seg туда заводятся старые добрые константы начала адресного пространства оперативки, портов и направлений портов, а регистр-счётчик превращается обратно в обычную «защёлку». И да, даже в полной версии, пока мы работаем в рамках одного сегмента, для задания адреса следующей переменной в оперативке достаточно только двух вызовов SCN, третий нужен только в случае, если она в другом сегменте — не забываем, что Seg должен быть «взведён» только в узком диапазоне применений, а для остальных — один раз задали и гоняем туда-сюда P.
С операцией MOV PC, A😛 (то есть JMP A😛, если человеческими словами) я немного поигрался и вернул всё, как было. Учитывая, что у нас в принципе нет аппаратной поддержки подпрограмм — возможность передать вместе с параметрами адрес возврата перед переходом, а потом его загрузить в A😛 и выполнить эту команду, в общем-то, необходимейшая вещь. Она позволяет реализовать подпрограммы хотя бы «вручную», а без этого написание микрокода для любого вменяемого проца (кода для виртуалки, реализующей минималистичный токенизированный Бейсик, если угодно) быстро превратится в ад ветвлений. В конце концов, нам потребуется сложение и вычитание. И умножение с делением, которые дёргают сложение и вычитание. И куча более сложных операций, которые дёргают по очереди одно и другое…
А вот с командой Jcc я, конечно, порезвился. Ну, что я бит операнда поменял и теперь сначала Jcc B😛 идут, а потом уже Jcc A😛 — это мелочи, это я просто оптимизировал отдельные транзюки в управлении выбором (в камментах кода симулятора это есть). А вот что «взведённый» Seg подменяет собой что A, что B — это уже поинтереснее зверь! Переходы по коротким адресам из начала памяти (первые 1024 байта, потому что три бита задаём через Seg и ещё один — через то, A или B он подменит собой) теперь можно делать тупо через SCN-SCN-SCN-Jcc (а можно и не делать, если мы резвимся в пределах сегмента и как выставили B, так его и не трогаем — все хреновшества срабатывают только при «взведённом» Seg, то есть только если буквально предыдущей командой был третий вызов SCN). Ну а в это самое начало мы, конечно, сложим все наши библиотечные функции, которые постоянно дёргать надо…
Была мысль заменить A😛 в правой половине таблицы на H:L, чтобы не перегружать аккумулятор функционалом (да и обычно когда получены флаги, он занят результатом, а не адресом), но потребное количество «мухов» (мультиплексоров) явно бьёт всю экономию на ручном перекидывании H:L в A😛. Перезапись L→A→P, конечно, Seg флагами перезапишет — но это по понятной причине не считается за его «взведение», по смыслу флаги совсем не адрес и такая операция (в отличие от третьего SCN) этот механизм не активирует, да и на Jcc не повлияет, потому что оригиналы флагов не разрушаются. Плюс, в отличие от «мухов», ручное перекидывание более универсально в плане источника данных. Есть тот момент, когда в вопросах попухания устройства надо остановиться. Да и, в отличие от «продвинутой адресации», это уже не заменяется обратно на проволочки в случае tiny-версии (выражаясь в терминах AVR).
Ну, и дальше снова наша «великолепная пятёрка», которая пишет аккумулятор. MOV A, ? изменилась не очень сильно: теперь MOV A, P заодно восстанавливает флаги, ранее сохранённые в наш сильно разросшийся по функционалу регистр сегмента (интересно, кто-нибудь сможет как-нибудь особо извращённо задавать сегмент, скормив ему комбинацию из флагов?), ну и адресация доступа к SRAM стала такая же, как и в MOV ?, A — она везде теперь такая.
Дальше начались перестановки: XOR A, R и NAND A, R теперь идут подряд. XOR затронули (почти!) только новшества с адресацией, а вот NAND — зверь новый, знакомый нам ранее только по комментариям к первой статье. Что XOR, что NAND доступны в обычном сумматоре типа «nine-NAND» в его левой половинке, до того, как у нас припожалует вальяжный сигнал переноса (и неспешно поползёт дальше, ибо СУПом не кормлен). То есть мы просто выводим их на правах промежуточных результатов, ну и тупо выбираем при помощи «мухов», что именно нас интересует — а сумматор всегда получает на вход всё и работает в более-менее одинаковом режиме (с поправкой на то, что его операнды тоже проходят «мухов»). Ну, и, помимо этого, NAND полезен тем, что (в отличие от AND) он имеет смысл для операндов A, A — он просто инвертирует A при этом!
Как мы это используем? Начнём с недостатков. Если AND A, NOR[H:L] позволял быстро посмотреть при помощи маски в пользовательском коде (который во флэшке лежит), взведён ли какой-то бит в A (при помощи итогового флага ZF), то сейчас мне пришлось заставить обе логические команды модифицировать и CF тоже. Поскольку у побитной логики никакого настоящего переноса в принципе быть не может, я взвожу CF в случае, если результат равен 111111. Сами понимаете, что теперь снова можно точно так же легко проверить любой бит в A 🙂
Возможно, впрочем, что насчёт XOR я погорячился. Проверять его результат на «все единицы» не столь полезно, а вот сохранять CF может быть нужно для того, чтобы быстро «прыгать» между указателями на операнды, делая им «XOR на разницу». В тестовом коде мы ещё вернёмся к этому…
Что же касается достоинств, то тут как раз вопрос о том, почему регистр флагов тоже разросся до трёхбитного: в нём появился флаг NF, сиречь Neg Flag. Он взводится в одном-единственном случае: если NAND был вызван для A, A (CF, кстати, мы при этом не трогаем. Пусть хоть ноль получится, хоть 111111, но он сохраняет старое значение). Во всех остальных случаях (если команда в принципе модифицирует флаги) он сбрасывается. А поскольку для получения отрицательного числа в допкоде нам нужно получить его инверсию плюс единичка — назначение этого флага довольно очевидно, он будет использован в качестве этой самой «плюс единички».
Ну, и в самом конце теперь две обычные арифметические команды: ADD и ADC. Точнее, чистого ADD теперь и вовсе не существует — вместо него теперь ADN, то есть по сути тот же ADC, но вместо CF на сумматор отдельный «мух» закидывает NF. Что в основном не меняет картину, потому что NF у нас в общем случае ноль — но только не тогда, когда мы только что инвертировали вытащенный из SRAM младший байт какой-то переменной! В этом случае NF взведён и к «сумме» будет добавлена та самая необходимая для допкода единичка, образуя разность. Ну, а дальше мы просто используем обычные ADC для остальных байт, не забывая инвертировать вычитаемое (именно поэтому мы не трогаем CF при NAND A, A — он у нас реальный, живой и нужный, с прошлого разряда). Итого фактически у нас есть как многобайтовое сложение, так и многобайтовое вычитание прямо в SRAM, без лишних телодвижений и хранения промежуточных результатов. Вообще, конечно, наличие таких «толстых» команд говорит в пользу того, что у нас всё-таки нативный код для VM, байт-код для которой — в пользовательской флэшке. А не микрокод, а «настоящий» код — в пользовательской флэшке.
Чёрт. Эта загадка будет вечной.
Итак, первая подспойлерная простыня кода: сам эмулятор. Сначала цепляю целиком, чтобы было относительно удобно скопипастить и собрать (думаю, чем угодно собрать можно; но, как разлившийся спирт, его точно можно собрать «ваткой»; надеюсь, однако, что не воткнул в статью ссылку на какой-нибудь новодельный фейковый репозиторий!)
Скрытый текст
Код:
#include <iostream.h>
#include <stdlib.h>
#define BOOL int
#define OPCODE 56 //bin. 111000
#define OPERAND 7 //bin. 000111
//Opcodes
#define SCN 0+ //The most special opcode, sets a constant nibble (destination depends on the number of calls).
#define MOV_R_A 8 //Move from accumulator. C defines does not match the actual mnemonics, sorry.
#define JCC 16 //Conditional jump
//All other opcodes set the accumulator and somewhat match the actual mnemonics (at least, MOV_A B is similar to the actual MOV A, B mnemonic).
#define MOV_A 24 //Does not modify flags unless used to restore them from Seg:P
#define XOR_A 32
#define NAND_A 40 //NAND (sets Neg flag if used for Accumulator inversion)
#define ADN_A 48 //Adds with Negative flag
#define ADC_A 56 //Adds with Carry flag
//For all opcodes except SCN and JCC (which use their very own operands)
#define A +0 //Accumulator...
#define P +0 //...or P register, that depends on a command
#define B +1
#define L +2
#define H +3
#define SRAM_P +4 //SRAM[Seg:P], where port control registers are mapped to SRAM memory area
#define SRAM_PPP +5 //SRAM[Seg:P++]
#define SRAM_PMM +6 //SRAM[Seg:P--]
#define FLROM_HL +7 //FLASH ROM for user's firmware (read-only, interpreting-only), if reading...
#define PC_JUMP +7 //...Program Counter (A. K. A. Jump) if writing.
//Operands for the Jcc opcode:
#define Z_B +0 //Jz B:P (unless B is overriden by setting Seg:P)
#define NZ_B +1 //Jnz B:P
#define C_B +2 //Jc B:P
#define NC_B +3 //Jnc B:P
#define Z_A +4 //Jz A:P (unless A is overriden by setting Seg:P)
#define NZ_A +5 //Jnz A:P
#define C_A +6 //Jc A:P
#define NC_A +7 //Jnc A:P
#define CCMASK 2
//Emulator prefixes:
#define DUMP_SRAM +64 //Totally impossible to have on a real hardware due to it's 6-bit nature. Use with opcode 0 operand 0 to terminate the emulator.
//6-bit and 3-bit registers are emulated via 8-bit variables
static unsigned char SRAM [8*64]; //Data
static unsigned char ROM [64*64] = { //Microcode, or the interpreter (dat's philosofik questn!)
#include "uCode.h" }; //I didn't tell you it's OK to work with C this way!
static unsigned char FLASHROM [64*64] = { //User's code (the actual code, or the script, dat's da same questn!)
#include "UserCode.h" }; //Ooooops. I did it again %)
static unsigned char Reg_A, Reg_Fl, Reg_B, Reg_L, Reg_H, Reg_P, Reg_Seg; //Flags with A, and Seg with P are somewhat 9-bit registers
//12-bit registers are emulated via 16-bit variables
static unsigned short PC;
//Inner things are just int.
int NibbleCounter_2bit;
//This is and example of my very own "VHDL-dialect of C language".
//It can be compiled by, for example, OpenWatcom 1.9 -- but it's still a HDL, so it can be directly synthesized without any "smart C-to-HDL converters".
#define Invalid(X) exit(X)
#define ClockRisingEdgeBegin //Means literally nothing for this C simulation. Needed only for the actual HDL synth.
#define ClockRisingEdgeEnd
#define ClockFallingEdgeBegin
#define ClockFallingEdgeEnd
void main (void)
{
//0..2 opcode family
volatile unsigned char ROM_Data_Bus; //surplus "volatile" is harmless in C, but tells the synthesizer it's a signal line, not a latch, so any attempts to make it hold any data must cause a syntax error.
volatile unsigned short ROM_Addr_Bus;
volatile unsigned char SRAM_In_Bus, SRAM_Out_Bus;
volatile unsigned short SRAM_Addr_Bus;
volatile BOOL Opcode_SCN, Opcode_Jcc, Opcode_MOV_from_A;
volatile BOOL Writing_P, Writing_P_From_Operand, Writing_P_From_A, P_DataMuxN;
volatile unsigned char P_DataMUX, P_DataMUX0, P_DataMUX1;
volatile BOOL Writing_Seg, Writing_Seg_From_Operand, Cnt_SCN_MSB_Still_Matters;
volatile unsigned char Seg_DataMUX, Seg_DataMUX0, Seg_DataMUX1;
volatile BOOL Seg_Is_Prepared; //Affects a lot of commands
volatile BOOL Writing_PC, Writing_PC_By_SCN, Writing_PC_By_MOV, Writing_PC_By_Jcc, Writing_PC_CC, PC_DataMuxNMSB, PC_DataMuxNLSB0, PC_DataMuxNLSB1;
volatile unsigned char PC_Data_L_NoMUX, PC_Data_H_MUXH, PC_Data_H_MUXL0, PC_Data_H_MUXL1, PC_Data_H_MUX0, PC_Data_H_MUX1, PC_Data_H_MUX2, PC_Data_H_MUX3;
volatile BOOL Writing_SRAM;
volatile BOOL Increment_SegP_Pair, Decrement_SegP_Pair;
volatile unsigned char B_L_H_Data_NoMUX;
volatile BOOL Writing_B, Writing_L, Writing_H;
//3..7 opcode family
volatile unsigned char FLASHROM_Data_Bus;
volatile unsigned short FLASHROM_Addr_Bus;
volatile BOOL Writing_A, Writing_Fl, KeepCarry, Opcode_x11, Opcode_1xx, Operand_0, Flags_DataMuxN;
volatile unsigned char Flags_DataMUX, Flags_DataMUX0, Flags_DataMUX1;
volatile BOOL Operand_MuxLSBN, Operand_MuxMidLN, Operand_MuxMidHN, Operand_MuxMSBN;
volatile unsigned char Operand_MUX_LSB_L, Operand_MUX_LSB_H, Operand_MUX_Mid_L, Operand_MUX_Mid_H, Operand_MUX_MSB;
volatile unsigned char Operand_MUX_LSB_L_0, Operand_MUX_LSB_L_1, Operand_MUX_LSB_H_2, Operand_MUX_LSB_H_3, Operand_MUX_Mid_H_0, Operand_MUX_Mid_H_1;
volatile BOOL CarryIn_MUX, CarryIn_MUX0, CarryIn_MUX1, CarryIn_MuxN;
volatile unsigned char ALU_In_2, ALU_OutSum, ALU_OutXOR, ALU_OutNAND;
volatile BOOL CarryOut, AllAre1, AllAre0, CarryOut_MUX, CarryOut_MuxN, NegOperation;
volatile BOOL A_DataMuxMSBN, A_DataMuxMidHN, A_DataMuxMidLN, A_DataMuxLSBN;
volatile unsigned char A_DataMUX_LSB0, A_DataMUX_LSB1, A_DataMUX_LSB; //XOR vs NAND result selection
volatile unsigned char A_DataMUX_Mid_H1, A_DataMUX_Mid_H, A_DataMUX_Mid_L0, A_DataMUX_Mid_L1, A_DataMUX_Mid_L, A_DataMUX_MSB;
for (BOOL Reset=1; ;Reset=0) //Infinite clock loop
{
//Signal definitions (and some time for them to settle). Values can't be used backwards, changed, used from the prev. loop etc (for the obvious reasons).
//C compiler will not warn you, but synthesizer must stop with a syntax error if you do.
//These signals are connected to latch/CMOS array outputs directly and always present on the corresponding inputs. Their lag are equal to the lag of those devices.
ROM_Addr_Bus = PC;
ROM_Data_Bus = ROM[ROM_Addr_Bus];
SRAM_Addr_Bus = Reg_Seg<<6|Reg_P;
SRAM_In_Bus = Reg_A;
SRAM_Out_Bus = SRAM[SRAM_Addr_Bus];
P_DataMUX0 = (((Reg_P)&63) >> 3 ) | ((ROM_Data_Bus&OPERAND)<<3); //Looks a bit complicated, but it's just a wire combination.
P_DataMUX1 = Reg_A;
Seg_DataMUX0 = ROM_Data_Bus&OPERAND;
Seg_DataMUX1 = Reg_Fl;
PC_Data_L_NoMUX = Reg_P; //Lower 6-bit byte of PC can be set only via P.
PC_Data_H_MUX0 = Reg_A; //Higher can be set either with A...
PC_Data_H_MUX1 = Reg_B; //...or with B, both of them support jumps to a calculated address...
PC_Data_H_MUX2 = (ROM_Data_Bus&4 )<<1 | Reg_Seg; //...with Seg, 4 bits only! For short jumps (can be conditional)...
PC_Data_H_MUX3 = (ROM_Data_Bus&OPERAND)<<3 | Reg_Seg; //...with Seg + operand, long (but only unconditional) jumps via 4th call of SCN.
B_L_H_Data_NoMUX = Reg_A; //Exactly the same physical wires as SRAM_In_Bus, P_DataMUX1 etc!
FLASHROM_Addr_Bus = Reg_H<<6|Reg_L;
FLASHROM_Data_Bus = FLASHROM[FLASHROM_Addr_Bus]; //At last, the most importaint part -- reading the actual user's byte code for our VM.
Flags_DataMUX1 = Reg_Seg; //Restoring saved flags
Operand_MUX_LSB_L_0 = Reg_A; //Exactly the same physical wires...
Operand_MUX_LSB_L_1 = Reg_B;
Operand_MUX_LSB_H_2 = Reg_L;
Operand_MUX_LSB_H_3 = Reg_H; //One of those four will be selected as Operand_MUX_Mid_L output later. These are operands 0..3
Operand_MUX_Mid_H_0 = SRAM_Out_Bus; //operands 4..6
Operand_MUX_Mid_H_1 = FLASHROM_Data_Bus; //operand 7
CarryIn_MUX0 = (Reg_Fl&4)>>2; //Neg flag
CarryIn_MUX1 = (Reg_Fl&2)>>1; //Carry flag
ALU_In_2 = Reg_A; //Yep that bus again :)
A_DataMUX_Mid_L1 = Reg_P;
if (Reset) //Initial latch states
{
//ToDo: explicitly define some "Reset Safety". But not too much! Everything costs it's logic gates.
ClockFallingEdgeBegin
ClockFallingEdgeEnd
ClockRisingEdgeBegin
ClockRisingEdgeEnd
PC=0;
Reg_A=0;
cout<<"PC(b4) Opcode Operand A Fl B L H P Seg PC"<<endl;
} else {
//These signals come from logic gates, so their lag is added to their sources' lag.
//There are few signals which must finish all their lags before the clock falling edge
Opcode_MOV_from_A = ((ROM_Data_Bus&OPCODE) == MOV_R_A);
Writing_SRAM = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)!=7 ) && ( (ROM_Data_Bus&4) ); //MSB of operand is set (SRAM/PC group of operands), but other two are not 11 (111=PC)
ClockFallingEdgeBegin //For the actual timing calculations. Places the earliest time of the falling edge.
//All other signals are independent from the falling edge and must finish all their lags before the rising edge, so the actual falling edge can be at any position from Beg to End.
//That means, each line in this section can have the actual time of "settled" signals (all lags finished) either before or after the falling edge.
//Processing the P register (which can be affected by SCN or MOV P, A)
Opcode_SCN = ((ROM_Data_Bus&OPCODE) == 0); //SCN
Opcode_Jcc = ((ROM_Data_Bus&OPCODE) == JCC);
Writing_P_From_Operand = Opcode_SCN && !(NibbleCounter_2bit&0x02); //Two first calls of SCN (in a row!) makes the opcode go to the P register.
Writing_P_From_A = Opcode_MOV_from_A && !(ROM_Data_Bus&OPERAND); //MOV P, A makes A go to the P register (as well as flags goin' to Seg -- see below about this).
Writing_P = Writing_P_From_Operand || Writing_P_From_A; //We must latch a new value in P in either case.
P_DataMuxN = (ROM_Data_Bus>>3)&1; //LSB of the opcode is enough to distinguish 3rd (or any) SCN from MOV P, A
if (P_DataMuxN) P_DataMUX = P_DataMUX1; else P_DataMUX = P_DataMUX0;
//Processing the Segment register (which can be affected by SCN or MOV P, A (which also stores flags to Seg))
Cnt_SCN_MSB_Still_Matters = Opcode_SCN && (NibbleCounter_2bit&0x02); //Third SCN call (in a row!) makes the opcode go to the Segment.
Writing_Seg_From_Operand = Cnt_SCN_MSB_Still_Matters && !(NibbleCounter_2bit&0x01);
Writing_Seg = Writing_Seg_From_Operand || Writing_P_From_A; //No separate "Writing_Seg_From_A" is needed, we write Seg from Flags if we write P from A.
if (P_DataMuxN) Seg_DataMUX = Seg_DataMUX1; else Seg_DataMUX = Seg_DataMUX0; //Also, no separate Seg_DataMuxN is needed.
//Processing the Program Counter (which can be affected by either 4th SCN (AKA Jump to a const. addr.), MOV PC, A:P (AKA Jump to a calculated addr.), and Jcc.
Seg_Is_Prepared = (NibbleCounter_2bit&0x02) && (NibbleCounter_2bit&0x01); //Having the Seg prepared changes some command addressing modes...
Writing_PC_By_SCN = Cnt_SCN_MSB_Still_Matters && (NibbleCounter_2bit&0x01); //...the first and the main usage -- jumping to a constant address by making the fourth SCN call (in a row!)
Writing_PC_By_MOV = Opcode_MOV_from_A && (ROM_Data_Bus&OPERAND)==7; //Actually, that "==7" is just a big AND(all_3_bits) :)
if (ROM_Data_Bus&CCMASK) Writing_PC_CC = (Reg_Fl>>1)&1; else Writing_PC_CC = Reg_Fl&1; //Selecting which flag to check: Zero (bit 0) or Carry (bit 1).
Writing_PC_By_Jcc = Opcode_Jcc && Writing_PC_CC^(ROM_Data_Bus&1); //True if flag is 1 and is checked to be SET (Jz, Jc) or it's 0 and is checked to be RESET (Jnz, Jnc).
Writing_PC = Writing_PC_By_SCN || Writing_PC_By_MOV || Writing_PC_By_Jcc;
PC_DataMuxNMSB = Seg_Is_Prepared && !(ROM_Data_Bus&8); //PC have a two-stage multiplexed input (and it's also a counter). First stage means selection between Seg/Op|Seg and A/B regs.
PC_DataMuxNLSB0 = (ROM_Data_Bus>>2)&1; //MSB of the Jcc operand exactly means jumping to A:P instead of B:P (which also counts for MOV PC, A because "PC" operand has MSB=1, too). One of them is selected on the second stage unless first stage selects Seg.
PC_DataMuxNLSB1 = Opcode_SCN; //The same (2nd stage if Seg *IS* selected). Allows to distinguish 4th SCN (which uses opcode+Seg for long jump) from MOV PC,A / Jcc.
if (PC_DataMuxNLSB1) PC_Data_H_MUXL1 = PC_Data_H_MUX3; else PC_Data_H_MUXL1 = PC_Data_H_MUX2; //2nd stage, "st1 = 1" branch. Depending on the actual opcode, we select either Seg (for Jcc) or operand<<3+Seg (for 4th SCN which must cause jump).
if (PC_DataMuxNLSB0) PC_Data_H_MUXL0 = PC_Data_H_MUX0; else PC_Data_H_MUXL0 = PC_Data_H_MUX1; //2nd stage, "st1 = 0" branch. Depending on the operand MSB, we select either A or B register.
if (PC_DataMuxNMSB) PC_Data_H_MUXH = PC_Data_H_MUXL1; else PC_Data_H_MUXH = PC_Data_H_MUXL0; //1st stage, which has the top priority for both opcodes. We select the Seg group, if Seg is prepared (unless we MOV), and select the A/B group if it's not.
//...we seem to finish both SCN and Jcc opcodes, let's finish the MOV R, A (for R other than P and PC)
Increment_SegP_Pair = ((ROM_Data_Bus&OPERAND) == 5) && !Opcode_SCN && !Opcode_Jcc;
Decrement_SegP_Pair = ((ROM_Data_Bus&OPERAND) == 6) && !Opcode_SCN && !Opcode_Jcc;
Writing_B = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)==1);
Writing_L = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)==2);
Writing_H = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)==3);
//Let's do the ALU part here. A + 2nd operand (MUX) -> ALU -> A.
//The most basic control lines for both A and Flags.
Opcode_x11 = ((ROM_Data_Bus>>3)&1) && ((ROM_Data_Bus>>3)&2);
Opcode_1xx = ((ROM_Data_Bus>>3)&4) >> 2;
Writing_A = Opcode_x11 || Opcode_1xx;
Operand_0 = (ROM_Data_Bus&OPERAND)==0; //for opcode 100, that means MOV A, P (switches the second operand from A to P)
Writing_Fl = Opcode_1xx || (Opcode_x11 && Operand_0); //MOV can modify flags only if MOV Fl:A Seg:P (which is the same as MOV A, P)
Flags_DataMuxN = !Opcode_1xx /* && Opcode_x11 && Operand_0 */; //Switching to MUX1 which is the Segment register. MOV A, P only.
//1st operand for the ALU is always A. Let's select the second one here!
Operand_MuxLSBN = (ROM_Data_Bus & 1); //for both Low and High branches of bit 0 (selecting A vs B and L vs H);
Operand_MuxMidLN = (ROM_Data_Bus & 2)>>1; //Low branch of bit 1 (selecting A/B vs L/H when MSB is 0);
Operand_MuxMidHN = (ROM_Data_Bus & 1) && (ROM_Data_Bus & 2); //High branch of bit 1 (selecting Flash ROM vs SRAM when MSB is 1);
Operand_MuxMSBN = (ROM_Data_Bus & 4)>>2; //MSB A. K. A. the final selection between the low and high branch outputs.
if (Operand_MuxLSBN) Operand_MUX_LSB_L = Operand_MUX_LSB_L_1; else Operand_MUX_LSB_L = Operand_MUX_LSB_L_0;
if (Operand_MuxLSBN) Operand_MUX_LSB_H = Operand_MUX_LSB_H_3; else Operand_MUX_LSB_H = Operand_MUX_LSB_H_2;
if (Operand_MuxMidLN) Operand_MUX_Mid_L = Operand_MUX_LSB_H; else Operand_MUX_Mid_L = Operand_MUX_LSB_L;
if (Operand_MuxMidHN) Operand_MUX_Mid_H = Operand_MUX_Mid_H_1; else Operand_MUX_Mid_H = Operand_MUX_Mid_H_0;
if (Operand_MuxMSBN) Operand_MUX_MSB = Operand_MUX_Mid_H; else Operand_MUX_MSB = Operand_MUX_Mid_L;
//Carry In is kind of operand, too!
CarryIn_MuxN = ((ROM_Data_Bus>>3) & 1); //ADD with Neg flag vs ADD with Carry; for other cases, Cin value does not matter.
if (CarryIn_MuxN) CarryIn_MUX = CarryIn_MUX1; else CarryIn_MUX = CarryIn_MUX0;
//Now we have all operands prepared and we can emulate the ALU (actually it's a single-mode adder with multiple outputs).
ALU_OutSum = (Operand_MUX_MSB + ALU_In_2 + CarryIn_MUX) & 63;
CarryOut = (Operand_MUX_MSB + ALU_In_2 + CarryIn_MUX) >> 6 & 1;
ALU_OutXOR = Operand_MUX_MSB ^ ALU_In_2; //This is a classic "nineNAND" adder, so we instantly have XOR as an intermediate result.
ALU_OutNAND = ~(Operand_MUX_MSB & ALU_In_2); //This is a classic "nineNAND" adder, so we instantly have NAND on a literally first NAND gate!
//Now let's select the needed result with even more MUX! %)
A_DataMuxLSBN = ((ROM_Data_Bus>>3)&1); //0 selects the XOR result (for the 100 opcode) and 1 selects the NAND/NEG result (for the 101 opcode).
A_DataMuxMidHN = ((ROM_Data_Bus>>4)&1); //High branch of MSB. 1 selects the arithmetic result (110/111 opcode, both from the same output "Sum"), 0 selects bitwise logic one (100/101, depending on LSB).
A_DataMuxMidLN = Operand_0; //Low branch of MSB which means MOV operations. 1 selects P, 0 selects the ALU incoming operand -- completely bypassing the whole ALU.
A_DataMuxMSBN = Opcode_1xx; //1 selects the ALU result, 0 selects either P or the ALU operand -- completely bypassing the whole ALU, as said above (but making MOV A, ? do it's job).
A_DataMUX_LSB0 = ALU_OutXOR; //Yep they are same wires but we can't place them in the generic loop section (b4 reset branch) because it's OK for synth but not OK for the C compiler!
A_DataMUX_LSB1 = ALU_OutNAND; //The same
A_DataMUX_Mid_H1 = ALU_OutSum; //That again
A_DataMUX_Mid_L0 = Operand_MUX_MSB; //Yeah, the same wires (bus) as the ALU operand 1
if (A_DataMuxLSBN) A_DataMUX_LSB = A_DataMUX_LSB1; else A_DataMUX_LSB = A_DataMUX_LSB0; //First of all, select 101 vs 100 opcode (NAND vs XOR).
if (A_DataMuxMidHN) A_DataMUX_Mid_H = A_DataMUX_Mid_H1; else A_DataMUX_Mid_H = A_DataMUX_LSB; //Second, select 110/111 (ADN/ADC) from ALU vs 100/101 from the LSB MUX output.
if (A_DataMuxMidLN) A_DataMUX_Mid_L = A_DataMUX_Mid_L1; else A_DataMUX_Mid_L = A_DataMUX_Mid_L0; //At the same time, for the low branch of MSB (which stands for MOV), we select P (as a substitute of meaningless MOV A, A) or a common operand.
if (A_DataMuxMSBN) A_DataMUX_MSB = A_DataMUX_Mid_H; else A_DataMUX_MSB = A_DataMUX_Mid_L; //the most general selection -- 1 stands for ALU result, 0 stands for simple MOV opcode.
//...and flags -- they are results, too!
//cout<<(int)(CarryOut_MuxN)<<" "<<(int)(A_DataMUX_LSB)<<" "<<(int)(AllAre1)<<endl;
AllAre1 = (A_DataMUX_LSB&63 == 63); //Counts only for bitwise logic. For the ADC/ADN, we have a real Carry flag.
AllAre0 = (A_DataMUX_Mid_H == 0); //Counts for everything except MOV.
CarryOut_MuxN = A_DataMuxMidHN; //Exactly the same wire, because it selects the arithmetic result (vs bitwise logic) and, obviously, must select the real Carry here.
if (CarryOut_MuxN) CarryOut_MUX = CarryOut; else CarryOut_MUX = AllAre1; //For XOR/NAND there are obviously no Carry, so we use "result is 111111" flag instead. This allows to check a needed bit in a flag field very quickly!
NegOperation = ((ROM_Data_Bus&63) == NAND_A A); //The one and the only, Neg A! Consists of NAND A, A (so A = ~A) and setting the Neg flag to 1 (this is the only opcode/operand combinations which does it). So on next ADN, ~A+1 is added -- effectively meaning adding -A.
KeepCarry = NegOperation; //We inventeg the Neg flag literally for this.
Flags_DataMUX0 = AllAre0 | CarryOut_MUX<<1 | NegOperation<<2; //Zero, Carry and Neg flags. We know them, we love them.
if (Flags_DataMuxN) Flags_DataMUX = Flags_DataMUX1; else Flags_DataMUX = Flags_DataMUX0; //For the 1xx opcodes, we select the actual flag values. For the 0xx, we select the Seg which can be used to store them. It'll not be written unless the exact MOV A, P is engaged.
ClockFallingEdgeEnd //For the actual timing calculations. Places the latest time of the falling edge.
//Operations in this sections are triggered exactly by the falling edge of the clock. During actual synthesis, they can happen even BEFORE some operations
//from the PREVIOUS section! Of course, only if they are not depending on their results (which is true in our case because writing SRAM is mutually exclusive with other opcode+operand combinations).
//Oh that pain-in-ass SRAM writing... it's not a "fwoosh and ready" latching operation. It requires some powerful CMOS structures to be turned on,
//rewriting the memory content with brute force. During this, SRAM_Addr_Bus can't be changed (which is true, because both Seg and P stays still until *RISING* edge change them),
//and Writing_SRAM must be true (it is, because PC stays still waiting for the *RISING* edge so ROM data bus does, too), and also SRAM_Out_Bus becomes invalid (it does not matter because all
//the invalid data in other lines it causes will never be written anywhere; it can't disturb control signals, and SRAM-writing opcodes are obviously mutually exclusive with any SRAM-using opcodes).
if (Writing_SRAM) SRAM[SRAM_Addr_Bus] = SRAM_In_Bus; //So we are not "writing an array cell" here; we power the writing lines here.
ClockRisingEdgeBegin //The same for the rising edge. Those lines separate the signal definitions and the final latching their results.
//We have no operations insensitive to the rising edge, so this section is empty: literally everything must happen strictly before it, or strictly after it.
ClockRisingEdgeEnd
//Here we place all latches which change their state on the rising edge. Everything must be finished here. No future use of their values is allowed.
//In-to-out time of all latches is added to the "signal settle time" of the next simulation loop.
cout<<(int)PC<<" ";
//First of all, we must stop the SRAM writing lines here, because rising edge means no more stable signals! New loop begins it's lags, delays and "binary spikes".
//At the same time, latches have their new values.
if (Opcode_SCN) NibbleCounter_2bit++; else NibbleCounter_2bit=0; //A counter with a reset input can be processed this way.
if (Writing_PC) PC = (PC_Data_H_MUXH&63)<<6 | (PC_Data_L_NoMUX&63); else PC++; //Yes, we can synth this by using a counter with a latch option.
if (Writing_P) Reg_P = (P_DataMUX&63); //And even this is possible! It's a reversible counter with separate latch options for lower 6 bits and higher 3 bits.
if (Writing_Seg) Reg_Seg = (Seg_DataMUX&63);
if (!Writing_P && !Writing_Seg)
{
if (Increment_SegP_Pair && !Decrement_SegP_Pair)
{
Reg_Seg = (Reg_Seg<<6|Reg_P)+1 >> 6;
Reg_P = (Reg_Seg<<6|Reg_P)+1 & 63;
}
if (!Increment_SegP_Pair && Decrement_SegP_Pair)
{
Reg_Seg = (Reg_Seg<<6|Reg_P)-1 >> 6;
Reg_P = (Reg_Seg<<6|Reg_P)-1 & 63;
}
if (Increment_SegP_Pair && Decrement_SegP_Pair)
{
Invalid (Reg_Seg); //for the C compiler, it means stop with error. For the synthesizer, it means "don't care about this case and don't waste any gates on it, it's impossible".
Invalid (Reg_P);
}
}
if ((Increment_SegP_Pair || Decrement_SegP_Pair) && (Writing_P || Writing_Seg))
{
Invalid (Reg_Seg); //for the C compiler, it means stop with error. For the synthesizer, it means "don't care about this case and don't waste any gates on it, it's impossible".
Invalid (Reg_P);
}
if (Writing_B) Reg_B = (B_L_H_Data_NoMUX&63);
if (Writing_L) Reg_L = (B_L_H_Data_NoMUX&63);
if (Writing_H) Reg_H = (B_L_H_Data_NoMUX&63);
if (Writing_A) Reg_A = (A_DataMUX_MSB&63); //Oh yeah! Finally, the actual math/boolean result goes into the Acc!
if (Writing_Fl)
{
if (KeepCarry) //Yep, this flip-flop must be controlled separately
{
Reg_Fl = Flags_DataMUX&5 | Reg_Fl&2; //We don't rewrite bit 1
} else {
Reg_Fl = Flags_DataMUX; //We rewrite all flags
}
}
cout<<(int)(ROM_Data_Bus>>3)<<" "<<(ROM_Data_Bus&OPERAND)<<" "<<(int)Reg_A<<" "<<(int)Reg_Fl<<" "<<(int)Reg_B<<" "<<(int)Reg_L<<" "<<(int)Reg_H<<" "<<(int)Reg_P<<" "<<(int)Reg_Seg<<" "<<(int)PC<<endl;
if (ROM_Data_Bus>>6 == 1)
{
for (int i=0; i<8*64; i++) cout<<i<<" ";
cout<<endl;
for (int i=0; i<8*64; i++) cout<<(int)(SRAM[i])<<" ";
cout<<endl;
if (ROM_Data_Bus==DUMP_SRAM) exit (0); //Last dump on the emulation termination
}
}
}
}
Протестировал почти всё, исправил кучу перепутанных битных линий 🙂 и проверил, что вычитание действительно работает. Теперь будем разбирать его по кусочкам, заодно комментируя на нашем родном берёзово-хабровом, а не буржуйском итч-иошном (нет, не надо рассчитывать на то, что оно обязательно там появится. Я просто закладывал такую возможность. Собирайте здесь и сейчас из текста статьи, если хотите — но не ждите ничего и никого).
Начну разбор этого кода я с того факта, что поделие сие написано на ядрёной фене, суржике VHDL и Си. Много лет назад я грозился про него рассказать — и вот, действительно, достаточно весомая оказия для этого (я одновременно могу ввести в суть структуры того, как должно быть описано железо, того, как оно работает, и мне не требуется для этого излагать это на языке, который знают тут не все. Более чем веская причина, я считаю). Здесь я разберу самый простой пример — один clock domain, один главный цикл for. Суть этой фени в особом структурировании кода на Си таким образом, что он становится VHDL-подобен и может быть синтезирован 1:1, не гадая, «чего этот программист имел в виду». Каждое значение устанавливается один раз, зависят они одно от другого однонаправленно, есть определённый чёткий момент, когда значения «защёлкиваются» в регистрах по тому или иному фронту синхронизации — в общем, это чёткая, жёсткая симуляция именно работы «железа», а не просто «в случае команды bar регистры foo становятся Кар и Мяу, а как — ваши половые трудности», как это было бы «просто на Си» (а я как раз хочу про «кухню» рассказать с отягчающими подробностями). Причём в данном случае я ещё и от себя жести добавил — обычно 80% описаний каждого проводочка можно пропустить, ибо они очевидны и для синтезатора тоже. Если мы присваиваем значение регистра равным значению другого регистра — ну ясно же, что между ними есть проводок с этим значением!
Я описал их все.
Ибо.
Код:
#define OPCODE 56 //bin. 111000
#define OPERAND 7 //bin. 000111
//Opcodes
#define SCN 0+ //The most special opcode, sets a constant nibble (destination depends on the number of calls).
#define MOV_R_A 8 //Move from accumulator. C defines does not match the actual mnemonics, sorry.
#define JCC 16 //Conditional jump
//All other opcodes set the accumulator and somewhat match the actual mnemonics (at least, MOV_A B is similar to the actual MOV A, B mnemonic).
#define MOV_A 24 //Does not modify flags unless used to restore them from Seg:P
#define XOR_A 32
#define NAND_A 40 //NAND (sets Neg flag if used for Accumulator inversion)
#define ADN_A 48 //Adds with Negative flag
#define ADC_A 56 //Adds with Carry flag
//For all opcodes except SCN and JCC (which use their very own operands)
#define A +0 //Accumulator...
#define P +0 //...or P register, that depends on a command
#define B +1
#define L +2
#define H +3
#define SRAM_P +4 //SRAM[Seg:P], where port control registers are mapped to SRAM memory area
#define SRAM_PPP +5 //SRAM[Seg:P++]
#define SRAM_PMM +6 //SRAM[Seg:P--]
#define FLROM_HL +7 //FLASH ROM for user's firmware (read-only, interpreting-only), if reading...
#define PC_JUMP +7 //...Program Counter (A. K. A. Jump) if writing.
//Operands for the Jcc opcode:
#define Z_B +0 //Jz B:P (unless B is overriden by setting Seg:P)
#define NZ_B +1 //Jnz B:P
#define C_B +2 //Jc B:P
#define NC_B +3 //Jnc B:P
#define Z_A +4 //Jz A:P (unless A is overriden by setting Seg:P)
#define NZ_A +5 //Jnz A:P
#define C_A +6 //Jc A:P
#define NC_A +7 //Jnc A:P
#define CCMASK 2
//Emulator prefixes:
#define DUMP_SRAM +64 //Totally impossible to have on a real hardware due to it's 6-bit nature. Use with opcode 0 operand 0 to terminate the emulator.
Этот леденящий душу синтаксический ужас продиктован почти исключительно моей ленью. Его задача — позволить положить морской якорь на написание ассемблера (по крайней мере, пока), и сбацать тестовый код прямо в .h-файле, накидав его дефайнами, отдалённо напоминающими мнемоники этого самого ассемблера. Хотя в паре мест самого эмулятора я таки использую эти дефайны, чтобы не озадачивать при чтении вопросами, что это за «волшебная циферка».
Вот только обычно всё решает один-два бита. Поскольку у нас тут симуляция реального, хорошо оптимизированного железа. И тут не встретится (или почти не встретится) if (command == ADN || command == ADC || command == NAND || command == XOR) ModifyFlags(), а будет жёсткое, суровое, как драка канадского лесоруба с сибирским, безжалостное if (OpcodeMSB). Хотя любой уважающий себя синтезатор (а их пишут не дураки и пишут по своему образу и подобию), конечно, способен разобраться в круговерти тавтологических условий и свести их к одной однобитной линии. Я пишу так не потому, что «мой суржик на другое не способен» — способен так же, как способен VHDL или Verilog. Я пишу так именно потому, что ставлю перед собой задачу показать, как в проектировании железа понятие «группа команд» превращается в одну медную полоску, которую видно не во всякий микроскоп.
Дефайны состоят из масок (две, самые употребимые в коде симулятора), группы опкодов, группы операндов и группы префиксов (одна штука пока). Чтобы набрать в .h-файлах ассемблероподобное кашепойло, нужно набрать операндоподобный дефайн и параметроподобный дефайн, добавить по вкусу префикс (или нет) и закончить запятой. За счёт стратегически расставленных плюсиков при компиляции это всё соберётся в циферку, укладывающуюся в наши 6 бит. Или не укладывающуюся, если использовать префикс: по понятной причине он с пространством команд нашего предмета статьи вообще не пересекается, ибо существовать может только в эмуляторе, где под него физически биты есть. Сейчас он один и предписывает после исполнения команды сдампить SRAM в stdout (там вообще всегда результат дампится в stdout, но SRAM каждый раз кидать как-то жирно). Ну, и если это была команда SCN 0 — остановить эмулятор
Код:
//6-bit and 3-bit registers are emulated via 8-bit variables
static unsigned char SRAM [8*64]; //Data
static unsigned char ROM [64*64] = { //Microcode, or the interpreter (dat's philosofik questn!)
#include "uCode.h" }; //I didn't tell you it's OK to work with C this way!
static unsigned char FLASHROM [64*64] = { //User's code (the actual code, or the script, dat's da same questn!)
#include "UserCode.h" }; //Ooooops. I did it again %)
static unsigned char Reg_A, Reg_Fl, Reg_B, Reg_L, Reg_H, Reg_P, Reg_Seg; //Flags with A, and Seg with P are somewhat 9-bit registers
//12-bit registers are emulated via 16-bit variables
static unsigned short PC;
//Inner things are just int.
int NibbleCounter_2bit;
Тут мне сначала пришлось посетовать, как же плохо моя феня описывает битности, не кратные 8. А дабы симуляция точной была, лишнее безжалостно обрезать надобно. Это, однако, закрывает возможности преднамеренного эксплойта расхождений между железом и эмуляцией, то есть мои префиксы превратятся в тыкву (а вот логирование как таковое делается элементарно — просто предписываем тем или иным образом при синтезе игнорировать эти непонятные письмена). Тем не менее, если сия затея вам по нраву — проблему надо как-то решать, чтобы можно было один раз описать нестандартную битность и предписать таким образом компилятору каждый раз делать «чик» при каждом присваивании. И не превратить язык в ад перегруза перегруженной перегруженности перегрузок, а то, знаете, одна такая попытка уже была… видел я её… там вроде бы как тоже «всё сделано стандартными средствами языка», но то, что получилось, не просто не «си-подобное» — оно потеряло настолько любое сходство с…, что намного легче любой HDL освоить, чемЪ. Да и, честно говоря, хочется остаться в рамках именно Си: как нетрудно видеть, «плюсы» у меня там больше загрязняющая примесь, чем как-то используются.
Теперь мы имеем описания библиотечных элементов. При наличии реального синтезатора, конечно, это всё будет несколько не так, особенно в отношении SRAM (которую мы не только читаем, но и пишем): у нас есть некий .h-файл, в котором описано её поведение и торчащие из неё линии, а мы в своём коде никаких массивов не описываем, а работаем именно с этими «ногами». Потому, что это обычная, стандартная SRAM, и мы не пытаемся её сложить из отдельных регистров, к каждому из которых подведены свои линии управления и так далее. Мы просто подаём данные на вход, подаём адрес, подаём команду «пиши» и ждём некоторое время, пока транзисторы записи «пересиливают» транзисторы хранения, обычной грубой силой более широких затворов и более жирных микроампер. Потом мы снимаем команду «пиши» и после этого можем, наконец, позволить себе менять что-то на остальных линиях.
Масочные ROM и NOR FLASH ROM, да и сама SRAM на чтение — это намного проще. Подал адрес, подождал, устоялись данные. Можно принимать их во внимание. Потребляющие их линии подключены к ним всегда — но пока данные не устоялись, плюс пока через всю обработку не прошли эти актуальные значения, на выходе у нас мельтешит какая-то дребедень, и в силу «гонки состояний» она до определённого момента являет собой совершенно произвольную кашу. Нельзя рассчитывать на то, что после прибавления единички один битик вежливо подвинется в одну сторону, а другой — в другую, да ещё и одновременно, и это относится и к библиотечным элементам, и к тому, что мы вытворяем. Есть определённые библиотечные элементы, скажем, счётчики-делители, которые за счёт схемотехники гарантируют, что при добавлении единички младший разряд сразу инвертируется, а не будет скакать из нуля в единицу туда-сюда сорок раз, пока не определится со своим новым значением. Но в любой достаточно сложной схеме таких гарантий никто не даст; к счастью, у нас офигенно синхронная схема, в которой на вход кидаются данные, а через некоторое время приходит фронт тактового сигнала и результаты защёлкиваются в регистрах. И нам важно только то, что к моменту этого фронта они точно устаканятся, а сколько раз они решат в процессе превратиться в лису, сову и дракона — это исключительно их половые трудности.
А дальше — они! Благословенные наши регистры, счётчики и регистры-счётчики. Вроде бы библиотечные, а вроде бы — нет. Любой уважающий себя синтезатор их легко соберёт из триггеров, а триггеры — из КМОПов, сообразно их использованию. Были в коде инкременты и декременты — ставим реверсивный счётчик. Были присваивания — ставим защёлку. И так далее. Они идеально синхронные, даже в плане линий управления. Вот подали мы команду «защёлкни новое значение», сняли, опять подали — гонки состояний не унимаются. А ему — до лампочки. Он выполнит то действие, которое будет на входах в момент фронта «синхры». И время ему на это нужно какое-то пренебрежимо малое, потому что он уже всё посчитал и решил — надо только защёлкнуть результат (ну, обычно. Бывают ещё всякие переносы…) Как правило, подкузьмить его внезапным изменением хотелок на его входах в тот момент, когда идёт «процесс защёлкивания» — это должен быть или очень неправильно реализованный регистр, или очень стараться надо специально. Другое дело, что на его выходах изменённый результат когдаааа ещё появится… но хорошо спроектированный девайс такого типа, не в пример нашей записи SRAM, требует времени удержания уровней сигналов во время прохождения фронта синхронизации чуть ли не как длительность нарастания самого фронта. А если там что-то очень долгое — то первой он защёлкнет именно то, какую команду выполнять, и станет глух к её изменениям.
Хотя, конечно, уважающий себя синтезатор учитывает все лаги, и если какие-то гарантированно достаточно велики — что-то где-то упростит, потому что «ну пока эта масочная память свои паразитные ёмкости перезаряжает, ничего ж не изменится один фиг».
А вот счётчик NibbleCounter_2bit, который считает, сколько раз мы SCN вызвали — описывать мне стало лень, что я и откомментировал в немного странной формулировке. Считайте, что это тоже что-то библиотечное. Хотя надо было б дать описание его из пары триггеров-то…
Код:
#define Invalid(X) exit(X)
#define ClockRisingEdgeBegin //Means literally nothing for this C simulation. Needed only for the actual HDL synth.
#define ClockRisingEdgeEnd
#define ClockFallingEdgeBegin
#define ClockFallingEdgeEnd
Теперь о дефайнах, которые нужны для синтезатора — пусть он только у нас с вами в голове, это ничего не меняет. Ты и есть синтезатор. И, чтобы можно было понять, как наш нано-камешек работает — я объясню, как понимать сии письмена.
Дело в том, что у нас сишными переменными описываются две базовые вещи: хранилище значения (о благословенные наши регистры-счётчики!) и сигнал, сиречь проволочка, бегущая от логики до логики со всеми гонками состояний. И есть цикл симуляции, когда мы должны определиться со всеми значениями всего на свете, между одним фронтом синхроимпульса и другим. С сигналами всё просто. Особенно у меня тут: я почти полностью детерминированно их описываю, причём сразу задаю описание «или такой, или такой» (небольшое исключение в цепях сигнала Reset). Но это не обязательно в общем случае: проволочка не хранит значений и не может быть не подключена никуда вообще. Поэтому важно за цикл симуляции просто определить её значение так, чтобы оно а) было присвоено один раз, без «я передумал» б) использовалось по коду ниже, чем было присвоено, или показания эмуляции и железа разойдутся на такт. Даже если для разных случаев мы в разных местах значения определяем — синтезатор все их выловит и сделает переключение этой проволочки между разными вариантами мультиплексирования. Если для какой-то ситуации мы вообще не определили значения — значит, это не важно. Вообще. И надо делать так, чтобы избежать лишней логики. А вот изменения значений в хранилищах данных — дело другое. Если не было команды менять содержимое — значит, менять его нельзя. А как же быть с ситуациями, когда сигналы друг другу противоречат? Ведь такая ситуация никогда не возникнет, а синтезатор её будет обрабатывать, тратить кучу логических элементов просто на то, чтобы запретить изменение какого-нибудь регистра (а его надо именно запретить, ведь для этой ситуации, буде она невозможна, мы не дали отмашку что-то делать), всё это внесёт задержки, а сочетание сигналов, требующее такой реакции, в принципе невозможно создать? Синтезатору неведомо, какое сочетание сигналов можно создать, а какое — нельзя. Сам он упразднить эту тавтологию не может — для этого потенциально может потребоваться даже не полный анализ 2^64^64 вариантов состояния логики, а сопроводительная записка от инженерного отдела. Для этого я ввёл эту простую команду Invalid, останавливающую симулятор с ошибкой, а синтезатору предписывающую упрощать логику и не обрабатывать эту ситуацию в принципе. Что получится, то получится. Инкрементируй, декрементируй, экс… обнуляй, перезаписывай, в общем, не важно: эта ситуация заблокирована на более высоких уровнях логики.
Следующие дефайны нужны именно для упорядочивания цикла симуляции. Они попарно задают «ворота», в которые может приходить фронт и спад сигнала синхронизации, соответственно. Ну, то есть задают ТЗ на то, в какие лаги там надо уложиться, чтобы всё успеть. И, что намного менее очевидно — то, что можно переставлять, переносить, менять местами. А заставляет нас всё это делать опять-таки наша SRAM: со всеми остальными всё относительно просто, защёлкнули последствия выполнения очередной команды и автоматически сигналы побежали формировать следующую. А если мы её начнём защёлкивать вместе со всеми как результат выполнения команды на запись в неё-голубушку, то остальные успеют перейти к следующей команде и перекозявят сигналы на её входе, которые должны оставаться постоянными всё время записи. Даже отдельный регистр-защёлка не спасёт: ну, защёлкнули мы там данные для записи, адрес, сам факт режима записи… а кто потом её переключит в режим чтения, чтобы к следующей команде она смогла выдать данные (а всё остальное для этой команды уже давно бежит по цепочкам логики)? И какие задержки это добавит, причём даже там, где и записи-то по факту не было?
К счастью, у нас есть не только фронт «синхры», но и спад (причём в нашем случае — спад «предыдущего» импульса, если о них так можно говорить, потому что отреагировать нам надо заранее, а не с задержкой). И определиться с тем, пишем мы или не пишем, мы можем намного быстрее, чем протащить кучу переносов через АЛУ. Поэтому синхронизация у нас, по сути, двухфазная: через некоторое время после очередного фронта у нас «устаканиваются» новые значения для команды и для данных, после чего мы можем быстро (к спаду) понять, надо ли нам писать в память, при необходимости включить могучие транзисторы, а к очередному фронту, который ознаменует окончание обработки этой команды — выключить, в результате чего память снова будет способна делать что-то другое (например, вместе со всеми выдавать значение данных для уже следующей команды). Поскольку остальные процессы никак с этим не пересекаются — они начинаются тогда же. Но, поскольку они длиннее, чем просто выбор «пишем/не пишем» — у них есть полное время от фронта до фронта.
Синтезатор, конечно, понимает, что если что-то защёлкивается по фронту — значит, надо уложить все «евойные» лаги в отведённую длительность (в нашем случае схема настолько детерминированная, что он может только ответить нам, уложились мы или нет в заданную тактовую, и сколько у нас запаса, если уложились; в сложных схемах, однако ж, у него обычно изрядный простор для манёвров, идти по более быстрому пути или по более экономичному). Но в этом случае со SRAM приходится задавать некие «воротца», указывающие, что надо привязывать к фронтам и спадам, а что — не надо. И тут всё (относительно) просто: если что-то указано до Begin — значит, это надо закончить до соответствующего фронта, будь то передний или задний (доселе именовавшийся спадом). Если указано после — значит, конкретно это событие является синхронным и должно триггериться именно этим фронтом. До — асинхронные переключения всяких «мухов», после — выполнение всяких защёлкиваний и инкрементов счётчиков (наверное, если туда поместить присвоение значения сигналу, синтезатор должен выплюнуть ворнинг и рассматривать его как сигнал, прикрученный таки к какой-то защёлке. Или ошибку. ХЗ). А вот между ними помещается самое интересное — это вещи, индифферентные к данному из фронтов. То есть им вовсе не обязательно закончиться до Begin или триггернуться по End. Но тут водится небольшая мозговая козявка: хотя синтаксически они и описаны выше того, что триггерится End'ом, они-то как раз не обязаны закончиться раньше него (то, что обязано — праааавильно, оно в секции перед Begin). Потому, что в «железе» всё на самом деле происходит одновременно и параллельно. А Begin и End — это просто рамки, в пределах которых может произойти данный фронт (а если он за них вышел — всё, синтез не удался, хотелки не соответствуют быстродействию кремния).
Ну что ж, начинается истинная Боль. Потому, что мы сейчас должны перейти от простой и понятной логики «что делает каждая команда» к логике «чем определяется значение каждого регистра после завершения команды». А оно определяется — прааааавильно, всем и сразу. Всеми командами, которые на него способны хоть как-то повлиять. И логикой выбора того, какое именно значение он в итоге примет.
Код:
void main (void)
{
//0..2 opcode family
volatile unsigned char ROM_Data_Bus; //surplus "volatile" is harmless in C, but tells the synthesizer it's a signal line, not a latch, so any attempts to make it hold any data must cause a syntax error.
volatile unsigned short ROM_Addr_Bus;
volatile unsigned char SRAM_In_Bus, SRAM_Out_Bus;
volatile unsigned short SRAM_Addr_Bus;
volatile BOOL Opcode_SCN, Opcode_Jcc, Opcode_MOV_from_A;
volatile BOOL Writing_P, Writing_P_From_Operand, Writing_P_From_A, P_DataMuxN;
volatile unsigned char P_DataMUX, P_DataMUX0, P_DataMUX1;
volatile BOOL Writing_Seg, Writing_Seg_From_Operand, Cnt_SCN_MSB_Still_Matters;
volatile unsigned char Seg_DataMUX, Seg_DataMUX0, Seg_DataMUX1;
volatile BOOL Seg_Is_Prepared; //Affects a lot of commands
volatile BOOL Writing_PC, Writing_PC_By_SCN, Writing_PC_By_MOV, Writing_PC_By_Jcc, Writing_PC_CC, PC_DataMuxNMSB, PC_DataMuxNLSB0, PC_DataMuxNLSB1;
volatile unsigned char PC_Data_L_NoMUX, PC_Data_H_MUXH, PC_Data_H_MUXL0, PC_Data_H_MUXL1, PC_Data_H_MUX0, PC_Data_H_MUX1, PC_Data_H_MUX2, PC_Data_H_MUX3;
volatile BOOL Writing_SRAM;
volatile BOOL Increment_SegP_Pair, Decrement_SegP_Pair;
volatile unsigned char B_L_H_Data_NoMUX;
volatile BOOL Writing_B, Writing_L, Writing_H;
//3..7 opcode family
volatile unsigned char FLASHROM_Data_Bus;
volatile unsigned short FLASHROM_Addr_Bus;
volatile BOOL Writing_A, Writing_Fl, KeepCarry, Opcode_x11, Opcode_1xx, Operand_0, Flags_DataMuxN;
volatile unsigned char Flags_DataMUX, Flags_DataMUX0, Flags_DataMUX1;
volatile BOOL Operand_MuxLSBN, Operand_MuxMidLN, Operand_MuxMidHN, Operand_MuxMSBN;
volatile unsigned char Operand_MUX_LSB_L, Operand_MUX_LSB_H, Operand_MUX_Mid_L, Operand_MUX_Mid_H, Operand_MUX_MSB;
volatile unsigned char Operand_MUX_LSB_L_0, Operand_MUX_LSB_L_1, Operand_MUX_LSB_H_2, Operand_MUX_LSB_H_3, Operand_MUX_Mid_H_0, Operand_MUX_Mid_H_1;
volatile BOOL CarryIn_MUX, CarryIn_MUX0, CarryIn_MUX1, CarryIn_MuxN;
volatile unsigned char ALU_In_2, ALU_OutSum, ALU_OutXOR, ALU_OutNAND;
volatile BOOL CarryOut, AllAre1, AllAre0, CarryOut_MUX, CarryOut_MuxN, NegOperation;
volatile BOOL A_DataMuxMSBN, A_DataMuxMidHN, A_DataMuxMidLN, A_DataMuxLSBN;
volatile unsigned char A_DataMUX_LSB0, A_DataMUX_LSB1, A_DataMUX_LSB; //XOR vs NAND result selection
volatile unsigned char A_DataMUX_Mid_H1, A_DataMUX_Mid_H, A_DataMUX_Mid_L0, A_DataMUX_Mid_L1, A_DataMUX_Mid_L, A_DataMUX_MSB;
К счастью, из-за крайней простоты нашего девайса мы можем хотя бы отделить всё, что не пишет в регистр-аккумулятор, и всё, что в него пишет, разбив всё на две группы. В сумме эта простынка описывает исключительно наши сигналы-«проводки», которые сами по себе ничего не хранят, а просто переключаются при помощи «мухов» между источниками. Чтобы это обозначить для синтезатора (даже для того, который сейчас живёт внутри наших голов), я нагло использовал слово volatile. Выбор, конечно, не просто так — я подумываю о том, что в случае развития этого странного языка можно вполне себе даже вести симуляцию разных clock domain в разных тредах. Но пока просто рассматривайте это как словесное украшение. Разумеется, этот выбор говорит о полном моём пренебрежении оптимальностью того кода, который соберёт бедный компилятор (как, впрочем, и всё остальное в этом коде). Я целиком и полностью нацелился на максимальное объяснение цифровой схемотехнической «кухни» моего крошечного «часового проца», вплоть до того, что кто-то может захотеть сделать его на ПЛИС, выделить несколько регистров для управления DRAM, прицепить несколько мегабайт, в качестве микрокода сделать эмуляцию 80386 и запустить на нём не просто Doom, а тот самый, DOS'овский первый Doom, без всякого портирования. И не встретит на этом пути никаких особых препятствий.
Впрочем, расписывать значение каждого слова в этой простынке я всё-таки не стану. Это будет не столько «объяснение», сколько выклёвывание мозга. И если с говорящими названиями типа «шина SRAM, данные» всё более-менее просто, то принципы построения остальных названий я сейчас расскажу. Дело в том, что я использую тут чисто двоичные «мухи», которые имеют ровно два входа — или один выбран, или второй. А для того, чтобы выбирать из достаточно большого количества сигналов — я их каскадирую. И я даже не пытаюсь свести их, скажем, к одному четырёхвходовому. Потому, что критерии дальнейшего выбора для той ветки, у которой старший «мух» получил 0, и той ветки, у которой он получил 1 — они часто бывают, чёрт их дери, разными. И это дорывает мозг, потому что у них в сумме могут быть четыре входа и три управляющих сигнала. А не два. Потому, что при старшем бите 0 работает один вход младшего бита, а при старшем бите 1 — другой. Потому, что нужно так. Потому, что итоговое значение вот так вот выбираться должно. И имена у них получаются соответствующие.
Булевы переменные, понятное дело, соответствуют однобитным сигналам-«проволочкам». Остальные, соответственно, шинам. Имена «Opcode кто-то там» — это сигналы, получающие единицу, если мы сейчас разгребаем именно этот код операции. Я, конечно, опускаю то обстоятельство, что активными могут быть и единица, и ноль, потому, что результат проверки на соответствие значению (как и все остальные результаты) может в силу схемотехники изначально быть инверсным — и нет абсолютно никакого смысла его специально инвертировать для того, чтобы приёмная логика его инвертировала ещё раз, потому что ей, внезапно, вдруг тоже оказался удобнее инверсный вход (а мультиплексор просто примет любой вариант — мы всегда можем просто поменять входы данных местами). Об этом совершенно не надо думать — это, наверное, первое, ради чего в принципе синтезаторы появились. Чтобы хотя бы от этого «синтезатор в голове» разгрузить.
Сигналы «Writing кого-то там» — это окончательное и бесповоротное решение данного товарища в момент фронта синхронизации защёлкнуть. Складываются из кучи причин «Writing кого-то там из кого-то там», например. Или из других. Сигналы «Кто-то там ДатаМух N» — это как раз выбор того, какой из двух вариантов пойдёт нам на выход «муха». Причём те, которые относятся не к старшему биту выбора — они с суффиксами, намекающими, к какому из вариантов старшего бита они относятся. И так далее до. Что же до самих данных — они, во-первых, не булевы (как правило. Вход переноса, который «мухается» или на Carry Flag, или на Neg Flag — исключение), а, во-вторых, не имеют в себе буковки «N». Зато те из них, которые промежуточные (да, я даже их расписал) — имеют не только циферку с номером входа, но и буковки, намекающие на их сущность так же, как и суффиксы у их сигналов-селекторов. И финишный выход не имеет ничего, кроме указания на то, что это данные для какого-то регистра и что они мультиплексированные (или нет, такие переменные тоже легко заметить). А поскольку у нас есть ещё и регистры с опцией реверсивного счётчика, для них есть ещё парочка управляющих команд — нужно ли инкрементировать/декрементировать регистр в момент фронта импульса. Или вообще перезаписать из входов, да. Или перезаписать частично, причём с очень хитро-разными масками. Это собирается автоматически, но из триггеров, поэтому можно управлять каждым битом раздельно.
Код:
for (BOOL Reset=1; ;Reset=0) //Infinite clock loop
{
ROM_Addr_Bus = PC;
ROM_Data_Bus = ROM[ROM_Addr_Bus];
SRAM_Addr_Bus = Reg_Seg<<6|Reg_P;
SRAM_In_Bus = Reg_A;
SRAM_Out_Bus = SRAM[SRAM_Addr_Bus];
P_DataMUX0 = (((Reg_P)&63) >> 3 ) | ((ROM_Data_Bus&OPERAND)<<3); //Looks a bit complicated, but it's just a wire combination.
P_DataMUX1 = Reg_A;
Seg_DataMUX0 = ROM_Data_Bus&OPERAND;
Seg_DataMUX1 = Reg_Fl;
PC_Data_L_NoMUX = Reg_P; //Lower 6-bit byte of PC can be set only via P.
PC_Data_H_MUX0 = Reg_A; //Higher can be set either with A...
PC_Data_H_MUX1 = Reg_B; //...or with B, both of them support jumps to a calculated address...
PC_Data_H_MUX2 = (ROM_Data_Bus&4 )<<1 | Reg_Seg; //...with Seg, 4 bits only! For short jumps (can be conditional)...
PC_Data_H_MUX3 = (ROM_Data_Bus&OPERAND)<<3 | Reg_Seg; //...with Seg + operand, long (but only unconditional) jumps via 4th call of SCN.
B_L_H_Data_NoMUX = Reg_A; //Exactly the same physical wires as SRAM_In_Bus, P_DataMUX1 etc!
FLASHROM_Addr_Bus = Reg_H<<6|Reg_L;
FLASHROM_Data_Bus = FLASHROM[FLASHROM_Addr_Bus]; //At last, the most importaint part -- reading the actual user's byte code for our VM.
Flags_DataMUX1 = Reg_Seg; //Restoring saved flags
Operand_MUX_LSB_L_0 = Reg_A; //Exactly the same physical wires...
Operand_MUX_LSB_L_1 = Reg_B;
Operand_MUX_LSB_H_2 = Reg_L;
Operand_MUX_LSB_H_3 = Reg_H; //One of those four will be selected as Operand_MUX_Mid_L output later. These are operands 0..3
Operand_MUX_Mid_H_0 = SRAM_Out_Bus; //operands 4..6
Operand_MUX_Mid_H_1 = FLASHROM_Data_Bus; //operand 7
CarryIn_MUX0 = (Reg_Fl&4)>>2; //Neg flag
CarryIn_MUX1 = (Reg_Fl&2)>>1; //Carry flag
ALU_In_2 = Reg_A; //Yep that bus again :)
A_DataMUX_Mid_L1 = Reg_P;
if (Reset) //Initial latch states
{
//ToDo: explicitly define some "Reset Safety". But not too much! Everything costs it's logic gates.
ClockFallingEdgeBegin
ClockFallingEdgeEnd
ClockRisingEdgeBegin
ClockRisingEdgeEnd
PC=0;
Reg_A=0;
cout<<"PC(b4) Opcode Operand A Fl B L H P Seg PC"<<endl;
} else {
Как легко видеть, мы перешли к Сути — к циклу симуляции. Это бесконечный for, первый прогон которого — Reset. В принципе, синтезатор (даже на ручной внутричерепной тяге) может и сам до всего догадаться, но, чтобы облегчить ему работу (особенно во втором варианте), кое-что лучше описать в явной форме. В самом начале мы видим такой пример — простынка безусловных присвоений сигналам значений из регистров, счётчиков, регистров-счётчиков, из выходных линий памяти во всех её ипостасях, и так далее. А поскольку сигналы — это просто проволочки, то эта простынка описывает вещи, которые подключены всегда, напрямую (плюс-минус возможная буферизация, если выходного тока одного элемента может не хватить на переключение целой оравы потребителей), а поскольку прошлый цикл закончился присвоением новых значений всем этим регистрам — время «от входа до выхода» открывает счёт суммарного лага (то есть PC, значением которого мы интересуемся в первой же строчке — это не просто PC, это «PC, который уже защёлкнул по фронту новое значение, но до выхода оно ещё не дошло»). Дальше к нему, конечно, добавляется лаг нашего масочного ПЗУ! Вот уж лаг так лаг. Да, оно быстрое, конечно… на фоне «драмы». SRAM я, конечно, описал несколько упрощённо — все трудности я выше рассказывал, но в целом читается она как раз примерно так, основные «недоговорки» будут касаться записи. А вот с этими «смещениями на N бит» туда-сюда — всё вообще просто. Это даже не операция, а просто описание того, из каких проводочков какие значения берутся. Да и вся эта простынка, по сути, только для компилятора является кучей операций. Для синтезатора же это просто netlist, сиречь список соединений. И да, как я уже говорил, можно было бы особо-то не расписывать все эти сигналы, потому что если мы ниже дадим какому-нибудь «муху» в качестве входного значения напрямую регистр — не только компилятор поймёт это правильно, но и синтезатор поймёт, что там подразумевался простейший сигнал «от одного до другого». Но мы сейчас бу́-дем пи-са́ть ка́к в бук-ва-ре́, по слогам и с ударениями.
Что мы тут видим? Мы видим, что к мощным перезаписывающим транзисторам SRAM подведён напрямую регистр A, потому что в системе команд нет других источников значений для SRAM. Как только мы их включим — ячейка, выбранная при помощи шины адреса, перезапишется. Дальше мы видим, что для регистра P существует два источника данных — это A и операнд SCN (плюс частично — старое его значение). Это вполне вяжется с тем, что мы помним про систему команд. Аналогично, Seg может использоваться как сегмент, а может — как временное хранилище флагов. Дальше мы видим, что младший байт PC может задаваться только через регистр P (обычный переход к следующей операции не в счёт, уважающий себя регистр-счётчик это делает через управляющие входы, а не при помощи внешнего, простигосподи, «сумматора с единичкой»). Как мы помним, там мог бы быть ещё вариант L для Jcc H:L, но здравый смысл вовремя очнулся 🙂 А вот старший может задаваться аж четырьмя способами! Через A (Jcc A😛 или MOV PC A😛), через B (Jcc B😛), через третий вызов SCN с последующим Jcc и через четвёртый вызов SCN.
Аналогично не составляет труда понять всё остальное, кроме того, куда девался второй вход «муха» для регистра флагов. А он сам по себе собирается из кучи разных вариантов! А тут мы описываем самую базу, у нас даже секций для фронтов импульса тут нет — то есть тут ничего не происходит. Просто счётчик лагов синтезатора копит лаги для того, чтобы прибавить их потом ко всему, что будет их использовать. И второй вход, естественно, описан уже там. В принципе, там могло быть описано и всё остальное, это больше вопрос структурирования и читаемости.
Дальше мы видим, как некоторые проволочки цепляются к другим проволочкам — по сути, это просто раздача новых имён, с точки зрения синтеза ничего не происходит. «Мы будем использовать линию выхода SRAM как какой-то вход какого-то мультиплексора» (судя по имени. Сам факт того, что это мультиплексор — всплывёт только в момент его описания). По́ сло-га́м, ка́к о-бе-ща́л.
Теперь мы переходим наконец к настоящему поведенческому описанию. Сначала у нас традиционная ветка Reset, то есть что железо должно делать, когда подан соответствующий сигнал. Тут мы не видим описания ровно ничего, кроме того, что надо «обресетить» регистр программного адреса (PC) и зачем-то A (на самом деле — для примера, захотелось мне так. А вот NibbleCounter_2bit, на грамотное описание которого я забил в самом начале, тут должен быть — а его нет). А что с сигналами? А что угодно. Сигнал не описан — сигнал может вести себя произвольно, как мы выше уже разбирали, касаясь Invalid(). Главное, что остальным хранилищам информации что-либо присваивать запрещается. Все управляющие входы к моменту прихода фронта синхронизации выставляются в FALSE, кроме тех, которые отвечают за сброс PC и A. И многообещающее «else» наконец открывает нам мир внутренних принципов работы процессора.
Со всей имеющейся вводной информацией будет легко 😉
Код:
Opcode_MOV_from_A = ((ROM_Data_Bus&OPCODE) == MOV_R_A);
Writing_SRAM = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)!=7 ) && ( (ROM_Data_Bus&4) ); //MSB of operand is set (SRAM/PC group of operands), but other two are not 11 (111=PC)
ClockFallingEdgeBegin //For the actual timing calculations. Places the earliest time of the falling edge.
//Processing the P register (which can be affected by SCN or MOV P, A)
Opcode_SCN = ((ROM_Data_Bus&OPCODE) == 0); //SCN
Opcode_Jcc = ((ROM_Data_Bus&OPCODE) == JCC);
Writing_P_From_Operand = Opcode_SCN && !(NibbleCounter_2bit&0x02); //Two first calls of SCN (in a row!) makes the opcode go to the P register.
Writing_P_From_A = Opcode_MOV_from_A && !(ROM_Data_Bus&OPERAND); //MOV P, A makes A go to the P register (as well as flags goin' to Seg -- see below about this).
Writing_P = Writing_P_From_Operand || Writing_P_From_A; //We must latch a new value in P in either case.
P_DataMuxN = (ROM_Data_Bus>>3)&1; //LSB of the opcode is enough to distinguish 3rd (or any) SCN from MOV P, A
if (P_DataMuxN) P_DataMUX = P_DataMUX1; else P_DataMUX = P_DataMUX0;
//Processing the Segment register (which can be affected by SCN or MOV P, A (which also stores flags to Seg))
Cnt_SCN_MSB_Still_Matters = Opcode_SCN && (NibbleCounter_2bit&0x02); //Third SCN call (in a row!) makes the opcode go to the Segment.
Writing_Seg_From_Operand = Cnt_SCN_MSB_Still_Matters && !(NibbleCounter_2bit&0x01);
Writing_Seg = Writing_Seg_From_Operand || Writing_P_From_A; //No separate "Writing_Seg_From_A" is needed, we write Seg from Flags if we write P from A.
if (P_DataMuxN) Seg_DataMUX = Seg_DataMUX1; else Seg_DataMUX = Seg_DataMUX0; //Also, no separate Seg_DataMuxN is needed.
//Processing the Program Counter (which can be affected by either 4th SCN (AKA Jump to a const. addr.), MOV PC, A:P (AKA Jump to a calculated addr.), and Jcc.
Seg_Is_Prepared = (NibbleCounter_2bit&0x02) && (NibbleCounter_2bit&0x01); //Having the Seg prepared changes some command addressing modes...
Writing_PC_By_SCN = Cnt_SCN_MSB_Still_Matters && (NibbleCounter_2bit&0x01); //...the first and the main usage -- jumping to a constant address by making the fourth SCN call (in a row!)
Writing_PC_By_MOV = Opcode_MOV_from_A && (ROM_Data_Bus&OPERAND)==7; //Actually, that "==7" is just a big AND(all_3_bits) :)
if (ROM_Data_Bus&CCMASK) Writing_PC_CC = (Reg_Fl>>1)&1; else Writing_PC_CC = Reg_Fl&1; //Selecting which flag to check: Zero (bit 0) or Carry (bit 1).
Writing_PC_By_Jcc = Opcode_Jcc && Writing_PC_CC^(ROM_Data_Bus&1); //True if flag is 1 and is checked to be SET (Jz, Jc) or it's 0 and is checked to be RESET (Jnz, Jnc).
Writing_PC = Writing_PC_By_SCN || Writing_PC_By_MOV || Writing_PC_By_Jcc;
PC_DataMuxNMSB = Seg_Is_Prepared && !(ROM_Data_Bus&8); //PC have a two-stage multiplexed input (and it's also a counter). First stage means selection between Seg/Op|Seg and A/B regs.
PC_DataMuxNLSB0 = (ROM_Data_Bus>>2)&1; //MSB of the Jcc operand exactly means jumping to A:P instead of B:P (which also counts for MOV PC, A because "PC" operand has MSB=1, too). One of them is selected on the second stage unless first stage selects Seg.
PC_DataMuxNLSB1 = Opcode_SCN; //The same (2nd stage if Seg *IS* selected). Allows to distinguish 4th SCN (which uses opcode+Seg for long jump) from MOV PC,A / Jcc.
if (PC_DataMuxNLSB1) PC_Data_H_MUXL1 = PC_Data_H_MUX3; else PC_Data_H_MUXL1 = PC_Data_H_MUX2; //2nd stage, "st1 = 1" branch. Depending on the actual opcode, we select either Seg (for Jcc) or operand<<3+Seg (for 4th SCN which must cause jump).
if (PC_DataMuxNLSB0) PC_Data_H_MUXL0 = PC_Data_H_MUX0; else PC_Data_H_MUXL0 = PC_Data_H_MUX1; //2nd stage, "st1 = 0" branch. Depending on the operand MSB, we select either A or B register.
if (PC_DataMuxNMSB) PC_Data_H_MUXH = PC_Data_H_MUXL1; else PC_Data_H_MUXH = PC_Data_H_MUXL0; //1st stage, which has the top priority for both opcodes. We select the Seg group, if Seg is prepared (unless we MOV), and select the A/B group if it's not.
//...we seem to finish both SCN and Jcc opcodes, let's finish the MOV R, A (for R other than P and PC)
Increment_SegP_Pair = ((ROM_Data_Bus&OPERAND) == 5) && !Opcode_SCN && !Opcode_Jcc;
Decrement_SegP_Pair = ((ROM_Data_Bus&OPERAND) == 6) && !Opcode_SCN && !Opcode_Jcc;
Writing_B = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)==1);
Writing_L = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)==2);
Writing_H = Opcode_MOV_from_A && ( (ROM_Data_Bus&OPERAND)==3);
Как уже мы знаем, пока длится высокий уровень предыдущего синхроимпульса, мы должны успеть определиться с тем, пишем ли мы в SRAM. А также должны устояться адреса, выходящие со свежеприсвоенных регистров, ну и так далее. И потом их никто не должен трогать пол-цикла (вот это — самое простое. Если мы пишем в SRAM, то никакие другие операции явно не происходят). Фактически первые строки означают задание на расчёт лагов, после которых значение Writing_SRAM является установившимся, правильным и ему можно доверять (насчёт адресов, которые могут в теории ещё «трепыхаться» на выходе с регистра, мы тихо помолчим — вместо нормальной библиотечной SRAM, где это бы тоже прописывалось по отношению к её входам, у нас тут несколько более «игрушечная»).
Дальше у нас начинается окно Begin-End, где располагаются вещи, индифферентные к спаду «синхры». Поскольку по спаду только начинается запись SRAM, тут помещается практически весь процессор с ушками, рожками и ножками, только глазки из бездны моргают робко. Два самых простых начальных условия — мы определяем промежуточные сигналы «у нас опкод SCN» и «у нас опкод JCC» (с записью из A определились раньше — у неё требования к таймингам жёстче). Обычно используется страшный грозный зверь — дешифратор команды, который для каждой команды генерирует свой отдельный управляющий сигнал на отдельном выходе, да ещё и со всякими опциями, вариациями… но у нас система не только упрощённая, но и очень оптимизированная, поэтому такой чести удостоились немногие. Основная масса команд идёт как единая группа. Дальше мы определяемся с тем, какие у нас есть причины записать регистр P, ну и по итогу с тем, пишем ли мы его. Этот сигнал станет по итогу управляющим для регистира P: он придёт на вход «запись», традиционно подёргается из-за гонок состояний туда-сюда, успокоится к моменту прихода фронта синхроимпульса и, если успокоился он в активном уровне, регистр тут же защёлкнет значение на входной шине (и оно медленно поползёт через его ключи к его выходу).
Следующим мы видим нечто более интересное: в качестве критерия того, из какого источника писать P, выступает всего один бит кода операции! Мы не пользуемся даже готовыми линиями, чётко указывающими, что операция у нас SCN (или не SCN). Мы берём один бит, который будет задавать «мусорное» значение для любой команды, кроме двух, нужных нам. Почему? А потому, что у нас нет «готового» значения Opcode_SCN. Всё происходит более-менее одновременно. И этот бит у нас по определению будет раньше, чем вычислится Opcode_SCN — так зачем вносить лишний лаг? «Мух» в любом случае в железе есть, и он никуда не денется. Что-то он должен выбрать в любом случае, или первый вход, или второй. Даже если у нас совсем другая операция, которая его выходом пользоваться не будет. С точки зрения симуляции это выглядит странно и расточительно, но показать разницу между реальным миром, где как раз это — экономия ресурсов, и его жалким софтовым подобием в симуляции, как раз входит в круг моих сегодняшних задач. Примерно то же самое мы видим дальше с регистром сегмента, но они уже настолько срослись в нечто девятибитное, что я даже не всегда удосужился выделить ему отдельные сигналы, хотя вроде обещал, обещал…
Дальше у нас, как легко видеть, PC. Ох уж этот PC! Переходов у нас тьма-тьмущая вариантов, чтобы с нашими 6 битами было можно писать хоть что-нибудь (серьёзно, в какой-то момент написания тестовых микрокодов я ощутил натуральную клаустрофобию, настолько психологически «тесным» показался наш герой дня). Поэтому всё у нас тут то же самое, но толще, выше и мощнее — как минимум, выбор значения уже среди четырёх кандидатов. Определившись с тем, будет ли у нас вообще переход (была ли команда безусловного, была ли условного, сработали ли для неё условия…) — выбираем для неё правильный источник значения. Первое ветвление (с точки зрения как симуляции, так и прохождения сигналов оно, конечно, второе, но с иерархической точки зрения оно именно что первое!) — выбор того, будем мы использовать что-то из тусовки Seg или что-то из тусовки AB. Тут мне пришлось отдельной проверкой бита заблокировать использование Seg в случае, если у нас MOV PC, A😛, потому что иначе, если мы Seg по какой-то причине прогрузили перед этим, произойдёт подмена A на Seg. А такая команда была бы абсолютно тавтологична четвёртому вызову SCN 0 и совершенно не полезна, а только усложнит возврат по вычисленному адресу (что, как я уже говорил, в отсутствие Call/Ret является почти абсолютно необходимым). Уже привычным (надеюсь!) образом я взял один ключевой бит, отличающий одну ситуацию от другой. Любые «третьи, четвёртые и прочие» ситуации не рассматриваются по той же причине — когда результат не будет далее выбран, он может быть любым. Следующим, точно так же — одним битом — мы не менее лихо отличаем B от A. На случай, если нам это вообще понадобится, конечно. Не просто так у нас старший бит операнда равен 1 для перехода по адресу из A как в случае Jcc A😛, так и в случае MOV PC A😛 ! А вот на случай, если нам понадобится правильно задать другой вход в нашем первом ветвлении — мы сейчас и его вычислим. Он зависит от того, SCN у нас или что-то иное. Если SCN — у нас «Seg и значение операнда» (для обработки, естественно, безусловного перехода по четвёртому вызову SCN подряд. Если у нас он не четвёртый — опять же, считай не считай, использоваться это не будет. Пожалуй, мне надо перестать повторяться — это настолько базовый принцип, что он будет встречаться практически в каждой строчке симуляции). Если не SCN — то «Seg и один старший бит операнда», чтобы хотя бы немного расширить диапазон коротких быстрых переходов по Jcc.
Дальше мы можем видеть, как через наши «мухи» идёт уже сам сигнал. Сначала один из них определяется с тем, какой вариант Seg с операндом выбрать — шестибитный или четырёхбитный, то есть требуемый для четвёртого SCN или для Jcc «со взведённым Seg». Затем другой определяется с тем, какой вариант выбрать между A и B (A, которое для Jcc и для MOV PC, A😛, или B, которое для Jcc). И, наконец, последний (иерархически первый! «Капитан, КАПИТАН Джек Воробей!», — пытается этот мультиплексор меня сейчас поправить), выбирает среди их выходов тот, который соответствует задаче.
И нет, мне не обидно за их напрасный труд, потому что всё это, как правило, нигде не будет в итоге «защёлкнуто» — с точки зрения статистики у нас, как правило, ни та и не другая команда :-D Извините, не удержался.
Ну, и напоследок мы формируем управляющие сигналы, указывающие, нужно ли перезаписывать какие-то другие регистры содержимым аккумулятора и нужно ли крутить туда-сюда суммарный девятибитный регистр-счётчик, образованный из Seg и P. Тут вроде всё просто.
Перейдём теперь к АЛУ, включая нерешённый пока что вопрос: иметь Carry для XOR «ради чего-нибудь» или не портить его, чтобы при помощи «XOR на разницу» быстро прыгать между двумя адресами операндов в SRAM.
Код:
Opcode_x11 = ((ROM_Data_Bus>>3)&1) && ((ROM_Data_Bus>>3)&2);
Opcode_1xx = ((ROM_Data_Bus>>3)&4) >> 2;
Writing_A = Opcode_x11 || Opcode_1xx;
Operand_0 = (ROM_Data_Bus&OPERAND)==0; //for opcode 100, that means MOV A, P (switches the second operand from A to P)
Writing_Fl = Opcode_1xx || (Opcode_x11 && Operand_0); //MOV can modify flags only if MOV Fl:A Seg:P (which is the same as MOV A, P)
Flags_DataMuxN = !Opcode_1xx /* && Opcode_x11 && Operand_0 */; //Switching to MUX1 which is the Segment register. MOV A, P only.
//1st operand for the ALU is always A. Let's select the second one here!
Operand_MuxLSBN = (ROM_Data_Bus & 1); //for both Low and High branches of bit 0 (selecting A vs B and L vs H);
Operand_MuxMidLN = (ROM_Data_Bus & 2)>>1; //Low branch of bit 1 (selecting A/B vs L/H when MSB is 0);
Operand_MuxMidHN = (ROM_Data_Bus & 1) && (ROM_Data_Bus & 2); //High branch of bit 1 (selecting Flash ROM vs SRAM when MSB is 1);
Operand_MuxMSBN = (ROM_Data_Bus & 4)>>2; //MSB A. K. A. the final selection between the low and high branch outputs.
if (Operand_MuxLSBN) Operand_MUX_LSB_L = Operand_MUX_LSB_L_1; else Operand_MUX_LSB_L = Operand_MUX_LSB_L_0;
if (Operand_MuxLSBN) Operand_MUX_LSB_H = Operand_MUX_LSB_H_3; else Operand_MUX_LSB_H = Operand_MUX_LSB_H_2;
if (Operand_MuxMidLN) Operand_MUX_Mid_L = Operand_MUX_LSB_H; else Operand_MUX_Mid_L = Operand_MUX_LSB_L;
if (Operand_MuxMidHN) Operand_MUX_Mid_H = Operand_MUX_Mid_H_1; else Operand_MUX_Mid_H = Operand_MUX_Mid_H_0;
if (Operand_MuxMSBN) Operand_MUX_MSB = Operand_MUX_Mid_H; else Operand_MUX_MSB = Operand_MUX_Mid_L;
//Carry In is kind of operand, too!
CarryIn_MuxN = ((ROM_Data_Bus>>3) & 1); //ADD with Neg flag vs ADD with Carry; for other cases, Cin value does not matter.
if (CarryIn_MuxN) CarryIn_MUX = CarryIn_MUX1; else CarryIn_MUX = CarryIn_MUX0;
//Now we have all operands prepared and we can emulate the ALU (actually it's a single-mode adder with multiple outputs).
ALU_OutSum = (Operand_MUX_MSB + ALU_In_2 + CarryIn_MUX) & 63;
CarryOut = (Operand_MUX_MSB + ALU_In_2 + CarryIn_MUX) >> 6 & 1;
ALU_OutXOR = Operand_MUX_MSB ^ ALU_In_2; //This is a classic "nineNAND" adder, so we instantly have XOR as an intermediate result.
ALU_OutNAND = ~(Operand_MUX_MSB & ALU_In_2); //This is a classic "nineNAND" adder, so we instantly have NAND on a literally first NAND gate!
//Now let's select the needed result with even more MUX! %)
A_DataMuxLSBN = ((ROM_Data_Bus>>3)&1); //0 selects the XOR result (for the 100 opcode) and 1 selects the NAND/NEG result (for the 101 opcode).
A_DataMuxMidHN = ((ROM_Data_Bus>>4)&1); //High branch of MSB. 1 selects the arithmetic result (110/111 opcode, both from the same output "Sum"), 0 selects bitwise logic one (100/101, depending on LSB).
A_DataMuxMidLN = Operand_0; //Low branch of MSB which means MOV operations. 1 selects P, 0 selects the ALU incoming operand -- completely bypassing the whole ALU.
A_DataMuxMSBN = Opcode_1xx; //1 selects the ALU result, 0 selects either P or the ALU operand -- completely bypassing the whole ALU, as said above (but making MOV A, ? do it's job).
A_DataMUX_LSB0 = ALU_OutXOR; //Yep they are same wires but we can't place them in the generic loop section (b4 reset branch) because it's OK for synth but not OK for the C compiler!
A_DataMUX_LSB1 = ALU_OutNAND; //The same
A_DataMUX_Mid_H1 = ALU_OutSum; //That again
A_DataMUX_Mid_L0 = Operand_MUX_MSB; //Yeah, the same wires (bus) as the ALU operand
if (A_DataMuxLSBN) A_DataMUX_LSB = A_DataMUX_LSB1; else A_DataMUX_LSB = A_DataMUX_LSB0; //First of all, select 101 vs 100 opcode (NAND vs XOR).
if (A_DataMuxMidHN) A_DataMUX_Mid_H = A_DataMUX_Mid_H1; else A_DataMUX_Mid_H = A_DataMUX_LSB; //Second, select 110/111 (ADN/ADC) from ALU vs 100/101 from the LSB MUX output.
if (A_DataMuxMidLN) A_DataMUX_Mid_L = A_DataMUX_Mid_L1; else A_DataMUX_Mid_L = A_DataMUX_Mid_L0; //At the same time, for the low branch of MSB (which stands for MOV), we select P (as a substitute of meaningless MOV A, A) or a common operand.
if (A_DataMuxMSBN) A_DataMUX_MSB = A_DataMUX_Mid_H; else A_DataMUX_MSB = A_DataMUX_Mid_L; //the most general selection -- 1 stands for ALU result, 0 stands for simple MOV opcode.
//...and flags -- they are results, too!
//cout<<(int)(CarryOut_MuxN)<<" "<<(int)(A_DataMUX_LSB)<<" "<<(int)(AllAre1)<<endl;
AllAre1 = (A_DataMUX_LSB&63 == 63); //Counts only for bitwise logic. For the ADC/ADN, we have a real Carry flag.
AllAre0 = (A_DataMUX_Mid_H == 0); //Counts for everything except MOV.
CarryOut_MuxN = A_DataMuxMidHN; //Exactly the same wire, because it selects the arithmetic result (vs bitwise logic) and, obviously, must select the real Carry here.
if (CarryOut_MuxN) CarryOut_MUX = CarryOut; else CarryOut_MUX = AllAre1; //For XOR/NAND there are obviously no Carry, so we use "result is 111111" flag instead. This allows to check a needed bit in a flag field very quickly!
NegOperation = ((ROM_Data_Bus&63) == NAND_A A); //The one and the only, Neg A! Consists of NAND A, A (so A = ~A) and setting the Neg flag to 1 (this is the only opcode/operand combinations which does it). So on next ADN, ~A+1 is added -- effectively meaning adding -A.
KeepCarry = NegOperation; //We inventeg the Neg flag literally for this.
Flags_DataMUX0 = AllAre0 | CarryOut_MUX<<1 | NegOperation<<2; //Zero, Carry and Neg flags. We know them, we love them.
if (Flags_DataMuxN) Flags_DataMUX = Flags_DataMUX1; else Flags_DataMUX = Flags_DataMUX0; //For the 1xx opcodes, we select the actual flag values. For the 0xx, we select the Seg which can be used to store them. It'll not be written unless the exact MOV A, P is engaged.
Думаю, даже сидящие рядом с читателем кошки уже заметили, что логика АЛУ проста, как кирпич — один вход всегда аккумулятор, выход всегда идёт в аккумулятор (и примкнувшие к нему три флага), а второй вход мы выбираем стандартным выбором операнда (с той только разницей, что мы не можем читать PC, зато можем читать флэшку с юзерским кодом и его данными). Поэтому практически всё сводится, опять же, к пачке «мухов»: мы выбираем, откуда брать второй операнд, откуда брать флаг переноса (из Carry Flag или из Neg Flag), ну и откуда брать результат. Причём взять его «из-под носа АЛУ, прямо со второго входа» тоже вариант — им реализуется команда MOV A, R. Но сначала нам, конечно, надо вообще определиться, в ту ли в принципе группу команд мы в этот раз попали? Названия «Opcode_какие-то-биты» говорят сами за себя. И, конечно, если любой из них «выстреливает», то аккумулятор на этом такте перезаписывать надо. А вот флаги, как видно чуть ниже, мы перезаписываем только в случае, если команда реально что-то делает (старшая четвёрка, без MOV), или таки MOV, но особенный, восстанавливающий A из P, а флаги — из Seg. И рядом сразу же задаём для их «муха» особенный источник (тот самый Seg), если команда ничего не вычисляет (то есть MOV).
Дальше идёт выбор второго операнда — наше «любимое» мозголомное ветвление, но в этот раз от младших бит к старшим, то есть плюс-минус в порядке прохождения сигнала. Число веток не является в этот раз степенью двойки, но тем, кто осилил со мной разобраться в предыдущем ветвлении, не составит труда уже и этот клубок сопоставить с тем, как от каждого бита операнда зависит то, какую шину данных мы должны выбрать. А также заметить то, что среди них нет P! Разумеется, это потому, что мы выбираем операнд именно для АЛУ — а АЛУ работает всегда с A. P — привилегия команды MOV A, R, потому что MOV A, A не имеет вообще никакого смысла (да и надо ведь как-то перекладывать обратно в аккумулятор и флаги значения из P и Seg), а вот все остальные команды имеют какой-то смысл, даже если их совершать над аккумулятором и аккумулятором же.
Само АЛУ я описал препохабнейше. Просто как набор отдельных операций на Си. Тут ситуация как со SRAM — надо или здесь и сейчас развивать свою «феню», или описывать каждый логический элемент, сделав это месиво окончательно нечитаемым, или просто на словах объяснить, что там должен быть библиотечный сумматор из шести таких вот разрядов:
Классический «nine-NAND» из Википедии.
Как нетрудно видеть, там есть и NAND (с него всё начинается), и XOR (первая четвёрка), и ADD (всё вместе). Вопрос только в том, откуда это всё с него каким-то дополнительным проводком вытянуть. Эти проводки, включая выходной, носят довольно очевидные имена «ALU_Out-что-то-там» и поступают, как нетрудно догадаться, на вход схемы выбора результата (вместе со вторым операндом и отдельно специально приехавшим туда P, который, как мы видели выше, выбирается по слегка другой логике, нежели второй операнд). Схема эта традиционно многоэтажна, обла, озорна, огромна, стозевна и лаяй, а ещё у неё есть несколько промежуточных выходов: после того, как мы выбрали, скажем, XOR у нас или NAND, мы можем отправить этот результат на дальнейшие туры конкурса, а пока от него тихо в сторонке посчитать наш «липовый Carry для быстрой проверки значения бита»: равны все биты логического результата единице или нет?
Дальше мы из двух вариантов (настоящий арифметический и липовый логический) выбираем значение для будущего Carry Flag, значение для Zero Flag выбирать не из чего — оно всегда определяется вопросами равенства результата нулю, буде у нас вообще регистр флагов в принципе записан по итогам (не считая восстановления его через MOV A, P), Neg Flag бывает в единичке только тогда, когда операция была конкретно NAND A, A, ну а дальше у нас выставляется особенная линия, требующая от нас синтезировать особо головоломный регистр под эти самые флаги — линия «не трожь Carry», которая позволяет сделать для его записи исключение, даже когда пошла команда писать весь регистр флагов (альтернативное решение — сделать отдельные линии для команд записи Carry и всего остального, разумеется). Линия эта активна как минимум в той ситуации, когда мы выполнили этот самый NAND A, A, потому что нам надо в этом случае флаг переноса обязательно сохранить — без этого никакая относительно быстрая арифметика не заработает, как мы увидим сейчас, разбирая микрокод к этой машинке. А ещё можете на свой вкус добавить туда второй вариант активации этой линии — в случае, если команда была XOR, а не NAND. Этой дилеммой мы тоже озадачимся при разборе микрокода поподробнее.
Завершает главную секцию выбор того, надо ли нам записать флаги после операции или флаги, взятые из Seg при их восстановлении. Это тривиально.
Код:
ClockFallingEdgeEnd
if (Writing_SRAM) SRAM[SRAM_Addr_Bus] = SRAM_In_Bus;
ClockRisingEdgeBegin
ClockRisingEdgeEnd
Тут у нас находится крошечная секция того, что мы должны сделать по заднему фронту (сиречь спаду) синхроимпульса: включить, если нужно, мощные «пишущие транзисторы» SRAM. Ирония ситуации в том, что положение этой строчки совершенно не означает, что вся орава, которую мы только что разбирали в основной секции, должна завершиться к этому моменту: в конце концов, это только половина интервала тактирования, и там половина этих процессов ещё идёт. Впрочем, я это уже говорил в самом начале. Да и настоящей библиотеки у нас нет, поэтому мы просто присваиваем значение элементу массива. Чик — и записали. Магия.
Ну, и за ней — секция того, что индифферентно к переднему фронту. Она пуста: такого у нас просто нет. Всё или должно закончиться до него (простынки выше), или триггерится непосредственно им (к этому мы перейдём сейчас, потому что описание этого всего как раз здесь-то и начинается…)
Код:
cout<<(int)PC<<" ";
//First of all, we must stop the SRAM writing lines here, because rising edge means no more stable signals! New loop begins it's lags, delays and "binary spikes".
//At the same time, latches have their new values.
if (Opcode_SCN) NibbleCounter_2bit++; else NibbleCounter_2bit=0; //A counter with a reset input can be processed this way.
if (Writing_PC) PC = (PC_Data_H_MUXH&63)<<6 | (PC_Data_L_NoMUX&63); else PC++; //Yes, we can synth this by using a counter with a latch option.
if (Writing_P) Reg_P = (P_DataMUX&63); //And even this is possible! It's a reversible counter with separate latch options for lower 6 bits and higher 3 bits.
if (Writing_Seg) Reg_Seg = (Seg_DataMUX&63);
if (!Writing_P && !Writing_Seg)
{
if (Increment_SegP_Pair && !Decrement_SegP_Pair)
{
Reg_Seg = (Reg_Seg<<6|Reg_P)+1 >> 6;
Reg_P = (Reg_Seg<<6|Reg_P)+1 & 63;
}
if (!Increment_SegP_Pair && Decrement_SegP_Pair)
{
Reg_Seg = (Reg_Seg<<6|Reg_P)-1 >> 6;
Reg_P = (Reg_Seg<<6|Reg_P)-1 & 63;
}
if (Increment_SegP_Pair && Decrement_SegP_Pair)
{
Invalid (Reg_Seg); //for the C compiler, it means stop with error. For the synthesizer, it means "don't care about this case and don't waste any gates on it, it's impossible".
Invalid (Reg_P);
}
}
if ((Increment_SegP_Pair || Decrement_SegP_Pair) && (Writing_P || Writing_Seg))
{
Invalid (Reg_Seg); //for the C compiler, it means stop with error. For the synthesizer, it means "don't care about this case and don't waste any gates on it, it's impossible".
Invalid (Reg_P);
}
if (Writing_B) Reg_B = (B_L_H_Data_NoMUX&63);
if (Writing_L) Reg_L = (B_L_H_Data_NoMUX&63);
if (Writing_H) Reg_H = (B_L_H_Data_NoMUX&63);
if (Writing_A) Reg_A = (A_DataMUX_MSB&63); //Oh yeah! Finally, the actual math/boolean result goes into the Acc!
if (Writing_Fl)
{
if (KeepCarry) //Yep, this flip-flop must be controlled separately
{
Reg_Fl = Flags_DataMUX&5 | Reg_Fl&2; //We don't rewrite bit 1
} else {
Reg_Fl = Flags_DataMUX; //We rewrite all flags
}
}
Ну, логирование в stdout явно не относится к защёлкам и счётчикам — а вот остальное вполне даже да. В самом начале, будь у нас настоящая библиотека с настоящей SRAM, нам надо было бы подать команду на отключение транзисторов записи (и они ещё должны успеть отключиться раньше, чем какое-нибудь изменение какого-нибудь регистра тут пролетит через все цепи и изменит, скажем, адрес или данные на входе; но, во-первых, обычно библиотеки проектируют с такими параметрами цепей, чтобы оно более-менее без геморроя стыковалось, а во-вторых — никакой регистр, способный на это повлиять, не может измениться на том же такте, на котором память записывалась, потому что это разные операции).
Дальше мы видим, как простейший двухбитный счётчик со сбросом, который я почему-то поленился нормально описать выше, инкрементируется для случаев, когда команда была SCN, и сбрасывается во всех остальных случаях. Счёт у него «гвоздями прибит», а вот Reset — его единственный управляющий сигнал. А вот PC уже немножко посложнее: счёт так же «прибит гвоздями», но по специальному управляющему сигналу он может читать входное значение вместо того, чтобы инкрементировать текущее.
Ещё сложнее у нас парочка P и Seg: мало того, что этот 9-битный счётчик является управляемым и без единички на управляющем входе «не тикает», так он ещё и реверсивен, то есть умеет считать в обратном направлении, если единичка на другом управляющем входе! Ну, и отдельную офигенную круть ему придаёт то, что у него есть раздельные входы для P и Seg, позволяющие читать входное значение для одного и/или другого по отдельности. А чтобы синтезатор не рехнулся понимать, что мы имели в виду — как я уже расписывал в деталях выше, ему надо указать, какие ситуации не нужно вовсе принимать к рассмотрению.
В этом месте я предлагаю подумать и порассуждать в комментариях, а нужен ли нам этот самый декремент? Не лучше ли сделать регистры-дубликаты (что по числу транзисторов может быть где-то порядка этого самого свойства реверсируемости), а третий вариант операнда для работы с памятью (шестой операнд в абсолютных величинах) превратить в «выполни доступ к памяти и свопни регистры друг с другом»? Это может сильно ускорить многобайтовые операции над двумя операндами. Даже больше, лучше и удобнее, чем это вот «убрать запись CF по XOR и использовать XOR для прыжков между двумя значениями P». И как именно, кстати, сделать эту перестановку, чтобы она не мешала автоинкременту (типичному для таких операций, как мы увидим уже практически прямо сейчас)?
Ну, и завершает секцию запись, если оно было нужно, регистров A, B, H, L, ну и флагов — как полностью, так и с исключением для Carry, буде тот требовалось сохранить.
Код:
cout<<(int)(ROM_Data_Bus>>3)<<" "<<(ROM_Data_Bus&OPERAND)<<" "<<(int)Reg_A<<" "<<(int)Reg_Fl<<" "<<(int)Reg_B<<" "<<(int)Reg_L<<" "<<(int)Reg_H<<" "<<(int)Reg_P<<" "<<(int)Reg_Seg<<" "<<(int)PC<<endl;
if (ROM_Data_Bus>>6 == 1)
{
for (int i=0; i<8*64; i++) cout<<i<<" ";
cout<<endl;
for (int i=0; i<8*64; i++) cout<<(int)(SRAM[i])<<" ";
cout<<endl;
if (ROM_Data_Bus==DUMP_SRAM) exit (0); //Last dump on the emulation termination
}
}
}
}
…и пятисекундка логирования. Значения регистров, при необходимости — дамп памяти, а если его заказали для команды SCN 0 — симулятор завершается
Скрытый текст
Код:
// First of all, here are actual assembler mnemonics (I think it's nearly impossible to forget the device have six-bit bytes instead of eight-bit, but...):
// SCN 0..7 Set Constant Nibble (up to four calls in a row; each one causes different effects).
// MOV P, A Stores accumulator in P register and Flags in Seg register
// MOV B, A Stores accumulator in B register
// MOV L, A Stores accumulator in L(ow) register
// MOV H, A Stores accumulator in H(igh) register
// MOV SRAM, A Stores accumulator in SRAM[Seg:P] memory byte
// MOV SRAMP, A Stores accumulator in SRAM[Seg:P] memory byte; increments the Seg:P register pair value.
// MOV SRAMM, A Stores accumulator in SRAM[Seg:P] memory byte; decrements the Seg:P register pair value.
// MOV PC, A:P Unconditional jump to A:P address in microcode ROM. Alternate mnemonics can be JMP A:P
// Jz B:P Jump to B:P address in microcode ROM, if Zero flag is set. Jump to Seg:P, if Zero flag is set and exactly three SCN commands has been executed before Jz.
// Jnz B:P Jump to B:P address in microcode ROM, if Zero flag is clear. Jump to Seg:P, if Zero flag is clear and exactly three SCN commands has been executed before Jnz.
// Jc B:P Jump to B:P address in microcode ROM, if Carry flag is set. Jump to Seg:P, if Carry flag is set and exactly three SCN commands has been executed before Jc.
// Jnc B:P Jump to B:P address in microcode ROM, if Carry flag is clear. Jump to Seg:P, if Carry flag is clear and exactly three SCN commands has been executed before Jnc.
// Jz A:P Jump to A:P address in microcode ROM, if Zero flag is set. Jump to (8+Seg):P, if Zero flag is set and exactly three SCN commands has been executed before Jz.
// Jnz A:P Jump to A:P address in microcode ROM, if Zero flag is clear. Jump to (8+Seg):P, if Zero flag is clear and exactly three SCN commands has been executed before Jnz.
// Jc A:P Jump to A:P address in microcode ROM, if Carry flag is set. Jump to (8+Seg):P, if Carry flag is set and exactly three SCN commands has been executed before Jc.
// Jnc A:P Jump to A:P address in microcode ROM, if Carry flag is clear. Jump to (8+Seg):P, if Carry flag is clear and exactly three SCN commands has been executed before Jnc.
// MOV A, P Restores accumulator from P register and Flags from Seg register
// MOV A, B Restores accumulator from B register
// MOV A, L Restores accumulator from L(ow) register
// MOV A, H Restores accumulator from H(igh) register
// MOV A, SRAM Loads accumulator from SRAM[Seg:P] memory byte
// MOV A, SRAMP Loads accumulator from SRAM[Seg:P] memory byte; increments the Seg:P register pair value.
// MOV A, SRAMM Loads accumulator from SRAM[Seg:P] memory byte; decrements the Seg:P register pair value.
// MOV A, ROM Loads accumulator from user flash ROM[H:L] (not to be confused with the microcode ROM!) which stores the actual user program and data to interpret/execute with our VM/microcode.
// I'm not certain on it's either a microcode or a native code for VM. For a microcode, those assembler commands are way too beefy. For a real native code, even for a very minimalistic uC, they totally lack any call/ret/stack support.
// XOR A, A Sets A to zero. Sets Zero flag, clears Carry and Neg flags.
// XOR A, B Calculates bitwise exclusive OR of the accumulator value and register B value, stores it in accumulator. Always clears Neg flag. Sets/clears Zero and Carry(!) flag if new accumulator value is 0 or 63 (which is not an actual carry, but still a useful option).
// XOR A, L Calculates bitwise exclusive OR of the accumulator value and register L value, stores it in accumulator. Always clears Neg flag. Sets/clears Zero and Carry(!) flag if new accumulator value is 0 or 63 (which is not an actual carry, but still a useful option).
// XOR A, H Calculates bitwise exclusive OR of the accumulator value and register H value, stores it in accumulator. Always clears Neg flag. Sets/clears Zero and Carry(!) flag if new accumulator value is 0 or 63 (which is not an actual carry, but still a useful option).
// XOR A, SRAM Calculates bitwise exclusive OR of the accumulator value and SRAM[Seg:P] memory byte; stores it the same way and sets flags the same way (clears Neg, sets Zero if new A value is zero (otherwise clears Zero), sets Carry if new A value is binary 111111, otherwise clears Carry).
// XOR A, SRAMP Calculates bitwise exclusive OR of the accumulator value and SRAM[Seg:P] memory byte; increments the Seg:P register pair value, sets Flags the same way.
// XOR A, SRAMM Calculates bitwise exclusive OR of the accumulator value and SRAM[Seg:P] memory byte; decrements the Seg:P register pair value, sets Flags the same way.
// XOR A, ROM Calculates bitwise exclusive OR of the accumulator value and user flash ROM[H:L] byte value (not to be confused with the microcode ROM!); sets Flags the same way.
// NAND A, A Alternate mnemonics is NEG A. Bitwise inversion of the accumulator. Sets Neg flag (it's the only operation which does). Zero is set if A was 63 before (it's 0 now). Carry is kept intact: we need it for multi-byte subtraction.
// NAND A, B Bitwise AND of the accumulator value and register B value, inverted. Flags are set the common way (for bitwise operations): Neg is clear, Zero is set/clear if the result is zero/non-zero, Carry is set/clear if the result is 63/less than 63.
// NAND A, L Bitwise AND of the accumulator value and register L value, inverted. Flags are set the common way (for bitwise operations): Neg is clear, Zero is set/clear if the result is zero/non-zero, Carry is set/clear if the result is 63/less than 63.
// NAND A, H Bitwise AND of the accumulator value and register H value, inverted. Flags are set the common way (for bitwise operations): Neg is clear, Zero is set/clear if the result is zero/non-zero, Carry is set/clear if the result is 63/less than 63.
// NAND A, SRAM Bitwise AND of the accumulator value and SRAM[Seg:P] memory byte value, inverted. Flags are set the common way (for bitwise operations): Neg is clear, Zero is set/clear if the result is zero/non-zero, Carry is set/clear if the result is 63/less than 63.
// NAND A, SRAMP Bitwise AND of the accumulator value and SRAM[Seg:P] memory byte, inverted; increments the Seg:P register pair value. Flags are set the common way (for bitwise operations: Neg is clear, Zero is set/clear if the result is zero/non-zero, Carry is set/clear if the result is 63/less than 63.
// NAND A, SRAMM Bitwise AND of the accumulator value and SRAM[Seg:P] memory byte, inverted; decrements the Seg:P register pair value. Flags are set the common way (for bitwise operations): Neg is clear, Zero is set/clear if the result is zero/non-zero, Carry is set/clear if the result is 63/less than 63.
// NAND A, ROM Bitwise AND of the accumulator value and user flash ROM[H:L] byte (not to be confused with the microcode ROM!), inverted. Flags are set the common way (for bitwise operations): Neg is clear, Zero is set/clear if the result is zero/non-zero, Carry is set/clear if the result is 63/less than 63.
// ADN A, A Arithmetic addition of the accumulator value to the accumulator value; also increments the result if the Neg flag is set. So A=A+A+Neg. Clears the Neg flag, sets the Zero flag if the result is 0 (obviously clears it if not), sets the Carry flag if the result is greater than 63 (obviously clears it if not).
// ADN A, B Arithmetic addition of B register value to the accumulator value; also increments the result if the Neg flag is set. So A=A+B+Neg. Clears the Neg flag, sets two others the same way as prevous, which is the common use of Zero and Carry flags in most processors in the world.
// ADN A, L Arithmetic addition of L register value to the accumulator value; also increments the result if the Neg flag is set. So A=A+L+Neg. Clears the Neg flag, sets two others the same way.
// ADN A, H Arithmetic addition of H register value to the accumulator value; also increments the result if the Neg flag is set. So A=A+H+Neg. Clears the Neg flag, sets two others the same way.
// ADN A, SRAM Arithmetic addition of SRAM[Seg:P] memory byte value to the accumulator value; also increments the result if the Neg flag is set. Clears the Neg flag, sets two others the same way.
// ADN A, SRAMP Arithmetic addition of SRAM[Seg:P] memory byte to the accumulator value; also increments the result if the Neg flag is set. Increments the Seg:P register pair value. Clears the Neg flag, sets two others the same way.
// ADN A, SRAMM Arithmetic addition of SRAM[Seg:P] memory byte to the accumulator value; also increments the result if the Neg flag is set. Decrements the Seg:P register pair value. Clears the Neg flag, sets two others the same way.
// ADN A, ROM Arithmetic addition of user flash ROM[H:L] byte value (not to be confused with the microcode ROM!) memory byte to the accumulator value; also increments the result if the Neg flag is set. Flags are set the common (for arithmetic operations) way.
// ADC A, A Arithmetic addition of the accumulator value to the accumulator value, with carry. ADN is literally the same as ADC with Neg flag used instead of Carry flag. Flags are set the common (for arithmetic operations) way.
// ADC A, B Arithmetic addition of B register value to the accumulator value, with carry; so A=A+B+Carry. Flags are set the common (for arithmetic operations) way.
// ADC A, L Arithmetic addition of L register value to the accumulator value, with carry; so A=A+L+Carry. Flags are set the common (for arithmetic operations) way.
// ADC A, H Arithmetic addition of H register value to the accumulator value, with carry; so A=A+H+Carry. Flags are set the common (for arithmetic operations) way.
// ADC A, SRAM Arithmetic addition of SRAM[Seg:P] to the accumulator value, with carry. Flags are set the common (for arithmetic operations) way.
// ADC A, SRAMP Arithmetic addition of SRAM[Seg:P] to the accumulator value, with carry; increments the Seg:P register pair value. Flags are set the common (for arithmetic operations) way.
// ADC A, SRAMM Arithmetic addition of SRAM[Seg:P] to the accumulator value, with carry; decrements the Seg:P register pair value. Flags are set the common (for arithmetic operations) way.
// ADC A, ROM Arithmetic addition of user flash ROM[H:L] byte value (not to be confused with the microcode ROM!) to the accumulator value, with carry. Flags are set the common (for arithmetic operations) way.
// Now the debugging/testing code. Due to the lack of the actual assembler, I use C #define to write something readable, or maybe even similar to the actual mnemonics.
SCN 0x3, //Goes to P (high)
SCN 0x1, //Goes to P (high), while prev. is shifted to P (low)
SCN 0x4, //Goes to Seg
SCN 0x7, //Goes to PC MSBs (with Seg), and P goes to PC LSBs. So we jump to (7*8+4)*64 + (1*8+3) = 3851
//After we returned...
XOR_A A, //Reset the A and set the ZF
MOV_R_A P, //MOV P, A and check if Seg stores the ZF
NAND_A A, //Inverts A and sets the NegF
MOV_R_A P, //MOV P, A and check if Seg stores the NegF
SCN 0x6, //Goes to P (high)
SCN 0x3, //Goes to P (high), while prev. is shifted to P (low)
MOV_A P, //Copy P to A
MOV_R_A B, //Copy A to B
ADN_A B, //Double A plus one
MOV_R_A H, //Copy A to H
ADC_A B, //Overflow to set the Carry flag
MOV_R_A L, //Copy A to L
SCN 0x5, //Goes to P (high)
SCN 0x7, //Goes to P (high), while prev. is shifted to P (low)
SCN 0x2, //Goes to Seg
MOV_R_A SRAM_P DUMP_SRAM, //Copy A to SRAM[2:61] and dump it for debugging purposes
ADC_A L,
MOV_R_A SRAM_PPP,
ADC_A H,
MOV_R_A SRAM_PPP,
ADC_A B,
MOV_R_A SRAM_PPP DUMP_SRAM,
ADN_A A, //Calculating some sort of a bigger address to jump (Segment)
SCN 0x5,
SCN 0x1, //Loading Offset to P
SCN 0x7, //For totally different purpose!
MOV_R_A PC_JUMP, //Jumping to A:P
//After we returned...
MOV_R_A B,
NAND_A B, //must cause "Carry" flag (actually, "all 1" flag)
SCN 6,
SCN 4,
JCC NC_A, //this time we shouldn't jump
JCC C_B, //this time we should
DUMP_SRAM, //...and terminate. But we jump to the next operation before.
XOR_A A, //Reset the A and set the ZF
SCN 3,
SCN 5,
JCC Z_A, //jumping again
DUMP_SRAM, //...and terminate. But we jump to the next operation before.
SCN 7,
SCN 7,
SCN 7,
JCC Z_A, //Having Seg set exactly in prev. op., we totally ignore A and jump to 1000+Seg:P instead. It's 1023.
//After we returned, let's begin a more complicated code. Such as multi-byte copy in reverse order, for example.
SCN 1,
SCN 0,
MOV_A P,
MOV_R_A B,
SCN 2,
SCN 0,
MOV_A P,
MOV_R_A H,
SCN 0,
SCN 5,
MOV_A P,
MOV_R_A L, //Src addr loaded.
SCN 7,
SCN 0,
SCN 2, //Dst addr loaded.
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM DUMP_SRAM,
//Now let's have another copy, but without reverse
SCN 1,
SCN 0,
MOV_A P,
MOV_R_A B,
SCN 2,
SCN 0,
MOV_A P,
MOV_R_A H,
SCN 0,
SCN 5,
MOV_A P,
MOV_R_A L, //Src addr loaded.
SCN 0,
SCN 2,
SCN 2, //Dst addr loaded.
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP DUMP_SRAM,
//And now, we dare to subtract them. X=X-Y.
SCN 0,
SCN 0,
SCN 2, //Y byte 0 Seg:P
MOV_A SRAM_P,
NAND_A A, //Neg Y byte 0
SCN 0,
SCN 2, //X byte 0, Seg is the same
ADN_A SRAM_P,
MOV_R_A SRAM_P,
SCN 1,
SCN 0, //Y byte 1
MOV_A SRAM_P,
NAND_A A,
SCN 1,
SCN 2, //X byte 1
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 2,
SCN 0, //Y byte 2
MOV_A SRAM_P,
NAND_A A,
SCN 2,
SCN 2, //X byte 2
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 3,
SCN 0, //Y byte 3
MOV_A SRAM_P,
NAND_A A,
SCN 3,
SCN 2, //X byte 3
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 4,
SCN 0, //Y byte 4
MOV_A SRAM_P,
NAND_A A,
SCN 4,
SCN 2, //X byte 4
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 5,
SCN 0, //Y byte 5
MOV_A SRAM_P,
NAND_A A,
SCN 5,
SCN 2, //X byte 5
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 6,
SCN 0, //Y byte 6
MOV_A SRAM_P,
NAND_A A,
SCN 6,
SCN 2, //X byte 6
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 7,
SCN 0, //Y byte 7
MOV_A SRAM_P,
NAND_A A,
SCN 7,
SCN 2, //X byte 7
ADC_A SRAM_P,
MOV_R_A SRAM_P,
DUMP_SRAM, //terminate at last!
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
SCN 7,
SCN 5,
SCN 0,
JCC Z_B, //Jumping back. This time without any additional "1" because we use the pseudo-"B" (actually Seg because we loaded it exactly in prev. op.)
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
MOV_A P, //Checking our fake Flags
XOR_A A, //resetting A (Segment = 0)
SCN 0x7,
SCN 0x3, //setting offset to 31 (continue our code)
MOV_R_A PC_JUMP,//getting back
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
SCN 0x4, //Goes to P (high)
SCN 0x0, //Goes to P (high), while prev. is shifted to P (low)
SCN 0x0, //Goes to Seg
SCN 0x0, //Goes to PC MSBs (with Seg), and P goes to PC LSBs. So we jump back to 4 and continue the code.
Начинается он с камментов на буржуинском (я ещё надеюсь довести его до достаточного для itch.io уровня приличия, чтобы выложить в качестве «игрушечного», но не советую кому-то надеяться вместе со мной, особенно с учётом того, что мы ещё не определились с теми самыми дилеммами), после чего можно посмотреть сам «код». Приступим же.
Код:
SCN 0x3, //Goes to P (high)
SCN 0x1, //Goes to P (high), while prev. is shifted to P (low)
SCN 0x4, //Goes to Seg
SCN 0x7, //Goes to PC MSBs (with Seg), and P goes to PC LSBs. So we jump to (7*8+4)*64 + (1*8+3) = 3851
Сначала, конечно, проверяем работу нашей самой задорной команды. Делаем безусловный переход аж на 3851-ю строку. Там нас ждёт просто переход обратно:
Код:
SCN 0x4, //Goes to P (high)
SCN 0x0, //Goes to P (high), while prev. is shifted to P (low)
SCN 0x0, //Goes to Seg
SCN 0x0, //Goes to PC MSBs (with Seg), and P goes to PC LSBs. So we jump back to 4 and continue the code.
Это как раз та причина, по которой под спойлер лучше без нужды не заглядывать. Я, конечно, мог бы этого избежать, но при современном весе веб-страниц в целом и любой страницы Хабра в частности не вижу в этом смысла — подумаешь, почти 4 тысячи строк из одних нулей. Килобайты. Зато на случай всякого… всякого всякого без всяких внешних ресурсов всё, необходимое для сборки проекта, лежит неотъемлемо от статьи. Менять же тестовые адреса «на что-то попроще» тоже не хочу — длинный прыжок важен, а мы будем ещё наверняка что-то в процессоре менять и тестировать его заново.
Код:
//After we returned...
XOR_A A, //Reset the A and set the ZF
MOV_R_A P, //MOV P, A and check if Seg stores the ZF
NAND_A A, //Inverts A and sets the NegF
MOV_R_A P, //MOV P, A and check if Seg stores the NegF
SCN 0x6, //Goes to P (high)
SCN 0x3, //Goes to P (high), while prev. is shifted to P (low)
MOV_A P, //Copy P to A
MOV_R_A B, //Copy A to B
ADN_A B, //Double A plus one
MOV_R_A H, //Copy A to H
ADC_A B, //Overflow to set the Carry flag
MOV_R_A L, //Copy A to L
SCN 0x5, //Goes to P (high)
SCN 0x7, //Goes to P (high), while prev. is shifted to P (low)
SCN 0x2, //Goes to Seg
MOV_R_A SRAM_P DUMP_SRAM, //Copy A to SRAM[2:61] and dump it for debugging purposes
ADC_A L,
MOV_R_A SRAM_PPP,
ADC_A H,
MOV_R_A SRAM_PPP,
ADC_A B,
MOV_R_A SRAM_PPP DUMP_SRAM,
ADN_A A, //Calculating some sort of a bigger address to jump (Segment)
SCN 0x5,
SCN 0x1, //Loading Offset to P
SCN 0x7, //For totally different purpose!
MOV_R_A PC_JUMP, //Jumping to A:P
Тут мы развлекаемся с разными командами и напоследок вычисляем адрес в виде A😛 (регистр P, правда, задаём напрямую), причём зачем-то перед этим задали и Seg тоже, хотя для перехода это совсем не нужно…
Код:
MOV_A P, //Checking our fake Flags
XOR_A A, //resetting A (Segment = 0)
SCN 0x7,
SCN 0x3, //setting offset to 31 (continue our code)
MOV_R_A PC_JUMP,//getting back
…а потому, что там, за туманами и простынёй нулей, мы проверяем «восстановление» флагов из Seg. А потом — делаем скок обратно.
Код:
//After we returned...
MOV_R_A B,
NAND_A B, //must cause "Carry" flag (actually, "all 1" flag)
SCN 6,
SCN 4,
JCC NC_A, //this time we shouldn't jump
JCC C_B, //this time we should
DUMP_SRAM, //...and terminate. But we jump to the next operation before.
XOR_A A, //Reset the A and set the ZF
SCN 3,
SCN 5,
JCC Z_A, //jumping again
DUMP_SRAM, //...and terminate. But we jump to the next operation before.
SCN 7,
SCN 7,
SCN 7,
JCC Z_A, //Having Seg set exactly in prev. op., we totally ignore A and jump to 1000+Seg:P instead. It's 1023.
Проверяем корректную работу Jcc, не во всех вариантах, конечно, но хотя бы в самых важных. Ставим команду SCN 0, больше для симулятора, чем для проца (состоит из одного только префикса DUMP_SRAM) — она выполняет остановку симуляции, и перепрыгиваем её. А потом нам надо проверить, как работает подмена A/B на Seg…
Код:
SCN 7,
SCN 5,
SCN 0,
JCC Z_B, //Jumping back. This time without any additional "1" because we use the pseudo-"B" (actually Seg because we loaded it exactly in prev. op.)
…что опять потребовало командировку к чёрту на рога и обратно. Теперь давайте о серьёзных вещах.
Код:
//After we returned, let's begin a more complicated code. Such as multi-byte copy in reverse order, for example.
SCN 1,
SCN 0,
MOV_A P,
MOV_R_A B,
SCN 2,
SCN 0,
MOV_A P,
MOV_R_A H,
SCN 0,
SCN 5,
MOV_A P,
MOV_R_A L, //Src addr loaded.
SCN 7,
SCN 0,
SCN 2, //Dst addr loaded.
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PMM DUMP_SRAM,
В пользовательской флэш-памяти лежит аж восьмибайтное число (точнее, 48-битное, как у добротного DSP — байты-то у нас так себе, не уродились, как репа в плохой год). Сейчас мы его будем читать в SRAM по-человечески и задом наперёд, чтобы получить из него два разных числа.
Никакой экономии операций я тут не пытался делать, просто «в лоб» прогрузил H, L, единичку в B и адрес назначения в Seg😛. И просто, спокойно, отматывая декрементом адрес назначения назад, а адрес источника проматывая простым сложением вперёд, вычитал спокойненько все байты. Естественно, адрес назначения грузил соответствующий последнему байту — он же отматывается назад!
Код:
//Now let's have another copy, but without reverse
SCN 1,
SCN 0,
MOV_A P,
MOV_R_A B,
SCN 2,
SCN 0,
MOV_A P,
MOV_R_A H,
SCN 0,
SCN 5,
MOV_A P,
MOV_R_A L, //Src addr loaded.
SCN 0,
SCN 2,
SCN 2, //Dst addr loaded.
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP,
MOV_A L,
ADN_A B,
MOV_R_A L, //Short address (no carry to H)
MOV_A FLROM_HL,
MOV_R_A SRAM_PPP DUMP_SRAM,
Теперь, в том же сегменте, но на небольшом расстоянии, снова выставил адрес назначения — уже на первый байт, после чего снова прочитал то же число, но уже проматывая оба адреса вперёд. Переходим к сладкому: используя Intel Byte Order, вычитаем одно из другого. Со сложением всё просто — бери да складывай. А как вычитать?
Код:
//And now, we dare to subtract them. X=X-Y.
SCN 0,
SCN 0,
SCN 2, //Y byte 0 Seg:P
MOV_A SRAM_P,
NAND_A A, //Neg Y byte 0
SCN 0,
SCN 2, //X byte 0, Seg is the same
ADN_A SRAM_P,
MOV_R_A SRAM_P,
SCN 1,
SCN 0, //Y byte 1
MOV_A SRAM_P,
NAND_A A,
SCN 1,
SCN 2, //X byte 1
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 2,
SCN 0, //Y byte 2
MOV_A SRAM_P,
NAND_A A,
SCN 2,
SCN 2, //X byte 2
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 3,
SCN 0, //Y byte 3
MOV_A SRAM_P,
NAND_A A,
SCN 3,
SCN 2, //X byte 3
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 4,
SCN 0, //Y byte 4
MOV_A SRAM_P,
NAND_A A,
SCN 4,
SCN 2, //X byte 4
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 5,
SCN 0, //Y byte 5
MOV_A SRAM_P,
NAND_A A,
SCN 5,
SCN 2, //X byte 5
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 6,
SCN 0, //Y byte 6
MOV_A SRAM_P,
NAND_A A,
SCN 6,
SCN 2, //X byte 6
ADC_A SRAM_P,
MOV_R_A SRAM_P,
SCN 7,
SCN 0, //Y byte 7
MOV_A SRAM_P,
NAND_A A,
SCN 7,
SCN 2, //X byte 7
ADC_A SRAM_P,
MOV_R_A SRAM_P,
DUMP_SRAM, //terminate at last!
…а для вычитания мы используем дополнительный код. Для этого все 8 байт вычитаемого нужно инвертировать побитно, прибавить единичку и побайтно сложить с переносом с уменьшаемым. Причём крайне желательно не пихать куда-то в память ещё и промежуточный (инвертированный) результат — её и так мало!
На помощь, конечно, приходит команда NAND_A A и её отдельный флаг. Для того, чтобы у нас добавилась строго единичка, мы после инвертирования первого байта используем для сложения команду ADN. Флаг «результат был инвертирован», он же флаг Neg («инверсия с целью получения отрицательного числа в дополнительном коде», да-да!) для того и сделан нами. Единичка прибавляется к своему законному результату, и он становится первым байтом разности. Перейдя к следующему, мы снова делаем NAND_A A, но настоящий-то флаг Carry он не трогает! Поэтому ADC прекрасно выполняет обработку следующего байта, уже с совершенно настоящим переносом. И так далее. Один флаг, а уже сразу аппаратная поддержка вычитания!
Ну, и в конце уже не перепрыгиваем остановку симулятора, а честно выполняем.
А вот с адресацией у нас тут полное болото. Ну то есть, в принципе, для микрокода это может быть не так уж и страшно: есть фиксированные места в SRAM, в которых лежат регистры целевого процессора, и можно сделать примерно как в этом примере, путём копирования в нужное место и там уже сложения/вычитания. Да оно ещё и повторяется в каждом сегменте, то есть можно задать Seg до начала и дальше всё тот же код выполнит сложение/вычитание для тех переменных, которые в этом сегменте лежат на предназначенных для сложений и вычитаний местах. Но надо ли говорить, что это уже не минималистичный процессор, а какой-то почти эзотерический? Это, конечно, нормально «в Tiny-исполнении», когда задача в основном время показывать и барабан стиралки крутить, а память скукожилась обратно в 64 байта, включая порты, и никаких инкрементов-декрементов вовсе нет. Но «в Mega-исполнении» же дичь?
Итак, я жду в комментариях оценки следующих вещей:
1) Какое применение можно придумать для флага «результат из одних единиц» (он же — «фальшивый Carry») для команды XOR (с NAND всё понятно — быстрая проверка наличия единицы в некоем бите). Надо ли вообще за него держаться, или просто сделать KeepCarry для XOR тоже?
2) А поможет ли это? Ведь даже если мы положим XOR от обоих адресов в B и будем регулярно брать адрес из P, получать из него то один, то другой при помощи XOR его с B (господи, да оно вообще сработает ли хотя бы математически, или это мне мерещится?), класть его обратно — флаги ведь последуют вместе с ним? Не придётся ли для этого ещё и отказываться от сохранения флагов в Seg?
3) А не придётся ли отказываться от сохранения флагов в Seg в любом случае? Не покажут ли первые полчаса попыток играться с этим процом, что они больше вредят, чем помогают? А если помогают, стоят ли они потраченных на них «мухов»?
4) А не лучше ли сделать, действительно, теневую пару со вторыми Seg😛 и переключать по необходимости после доступа к памяти, вместо этого дурацкого декремента?
5) А как при этом инкрементировать, чтобы перейти к следующему байту операнда?
Попробуйте представить себе, как выглядел бы код, если изменить работу команд тем или иным образом, и написать его, а потом — прокомментировать. На этом я прощаюсь с вами, приятной вам сборки обратно разорванных над этим материалом мозгов, до новых встреч ^_______^