Об этом блоге

Об этом блоге

Добра тебе, читатель.
Это запасной аэродром Великого Позитроника.
А основная писанина — в тележеньке.

вторник, 26 февраля 2013 г.

Пилим восьмибитный процессор. Часть вторая: разбор команд и управляемый счётчик.

Итак, у меня есть примерное представление о том, каким должен быть процессор, какие данные он будет получать на вход, какие данные будут у него на выходе, но довольно мало понимания того, как он устроен внутри. Классический чёрный ящик.
Но любой программист знает: для выполнения сложной задачи её нужно разбить на маленькие и решать по порядку. Забегая вперёд скажу: рисование логических схем - это то же программирование, только вместо участков кода используем логические преобразования, вот и всё.
Я начал с памяти. Её, как помнится, 2048 бит - 256 блоков по байту. В Logisim есть встроенный элемент "ОЗУ", в котором настраиваются как разрядность данных, так и разрядность адреса.

Да, пока не забыл: я не стал морочиться, реализуя всю логику на базовых элементах (которых, как вы знаете, если читали предыдущие посты, три - И/ИЛИ/НЕ). Та же ОЗУ (дальше я продолжу называть её проcто памятью, что не совсем верно технически, но зато привычно) состоит из регистров, каждый из которых имеет свой адрес (т.е. у меня 256 регистров). Регистры состоят из триггеров, каждый из которых хранит состояние одного бита (так что у меня - восемь триггеров в регистре). Каждый триггер, в зависимости от типа, можно реализовать различной логикой, например T-триггер реализуется восемью элементами ИЛИ-НЕ. Итого: 16 простейших элементов в триггере, восемь триггеров в регистре, 256 регистров - итого 32768 элементов! И не будем сейчас о том, что сами элементы эти тоже состоят из транзисторов, иначе это число возрастёт ещё на порядок.
В общем - не мучить себя и вас, использовать готовое. В конце концов, когда мы программируем, то не используем только базовые команды, а работаем и с функциями языка - ну так и тут то же самое.

Чтобы исполнить программу, нужно знать адрес в памяти, на котором она начинается. Поскольку до операционных систем и прочих сложностей моему процессору пока далеко, считаем просто: исполнение начинается с первого блока (т.е. блока с адресом 0x0). Теперь нужна собственно программа, хоть самая простенькая, чтобы было на чём тестировать работу. Может показаться, что я забегаю вперёд - ещё ничего нет, а я уже что-то тестировать собрался - но это не так, с пустой памятью тоже не особо потестируешь.
Самая простая команда, пришедшая мне в голову - MOV B,1Fh. Транслировать её в двоичный (а затем и в шестнадцатеричный) код не составляет никакого труда в уме (я не издеваюсь) 00010100 00000001 00011111 -> 0x14 0x1 x1F. С помощью встроенного в программу редактора вношу эти значения в память:


...а потом трах-дибидох: и у меня уже готовая схема контроллера памяти:


Не стану я расписывать, как составлял эту схему, просто дам результат, и покажу, как он работает. Оговорюсь: на самом деле это ещё не контроллер, хотя бы потому, что схема работает только на чтение, и, к тому же, выполняет заодно роль парсера команд. Но я уже привык думать о ней, как о контроллере, и в будущем именно эту задачу схема будет выполнять.
Что сейчас делает контроллер? Он читает память до тех пор, пока не будет считана одна команда (опкод и параметры). При этом контроллер должен учитывать то, сколько байт параметров следует за опкодом (например XOR A,B - два параметра, XOR S,[10h] - один параметр, а команды XOR S,S или NOP не имеют параметров вообще). Считанные данные запоминаются в промежуточный буфер, а после того, как считывание закончится, контроллер отдаёт их в ядро процессора и ждёт от него дальнейших инструкций. Да, ещё нужно не забывать про такие мелочи, как управление навигацией по памяти.

Вход схемы - один контакт, по которому передаются те данные из памяти (с выхода В), на который указывает значение на контакте A (для памяти это входной контакт, для контроллера - один из выходных). При включении схемы адрес в A - 0x0, соответственно выход - 0x14h. Что с этим числом происходит дальше?
Оно попадает в схему Buffer selector (на схеме обозначена как Bs). Эта схема определяет в каком именно из буферов нужно запомнить текущее значение памяти. Буферов в схеме три, поскольку максимальное количество операндов в команде - два (и один буфер на саму команду); если в дальнейшем у меня появятся команды, работающие с большим числом операндов, количество буферов может быть легко расширено.
Вот как выглядит схема Bs внутри:


На вход Operand counter подаются два бита, которыми закодирован номер операнда (откуда берутся эти биты - будет видно дальше); на первом шаге обработки там всегда будет ноль. Считаем, что 0 операнд - это сам опкод, 1 операнд - первый параметр команды и т.д. Схема Bs, как видно, занимается тем, что в зависимости от числа на входе даёт сигнал на тот или иной выход.
Каждый из выходов подключён к соответствующему буферу (буфера реализованы восьмибитными элементами "регистр"). Есть сигнал на входе "включение" - регистр запоминает то, что ему подано на вход D - а туда подведён контакт с данными из памяти.
Можно заметить, что у схемы Bs есть неиспользуемый выход. Он отвечает за ошибку и сигнал на нём возникает, когда на вход схемы подано невозможное число параметров (сейчас это три). Программно такую ошибку воспроизвести не получится, но при проектировании и отладке "железа" подобные ходы попросту необходимы.
Но вернёмся к контроллеру. Что происходит после того, как в буфер команды записалось значение? Очевидно, что нам нужно проверить, есть ли у только что записанной команды параметры, и сколько их.
Этим занимается схема Operand Сounter (на схеме обозначена OpC). Как она это делает - просто. А вот разобраться - чуть сложнее.
Для начала я составил в Excel список всех возможных операндов примерно в таком виде (на самом деле этот список полностью не нужен - достаточно взять один операнд, составить таблицу для него, а коды остальных операндов будут отличаться одним-двумя переставленными битами):


Клик для увеличения

Затем я по этому списку построил схему логических переключателей, которая и находится внутри OpC. Всю схему я приводить не буду - для шести команд там около сотни логических элементов, и это при том, что я пользовался встроенными в Logisim возможностями подключать к одному элементу до восьми входов (если бы этой возможности не было, мне бы пришлось использовать восемь аналогичных элементов, вместо одного). Вот часть участка схемы, определяющая количество параметров для команды NOP (исключительный случай - всегда ноль параметров) и различных вариаций команды MOV:


Клик для увеличения

Пояснение по вон тем загогулинкам, похожим на пружинки - это согласующие резисторы. Их задача - согласовывать значение на контакте, если оно не определено; перед резисторами стоят транзисторы n-типа, которые при закрытом вентиле обрывают цепь (выдавая в неё плавающее, неопределённое значение), при открытом - заданную константу. Поскольку логическое ИЛИ не умеет работать с плавающим значением, нам нужно привести его к нулю - вот тут и нужно согласование.
Безусловно, эту же схему можно реализовать чуть проще - но я пока не стал заморачиваться, она, скорее всего, так или иначе претерпит изменения при доработке процессора и добавлении новых команд.

На выходе из OpC стоит расширитель битов, дополняющий нулями двухбитное значение количества операндов до восьмибитного, например 10 -> 00000010. Зачем он нужен?
Он не нужен. Я поставил его на будущее - когда количество операндов разрастётся, и их перечисление двумя битами уже не зашифруешь.
Ладно, куда дальше идут эти восемь бит? В счётчик, незамысловато названный for:


Может показаться странным, но именно это самая сложная часть всей схемы. Дело в том, что при его создании требовалось решить сразу две проблемы:
1) Логический счётчик в Logisim может считать до жёстко заданного количества тактов (о тактах я скажу чуть ниже). То есть задали ему верхний лимит в 4 - он и будет считать 0,1,2,3,4,0,1,2,3... Требовалось же сделать лимит счёта изменяемым. Первоначальный черновой вариант был "деревянным" - несколько счётчиков с заранее выставленными лимитами, переключение меж которыми происходило при необходимости, однако потом я придумал универсальный счётчик.
2) Счётчик должен работать на полтакта опережая такты считывания из памяти для того, чтобы сразу же сигнализировать о окончании счёта (иначе это может привести к паузе в один такт между окончание заполнения буферов и передачей значений на обработку в ядро процессора.

Как решены эти задачи - видно на схеме. Счётчик qt дополнен регистром en, который хранит поданное ему на вход D значение лимита счёта (поскольку значение на входе всё время обновляется, запоминать его нужно только после окончания предыдущего счёта). После каждого такта значения в регистре и на выходе счётчика сравниваются компаратором, и если значения равные - на выход "Конец цикла" подаётся сигнал, счётчик сбрасывается, а через полтакта в en грузится новое значение лимита. Ну а на выходе "Выход счётчика" - собственно текущее значение счётчика, которое подаётся уже на вход схемы Bs (предварительно от значения отбрасываются шесть старших битов).
Цикл замкнулся, в буферах - команда с параметрами, на выходе "Execution" - единица, а это значит, что ядро CPU может обрабатывать значения.

Да, тактовый генератор и указатель адреса (они в левом нижнем углу схемы). ТГ бесконечно переключает импульсы (1->0->1->0), которые подаются на вход некоторых элементов, и используются ими как сигналы для обновления значений. Например тот же счётчик по сигналу ТГ увеличивает значение, а регистр - запоминает новое значение, поданное на один из входов. Подключив несколько элементов к одному ТГ мы их синхронизируем, вызвав одновременное переключение состояний.
В реальных микросхемах никакая последовательность взаимосвязанных событий не происходит одновременно, всегда есть задержка на распространение сигнала. Но поскольку сигнал распространяется со скоростью света, этой задержкой в большинстве случаев можно пренебречь, считая синхронизацию абсолютной. Исключения есть, но в Logisim, который симулирует логику, но никак не физику, мы с ними вряд ли столкнёмся - в инструкции что-то упоминалось насчёт таких случаев, но, в целом, вывод именно такой.
Участок схемы, помеченный как "Указатель адреса" представляет собой счётчик (он, собственно, считает адреса) и регистр, хранящий текущее значение адреса. Снова возникает вопрос - а зачем регистр, если значение всегда в счётчике? А затем, что меняя значение этого регистра, мы изменим и значение счётчика, а, значит, перейдём на новый адрес памяти. Хотя в текущей схеме этот регистр никак не используется (вообще-то это адресный регистр, который должен находиться в ядре CPU, но поскольку ядра ещё нет, регистр сделан вот так), он обязательно пригодится в будущем.
Ну и напоследок, давайте посмотрим, как эта схема читает память (я немножко изменил подключения для наглядности). Разбираемый набор команд: MOV B,0Fh; MOV S,A3h; MOV [1Ah],10h (потом гифка зацикливается):


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

Комментариев нет: